| /* | |
| * GPAC - Multimedia Framework C SDK | |
| * | |
| * Authors: Cyril Concolato, Jean Le Feuvre | |
| * Copyright (c) Telecom ParisTech 2000-2026 | |
| * All rights reserved | |
| * | |
| * This file is part of GPAC / ISO Media File Format sub-project | |
| * | |
| * GPAC is free software; you can redistribute it and/or modify | |
| * it under the terms of the GNU Lesser General Public License as published by | |
| * the Free Software Foundation; either version 2, or (at your option) | |
| * any later version. | |
| * | |
| * GPAC is distributed in the hope that it will be useful, | |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| * GNU Lesser General Public License for more details. | |
| * | |
| * You should have received a copy of the GNU Lesser General Public | |
| * License along with this library; see the file COPYING. If not, write to | |
| * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. | |
| * | |
| */ | |
| #include <gpac/list.h> | |
| #include <gpac/internal/isomedia_dev.h> | |
| #include <gpac/internal/media_dev.h> | |
| #include <gpac/webvtt.h> | |
| #include <gpac/constants.h> | |
| #ifndef GPAC_DISABLE_VTT | |
| struct _webvtt_sample | |
| { | |
| u64 start; | |
| u64 end; | |
| GF_List *cues; | |
| }; | |
| #ifndef GPAC_DISABLE_ISOM | |
| typedef struct | |
| { | |
| GF_ISOM_BOX | |
| GF_StringBox *id; | |
| GF_StringBox *time; | |
| GF_StringBox *settings; | |
| GF_StringBox *payload; | |
| } GF_VTTCueBox; | |
| GF_Box *boxstring_box_new() { | |
| //type is assigned by caller | |
| ISOM_DECL_BOX_ALLOC(GF_StringBox, 0); | |
| return (GF_Box *)tmp; | |
| } | |
| GF_Box *boxstring_new_with_data(u32 type, const char *string, GF_List **parent) | |
| { | |
| GF_Box *a = NULL; | |
| switch (type) { | |
| case GF_ISOM_BOX_TYPE_VTTC_CONFIG: | |
| case GF_ISOM_BOX_TYPE_CTIM: | |
| case GF_ISOM_BOX_TYPE_IDEN: | |
| case GF_ISOM_BOX_TYPE_STTG: | |
| case GF_ISOM_BOX_TYPE_PAYL: | |
| case GF_ISOM_BOX_TYPE_VTTA: | |
| if (string) { | |
| /* remove trailing spaces; spec. \r, \n; skip if empty */ | |
| size_t len = strlen(string); | |
| while (len && isspace(string[len-1])) { | |
| len--; | |
| } | |
| if (!len) break; | |
| if (parent) { | |
| a = gf_isom_box_new_parent(parent, type); | |
| } else { | |
| a = gf_isom_box_new(type); | |
| } | |
| if (a) { | |
| char* str = ((GF_StringBox *)a)->string = gf_malloc(len + 1); | |
| memcpy(str, string, len); | |
| str[len] = '\0'; | |
| } | |
| } | |
| break; | |
| default: | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_CONTAINER, ("[iso file] Box type %s is not a boxstring, cannot initialize with data\n", gf_4cc_to_str(type) )); | |
| break; | |
| } | |
| return a; | |
| } | |
| GF_Box *vtcu_box_new() | |
| { | |
| ISOM_DECL_BOX_ALLOC(GF_VTTCueBox, GF_ISOM_BOX_TYPE_VTCC_CUE); | |
| return (GF_Box *)tmp; | |
| } | |
| GF_Box *vtte_box_new() { | |
| ISOM_DECL_BOX_ALLOC(GF_Box, GF_ISOM_BOX_TYPE_VTTE); | |
| return (GF_Box *)tmp; | |
| } | |
| void boxstring_box_del(GF_Box *s) | |
| { | |
| GF_StringBox *box = (GF_StringBox *)s; | |
| if (box->string) gf_free(box->string); | |
| gf_free(box); | |
| } | |
| void vtcu_box_del(GF_Box *s) | |
| { | |
| gf_free(s); | |
| } | |
| void vtte_box_del(GF_Box *s) | |
| { | |
| gf_free(s); | |
| } | |
| GF_Box *wvtt_box_new() | |
| { | |
| ISOM_DECL_BOX_ALLOC(GF_WebVTTSampleEntryBox, GF_ISOM_BOX_TYPE_WVTT); | |
| gf_isom_sample_entry_init((GF_SampleEntryBox *)tmp); | |
| return (GF_Box *)tmp; | |
| } | |
| void wvtt_box_del(GF_Box *s) | |
| { | |
| gf_isom_sample_entry_predestroy((GF_SampleEntryBox *)s); | |
| gf_free(s); | |
| } | |
| GF_Err boxstring_box_read(GF_Box *s, GF_BitStream *bs) | |
| { | |
| GF_StringBox *box = (GF_StringBox *)s; | |
| if (s->size > GF_UINT_MAX-1) return GF_ISOM_INVALID_FILE; | |
| box->string = (char *)gf_malloc((u32)(s->size+1)); | |
| if (!box->string) return GF_OUT_OF_MEM; | |
| gf_bs_read_data(bs, box->string, (u32)(s->size)); | |
| box->string[(u32)(s->size)] = 0; | |
| return GF_OK; | |
| } | |
| GF_Err vtcu_on_child_box(GF_Box *s, GF_Box *a, Bool is_rem) | |
| { | |
| GF_VTTCueBox *ptr = (GF_VTTCueBox *)s; | |
| switch (a->type) { | |
| case GF_ISOM_BOX_TYPE_CTIM: | |
| BOX_FIELD_ASSIGN(time, GF_StringBox); | |
| break; | |
| case GF_ISOM_BOX_TYPE_IDEN: | |
| BOX_FIELD_ASSIGN(id, GF_StringBox); | |
| break; | |
| case GF_ISOM_BOX_TYPE_STTG: | |
| BOX_FIELD_ASSIGN(settings, GF_StringBox); | |
| break; | |
| case GF_ISOM_BOX_TYPE_PAYL: | |
| BOX_FIELD_ASSIGN(payload, GF_StringBox); | |
| break; | |
| } | |
| return GF_OK; | |
| } | |
| GF_Err vtcu_box_read(GF_Box *s, GF_BitStream *bs) | |
| { | |
| return gf_isom_box_array_read(s, bs); | |
| } | |
| GF_Err vtte_box_read(GF_Box *s, GF_BitStream *bs) | |
| { | |
| return gf_isom_box_array_read(s, bs); | |
| } | |
| GF_Err wvtt_on_child_box(GF_Box *s, GF_Box *a, Bool is_rem) | |
| { | |
| GF_WebVTTSampleEntryBox *ptr = (GF_WebVTTSampleEntryBox *)s; | |
| switch (a->type) { | |
| case GF_ISOM_BOX_TYPE_VTTC_CONFIG: | |
| BOX_FIELD_ASSIGN(config, GF_StringBox); | |
| break; | |
| } | |
| return GF_OK; | |
| } | |
| GF_Err wvtt_box_read(GF_Box *s, GF_BitStream *bs) | |
| { | |
| GF_Err e; | |
| GF_WebVTTSampleEntryBox *wvtt = (GF_WebVTTSampleEntryBox *)s; | |
| e = gf_isom_base_sample_entry_read((GF_SampleEntryBox *)wvtt, bs); | |
| if (e) return e; | |
| wvtt->size -= 8; | |
| return gf_isom_box_array_read(s, bs); | |
| } | |
| #ifndef GPAC_DISABLE_ISOM_WRITE | |
| GF_Err boxstring_box_write(GF_Box *s, GF_BitStream *bs) | |
| { | |
| GF_Err e; | |
| GF_StringBox *box = (GF_StringBox *)s; | |
| e = gf_isom_box_write_header(s, bs); | |
| if (e) return e; | |
| if (box->string) { | |
| gf_bs_write_data(bs, box->string, (u32)(box->size-8)); | |
| } | |
| return GF_OK; | |
| } | |
| GF_Err vtcu_box_write(GF_Box *s, GF_BitStream *bs) | |
| { | |
| return gf_isom_box_write_header(s, bs); | |
| } | |
| GF_Err vtte_box_write(GF_Box *s, GF_BitStream *bs) | |
| { | |
| GF_Err e; | |
| e = gf_isom_box_write_header(s, bs); | |
| return e; | |
| } | |
| GF_Err wvtt_box_write(GF_Box *s, GF_BitStream *bs) | |
| { | |
| GF_Err e; | |
| GF_WebVTTSampleEntryBox *wvtt = (GF_WebVTTSampleEntryBox *)s; | |
| e = gf_isom_box_write_header(s, bs); | |
| gf_bs_write_data(bs, wvtt->reserved, 6); | |
| gf_bs_write_u16(bs, wvtt->dataReferenceIndex); | |
| return e; | |
| } | |
| GF_Err boxstring_box_size(GF_Box *s) | |
| { | |
| GF_StringBox *box = (GF_StringBox *)s; | |
| if (box->string) | |
| box->size += strlen(box->string); | |
| return GF_OK; | |
| } | |
| GF_Err vtcu_box_size(GF_Box *s) | |
| { | |
| u32 pos=0; | |
| GF_VTTCueBox *cuebox = (GF_VTTCueBox *)s; | |
| gf_isom_check_position(s, (GF_Box*)cuebox->id, &pos); | |
| gf_isom_check_position(s, (GF_Box*)cuebox->time, &pos); | |
| gf_isom_check_position(s, (GF_Box*)cuebox->settings, &pos); | |
| gf_isom_check_position(s, (GF_Box*)cuebox->payload, &pos); | |
| return GF_OK; | |
| } | |
| GF_Err vtte_box_size(GF_Box *s) | |
| { | |
| return GF_OK; | |
| } | |
| GF_Err wvtt_box_size(GF_Box *s) | |
| { | |
| u32 pos=0; | |
| GF_WebVTTSampleEntryBox *wvtt = (GF_WebVTTSampleEntryBox *)s; | |
| s->size += 8; // reserved and dataReferenceIndex | |
| gf_isom_check_position(s, (GF_Box *)wvtt->config, &pos); | |
| return GF_OK; | |
| } | |
| static GF_Err wvtt_write_cue(GF_BitStream *bs, GF_WebVTTCue *cue) | |
| { | |
| GF_Err e; | |
| GF_VTTCueBox *cuebox; | |
| if (!cue) return GF_OK; | |
| if (cue->pre_text) { | |
| GF_Box *b = boxstring_new_with_data(GF_ISOM_BOX_TYPE_VTTA, cue->pre_text, NULL); | |
| e = gf_isom_box_size(b); | |
| if (!e) e = gf_isom_box_write(b, bs); | |
| gf_isom_box_del(b); | |
| } | |
| cuebox = (GF_VTTCueBox *)gf_isom_box_new(GF_ISOM_BOX_TYPE_VTCC_CUE); | |
| if (cue->id) { | |
| cuebox->id = (GF_StringBox *)boxstring_new_with_data(GF_ISOM_BOX_TYPE_IDEN, cue->id, &cuebox->child_boxes); | |
| } | |
| if (cue->settings) { | |
| cuebox->settings = (GF_StringBox *)boxstring_new_with_data(GF_ISOM_BOX_TYPE_STTG, cue->settings, &cuebox->child_boxes); | |
| } | |
| if (cue->text) { | |
| cuebox->payload = (GF_StringBox *)boxstring_new_with_data(GF_ISOM_BOX_TYPE_PAYL, cue->text, &cuebox->child_boxes); | |
| } | |
| /* TODO: check if a time box should be written */ | |
| e = gf_isom_box_size((GF_Box *)cuebox); | |
| if (!e) e = gf_isom_box_write((GF_Box *)cuebox, bs); | |
| gf_isom_box_del((GF_Box *)cuebox); | |
| return e; | |
| } | |
| GF_ISOSample *gf_isom_webvtt_to_sample(void *s) | |
| { | |
| GF_Err e = GF_OK; | |
| GF_ISOSample *res; | |
| GF_BitStream *bs; | |
| u32 i; | |
| GF_WebVTTSample *samp = (GF_WebVTTSample *)s; | |
| if (!samp) return NULL; | |
| bs = gf_bs_new(NULL, 0, GF_BITSTREAM_WRITE); | |
| if (gf_list_count(samp->cues)) { | |
| GF_WebVTTCue *cue; | |
| i=0; | |
| while ((cue = (GF_WebVTTCue *)gf_list_enum(samp->cues, &i))) { | |
| e = wvtt_write_cue(bs, cue); | |
| if (e) break; | |
| } | |
| if (e) { | |
| gf_bs_del(bs); | |
| return NULL; | |
| } | |
| } else { | |
| GF_Box *cuebox = gf_isom_box_new(GF_ISOM_BOX_TYPE_VTTE); | |
| e = gf_isom_box_size(cuebox); | |
| if (!e) e = gf_isom_box_write(cuebox, bs); | |
| gf_isom_box_del(cuebox); | |
| if (e) { | |
| gf_bs_del(bs); | |
| return NULL; | |
| } | |
| } | |
| res = gf_isom_sample_new(); | |
| if (!res) { | |
| gf_bs_del(bs); | |
| return NULL; | |
| } | |
| gf_bs_get_content(bs, &res->data, &res->dataLength); | |
| gf_bs_del(bs); | |
| res->IsRAP = RAP; | |
| return res; | |
| } | |
| u32 gf_isom_webvtt_cues_count(void *s) | |
| { | |
| GF_WebVTTSample *samp = (GF_WebVTTSample *)s; | |
| if (!samp) return 0; | |
| return gf_list_count(samp->cues); | |
| } | |
| #endif /*GPAC_DISABLE_ISOM_WRITE*/ | |
| #ifndef GPAC_DISABLE_ISOM_DUMP | |
| GF_Err boxstring_box_dump(GF_Box *a, FILE * trace) | |
| { | |
| char *szName; | |
| GF_StringBox *sbox = (GF_StringBox *)a; | |
| switch (a->type) { | |
| case GF_ISOM_BOX_TYPE_VTTC_CONFIG: | |
| szName = "WebVTTConfigurationBox"; | |
| break; | |
| case GF_ISOM_BOX_TYPE_CTIM: | |
| szName = "CueTimeBox"; | |
| break; | |
| case GF_ISOM_BOX_TYPE_IDEN: | |
| szName = "CueIDBox"; | |
| break; | |
| case GF_ISOM_BOX_TYPE_STTG: | |
| szName = "CueSettingsBox"; | |
| break; | |
| case GF_ISOM_BOX_TYPE_PAYL: | |
| szName = "CuePayloadBox"; | |
| break; | |
| case GF_ISOM_BOX_TYPE_VTTA: | |
| szName = "VTTAdditionalCueBox"; | |
| break; | |
| default: | |
| szName = "StringBox"; | |
| break; | |
| } | |
| gf_isom_box_dump_start(a, szName, trace); | |
| gf_fprintf(trace, ">"); | |
| if (sbox->string && strlen(sbox->string)) | |
| gf_fprintf(trace, "<![CDATA[\n%s\n]]>", sbox->string); | |
| gf_isom_box_dump_done(szName, a, trace); | |
| return GF_OK; | |
| } | |
| GF_Err vtcu_box_dump(GF_Box *a, FILE * trace) | |
| { | |
| gf_isom_box_dump_start(a, "WebVTTCueBox", trace); | |
| gf_fprintf(trace, ">\n"); | |
| gf_isom_box_dump_done("WebVTTCueBox", a, trace); | |
| return GF_OK; | |
| } | |
| GF_Err vtte_box_dump(GF_Box *a, FILE * trace) | |
| { | |
| gf_isom_box_dump_start(a, "WebVTTEmptyCueBox", trace); | |
| gf_fprintf(trace, ">\n"); | |
| gf_isom_box_dump_done("WebVTTEmptyCueBox", a, trace); | |
| return GF_OK; | |
| } | |
| GF_Err wvtt_box_dump(GF_Box *a, FILE * trace) | |
| { | |
| gf_isom_box_dump_start(a, "WebVTTSampleEntryBox", trace); | |
| gf_fprintf(trace, ">\n"); | |
| gf_isom_box_dump_done("WebVTTSampleEntryBox", a, trace); | |
| return GF_OK; | |
| } | |
| #endif /* GPAC_DISABLE_ISOM_DUMP */ | |
| #endif /*GPAC_DISABLE_ISOM*/ | |
| typedef enum { | |
| WEBVTT_PARSER_STATE_WAITING_SIGNATURE, | |
| WEBVTT_PARSER_STATE_WAITING_HEADER, | |
| WEBVTT_PARSER_STATE_WAITING_CUE, | |
| WEBVTT_PARSER_STATE_WAITING_CUE_TIMESTAMP, | |
| WEBVTT_PARSER_STATE_WAITING_CUE_PAYLOAD | |
| } GF_WebVTTParserState; | |
| struct _webvtt_parser { | |
| GF_WebVTTParserState state; | |
| Bool is_init, is_srt, suspend, is_eof, prev_line_empty, in_comment; | |
| char *comment_text; | |
| /* List of non-overlapping GF_WebVTTSample */ | |
| GF_List *samples; | |
| FILE **vtt_in; | |
| s32 unicode_type; | |
| u64 last_duration; | |
| void *user; | |
| GF_Err (*report_message)(void *, GF_Err, char *, const char *); | |
| void (*on_header_parsed)(void *, const char *); | |
| void (*on_sample_parsed)(void *, GF_WebVTTSample *); | |
| void (*on_cue_read)(void *, GF_WebVTTCue *); | |
| }; | |
| #if !defined(GPAC_DISABLE_MEDIA_IMPORT) || !defined(GPAC_DISABLE_ISOM) | |
| static Bool gf_webvtt_timestamp_is_zero(GF_WebVTTTimestamp *ts) | |
| { | |
| return (ts->hour == 0 && ts->min == 0 && ts->sec == 0 && ts->ms == 0) ? GF_TRUE : GF_FALSE; | |
| } | |
| #endif | |
| #ifndef GPAC_DISABLE_MEDIA_IMPORT | |
| static Bool gf_webvtt_timestamp_greater(GF_WebVTTTimestamp *ts1, GF_WebVTTTimestamp *ts2) | |
| { | |
| u64 t_ts1 = (60 * 60 * ts1->hour + 60 * ts1->min + ts1->sec) * 1000 + ts1->ms; | |
| u64 t_ts2 = (60 * 60 * ts2->hour + 60 * ts2->min + ts2->sec) * 1000 + ts2->ms; | |
| return (t_ts1 >= t_ts2) ? GF_TRUE : GF_FALSE; | |
| } | |
| /* mark the overlapped cue in the previous sample as split */ | |
| /* duplicate the cue, mark it as split and adjust its timing */ | |
| /* adjust the end of the overlapped cue in the previous sample */ | |
| static GF_WebVTTCue *gf_webvtt_cue_split_at(GF_WebVTTCue *cue, GF_WebVTTTimestamp *time) | |
| { | |
| GF_WebVTTCue *dup_cue; | |
| cue->split = GF_TRUE; | |
| cue->orig_start = cue->start; | |
| cue->orig_end = cue->end; | |
| GF_SAFEALLOC(dup_cue, GF_WebVTTCue); | |
| if (!dup_cue) return NULL; | |
| dup_cue->split = GF_TRUE; | |
| if (time) dup_cue->start = *time; | |
| dup_cue->end = cue->end; | |
| dup_cue->orig_start = cue->orig_start; | |
| dup_cue->orig_end = cue->orig_end; | |
| dup_cue->id = gf_strdup((cue->id ? cue->id : "")); | |
| dup_cue->settings = gf_strdup((cue->settings ? cue->settings : "")); | |
| dup_cue->text = gf_strdup((cue->text ? cue->text : "")); | |
| if (time) cue->end = *time; | |
| return dup_cue; | |
| } | |
| static GF_Err gf_webvtt_cue_add_property(GF_WebVTTCue *cue, GF_WebVTTCuePropertyType type, char *text_data, u32 text_len) | |
| { | |
| char **prop = NULL; | |
| u32 len; | |
| if (!cue) return GF_BAD_PARAM; | |
| if (!text_len) return GF_OK; | |
| switch(type) | |
| { | |
| case WEBVTT_ID: | |
| prop = &cue->id; | |
| break; | |
| case WEBVTT_SETTINGS: | |
| prop = &cue->settings; | |
| break; | |
| case WEBVTT_PAYLOAD: | |
| prop = &cue->text; | |
| break; | |
| case WEBVTT_POSTCUE_TEXT: | |
| prop = &cue->post_text; | |
| break; | |
| case WEBVTT_PRECUE_TEXT: | |
| prop = &cue->pre_text; | |
| break; | |
| } | |
| if (*prop) { | |
| len = (u32) strlen(*prop); | |
| (*prop) = (char*)gf_realloc((*prop), sizeof(char) * (len + text_len + 1) ); | |
| memcpy((*prop) + len, text_data, text_len); | |
| (*prop)[len+text_len] = 0; | |
| } else { | |
| *prop = gf_strdup(text_data); | |
| } | |
| return GF_OK; | |
| } | |
| static GF_WebVTTCue *gf_webvtt_cue_new() | |
| { | |
| GF_WebVTTCue *cue; | |
| GF_SAFEALLOC(cue, GF_WebVTTCue); | |
| return cue; | |
| } | |
| GF_EXPORT | |
| void gf_webvtt_cue_del(GF_WebVTTCue * cue) | |
| { | |
| if (!cue) return; | |
| if (cue->id) gf_free(cue->id); | |
| if (cue->settings) gf_free(cue->settings); | |
| if (cue->text) gf_free(cue->text); | |
| if (cue->pre_text) gf_free(cue->pre_text); | |
| if (cue->post_text) gf_free(cue->post_text); | |
| gf_free(cue); | |
| } | |
| static GF_WebVTTSample *gf_webvtt_sample_new() | |
| { | |
| GF_WebVTTSample *samp; | |
| GF_SAFEALLOC(samp, GF_WebVTTSample); | |
| if (!samp) return NULL; | |
| samp->cues = gf_list_new(); | |
| return samp; | |
| } | |
| u64 gf_webvtt_sample_get_start(GF_WebVTTSample * samp) | |
| { | |
| return samp->start; | |
| } | |
| u64 gf_webvtt_sample_get_end(GF_WebVTTSample * samp) | |
| { | |
| return samp->end; | |
| } | |
| void gf_webvtt_sample_del(GF_WebVTTSample * samp) | |
| { | |
| while (gf_list_count(samp->cues)) { | |
| GF_WebVTTCue *cue = (GF_WebVTTCue *)gf_list_get(samp->cues, 0); | |
| gf_list_rem(samp->cues, 0); | |
| gf_webvtt_cue_del(cue); | |
| } | |
| gf_list_del(samp->cues); | |
| gf_free(samp); | |
| } | |
| GF_WebVTTParser *gf_webvtt_parser_new() | |
| { | |
| GF_WebVTTParser *parser; | |
| GF_SAFEALLOC(parser, GF_WebVTTParser); | |
| if (!parser) return NULL; | |
| parser->samples = gf_list_new(); | |
| return parser; | |
| } | |
| extern s32 gf_text_get_utf_type(FILE *in_src); | |
| GF_Err gf_webvtt_parser_init(GF_WebVTTParser *parser, FILE **vtt_file, s32 unicode_type, Bool is_srt, | |
| void *user, GF_Err (*report_message)(void *, GF_Err, char *, const char *), | |
| void (*on_sample_parsed)(void *, GF_WebVTTSample *), | |
| void (*on_header_parsed)(void *, const char *)) | |
| { | |
| if (!parser) return GF_BAD_PARAM; | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_SIGNATURE; | |
| #ifdef GPAC_ENABLE_COVERAGE | |
| if (gf_sys_is_cov_mode()) { | |
| gf_webvtt_parser_restart(parser); | |
| } | |
| #endif | |
| parser->is_srt = is_srt; | |
| if (is_srt) | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_CUE; | |
| parser->vtt_in = vtt_file; | |
| parser->unicode_type = unicode_type; | |
| parser->user = user; | |
| parser->report_message = report_message; | |
| parser->on_sample_parsed = on_sample_parsed; | |
| parser->on_header_parsed = on_header_parsed; | |
| return GF_OK; | |
| } | |
| void gf_webvtt_parser_suspend(GF_WebVTTParser *vttparser) | |
| { | |
| vttparser->suspend = GF_TRUE; | |
| } | |
| void gf_webvtt_parser_restart(GF_WebVTTParser *parser) | |
| { | |
| if (!parser || !parser->vtt_in || !*(parser->vtt_in)) return; | |
| gf_fseek(*parser->vtt_in, 0, SEEK_SET); | |
| parser->last_duration = 0; | |
| while (gf_list_count(parser->samples)) { | |
| gf_webvtt_sample_del((GF_WebVTTSample *)gf_list_get(parser->samples, 0)); | |
| gf_list_rem(parser->samples, 0); | |
| } | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_SIGNATURE; | |
| } | |
| void gf_webvtt_parser_reset(GF_WebVTTParser *parser) | |
| { | |
| if (!parser) return; | |
| while (gf_list_count(parser->samples)) { | |
| gf_webvtt_sample_del((GF_WebVTTSample *)gf_list_get(parser->samples, 0)); | |
| gf_list_rem(parser->samples, 0); | |
| } | |
| if (parser->comment_text) gf_free(parser->comment_text); | |
| parser->comment_text = NULL; | |
| parser->prev_line_empty = GF_FALSE; | |
| parser->in_comment = GF_FALSE; | |
| parser->last_duration = 0; | |
| parser->on_header_parsed = NULL; | |
| parser->on_sample_parsed = NULL; | |
| parser->report_message = NULL; | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_SIGNATURE; | |
| parser->unicode_type = 0; | |
| parser->user = NULL; | |
| parser->vtt_in = NULL; | |
| } | |
| void gf_webvtt_parser_del(GF_WebVTTParser *parser) | |
| { | |
| if (parser) { | |
| gf_webvtt_parser_reset(parser); | |
| gf_list_del(parser->samples); | |
| gf_free(parser); | |
| } | |
| } | |
| #if 0 | |
| u64 gf_webvtt_parser_last_duration(GF_WebVTTParser *parser) | |
| { | |
| return parser ? parser->last_duration : 0; | |
| } | |
| #endif | |
| static GF_Err gf_webvtt_add_cue_to_samples(GF_WebVTTParser *parser, GF_List *samples, GF_WebVTTCue *cue) | |
| { | |
| s32 i; | |
| u64 cue_start; | |
| u64 cue_end; | |
| u64 sample_end; | |
| if (!cue) | |
| return GF_BAD_PARAM; | |
| cue_start = gf_webvtt_timestamp_get(&cue->start); | |
| cue_end = gf_webvtt_timestamp_get(&cue->end); | |
| sample_end = cue_start; | |
| if (!parser->is_init) { | |
| sample_end = 0; // samples start at ts zero | |
| parser->is_init = GF_TRUE; | |
| } | |
| /* samples in the samples list are contiguous: sample(n)->start == sample(n-1)->end */ | |
| for (i = 0; i < (s32)gf_list_count(samples); i++) { | |
| GF_WebVTTSample *sample; | |
| sample = (GF_WebVTTSample *)gf_list_get(samples, i); | |
| /* save the sample end in case there are no more samples to test */ | |
| sample_end = sample->end; | |
| if (cue_start < sample->start) { | |
| /* cues must be ordered according to their start time, so drop the cue */ | |
| gf_webvtt_cue_del(cue); | |
| GF_LOG(GF_LOG_ERROR, GF_LOG_CONTAINER, ("[VTT] Cue not in order, broken file\n")); | |
| return GF_BAD_PARAM; | |
| } else if (cue_start == sample->start && cue_end == sample->end) { | |
| /* if the timing of the new cue matches the sample, no need to split, add the cue to the sample */ | |
| gf_list_add(sample->cues, cue); | |
| /* the cue does not need to processed further */ | |
| return GF_OK; | |
| } else if (cue_start >= sample->end) { | |
| /* flush the current sample */ | |
| gf_list_del_item(samples, sample); | |
| parser->on_sample_parsed(parser->user, sample); | |
| sample = NULL; | |
| i--; | |
| /* process the cue with next sample (if any) or create a new sample */ | |
| continue; | |
| } else { | |
| u32 j; | |
| if (cue_start > sample->start) { | |
| /* create a new sample, insert it after the current one */ | |
| GF_WebVTTSample *new_sample = gf_webvtt_sample_new(); | |
| new_sample->start = cue_start; | |
| new_sample->end = sample->end; | |
| gf_list_insert(samples, new_sample, i+1); | |
| /* split the cues from the old sample into the new one */ | |
| for (j = 0; j < gf_list_count(sample->cues); j++) { | |
| GF_WebVTTCue *old_cue = (GF_WebVTTCue *)gf_list_get(sample->cues, j); | |
| GF_WebVTTCue *new_cue = gf_webvtt_cue_split_at(old_cue, &cue->start); | |
| gf_list_add(new_sample->cues, new_cue); | |
| } | |
| /* adjust the end of the old sample and flush it */ | |
| sample->end = cue_start; | |
| gf_list_del_item(samples, sample); | |
| parser->on_sample_parsed(parser->user, sample); | |
| sample = NULL; | |
| i--; | |
| /* process the cue again with this new sample */ | |
| continue; | |
| } | |
| if (cue_end > sample->end) { | |
| /* the cue is longer than the sample, we split the cue, add one part to the current sample | |
| and reevaluate with the last part of the cue */ | |
| GF_WebVTTCue *old_cue = (GF_WebVTTCue *)gf_list_get(sample->cues, 0); | |
| GF_WebVTTCue *new_cue = gf_webvtt_cue_split_at(cue, &old_cue->end); | |
| gf_list_add(sample->cues, cue); | |
| cue = new_cue; | |
| cue_start = sample->end; | |
| /* cue_end unchanged */ | |
| /* process the remaining part of the cue (i.e. the new cue) with the other samples */ | |
| continue; | |
| } else { /* cue_end < sample->end */ | |
| GF_WebVTTSample *new_sample = gf_webvtt_sample_new(); | |
| new_sample->start = cue_end; | |
| new_sample->end = sample->end; | |
| gf_list_insert(samples, new_sample, i+1); | |
| for (j = 0; j < gf_list_count(sample->cues); j++) { | |
| GF_WebVTTCue *old_cue = (GF_WebVTTCue *)gf_list_get(sample->cues, j); | |
| GF_WebVTTCue *new_cue = gf_webvtt_cue_split_at(old_cue, &cue->end); | |
| gf_list_add(new_sample->cues, new_cue); | |
| } | |
| gf_list_add(sample->cues, cue); | |
| sample->end = new_sample->start; | |
| /* done with this cue */ | |
| return GF_OK; | |
| } | |
| } | |
| } | |
| /* (a part of) the cue remains (was not overlapping) */ | |
| if (cue_start > sample_end) { | |
| /* if the new cue start is greater than the last sample end, | |
| create an empty sample to fill the gap, flush it */ | |
| GF_WebVTTSample *esample = gf_webvtt_sample_new(); | |
| esample->start = sample_end; | |
| esample->end = cue_start; | |
| parser->on_sample_parsed(parser->user, esample); | |
| } | |
| /* if the cue has not been added to a sample, create a new sample for it */ | |
| { | |
| GF_WebVTTSample *sample; | |
| sample = gf_webvtt_sample_new(); | |
| gf_list_add(samples, sample); | |
| sample->start = cue_start; | |
| sample->end = cue_end; | |
| gf_list_add(sample->cues, cue); | |
| } | |
| return GF_OK; | |
| } | |
| #define REM_TRAIL_MARKS(__str, __sep) while (1) { \ | |
| u32 _len = (u32) strlen(__str); \ | |
| if (!_len) break; \ | |
| _len--; \ | |
| if (strchr(__sep, __str[_len])) { \ | |
| had_marks = GF_TRUE; \ | |
| __str[_len] = 0; \ | |
| } else break; \ | |
| } | |
| extern char *gf_text_get_utf8_line(char *szLine, u32 lineSize, FILE *txt_in, s32 unicode_type, Bool *in_progress); | |
| GF_Err gf_webvtt_parse_timestamp(GF_WebVTTParser *parser, GF_WebVTTTimestamp *ts, const char *line) | |
| { | |
| u32 len; | |
| u32 pos; | |
| u32 pos2; | |
| u32 value1; | |
| u32 value2; | |
| u32 value3; | |
| u32 value4; | |
| Bool is_hour = GF_FALSE; | |
| if (!ts || !line) return GF_BAD_PARAM; | |
| len = (u32) strlen(line); | |
| if (!len) return GF_BAD_PARAM; | |
| pos = 0; | |
| if (!(line[pos] >= '0' && line[pos] <= '9')) return GF_BAD_PARAM; | |
| value1 = 0; | |
| while (pos < len && line[pos] >= '0' && line[pos] <= '9') { | |
| value1 = value1*10 + (line[pos]-'0'); | |
| pos++; | |
| } | |
| if (pos>2 || value1>59) { | |
| is_hour = GF_TRUE; | |
| } | |
| if (pos == len || line[pos] != ':') { | |
| return GF_BAD_PARAM; | |
| } else { | |
| pos++; | |
| } | |
| value2 = 0; | |
| pos2 = 0; | |
| while (pos < len && line[pos] >= '0' && line[pos] <= '9') { | |
| value2 = value2*10 + (line[pos]-'0'); | |
| pos++; | |
| pos2++; | |
| if (pos2 > 2) return GF_BAD_PARAM; | |
| } | |
| if (is_hour || (pos < len && line[pos] == ':')) { | |
| if (pos == len || line[pos] != ':') { | |
| return GF_BAD_PARAM; | |
| } else { | |
| pos++; | |
| pos2 = 0; | |
| value3 = 0; | |
| while (pos < len && line[pos] >= '0' && line[pos] <= '9') { | |
| value3 = value3*10 + (line[pos]-'0'); | |
| pos++; | |
| pos2++; | |
| if (pos2 > 2) return GF_BAD_PARAM; | |
| } | |
| } | |
| } else { | |
| value3 = value2; | |
| value2 = value1; | |
| value1 = 0; | |
| } | |
| /* checking SRT syntax for timestamp with , */ | |
| if (pos == len || (!parser->is_srt && line[pos] != '.') || (parser->is_srt && line[pos] != ',')) { | |
| return GF_BAD_PARAM; | |
| } else { | |
| pos++; | |
| } | |
| pos2 = 0; | |
| value4 = 0; | |
| while (pos < len && line[pos] >= '0' && line[pos] <= '9') { | |
| value4 = value4*10 + (line[pos]-'0'); | |
| pos++; | |
| pos2++; | |
| if (pos2 > 4) return GF_BAD_PARAM; | |
| } | |
| if (value2>59 || value3 > 59) return GF_BAD_PARAM; | |
| ts->hour = value1; | |
| ts->min = value2; | |
| ts->sec = value3; | |
| ts->ms = value4; | |
| return GF_OK; | |
| } | |
| #define SKIP_WHITESPACE \ | |
| while (pos < len && (line[pos] == ' ' || line[pos] == '\t' || \ | |
| line[pos] == '\r' || line[pos] == '\f' || line[pos] == '\n')) pos++; | |
| GF_Err gf_webvtt_parser_parse_timings_settings(GF_WebVTTParser *parser, GF_WebVTTCue *cue, char *line, u32 len) | |
| { | |
| GF_Err e; | |
| char *timestamp_string; | |
| u32 pos; | |
| pos = 0; | |
| if (!cue || !line || !len) return GF_BAD_PARAM; | |
| SKIP_WHITESPACE | |
| timestamp_string = line + pos; | |
| while (pos < len && line[pos] != ' ' && line[pos] != '\t') pos++; | |
| if (pos == len) { | |
| e = GF_CORRUPTED_DATA; | |
| parser->report_message(parser->user, e, "Error scanning WebVTT cue timing in", line); | |
| return e; | |
| } | |
| line[pos] = 0; | |
| e = gf_webvtt_parse_timestamp(parser, &cue->start, timestamp_string); | |
| if (e) { | |
| parser->report_message(parser->user, e, "Bad VTT timestamp formatting", timestamp_string); | |
| return e; | |
| } | |
| line[pos] = ' '; | |
| SKIP_WHITESPACE | |
| if (pos == len) { | |
| e = GF_CORRUPTED_DATA; | |
| parser->report_message(parser->user, e, "Error scanning WebVTT cue timing", line); | |
| return e; | |
| } | |
| if ( (pos+2)>= len || line[pos] != '-' || line[pos+1] != '-' || line[pos+2] != '>') { | |
| e = GF_CORRUPTED_DATA; | |
| parser->report_message(parser->user, e, "Error scanning WebVTT cue timing", line); | |
| return e; | |
| } else { | |
| pos += 3; | |
| SKIP_WHITESPACE | |
| if (pos == len) { | |
| e = GF_CORRUPTED_DATA; | |
| parser->report_message(parser->user, e, "Error scanning WebVTT cue timing in", line); | |
| return e; | |
| } | |
| timestamp_string = line + pos; | |
| while (pos < len && line[pos] != ' ' && line[pos] != '\t') pos++; | |
| if (pos < len) { | |
| line[pos] = 0; | |
| } | |
| e = gf_webvtt_parse_timestamp(parser, &cue->end, timestamp_string); | |
| if (e) { | |
| parser->report_message(parser->user, e, "Bad VTT timestamp formatting", timestamp_string); | |
| return e; | |
| } | |
| if (pos < len) { | |
| line[pos] = ' '; | |
| } | |
| SKIP_WHITESPACE | |
| if (pos < len) { | |
| char *settings = line + pos; | |
| e = gf_webvtt_cue_add_property(cue, WEBVTT_SETTINGS, settings, (u32) strlen(settings)); | |
| } | |
| if (!gf_webvtt_timestamp_greater(&cue->end, &cue->start)) { | |
| parser->report_message(parser->user, e, "Bad VTT timestamps, end smaller than start", timestamp_string); | |
| cue->end = cue->start; | |
| cue->end.ms += 1; | |
| return GF_NON_COMPLIANT_BITSTREAM; | |
| } | |
| } | |
| return e; | |
| } | |
| GF_Err gf_webvtt_parser_parse_internal(GF_WebVTTParser *parser, GF_WebVTTCue *cue, FILE *ext_file, Bool is_eof) | |
| { | |
| char szLine[2049]; | |
| char *sOK; | |
| u32 len; | |
| GF_Err e; | |
| char *prevLine = NULL; | |
| char *header = NULL; | |
| u32 header_len = 0; | |
| Bool had_marks = GF_FALSE; | |
| u32 is_resume = ext_file ? 1 : 9; | |
| if (!parser) return GF_BAD_PARAM; | |
| parser->suspend = GF_FALSE; | |
| if (parser->is_srt) { | |
| parser->on_header_parsed(parser->user, "WEBVTT\n"); | |
| } | |
| szLine[2048]=0; | |
| while (!parser->is_eof) { | |
| Bool in_progress = is_eof; | |
| if (!cue && parser->suspend) | |
| break; | |
| sOK = gf_text_get_utf8_line(szLine, 2048, ext_file ? ext_file : *parser->vtt_in, parser->unicode_type, &in_progress); | |
| if (in_progress) { | |
| parser->suspend = GF_TRUE; | |
| if (ext_file && cue && !cue->text) { | |
| gf_webvtt_add_cue_to_samples(parser, parser->samples, cue); | |
| } | |
| break; | |
| } | |
| had_marks = GF_FALSE; | |
| REM_TRAIL_MARKS(szLine, "\r\n") | |
| len = (u32) strlen(szLine); | |
| if (parser->is_srt && sOK && !strncmp(sOK, "WEBVTT", 6)) { | |
| parser->is_srt = GF_FALSE; | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_SIGNATURE; | |
| } | |
| switch (parser->state) { | |
| case WEBVTT_PARSER_STATE_WAITING_SIGNATURE: | |
| if (!sOK || len < 6 || strnicmp(szLine, "WEBVTT", 6) || (len > 6 && szLine[6] != ' ' && szLine[6] != '\t')) { | |
| e = GF_CORRUPTED_DATA; | |
| parser->report_message(parser->user, e, "Bad WEBVTT file signature", szLine); | |
| goto exit; | |
| } else { | |
| if (had_marks) { | |
| szLine[len] = '\n'; | |
| len++; | |
| } | |
| header = gf_strdup(szLine); | |
| header_len = len; | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_HEADER; | |
| } | |
| break; /* proceed to next line */ | |
| case WEBVTT_PARSER_STATE_WAITING_HEADER: | |
| if (prevLine) { | |
| u32 prev_len = (u32) strlen(prevLine); | |
| header = (char *)gf_realloc(header, header_len + prev_len + 1); | |
| memcpy(header+header_len, prevLine, prev_len+1); | |
| header_len += prev_len; | |
| gf_free(prevLine); | |
| prevLine = NULL; | |
| } | |
| if (sOK && len) { | |
| if (strstr(szLine, "-->")) { | |
| parser->on_header_parsed(parser->user, header); | |
| /* continue to the next state without breaking */ | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_CUE_TIMESTAMP; | |
| /* no break, continue to the next state*/ | |
| } else { | |
| if (had_marks) { | |
| szLine[len] = '\n'; | |
| len++; | |
| } | |
| prevLine = gf_strdup(szLine); | |
| break; /* proceed to next line */ | |
| } | |
| } else { | |
| parser->on_header_parsed(parser->user, header); | |
| if (header) gf_free(header); | |
| header = NULL; | |
| if (!sOK) { | |
| /* end of file, parsing is done */ | |
| parser->is_eof = GF_TRUE; | |
| break; | |
| } else { | |
| /* empty line means end of header */ | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_CUE; | |
| /* no break, continue to the next state*/ | |
| } | |
| } | |
| case WEBVTT_PARSER_STATE_WAITING_CUE: | |
| if (sOK && len) { | |
| if (strstr(szLine, "-->")) { | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_CUE_TIMESTAMP; | |
| /* continue to the next state without breaking */ | |
| } else { | |
| /* discard the previous line */ | |
| /* should we do something with it ? callback ?*/ | |
| if (prevLine && !parser->in_comment) { | |
| gf_free(prevLine); | |
| prevLine = NULL; | |
| } | |
| /* save this new line */ | |
| if (had_marks) { | |
| szLine[len] = '\n'; | |
| len++; | |
| } | |
| if (prevLine) | |
| gf_dynstrcat(&prevLine, szLine, NULL); | |
| else | |
| prevLine = gf_strdup(szLine); | |
| if (parser->prev_line_empty && (!strncmp(prevLine, "NOTE ", 5) || !strcmp(prevLine, "NOTE\n"))) | |
| parser->in_comment = GF_TRUE; | |
| /* stay in the same state */ | |
| break; | |
| } | |
| } else { | |
| /* discard the previous line */ | |
| /* should we do something with it ? callback ?*/ | |
| if (parser->in_comment) { | |
| if (!parser->comment_text) { | |
| parser->comment_text = prevLine; | |
| prevLine = NULL; | |
| } else { | |
| gf_dynstrcat(&parser->comment_text, prevLine, "\n"); | |
| gf_free(prevLine); | |
| prevLine = NULL; | |
| } | |
| parser->in_comment = GF_FALSE; | |
| } | |
| if (prevLine) { | |
| gf_free(prevLine); | |
| prevLine = NULL; | |
| } | |
| if (!sOK) { | |
| parser->is_eof = GF_TRUE; | |
| break; | |
| } else { | |
| /* remove empty lines and stay in the same state */ | |
| break; | |
| } | |
| } | |
| case WEBVTT_PARSER_STATE_WAITING_CUE_TIMESTAMP: | |
| if (sOK && len) { | |
| if (cue == NULL) { | |
| cue = gf_webvtt_cue_new(); | |
| } | |
| if (prevLine) { | |
| gf_webvtt_cue_add_property(cue, WEBVTT_ID, prevLine, (u32) strlen(prevLine)); | |
| gf_free(prevLine); | |
| prevLine = NULL; | |
| } | |
| if (parser->comment_text) { | |
| gf_webvtt_cue_add_property(cue, WEBVTT_PRECUE_TEXT, parser->comment_text, (u32) strlen(parser->comment_text)); | |
| gf_free(parser->comment_text); | |
| parser->comment_text = NULL; | |
| } | |
| e = gf_webvtt_parser_parse_timings_settings(parser, cue, szLine, len); | |
| if (e) { | |
| if (cue) gf_webvtt_cue_del(cue); | |
| cue = NULL; | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_CUE; | |
| } else { | |
| // start = (u32)gf_webvtt_timestamp_get(&cue->start); | |
| // end = (u32)gf_webvtt_timestamp_get(&cue->end); | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_CUE_PAYLOAD; | |
| } | |
| } else { | |
| /* not possible */ | |
| gf_assert(0); | |
| if (header) gf_free(header); | |
| return GF_NON_COMPLIANT_BITSTREAM; | |
| } | |
| break; | |
| case WEBVTT_PARSER_STATE_WAITING_CUE_PAYLOAD: | |
| if ((is_resume==1) && !cue) { | |
| GF_WebVTTSample *sample = (GF_WebVTTSample *)gf_list_last(parser->samples); | |
| cue = sample ? gf_list_last(sample->cues) : NULL; | |
| is_resume = 2; | |
| } | |
| if (sOK && len) { | |
| if (!strncmp(szLine, "NOTE ", 5)) { | |
| if (had_marks) { | |
| szLine[len] = '\n'; | |
| len++; | |
| } | |
| gf_webvtt_cue_add_property(cue, WEBVTT_POSTCUE_TEXT, szLine, len); | |
| parser->in_comment = GF_TRUE; | |
| len = 0; | |
| } else if (parser->in_comment) { | |
| parser->in_comment = GF_FALSE; | |
| } | |
| } | |
| //special case when injecting multiple times the header, ignore it | |
| if (ext_file && !strcmp(szLine, "WEBVTT")) { | |
| } | |
| else if (sOK && len) { | |
| if (had_marks) { | |
| szLine[len] = '\n'; | |
| len++; | |
| } | |
| assert(cue); | |
| gf_webvtt_cue_add_property(cue, parser->in_comment ? WEBVTT_POSTCUE_TEXT : WEBVTT_PAYLOAD, szLine, len); | |
| /* remain in the same state as a cue payload can have multiple lines */ | |
| break; | |
| } else { | |
| /* end of the current cue */ | |
| if (is_resume!=2) { | |
| gf_webvtt_add_cue_to_samples(parser, parser->samples, cue); | |
| } | |
| cue = NULL; | |
| parser->in_comment = GF_FALSE; | |
| if (!sOK) { | |
| parser->is_eof = GF_TRUE; | |
| break; | |
| } else { | |
| /* empty line, move to next cue */ | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_CUE; | |
| break; | |
| } | |
| } | |
| } | |
| parser->prev_line_empty = len ? GF_FALSE : GF_TRUE; | |
| } | |
| if (header) gf_free(header); | |
| header = NULL; | |
| if (parser->suspend) | |
| return GF_OK; | |
| /* no more cues to come, flush everything */ | |
| if (cue) { | |
| gf_webvtt_add_cue_to_samples(parser, parser->samples, cue); | |
| cue = NULL; | |
| } | |
| while (gf_list_count(parser->samples) > 0) { | |
| GF_WebVTTSample *sample = (GF_WebVTTSample *)gf_list_get(parser->samples, 0); | |
| parser->last_duration = (sample->end > sample->start) ? sample->end - sample->start : 0; | |
| gf_list_rem(parser->samples, 0); | |
| parser->on_sample_parsed(parser->user, sample); | |
| } | |
| e = GF_EOS; | |
| exit: | |
| if (cue) gf_webvtt_cue_del(cue); | |
| if (prevLine) gf_free(prevLine); | |
| if (header) gf_free(header); | |
| return e; | |
| } | |
| GF_Err gf_webvtt_parser_parse(GF_WebVTTParser *parser) | |
| { | |
| return gf_webvtt_parser_parse_internal(parser, NULL, NULL, GF_TRUE); | |
| } | |
| GF_Err gf_webvtt_parser_parse_ext(GF_WebVTTParser *parser, FILE *ext_file, Bool in_eos) | |
| { | |
| return gf_webvtt_parser_parse_internal(parser, NULL, ext_file, in_eos); | |
| } | |
| void gf_webvtt_parser_not_done(GF_WebVTTParser *parser) | |
| { | |
| if (parser) parser->is_eof = GF_FALSE; | |
| } | |
| GF_Err gf_webvtt_parser_flush(GF_WebVTTParser *parser) | |
| { | |
| while (gf_list_count(parser->samples) > 0) { | |
| GF_WebVTTSample *sample = (GF_WebVTTSample *)gf_list_get(parser->samples, 0); | |
| parser->last_duration = (sample->end > sample->start) ? sample->end - sample->start : 0; | |
| gf_list_rem(parser->samples, 0); | |
| parser->on_sample_parsed(parser->user, sample); | |
| } | |
| return GF_OK; | |
| } | |
| GF_Err gf_webvtt_parser_parse_payload(GF_WebVTTParser *parser, u64 start, u64 end, const char *vtt_pre, const char *vtt_cueid, const char *vtt_settings) | |
| { | |
| GF_WebVTTCue *cue = gf_webvtt_cue_new(); | |
| parser->state = WEBVTT_PARSER_STATE_WAITING_CUE_PAYLOAD; | |
| gf_webvtt_timestamp_set(&cue->start, start); | |
| gf_webvtt_timestamp_set(&cue->end, end); | |
| parser->is_init = GF_TRUE; | |
| if (vtt_cueid) cue->id = gf_strdup(vtt_cueid); | |
| if (vtt_settings) cue->settings = gf_strdup(vtt_settings); | |
| if (vtt_pre) cue->pre_text = gf_strdup(vtt_pre); | |
| GF_Err e = gf_webvtt_parser_parse_internal(parser, cue, NULL, GF_TRUE); | |
| parser->is_eof = GF_FALSE; | |
| return e; | |
| } | |
| GF_EXPORT | |
| GF_List *gf_webvtt_parse_iso_cues(GF_ISOSample *iso_sample, u64 start, u64 end) | |
| { | |
| return gf_webvtt_parse_cues_from_data(iso_sample->data, iso_sample->dataLength, start, end); | |
| } | |
| GF_EXPORT | |
| GF_List *gf_webvtt_parse_cues_from_data(const u8 *data, u32 dataLength, u64 start, u64 end) | |
| { | |
| GF_List *cues; | |
| GF_WebVTTCue *cue; | |
| GF_VTTCueBox *cuebox; | |
| GF_BitStream *bs; | |
| char *pre_text; | |
| GF_Box *box = NULL; | |
| cue = NULL; | |
| pre_text = NULL; | |
| cues = gf_list_new(); | |
| if (!cues) return NULL; | |
| bs = gf_bs_new((u8 *)data, dataLength, GF_BITSTREAM_READ); | |
| if (!bs) { | |
| gf_list_del(cues); | |
| return NULL; | |
| } | |
| while(gf_bs_available(bs)) | |
| { | |
| GF_Err e; | |
| e = gf_isom_box_parse(&box, bs); | |
| if (e) goto err; | |
| if (box->type == GF_ISOM_BOX_TYPE_VTCC_CUE) { | |
| cuebox = (GF_VTTCueBox *)box; | |
| cue = gf_webvtt_cue_new(); | |
| if (!cue) goto err; | |
| if (pre_text) { | |
| gf_webvtt_cue_add_property(cue, WEBVTT_PRECUE_TEXT, pre_text, (u32) strlen(pre_text)); | |
| gf_free(pre_text); | |
| pre_text = NULL; | |
| } | |
| gf_list_add(cues, cue); | |
| gf_webvtt_timestamp_set(&cue->start, start); | |
| gf_webvtt_timestamp_set(&cue->end, end); | |
| if (cuebox->id) { | |
| gf_webvtt_cue_add_property(cue, WEBVTT_ID, cuebox->id->string, (u32) strlen(cuebox->id->string)); | |
| } | |
| if (cuebox->settings) { | |
| gf_webvtt_cue_add_property(cue, WEBVTT_SETTINGS, cuebox->settings->string, (u32) strlen(cuebox->settings->string)); | |
| } | |
| if (cuebox->payload) { | |
| gf_webvtt_cue_add_property(cue, WEBVTT_PAYLOAD, cuebox->payload->string, (u32) strlen(cuebox->payload->string)); | |
| } | |
| } else if (box->type == GF_ISOM_BOX_TYPE_VTTA) { | |
| GF_StringBox *sbox = (GF_StringBox *)box; | |
| if (cue) { | |
| gf_webvtt_cue_add_property(cue, WEBVTT_POSTCUE_TEXT, sbox->string, (u32) strlen(sbox->string)); | |
| } else { | |
| pre_text = gf_strdup(sbox->string); | |
| } | |
| } | |
| gf_isom_box_del(box); | |
| box = NULL; | |
| } | |
| gf_bs_del(bs); | |
| return cues; | |
| err: | |
| if (box) gf_isom_box_del(box); | |
| while (cues && gf_list_count(cues)) { | |
| GF_WebVTTCue *c = (GF_WebVTTCue *)gf_list_get(cues, 0); | |
| gf_list_rem(cues, 0); | |
| gf_webvtt_cue_del(c); | |
| } | |
| if (cues) gf_list_del(cues); | |
| if (pre_text) gf_free(pre_text); | |
| gf_bs_del(bs); | |
| return NULL; | |
| } | |
| GF_Err gf_webvtt_merge_cues(GF_WebVTTParser *parser, u64 start, GF_List *cues) | |
| { | |
| GF_WebVTTSample *wsample; | |
| GF_WebVTTSample *prev_wsample; | |
| Bool has_continuation_cue = GF_FALSE; | |
| gf_assert(gf_list_count(parser->samples) <= 1); | |
| wsample = gf_webvtt_sample_new(); | |
| wsample->start = start; | |
| prev_wsample = (GF_WebVTTSample *)gf_list_last(parser->samples); | |
| while (gf_list_count(cues)) { | |
| GF_WebVTTCue *cue = (GF_WebVTTCue *)gf_list_get(cues, 0); | |
| gf_list_rem(cues, 0); | |
| /* add the cue to the current sample */ | |
| gf_list_add(wsample->cues, cue); | |
| /* update with the previous sample */ | |
| if (prev_wsample) { | |
| Bool do_del = GF_TRUE; | |
| Bool found = GF_FALSE; | |
| while (!found && gf_list_count(prev_wsample->cues)) { | |
| GF_WebVTTCue *old_cue = (GF_WebVTTCue *)gf_list_get(prev_wsample->cues, 0); | |
| gf_list_rem(prev_wsample->cues, 0); | |
| if ( | |
| ((!cue->id && !old_cue->id) || (old_cue->id && cue->id && !strcmp(old_cue->id, cue->id))) && | |
| ((!cue->settings && !old_cue->settings) || (old_cue->settings && cue->settings && !strcmp(old_cue->settings, cue->settings))) && | |
| ((!cue->text && !old_cue->text) || (old_cue->text && cue->text && !strcmp(old_cue->text, cue->text))) | |
| ) { | |
| /* if it is the same cue, update its start with the initial start */ | |
| cue->start = old_cue->start; | |
| has_continuation_cue = GF_TRUE; | |
| found = GF_TRUE; | |
| if (old_cue->pre_text) { | |
| cue->pre_text = old_cue->pre_text; | |
| old_cue->pre_text = NULL; | |
| } | |
| if (old_cue->post_text) { | |
| cue->post_text = old_cue->post_text; | |
| old_cue->post_text = NULL; | |
| } | |
| } else { | |
| /* finalize the end cue time */ | |
| if (gf_webvtt_timestamp_is_zero(&old_cue->end)) { | |
| gf_webvtt_timestamp_set(&old_cue->end, wsample->start); | |
| } | |
| /* transfer the cue */ | |
| if (!has_continuation_cue) { | |
| /* the cue can be safely serialized while keeping the order */ | |
| parser->on_cue_read(parser->user, old_cue); | |
| } else { | |
| /* keep the cue in the current sample to respect cue start ordering */ | |
| gf_list_add(wsample->cues, old_cue); | |
| do_del = GF_FALSE; | |
| } | |
| } | |
| /* delete the old cue */ | |
| if (do_del) | |
| gf_webvtt_cue_del(old_cue); | |
| } | |
| } | |
| } | |
| /* No cue in the current sample */ | |
| if (prev_wsample) { | |
| while (gf_list_count(prev_wsample->cues)) { | |
| Bool do_del = GF_TRUE; | |
| GF_WebVTTCue *cue = (GF_WebVTTCue *)gf_list_get(prev_wsample->cues, 0); | |
| gf_list_rem(prev_wsample->cues, 0); | |
| /* finalize the end cue time */ | |
| if (gf_webvtt_timestamp_is_zero(&cue->end)) { | |
| gf_webvtt_timestamp_set(&cue->end, wsample->start); | |
| } | |
| /* transfer the cue */ | |
| if (!has_continuation_cue) { | |
| /* the cue can be safely serialized while keeping the order */ | |
| parser->on_cue_read(parser->user, cue); | |
| } else { | |
| /* keep the cue in the current sample to respect cue start ordering */ | |
| gf_list_add(wsample->cues, cue); | |
| do_del = GF_FALSE; | |
| } | |
| if (do_del) | |
| gf_webvtt_cue_del(cue); | |
| } | |
| gf_webvtt_sample_del(prev_wsample); | |
| gf_list_rem_last(parser->samples); | |
| prev_wsample = NULL; | |
| } else { | |
| /* nothing to do */ | |
| } | |
| if (gf_list_count(wsample->cues)) { | |
| gf_list_add(parser->samples, wsample); | |
| } else { | |
| gf_webvtt_sample_del(wsample); | |
| } | |
| return GF_OK; | |
| } | |
| static GF_Err gf_webvtt_parse_iso_sample(GF_WebVTTParser *parser, u32 timescale, GF_ISOSample *iso_sample, u32 duration, Bool merge, Bool box_mode) | |
| { | |
| if (merge) { | |
| u64 start; | |
| u64 end; | |
| GF_List *cues; | |
| start = (iso_sample->DTS * 1000) / timescale; | |
| end = (iso_sample->DTS + duration) * 1000 / timescale; | |
| cues = gf_webvtt_parse_iso_cues(iso_sample, start, end); | |
| gf_webvtt_merge_cues(parser, start, cues); | |
| gf_list_del(cues); | |
| } else { | |
| GF_Err gf_webvtt_dump_iso_sample(FILE *dump, u32 timescale, GF_ISOSample *iso_sample, Bool box_mode); | |
| gf_webvtt_dump_iso_sample((FILE *)parser->user, timescale, iso_sample, box_mode); | |
| } | |
| return GF_OK; | |
| } | |
| #endif //GPAC_DISABLE_MEDIA_IMPORT | |
| void gf_webvtt_timestamp_set(GF_WebVTTTimestamp *ts, u64 value) | |
| { | |
| u64 tmp; | |
| if (!ts) return; | |
| tmp = value; | |
| ts->hour = (u32)(tmp/(3600*1000)); | |
| tmp -= (u64) ts->hour*3600*1000; | |
| ts->min = (u32)(tmp/(60*1000)); | |
| tmp -= (u64) ts->min*60*1000; | |
| ts->sec = (u32)(tmp/1000); | |
| tmp -= (u64) ts->sec*1000; | |
| ts->ms = (u32)tmp; | |
| } | |
| u64 gf_webvtt_timestamp_get(GF_WebVTTTimestamp *ts) | |
| { | |
| if (!ts) return 0; | |
| return (u64)(3600*ts->hour + 60*ts->min + ts->sec)*1000 + ts->ms; | |
| } | |
| void gf_webvtt_timestamp_dump(GF_WebVTTTimestamp *ts, FILE *dump, Bool dump_hour) | |
| { | |
| if (dump_hour || ts->hour != 0) { | |
| gf_fprintf(dump, "%02u:", ts->hour); | |
| } | |
| gf_fprintf(dump, "%02u:%02u.%03u", ts->min, ts->sec, ts->ms); | |
| } | |
| GF_Err gf_webvtt_dump_header_boxed(FILE *dump, const u8 *data, u32 dataLength, u32 *dumpedLength) | |
| { | |
| #ifdef GPAC_DISABLE_ISOM | |
| return GF_NOT_SUPPORTED; | |
| #else | |
| GF_Err e; | |
| GF_Box *box; | |
| GF_StringBox *config; | |
| GF_BitStream *bs; | |
| *dumpedLength = 0; | |
| bs = gf_bs_new((u8 *)data, dataLength, GF_BITSTREAM_READ); | |
| e = gf_isom_box_parse(&box, bs); | |
| if (!box || (box->type != GF_ISOM_BOX_TYPE_VTTC_CONFIG)) { | |
| gf_bs_del(bs); | |
| if (box) gf_isom_box_del(box); | |
| return GF_BAD_PARAM; | |
| } | |
| config = (GF_StringBox *)box; | |
| if (config->string) { | |
| gf_fprintf(dump, "%s", config->string); | |
| *dumpedLength = (u32)strlen(config->string)+1; | |
| } | |
| gf_bs_del(bs); | |
| gf_isom_box_del(box); | |
| return e; | |
| #endif | |
| } | |
| #ifndef GPAC_DISABLE_ISOM | |
| GF_Err gf_webvtt_dump_header(FILE *dump, GF_ISOFile *file, u32 track, Bool box_mode, u32 index) | |
| { | |
| GF_WebVTTSampleEntryBox *wvtt; | |
| wvtt = gf_webvtt_isom_get_description(file, track, index); | |
| if (!wvtt) return GF_BAD_PARAM; | |
| if (box_mode) { | |
| #ifndef GPAC_DISABLE_ISOM_DUMP | |
| gf_isom_box_dump(wvtt, dump); | |
| #else | |
| return GF_NOT_SUPPORTED; | |
| #endif | |
| } else { | |
| gf_fprintf(dump, "%s\n\n", wvtt->config->string); | |
| } | |
| return GF_OK; | |
| } | |
| GF_Err gf_webvtt_dump_iso_sample(FILE *dump, u32 timescale, GF_ISOSample *iso_sample, Bool box_mode) | |
| { | |
| GF_Err e = GF_OK; | |
| GF_BitStream *bs; | |
| GF_Box *box = NULL; | |
| if (box_mode) { | |
| gf_fprintf(dump, "<WebVTTSample decodingTimeStamp=\""LLU"\" compositionTimeStamp=\""LLD"\" RAP=\"%d\" dataLength=\"%d\" >\n", iso_sample->DTS, (s64)iso_sample->DTS + iso_sample->CTS_Offset, iso_sample->IsRAP, iso_sample->dataLength); | |
| } | |
| bs = gf_bs_new(iso_sample->data, iso_sample->dataLength, GF_BITSTREAM_READ); | |
| if (!bs) return GF_OUT_OF_MEM; | |
| while(gf_bs_available(bs)) | |
| { | |
| GF_WebVTTTimestamp ts; | |
| e = gf_isom_box_parse(&box, bs); | |
| if (e) goto err; | |
| if (box_mode) { | |
| #ifndef GPAC_DISABLE_ISOM_DUMP | |
| gf_isom_box_dump(box, dump); | |
| #endif | |
| } else if (box->type == GF_ISOM_BOX_TYPE_VTCC_CUE) { | |
| GF_VTTCueBox *cuebox = (GF_VTTCueBox *)box; | |
| if (cuebox->id) gf_fprintf(dump, "%s", cuebox->id->string); | |
| gf_webvtt_timestamp_set(&ts, (iso_sample->DTS * 1000) / timescale); | |
| gf_webvtt_timestamp_dump(&ts, dump, GF_FALSE); | |
| gf_fprintf(dump, " --> NEXT"); | |
| if (cuebox->settings) gf_fprintf(dump, " %s", cuebox->settings->string); | |
| gf_fprintf(dump, "\n"); | |
| if (cuebox->payload) gf_fprintf(dump, "%s", cuebox->payload->string); | |
| gf_fprintf(dump, "\n"); | |
| } else if (box->type == GF_ISOM_BOX_TYPE_VTTE) { | |
| gf_webvtt_timestamp_set(&ts, (iso_sample->DTS * 1000) / timescale); | |
| gf_webvtt_timestamp_dump(&ts, dump, GF_FALSE); | |
| gf_fprintf(dump, " --> NEXT\n\n"); | |
| } else if (box->type == GF_ISOM_BOX_TYPE_VTTA) { | |
| gf_fprintf(dump, "%s\n\n", ((GF_StringBox *)box)->string); | |
| } | |
| gf_isom_box_del(box); | |
| box = NULL; | |
| } | |
| gf_bs_del(bs); | |
| if (box_mode) { | |
| gf_fprintf(dump, "</WebVTTSample>\n"); | |
| } | |
| return GF_OK; | |
| err: | |
| if (box) gf_isom_box_del(box); | |
| gf_bs_del(bs); | |
| return e; | |
| } | |
| #endif | |
| #ifndef GPAC_DISABLE_ISOM | |
| GF_Err gf_webvtt_parser_finalize(GF_WebVTTParser *parser, u64 duration) | |
| { | |
| GF_WebVTTSample *sample; | |
| gf_assert(gf_list_count(parser->samples) <= 1); | |
| sample = (GF_WebVTTSample *)gf_list_get(parser->samples, 0); | |
| if (sample) { | |
| while (gf_list_count(sample->cues)) { | |
| GF_WebVTTCue *cue = (GF_WebVTTCue *)gf_list_get(sample->cues, 0); | |
| gf_list_rem(sample->cues, 0); | |
| if (gf_webvtt_timestamp_is_zero(&cue->end)) { | |
| gf_webvtt_timestamp_set(&cue->end, duration); | |
| } | |
| parser->on_cue_read(parser->user, cue); | |
| gf_webvtt_cue_del(cue); | |
| } | |
| gf_webvtt_sample_del(sample); | |
| gf_list_rem(parser->samples, 0); | |
| } | |
| return GF_OK; | |
| } | |
| #ifndef GPAC_DISABLE_MEDIA_IMPORT | |
| static void gf_webvtt_dump_cue(void *user, GF_WebVTTCue *cue) | |
| { | |
| FILE *dump = (FILE *)user; | |
| if (!cue || !dump) return; | |
| if (cue->pre_text) { | |
| gf_fprintf(dump, "%s", cue->pre_text); | |
| gf_fprintf(dump, "\n"); | |
| gf_fprintf(dump, "\n"); | |
| } | |
| if (cue->id) gf_fprintf(dump, "%s\n", cue->id); | |
| if (cue->start.hour || cue->end.hour) { | |
| gf_webvtt_timestamp_dump(&cue->start, dump, GF_TRUE); | |
| gf_fprintf(dump, " --> "); | |
| gf_webvtt_timestamp_dump(&cue->end, dump, GF_TRUE); | |
| } else { | |
| gf_webvtt_timestamp_dump(&cue->start, dump, GF_FALSE); | |
| gf_fprintf(dump, " --> "); | |
| gf_webvtt_timestamp_dump(&cue->end, dump, GF_FALSE); | |
| } | |
| if (cue->settings) { | |
| gf_fprintf(dump, " %s", cue->settings); | |
| } | |
| gf_fprintf(dump, "\n"); | |
| if (cue->text) gf_fprintf(dump, "%s", cue->text); | |
| gf_fprintf(dump, "\n"); | |
| gf_fprintf(dump, "\n"); | |
| if (cue->post_text) { | |
| gf_fprintf(dump, "%s", cue->post_text); | |
| gf_fprintf(dump, "\n"); | |
| gf_fprintf(dump, "\n"); | |
| } | |
| } | |
| #endif | |
| //unused | |
| #if 0 | |
| static GF_Err gf_webvtt_dump_cues(FILE *dump, GF_List *cues) | |
| { | |
| u32 i; | |
| for (i = 0; i < gf_list_count(cues); i++) { | |
| GF_WebVTTCue *cue = (GF_WebVTTCue *)gf_list_get(cues, i); | |
| gf_webvtt_dump_cue(dump, cue); | |
| } | |
| return GF_OK; | |
| } | |
| GF_Err gf_webvtt_dump_sample(FILE *dump, GF_WebVTTSample *samp) | |
| { | |
| gf_fprintf(stdout, "NOTE New WebVTT Sample ("LLD"-"LLD")\n\n", samp->start, samp->end); | |
| return gf_webvtt_dump_cues(dump, samp->cues); | |
| } | |
| #endif | |
| void gf_webvtt_parser_cue_callback(GF_WebVTTParser *parser, void (*on_cue_read)(void *, GF_WebVTTCue *), void *udta) | |
| { | |
| parser->on_cue_read = on_cue_read; | |
| parser->user = udta; | |
| } | |
| #ifndef GPAC_DISABLE_MEDIA_EXPORT | |
| GF_EXPORT | |
| GF_Err gf_webvtt_dump_iso_track(GF_MediaExporter *dumper, u32 track, Bool merge, Bool box_dump) | |
| { | |
| #ifdef GPAC_DISABLE_MEDIA_IMPORT | |
| return GF_NOT_SUPPORTED; | |
| #else | |
| GF_Err e; | |
| u32 i; | |
| u32 count; | |
| u32 timescale; | |
| FILE *out; | |
| u32 di; | |
| u64 duration; | |
| GF_WebVTTParser *parser; | |
| out = (dumper->dump_file ? dumper->dump_file : stdout); | |
| if (!out) return GF_IO_ERR;// gf_export_message(dumper, GF_IO_ERR, "Error opening %s for writing - check disk access & permissions", szName); | |
| parser = gf_webvtt_parser_new(); | |
| parser->user = out; | |
| parser->on_cue_read = gf_webvtt_dump_cue; | |
| if (box_dump) | |
| gf_fprintf(out, "<WebVTTTrack trackID=\"%d\">\n", gf_isom_get_track_id(dumper->file, track) ); | |
| e = gf_webvtt_dump_header(out, dumper->file, track, box_dump, 1); | |
| if (e) goto exit; | |
| timescale = gf_isom_get_media_timescale(dumper->file, track); | |
| count = gf_isom_get_sample_count(dumper->file, track); | |
| for (i=0; i<count; i++) { | |
| u32 sdur; | |
| GF_ISOSample *samp = gf_isom_get_sample(dumper->file, track, i+1, &di); | |
| if (!samp) { | |
| e = gf_isom_last_error(dumper->file); | |
| goto exit; | |
| } | |
| sdur = gf_isom_get_sample_duration(dumper->file, track, i+1); | |
| e = gf_webvtt_parse_iso_sample(parser, timescale, samp, sdur, merge, box_dump); | |
| if (e) { | |
| goto exit; | |
| } | |
| gf_isom_sample_del(&samp); | |
| } | |
| duration = gf_isom_get_media_duration(dumper->file, track); | |
| gf_webvtt_parser_finalize(parser, duration); | |
| if (box_dump) | |
| gf_fprintf(out, "</WebVTTTrack>\n"); | |
| exit: | |
| gf_webvtt_parser_del(parser); | |
| return e; | |
| #endif | |
| } | |
| #endif /*GPAC_DISABLE_MEDIA_EXPORT*/ | |
| #endif /*GPAC_DISABLE_ISOM_DUMP*/ | |
| #endif /*GPAC_DISABLE_VTT*/ |