1 /*****************************************************************************
2  * chapter.c
3  *****************************************************************************
4  * Copyright (C) 2010-2017 L-SMASH project
5  *
6  * Authors: Yusuke Nakamura <muken.the.vfrmaniac@gmail.com>
7  * Contributors: Takashi Hirata <silverfilain@gmail.com>
8  *
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.
12  *
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  *****************************************************************************/
21 
22 /* This file is available under an ISC license. */
23 
24 #include "common/internal.h" /* must be placed first */
25 
26 #include <stdlib.h>
27 #include <string.h>
28 #include <inttypes.h>
29 #include <ctype.h>
30 
31 #include "box.h"
32 
33 #define CHAPTER_BUFSIZE 512
34 #define UTF8_BOM "\xEF\xBB\xBF"
35 #define UTF8_BOM_LENGTH 3
36 
isom_get_start_time(char * chap_time,isom_chapter_entry_t * data)37 static int isom_get_start_time( char *chap_time, isom_chapter_entry_t *data )
38 {
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;
51 }
52 
isom_lumber_line(char * buff,int bufsize,FILE * chapter)53 static int isom_lumber_line( char *buff, int bufsize, FILE *chapter  )
54 {
55     char *tail;
56     /* remove newline codes and skip empty line */
57     do
58     {
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;
66 }
67 
isom_read_simple_chapter(FILE * chapter,isom_chapter_entry_t * data)68 static int isom_read_simple_chapter( FILE *chapter, isom_chapter_entry_t *data )
69 {
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;
91 }
92 
isom_read_minimum_chapter(FILE * chapter,isom_chapter_entry_t * data)93 static int isom_read_minimum_chapter( FILE *chapter, isom_chapter_entry_t *data )
94 {
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;
112 }
113 
114 typedef int (*fn_get_chapter_data)( FILE *, isom_chapter_entry_t * );
115 
isom_check_chap_line(char * file_name)116 static fn_get_chapter_data isom_check_chap_line( char *file_name )
117 {
118     FILE *fp = lsmash_fopen( file_name, "rb" );
119     if( !fp )
120     {
121         lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to open the chapter file \"%s\".\n", file_name );
122         return NULL;
123     }
124     char buff[CHAPTER_BUFSIZE];
125     fn_get_chapter_data fnc = NULL;
126     if( fgets( buff, CHAPTER_BUFSIZE, fp ) != NULL )
127     {
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" );
136     }
137     fclose( fp );
138     return fnc;
139 }
140 
isom_add_chpl_entry(isom_chpl_t * chpl,isom_chapter_entry_t * chap_data)141 static int isom_add_chpl_entry( isom_chpl_t *chpl, isom_chapter_entry_t *chap_data )
142 {
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) );
147     if( !data )
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 )
153     {
154         lsmash_free( data );
155         return LSMASH_ERR_MEMORY_ALLOC;
156     }
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 )
160     {
161         lsmash_free( data->chapter_name );
162         lsmash_free( data );
163         return LSMASH_ERR_MEMORY_ALLOC;
164     }
165     return 0;
166 }
167 
lsmash_set_tyrant_chapter(lsmash_root_t * root,char * file_name,int add_bom)168 int lsmash_set_tyrant_chapter( lsmash_root_t *root, char *file_name, int add_bom )
169 {
170     if( isom_check_initializer_present( root ) < 0 )
171         goto error_message;
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 )
177         goto error_message;
178     /* check each line format */
179     fn_get_chapter_data fnc = isom_check_chap_line( file_name );
180     if( !fnc )
181         goto error_message;
182     FILE *chapter = lsmash_fopen( file_name, "rb" );
183     if( !chapter )
184     {
185         lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to open the chapter file \"%s\".\n", file_name );
186         goto error_message;
187     }
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 ) )) )
190         goto fail;
191     file->moov->udta->chpl->version = 1;    /* version = 1 is popular. */
192     isom_chapter_entry_t data = { 0 };
193     while( !fnc( chapter, &data ) )
194     {
195         if( add_bom )
196         {
197             char *chapter_name_with_bom = (char *)lsmash_malloc( strlen( data.chapter_name ) + 1 + UTF8_BOM_LENGTH );
198             if( !chapter_name_with_bom )
199                 goto fail2;
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;
203         }
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 )
206         {
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 );
211             break;
212         }
213         if( isom_add_chpl_entry( file->moov->udta->chpl, &data ) < 0 )
214             goto fail2;
215         lsmash_freep( &data.chapter_name );
216     }
217     fclose( chapter );
218     return 0;
219 fail2:
220     lsmash_free( data.chapter_name );
221 fail:
222     fclose( chapter );
223 error_message:
224     lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to set chapter list.\n" );
225     return LSMASH_ERR_NAMELESS;
226 }
227 
lsmash_create_reference_chapter_track(lsmash_root_t * root,uint32_t track_ID,char * file_name)228 int lsmash_create_reference_chapter_track( lsmash_root_t *root, uint32_t track_ID, char *file_name )
229 {
230     if( isom_check_initializer_present( root ) < 0 )
231         goto error_message;
232     lsmash_file_t *file = root->file;
233     if( LSMASH_IS_NON_EXISTING_BOX( file->moov->mvhd ) )
234         goto error_message;
235     if( file->forbid_tref || (!file->qt_compatible && !file->itunes_movie) )
236     {
237         lsmash_log( NULL, LSMASH_LOG_ERROR, "reference chapter is not available for this file.\n" );
238         goto error_message;
239     }
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 ) )
244     {
245         lsmash_log( NULL, LSMASH_LOG_ERROR, "the specified track ID to apply the chapter doesn't exist.\n" );
246         goto error_message;
247     }
248     if( LSMASH_IS_NON_EXISTING_BOX( trak->tref ) && LSMASH_IS_BOX_ADDITION_FAILURE( isom_add_tref( trak ) ) )
249         goto error_message;
250     /* Create a track_ID for a new chapter track. */
251     uint32_t *id = (uint32_t *)lsmash_malloc( sizeof(uint32_t) );
252     if( !id )
253         goto error_message;
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 ) )
258     {
259         lsmash_free( id );
260         goto error_message;
261     }
262     chap->ref_count = 1;
263     chap->track_ID  = id;
264     /* Create a reference chapter track. */
265     if( chapter_track_ID != lsmash_create_track( root, ISOM_MEDIA_HANDLER_TYPE_TEXT_TRACK ) )
266         goto error_message;
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 )
272         goto fail;
273     /* Set media parameters. */
274     uint64_t media_timescale = lsmash_get_media_timescale( root, track_ID );
275     if( media_timescale == 0 )
276         goto fail;
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 )
283         goto fail;
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 )
291         goto fail;
292     /* Check each line format. */
293     fn_get_chapter_data fnc = isom_check_chap_line( file_name );
294     if( !fnc )
295         goto fail;
296     /* Open chapter format file. */
297     chapter = lsmash_fopen( file_name, "rb" );
298     if( !chapter )
299     {
300         lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to open the chapter file \"%s\".\n", file_name );
301         goto fail;
302     }
303     /* Parse the file and write text samples. */
304     isom_chapter_entry_t data;
305     while( !fnc( chapter, &data ) )
306     {
307         /* set start_time */
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 );
313         if( !sample )
314         {
315             lsmash_free( data.chapter_name );
316             goto fail;
317         }
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 );
321         if( is_qt_text )
322         {
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] =
327                 {
328                     0x00, 0x00, 0x00, 0x0C,     /* size: 12 */
329                     0x65, 0x6E, 0x63, 0x64,     /* type: 'encd' */
330                     0x00, 0x00, 0x01, 0x00      /* Unicode Encoding */
331                 };
332             memcpy( sample->data + 2 + name_length, encd, 12 );
333         }
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 )
339         {
340             lsmash_free( data.chapter_name );
341             goto fail;
342         }
343         lsmash_freep( &data.chapter_name );
344     }
345     if( lsmash_flush_pooled_samples( root, chapter_track_ID, 0 ) < 0 )
346         goto fail;
347     isom_trak_t *chapter_trak = isom_get_trak( file, chapter_track_ID );
348     if( LSMASH_IS_NON_EXISTING_BOX( chapter_trak ) )
349         goto fail;
350     fclose( chapter );
351     chapter_trak->is_chapter       = 1;
352     chapter_trak->related_track_ID = track_ID;
353     return 0;
354 fail:
355     if( chapter )
356         fclose( chapter );
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 );
365 error_message:
366     lsmash_log( NULL, LSMASH_LOG_ERROR, "failed to set reference chapter.\n" );
367     return LSMASH_ERR_NAMELESS;
368 }
369 
lsmash_count_tyrant_chapter(lsmash_root_t * root)370 uint32_t lsmash_count_tyrant_chapter( lsmash_root_t *root )
371 {
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;
375     return 0;
376 }
377 
lsmash_get_tyrant_chapter(lsmash_root_t * root,uint32_t index,double * timestamp)378 char *lsmash_get_tyrant_chapter( lsmash_root_t *root, uint32_t index, double *timestamp )
379 {
380     if( isom_check_initializer_present( root ) < 0 )
381         return NULL;
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 ) )
385         return NULL;
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 );
388     if( !data )
389         return NULL;
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;
395 }
396 
397 
lsmash_print_chapter_list(lsmash_root_t * root)398 int lsmash_print_chapter_list( lsmash_root_t *root )
399 {
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 ) )
405     {
406         isom_chpl_t *chpl = file->moov->udta->chpl;
407         uint32_t timescale;
408         if( chpl->version == 0 )
409         {
410             if( LSMASH_IS_NON_EXISTING_BOX( file->moov->mvhd ) )
411                 return LSMASH_ERR_NAMELESS;
412             timescale = file->moov->mvhd->timescale;
413         }
414         else
415             timescale = 10000000;
416         uint32_t i = 1;
417         for( lsmash_entry_t *entry = chpl->list->head; entry; entry = entry->next )
418         {
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 */
426             {
427                 data->chapter_name += UTF8_BOM_LENGTH;
428 #ifdef _WIN32
429                 if( i == 1 )
430                     printf( UTF8_BOM );    /* add BOM on Windows */
431 #endif
432             }
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 );
435         }
436         return 0;
437     }
438     lsmash_log( NULL, LSMASH_LOG_ERROR, "this file doesn't have a chapter list.\n" );
439     return LSMASH_ERR_NAMELESS;
440 }
441