1 /*****************************************************************************
3 *****************************************************************************
4 * Copyright (C) 2010-2017 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 )
42 return LSMASH_ERR_INVALID_DATA
;
47 return LSMASH_ERR_INVALID_DATA
;
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
)
60 return LSMASH_ERR_NAMELESS
;
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 )
73 return LSMASH_ERR_NAMELESS
;
74 char *chapter_time
= strchr( buff
, '=' ); /* find separator */
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 */
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';
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';
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" );
121 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to open the chapter file \"%s\".\n", file_name
);
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
;
135 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "the chapter file is malformed.\n" );
141 static int isom_add_chpl_entry( isom_chpl_t
*chpl
, isom_chapter_entry_t
*chap_data
)
143 assert( LSMASH_IS_EXISTING_BOX( chpl
) );
144 if( !chap_data
->chapter_name
|| !chpl
->list
)
145 return LSMASH_ERR_FUNCTION_PARAM
;
146 isom_chpl_entry_t
*data
= lsmash_malloc( sizeof(isom_chpl_entry_t
) );
148 return LSMASH_ERR_MEMORY_ALLOC
;
149 data
->start_time
= chap_data
->start_time
;
150 data
->chapter_name_length
= strlen( chap_data
->chapter_name
);
151 data
->chapter_name
= (char *)lsmash_malloc( data
->chapter_name_length
+ 1 );
152 if( !data
->chapter_name
)
155 return LSMASH_ERR_MEMORY_ALLOC
;
157 memcpy( data
->chapter_name
, chap_data
->chapter_name
, data
->chapter_name_length
);
158 data
->chapter_name
[ data
->chapter_name_length
] = '\0';
159 if( lsmash_list_add_entry( chpl
->list
, data
) < 0 )
161 lsmash_free( data
->chapter_name
);
163 return LSMASH_ERR_MEMORY_ALLOC
;
168 int lsmash_set_tyrant_chapter( lsmash_root_t
*root
, char *file_name
, int add_bom
)
170 if( isom_check_initializer_present( root
) < 0 )
172 /* This function should be called after updating of the latest movie duration. */
173 lsmash_file_t
*file
= root
->file
;
174 if( LSMASH_IS_NON_EXISTING_BOX( file
->moov
->mvhd
)
175 || file
->moov
->mvhd
->timescale
== 0
176 || file
->moov
->mvhd
->duration
== 0 )
178 /* check each line format */
179 fn_get_chapter_data fnc
= isom_check_chap_line( file_name
);
182 FILE *chapter
= lsmash_fopen( file_name
, "rb" );
185 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to open the chapter file \"%s\".\n", file_name
);
188 if( (LSMASH_IS_NON_EXISTING_BOX( file
->moov
->udta
) && LSMASH_IS_BOX_ADDITION_FAILURE( isom_add_udta( file
->moov
) ))
189 || (LSMASH_IS_NON_EXISTING_BOX( file
->moov
->udta
->chpl
) && LSMASH_IS_BOX_ADDITION_FAILURE( isom_add_chpl( file
->moov
->udta
) )) )
191 file
->moov
->udta
->chpl
->version
= 1; /* version = 1 is popular. */
192 isom_chapter_entry_t data
= { 0 };
193 while( !fnc( chapter
, &data
) )
197 char *chapter_name_with_bom
= (char *)lsmash_malloc( strlen( data
.chapter_name
) + 1 + UTF8_BOM_LENGTH
);
198 if( !chapter_name_with_bom
)
200 sprintf( chapter_name_with_bom
, "%s%s", UTF8_BOM
, data
.chapter_name
);
201 lsmash_free( data
.chapter_name
);
202 data
.chapter_name
= chapter_name_with_bom
;
204 data
.start_time
= (data
.start_time
+ 50) / 100; /* convert to 100ns unit */
205 if( data
.start_time
/ 1e7
> (double)file
->moov
->mvhd
->duration
/ file
->moov
->mvhd
->timescale
)
207 lsmash_log( NULL
, LSMASH_LOG_WARNING
,
208 "a chapter point exceeding the actual duration detected."
209 "This chapter point and the following ones (if any) will be cut off.\n" );
210 lsmash_free( data
.chapter_name
);
213 if( isom_add_chpl_entry( file
->moov
->udta
->chpl
, &data
) < 0 )
215 lsmash_freep( &data
.chapter_name
);
220 lsmash_free( data
.chapter_name
);
224 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to set chapter list.\n" );
225 return LSMASH_ERR_NAMELESS
;
228 int lsmash_create_reference_chapter_track( lsmash_root_t
*root
, uint32_t track_ID
, char *file_name
)
230 if( isom_check_initializer_present( root
) < 0 )
232 lsmash_file_t
*file
= root
->file
;
233 if( LSMASH_IS_NON_EXISTING_BOX( file
->moov
->mvhd
) )
235 if( file
->forbid_tref
|| (!file
->qt_compatible
&& !file
->itunes_movie
) )
237 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "reference chapter is not available for this file.\n" );
240 FILE *chapter
= NULL
; /* shut up 'uninitialized' warning */
241 /* Create a Track Reference Box. */
242 isom_trak_t
*trak
= isom_get_trak( file
, track_ID
);
243 if( LSMASH_IS_NON_EXISTING_BOX( trak
) )
245 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "the specified track ID to apply the chapter doesn't exist.\n" );
248 if( LSMASH_IS_NON_EXISTING_BOX( trak
->tref
) && LSMASH_IS_BOX_ADDITION_FAILURE( isom_add_tref( trak
) ) )
250 /* Create a track_ID for a new chapter track. */
251 uint32_t *id
= (uint32_t *)lsmash_malloc( sizeof(uint32_t) );
254 uint32_t chapter_track_ID
= *id
= file
->moov
->mvhd
->next_track_ID
;
255 /* Create a Track Reference Type Box. */
256 isom_tref_type_t
*chap
= isom_add_track_reference_type( trak
->tref
, QT_TREF_TYPE_CHAP
);
257 if( LSMASH_IS_NON_EXISTING_BOX( chap
) )
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
== 0 )
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
, .data_ref_index
= 1 };
289 uint32_t sample_entry
= lsmash_add_sample_entry( root
, chapter_track_ID
, &summary
);
290 if( sample_entry
== 0 )
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
);
348 if( LSMASH_IS_NON_EXISTING_BOX( chapter_trak
) )
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" );
367 return LSMASH_ERR_NAMELESS
;
370 uint32_t lsmash_count_tyrant_chapter( lsmash_root_t
*root
)
372 if( isom_check_initializer_present( root
) < 0
373 && root
->file
->initializer
->moov
->udta
->chpl
->list
)
374 return root
->file
->initializer
->moov
->udta
->chpl
->list
->entry_count
;
378 char *lsmash_get_tyrant_chapter( lsmash_root_t
*root
, uint32_t index
, double *timestamp
)
380 if( isom_check_initializer_present( root
) < 0 )
382 lsmash_file_t
*file
= root
->file
->initializer
;
383 if( LSMASH_IS_NON_EXISTING_BOX( file
->moov
->mvhd
)
384 || LSMASH_IS_NON_EXISTING_BOX( file
->moov
->udta
->chpl
) )
386 isom_chpl_t
*chpl
= file
->moov
->udta
->chpl
;
387 isom_chpl_entry_t
*data
= (isom_chpl_entry_t
*)lsmash_list_get_entry_data( chpl
->list
, index
);
390 double timescale
= chpl
->version
? 10000000.0 : file
->moov
->mvhd
->timescale
;
391 *timestamp
= data
->start_time
/ timescale
;
392 if( !memcmp( data
->chapter_name
, UTF8_BOM
, UTF8_BOM_LENGTH
) )
393 return data
->chapter_name
+ UTF8_BOM_LENGTH
;
394 return data
->chapter_name
;
398 int lsmash_print_chapter_list( lsmash_root_t
*root
)
400 if( isom_check_initializer_present( root
) < 0
401 || !(root
->file
->initializer
->flags
& LSMASH_FILE_MODE_READ
) )
402 return LSMASH_ERR_FUNCTION_PARAM
;
403 lsmash_file_t
*file
= root
->file
->initializer
;
404 if( LSMASH_IS_EXISTING_BOX( file
->moov
->udta
->chpl
) )
406 isom_chpl_t
*chpl
= file
->moov
->udta
->chpl
;
408 if( chpl
->version
== 0 )
410 if( LSMASH_IS_NON_EXISTING_BOX( file
->moov
->mvhd
) )
411 return LSMASH_ERR_NAMELESS
;
412 timescale
= file
->moov
->mvhd
->timescale
;
415 timescale
= 10000000;
417 for( lsmash_entry_t
*entry
= chpl
->list
->head
; entry
; entry
= entry
->next
)
419 isom_chpl_entry_t
*data
= (isom_chpl_entry_t
*)entry
->data
;
420 int64_t start_time
= data
->start_time
/ timescale
;
421 int hh
= start_time
/ 3600;
422 int mm
= (start_time
/ 60) % 60;
423 int ss
= start_time
% 60;
424 int ms
= ((data
->start_time
/ (double)timescale
) - hh
* 3600 - mm
* 60 - ss
) * 1e3
+ 0.5;
425 if( !memcmp( data
->chapter_name
, UTF8_BOM
, UTF8_BOM_LENGTH
) ) /* detect BOM */
427 data
->chapter_name
+= UTF8_BOM_LENGTH
;
430 printf( UTF8_BOM
); /* add BOM on Windows */
433 printf( "CHAPTER%02"PRIu32
"=%02d:%02d:%02d.%03d\n", i
, hh
, mm
, ss
, ms
);
434 printf( "CHAPTER%02"PRIu32
"NAME=%s\n", i
++, data
->chapter_name
);
438 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "this file doesn't have a chapter list.\n" );
439 return LSMASH_ERR_NAMELESS
;