[FFmpeg-devel] [PATCH] [WIP] lavf: VobSub demuxer.

Clément Bœsch ubitux at gmail.com
Thu Sep 6 00:40:46 CEST 2012


TODO: raise AVPacket with the PTS from the .IDX (+honor delay and related settings)
TODO: seek?
TODO: lavf minor bump
TODO: Changelog entry
---

Hey,

This is the VobSub demuxer I've try to write so far. It kind of works, but the
timestamp from the .IDX are *not* used. It seems the .SUB actually contains
everything needed to time it properly, but TS look somehow fishy (even though
they seem to appear at the correct time at playback).

I'm still wondering how I am supposed to override the index entries built by
the MPEG demuxer with one I write myself. Currently, the index is reset after
the MPEG 1st pass (see around line 90), and then rebuild within the stream
using the timestamp lines from the .IDX (see around line 160). Unfortunately,
read_packet() from the MPEG demuxer doesn't use that index to set the pkt->pts
so the PTS are instead based on the content of the PES packet.

Note: The number of timestamp entries in the .IDX seems to match (minus 1) the
number of entries detected by the MPEG demuxer with my test sample.

Anyway, if anyone has any idea what extra timing/offset information the .IDX
really adds, and how I am suppose to exploit them, any suggestion is welcome.

Thanks,

---
 doc/general.texi         |   1 +
 libavformat/Makefile     |   1 +
 libavformat/allformats.c |   1 +
 libavformat/vobsubdec.c  | 265 +++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 268 insertions(+)
 create mode 100644 libavformat/vobsubdec.c

diff --git a/doc/general.texi b/doc/general.texi
index ef9cf72..0c98c4b 100644
--- a/doc/general.texi
+++ b/doc/general.texi
@@ -899,6 +899,7 @@ performance on systems without hardware floating point support).
 @item SAMI             @tab   @tab X @tab   @tab X
 @item SubRip (SRT)     @tab X @tab X @tab X @tab X
 @item SubViewer        @tab   @tab X @tab   @tab X
+ at item VobSub (IDX+SUB) @tab   @tab X @tab   @tab X
 @item 3GPP Timed Text  @tab   @tab   @tab X @tab X
 @item XSUB             @tab   @tab   @tab X @tab X
 @end multitable
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 72f9c22..81f3287 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -342,6 +342,7 @@ OBJS-$(CONFIG_VC1_DEMUXER)               += rawdec.o
 OBJS-$(CONFIG_VC1T_DEMUXER)              += vc1test.o
 OBJS-$(CONFIG_VC1T_MUXER)                += vc1testenc.o
 OBJS-$(CONFIG_VMD_DEMUXER)               += sierravmd.o
+OBJS-$(CONFIG_VOBSUB_DEMUXER)            += vobsubdec.o
 OBJS-$(CONFIG_VOC_DEMUXER)               += vocdec.o voc.o
 OBJS-$(CONFIG_VOC_MUXER)                 += vocenc.o voc.o
 OBJS-$(CONFIG_VQF_DEMUXER)               += vqf.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index 9df6280..193bdcb 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -244,6 +244,7 @@ void av_register_all(void)
     REGISTER_DEMUXER  (VC1, vc1);
     REGISTER_MUXDEMUX (VC1T, vc1t);
     REGISTER_DEMUXER  (VMD, vmd);
+    REGISTER_DEMUXER  (VOBSUB, vobsub);
     REGISTER_MUXDEMUX (VOC, voc);
     REGISTER_DEMUXER  (VQF, vqf);
     REGISTER_DEMUXER  (W64, w64);
