[FFmpeg-devel] [PATCH 1/2] avformat/ttml: Add demuxer
TheDaChicken
steve.rock.pet at gmail.com
Fri Mar 17 03:55:26 EET 2023
From: Aidan <steve.rock.pet at gmail.com>
Requested: #4859
Signed-off-by: Aidan Vaughan <steve.rock.pet at gmail.com>
---
libavformat/Makefile | 1 +
libavformat/allformats.c | 1 +
libavformat/ttmldec.c | 432 +++++++++++++++++++++++++++++++++++++++
libavformat/version.h | 2 +-
4 files changed, 435 insertions(+), 1 deletion(-)
create mode 100644 libavformat/ttmldec.c
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 47bbbbfb2a..5f29e05618 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -576,6 +576,7 @@ OBJS-$(CONFIG_TRUEHD_DEMUXER) += rawdec.o mlpdec.o
OBJS-$(CONFIG_TRUEHD_MUXER) += rawenc.o
OBJS-$(CONFIG_TTA_DEMUXER) += tta.o apetag.o img2.o
OBJS-$(CONFIG_TTA_MUXER) += ttaenc.o apetag.o img2.o
+OBJS-$(CONFIG_TTML_DEMUXER) += ttmldec.o
OBJS-$(CONFIG_TTML_MUXER) += ttmlenc.o
OBJS-$(CONFIG_TTY_DEMUXER) += tty.o sauce.o
OBJS-$(CONFIG_TY_DEMUXER) += ty.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index cb5b69e9cd..0280592f7b 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -459,6 +459,7 @@ extern const AVInputFormat ff_truehd_demuxer;
extern const FFOutputFormat ff_truehd_muxer;
extern const AVInputFormat ff_tta_demuxer;
extern const FFOutputFormat ff_tta_muxer;
+extern const AVInputFormat ff_ttml_demuxer;
extern const FFOutputFormat ff_ttml_muxer;
extern const AVInputFormat ff_txd_demuxer;
extern const AVInputFormat ff_tty_demuxer;
diff --git a/libavformat/ttmldec.c b/libavformat/ttmldec.c
new file mode 100644
index 0000000000..e63a2d04c8
--- /dev/null
+++ b/libavformat/ttmldec.c
@@ -0,0 +1,432 @@
+/*
+ * TTML subtitle demuxer
+ * Copyright (c) 2023 Aidan Vaughan (TheDaChicken)
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg 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.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg 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 FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * TTML subtitle demuxer
+ * @see https://www.w3.org/TR/ttml1/
+ * @see https://www.w3.org/TR/ttml2/
+ * @see https://www.w3.org/TR/ttml-imsc/rec
+ */
+
+#include <libxml/parser.h>
+#include "avformat.h"
+#include "ttmlenc.h"
+#include "internal.h"
+#include "subtitles.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/opt.h"
+
+typedef struct {
+ const AVClass *class;
+ FFDemuxSubtitlesQueue q;
+ int kind;
+ const char* lang;
+ uint8_t* header;
+ int header_size;
+ int cea_608_drawing;
+} TTMLContext;
+
+/* https://www.w3.org/TR/2018/REC-ttml2-20181108/#timing-value-time-expression */
+static int64_t read_ts(const char *s)
+{
+ /* Clock-time format */
+ int hh, mm, ss, ms;
+ if (sscanf(s, "%u:%u:%u.%u", &hh, &mm, &ss, &ms) == 4) return (hh*3600LL + mm*60LL + ss) * 1000LL + ms;
+ if (sscanf(s, "%u:%u:%u:%u", &hh, &mm, &ss, &ms) == 4) return (hh*3600LL + mm*60LL + ss) * 1000LL + ms;
+ /* ^^^ may contain seconds not as a fraction (.) ???? */
+ if (sscanf(s, "%u:%u.%u", &mm, &ss, &ms) == 3) return ( mm*60LL + ss) * 1000LL + ms;
+ return AV_NOPTS_VALUE;
+}
+
+static int dump_ns(AVBPrint* buf, xmlNsPtr cur, int another) {
+ if (cur->prefix != NULL) {
+ av_bprintf(buf, "xmlns:%s", cur->prefix);
+ }
+ else
+ av_bprintf(buf, "xmlns");
+ av_bprintf(buf, "=\"");
+ av_bprint_escape(buf, cur->href, NULL,
+ AV_ESCAPE_MODE_XML,
+ AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
+ av_bprintf(buf, "\"");
+ if(another)
+ av_bprintf(buf, " ");
+ return 0;
+}
+
+static int dump_attr(AVBPrint* buf, xmlNsPtr ns, const char* name, const xmlChar *val, int another) {
+ if(!val)
+ return 0;
+
+ if ((ns != NULL) && (ns->prefix != NULL)) {
+ av_bprintf(buf, "%s:", ns->prefix);
+ }
+ av_bprintf(buf, "%s=\"", name);
+ av_bprint_escape(buf, val, NULL,
+ AV_ESCAPE_MODE_XML,
+ AV_ESCAPE_FLAG_XML_DOUBLE_QUOTES);
+ av_bprintf(buf, "\"");
+ if(another)
+ av_bprintf(buf, " ");
+ return 0;
+}
+
+static int parse_body(AVFormatContext *s, const char *url, xmlDoc *doc, xmlNodePtr body)
+{
+ int ret = 0;
+ TTMLContext *ttml = s->priv_data;
+ xmlNodePtr div = NULL;
+ xmlNodePtr p_node = NULL;
+ xmlAttrPtr body_attr = body->properties;
+ xmlChar* val = NULL;
+ xmlBufferPtr p_buf;
+ AVBPrint p_attr_buf;
+ AVBPrint body_attr_buf;
+
+ div = xmlFirstElementChild(body);
+ if(!av_stristr(div->name, "div")) /* div must exist for proper ttml */
+ {
+ av_log(s, AV_LOG_ERROR, "Unable to parse '%s' - wrong node in body name[%s]\n",
+ url, div->name);
+ return AVERROR_INVALIDDATA;
+ }
+
+ av_bprint_init(&p_attr_buf, 0, INT_MAX);
+ av_bprint_init(&body_attr_buf, 0, INT_MAX);
+
+ while (body_attr) {
+ val = xmlGetProp(body, body_attr->name);
+ dump_attr(&body_attr_buf, body_attr->ns, body_attr->name, val, body_attr->next != NULL);
+ body_attr = body_attr->next;
+ xmlFree(val);
+ }
+
+ p_buf = xmlBufferCreate();
+ p_node = xmlFirstElementChild(div);
+ while(p_node) {
+ AVPacket *sub;
+ xmlNodePtr child = NULL;
+ xmlAttrPtr p_attr = p_node->properties;
+ int64_t ts_start, ts_end = AV_NOPTS_VALUE, ts_dur = AV_NOPTS_VALUE;
+
+ if (strncmp(p_node->name, "p", 1) != 0) {
+ av_log(s, AV_LOG_ERROR, "Unable to parse '%s' - wrong subtitle node name[%s]\n",
+ url, div->name);
+ ret = AVERROR_INVALIDDATA;
+ goto cleanup;
+ }
+
+ dump_attr(&p_attr_buf, p_attr->ns, "default", body_attr_buf.str, p_attr->next != NULL);
+ while (p_attr) {
+ val = xmlGetProp(p_node, p_attr->name);
+
+ if(!val)
+ {
+ av_log(s, AV_LOG_WARNING, "parse_body p_attr->name = %s val is NULL\n", p_attr->name);
+ p_attr = p_attr->next;
+ continue;
+ }
+
+ if (!strncmp(p_attr->name, "begin", 5)) {
+ ts_start = read_ts(val); /* doc: https://www.w3.org/TR/2018/REC-ttml2-20181108/#timing-attribute-begin */
+ } else if (!strncmp(p_attr->name, "end", 3)) {
+ ts_end = read_ts(val); /* doc: https://www.w3.org/TR/2018/REC-ttml2-20181108/#timing-attribute-end */
+ } else if (!strncmp(p_attr->name, "dur", 3)) {
+ ts_dur = read_ts(val); /* doc: https://www.w3.org/TR/2018/REC-ttml2-20181108/#timing-attribute-dur */
+ } else {
+ dump_attr(&p_attr_buf, p_attr->ns, p_attr->name, val, p_attr->next != NULL);
+ }
+
+ p_attr = p_attr->next;
+ xmlFree(val);
+ }
+
+ /* couldn't find pts in p tag */
+ if(ts_start == AV_NOPTS_VALUE || (ts_end == AV_NOPTS_VALUE && ts_dur == AV_NOPTS_VALUE)) {
+ av_log(s, AV_LOG_ERROR, "Unable to parse '%s' - no timestamp line[%d]\n",
+ url, p_node->line);
+ ret = AVERROR_INVALIDDATA;
+ goto cleanup;
+ }
+
+ /* dump data to packet for decoding */
+ child = p_node->children;
+ while (child != NULL) {
+ if(!strncmp(child->name, "metadata", 8) && !ttml->cea_608_drawing)
+ { /* check if the "metadata" tag is meant to contain cea-608 rows/cols */
+ val = xmlGetProp(child, "ccrow");
+ if(val) {
+ xmlFree(val);
+ goto next;
+ }
+ }
+
+ ret = xmlNodeDump(p_buf, doc, child, 0, 0);
+ if (ret < 0) {
+ ret = AVERROR_INVALIDDATA;
+ goto cleanup;
+ }
+ next:
+ child = child->next;
+ }
+
+ /* create packet */
+ sub = ff_subtitles_queue_insert(&ttml->q, xmlBufferContent(p_buf), xmlBufferLength(p_buf), 0);
+ if (!sub) {
+ ret = AVERROR(ENOMEM);
+ goto cleanup;
+ }
+ sub->pts = ts_start;
+ if(ts_dur != AV_NOPTS_VALUE)
+ sub->duration = ts_dur;
+ else
+ sub->duration = ts_end - ts_start;
+
+ if (p_attr_buf.len)
+ {
+ /* Keep the properties of p styles */
+ uint8_t *side_data_buf = av_packet_new_side_data(sub, AV_PKT_DATA_WEBVTT_SETTINGS,
+ p_attr_buf.len);
+ if(!side_data_buf)
+ goto cleanup;
+ memcpy(side_data_buf, p_attr_buf.str, p_attr_buf.len);
+ }
+
+ p_node = xmlNextElementSibling(p_node);
+ xmlBufferEmpty(p_buf);
+ av_bprint_clear(&p_attr_buf);
+ }
+
+ ff_subtitles_queue_finalize(s, &ttml->q);
+
+ cleanup:
+ av_bprint_finalize(&p_attr_buf, NULL);
+ av_bprint_finalize(&body_attr_buf, NULL);
+ xmlBufferFree(p_buf);
+ return ret;
+}
+
+static int parse_ttml(AVFormatContext *s, AVIOContext *in, const char *url) {
+ TTMLContext *ttml = s->priv_data;
+ int ret = 0;
+ AVBPrint buf;
+ xmlDoc *doc = NULL;
+ xmlNodePtr root_element = NULL;
+ xmlNodePtr child_node = NULL;
+ xmlAttrPtr attr_ptr = NULL;
+ xmlNsPtr ns_ptr = NULL;
+ xmlChar *val = NULL;
+
+ av_bprint_init(&buf, 0, INT_MAX);
+
+ ret = avio_read_to_bprint(in, &buf, SIZE_MAX);
+ if (ret < 0 || !avio_feof(in)) {
+ av_log(s, AV_LOG_ERROR, "Unable to parse '%s'\n", url);
+ if (ret == 0)
+ ret = AVERROR_INVALIDDATA;
+ goto cleanup;
+ }
+
+ LIBXML_TEST_VERSION
+
+ doc = xmlReadMemory(buf.str, buf.len, url, NULL, 0);
+ root_element = xmlDocGetRootElement(doc);
+
+ if (!root_element) {
+ ret = AVERROR_INVALIDDATA;
+ av_log(s, AV_LOG_ERROR, "Unable to parse '%s' - missing root node\n", url);
+ goto cleanup;
+ }
+
+ if (root_element->type != XML_ELEMENT_NODE || av_strcasecmp(root_element->name, "TT")) {
+ ret = AVERROR_INVALIDDATA;
+ av_log(s, AV_LOG_ERROR, "Unable to parse '%s' - wrong root node name[%s] type[%d]\n",
+ url, root_element->name, (int) root_element->type);
+ goto cleanup;
+ }
+
+ av_bprint_clear(&buf);
+
+ child_node = xmlFirstElementChild(root_element);
+ while(child_node) /* traverse through root nodes to get head, body */
+ {
+ if(!strncmp(child_node->name, "head", 4))
+ {
+ xmlBufferPtr header_buf = xmlBufferCreate();
+ if(!header_buf)
+ return AVERROR(ENOMEM);
+
+ /* read and create extra-data */
+ /* extra-data first contains namespace (attrs) of tt */
+ attr_ptr = root_element->properties;
+ while (attr_ptr) {
+ val = xmlGetProp(root_element, attr_ptr->name);
+ if (!strncmp(attr_ptr->name, "lang", 4)) {
+ ttml->lang = av_strdup(val);
+ } else {
+ dump_attr(&buf, attr_ptr->ns, attr_ptr->name, val, attr_ptr->next != NULL);
+ }
+ attr_ptr = attr_ptr->next;
+ xmlFree(val);
+ }
+
+ /* take definitions */
+ ns_ptr = root_element->nsDef;
+ if(ns_ptr)
+ av_bprintf(&buf, " ");
+ while (ns_ptr) {
+ dump_ns(&buf, ns_ptr, ns_ptr->next != NULL);
+ ns_ptr = ns_ptr->next;
+ }
+
+ av_bprint_chars(&buf, '\0', 1);
+
+ /* dump head element containing the styles */
+ xmlNodeDump(header_buf, doc, child_node, 0,0);
+
+ av_bprint_append_data(&buf, xmlBufferContent(header_buf), xmlBufferLength(header_buf));
+ av_bprint_chars(&buf, '\n', 1); /* new line due to head */
+ av_bprint_chars(&buf, '\0', 1);
+
+ ttml->header = av_malloc(buf.len);
+ if(!ttml->header)
+ return AVERROR(ENOMEM);
+
+ /* copy */
+ ttml->header_size = buf.len;
+ memcpy(ttml->header, buf.str, buf.len);
+
+ xmlBufferFree(header_buf);
+ }
+ else if(!strncmp(child_node->name, "body", 4) &&
+ (ret = parse_body(s, url, doc, child_node)) < 0)
+ {
+ av_log(s, AV_LOG_ERROR, "Unable to parse '%s' - body\n", url);
+ goto cleanup;
+ }
+ child_node = xmlNextElementSibling(child_node);
+ }
+
+
+ cleanup:
+ xmlFreeDoc(doc);
+ xmlCleanupParser();
+ av_bprint_finalize(&buf, NULL);
+ return ret;
+}
+
+static int ttml_probe(const AVProbeData *p)
+{
+ if (!av_stristr(p->buf, "<tt"))
+ return 0;
+
+ if (av_stristr(p->buf, "xmlns=\"http://www.w3.org/ns/ttml\"") /* current */ ||
+ av_stristr(p->buf, "xmlns=\"http://www.w3.org/2006/\"")) /* for unstable old urls */
+ return AVPROBE_SCORE_MAX;
+
+ return 0;
+}
+
+static int ttml_read_header(AVFormatContext *s)
+{
+ const size_t base_extradata_size = TTMLENC_EXTRADATA_SIGNATURE_SIZE + 1 +
+ AV_INPUT_BUFFER_PADDING_SIZE;
+ TTMLContext *ttml = s->priv_data;
+ AVStream *st;
+ int ret = 0;
+
+ if((ret = parse_ttml(s, s->pb, s->url)) < 0)
+ return ret;
+
+ st = avformat_new_stream(s, NULL);
+ if (!st)
+ return AVERROR(ENOMEM);
+ avpriv_set_pts_info(st, 64, 1, 1000);
+ st->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
+ st->codecpar->codec_id = AV_CODEC_ID_TTML;
+ st->codecpar->extradata = av_malloc(ttml->header_size +
+ base_extradata_size);
+ if (!st->codecpar->extradata)
+ return AVERROR(ENOMEM);
+
+ memcpy(st->codecpar->extradata, TTMLENC_EXTRADATA_SIGNATURE,
+ TTMLENC_EXTRADATA_SIGNATURE_SIZE);
+ memcpy(st->codecpar->extradata + TTMLENC_EXTRADATA_SIGNATURE_SIZE,
+ ttml->header, ttml->header_size);
+ st->codecpar->extradata_size = ttml->header_size + base_extradata_size;
+
+ av_dict_set(&st->metadata, "language", ttml->lang, 0);
+
+ return ret;
+}
+
+static int ttml_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+ TTMLContext *ttml = s->priv_data;
+ return ff_subtitles_queue_read_packet(&ttml->q, pkt);
+}
+
+static int ttml_read_seek(AVFormatContext *s, int stream_index,
+ int64_t min_ts, int64_t ts, int64_t max_ts, int flags)
+{
+ TTMLContext *ttml = s->priv_data;
+ return ff_subtitles_queue_seek(&ttml->q, s, stream_index,
+ min_ts, ts, max_ts, flags);
+}
+
+static int ttml_read_close(AVFormatContext *s)
+{
+ TTMLContext *ttml = s->priv_data;
+ ff_subtitles_queue_clean(&ttml->q);
+ av_free(ttml->header);
+ return 0;
+}
+
+#define OFFSET(x) offsetof(TTMLContext, x)
+#define KIND_FLAGS AV_OPT_FLAG_SUBTITLE_PARAM|AV_OPT_FLAG_DECODING_PARAM
+
+static const AVOption options[] = {
+ { "cea_608_drawing", "Enable unofficial ttml cea-608 support", OFFSET(kind), AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, KIND_FLAGS},
+ { NULL }
+};
+
+static const AVClass ttml_demuxer_class = {
+ .class_name = "TTML demuxer",
+ .item_name = av_default_item_name,
+ .option = options,
+ .version = LIBAVUTIL_VERSION_INT,
+};
+
+const AVInputFormat ff_ttml_demuxer = {
+ .name = "ttml",
+ .long_name = NULL_IF_CONFIG_SMALL("TTML subtitle"),
+ .priv_data_size = sizeof(TTMLContext),
+ .priv_class = &ttml_demuxer_class,
+ .flags_internal = FF_FMT_INIT_CLEANUP,
+ .read_probe = ttml_probe,
+ .read_header = ttml_read_header,
+ .read_packet = ttml_read_packet,
+ .read_seek2 = ttml_read_seek,
+ .read_close = ttml_read_close,
+ .extensions = "ttml",
+ .mime_type = "application/ttml+xml",
+};
diff --git a/libavformat/version.h b/libavformat/version.h
index af7d0a1024..e2634b85ae 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,7 +31,7 @@
#include "version_major.h"
-#define LIBAVFORMAT_VERSION_MINOR 4
+#define LIBAVFORMAT_VERSION_MINOR 5
#define LIBAVFORMAT_VERSION_MICRO 100
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
--
2.37.1.windows.1
More information about the ffmpeg-devel
mailing list