camogm_mov.c 14.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
/** @file camogm_mov.c
 * @brief Provides writing to file compatible with Apple Quicktime(R) for @e camogm
 * @copyright Copyright (C) 2016 Elphel, Inc.
 *
 * @par <b>License</b>
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
Mikhail Karpenko's avatar
Mikhail Karpenko committed
16
 */
17

18 19 20 21 22
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <errno.h>
23 24 25 26
#include <unistd.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
27 28 29

#include "camogm_mov.h"

30 31
/** @brief Quicktime header length (w/o index tables) enough to accommodate static data */
#define QUICKTIME_MIN_HEADER 0x300
32

33
// for the parser
Mikhail Karpenko's avatar
Mikhail Karpenko committed
34 35
const char hexStr[] = "0123456789abcdef";
const char qtSourceFileName[] = "/etc/qt_source";
36
char comStr[1024];
Mikhail Karpenko's avatar
Mikhail Karpenko committed
37 38 39 40 41 42 43
int width = 1280;
int height = 1024;
int nframes = 100;
int sample_dur = 80;
int samplesPerChunk = 10;
int framesize = 80000;
int timescale = 600;
44
int * sizes;    // array of frame sizes
45
int iPos;       // position in the string "iFile"
Mikhail Karpenko's avatar
Mikhail Karpenko committed
46
int ofd;        // output file descriptor (file opened by the caller)
47
int iFileLen;
Mikhail Karpenko's avatar
Mikhail Karpenko committed
48 49 50 51 52
char * q_template = NULL;
long headerSize = 0;
const char *iFile = NULL;


53
int quicktime_template_parser(camogm_state *state,
54 55 56 57 58 59 60 61 62 63 64 65
		const char * i_iFile,     // now - string containing header template
		int i_ofd,                // output file descriptor (opened)
		int i_width,              // width in pixels
		int i_height,
		int i_nframes,
		int i_sample_dur,
		int i_samplesPerChunk,
		int i_framesize,
		int i_timescale,
		int * i_sizes,
		int data_start            // put zero if the header is written before data (no gap)
);
66 67
void putBigEndian(unsigned long d, int l);
int parse_special(void);
68
int parse(camogm_state *state, int top);
69 70 71 72 73

/**
 * @brief Called when format is changed to MOV (only once) and recording is stopped.
 * Read frame template from the file if it is not done yet.
 */
Mikhail Karpenko's avatar
Mikhail Karpenko committed
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
int camogm_init_mov(void)
{
	FILE* qt_header;
	int size;

	if ((qt_header = fopen(qtSourceFileName, "r")) == NULL) {
		D0(fprintf(debug_file, "Error opening Quicktime header template %s for reading\n", qtSourceFileName));
		return -CAMOGM_FRAME_FILE_ERR;
	}
	fseek(qt_header, 0, SEEK_END);
	size = ftell(qt_header);
	if (!((q_template = malloc(size + 1)))) {
		D0(fprintf(debug_file, "Could not allocate %d bytes of memory for Quicktime header template\n", (size + 1)));
		fclose(qt_header);
		return -CAMOGM_FRAME_MALLOC;
	}
	fseek(qt_header, 0, SEEK_SET); //rewind
	if (fread(q_template, size, 1, qt_header) < 1) {
		D0(fprintf(debug_file, "Could not read %d bytes of Quicktime header template from %s\n", (size + 1), qtSourceFileName));
		free(q_template);
		q_template = NULL;
		fclose(qt_header);
		return -CAMOGM_FRAME_FILE_ERR;
	}
	q_template[size] = 0;
	return 0;
100 101
}

Mikhail Karpenko's avatar
Mikhail Karpenko committed
102 103 104 105 106 107
void camogm_free_mov(void)
{
	if (q_template) {
		free(q_template);
		q_template = NULL;
	}
108 109
}

110 111 112 113 114
/**
 * @brief Start MOV recording
 * @param[in]   state   a pointer to a structure containing current state
 * @return      0 if recording started successfully and negative error code otherwise
 */