diff --git a/libavformat/vobsubdec.c b/libavformat/vobsubdec.c
new file mode 100644
index 0000000..572c615
--- /dev/null
+++ b/libavformat/vobsubdec.c
@@ -0,0 +1,265 @@
+/*
+ * Copyright (c) 2012 Clément Bœsch
+ *
+ * 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
+ * VobSub subtitle demuxer
+ * @see http://wiki.multimedia.cx/index.php?title=VOBsub
+ */
+
+#include "avformat.h"
+#include "internal.h"
+#include "subtitles.h"
+#include "libavutil/avstring.h"
+#include "libavutil/bprint.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/opt.h"
+#include "libavutil/timestamp.h"
+
+typedef struct {
+    const AVClass *class;
+    AVFormatContext *sub_ctx;
+    int cur_st_id;
+    int strip_header;
+} VobSubContext;
+
+#define REF_STRING "# VobSub index file,"
+
+static int vobsub_probe(AVProbeData *p)
+{
+    if (!strncmp(p->buf, REF_STRING, sizeof(REF_STRING) - 1))
+        return AVPROBE_SCORE_MAX;
+    return 0;
+}
+
+static int vobsub_read_header(AVFormatContext *s)
+{
+    int i, ret, has_subtitle_stream = 0, header_parsed = 0;
+    VobSubContext *vobsub = s->priv_data;
+    char *sub_name = av_strdup(s->filename);
+    size_t fname_len = strlen(sub_name);
+    char *ext = sub_name - 3 + fname_len;
+    AVInputFormat *mpeg_format = av_find_input_format("mpeg");
+    AVBPrint header;
+
+    /* Open the related .SUB bitmaps file */
+    if (!mpeg_format) {
+        av_log(s, AV_LOG_ERROR, "Unable to find MPEG demuxer\n");
+        return AVERROR(EINVAL);
+    }
+    if (fname_len < 4 || *(ext - 1) != '.') {
+        av_log(s, AV_LOG_ERROR, "The input index filename is to short "
+               "to guess the associated .SUB file\n");
+        return AVERROR_INVALIDDATA;
+    }
+    memcpy(ext, !strncmp(ext, "IDX", 3) ? "SUB" : "sub", 3);
+    ret = avformat_open_input(&vobsub->sub_ctx, sub_name, mpeg_format, NULL);
+    if (ret < 0) {
+        av_log(s, AV_LOG_ERROR, "Unable to open %s as MPEG subtitles\n", sub_name);
+        return ret;
+    }
+
+    /* The .SUB contains enough information to guess the number of subtitles
+     * stream, so we fill the streams in the format context. The .idx file will
+     * help adding metadata. */
+    ret = avformat_find_stream_info(vobsub->sub_ctx, NULL);
+    if (ret < 0)
+        return ret;
+
+    /* Reset the index entries set by the MPEG demuxer since we'll reconstruct
+     * that index based on the content of the .IDX. Also make sure there is at
+     * least one DVD subtitle stream. */
+    for (i = 0; i < vobsub->sub_ctx->nb_streams; i++) {
+        AVStream *st = vobsub->sub_ctx->streams[i];
+        if (st->codec->codec_id == AV_CODEC_ID_DVD_SUBTITLE) {
+            //av_log(0,0,"MPEG stream[%d] nb index entries = %d\n", i, st->nb_index_entries);
+            st->nb_index_entries = 0;
+            has_subtitle_stream = 1;
+        }
+    }
+    if (!has_subtitle_stream) {
+        av_log(s, AV_LOG_ERROR, "No DVD subtitle stream found in %s\n",
+               vobsub->sub_ctx->filename);
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* Extract .IDX header to avctx->extradata, add some info in the AVStreams
+     * and build indexes */
+    av_bprint_init(&header, FF_INPUT_BUFFER_PADDING_SIZE, AV_BPRINT_SIZE_UNLIMITED);
+    while (!url_feof(s->pb)) {
+        char line[2048];
+        int len = ff_get_line(s->pb, line, sizeof(line));
+
+        if (!len)
+            break;
+        if (vobsub->strip_header && (line[0] == '#' ||
+            line[0] == '\n' || (line[0] == '\r' && line[1] == '\n')))
+            continue;
+
+        line[strcspn(line, "\r\n")] = 0;
+
+        if (!strncmp(line, "id:", 3)) {
+            /* New stream */
+
+            AVStream *st;
+#define IDBS 63
+            char id[IDBS+1] = {0};
+            int n = sscanf(line, "id: %" AV_STRINGIFY(IDBS) "[^,], index: %u",
+                           id, &vobsub->cur_st_id);
+            if (n != 2) {
+                av_log(s, AV_LOG_WARNING, "Unable to parse index line '%s', "
+                       "assuming 'id: und, index: 0'\n", line);
+                strcpy(id, "und");
+                vobsub->cur_st_id = 0;
+            }
+
+            if (vobsub->cur_st_id >= vobsub->sub_ctx->nb_streams) {
+                av_log(s, AV_LOG_WARNING, "Invalid stream index %d "
+                       "(%s contains %d stream%s), assuming 0\n",
+                       vobsub->cur_st_id, vobsub->sub_ctx->filename,
+                       vobsub->sub_ctx->nb_streams,
+                       vobsub->sub_ctx->nb_streams > 1 ? "s" : "");
+                vobsub->cur_st_id = 0;
+            }
+
+            st = vobsub->sub_ctx->streams[vobsub->cur_st_id];
+            av_dict_set(&st->metadata, "language", id, 0);
+            av_log(s, AV_LOG_DEBUG, "IDX stream[%d] id=%s\n",
+                   vobsub->cur_st_id, id);
+            header_parsed = 1;
+
+        } else if (!strncmp(line, "alt:", 3)) {
+            /* Alternative name for the current stream */
+
+            char *p = line + 4;
+            AVStream *st = vobsub->sub_ctx->streams[vobsub->cur_st_id];
+
+            while (*p == ' ')
+                p++;
+            av_dict_set(&st->metadata, "title", p, 0);
+            av_log(s, AV_LOG_DEBUG, "IDX stream[%d] name=%s\n", vobsub->cur_st_id, p);
+            header_parsed = 1;
+
+        } else if (!strncmp(line, "timestamp:", 10)) {
+            /* timestamp+offset index entry */
+
+            int hh, mm, ss, ms;
+            int64_t pos, timestamp;
+            AVStream *st = vobsub->sub_ctx->streams[vobsub->cur_st_id];
+
+            if (sscanf(line, "timestamp: %02d:%02d:%02d:%03d, filepos: %"PRIx64,
+                       &hh, &mm, &ss, &ms, &pos) != 5) {
+                av_log(s, AV_LOG_ERROR, "Unable to parse timestamp line '%s', abort parsing\n", line);
+                break;
+            }
+            timestamp = (hh*3600 + mm*60 + ss) * 1000 + ms;
+            timestamp = av_rescale_q(timestamp, (AVRational){1,1000}, st->time_base);
+            //av_log(0,0,"timestamp=%"PRId64" pos=%"PRIx64"\n", timestamp, pos);
+            av_add_index_entry(st, pos, timestamp, 0, 0, 0);
+
+        } else if (!header_parsed) {
+            /* still parsing the header */
+
+            av_bprintf(&header, "%s\n", line);
+        }
+    }
+
+    av_log(s, AV_LOG_DEBUG, "VobSub header:\n%s\n", header.str);
+
+    /* Dump the header in each subtitles stream and create an identical stream
+     * in the local context */
+    for (i = 0; i < vobsub->sub_ctx->nb_streams; i++) {
+        AVStream *sub_st = vobsub->sub_ctx->streams[i];
+        if (sub_st->codec->codec_id == AV_CODEC_ID_DVD_SUBTITLE) {
+            AVStream *st = avformat_new_stream(s, NULL);
+            if (!st)
+                return AVERROR(ENOMEM);
+            st->id = i;
+            av_bprint_finalize(&header, (char **)&sub_st->codec->extradata);
+            sub_st->codec->extradata_size = header.len + 1;
+            avcodec_copy_context(st->codec, sub_st->codec);
+            //av_log(0,0,"STREAM[%d]: %d nb index entries\n", i, vobsub->sub_ctx->streams[i]->nb_index_entries);
+        }
+    }
+
+    return 0;
+}
+
+#define CB_IF_AVAILABLE(cb, ...) vobsub->sub_ctx && vobsub->sub_ctx->iformat && \
+    vobsub->sub_ctx->iformat->cb ? vobsub->sub_ctx->iformat->cb(__VA_ARGS__) : 0
+
+static int vobsub_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    VobSubContext *vobsub = s->priv_data;
+    int ret = CB_IF_AVAILABLE(read_packet, vobsub->sub_ctx, pkt);
+    //int i;
+
+    //for (i = 0; i < vobsub->sub_ctx->nb_streams; i++) {
+    //    av_log(0,0,"STREAM[%d]: %d nb index entries | first: PTS=%s POS=%"PRIx64"\n",
+    //           i, vobsub->sub_ctx->streams[i]->nb_index_entries,
+    //           av_ts2str(vobsub->sub_ctx->streams[i]->index_entries[0].timestamp),
+    //                     vobsub->sub_ctx->streams[i]->index_entries[0].pos);
+    //}
+    //av_log(0,0,"read pkt pts=%s dts=%s pos=%"PRId64"\n",
+    //       av_ts2str(pkt->pts), av_ts2str(pkt->dts), pkt->pos);
+    return ret;
+}
+
+static int64_t vobsub_read_timestamp(AVFormatContext *s, int stream_index,
+                                     int64_t *pos, int64_t pos_limit)
+{
+    /* XXX: looks unused */
+    VobSubContext *vobsub = s->priv_data;
+    return CB_IF_AVAILABLE(read_timestamp, vobsub->sub_ctx, stream_index, pos, pos_limit);
+}
+
+static int vobsub_read_close(AVFormatContext *s)
+{
+    VobSubContext *vobsub = s->priv_data;
+    return CB_IF_AVAILABLE(read_close, vobsub->sub_ctx);
+}
+
+#define OFFSET(x) offsetof(VobSubContext, x)
+#define FLAGS AV_OPT_FLAG_SUBTITLE_PARAM | AV_OPT_FLAG_DECODING_PARAM
+static const AVOption options[] = {
+    { "strip_header", "Remove comments and empty lines from the header", OFFSET(strip_header), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, FLAGS },
+    { NULL }
+};
+
+static const AVClass vobsub_demuxer_class = {
+    .class_name = "vobsubdec",
+    .item_name  = av_default_item_name,
+    .option     = options,
+    .version    = LIBAVUTIL_VERSION_INT,
+};
+
+AVInputFormat ff_vobsub_demuxer = {
+    .name           = "vobsub",
+    .long_name      = NULL_IF_CONFIG_SMALL("VobSub subtitle format"),
+    .priv_data_size = sizeof(VobSubContext),
+    .read_probe     = vobsub_probe,
+    .read_header    = vobsub_read_header,
+    .read_packet    = vobsub_read_packet,
+    .read_timestamp = vobsub_read_timestamp,
+    .read_close     = vobsub_read_close,
+    .flags          = AVFMT_SHOW_IDS | AVFMT_TS_DISCONT, /* same as mpeg demuxer */
+    .extensions     = "idx",
+    .priv_class     = &vobsub_demuxer_class,
+};
-- 
1.7.12



More information about the ffmpeg-devel mailing list