1 /*****************************************************************************
3 *****************************************************************************
4 * Copyright (C) 2010-2014 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 */
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
)
41 if( sscanf( chap_time
, "%"SCNu64
":%2"SCNu64
":%lf", &hh
, &mm
, &ss
) != 3 )
49 data
->start_time
= (hh
* 3600 + mm
* 60 + ss
) * 1e9
;
53 static int isom_lumber_line( char *buff
, int bufsize
, FILE *chapter
)
56 /* remove newline codes and skip empty line */
59 if( fgets( buff
, bufsize
, chapter
) == NULL
)
61 tail
= &buff
[ strlen( buff
) - 1 ];
62 while( tail
>= buff
&& (*tail
== '\n' || *tail
== '\r') )
64 } while( tail
< buff
);
68 static int isom_read_simple_chapter( FILE *chapter
, isom_chapter_entry_t
*data
)
70 char buff
[CHAPTER_BUFSIZE
];
72 if( isom_lumber_line( buff
, CHAPTER_BUFSIZE
, chapter
) < 0 )
74 char *chapter_time
= strchr( buff
, '=' ); /* find separator */
76 || isom_get_start_time( chapter_time
, data
) < 0
77 || isom_lumber_line( buff
, CHAPTER_BUFSIZE
, chapter
) < 0 ) /* get chapter_name */
79 char *chapter_name
= strchr( buff
, '=' ); /* find separator */
82 int len
= LSMASH_MIN( 255, strlen( chapter_name
) ); /* We support length of chapter_name up to 255 */
83 data
->chapter_name
= (char *)lsmash_malloc( len
+ 1 );
84 if( !data
->chapter_name
)
86 memcpy( data
->chapter_name
, chapter_name
, len
);
87 data
->chapter_name
[len
] = '\0';
91 static int isom_read_minimum_chapter( FILE *chapter
, isom_chapter_entry_t
*data
)
93 char buff
[CHAPTER_BUFSIZE
];
94 if( isom_lumber_line( buff
, CHAPTER_BUFSIZE
, chapter
) < 0 ) /* read newline */
96 char *p_buff
= &buff
[ !memcmp( buff
, UTF8_BOM
, UTF8_BOM_LENGTH
) ? UTF8_BOM_LENGTH
: 0 ]; /* BOM detection */
97 if( isom_get_start_time( p_buff
, data
) < 0 ) /* get start_time */
99 /* get chapter_name */
100 char *chapter_name
= strchr( buff
, ' ' ); /* find separator */
101 if( !chapter_name
++ )
103 int len
= LSMASH_MIN( 255, strlen( chapter_name
) ); /* We support length of chapter_name up to 255 */
104 data
->chapter_name
= (char *)lsmash_malloc( len
+ 1 );
105 if( !data
->chapter_name
)
107 memcpy( data
->chapter_name
, chapter_name
, len
);
108 data
->chapter_name
[len
] = '\0';
112 typedef int (*fn_get_chapter_data
)( FILE *, isom_chapter_entry_t
* );
114 static fn_get_chapter_data
isom_check_chap_line( char *file_name
)
116 FILE *fp
= lsmash_fopen( file_name
, "rb" );
119 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to open the chapter file \"%s\".\n", file_name
);
122 char buff
[CHAPTER_BUFSIZE
];
123 fn_get_chapter_data fnc
= NULL
;
124 if( fgets( buff
, CHAPTER_BUFSIZE
, fp
) != NULL
)
126 char *p_buff
= &buff
[ !memcmp( buff
, UTF8_BOM
, UTF8_BOM_LENGTH
) ? UTF8_BOM_LENGTH
: 0 ]; /* BOM detection */
127 if( !strncmp( p_buff
, "CHAPTER", 7 ) )
128 fnc
= isom_read_simple_chapter
;
129 else if( isdigit( (unsigned char)p_buff
[0] ) && isdigit( (unsigned char)p_buff
[1] ) && p_buff
[2] == ':'
130 && isdigit( (unsigned char)p_buff
[3] ) && isdigit( (unsigned char)p_buff
[4] ) && p_buff
[5] == ':' )
131 fnc
= isom_read_minimum_chapter
;
133 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "the chapter file is malformed.\n" );
139 static int isom_add_chpl_entry( isom_chpl_t
*chpl
, isom_chapter_entry_t
*chap_data
)
141 if( !chap_data
->chapter_name
145 isom_chpl_entry_t
*data
= lsmash_malloc( sizeof(isom_chpl_entry_t
) );
148 data
->start_time
= chap_data
->start_time
;
149 data
->chapter_name_length
= strlen( chap_data
->chapter_name
);
150 data
->chapter_name
= (char *)lsmash_malloc( data
->chapter_name_length
+ 1 );
151 if( !data
->chapter_name
)
156 memcpy( data
->chapter_name
, chap_data
->chapter_name
, data
->chapter_name_length
);
157 data
->chapter_name
[ data
->chapter_name_length
] = '\0';
158 if( lsmash_add_entry( chpl
->list
, data
) < 0 )
160 lsmash_free( data
->chapter_name
);
167 int lsmash_set_tyrant_chapter( lsmash_root_t
*root
, char *file_name
, int add_bom
)
169 if( isom_check_initializer_present( root
) < 0 )
171 /* This function should be called after updating of the latest movie duration. */
172 lsmash_file_t
*file
= root
->file
;
176 || file
->moov
->mvhd
->timescale
== 0
177 || file
->moov
->mvhd
->duration
== 0 )
179 /* check each line format */
180 fn_get_chapter_data fnc
= isom_check_chap_line( file_name
);
183 FILE *chapter
= lsmash_fopen( file_name
, "rb" );
186 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to open the chapter file \"%s\".\n", file_name
);
189 if( (!file
->moov
->udta
&& !isom_add_udta( file
->moov
))
190 || (!file
->moov
->udta
->chpl
&& !isom_add_chpl( file
->moov
->udta
)) )
192 file
->moov
->udta
->chpl
->version
= 1; /* version = 1 is popular. */
193 isom_chapter_entry_t data
= { 0 };
194 while( !fnc( chapter
, &data
) )
198 char *chapter_name_with_bom
= (char *)lsmash_malloc( strlen( data
.chapter_name
) + 1 + UTF8_BOM_LENGTH
);
199 if( !chapter_name_with_bom
)
201 sprintf( chapter_name_with_bom
, "%s%s", UTF8_BOM
, data
.chapter_name
);
202 lsmash_free( data
.chapter_name
);
203 data
.chapter_name
= chapter_name_with_bom
;
205 data
.start_time
= (data
.start_time
+ 50) / 100; /* convert to 100ns unit */
206 if( data
.start_time
/ 1e7
> (double)file
->moov
->mvhd
->duration
/ file
->moov
->mvhd
->timescale
)
208 lsmash_log( NULL
, LSMASH_LOG_WARNING
,
209 "a chapter point exceeding the actual duration detected."
210 "This chapter point and the following ones (if any) will be cut off.\n" );
211 lsmash_free( data
.chapter_name
);
214 if( isom_add_chpl_entry( file
->moov
->udta
->chpl
, &data
) < 0 )
216 lsmash_freep( &data
.chapter_name
);
221 lsmash_free( data
.chapter_name
);
225 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to set chapter list.\n" );
229 int lsmash_create_reference_chapter_track( lsmash_root_t
*root
, uint32_t track_ID
, char *file_name
)
231 if( isom_check_initializer_present( root
) < 0 )
233 lsmash_file_t
*file
= root
->file
;
236 || !file
->moov
->mvhd
)
238 if( file
->forbid_tref
|| (!file
->qt_compatible
&& !file
->itunes_movie
) )
240 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "reference chapter is not available for this file.\n" );
243 FILE *chapter
= NULL
; /* shut up 'uninitialized' warning */
244 /* Create a Track Reference Box. */
245 isom_trak_t
*trak
= isom_get_trak( file
, track_ID
);
248 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "the specified track ID to apply the chapter doesn't exist.\n" );
251 if( !trak
->tref
&& !isom_add_tref( trak
) )
253 /* Create a track_ID for a new chapter track. */
254 uint32_t *id
= (uint32_t *)lsmash_malloc( sizeof(uint32_t) );
257 uint32_t chapter_track_ID
= *id
= file
->moov
->mvhd
->next_track_ID
;
258 /* Create a Track Reference Type Box. */
259 isom_tref_type_t
*chap
= isom_add_track_reference_type( trak
->tref
, QT_TREF_TYPE_CHAP
);
261 goto error_message
; /* no need to free id */
264 /* Create a reference chapter track. */
265 if( chapter_track_ID
!= lsmash_create_track( root
, ISOM_MEDIA_HANDLER_TYPE_TEXT_TRACK
) )
267 /* Set track parameters. */
268 lsmash_track_parameters_t track_param
;
269 lsmash_initialize_track_parameters( &track_param
);
270 track_param
.mode
= ISOM_TRACK_IN_MOVIE
| ISOM_TRACK_IN_PREVIEW
;
271 if( lsmash_set_track_parameters( root
, chapter_track_ID
, &track_param
) < 0 )
273 /* Set media parameters. */
274 uint64_t media_timescale
= lsmash_get_media_timescale( root
, track_ID
);
275 if( !media_timescale
)
277 lsmash_media_parameters_t media_param
;
278 lsmash_initialize_media_parameters( &media_param
);
279 media_param
.timescale
= media_timescale
;
280 media_param
.ISO_language
= file
->max_3gpp_version
>= 6 || file
->itunes_movie
? ISOM_LANGUAGE_CODE_UNDEFINED
: 0;
281 media_param
.MAC_language
= 0;
282 if( lsmash_set_media_parameters( root
, chapter_track_ID
, &media_param
) < 0 )
284 /* Create a sample description. */
285 lsmash_codec_type_t sample_type
= file
->max_3gpp_version
>= 6 || file
->itunes_movie
286 ? ISOM_CODEC_TYPE_TX3G_TEXT
287 : QT_CODEC_TYPE_TEXT_TEXT
;
288 lsmash_summary_t summary
= { .sample_type
= sample_type
};
289 uint32_t sample_entry
= lsmash_add_sample_entry( root
, chapter_track_ID
, &summary
);
292 /* Check each line format. */
293 fn_get_chapter_data fnc
= isom_check_chap_line( file_name
);
296 /* Open chapter format file. */
297 chapter
= lsmash_fopen( file_name
, "rb" );
300 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to open the chapter file \"%s\".\n", file_name
);
303 /* Parse the file and write text samples. */
304 isom_chapter_entry_t data
;
305 while( !fnc( chapter
, &data
) )
308 data
.start_time
= data
.start_time
* 1e-9 * media_timescale
+ 0.5;
309 /* write a text sample here */
310 int is_qt_text
= lsmash_check_codec_type_identical( sample_type
, QT_CODEC_TYPE_TEXT_TEXT
);
311 uint16_t name_length
= strlen( data
.chapter_name
);
312 lsmash_sample_t
*sample
= lsmash_create_sample( 2 + name_length
+ 12 * is_qt_text
);
315 lsmash_free( data
.chapter_name
);
318 sample
->data
[0] = (name_length
>> 8) & 0xff;
319 sample
->data
[1] = name_length
& 0xff;
320 memcpy( sample
->data
+ 2, data
.chapter_name
, name_length
);
323 /* QuickTime Player requires Text Encoding Attribute Box ('encd') if media language is ISO language codes : undefined.
324 * Also this box can avoid garbling if the QuickTime text sample is encoded by Unicode characters.
325 * Note: 3GPP Timed Text supports only UTF-8 or UTF-16, so this box isn't needed. */
326 static const uint8_t encd
[12] =
328 0x00, 0x00, 0x00, 0x0C, /* size: 12 */
329 0x65, 0x6E, 0x63, 0x64, /* type: 'encd' */
330 0x00, 0x00, 0x01, 0x00 /* Unicode Encoding */
332 memcpy( sample
->data
+ 2 + name_length
, encd
, 12 );
334 sample
->dts
= data
.start_time
;
335 sample
->cts
= data
.start_time
;
336 sample
->prop
.ra_flags
= ISOM_SAMPLE_RANDOM_ACCESS_FLAG_SYNC
;
337 sample
->index
= sample_entry
;
338 if( lsmash_append_sample( root
, chapter_track_ID
, sample
) < 0 )
340 lsmash_free( data
.chapter_name
);
343 lsmash_freep( &data
.chapter_name
);
345 if( lsmash_flush_pooled_samples( root
, chapter_track_ID
, 0 ) < 0 )
347 isom_trak_t
*chapter_trak
= isom_get_trak( file
, chapter_track_ID
);
351 chapter_trak
->is_chapter
= 1;
352 chapter_trak
->related_track_ID
= track_ID
;
357 /* Remove chapter track reference. */
358 if( trak
->tref
->ref_list
.tail
)
359 isom_remove_box_by_itself( trak
->tref
->ref_list
.tail
->data
);
360 if( trak
->tref
->ref_list
.entry_count
== 0 )
361 isom_remove_box_by_itself( trak
->tref
);
362 /* Remove the reference chapter track attached at tail of the list. */
363 if( file
->moov
->trak_list
.tail
)
364 isom_remove_box_by_itself( file
->moov
->trak_list
.tail
->data
);
366 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to set reference chapter.\n" );
370 uint32_t lsmash_count_tyrant_chapter( lsmash_root_t
*root
)
372 if( isom_check_initializer_present( root
) < 0
373 && root
->file
->initializer
->moov
374 && root
->file
->initializer
->moov
->udta
375 && root
->file
->initializer
->moov
->udta
->chpl
376 && root
->file
->initializer
->moov
->udta
->chpl
->list
)
377 return root
->file
->initializer
->moov
->udta
->chpl
->list
->entry_count
;
381 char *lsmash_get_tyrant_chapter( lsmash_root_t
*root
, uint32_t index
, double *timestamp
)
383 if( isom_check_initializer_present( root
) < 0 )
385 lsmash_file_t
*file
= root
->file
->initializer
;
389 || !file
->moov
->udta
->chpl
)
391 isom_chpl_t
*chpl
= file
->moov
->udta
->chpl
;
392 lsmash_entry_t
*entry
= lsmash_get_entry( chpl
->list
, index
);
393 if( !entry
|| !entry
->data
)
395 isom_chpl_entry_t
*data
= (isom_chpl_entry_t
*)entry
->data
;
396 double timescale
= chpl
->version
? 10000000.0 : file
->moov
->mvhd
->timescale
;
397 *timestamp
= data
->start_time
/ timescale
;
398 if( !memcmp( data
->chapter_name
, UTF8_BOM
, UTF8_BOM_LENGTH
) )
399 return data
->chapter_name
+ UTF8_BOM_LENGTH
;
400 return data
->chapter_name
;
404 int lsmash_print_chapter_list( lsmash_root_t
*root
)
406 if( isom_check_initializer_present( root
) < 0
407 || !(root
->file
->initializer
->flags
& LSMASH_FILE_MODE_READ
) )
409 lsmash_file_t
*file
= root
->file
->initializer
;
412 && file
->moov
->udta
->chpl
)
414 isom_chpl_t
*chpl
= file
->moov
->udta
->chpl
;
419 && !file
->moov
->mvhd
)
421 timescale
= file
->moov
->mvhd
->timescale
;
424 timescale
= 10000000;
426 for( lsmash_entry_t
*entry
= chpl
->list
->head
; entry
; entry
= entry
->next
)
428 isom_chpl_entry_t
*data
= (isom_chpl_entry_t
*)entry
->data
;
429 int64_t start_time
= data
->start_time
/ timescale
;
430 int hh
= start_time
/ 3600;
431 int mm
= (start_time
/ 60) % 60;
432 int ss
= start_time
% 60;
433 int ms
= ((data
->start_time
/ (double)timescale
) - hh
* 3600 - mm
* 60 - ss
) * 1e3
+ 0.5;
434 if( !memcmp( data
->chapter_name
, UTF8_BOM
, UTF8_BOM_LENGTH
) ) /* detect BOM */
436 data
->chapter_name
+= UTF8_BOM_LENGTH
;
439 printf( UTF8_BOM
); /* add BOM on Windows */
442 printf( "CHAPTER%02"PRIu32
"=%02d:%02d:%02d.%03d\n", i
, hh
, mm
, ss
, ms
);
443 printf( "CHAPTER%02"PRIu32
"NAME=%s\n", i
++, data
->chapter_name
);
448 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "this file doesn't have a chapter list.\n" );