115
int camogm_start_mov(camogm_state *state)
Mikhail Karpenko's avatar
Mikhail Karpenko committed
116
{
117

118
	// allocate memory for the frame index table
Mikhail Karpenko's avatar
Mikhail Karpenko committed
119
	if (!((state->frame_lengths = malloc(4 * state->max_frames)))) return -CAMOGM_FRAME_MALLOC;
120
	// open file for writing
121
	sprintf(state->path, "%s%010ld_%06ld.mov", state->path_prefix, state->frame_params[state->port_num].timestamp_sec, state->frame_params[state->port_num].timestamp_usec);
Mikhail Karpenko's avatar
Mikhail Karpenko committed
122 123 124 125
	if (((state->ivf = open(state->path, O_RDWR | O_CREAT, 0777))) < 0) {
		D0(fprintf(debug_file, "Error opening %s for writing, returned %d, errno=%d\n", state->path, state->ivf, errno));
		return -CAMOGM_FRAME_FILE_ERR;
	}
126 127
	// skip header (plus extra)
	// Quicktime (and else?) - frame data start (0xff 0xd8...)
Mikhail Karpenko's avatar
Mikhail Karpenko committed
128 129 130
	state->frame_data_start = QUICKTIME_MIN_HEADER + 16 + 4 * (state->max_frames) + ( 4 * (state->max_frames)) / (state->frames_per_chunk); // 8 bytes for "skip" tag
	lseek(state->ivf, state->frame_data_start, SEEK_SET);
	return 0;
131 132
}

133 134 135 136 137
/**
 * @brief Write a frame to file
 * @param[in]   state   a pointer to a structure containing current state
 * @return      0 if frame was saved successfully and negative error code otherwise
 */
138
int camogm_frame_mov(camogm_state *state)
Mikhail Karpenko's avatar
Mikhail Karpenko committed
139 140 141 142 143 144 145 146 147 148 149 150 151 152
{
	int i, j;
	ssize_t iovlen, l;
	struct iovec chunks_iovec[7];

	l = 0;
	for (i = 0; i < (state->chunk_index) - 1; i++) {
		chunks_iovec[i].iov_base = state->packetchunks[i + 1].chunk;
		chunks_iovec[i].iov_len = state->packetchunks[i + 1].bytes;
		l += chunks_iovec[i].iov_len;
	}
	iovlen = writev(state->ivf, chunks_iovec, (state->chunk_index) - 1);
	if (iovlen < l) {
		j = errno;
Mikhail Karpenko's avatar
Mikhail Karpenko committed
153 154
		D0(fprintf(debug_file, "writev error %d (returned %d, expected %d, file descriptor %d, chn %d)\n", j, iovlen, l, state->ivf, state->port_num));
		perror(strerror(j));
Mikhail Karpenko's avatar
Mikhail Karpenko committed
155 156 157 158 159 160
		close(state->ivf);
		state->ivf = -1;
		return -CAMOGM_FRAME_FILE_ERR;
	}
	state->frame_lengths[state->frameno] = l;
	return 0;
161
}
162 163 164 165 166 167 168

/**
 * @brief Move to the start of the file and insert generated header
 * @param[in]   state   pointer to the #camogm_state structure for current sensor port
 * @return      this function is always successful and returns 0
 */
