WebM muxer writes WebVTT subtitle track
authorMatthew Heaney <matthewjheaney@google.com>
Mon, 8 Jul 2013 23:25:30 +0000 (16:25 -0700)
committerMichael Niedermayer <michaelni@gmx.at>
Tue, 9 Jul 2013 01:09:12 +0000 (03:09 +0200)
The Matroska muxer now allows WebVTT subtitle tracks to be written
while in WebM muxing mode.

WebVTT subtitle tracks have four kinds: "subtitles", "captions",
"descriptions", and "metadata". Each text track kind has a distinct
Mastroska CodecID and track type, as described in the temporal
metadata guidelines here:

http://wiki.webmproject.org/webm-metadata/temporal-metadata/webvtt-in-webm

When the stream has codec id AV_CODEC_ID_WEBVTT, the stream packet is
serialized per the temporal metadata guidelines cited above. The
WebVTT cue is written as a Matroska block group. The block frame
comprises the WebVTT cue id, followed by the cue settings, followed by
the cue text.  (The block timestamp is synthesized from the cue
timestamp.)

Signed-off-by: Michael Niedermayer <michaelni@gmx.at>
libavformat/matroska.h
libavformat/matroskaenc.c

index af662be..886a346 100644 (file)
@@ -230,6 +230,7 @@ typedef enum {
   MATROSKA_TRACK_TYPE_LOGO     = 0x10,
   MATROSKA_TRACK_TYPE_SUBTITLE = 0x11,
   MATROSKA_TRACK_TYPE_CONTROL  = 0x20,
+  MATROSKA_TRACK_TYPE_METADATA = 0x21,
 } MatroskaTrackType;
 
 typedef enum {
index dfbb174..7efda5a 100644 (file)
@@ -600,7 +600,11 @@ static int mkv_write_tracks(AVFormatContext *s)
         if ((tag = av_dict_get(st->metadata, "title", NULL, 0)))
             put_ebml_string(pb, MATROSKA_ID_TRACKNAME, tag->value);
         tag = av_dict_get(st->metadata, "language", NULL, 0);
-        put_ebml_string(pb, MATROSKA_ID_TRACKLANGUAGE, tag ? tag->value:"und");
+        if (mkv->mode != MODE_WEBM || codec->codec_id != AV_CODEC_ID_WEBVTT) {
+            put_ebml_string(pb, MATROSKA_ID_TRACKLANGUAGE, tag ? tag->value:"und");
+        } else if (tag && tag->value) {
+            put_ebml_string(pb, MATROSKA_ID_TRACKLANGUAGE, tag->value);
+        }
 
         if (default_stream_exists) {
             put_ebml_uint(pb, MATROSKA_ID_TRACKFLAGDEFAULT, !!(st->disposition & AV_DISPOSITION_DEFAULT));
@@ -608,22 +612,41 @@ static int mkv_write_tracks(AVFormatContext *s)
         if (st->disposition & AV_DISPOSITION_FORCED)
             put_ebml_uint(pb, MATROSKA_ID_TRACKFLAGFORCED, 1);
 
-        // look for a codec ID string specific to mkv to use,
-        // if none are found, use AVI codes
-        for (j = 0; ff_mkv_codec_tags[j].id != AV_CODEC_ID_NONE; j++) {
-            if (ff_mkv_codec_tags[j].id == codec->codec_id) {
-                put_ebml_string(pb, MATROSKA_ID_CODECID, ff_mkv_codec_tags[j].str);
-                native_id = 1;
-                break;
+        if (mkv->mode == MODE_WEBM && codec->codec_id == AV_CODEC_ID_WEBVTT) {
+            const char *codec_id;
+            if (st->disposition & AV_DISPOSITION_CAPTIONS) {
+                codec_id = "D_WEBVTT/CAPTIONS";
+                native_id = MATROSKA_TRACK_TYPE_SUBTITLE;
+            } else if (st->disposition & AV_DISPOSITION_DESCRIPTIONS) {
+                codec_id = "D_WEBVTT/DESCRIPTIONS";
+                native_id = MATROSKA_TRACK_TYPE_METADATA;
+            } else if (st->disposition & AV_DISPOSITION_METADATA) {
+                codec_id = "D_WEBVTT/METADATA";
+                native_id = MATROSKA_TRACK_TYPE_METADATA;
+            } else {
+                codec_id = "D_WEBVTT/SUBTITLES";
+                native_id = MATROSKA_TRACK_TYPE_SUBTITLE;
+            }
+            put_ebml_string(pb, MATROSKA_ID_CODECID, codec_id);
+        } else {
+            // look for a codec ID string specific to mkv to use,
+            // if none are found, use AVI codes
+            for (j = 0; ff_mkv_codec_tags[j].id != AV_CODEC_ID_NONE; j++) {
+                if (ff_mkv_codec_tags[j].id == codec->codec_id) {
+                    put_ebml_string(pb, MATROSKA_ID_CODECID, ff_mkv_codec_tags[j].str);
+                    native_id = 1;
+                    break;
+                }
             }
         }
 
         if (mkv->mode == MODE_WEBM && !(codec->codec_id == AV_CODEC_ID_VP8 ||
                                         codec->codec_id == AV_CODEC_ID_VP9 ||
                                       ((codec->codec_id == AV_CODEC_ID_OPUS)&&(codec->strict_std_compliance <= FF_COMPLIANCE_EXPERIMENTAL)) ||
-                                        codec->codec_id == AV_CODEC_ID_VORBIS)) {
+                                        codec->codec_id == AV_CODEC_ID_VORBIS ||
+                                        codec->codec_id == AV_CODEC_ID_WEBVTT)) {
             av_log(s, AV_LOG_ERROR,
-                   "Only VP8,VP9 video and Vorbis,Opus(experimental, use -strict -2) audio are supported for WebM.\n");
+                   "Only VP8,VP9 video and Vorbis,Opus(experimental, use -strict -2) audio and WebVTT subtitles are supported for WebM.\n");
             return AVERROR(EINVAL);
         }
 
@@ -717,18 +740,25 @@ static int mkv_write_tracks(AVFormatContext *s)
                 break;
 
             case AVMEDIA_TYPE_SUBTITLE:
-                put_ebml_uint(pb, MATROSKA_ID_TRACKTYPE, MATROSKA_TRACK_TYPE_SUBTITLE);
                 if (!native_id) {
                     av_log(s, AV_LOG_ERROR, "Subtitle codec %d is not supported.\n", codec->codec_id);
                     return AVERROR(ENOSYS);
                 }
+
+                if (mkv->mode != MODE_WEBM || codec->codec_id != AV_CODEC_ID_WEBVTT)
+                    native_id = MATROSKA_TRACK_TYPE_SUBTITLE;
+
+                put_ebml_uint(pb, MATROSKA_ID_TRACKTYPE, native_id);
                 break;
             default:
                 av_log(s, AV_LOG_ERROR, "Only audio, video, and subtitles are supported for Matroska.\n");
                 return AVERROR(EINVAL);
         }
-        ret = mkv_write_codecprivate(s, pb, codec, native_id, qt_id);
-        if (ret < 0) return ret;
+
+        if (mkv->mode != MODE_WEBM || codec->codec_id != AV_CODEC_ID_WEBVTT) {
+            ret = mkv_write_codecprivate(s, pb, codec, native_id, qt_id);
+            if (ret < 0) return ret;
+        }
 
         end_ebml_master(pb, track);
 
@@ -1308,6 +1338,44 @@ static int mkv_write_srt_blocks(AVFormatContext *s, AVIOContext *pb, AVPacket *p
     return duration;
 }
 
+static int mkv_write_vtt_blocks(AVFormatContext *s, AVIOContext *pb, AVPacket *pkt)
+{
+    MatroskaMuxContext *mkv = s->priv_data;
+    ebml_master blockgroup;
+    int id_size, settings_size, size;
+    uint8_t *id, *settings;
+    int64_t ts = mkv->tracks[pkt->stream_index].write_dts ? pkt->dts : pkt->pts;
+    const int flags = 0;
+
+    id_size = 0;
+    id = av_packet_get_side_data(pkt, AV_PKT_DATA_WEBVTT_IDENTIFIER,
+                                 &id_size);
+
+    settings_size = 0;
+    settings = av_packet_get_side_data(pkt, AV_PKT_DATA_WEBVTT_SETTINGS,
+                                       &settings_size);
+
+    size = id_size + 1 + settings_size + 1 + pkt->size;
+
+    av_log(s, AV_LOG_DEBUG, "Writing block at offset %" PRIu64 ", size %d, "
+           "pts %" PRId64 ", dts %" PRId64 ", duration %d, flags %d\n",
+           avio_tell(pb), size, pkt->pts, pkt->dts, pkt->duration, flags);
+
+    blockgroup = start_ebml_master(pb, MATROSKA_ID_BLOCKGROUP, mkv_blockgroup_size(size));
+
+    put_ebml_id(pb, MATROSKA_ID_BLOCK);
+    put_ebml_num(pb, size+4, 0);
+    avio_w8(pb, 0x80 | (pkt->stream_index + 1));     // this assumes stream_index is less than 126
+    avio_wb16(pb, ts - mkv->cluster_pts);
+    avio_w8(pb, flags);
+    avio_printf(pb, "%.*s\n%.*s\n%.*s", id_size, id, settings_size, settings, pkt->size, pkt->data);
+
+    put_ebml_uint(pb, MATROSKA_ID_BLOCKDURATION, pkt->duration);
+    end_ebml_master(pb, blockgroup);
+
+    return pkt->duration;
+}
+
 static void mkv_flush_dynbuf(AVFormatContext *s)
 {
     MatroskaMuxContext *mkv = s->priv_data;
@@ -1363,6 +1431,8 @@ static int mkv_write_packet_internal(AVFormatContext *s, AVPacket *pkt)
 #endif
     } else if (codec->codec_id == AV_CODEC_ID_SRT) {
         duration = mkv_write_srt_blocks(s, pb, pkt);
+    } else if (codec->codec_id == AV_CODEC_ID_WEBVTT) {
+        duration = mkv_write_vtt_blocks(s, pb, pkt);
     } else {
         ebml_master blockgroup = start_ebml_master(pb, MATROSKA_ID_BLOCKGROUP, mkv_blockgroup_size(pkt->size));
         /* For backward compatibility, prefer convergence_duration. */
@@ -1606,6 +1676,7 @@ AVOutputFormat ff_webm_muxer = {
     .priv_data_size    = sizeof(MatroskaMuxContext),
     .audio_codec       = AV_CODEC_ID_VORBIS,
     .video_codec       = AV_CODEC_ID_VP8,
+    .subtitle_codec    = AV_CODEC_ID_WEBVTT,
     .write_header      = mkv_write_header,
     .write_packet      = mkv_write_packet,
     .write_trailer     = mkv_write_trailer,