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
) )
74 char *chapter_time
= strchr( buff
, '=' ); /* find separator */
76 || isom_get_start_time( chapter_time
, data
)
77 || isom_lumber_line( buff
, CHAPTER_BUFSIZE
, chapter
) ) /* 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
) ) /* 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
) ) /* 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
) )
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
) )
216 lsmash_freep( &data
.chapter_name
);
221 if( data
.chapter_name
)
222 lsmash_free( data
.chapter_name
);
226 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to set chapter list.\n" );
230 int lsmash_create_reference_chapter_track( lsmash_root_t
*root
, uint32_t track_ID
, char *file_name
)
232 if( isom_check_initializer_present( root
) < 0 )
234 lsmash_file_t
*file
= root
->file
;
237 || !file
->moov
->mvhd
)
239 if( file
->forbid_tref
|| (!file
->qt_compatible
&& !file
->itunes_movie
) )
241 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "reference chapter is not available for this file.\n" );
244 FILE *chapter
= NULL
; /* shut up 'uninitialized' warning */
245 /* Create a Track Reference Box. */
246 isom_trak_t
*trak
= isom_get_trak( file
, track_ID
);
249 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "the specified track ID to apply the chapter doesn't exist.\n" );
252 if( !trak
->tref
&& !isom_add_tref( trak
) )
254 /* Create a track_ID for a new chapter track. */
255 uint32_t *id
= (uint32_t *)lsmash_malloc( sizeof(uint32_t) );
258 uint32_t chapter_track_ID
= *id
= file
->moov
->mvhd
->next_track_ID
;
259 /* Create a Track Reference Type Box. */
260 isom_tref_type_t
*chap
= isom_add_track_reference_type( trak
->tref
, QT_TREF_TYPE_CHAP
);
262 goto error_message
; /* no need to free id */
265 /* Create a reference chapter track. */
266 if( chapter_track_ID
!= lsmash_create_track( root
, ISOM_MEDIA_HANDLER_TYPE_TEXT_TRACK
) )
268 /* Set track parameters. */
269 lsmash_track_parameters_t track_param
;
270 lsmash_initialize_track_parameters( &track_param
);
271 track_param
.mode
= ISOM_TRACK_IN_MOVIE
| ISOM_TRACK_IN_PREVIEW
;
272 if( lsmash_set_track_parameters( root
, chapter_track_ID
, &track_param
) )
274 /* Set media parameters. */
275 uint64_t media_timescale
= lsmash_get_media_timescale( root
, track_ID
);
276 if( !media_timescale
)
278 lsmash_media_parameters_t media_param
;
279 lsmash_initialize_media_parameters( &media_param
);
280 media_param
.timescale
= media_timescale
;
281 media_param
.ISO_language
= file
->max_3gpp_version
>= 6 || file
->itunes_movie
? ISOM_LANGUAGE_CODE_UNDEFINED
: 0;
282 media_param
.MAC_language
= 0;
283 if( lsmash_set_media_parameters( root
, chapter_track_ID
, &media_param
) )
285 /* Create a sample description. */
286 lsmash_codec_type_t sample_type
= file
->max_3gpp_version
>= 6 || file
->itunes_movie
287 ? ISOM_CODEC_TYPE_TX3G_TEXT
288 : QT_CODEC_TYPE_TEXT_TEXT
;
289 lsmash_summary_t summary
= { .sample_type
= sample_type
};
290 uint32_t sample_entry
= lsmash_add_sample_entry( root
, chapter_track_ID
, &summary
);
293 /* Check each line format. */
294 fn_get_chapter_data fnc
= isom_check_chap_line( file_name
);
297 /* Open chapter format file. */
298 chapter
= lsmash_fopen( file_name
, "rb" );
301 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to open the chapter file \"%s\".\n", file_name
);
304 /* Parse the file and write text samples. */
305 isom_chapter_entry_t data
;
306 while( !fnc( chapter
, &data
) )
309 data
.start_time
= data
.start_time
* 1e-9 * media_timescale
+ 0.5;
310 /* write a text sample here */
311 int is_qt_text
= lsmash_check_codec_type_identical( sample_type
, QT_CODEC_TYPE_TEXT_TEXT
);
312 uint16_t name_length
= strlen( data
.chapter_name
);
313 lsmash_sample_t
*sample
= lsmash_create_sample( 2 + name_length
+ 12 * is_qt_text
);
316 lsmash_free( data
.chapter_name
);
319 sample
->data
[0] = (name_length
>> 8) & 0xff;
320 sample
->data
[1] = name_length
& 0xff;
321 memcpy( sample
->data
+ 2, data
.chapter_name
, name_length
);
324 /* QuickTime Player requires Text Encoding Attribute Box ('encd') if media language is ISO language codes : undefined.
325 * Also this box can avoid garbling if the QuickTime text sample is encoded by Unicode characters.
326 * Note: 3GPP Timed Text supports only UTF-8 or UTF-16, so this box isn't needed. */
327 static const uint8_t encd
[12] =
329 0x00, 0x00, 0x00, 0x0C, /* size: 12 */
330 0x65, 0x6E, 0x63, 0x64, /* type: 'encd' */
331 0x00, 0x00, 0x01, 0x00 /* Unicode Encoding */
333 memcpy( sample
->data
+ 2 + name_length
, encd
, 12 );
335 sample
->dts
= data
.start_time
;
336 sample
->cts
= data
.start_time
;
337 sample
->prop
.ra_flags
= ISOM_SAMPLE_RANDOM_ACCESS_FLAG_SYNC
;
338 sample
->index
= sample_entry
;
339 if( lsmash_append_sample( root
, chapter_track_ID
, sample
) )
341 lsmash_free( data
.chapter_name
);
344 lsmash_freep( &data
.chapter_name
);
346 if( lsmash_flush_pooled_samples( root
, chapter_track_ID
, 0 ) )
348 isom_trak_t
*chapter_trak
= isom_get_trak( file
, chapter_track_ID
);
352 chapter_trak
->is_chapter
= 1;
353 chapter_trak
->related_track_ID
= track_ID
;
358 /* Remove chapter track reference. */
359 if( trak
->tref
->ref_list
.tail
)
360 isom_remove_box_by_itself( trak
->tref
->ref_list
.tail
->data
);
361 if( trak
->tref
->ref_list
.entry_count
== 0 )
362 isom_remove_box_by_itself( trak
->tref
);
363 /* Remove the reference chapter track attached at tail of the list. */
364 if( file
->moov
->trak_list
.tail
)
365 isom_remove_box_by_itself( file
->moov
->trak_list
.tail
->data
);
367 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to set reference chapter.\n" );
371 uint32_t lsmash_count_tyrant_chapter( lsmash_root_t
*root
)
373 if( isom_check_initializer_present( root
) < 0
374 && root
->file
->initializer
->moov
375 && root
->file
->initializer
->moov
->udta
376 && root
->file
->initializer
->moov
->udta
->chpl
377 && root
->file
->initializer
->moov
->udta
->chpl
->list
)
378 return root
->file
->initializer
->moov
->udta
->chpl
->list
->entry_count
;
382 char *lsmash_get_tyrant_chapter( lsmash_root_t
*root
, uint32_t index
, double *timestamp
)
384 if( isom_check_initializer_present( root
) < 0 )
386 lsmash_file_t
*file
= root
->file
->initializer
;
390 || !file
->moov
->udta
->chpl
)
392 isom_chpl_t
*chpl
= file
->moov
->udta
->chpl
;
393 lsmash_entry_t
*entry
= lsmash_get_entry( chpl
->list
, index
);
394 if( !entry
|| !entry
->data
)
396 isom_chpl_entry_t
*data
= (isom_chpl_entry_t
*)entry
->data
;
397 double timescale
= chpl
->version
? 10000000.0 : file
->moov
->mvhd
->timescale
;
398 *timestamp
= data
->start_time
/ timescale
;
399 if( !memcmp( data
->chapter_name
, UTF8_BOM
, UTF8_BOM_LENGTH
) )
400 return data
->chapter_name
+ UTF8_BOM_LENGTH
;
401 return data
->chapter_name
;
405 int lsmash_print_chapter_list( lsmash_root_t
*root
)
407 if( isom_check_initializer_present( root
) < 0
408 || !(root
->file
->initializer
->flags
& LSMASH_FILE_MODE_READ
) )
410 lsmash_file_t
*file
= root
->file
->initializer
;
413 && file
->moov
->udta
->chpl
)
415 isom_chpl_t
*chpl
= file
->moov
->udta
->chpl
;
420 && !file
->moov
->mvhd
)
422 timescale
= file
->moov
->mvhd
->timescale
;
425 timescale
= 10000000;
427 for( lsmash_entry_t
*entry
= chpl
->list
->head
; entry
; entry
= entry
->next
)
429 isom_chpl_entry_t
*data
= (isom_chpl_entry_t
*)entry
->data
;
430 int64_t start_time
= data
->start_time
/ timescale
;
431 int hh
= start_time
/ 3600;
432 int mm
= (start_time
/ 60) % 60;
433 int ss
= start_time
% 60;
434 int ms
= ((data
->start_time
/ (double)timescale
) - hh
* 3600 - mm
* 60 - ss
) * 1e3
+ 0.5;
435 if( !memcmp( data
->chapter_name
, UTF8_BOM
, UTF8_BOM_LENGTH
) ) /* detect BOM */
437 data
->chapter_name
+= UTF8_BOM_LENGTH
;
440 printf( UTF8_BOM
); /* add BOM on Windows */
443 printf( "CHAPTER%02"PRIu32
"=%02d:%02d:%02d.%03d\n", i
, hh
, mm
, ss
, ms
);
444 printf( "CHAPTER%02"PRIu32
"NAME=%s\n", i
++, data
->chapter_name
);
449 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "this file doesn't have a chapter list.\n" );