int camogm_end_mov(camogm_state *state)
Mikhail Karpenko's avatar
Mikhail Karpenko committed
169
{
170
	off_t l;
171
	int port = state->port_num;
172 173 174 175
	timescale = 10000;                                                      // frame period measured in 1/10000 of a second?
	// that was in old code. If that works - try to switch to microseconds
	l = lseek(state->ivf, 0, SEEK_CUR) - (state->frame_data_start) + 8;     // 4-byte length+"mdat"
	// fill in the header in the beginning of the file
Mikhail Karpenko's avatar
Mikhail Karpenko committed
176
	lseek(state->ivf, 0, SEEK_SET);
177
	quicktime_template_parser(state,
178 179 180 181 182 183 184 185 186 187 188 189
			q_template,   // now - string containing header template
			state->ivf,   // output file descriptor (opened)
			state->width, // width in pixels
			state->height,
			state->frameno,
			state->frame_period[port] / (1000000 / timescale),
			state->frames_per_chunk,
			0,                    // frame size - will look in the table
			(int)((float)timescale / (state->timescale)),
			state->frame_lengths, // array of frame lengths to build an index
			state->frame_data_start
	);
190
#if 0
191 192
	// now we need to overwrite last mdat tag in the header to the skip the gap, instead of the length 'mdat
	// put length 'skip length 'mdat
Mikhail Karpenko's avatar
Mikhail Karpenko committed
193
	he = lseek(state->ivf, 0, SEEK_CUR);    // just after the original header end
194
	l = state->frame_data_start - he;       // should be >=
Mikhail Karpenko's avatar
Mikhail Karpenko committed
195 196 197 198 199 200 201 202 203 204 205
	D4(fprintf(debug_file, "Remaining gap between Quicktime header and the data is %d (it should be>=8) \n", (int)l));
	lseek(state->ivf, he - 8, SEEK_SET);
	read(state->ivf, mdat_tag, 8); //!read calculated length+'mdat' tag
	lseek(state->ivf, state->frame_data_start - 8, SEEK_SET);
	write(state->ivf, mdat_tag, 8);
	skip_tag[0] = (l >> 24) & 0xff;
	skip_tag[1] = (l >> 16) & 0xff;
	skip_tag[2] = (l >>  8) & 0xff;
	skip_tag[3] = (l      ) & 0xff;
	lseek(state->ivf, he - 8, SEEK_SET);
	write(state->ivf, skip_tag, 8);
206
#endif
Mikhail Karpenko's avatar
Mikhail Karpenko committed
207 208
	close(state->ivf);
	state->ivf = -1;
209
	// free memory used for index
Mikhail Karpenko's avatar
Mikhail Karpenko committed
210 211 212 213 214
	if (state->frame_lengths) {
		free(state->frame_lengths);
		state->frame_lengths = NULL;
	}
	return 0;
215 216
}

217 218 219 220 221 222
/**
 * @brief Starts with the input file pointer just after the opening "{",
 * and output file - at the beginning of it's output
 * on exit - input pointer - after closing "}", output after it's output
 * @param[in]   d
 * @param[in]   l
223
 * @return      None
Mikhail Karpenko's avatar
Mikhail Karpenko committed
224 225 226 227 228 229 230 231 232 233
 */
void putBigEndian(unsigned long d, int l)
{
	unsigned char od[4];

	od[3] = d;
	od[2] = d >> 8;
	od[1] = d >> 16;
	od[0] = d >> 24;
	if (l) write(ofd, &od[4 - l], l);
234
}
235

236
/** @brief Temporary replacement for fgets to read from string */
Mikhail Karpenko's avatar
Mikhail Karpenko committed
237 238 239 240 241
char * sfgets(char * str, int size, const char * stream, int * pos)
{
	int l;
	const char * eol = strchr(&stream[*pos], '\n');

242
	if (!eol) eol = stream + (strlen(stream) - 1);  // pointer to last before '\0'
Mikhail Karpenko's avatar
Mikhail Karpenko committed
243 244 245 246 247 248
	l = (eol - stream) - (*pos);
	if (l >= size) l = size - 1;
	memcpy(str, &stream[*pos], l);
	str[l] = '\0';
	*pos += l;
	return str;
249 250
}

