codec: webvtt: align cue text
[vlc.git] / modules / codec / webvtt / subsvtt.c
blob47138a1f102759a9bfbb74b091c326afa692020c
1 /*****************************************************************************
2 * subsvtt.c: Decoder for WEBVTT as ISO1446-30 payload
3 *****************************************************************************
4 * Copyright (C) 2017 VideoLabs, VLC authors and VideoLAN
6 * This program is free software; you can redistribute it and/or modify it
7 * under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation; either version 2.1 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
19 *****************************************************************************/
21 /*****************************************************************************
22 * Preamble
23 *****************************************************************************/
25 #ifdef HAVE_CONFIG_H
26 # include "config.h"
27 #endif
29 #include <vlc_common.h>
30 #include <vlc_charset.h>
31 #include <vlc_subpicture.h>
32 #include <vlc_codec.h>
33 #include <vlc_stream.h>
34 #include <vlc_memstream.h>
35 #include <assert.h>
37 #include "../codec/substext.h"
38 #include "../demux/mp4/minibox.h"
39 #include "webvtt.h"
41 #include <ctype.h>
43 //#define SUBSVTT_DEBUG
45 /*****************************************************************************
46 * Local prototypes
47 *****************************************************************************/
49 typedef struct webvtt_region_t webvtt_region_t;
50 typedef struct webvtt_dom_node_t webvtt_dom_node_t;
51 typedef struct webvtt_dom_cue_t webvtt_dom_cue_t;
53 #define WEBVTT_REGION_LINES_COUNT 18
54 #define WEBVTT_DEFAULT_LINE_HEIGHT_VH 5.33
55 #define WEBVTT_LINE_TO_HEIGHT_RATIO 1.06
57 enum webvtt_align_e
59 WEBVTT_ALIGN_AUTO,
60 WEBVTT_ALIGN_LEFT,
61 WEBVTT_ALIGN_CENTER,
62 WEBVTT_ALIGN_RIGHT,
63 WEBVTT_ALIGN_START,
64 WEBVTT_ALIGN_END,
67 typedef struct
69 char *psz_region;
70 enum webvtt_align_e vertical;
71 bool b_snap_to_lines;
72 float line;
73 enum webvtt_align_e linealign;
74 float position;
75 enum webvtt_align_e positionalign;
76 float size;
77 enum webvtt_align_e align;
78 } webvtt_cue_settings_t;
80 enum webvtt_node_type_e
82 NODE_TAG,
83 NODE_TEXT,
84 NODE_CUE,
85 NODE_REGION,
86 NODE_VIDEO,
89 #define WEBVTT_NODE_BASE_MEMBERS \
90 enum webvtt_node_type_e type;\
91 webvtt_dom_node_t *p_parent;\
92 webvtt_dom_node_t *p_next;
94 struct webvtt_region_t
96 WEBVTT_NODE_BASE_MEMBERS
97 char *psz_id;
98 float f_width;
99 unsigned i_lines_max_scroll;
100 float anchor_x;
101 float anchor_y;
102 float viewport_anchor_x;
103 float viewport_anchor_y;
104 bool b_scroll_up;
105 webvtt_dom_node_t *p_child;
108 struct webvtt_dom_cue_t
110 WEBVTT_NODE_BASE_MEMBERS
111 char *psz_id;
112 mtime_t i_start;
113 mtime_t i_stop;
114 webvtt_cue_settings_t settings;
115 unsigned i_lines;
116 webvtt_dom_node_t *p_child;
119 typedef struct
121 WEBVTT_NODE_BASE_MEMBERS
122 char *psz_text;
123 } webvtt_dom_text_t;
125 typedef struct
127 WEBVTT_NODE_BASE_MEMBERS
128 mtime_t i_start;
129 char *psz_tag;
130 char *psz_attrs;
131 webvtt_dom_node_t *p_child;
132 } webvtt_dom_tag_t;
134 typedef struct
136 WEBVTT_NODE_BASE_MEMBERS
137 webvtt_dom_node_t *p_child;
138 } webvtt_dom_video_t;
140 struct webvtt_dom_node_t
142 WEBVTT_NODE_BASE_MEMBERS
145 struct decoder_sys_t
147 webvtt_dom_video_t *p_root;
150 #define ATOM_iden VLC_FOURCC('i', 'd', 'e', 'n')
151 #define ATOM_payl VLC_FOURCC('p', 'a', 'y', 'l')
152 #define ATOM_sttg VLC_FOURCC('s', 't', 't', 'g')
153 #define ATOM_vttc VLC_FOURCC('v', 't', 't', 'c')
154 #define ATOM_vtte VLC_FOURCC('v', 't', 't', 'e')
155 #define ATOM_vttx VLC_FOURCC('v', 't', 't', 'x')
157 /*****************************************************************************
159 *****************************************************************************/
161 static bool parse_percent( const char *psz, float *value )
163 char *psz_end;
164 float d = us_strtof( psz, &psz_end );
165 if( d >= 0.0 && d <= 100.0 && *psz_end == '%' )
166 *value = d / 100.0;
167 return psz_end != psz;
170 static bool parse_percent_tuple( const char *psz, float *x, float *y )
172 char *psz_end;
173 float a = us_strtof( psz, &psz_end );
174 if( psz_end != psz &&
175 a >= 0.0 && a <= 100.0 && psz_end && *psz_end == '%' )
177 psz = strchr( psz_end, ',' );
178 if( psz )
180 float b = us_strtof( ++psz, &psz_end );
181 if( psz_end != psz &&
182 b >= 0.0 && b <= 100.0 && psz_end && *psz_end == '%' )
184 *x = a / 100.0;
185 *y = b / 100.0;
186 return true;
190 return false;
193 static void webvtt_cue_settings_ParseTuple( webvtt_cue_settings_t *p_settings,
194 const char *psz_key, const char *psz_value )
196 if( !strcmp( psz_key, "vertical" ) )
198 if( !strcmp( psz_value, "rl" ) )
199 p_settings->vertical = WEBVTT_ALIGN_RIGHT;
200 else if( !strcmp( psz_value, "lr" ) )
201 p_settings->vertical = WEBVTT_ALIGN_LEFT;
202 else
203 p_settings->vertical = WEBVTT_ALIGN_AUTO;
205 else if( !strcmp( psz_key, "line" ) )
207 if( strchr( psz_value, '%' ) )
208 parse_percent( psz_value, &p_settings->line );
209 // else /* todo */
210 const char *psz_align = strchr( psz_value, ',' );
211 if( psz_align++ )
213 if( !strcmp( psz_align, "center" ) )
214 p_settings->linealign = WEBVTT_ALIGN_CENTER;
215 else if( !strcmp( psz_align, "end" ) )
216 p_settings->linealign = WEBVTT_ALIGN_END;
217 else
218 p_settings->linealign = WEBVTT_ALIGN_START;
221 else if( !strcmp( psz_key, "position" ) )
223 parse_percent( psz_value, &p_settings->position );
224 const char *psz_align = strchr( psz_value, ',' );
225 if( psz_align++ )
227 if( !strcmp( psz_align, "line-left" ) )
228 p_settings->linealign = WEBVTT_ALIGN_LEFT;
229 else if( !strcmp( psz_align, "line-right" ) )
230 p_settings->linealign = WEBVTT_ALIGN_RIGHT;
231 else if( !strcmp( psz_align, "center" ) )
232 p_settings->linealign = WEBVTT_ALIGN_CENTER;
233 else
234 p_settings->linealign = WEBVTT_ALIGN_AUTO;
237 else if( !strcmp( psz_key, "size" ) )
239 parse_percent( psz_value, &p_settings->size );
241 else if( !strcmp( psz_key, "region" ) )
243 free( p_settings->psz_region );
244 p_settings->psz_region = strdup( psz_value );
246 else if( !strcmp( psz_key, "align" ) )
248 if( !strcmp( psz_value, "start" ) )
249 p_settings->align = WEBVTT_ALIGN_START;
250 else if( !strcmp( psz_value, "end" ) )
251 p_settings->align = WEBVTT_ALIGN_END;
252 else if( !strcmp( psz_value, "left" ) )
253 p_settings->align = WEBVTT_ALIGN_LEFT;
254 else if( !strcmp( psz_value, "right" ) )
255 p_settings->align = WEBVTT_ALIGN_RIGHT;
256 else
257 p_settings->align = WEBVTT_ALIGN_CENTER;
261 static void webvtt_cue_settings_Parse( webvtt_cue_settings_t *p_settings,
262 char *p_str )
264 char *p_save;
265 char *psz_tuple;
268 psz_tuple = strtok_r( p_str, " ", &p_save );
269 p_str = NULL;
270 if( psz_tuple )
272 const char *psz_split = strchr( psz_tuple, ':' );
273 if( psz_split && psz_split[1] != 0 && psz_split != psz_tuple )
275 char *psz_key = strndup( psz_tuple, psz_split - psz_tuple );
276 if( psz_key )
278 webvtt_cue_settings_ParseTuple( p_settings, psz_key, psz_split + 1 );
279 free( psz_key );
283 } while( psz_tuple );
286 static void webvtt_cue_settings_Clean( webvtt_cue_settings_t *p_settings )
288 free( p_settings->psz_region );
291 static void webvtt_cue_settings_Init( webvtt_cue_settings_t *p_settings )
293 p_settings->psz_region = NULL;
294 p_settings->vertical = WEBVTT_ALIGN_AUTO;
295 p_settings->b_snap_to_lines = true;
296 p_settings->line = -1;
297 p_settings->linealign = WEBVTT_ALIGN_START;
298 p_settings->position = -1;
299 p_settings->positionalign = WEBVTT_ALIGN_AUTO;
300 p_settings->size = 1.0; /* 100% */
301 p_settings->align = WEBVTT_ALIGN_CENTER;
304 /*****************************************************************************
306 *****************************************************************************/
307 #ifdef SUBSVTT_DEBUG
308 static void webvtt_domnode_Debug( webvtt_dom_node_t *p_node, int i_depth )
310 for( ; p_node ; p_node = p_node->p_next )
312 for( int i=0; i<i_depth; i++) printf(" ");
313 if( p_node->type == NODE_TEXT )
315 printf("TEXT %s\n", ((webvtt_dom_text_t *)p_node)->psz_text );
317 else if( p_node->type == NODE_TAG )
319 webvtt_dom_tag_t *p_tag = (webvtt_dom_tag_t *)p_node;
320 printf("TAG%s (%s)\n", p_tag->psz_tag, p_tag->psz_attrs );
321 webvtt_domnode_Debug( p_tag->p_child, i_depth + 1 );
323 else if( p_node->type == NODE_CUE )
325 webvtt_dom_cue_t *p_cue = (webvtt_dom_cue_t *)p_node;
326 printf("CUE %s\n", p_cue->psz_id );
327 webvtt_domnode_Debug( p_cue->p_child, i_depth + 1 );
329 else if( p_node->type == NODE_REGION )
331 webvtt_region_t *p_region = (webvtt_region_t *)p_node;
332 printf("REGION %s\n", p_region->psz_id );
333 webvtt_domnode_Debug( p_region->p_child, i_depth + 1 );
337 #endif
339 static void webvtt_domnode_ChainDelete( webvtt_dom_node_t *p_node );
340 static void webvtt_dom_cue_Delete( webvtt_dom_cue_t *p_cue );
341 static void webvtt_region_Delete( webvtt_region_t *p_region );
343 static void webvtt_dom_video_Delete( webvtt_dom_video_t *p_node )
345 webvtt_domnode_ChainDelete( p_node->p_child );
346 free( p_node );
349 static void webvtt_dom_text_Delete( webvtt_dom_text_t *p_node )
351 free( p_node->psz_text );
352 free( p_node );
355 static void webvtt_dom_tag_Delete( webvtt_dom_tag_t *p_node )
357 free( p_node->psz_attrs );
358 free( p_node->psz_tag );
359 webvtt_domnode_ChainDelete( p_node->p_child );
360 free( p_node );
363 static void webvtt_domnode_AppendLast( webvtt_dom_node_t **pp_append,
364 webvtt_dom_node_t *p_node )
366 while( *pp_append )
367 pp_append = &((*pp_append)->p_next);
368 *pp_append = p_node;
371 #define webvtt_domnode_AppendLast( a, b ) \
372 webvtt_domnode_AppendLast( (webvtt_dom_node_t **) a, (webvtt_dom_node_t *) b )
374 static void webvtt_domnode_ChainDelete( webvtt_dom_node_t *p_node )
376 while( p_node )
378 webvtt_dom_node_t *p_next = p_node->p_next;
380 if( p_node->type == NODE_TAG )
381 webvtt_dom_tag_Delete( (webvtt_dom_tag_t *) p_node );
382 else if( p_node->type == NODE_TEXT )
383 webvtt_dom_text_Delete( (webvtt_dom_text_t *) p_node );
384 else if( p_node->type == NODE_CUE )
385 webvtt_dom_cue_Delete( (webvtt_dom_cue_t *) p_node );
386 else if( p_node->type == NODE_REGION )
387 webvtt_region_Delete( (webvtt_region_t *) p_node );
388 else if( p_node->type == NODE_VIDEO )
389 webvtt_dom_video_Delete( (webvtt_dom_video_t *) p_node );
391 p_node = p_next;
395 static webvtt_dom_video_t * webvtt_dom_video_New( void )
397 webvtt_dom_video_t *p_node = calloc( 1, sizeof(*p_node) );
398 if( p_node )
399 p_node->type = NODE_VIDEO;
400 return p_node;
403 static webvtt_dom_text_t * webvtt_dom_text_New( webvtt_dom_node_t *p_parent )
405 webvtt_dom_text_t *p_node = calloc( 1, sizeof(*p_node) );
406 if( p_node )
408 p_node->type = NODE_TEXT;
409 p_node->p_parent = p_parent;
411 return p_node;
414 static webvtt_dom_tag_t * webvtt_dom_tag_New( webvtt_dom_node_t *p_parent )
416 webvtt_dom_tag_t *p_node = calloc( 1, sizeof(*p_node) );
417 if( p_node )
419 p_node->i_start = -1;
420 p_node->type = NODE_TAG;
421 p_node->p_parent = p_parent;
423 return p_node;
426 static webvtt_dom_node_t * webvtt_domnode_getParentByTag( webvtt_dom_node_t *p_parent,
427 const char *psz_tag )
429 for( ; p_parent ; p_parent = p_parent->p_parent )
431 if( p_parent->type == NODE_TAG )
433 webvtt_dom_tag_t *p_node = (webvtt_dom_tag_t *) p_parent;
434 if( p_node->psz_tag && psz_tag && !strcmp( p_node->psz_tag, psz_tag ) )
435 break;
438 return p_parent;
441 static const webvtt_dom_node_t * webvtt_domnode_getFirstChild( const webvtt_dom_node_t *p_node )
443 webvtt_dom_node_t *p_child = NULL;
444 switch( p_node->type )
446 case NODE_CUE:
447 p_child = ((webvtt_dom_cue_t *)p_node)->p_child;
448 break;
449 case NODE_REGION:
450 p_child = ((webvtt_region_t *)p_node)->p_child;
451 break;
452 case NODE_TAG:
453 p_child = ((webvtt_dom_tag_t *)p_node)->p_child;
454 break;
455 default:
456 break;
458 return p_child;
461 static inline bool IsEndTag( const char *psz )
463 return psz[1] == '/';
466 /* returns first opening and last chars of next tag, only when valid */
467 static const char * FindNextTag( const char *psz, const char **ppsz_taglast )
469 psz = strchr( psz, '<' );
470 if( psz )
472 *ppsz_taglast = strchr( psz + 1, '>' );
473 if( *ppsz_taglast )
475 const size_t tagsize = *ppsz_taglast - psz + 1;
476 if( tagsize <= 3 )
478 if( tagsize < 2 || IsEndTag(psz) )
479 *ppsz_taglast = psz = NULL;
481 } else psz = NULL;
483 return psz;
486 /* Points to first char of tag name and sets *ppsz_attrs to attributes */
487 static const char *SplitTag( const char *psz_tag, size_t *pi_tag, const char **ppsz_attrs )
489 psz_tag += IsEndTag( psz_tag ) ? 2 : 1;
490 const char *p = psz_tag;
491 *pi_tag = 0;
492 if( isalpha( *p ) )
494 while( isalnum( *p ) )
496 p++;
497 (*pi_tag)++;
499 while( isspace( *p ) )
500 p++;
502 *ppsz_attrs = p;
503 return psz_tag;
506 /*****************************************************************************
508 *****************************************************************************/
509 static webvtt_dom_cue_t * webvtt_dom_cue_New( mtime_t i_start, mtime_t i_end )
511 webvtt_dom_cue_t *p_cue = calloc( 1, sizeof(*p_cue) );
512 if( p_cue )
514 p_cue->type = NODE_CUE;
515 p_cue->psz_id = NULL;
516 p_cue->i_start = i_start;
517 p_cue->i_stop = i_end;
518 p_cue->p_child = NULL;
519 p_cue->i_lines = 0;
520 webvtt_cue_settings_Init( &p_cue->settings );
522 return p_cue;
525 static void webvtt_dom_cue_ClearText( webvtt_dom_cue_t *p_cue )
527 webvtt_domnode_ChainDelete( p_cue->p_child );
528 p_cue->p_child = NULL;
529 p_cue->i_lines = 0;
532 static void webvtt_dom_cue_Delete( webvtt_dom_cue_t *p_cue )
534 webvtt_dom_cue_ClearText( p_cue );
535 webvtt_cue_settings_Clean( &p_cue->settings );
536 free( p_cue->psz_id );
537 free( p_cue );
540 /* reduces by one line */
541 static unsigned webvtt_dom_cue_Reduced( webvtt_dom_cue_t *p_cue )
543 if( p_cue->i_lines < 1 )
544 return 0;
546 for( webvtt_dom_node_t *p_node = p_cue->p_child;
547 p_node; p_node = p_node->p_next )
549 if( p_node->type != NODE_TEXT )
550 continue;
551 webvtt_dom_text_t *p_textnode = (webvtt_dom_text_t *) p_node;
552 const char *nl = strchr( p_textnode->psz_text, '\n' );
553 if( nl )
555 size_t i_len = strlen( p_textnode->psz_text );
556 size_t i_remain = i_len - (nl - p_textnode->psz_text);
557 char *psz_new = strndup( nl + 1, i_remain );
558 free( p_textnode->psz_text );
559 p_textnode->psz_text = psz_new;
560 return --p_cue->i_lines;
562 else
564 free( p_textnode->psz_text );
565 p_textnode->psz_text = NULL;
566 /* FIXME: probably can do a local nodes cleanup */
570 return p_cue->i_lines;
573 /*****************************************************************************
575 *****************************************************************************/
577 static void webvtt_region_ParseTuple( webvtt_region_t *p_region,
578 const char *psz_key, const char *psz_value )
580 if( !strcmp( psz_key, "id" ) )
582 free( p_region->psz_id );
583 p_region->psz_id = strdup( psz_value );
585 else if( !strcmp( psz_key, "width" ) )
587 parse_percent( psz_value, &p_region->f_width );
589 else if( !strcmp( psz_key, "regionanchor" ) )
591 parse_percent_tuple( psz_value, &p_region->anchor_x,
592 &p_region->anchor_y );
594 else if( !strcmp( psz_key, "viewportanchor" ) )
596 parse_percent_tuple( psz_value, &p_region->viewport_anchor_x,
597 &p_region->viewport_anchor_y );
599 else if( !strcmp( psz_key, "lines" ) )
601 int i = atoi( psz_value );
602 if( i > 0 )
603 p_region->i_lines_max_scroll = __MIN(i, WEBVTT_REGION_LINES_COUNT);
605 else if( !strcmp( psz_key, "scroll" ) )
607 p_region->b_scroll_up = !strcmp( psz_value, "up" );
611 static void webvtt_region_Parse( webvtt_region_t *p_region, char *psz_line )
613 char *p_save;
614 char *psz_tuple;
615 char *p_str = psz_line;
618 psz_tuple = strtok_r( p_str, " ", &p_save );
619 p_str = NULL;
620 if( psz_tuple )
622 const char *psz_split = strchr( psz_tuple, ':' );
623 if( psz_split && psz_split[1] != 0 && psz_split != psz_tuple )
625 char *psz_key = strndup( psz_tuple, psz_split - psz_tuple );
626 if( psz_key )
628 webvtt_region_ParseTuple( p_region, psz_key, psz_split + 1 );
629 free( psz_key );
633 } while( psz_tuple );
636 static unsigned webvtt_region_CountLines( const webvtt_region_t *p_region )
638 unsigned i_lines = 0;
639 for( const webvtt_dom_node_t *p_node = p_region->p_child;
640 p_node; p_node = p_node->p_next )
642 assert( p_node->type == NODE_CUE );
643 if( p_node->type != NODE_CUE )
644 continue;
645 i_lines += ((const webvtt_dom_cue_t *)p_node)->i_lines;
647 return i_lines;
650 static void webvtt_region_ClearCues( webvtt_region_t *p_region )
652 webvtt_domnode_ChainDelete( p_region->p_child );
653 p_region->p_child = NULL;
656 static void ClearCuesByTime( webvtt_dom_node_t **pp_next, mtime_t i_time )
658 while( *pp_next )
660 webvtt_dom_node_t *p_node = *pp_next;
661 if( p_node )
663 if( p_node->type == NODE_CUE )
665 webvtt_dom_cue_t *p_cue = (webvtt_dom_cue_t *)p_node;
666 if( p_cue->i_stop <= i_time )
668 *pp_next = p_node->p_next;
669 p_node->p_next = NULL;
670 webvtt_dom_cue_Delete( p_cue );
671 continue;
674 else if( p_node->type == NODE_REGION )
676 webvtt_region_t *p_region = (webvtt_region_t *) p_node;
677 ClearCuesByTime( &p_region->p_child, i_time );
679 pp_next = &p_node->p_next;
684 /* Remove top most line/cue for bottom insert */
685 static void webvtt_region_Reduce( webvtt_region_t *p_region )
687 if( p_region->p_child )
689 assert( p_region->p_child->type == NODE_CUE );
690 if( p_region->p_child->type != NODE_CUE )
691 return;
692 webvtt_dom_cue_t *p_cue = (webvtt_dom_cue_t *)p_region->p_child;
693 if( p_cue->i_lines == 1 ||
694 webvtt_dom_cue_Reduced( p_cue ) < 1 )
696 p_region->p_child = p_cue->p_next;
697 p_cue->p_next = NULL;
698 webvtt_dom_cue_Delete( p_cue );
703 static void webvtt_region_AddCue( webvtt_region_t *p_region,
704 webvtt_dom_cue_t *p_cue )
706 webvtt_dom_node_t **pp_add = &p_region->p_child;
707 while( *pp_add )
708 pp_add = &((*pp_add)->p_next);
709 *pp_add = (webvtt_dom_node_t *)p_cue;
710 p_cue->p_parent = (webvtt_dom_node_t *)p_region;
712 for( ;; )
714 unsigned i_lines = webvtt_region_CountLines( p_region );
715 if( i_lines > 0 &&
716 ( i_lines > WEBVTT_REGION_LINES_COUNT ||
717 (p_region->b_scroll_up && i_lines > p_region->i_lines_max_scroll)) )
719 webvtt_region_Reduce( p_region ); /* scrolls up */
720 assert( webvtt_region_CountLines( p_region ) < i_lines );
722 else break;
726 static void webvtt_region_Delete( webvtt_region_t *p_region )
728 webvtt_region_ClearCues( p_region );
729 free( p_region->psz_id );
730 free( p_region );
733 static webvtt_region_t * webvtt_region_New( void )
735 webvtt_region_t *p_region = malloc(sizeof(*p_region));
736 if( p_region )
738 p_region->type = NODE_REGION;
739 p_region->psz_id = NULL;
740 p_region->p_next = NULL;
741 p_region->f_width = 1.0; /* 100% */
742 p_region->anchor_x = 0;
743 p_region->anchor_y = 1.0; /* 100% */
744 p_region->i_lines_max_scroll = 3;
745 p_region->viewport_anchor_x = 0;
746 p_region->viewport_anchor_y = 1.0; /* 100% */
747 p_region->b_scroll_up = false;
748 p_region->p_child = NULL;
750 return p_region;
753 static webvtt_region_t * webvtt_region_GetByID( decoder_sys_t *p_sys,
754 const char *psz_id )
756 if( !psz_id )
757 return NULL;
758 for( webvtt_dom_node_t *p_node = p_sys->p_root->p_child;
759 p_node; p_node = p_node->p_next )
761 if( p_node->type == NODE_REGION )
763 webvtt_region_t *p_region = (webvtt_region_t *) p_node;
764 if( p_region->psz_id && !strcmp( psz_id, p_region->psz_id ) )
765 return p_region;
768 return NULL;
771 /*****************************************************************************
773 *****************************************************************************/
774 static unsigned CountNewLines( const char *psz )
776 unsigned i = 0;
777 while( psz && *psz )
778 psz = strchr( psz + 1, '\n' );
779 return i;
782 static webvtt_dom_node_t * CreateDomNodes( const char *psz_text, unsigned *pi_lines )
784 webvtt_dom_node_t *p_head = NULL;
785 webvtt_dom_node_t **pp_append = &p_head;
786 webvtt_dom_node_t *p_parent = p_head;
787 *pi_lines = 0;
789 while( *psz_text )
791 const char *psz_taglast;
792 const char *psz_tag = FindNextTag( psz_text, &psz_taglast );
793 if( psz_tag )
795 if( psz_tag - psz_text > 0 )
797 webvtt_dom_text_t *p_node = webvtt_dom_text_New( p_parent );
798 if( p_node )
800 p_node->psz_text = strndup( psz_text, psz_tag - psz_text );
801 *pi_lines += ((*pi_lines == 0) ? 1 : 0) + CountNewLines( p_node->psz_text );
802 *pp_append = (webvtt_dom_node_t *) p_node;
803 pp_append = &p_node->p_next;
807 if( ! IsEndTag( psz_tag ) )
809 webvtt_dom_tag_t *p_node = webvtt_dom_tag_New( p_parent );
810 if( p_node )
812 const char *psz_attrs = NULL;
813 size_t i_name;
814 const char *psz_name = SplitTag( psz_tag, &i_name, &psz_attrs );
815 p_node->psz_tag = strndup( psz_name, i_name );
816 if( psz_attrs != psz_taglast )
817 p_node->psz_attrs = strndup( psz_attrs, psz_taglast - psz_attrs );
818 /* <hh:mm::ss:fff> time tags */
819 if( p_node->psz_attrs && isdigit(p_node->psz_attrs[0]) )
820 (void) webvtt_scan_time( p_node->psz_attrs, &p_node->i_start );
821 *pp_append = (webvtt_dom_node_t *) p_node;
822 p_parent = (webvtt_dom_node_t *) p_node;
823 pp_append = &p_node->p_child;
826 else
828 if( p_parent )
830 const char *psz_attrs = NULL;
831 size_t i_name;
832 const char *psz_name = SplitTag( psz_tag, &i_name, &psz_attrs );
833 char *psz_tagname = strndup( psz_name, i_name );
835 /* Close at matched parent node level due to unclosed tags
836 * like <b><v stuff>foo</b> */
837 p_parent = webvtt_domnode_getParentByTag( p_parent->p_parent, psz_tagname );
838 if( p_parent ) /* continue as parent next */
839 pp_append = &p_parent->p_next;
840 else /* back as top node */
841 pp_append = &p_head->p_next;
842 while( *pp_append )
843 pp_append = &((*pp_append)->p_next);
845 free( psz_tagname );
847 else break; /* End tag for non open tag */
849 psz_text = psz_taglast + 1;
851 else /* Special case: end */
853 webvtt_dom_text_t *p_node = webvtt_dom_text_New( p_parent );
854 if( p_node )
856 p_node->psz_text = strdup( psz_text );
857 *pi_lines += ((*pi_lines == 0) ? 1 : 0) + CountNewLines( p_node->psz_text );
858 *pp_append = (webvtt_dom_node_t *) p_node;
860 break;
864 return p_head;
867 static void ProcessCue( decoder_t *p_dec, const char *psz, webvtt_dom_cue_t *p_cue )
869 VLC_UNUSED(p_dec);
871 if( p_cue->p_child )
872 return;
873 p_cue->p_child = CreateDomNodes( psz, &p_cue->i_lines );
874 for( webvtt_dom_node_t *p_child = p_cue->p_child; p_child; p_child = p_child->p_next )
875 p_child->p_parent = (webvtt_dom_node_t *)p_cue;
876 #ifdef SUBSVTT_DEBUG
877 webvtt_domnode_Debug( (webvtt_dom_node_t *) p_cue, 0 );
878 #endif
881 static text_style_t * InheritStyles( decoder_t *p_dec, const webvtt_dom_node_t *p_node )
883 VLC_UNUSED(p_dec);
885 text_style_t *p_style = NULL;
886 for( ; p_node; p_node = p_node->p_parent )
888 if( p_node->type == NODE_TAG )
890 const webvtt_dom_tag_t *p_tagnode = (const webvtt_dom_tag_t *)p_node;
891 if ( p_tagnode->psz_tag == NULL )
893 continue;
895 else if ( !strcmp( p_tagnode->psz_tag, "b" ) )
897 if( p_style || (p_style = text_style_Create( STYLE_NO_DEFAULTS )) )
899 p_style->i_style_flags |= STYLE_BOLD;
900 p_style->i_features |= STYLE_HAS_FLAGS;
903 else if ( !strcmp( p_tagnode->psz_tag, "i" ) )
905 if( p_style || (p_style = text_style_Create( STYLE_NO_DEFAULTS )) )
907 p_style->i_style_flags |= STYLE_ITALIC;
908 p_style->i_features |= STYLE_HAS_FLAGS;
911 else if ( !strcmp( p_tagnode->psz_tag, "u" ) )
913 if( p_style || (p_style = text_style_Create( STYLE_NO_DEFAULTS )) )
915 p_style->i_style_flags |= STYLE_UNDERLINE;
916 p_style->i_features |= STYLE_HAS_FLAGS;
919 else if ( !strcmp( p_tagnode->psz_tag, "v" ) && p_tagnode->psz_attrs )
921 if( p_style || (p_style = text_style_Create( STYLE_NO_DEFAULTS )) )
923 unsigned a = 0;
924 for( char *p = p_tagnode->psz_attrs; *p; p++ )
925 a = (a << 3) ^ *p;
926 p_style->i_font_color = (0x7F7F7F | a) & 0xFFFFFF;
927 p_style->i_features |= STYLE_HAS_FONT_COLOR;
932 return p_style;
935 static int GetCueTextAlignment( const webvtt_dom_cue_t *p_cue )
937 switch( p_cue->settings.align )
939 case WEBVTT_ALIGN_LEFT:
940 return SUBPICTURE_ALIGN_LEFT;
941 case WEBVTT_ALIGN_RIGHT:
942 return SUBPICTURE_ALIGN_RIGHT;
943 case WEBVTT_ALIGN_START:
944 return ((p_cue->settings.vertical == WEBVTT_ALIGN_RIGHT) ?
945 SUBPICTURE_ALIGN_LEFT : SUBPICTURE_ALIGN_RIGHT);
946 case WEBVTT_ALIGN_END:
947 return ((p_cue->settings.vertical == WEBVTT_ALIGN_RIGHT)) ?
948 SUBPICTURE_ALIGN_RIGHT : SUBPICTURE_ALIGN_LEFT;
949 default:
950 return 0;
954 struct render_variables_s
956 const webvtt_region_t *p_region;
957 float i_left_offset;
958 float i_left;
959 float i_top_offset;
960 float i_top;
963 static text_segment_t *ConvertNodesToSegments( decoder_t *p_dec,
964 struct render_variables_s *p_vars,
965 const webvtt_dom_cue_t *p_cue,
966 const webvtt_dom_node_t *p_node,
967 mtime_t i_start )
969 text_segment_t *p_head = NULL;
970 text_segment_t **pp_append = &p_head;
971 for( ; p_node ; p_node = p_node->p_next )
973 while( *pp_append )
974 pp_append = &((*pp_append)->p_next);
976 if( p_node->type == NODE_TEXT )
978 const webvtt_dom_text_t *p_textnode = (const webvtt_dom_text_t *) p_node;
979 if( p_textnode->psz_text == NULL )
980 continue;
982 *pp_append = text_segment_New( p_textnode->psz_text );
983 if( *pp_append )
985 if( (*pp_append)->psz_text )
986 vlc_xml_decode( (*pp_append)->psz_text );
987 (*pp_append)->style = InheritStyles( p_dec, p_node );
990 else if( p_node->type == NODE_TAG )
992 const webvtt_dom_tag_t *p_tag = (const webvtt_dom_tag_t *)p_node;
993 if( p_tag->i_start <= i_start )
994 *pp_append = ConvertNodesToSegments( p_dec, p_vars, p_cue,
995 p_tag->p_child, i_start );
998 return p_head;
1001 static text_segment_t *ConvertCueToSegments( decoder_t *p_dec,
1002 struct render_variables_s *p_vars,
1003 const webvtt_dom_cue_t *p_cue,
1004 mtime_t i_start )
1006 return ConvertNodesToSegments( p_dec, p_vars, p_cue, p_cue->p_child, i_start );
1009 static text_segment_t * ConvertCuesToSegments( decoder_t *p_dec, mtime_t i_start, mtime_t i_stop,
1010 struct render_variables_s *p_vars,
1011 const webvtt_dom_cue_t *p_cue )
1013 text_segment_t *p_segments = NULL;
1014 text_segment_t **pp_append = &p_segments;
1015 VLC_UNUSED(i_stop);
1017 for( ; p_cue; p_cue = (const webvtt_dom_cue_t *) p_cue->p_next )
1019 assert( p_cue->type == NODE_CUE );
1020 if( p_cue->type != NODE_CUE )
1021 continue;
1023 if( p_cue->i_start > i_start || p_cue->i_stop <= i_start )
1024 continue;
1026 text_segment_t *p_new = ConvertCueToSegments( p_dec, p_vars, p_cue, i_start );
1027 if( p_new )
1029 while( *pp_append )
1030 pp_append = &((*pp_append)->p_next);
1032 if( p_segments ) /* auto newlines */
1034 *pp_append = text_segment_New( "\n" );
1035 if( *pp_append )
1036 pp_append = &((*pp_append)->p_next);
1039 if( p_cue->settings.vertical == WEBVTT_ALIGN_LEFT ) /* LTR */
1041 *pp_append = text_segment_New( "\u2067" );
1042 if( *pp_append )
1043 pp_append = &((*pp_append)->p_next);
1046 *pp_append = p_new;
1048 if( p_cue->settings.vertical == WEBVTT_ALIGN_LEFT )
1050 *pp_append = text_segment_New( "\u2069" );
1051 if( *pp_append )
1052 pp_append = &((*pp_append)->p_next);
1056 return p_segments;
1059 static void GetTimedTags( const webvtt_dom_node_t *p_node,
1060 mtime_t i_start, mtime_t i_stop, vlc_array_t *p_times )
1062 for( ; p_node; p_node = p_node->p_next )
1064 switch( p_node->type )
1066 case NODE_TAG:
1068 const webvtt_dom_tag_t *p_tag = (const webvtt_dom_tag_t *) p_node;
1069 if( p_tag->i_start > -1 && p_tag->i_start >= i_start && p_tag->i_start < i_stop )
1070 (void) vlc_array_append( p_times, (void *) p_tag );
1071 GetTimedTags( p_tag->p_child, i_start, i_stop, p_times );
1072 } break;
1073 case NODE_REGION:
1074 case NODE_CUE:
1075 case NODE_VIDEO:
1076 GetTimedTags( webvtt_domnode_getFirstChild( p_node ),
1077 i_start, i_stop, p_times );
1078 break;
1079 default:
1080 break;
1085 static void CreateSpuOrNewUpdaterRegion( decoder_t *p_dec,
1086 subpicture_t **pp_spu,
1087 subpicture_updater_sys_region_t **pp_updtregion )
1089 if( *pp_spu == NULL )
1091 *pp_spu = decoder_NewSubpictureText( p_dec );
1092 if( *pp_spu )
1093 *pp_updtregion = &(*pp_spu)->updater.p_sys->region;
1095 else
1097 subpicture_updater_sys_region_t *p_new =
1098 SubpictureUpdaterSysRegionNew( );
1099 if( p_new )
1101 SubpictureUpdaterSysRegionAdd( *pp_updtregion, p_new );
1102 *pp_updtregion = p_new;
1107 static void RenderRegions( decoder_t *p_dec, mtime_t i_start, mtime_t i_stop )
1109 subpicture_t *p_spu = NULL;
1110 subpicture_updater_sys_region_t *p_updtregion = NULL;
1112 for( const webvtt_dom_node_t *p_node = p_dec->p_sys->p_root->p_child;
1113 p_node; p_node = p_node->p_next )
1115 if( p_node->type == NODE_REGION )
1117 const webvtt_region_t *p_vttregion = (const webvtt_region_t *) p_node;
1118 /* Variables */
1119 struct render_variables_s v;
1120 v.p_region = p_vttregion;
1121 v.i_left_offset = p_vttregion->anchor_x * p_vttregion->f_width;
1122 v.i_left = p_vttregion->viewport_anchor_x - v.i_left_offset;
1123 v.i_top_offset = p_vttregion->anchor_y * p_vttregion->i_lines_max_scroll *
1124 WEBVTT_DEFAULT_LINE_HEIGHT_VH / 100.0;
1125 v.i_top = p_vttregion->viewport_anchor_y - v.i_top_offset;
1126 /* !Variables */
1128 text_segment_t *p_segments =
1129 ConvertCuesToSegments( p_dec, i_start, i_stop, &v,
1130 (const webvtt_dom_cue_t *)p_vttregion->p_child );
1131 if( !p_segments )
1132 continue;
1134 CreateSpuOrNewUpdaterRegion( p_dec, &p_spu, &p_updtregion );
1135 if( !p_spu || !p_updtregion )
1137 text_segment_ChainDelete( p_segments );
1138 continue;
1141 p_updtregion->align = SUBPICTURE_ALIGN_TOP|SUBPICTURE_ALIGN_LEFT;
1142 p_updtregion->inner_align = GetCueTextAlignment( (const webvtt_dom_cue_t *)p_vttregion->p_child );
1143 p_updtregion->origin.x = v.i_left;
1144 p_updtregion->origin.y = v.i_top;
1145 p_updtregion->extent.x = p_vttregion->f_width;
1147 p_updtregion->flags = UPDT_REGION_ORIGIN_X_IS_RATIO|UPDT_REGION_ORIGIN_Y_IS_RATIO
1148 | UPDT_REGION_EXTENT_X_IS_RATIO;
1149 p_updtregion->p_segments = p_segments;
1151 else if( p_node->type == NODE_CUE ) /* regionless cues */
1153 const webvtt_dom_cue_t *p_cue = (const webvtt_dom_cue_t *) p_node;
1154 /* Variables */
1155 struct render_variables_s v;
1156 v.p_region = NULL;
1157 v.i_left_offset = 0.0;
1158 v.i_left = 0.0;
1159 v.i_top_offset = 0.0;
1160 v.i_top = 0.0;
1161 /* !Variables */
1163 text_segment_t *p_segments = ConvertCuesToSegments( p_dec, i_start, i_stop, &v, p_cue );
1164 if( !p_segments )
1165 continue;
1167 CreateSpuOrNewUpdaterRegion( p_dec, &p_spu, &p_updtregion );
1168 if( !p_spu || !p_updtregion )
1170 text_segment_ChainDelete( p_segments );
1171 continue;
1174 p_updtregion->align = SUBPICTURE_ALIGN_BOTTOM;
1175 p_updtregion->inner_align = GetCueTextAlignment( p_cue );
1177 p_updtregion->p_segments = p_segments;
1181 if( p_spu )
1183 p_spu->i_start = i_start;
1184 p_spu->i_stop = i_stop;
1185 p_spu->b_ephemer = true; /* !important */
1186 p_spu->b_absolute = false;
1188 subpicture_updater_sys_t *p_spu_sys = p_spu->updater.p_sys;
1189 p_spu_sys->p_default_style->f_font_relsize = WEBVTT_DEFAULT_LINE_HEIGHT_VH /
1190 WEBVTT_LINE_TO_HEIGHT_RATIO;
1191 decoder_QueueSub( p_dec, p_spu );
1195 static int timedtagsArrayCmp( const void *a, const void *b )
1197 const webvtt_dom_tag_t *ta = *((const webvtt_dom_tag_t **) a);
1198 const webvtt_dom_tag_t *tb = *((const webvtt_dom_tag_t **) b);
1199 const int64_t result = ta->i_start - tb->i_start;
1200 return result == 0 ? 0 : result > 0 ? 1 : -1;
1203 static void Render( decoder_t *p_dec, mtime_t i_start, mtime_t i_stop )
1205 decoder_sys_t *p_sys = p_dec->p_sys;
1207 vlc_array_t timedtags;
1208 vlc_array_init( &timedtags );
1210 GetTimedTags( p_sys->p_root->p_child, i_start, i_stop, &timedtags );
1211 qsort( timedtags.pp_elems, timedtags.i_count, sizeof(*timedtags.pp_elems), timedtagsArrayCmp );
1213 mtime_t i_substart = i_start;
1214 for( size_t i=0; i<timedtags.i_count; i++ )
1216 const webvtt_dom_tag_t *p_tag =
1217 (const webvtt_dom_tag_t *) vlc_array_item_at_index( &timedtags, i );
1218 if( p_tag->i_start != i_substart ) /* might be duplicates */
1220 RenderRegions( p_dec, i_substart, p_tag->i_start );
1221 i_substart = p_tag->i_start;
1224 if( i_substart != i_stop )
1225 RenderRegions( p_dec, i_substart, i_stop );
1227 vlc_array_clear( &timedtags );
1230 static int ProcessISOBMFF( decoder_t *p_dec,
1231 const uint8_t *p_buffer, size_t i_buffer,
1232 mtime_t i_start, mtime_t i_stop )
1234 mp4_box_iterator_t it;
1235 mp4_box_iterator_Init( &it, p_buffer, i_buffer );
1236 while( mp4_box_iterator_Next( &it ) )
1238 if( it.i_type == ATOM_vttc || it.i_type == ATOM_vttx )
1240 webvtt_dom_cue_t *p_cue = webvtt_dom_cue_New( i_start, i_stop );
1241 if( !p_cue )
1242 continue;
1244 mp4_box_iterator_t vtcc;
1245 mp4_box_iterator_Init( &vtcc, it.p_payload, it.i_payload );
1246 while( mp4_box_iterator_Next( &vtcc ) )
1248 char *psz = NULL;
1249 switch( vtcc.i_type )
1251 case ATOM_iden:
1252 free( p_cue->psz_id );
1253 p_cue->psz_id = strndup( (char *) vtcc.p_payload, vtcc.i_payload );
1254 break;
1255 case ATOM_sttg:
1257 psz = strndup( (char *) vtcc.p_payload, vtcc.i_payload );
1258 if( psz )
1259 webvtt_cue_settings_Parse( &p_cue->settings, psz );
1260 } break;
1261 case ATOM_payl:
1263 psz = strndup( (char *) vtcc.p_payload, vtcc.i_payload );
1264 if( psz )
1265 ProcessCue( p_dec, psz, p_cue );
1266 } break;
1268 free( psz );
1271 webvtt_region_t *p_region = webvtt_region_GetByID( p_dec->p_sys,
1272 p_cue->settings.psz_region );
1273 if( p_region )
1275 webvtt_region_AddCue( p_region, p_cue );
1276 assert( p_region->p_child );
1278 else
1280 webvtt_domnode_AppendLast( &p_dec->p_sys->p_root->p_child, p_cue );
1281 p_cue->p_parent = (webvtt_dom_node_t *) p_dec->p_sys->p_root;
1285 return 0;
1288 struct parser_ctx
1290 webvtt_region_t *p_region;
1291 decoder_t *p_dec;
1294 static void ParserHeaderHandler( void *priv, enum webvtt_header_line_e s,
1295 bool b_new, const char *psz_line )
1297 struct parser_ctx *ctx = (struct parser_ctx *)priv;
1298 decoder_t *p_dec = ctx->p_dec;
1299 decoder_sys_t *p_sys = p_dec->p_sys;
1301 if( b_new || !psz_line /* commit */ )
1303 if( ctx->p_region )
1305 if( ctx->p_region->psz_id )
1307 webvtt_domnode_AppendLast( &p_sys->p_root->p_child, ctx->p_region );
1308 ctx->p_region->p_parent = (webvtt_dom_node_t *) p_sys->p_root;
1309 msg_Dbg( p_dec, "added new region %s", ctx->p_region->psz_id );
1311 /* incomplete region decl (no id at least) */
1312 else webvtt_region_Delete( ctx->p_region );
1313 ctx->p_region = NULL;
1316 if( !psz_line )
1317 return;
1319 if( b_new )
1321 if( s == WEBVTT_HEADER_REGION )
1322 ctx->p_region = webvtt_region_New();
1323 return;
1327 if( s == WEBVTT_HEADER_REGION && ctx->p_region )
1328 webvtt_region_Parse( ctx->p_region, (char*) psz_line );
1331 static void LoadExtradata( decoder_t *p_dec )
1333 stream_t *p_stream = vlc_stream_MemoryNew( p_dec,
1334 p_dec->fmt_in.p_extra,
1335 p_dec->fmt_in.i_extra,
1336 true );
1337 if( !p_stream )
1338 return;
1340 struct parser_ctx ctx;
1341 ctx.p_region = NULL;
1342 ctx.p_dec = p_dec;
1343 webvtt_text_parser_t *p_parser =
1344 webvtt_text_parser_New( &ctx, NULL, NULL, ParserHeaderHandler );
1345 if( p_parser )
1347 char *psz_line;
1348 while( (psz_line = vlc_stream_ReadLine( p_stream )) )
1349 webvtt_text_parser_Feed( p_parser, psz_line );
1350 webvtt_text_parser_Delete( p_parser );
1351 /* commit using null */
1352 ParserHeaderHandler( &ctx, 0, false, NULL );
1355 vlc_stream_Delete( p_stream );
1358 /****************************************************************************
1359 * DecodeBlock: decoder data entry point
1360 ****************************************************************************/
1361 static int DecodeBlock( decoder_t *p_dec, block_t *p_block )
1363 if( p_block == NULL ) /* No Drain */
1364 return VLCDEC_SUCCESS;
1366 if( p_block->i_flags & BLOCK_FLAG_DISCONTINUITY )
1367 ClearCuesByTime( &p_dec->p_sys->p_root->p_child, INT64_MAX );
1368 else
1369 ClearCuesByTime( &p_dec->p_sys->p_root->p_child, p_block->i_dts );
1371 ProcessISOBMFF( p_dec, p_block->p_buffer, p_block->i_buffer,
1372 p_block->i_pts, p_block->i_pts + p_block->i_length );
1374 Render( p_dec, p_block->i_pts, p_block->i_pts + p_block->i_length );
1376 block_Release( p_block );
1377 return VLCDEC_SUCCESS;
1380 /*****************************************************************************
1381 * CloseDecoder: clean up the decoder
1382 *****************************************************************************/
1383 void CloseDecoder( vlc_object_t *p_this )
1385 decoder_t *p_dec = (decoder_t *)p_this;
1386 decoder_sys_t *p_sys = p_dec->p_sys;
1388 webvtt_domnode_ChainDelete( (webvtt_dom_node_t *) p_sys->p_root );
1390 free( p_sys );
1393 /*****************************************************************************
1394 * OpenDecoder: probe the decoder and return score
1395 *****************************************************************************/
1396 int OpenDecoder( vlc_object_t *p_this )
1398 decoder_t *p_dec = (decoder_t*)p_this;
1399 decoder_sys_t *p_sys;
1401 if( p_dec->fmt_in.i_codec != VLC_CODEC_WEBVTT )
1402 return VLC_EGENERIC;
1404 /* Allocate the memory needed to store the decoder's structure */
1405 p_dec->p_sys = p_sys = calloc( 1, sizeof( *p_sys ) );
1406 if( unlikely( p_sys == NULL ) )
1407 return VLC_ENOMEM;
1409 p_sys->p_root = webvtt_dom_video_New();
1410 if( !p_sys->p_root )
1412 free( p_sys );
1413 return VLC_ENOMEM;
1416 p_dec->pf_decode = DecodeBlock;
1418 if( p_dec->fmt_in.i_extra )
1419 LoadExtradata( p_dec );
1421 return VLC_SUCCESS;