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 if( !chap_data
->chapter_name
146 return LSMASH_ERR_FUNCTION_PARAM
;
147 isom_chpl_entry_t
*data
= lsmash_malloc( sizeof(isom_chpl_entry_t
) );
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
)
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
);
164 return LSMASH_ERR_MEMORY_ALLOC
;
169 int lsmash_set_tyrant_chapter( lsmash_root_t
*root
, char *file_name
, int add_bom
)
171 if( isom_check_initializer_present( root
) < 0 )
173 /* This function should be called after updating of the latest movie duration. */
174 lsmash_file_t
*file
= root
->file
;
178 || file
->moov
->mvhd
->timescale
== 0
179 || file
->moov
->mvhd
->duration
== 0 )
181 /* check each line format */
182 fn_get_chapter_data fnc
= isom_check_chap_line( file_name
);
185 FILE *chapter
= lsmash_fopen( file_name
, "rb" );
188 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to open the chapter file \"%s\".\n", file_name
);
191 if( (!file
->moov
->udta
&& !isom_add_udta( file
->moov
))
192 || (!file
->moov
->udta
->chpl
&& !isom_add_chpl( file
->moov
->udta
)) )
194 file
->moov
->udta
->chpl
->version
= 1; /* version = 1 is popular. */
195 isom_chapter_entry_t data
= { 0 };
196 while( !fnc( chapter
, &data
) )
200 char *chapter_name_with_bom
= (char *)lsmash_malloc( strlen( data
.chapter_name
) + 1 + UTF8_BOM_LENGTH
);
201 if( !chapter_name_with_bom
)
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
);
216 if( isom_add_chpl_entry( file
->moov
->udta
->chpl
, &data
) < 0 )
218 lsmash_freep( &data
.chapter_name
);
223 lsmash_free( data
.chapter_name
);
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 )
235 lsmash_file_t
*file
= root
->file
;
238 || !file
->moov
->mvhd
)
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" );
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
);
250 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "the specified track ID to apply the chapter doesn't exist.\n" );
253 if( !trak
->tref
&& !isom_add_tref( trak
) )
255 /* Create a track_ID for a new chapter track. */
256 uint32_t *id
= (uint32_t *)lsmash_malloc( sizeof(uint32_t) );
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
);
269 /* Create a reference chapter track. */
270 if( chapter_track_ID
!= lsmash_create_track( root
, ISOM_MEDIA_HANDLER_TYPE_TEXT_TRACK
) )
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 )
278 /* Set media parameters. */
279 uint64_t media_timescale
= lsmash_get_media_timescale( root
, track_ID
);
280 if( !media_timescale
)
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 )
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
);
297 /* Check each line format. */
298 fn_get_chapter_data fnc
= isom_check_chap_line( file_name
);
301 /* Open chapter format file. */
302 chapter
= lsmash_fopen( file_name
, "rb" );
305 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "failed to open the chapter file \"%s\".\n", file_name
);
308 /* Parse the file and write text samples. */
309 isom_chapter_entry_t data
;
310 while( !fnc( chapter
, &data
) )
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
);
320 lsmash_free( data
.chapter_name
);
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
);
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
);
348 lsmash_freep( &data
.chapter_name
);
350 if( lsmash_flush_pooled_samples( root
, chapter_track_ID
, 0 ) < 0 )
352 isom_trak_t
*chapter_trak
= isom_get_trak( file
, chapter_track_ID
);
356 chapter_trak
->is_chapter
= 1;
357 chapter_trak
->related_track_ID
= track_ID
;
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
);
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
;
386 char *lsmash_get_tyrant_chapter( lsmash_root_t
*root
, uint32_t index
, double *timestamp
)
388 if( isom_check_initializer_present( root
) < 0 )
390 lsmash_file_t
*file
= root
->file
->initializer
;
394 || !file
->moov
->udta
->chpl
)
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
);
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
;
416 && file
->moov
->udta
->chpl
)
418 isom_chpl_t
*chpl
= file
->moov
->udta
->chpl
;
423 && !file
->moov
->mvhd
)
424 return LSMASH_ERR_NAMELESS
;
425 timescale
= file
->moov
->mvhd
->timescale
;
428 timescale
= 10000000;
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
;
443 printf( UTF8_BOM
); /* add BOM on Windows */
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
);
451 lsmash_log( NULL
, LSMASH_LOG_ERROR
, "this file doesn't have a chapter list.\n" );
452 return LSMASH_ERR_NAMELESS
;