Mikhail Karpenko's avatar
Mikhail Karpenko committed
251 252 253 254
int parse_special(void)
{
	time_t ltime;
	int n, j, l;
255 256
	char str[256];
	char c;
Mikhail Karpenko's avatar
Mikhail Karpenko committed
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
	int i = 0;
	int gap;

	while (((c = iFile[iPos++]) != 0x20) && (c != 0x09) && (c != 0x0a) && (c != 0x0d) && (c != 0x0) && (i < 255) && ( iPos < iFileLen )) str[i++] = c;
	str[i] = 0;

	D4(fprintf(debug_file, "parse_special, str=!%s\n", str));

	if (strcmp(str, "mdata") == 0) {
		putBigEndian(headerSize, 4); return 0;
	}                                                                    // will put zeroes on pass 1
	if (strcmp(str, "height") == 0) {
		putBigEndian(height, 2); return 0;
	}
	if (strcmp(str, "width") == 0) {
		putBigEndian(width, 2); return 0;
	}
	if (strcmp(str, "width") == 0) {
		putBigEndian(width, 2); return 0;
	}
	if (strcmp(str, "nframes") == 0) {
		putBigEndian(nframes, 4); return 0;
	}
	if (strcmp(str, "timescale") == 0) {
		putBigEndian(timescale, 4); return 0;
282
	}
Mikhail Karpenko's avatar
Mikhail Karpenko committed
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
	if (strcmp(str, "duration") == 0) {
		putBigEndian(nframes * sample_dur, 4); return 0;
	}
	if (strcmp(str, "frame_duration") == 0) {
		putBigEndian(sample_dur, 4); return 0;
	}
	if (strcmp(str, "samples_chunk") == 0) {
		putBigEndian(samplesPerChunk, 4); return 0;
	}                                                                                 // will put zeroes on pass 1
	if (strcmp(str, "sample_sizes") == 0) {
		if (sizes != NULL) for (i = 0; i < nframes; i++) putBigEndian(sizes[i], 4);
		else for (i = 0; i < nframes; i++) putBigEndian(framesize, 4);
		return 0;
	}
	if (strcmp(str, "chunk_offsets") == 0) {
		n = (nframes - 1) / samplesPerChunk + 1;
		putBigEndian(n, 4);
		if (sizes != NULL) {
			l = 0; j = 0;
			for (i = 0; i < nframes; i++) {
				if (j == 0) putBigEndian(headerSize + l, 4);
				j++; if (j >= samplesPerChunk) j = 0;
				l += sizes[i];
			}

		} else for (i = 0; i < n; i++) putBigEndian(headerSize + framesize * samplesPerChunk * i, 4);
		return 0;
310
	}
311
	// a hack - invlude length'skip if data position (header size is known and there is a gap)
Mikhail Karpenko's avatar
Mikhail Karpenko committed
312 313
	if (strcmp(str, "data_size") == 0) {
		gap = headerSize - lseek(ofd, 0, SEEK_CUR) - 8;
314
		if (gap > 0) { //it should be exactly 0 if there is no gap or >8 if there is
Mikhail Karpenko's avatar
Mikhail Karpenko committed
315 316 317 318 319 320 321 322 323
			D4(fprintf(debug_file, "Inserting a skip tag to compensate for a gap (%d bytes) between the header and the frame data\n", gap));
			if (gap < 8) {
				D0(fprintf(debug_file, "not enough room to insret 'skip' tag - %d (need 8)\n", gap));
				return -1;
			}
			D4(fprintf(debug_file, "writing hex %x, %x bytes\n", gap, 4));
			putBigEndian(gap, 4);
			D4(fprintf(debug_file, "writing string <%s>\n", "skip"));
			write(ofd, "skip", 4);
324
			lseek(ofd, gap - 8, SEEK_CUR); // lseek over the gap and proceed as before
Mikhail Karpenko's avatar
Mikhail Karpenko committed
325 326 327 328 329 330 331 332 333 334 335 336 337
		}
		if (sizes != NULL) {
			l = 0;
			for (i = 0; i < nframes; i++) l += sizes[i];
			D4(fprintf(debug_file, "writing hex %x, %x bytes\n", l, 4));
			putBigEndian(l, 4);
		} else putBigEndian(nframes * framesize, 4);
		return 0;
	}
	if (strcmp(str, "time") == 0) {
		time(&ltime);
		ltime += 2082801600;      // 1970->1904// 31,557,600 seconds/year
		putBigEndian(ltime, 4); return 0;
338 339 340 341
	}
	return -1;
}

