read: Fix a possible infinite loop of isom_read_skip_box_extra_bytes().
[L-SMASH.git] / core / chapter.c
blobbeb070597686f6790b668c8387ffebe393021f87
1 /*****************************************************************************
2 * chapter.c:
3 *****************************************************************************
4 * Copyright (C) 2010-2015 L-SMASH project
6 * Authors: Yusuke Nakamura <muken.the.vfrmaniac@gmail.com>
7 * Contributors: Takashi Hirata <silverfilain@gmail.com>
9 * Permission to use, copy, modify, and/or distribute this software for any
10 * purpose with or without fee is hereby granted, provided that the above
11 * copyright notice and this permission notice appear in all copies.
13 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
14 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
15 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
16 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
17 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
18 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
19 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
20 *****************************************************************************/
22 /* This file is available under an ISC license. */
24 #include "common/internal.h" /* must be placed first */
26 #include <stdlib.h>
27 #include <string.h>
28 #include <inttypes.h>
29 #include <ctype.h>
31 #include "box.h"
33 #define CHAPTER_BUFSIZE 512
34 #define UTF8_BOM "\xEF\xBB\xBF"
35 #define UTF8_BOM_LENGTH 3
37 static int isom_get_start_time( char *chap_time, isom_chapter_entry_t *data )
39 uint64_t hh, mm;
40 double ss;
41 if( sscanf( chap_time, "%"SCNu64":%2"SCNu64":%lf", &hh, &mm, &ss ) != 3 )
42 return LSMASH_ERR_INVALID_DATA;
43 /* check overflow */
44 if( hh >= 5124095
45 || mm >= 60
46 || ss >= 60 )
47 return LSMASH_ERR_INVALID_DATA;
48 /* 1ns timescale */
49 data->start_time = (hh * 3600 + mm * 60 + ss) * 1e9;
50 return 0;
53 static int isom_lumber_line( char *buff, int bufsize, FILE *chapter )
55 char *tail;
56 /* remove newline codes and skip empty line */
59 if( fgets( buff, bufsize, chapter ) == NULL )
60 return LSMASH_ERR_NAMELESS;
61 tail = &buff[ strlen( buff ) - 1 ];
62 while( tail >= buff && (*tail == '\n' || *tail == '\r') )
63 *tail-- = '\0';
64 } while( tail < buff );
65 return 0;
68 static int isom_read_simple_chapter( FILE *chapter, isom_chapter_entry_t *data )
70 char buff[CHAPTER_BUFSIZE];
71 /* get start_time */
72 if( isom_lumber_line( buff, CHAPTER_BUFSIZE, chapter ) < 0 )
73 return LSMASH_ERR_NAMELESS;
74 char *chapter_time = strchr( buff, '=' ); /* find separator */
75 if( !chapter_time++ )
76 return LSMASH_ERR_INVALID_DATA;
77 if( isom_get_start_time( chapter_time, data ) < 0 )
78 return LSMASH_ERR_INVALID_DATA;
79 if( isom_lumber_line( buff, CHAPTER_BUFSIZE, chapter ) < 0 ) /* get chapter_name */
80 return LSMASH_ERR_NAMELESS;
81 char *chapter_name = strchr( buff, '=' ); /* find separator */
82 if( !chapter_name++ )
83 return LSMASH_ERR_INVALID_DATA;
84 int len = LSMASH_MIN( 255, strlen( chapter_name ) ); /* We support length of chapter_name up to 255 */
85 data->chapter_name = (char *)lsmash_malloc( len + 1 );
86 if( !data->chapter_name )
87 return LSMASH_ERR_MEMORY_ALLOC;
88 memcpy( data->chapter_name, chapter_name, len );
89 data->chapter_name[len] = '\0';
90 return 0;
93 static int isom_read_minimum_chapter( FILE *chapter, isom_chapter_entry_t *data )
95 char buff[CHAPTER_BUFSIZE];
96 if( isom_lumber_line( buff, CHAPTER_BUFSIZE, chapter ) < 0 ) /* read newline */
97 return LSMASH_ERR_NAMELESS;
98 char *p_buff = &buff[ !memcmp( buff, UTF8_BOM, UTF8_BOM_LENGTH ) ? UTF8_BOM_LENGTH : 0 ]; /* BOM detection */
99 if( isom_get_start_time( p_buff, data ) < 0 ) /* get start_time */
100 return LSMASH_ERR_INVALID_DATA;
101 /* get chapter_name */
102 char *chapter_name = strchr( buff, ' ' ); /* find separator */
103 if( !chapter_name++ )
104 return LSMASH_ERR_INVALID_DATA;
105 int len = LSMASH_MIN( 255, strlen( chapter_name ) ); /* We support length of chapter_name up to 255 */
106 data->chapter_name = (char *)lsmash_malloc( len + 1 );
107 if( !data->chapter_name )
108 return LSMASH_ERR_MEMORY_ALLOC;
109 memcpy( data->chapter_name, chapter_name, len );
110 data->chapter_name[len] = '\0';
111 return 0;
114 typedef int (*fn_get_chapter_data)( FILE *, isom_chapter_entry_t * );
116 static fn_get_chapter_data isom_check_chap_line( char *file_name )
118 FILE *fp = lsmash_fopen( file_name, "rb" );
119 if( !fp )
121 lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to open the chapter file \"%s\".\n", file_name );
122 return NULL;
124 char buff[CHAPTER_BUFSIZE];
125 fn_get_chapter_data fnc = NULL;
126 if( fgets( buff, CHAPTER_BUFSIZE, fp ) != NULL )
128 char *p_buff = &buff[ !memcmp( buff, UTF8_BOM, UTF8_BOM_LENGTH ) ? UTF8_BOM_LENGTH : 0 ]; /* BOM detection */
129 if( !strncmp( p_buff, "CHAPTER", 7 ) )
130 fnc = isom_read_simple_chapter;
131 else if( isdigit( (unsigned char)p_buff[0] ) && isdigit( (unsigned char)p_buff[1] ) && p_buff[2] == ':'
132 && isdigit( (unsigned char)p_buff[3] ) && isdigit( (unsigned char)p_buff[4] ) && p_buff[5] == ':' )
133 fnc = isom_read_minimum_chapter;
134 else
135 lsmash_log( NULL, LSMASH_LOG_ERROR, "the chapter file is malformed.\n" );
137 fclose( fp );
138 return fnc;
141 static int isom_add_chpl_entry( isom_chpl_t *chpl, isom_chapter_entry_t *chap_data )
143 if( !chap_data->chapter_name
144 || !chpl
145 || !chpl->list )
146 return LSMASH_ERR_FUNCTION_PARAM;
147 isom_chpl_entry_t *data = lsmash_malloc( sizeof(isom_chpl_entry_t) );
148 if( !data )
149 return LSMASH_ERR_MEMORY_ALLOC;
150 data->start_time = chap_data->start_time;
151 data->chapter_name_length = strlen( chap_data->chapter_name );
152 data->chapter_name = (char *)lsmash_malloc( data->chapter_name_length + 1 );
153 if( !data->chapter_name )
155 lsmash_free( data );
156 return LSMASH_ERR_MEMORY_ALLOC;
158 memcpy( data->chapter_name, chap_data->chapter_name, data->chapter_name_length );
159 data->chapter_name[ data->chapter_name_length ] = '\0';
160 if( lsmash_add_entry( chpl->list, data ) < 0 )
162 lsmash_free( data->chapter_name );
163 lsmash_free( data );
164 return LSMASH_ERR_MEMORY_ALLOC;
166 return 0;
169 int lsmash_set_tyrant_chapter( lsmash_root_t *root, char *file_name, int add_bom )
171 if( isom_check_initializer_present( root ) < 0 )
172 goto error_message;
173 /* This function should be called after updating of the latest movie duration. */
174 lsmash_file_t *file = root->file;
175 if( !file
176 || !file->moov
177 || !file->moov->mvhd
178 || file->moov->mvhd->timescale == 0
179 || file->moov->mvhd->duration == 0 )
180 goto error_message;
181 /* check each line format */
182 fn_get_chapter_data fnc = isom_check_chap_line( file_name );
183 if( !fnc )
184 goto error_message;
185 FILE *chapter = lsmash_fopen( file_name, "rb" );
186 if( !chapter )
188 lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to open the chapter file \"%s\".\n", file_name );
189 goto error_message;
191 if( (!file->moov->udta && !isom_add_udta( file->moov ))
192 || (!file->moov->udta->chpl && !isom_add_chpl( file->moov->udta )) )
193 goto fail;
194 file->moov->udta->chpl->version = 1; /* version = 1 is popular. */
195 isom_chapter_entry_t data = { 0 };
196 while( !fnc( chapter, &data ) )
198 if( add_bom )
200 char *chapter_name_with_bom = (char *)lsmash_malloc( strlen( data.chapter_name ) + 1 + UTF8_BOM_LENGTH );
201 if( !chapter_name_with_bom )
202 goto fail2;
203 sprintf( chapter_name_with_bom, "%s%s", UTF8_BOM, data.chapter_name );
204 lsmash_free( data.chapter_name );
205 data.chapter_name = chapter_name_with_bom;
207 data.start_time = (data.start_time + 50) / 100; /* convert to 100ns unit */
208 if( data.start_time / 1e7 > (double)file->moov->mvhd->duration / file->moov->mvhd->timescale )
210 lsmash_log( NULL, LSMASH_LOG_WARNING,
211 "a chapter point exceeding the actual duration detected."
212 "This chapter point and the following ones (if any) will be cut off.\n" );
213 lsmash_free( data.chapter_name );
214 break;
216 if( isom_add_chpl_entry( file->moov->udta->chpl, &data ) < 0 )
217 goto fail2;
218 lsmash_freep( &data.chapter_name );
220 fclose( chapter );
221 return 0;
222 fail2:
223 lsmash_free( data.chapter_name );
224 fail:
225 fclose( chapter );
226 error_message:
227 lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to set chapter list.\n" );
228 return LSMASH_ERR_NAMELESS;
231 int lsmash_create_reference_chapter_track( lsmash_root_t *root, uint32_t track_ID, char *file_name )
233 if( isom_check_initializer_present( root ) < 0 )
234 goto error_message;
235 lsmash_file_t *file = root->file;
236 if( !file
237 || !file->moov
238 || !file->moov->mvhd )
239 goto error_message;
240 if( file->forbid_tref || (!file->qt_compatible && !file->itunes_movie) )
242 lsmash_log( NULL, LSMASH_LOG_ERROR, "reference chapter is not available for this file.\n" );
243 goto error_message;
245 FILE *chapter = NULL; /* shut up 'uninitialized' warning */
246 /* Create a Track Reference Box. */
247 isom_trak_t *trak = isom_get_trak( file, track_ID );
248 if( !trak )
250 lsmash_log( NULL, LSMASH_LOG_ERROR, "the specified track ID to apply the chapter doesn't exist.\n" );
251 goto error_message;
253 if( !trak->tref && !isom_add_tref( trak ) )
254 goto error_message;
255 /* Create a track_ID for a new chapter track. */
256 uint32_t *id = (uint32_t *)lsmash_malloc( sizeof(uint32_t) );
257 if( !id )
258 goto error_message;
259 uint32_t chapter_track_ID = *id = file->moov->mvhd->next_track_ID;
260 /* Create a Track Reference Type Box. */
261 isom_tref_type_t *chap = isom_add_track_reference_type( trak->tref, QT_TREF_TYPE_CHAP );
262 if( !chap )
264 lsmash_free( id );
265 goto error_message;
267 chap->ref_count = 1;
268 chap->track_ID = id;
269 /* Create a reference chapter track. */
270 if( chapter_track_ID != lsmash_create_track( root, ISOM_MEDIA_HANDLER_TYPE_TEXT_TRACK ) )
271 goto error_message;
272 /* Set track parameters. */
273 lsmash_track_parameters_t track_param;
274 lsmash_initialize_track_parameters( &track_param );
275 track_param.mode = ISOM_TRACK_IN_MOVIE | ISOM_TRACK_IN_PREVIEW;
276 if( lsmash_set_track_parameters( root, chapter_track_ID, &track_param ) < 0 )
277 goto fail;
278 /* Set media parameters. */
279 uint64_t media_timescale = lsmash_get_media_timescale( root, track_ID );
280 if( !media_timescale )
281 goto fail;
282 lsmash_media_parameters_t media_param;
283 lsmash_initialize_media_parameters( &media_param );
284 media_param.timescale = media_timescale;
285 media_param.ISO_language = file->max_3gpp_version >= 6 || file->itunes_movie ? ISOM_LANGUAGE_CODE_UNDEFINED : 0;
286 media_param.MAC_language = 0;
287 if( lsmash_set_media_parameters( root, chapter_track_ID, &media_param ) < 0 )
288 goto fail;
289 /* Create a sample description. */
290 lsmash_codec_type_t sample_type = file->max_3gpp_version >= 6 || file->itunes_movie
291 ? ISOM_CODEC_TYPE_TX3G_TEXT
292 : QT_CODEC_TYPE_TEXT_TEXT;
293 lsmash_summary_t summary = { .sample_type = sample_type, .data_ref_index = 1 };
294 uint32_t sample_entry = lsmash_add_sample_entry( root, chapter_track_ID, &summary );
295 if( !sample_entry )
296 goto fail;
297 /* Check each line format. */
298 fn_get_chapter_data fnc = isom_check_chap_line( file_name );
299 if( !fnc )
300 goto fail;
301 /* Open chapter format file. */
302 chapter = lsmash_fopen( file_name, "rb" );
303 if( !chapter )
305 lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to open the chapter file \"%s\".\n", file_name );
306 goto fail;
308 /* Parse the file and write text samples. */
309 isom_chapter_entry_t data;
310 while( !fnc( chapter, &data ) )
312 /* set start_time */
313 data.start_time = data.start_time * 1e-9 * media_timescale + 0.5;
314 /* write a text sample here */
315 int is_qt_text = lsmash_check_codec_type_identical( sample_type, QT_CODEC_TYPE_TEXT_TEXT );
316 uint16_t name_length = strlen( data.chapter_name );
317 lsmash_sample_t *sample = lsmash_create_sample( 2 + name_length + 12 * is_qt_text );
318 if( !sample )
320 lsmash_free( data.chapter_name );
321 goto fail;
323 sample->data[0] = (name_length >> 8) & 0xff;
324 sample->data[1] = name_length & 0xff;
325 memcpy( sample->data + 2, data.chapter_name, name_length );
326 if( is_qt_text )
328 /* QuickTime Player requires Text Encoding Attribute Box ('encd') if media language is ISO language codes : undefined.
329 * Also this box can avoid garbling if the QuickTime text sample is encoded by Unicode characters.
330 * Note: 3GPP Timed Text supports only UTF-8 or UTF-16, so this box isn't needed. */
331 static const uint8_t encd[12] =
333 0x00, 0x00, 0x00, 0x0C, /* size: 12 */
334 0x65, 0x6E, 0x63, 0x64, /* type: 'encd' */
335 0x00, 0x00, 0x01, 0x00 /* Unicode Encoding */
337 memcpy( sample->data + 2 + name_length, encd, 12 );
339 sample->dts = data.start_time;
340 sample->cts = data.start_time;
341 sample->prop.ra_flags = ISOM_SAMPLE_RANDOM_ACCESS_FLAG_SYNC;
342 sample->index = sample_entry;
343 if( lsmash_append_sample( root, chapter_track_ID, sample ) < 0 )
345 lsmash_free( data.chapter_name );
346 goto fail;
348 lsmash_freep( &data.chapter_name );
350 if( lsmash_flush_pooled_samples( root, chapter_track_ID, 0 ) < 0 )
351 goto fail;
352 isom_trak_t *chapter_trak = isom_get_trak( file, chapter_track_ID );
353 if( !chapter_trak )
354 goto fail;
355 fclose( chapter );
356 chapter_trak->is_chapter = 1;
357 chapter_trak->related_track_ID = track_ID;
358 return 0;
359 fail:
360 if( chapter )
361 fclose( chapter );
362 /* Remove chapter track reference. */
363 if( trak->tref->ref_list.tail )
364 isom_remove_box_by_itself( trak->tref->ref_list.tail->data );
365 if( trak->tref->ref_list.entry_count == 0 )
366 isom_remove_box_by_itself( trak->tref );
367 /* Remove the reference chapter track attached at tail of the list. */
368 if( file->moov->trak_list.tail )
369 isom_remove_box_by_itself( file->moov->trak_list.tail->data );
370 error_message:
371 lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to set reference chapter.\n" );
372 return LSMASH_ERR_NAMELESS;
375 uint32_t lsmash_count_tyrant_chapter( lsmash_root_t *root )
377 if( isom_check_initializer_present( root ) < 0
378 && root->file->initializer->moov
379 && root->file->initializer->moov->udta
380 && root->file->initializer->moov->udta->chpl
381 && root->file->initializer->moov->udta->chpl->list )
382 return root->file->initializer->moov->udta->chpl->list->entry_count;
383 return 0;
386 char *lsmash_get_tyrant_chapter( lsmash_root_t *root, uint32_t index, double *timestamp )
388 if( isom_check_initializer_present( root ) < 0 )
389 return NULL;
390 lsmash_file_t *file = root->file->initializer;
391 if( !file->moov
392 || !file->moov->mvhd
393 || !file->moov->udta
394 || !file->moov->udta->chpl )
395 return NULL;
396 isom_chpl_t *chpl = file->moov->udta->chpl;
397 isom_chpl_entry_t *data = (isom_chpl_entry_t *)lsmash_get_entry_data( chpl->list, index );
398 if( !data )
399 return NULL;
400 double timescale = chpl->version ? 10000000.0 : file->moov->mvhd->timescale;
401 *timestamp = data->start_time / timescale;
402 if( !memcmp( data->chapter_name, UTF8_BOM, UTF8_BOM_LENGTH ) )
403 return data->chapter_name + UTF8_BOM_LENGTH;
404 return data->chapter_name;
408 int lsmash_print_chapter_list( lsmash_root_t *root )
410 if( isom_check_initializer_present( root ) < 0
411 || !(root->file->initializer->flags & LSMASH_FILE_MODE_READ) )
412 return LSMASH_ERR_FUNCTION_PARAM;
413 lsmash_file_t *file = root->file->initializer;
414 if( file->moov
415 && file->moov->udta
416 && file->moov->udta->chpl )
418 isom_chpl_t *chpl = file->moov->udta->chpl;
419 uint32_t timescale;
420 if( !chpl->version )
422 if( !file->moov
423 && !file->moov->mvhd )
424 return LSMASH_ERR_NAMELESS;
425 timescale = file->moov->mvhd->timescale;
427 else
428 timescale = 10000000;
429 uint32_t i = 1;
430 for( lsmash_entry_t *entry = chpl->list->head; entry; entry = entry->next )
432 isom_chpl_entry_t *data = (isom_chpl_entry_t *)entry->data;
433 int64_t start_time = data->start_time / timescale;
434 int hh = start_time / 3600;
435 int mm = (start_time / 60) % 60;
436 int ss = start_time % 60;
437 int ms = ((data->start_time / (double)timescale) - hh * 3600 - mm * 60 - ss) * 1e3 + 0.5;
438 if( !memcmp( data->chapter_name, UTF8_BOM, UTF8_BOM_LENGTH ) ) /* detect BOM */
440 data->chapter_name += UTF8_BOM_LENGTH;
441 #ifdef _WIN32
442 if( i == 1 )
443 printf( UTF8_BOM ); /* add BOM on Windows */
444 #endif
446 printf( "CHAPTER%02"PRIu32"=%02d:%02d:%02d.%03d\n", i, hh, mm, ss, ms );
447 printf( "CHAPTER%02"PRIu32"NAME=%s\n", i++, data->chapter_name );
449 return 0;
451 lsmash_log( NULL, LSMASH_LOG_ERROR, "this file doesn't have a chapter list.\n" );
452 return LSMASH_ERR_NAMELESS;