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

Nicolas George nicolas.george at normalesup.org
Fri Sep 7 22:14:10 CEST 2012


Le primidi 21 fructidor, an CCXX, Clément Bœsch a écrit :
> 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 suspect there are tools to adjust the timing of vobsub files that work
only with the index file. I admit I myself sometimes tweaked a few things
with vim, and obviously I did not alter the timestamps in the sub file. If
the timestamps in the index file were ignored, this would have been rather
annoying.

Also, I would not be surprised if some DVD rip tools would just take the
subtitle packet from the VOB file: in that case, the timestamp in the packet
is relative to the last timestamp reset in the VOB file, while the timestamp
in the index is built using the navigation information.

> 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.

I suspect the solution would be to make most of the work with the vobsub
demuxer, and only use the underlying MPEG demuxer to extract the payload. I
suspect the simplest way to do that would be to use avio_alloc_context with
a custom read_packet function that will read in the sub file exactly at the
offset specified in the index.

> 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;

I usually find the style where you declare a lot of stuff with non-trivial
values long before you actually use them rather harder to follow.

> +
> +    /* Open the related .SUB bitmaps file */
> +    if (!mpeg_format) {
> +        av_log(s, AV_LOG_ERROR, "Unable to find MPEG demuxer\n");
> +        return AVERROR(EINVAL);

AVERROR_DEMUXER_NOT_FOUND? Or you could even declare
"extern AVInputFormat ff_mpeg_demuxer" and access it directly.

> +    }
> +    if (fname_len < 4 || *(ext - 1) != '.') {
> +        av_log(s, AV_LOG_ERROR, "The input index filename is to short "

too 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);

I do not think FF_INPUT_BUFFER_PADDING_SIZE will do any good here. It is way
below the initial size of a standard bprint anyway, and if you want padding
in the buffer at the end, you have to add it on top of the data.

> +    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,
> +};

Regards,

-- 
  Nicolas George
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 198 bytes
Desc: Digital signature
URL: <http://ffmpeg.org/pipermail/ffmpeg-devel/attachments/20120907/ade43998/attachment.asc>


More information about the ffmpeg-devel mailing list