342
int parse(camogm_state *state, int top)      // if top - will not include length
Mikhail Karpenko's avatar
Mikhail Karpenko committed
343
{
344 345
	long out_start, out_end;
	char c;
Mikhail Karpenko's avatar
Mikhail Karpenko committed
346
	unsigned long d, l;
347
	char * cp;
Mikhail Karpenko's avatar
Mikhail Karpenko committed
348 349 350 351 352

	D4(fprintf(debug_file, "parse(%x)\n", top));
	c = iFile[iPos++];
	D5(fprintf(debug_file, "%c", c));
	out_start = lseek(ofd, 0, SEEK_CUR);
353

Mikhail Karpenko's avatar
Mikhail Karpenko committed
354 355
	if (!top) putBigEndian(0, 4);
	while (( iPos < iFileLen ) && (c != '}')) {
356
		// skip white spaces strchr
Mikhail Karpenko's avatar
Mikhail Karpenko committed
357 358 359 360
		if ((c != ' ') && (c != 0x9) && (c != 0xa) && (c != 0xd)) {
			if (c == '!') {
				if (parse_special() < 0) return -1;
			}
361
			// children atoms
Mikhail Karpenko's avatar
Mikhail Karpenko committed
362
			else if (c == '{') {
363
				if (parse(state, 0) < 0) return -1;
364
				// skip comments
Mikhail Karpenko's avatar
Mikhail Karpenko committed
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384
			} else if (c == '#') sfgets( comStr, sizeof(comStr), iFile, &iPos);
			else if (c == '\'') {
				sfgets( comStr, sizeof(comStr), iFile, &iPos);
				if ((cp = strchr(comStr, 0x0a)) != NULL) cp[0] = 0;
				if ((cp = strchr(comStr, 0x0d)) != NULL) cp[0] = 0;
				if ((cp = strchr(comStr, '#')) != NULL) cp[0] = 0;
				cp = comStr + strlen(comStr) - 1;
				while ((cp > comStr) && ((cp[0] == 0x20) || (cp[0] == 0x09))) cp--;
				cp[1] = 0;
				write(ofd, comStr, strlen(comStr));
				D4(fprintf(debug_file, "writing string <%s>\n", comStr));
			} else if (strchr(hexStr, c)) {
				d = 0;
				l = 1;
				do {
					d = (d << 4) + (strchr(hexStr, c) - hexStr);
					l++;
				} while (( iPos < iFileLen ) && (l <= 8) && (strchr(hexStr, (c = iFile[iPos++]))) );
				l = (l) >> 1;
				putBigEndian(d, l);
385

Mikhail Karpenko's avatar
Mikhail Karpenko committed
386
				D4(fprintf(debug_file, "writing hex %lx, %lx bytes\n", d, l));
387 388


Mikhail Karpenko's avatar
Mikhail Karpenko committed
389 390
			} else if ((c == '}')) {
				break;
391

Mikhail Karpenko's avatar
Mikhail Karpenko committed
392 393 394
			} else {
				return -1;
			}
395

Mikhail Karpenko's avatar
Mikhail Karpenko committed
396 397
		}
		c = iFile[iPos++];
398

Mikhail Karpenko's avatar
Mikhail Karpenko committed
399
	}
400

401
	// fread fseek ftell
402
	if (!top) {
Mikhail Karpenko's avatar
Mikhail Karpenko committed
403 404 405 406 407
		out_end = lseek(ofd, 0, SEEK_CUR);
		lseek(ofd, out_start, SEEK_SET);
		putBigEndian((out_end - out_start), 4);
		lseek(state->ivf, out_end, SEEK_SET);
	}
408 409 410 411
	return 0;
}


412
int quicktime_template_parser( camogm_state *state,
413 414 415 416 417 418 419 420 421 422 423 424
		const char * i_iFile,     // now - string containing header template
		int i_ofd,                // output file descriptor (opened)
		int i_width,              // width in pixels
		int i_height,
		int i_nframes,
		int i_sample_dur,
		int i_samplesPerChunk,
		int i_framesize,
		int i_timescale,
		int * i_sizes,
		int data_start            // zero if dfata is not written yet (will be no gap)
)
Mikhail Karpenko's avatar
Mikhail Karpenko committed
425 426 427 428 429 430 431 432 433 434
{
	iFile =           i_iFile;
	width =           i_width;
	height =          i_height;
	nframes =         i_nframes;
	sample_dur =      i_sample_dur;
	samplesPerChunk = i_samplesPerChunk;
	framesize =       i_framesize;
	timescale =       i_timescale;
	sizes =           i_sizes;
435
	iPos = 0;                     // position in the string "iFile"
Mikhail Karpenko's avatar
Mikhail Karpenko committed
436 437 438 439 440
	ofd =             i_ofd;
	iFileLen =        strlen(iFile);
	lseek(ofd, 0, SEEK_SET);
	if (data_start) headerSize = data_start;
	D3(fprintf(debug_file, "PASS I\n"));
441

442
	while ( iPos < iFileLen ) parse(state, 1);  // pass 1
Mikhail Karpenko's avatar
Mikhail Karpenko committed
443 444 445
	if (!headerSize) headerSize = lseek(ofd, 0, SEEK_CUR);
	iPos = 0;
	lseek(ofd, 0, SEEK_SET);
446

Mikhail Karpenko's avatar
Mikhail Karpenko committed
447
	D3(fprintf(debug_file, "PASS II\n"));
448
	while ( iPos < iFileLen ) parse(state, 1);  // pass 2
449 450 451

	return 0;
}