diff mbox series

[FFmpeg-devel,v2] dvdvideo: add DVD-Video demuxer, powered by libdvdnav and libdvdread

Message ID 20240110084608.1889310-1-marth64@proxyid.net
State New
Headers show
Series [FFmpeg-devel,v2] dvdvideo: add DVD-Video demuxer, powered by libdvdnav and libdvdread | expand

Checks

Context Check Description
yinshiyou/make_loongarch64 success Make finished
yinshiyou/make_fate_loongarch64 success Make fate finished
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Marth64 Jan. 10, 2024, 8:46 a.m. UTC
Dust off those old DVD discs!

This version is greatly simplified and improved from the patch submitted in December.
Discontinuity issues are resolved, and many discs should work smoothly out of the box.
Also, GPL sector validation code is removed.

Basic Usage:
ffmpeg -f dvdvideo -title 1 -i PATH_TO_DVD_STRUCTURE ...

You will have to set your own title number, although the default value (1) gives good coverage.
Note that this demuxer will process one PGC only. If you are working with a multi-PGC title or
strange authoring, you may specify and target an exact PGC/PG (see options).

Known issues:
* Chapter points are not precise, this is highest priority to fix
* Subtitle palette support will come in the next patchset (to consolidate shared code)
* Some oddly authored discs with 1 cell and a command link back to itself will loop infinitely,
I am debugging this scenario.

Not implemented:
* Shuffle PGC mode, menus, or rarely used DVD features like karaoke extensions
* SDDS codec is not supported, as I don't have any material to test it with
* Seeking by time is a low priority at this time
* Probing will come but I have some concerns to think through first

Signed-off-by: Marth64 <marth64@proxyid.net>
---
 Changelog                 |    1 +
 configure                 |    8 +
 libavformat/Makefile      |    1 +
 libavformat/allformats.c  |    1 +
 libavformat/avlanguage.c  |   10 +-
 libavformat/dvdvideodec.c | 1022 +++++++++++++++++++++++++++++++++++++
 6 files changed, 1041 insertions(+), 2 deletions(-)
 create mode 100644 libavformat/dvdvideodec.c

Comments

Marth64 Jan. 10, 2024, 8:53 a.m. UTC | #1
I will add that I still had to maintain a dependency on GENPTS for the
inner MPEG demuxer. This is because dvdnav_get_current_time() does not
provide exact stream-level PTS. However, the crashing/hanging is fixed (by
parsing NAV packets), and the output is produced smoothly. GENPTS fills in
the blanks.

If anyone can think of a better way, please let me know. Right now, it
works well.

On Wed, Jan 10, 2024 at 2:47 AM Marth64 <marth64@proxyid.net> wrote:

> Dust off those old DVD discs!
>
> This version is greatly simplified and improved from the patch submitted
> in December.
> Discontinuity issues are resolved, and many discs should work smoothly out
> of the box.
> Also, GPL sector validation code is removed.
>
> Basic Usage:
> ffmpeg -f dvdvideo -title 1 -i PATH_TO_DVD_STRUCTURE ...
>
> You will have to set your own title number, although the default value (1)
> gives good coverage.
> Note that this demuxer will process one PGC only. If you are working with
> a multi-PGC title or
> strange authoring, you may specify and target an exact PGC/PG (see
> options).
>
> Known issues:
> * Chapter points are not precise, this is highest priority to fix
> * Subtitle palette support will come in the next patchset (to consolidate
> shared code)
> * Some oddly authored discs with 1 cell and a command link back to itself
> will loop infinitely,
> I am debugging this scenario.
>
> Not implemented:
> * Shuffle PGC mode, menus, or rarely used DVD features like karaoke
> extensions
> * SDDS codec is not supported, as I don't have any material to test it with
> * Seeking by time is a low priority at this time
> * Probing will come but I have some concerns to think through first
>
> Signed-off-by: Marth64 <marth64@proxyid.net>
> ---
>  Changelog                 |    1 +
>  configure                 |    8 +
>  libavformat/Makefile      |    1 +
>  libavformat/allformats.c  |    1 +
>  libavformat/avlanguage.c  |   10 +-
>  libavformat/dvdvideodec.c | 1022 +++++++++++++++++++++++++++++++++++++
>  6 files changed, 1041 insertions(+), 2 deletions(-)
>  create mode 100644 libavformat/dvdvideodec.c
>
> diff --git a/Changelog b/Changelog
> index 5b2899d05b..1b377fed2f 100644
> --- a/Changelog
> +++ b/Changelog
> @@ -18,6 +18,7 @@ version <next>:
>  - lavu/eval: introduce randomi() function in expressions
>  - VVC decoder
>  - fsync filter
> +- DVD-Video demuxer, powered by libdvdnav and libdvdread
>
>  version 6.1:
>  - libaribcaption decoder
> diff --git a/configure b/configure
> index e87a09ce83..1f21f4f1c2 100755
> --- a/configure
> +++ b/configure
> @@ -227,6 +227,8 @@ External library support:
>    --enable-libdavs2        enable AVS2 decoding via libdavs2 [no]
>    --enable-libdc1394       enable IIDC-1394 grabbing using libdc1394
>                             and libraw1394 [no]
> +  --enable-libdvdnav       enable libdvdnav, needed for DVD demuxing [no]
> +  --enable-libdvdread       enable libdvdread, needed for DVD demuxing
> [no]
>    --enable-libfdk-aac      enable AAC de/encoding via libfdk-aac [no]
>    --enable-libflite        enable flite (voice synthesis) support via
> libflite [no]
>    --enable-libfontconfig   enable libfontconfig, useful for drawtext
> filter [no]
> @@ -1806,6 +1808,8 @@ EXTERNAL_LIBRARY_GPL_LIST="
>      frei0r
>      libcdio
>      libdavs2
> +    libdvdnav
> +    libdvdread
>      librubberband
>      libvidstab
>      libx264
> @@ -3519,6 +3523,8 @@ dts_demuxer_select="dca_parser"
>  dtshd_demuxer_select="dca_parser"
>  dv_demuxer_select="dvprofile"
>  dv_muxer_select="dvprofile"
> +dvdvideo_demuxer_select="mpegps_demuxer"
> +dvdvideo_demuxer_deps="libdvdnav libdvdread"
>  dxa_demuxer_select="riffdec"
>  eac3_demuxer_select="ac3_parser"
>  evc_demuxer_select="evc_frame_merge_bsf evc_parser"
> @@ -6760,6 +6766,8 @@ enabled libdav1d          && require_pkg_config
> libdav1d "dav1d >= 0.5.0" "dav1d
>  enabled libdavs2          && require_pkg_config libdavs2 "davs2 >= 1.6.0"
> davs2.h davs2_decoder_open
>  enabled libdc1394         && require_pkg_config libdc1394 libdc1394-2
> dc1394/dc1394.h dc1394_new
>  enabled libdrm            && check_pkg_config libdrm libdrm xf86drm.h
> drmGetVersion
> +enabled libdvdnav         && require_pkg_config libdvdnav "dvdnav >=
> 6.1.1" dvdnav/dvdnav.h dvdnav_open2
> +enabled libdvdread        && require_pkg_config libdvdread "dvdread >=
> 6.1.2" dvdread/dvd_reader.h DVDOpen2 -ldvdread
>  enabled libfdk_aac        && { check_pkg_config libfdk_aac fdk-aac
> "fdk-aac/aacenc_lib.h" aacEncOpen ||
>                                 { require libfdk_aac fdk-aac/aacenc_lib.h
> aacEncOpen -lfdk-aac &&
>                                   warn "using libfdk without pkg-config";
> } }
> diff --git a/libavformat/Makefile b/libavformat/Makefile
> index 581e378d95..3c1cb21fe2 100644
> --- a/libavformat/Makefile
> +++ b/libavformat/Makefile
> @@ -192,6 +192,7 @@ OBJS-$(CONFIG_DTS_MUXER)                 += rawenc.o
>  OBJS-$(CONFIG_DV_MUXER)                  += dvenc.o
>  OBJS-$(CONFIG_DVBSUB_DEMUXER)            += dvbsub.o rawdec.o
>  OBJS-$(CONFIG_DVBTXT_DEMUXER)            += dvbtxt.o rawdec.o
> +OBJS-$(CONFIG_DVDVIDEO_DEMUXER)          += dvdvideodec.o
>  OBJS-$(CONFIG_DXA_DEMUXER)               += dxa.o
>  OBJS-$(CONFIG_EA_CDATA_DEMUXER)          += eacdata.o
>  OBJS-$(CONFIG_EA_DEMUXER)                += electronicarts.o
> diff --git a/libavformat/allformats.c b/libavformat/allformats.c
> index ce6be5f04d..ea88d4c094 100644
> --- a/libavformat/allformats.c
> +++ b/libavformat/allformats.c
> @@ -150,6 +150,7 @@ extern const AVInputFormat  ff_dv_demuxer;
>  extern const FFOutputFormat ff_dv_muxer;
>  extern const AVInputFormat  ff_dvbsub_demuxer;
>  extern const AVInputFormat  ff_dvbtxt_demuxer;
> +extern const AVInputFormat  ff_dvdvideo_demuxer;
>  extern const AVInputFormat  ff_dxa_demuxer;
>  extern const AVInputFormat  ff_ea_demuxer;
>  extern const AVInputFormat  ff_ea_cdata_demuxer;
> diff --git a/libavformat/avlanguage.c b/libavformat/avlanguage.c
> index 782a58adb2..202d9aa835 100644
> --- a/libavformat/avlanguage.c
> +++ b/libavformat/avlanguage.c
> @@ -29,7 +29,7 @@ typedef struct LangEntry {
>      uint16_t next_equivalent;
>  } LangEntry;
>
> -static const uint16_t lang_table_counts[] = { 484, 20, 184 };
> +static const uint16_t lang_table_counts[] = { 484, 20, 190 };
>  static const uint16_t lang_table_offsets[] = { 0, 484, 504 };
>
>  static const LangEntry lang_table[] = {
> @@ -539,7 +539,7 @@ static const LangEntry lang_table[] = {
>      /*0501*/ { "slk",  647 },
>      /*0502*/ { "sqi",  652 },
>      /*0503*/ { "zho",  686 },
> -    /*----- AV_LANG_ISO639_1 entries (184) -----*/
> +    /*----- AV_LANG_ISO639_1 entries (190) -----*/
>      /*0504*/ { "aa" ,    0 },
>      /*0505*/ { "ab" ,    1 },
>      /*0506*/ { "ae" ,   33 },
> @@ -724,6 +724,12 @@ static const LangEntry lang_table[] = {
>      /*0685*/ { "za" ,  478 },
>      /*0686*/ { "zh" ,   78 },
>      /*0687*/ { "zu" ,  480 },
> +    /*0688*/ { "in" ,  195 }, /* deprecated */
> +    /*0689*/ { "iw" ,  172 }, /* deprecated */
> +    /*0690*/ { "ji" ,  472 }, /* deprecated */
> +    /*0691*/ { "jw" ,  202 }, /* deprecated */
> +    /*0692*/ { "mo" ,  358 }, /* deprecated */
> +    /*0693*/ { "sh" ,  693 }, /* deprecated (no equivalent) */
>      { "", 0 }
>  };
>
> diff --git a/libavformat/dvdvideodec.c b/libavformat/dvdvideodec.c
> new file mode 100644
> index 0000000000..c99adb0d70
> --- /dev/null
> +++ b/libavformat/dvdvideodec.c
> @@ -0,0 +1,1022 @@
> +/*
> + * DVD-Video demuxer, powered by libdvdnav and libdvdread
> + * Author: Marth64 <marth64@proxyid.net>
> + *
> + * 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
> + */
> +
> +/**
> + * DVD-Video is not a directly accessible, linear container format in the
> + * traditional sense. Instead, it allows for complex and programmatic
> + * playback of carefully muxed streams. A typical DVD player relies on
> + * user GUI interaction to drive the direction of the demuxing.
> + * Ultimately, the logical playback sequence is defined by a title's PGC
> + * and a user selected "angle". An additional layer of control is defined
> by
> + * NAV packets in the MPEG-PS, but as these are processed by libdvdnav,
> + * they are witheld from the output of this demuxer.
> + *
> + * Therefore, the high-level approach is as follows:
> + * 1) Open the volume with libdvdread
> + * 2) Gather information about the user-requested title and PGC
> coordinates
> + * 3) Request playback at the coordinates and chosen angle with libdvdnav
> + * 4) Seek playback to first cell at the coordinates (skipping stills,
> etc.)
> + * 5) Begin the playback (reading and demuxing) of MPEG-PS blocks
> + * 6) End playback if the PGC or angle change, or nav leads to a menu or
> backwards
> + * 7) Close resources
> + **/
> +
> +#include <dvdread/dvd_reader.h>
> +#include <dvdread/ifo_read.h>
> +#include <dvdread/ifo_types.h>
> +#include <dvdread/nav_read.h>
> +#include <dvdnav/dvdnav.h>
> +
> +#include "libavutil/avutil.h"
> +#include "libavutil/intreadwrite.h"
> +#include "libavutil/mem.h"
> +#include "libavutil/opt.h"
> +#include "libavutil/samplefmt.h"
> +#include "libavutil/timestamp.h"
> +
> +#include "libavcodec/avcodec.h"
> +#include "libavformat/avio_internal.h"
> +#include "libavformat/avlanguage.h"
> +#include "libavformat/avformat.h"
> +#include "libavformat/demux.h"
> +#include "libavformat/internal.h"
> +#include "libavformat/url.h"
> +
> +#define DVDVIDEO_MAX_PS_SEARCH_BLOCKS                   128
> +#define DVDVIDEO_BLOCK_SIZE                             2048
> +#define DVDVIDEO_TIME_BASE_Q                            (AVRational) { 1,
> 90000 }
> +#define DVDVIDEO_PTS_WRAP_BITS                          32 /* DVD uses 32
> (PES allows 33) */
> +
> +#define DVDVIDEO_SUBP_CLUT_LEN                          16
> +#define DVDVIDEO_SUBP_CLUT_SIZE
>  DVDVIDEO_SUBP_CLUT_LEN * sizeof(uint32_t)
> +
> +typedef struct DVDVideoVTSVideoStreamEntry {
> +    int startcode;
> +    enum AVCodecID codec_id;
> +    int width;
> +    int height;
> +    AVRational dar;
> +    AVRational framerate;
> +    int has_cc;
> +} DVDVideoVTSVideoStreamEntry;
> +
> +typedef struct DVDVideoPGCAudioStreamEntry {
> +    int startcode;
> +    enum AVCodecID codec_id;
> +    int sample_fmt;
> +    int sample_rate;
> +    int bit_depth;
> +    int nb_channels;
> +    AVChannelLayout ch_layout;
> +    char *lang_iso;
> +} DVDVideoPGCAudioStreamEntry;
> +
> +typedef struct DVDVideoPGCSubtitleStreamEntry {
> +    int startcode;
> +    uint32_t *clut;
> +    char *lang_iso;
> +} DVDVideoPGCSubtitleStreamEntry;
> +
> +typedef struct DVDVideoDemuxContext {
> +    const AVClass               *class;
> +
> +    /* options */
> +    int                         opt_title;          /* the user-provided
> title number (1-indexed) */
> +    int                         opt_ptt;            /* the user-provided
> PTT number (1-indexed) */
> +    int                         opt_pgc;            /* the user-provided
> PGC number (1-indexed) */
> +    int                         opt_pg;             /* the user-provided
> PG number (1-indexed) */
> +    int                         opt_angle;          /* the user-provided
> angle number (1-indexed) */
> +    int                         opt_region;         /* the user-provided
> region digit */
> +
> +    /* subdemux */
> +    const AVInputFormat         *mpeg_fmt;          /* inner MPEG-PS
> (VOB) demuxer */
> +    AVFormatContext             *mpeg_ctx;          /* context for inner
> demuxer */
> +    uint8_t                     *mpeg_buf;          /* buffer for inner
> demuxer */
> +    FFIOContext                 mpeg_pb;            /* buffer context for
> inner demuxer */
> +
> +    /* volume */
> +    dvd_reader_t                *dvdread;           /* handle to
> libdvdread */
> +    ifo_handle_t                *vmg_ifo;           /* handle to the VMG
> (VIDEO_TS.IFO) */
> +    ifo_handle_t                *vts_ifo;           /* handle to the
> active VTS (VTS_nn_n.IFO) */
> +    dvdnav_t                    *dvdnav;            /* handle to
> libdvdnav */
> +
> +    /* playback control */
> +    pgc_t                       *play_pgc;          /* handle to the
> active PGC */
> +    int64_t                     play_ts_offset;     /* PTS discontinuity
> offset (e.g. VOB change) */
> +    int64_t                     play_vobu_e_ptm;    /* end PTS of the
> current VOBU */
> +    int                         play_vtsn;          /* number of the
> active VTS (video title set) */
> +    int                         play_celln;         /* number of the
> active cell */
> +    int                         play_pgn;           /* number of the
> active program */
> +    int                         play_ptt;           /* number of the
> active PTT (chapter) */
> +    int                         play_in_vts;        /* if our play state
> is in the VTS */
> +    int                         play_in_pgc;        /* if our play state
> is in the PGC */
> +    int                         play_in_ps;         /* if our play state
> is in the program stream */
> +    int                         play_skip_cell;     /* if this cell is
> being skipped*/
> +    int                         play_skip_cell_last;/* if last cell was
> skipped */
> +
> +} DVDVideoDemuxContext;
> +
> +static void dvdvideo_pgc_close(AVFormatContext *s)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    av_log(s, AV_LOG_TRACE, "closing DVD volume\n");
> +
> +    if (c->dvdnav)
> +        dvdnav_close(c->dvdnav);
> +
> +    if (c->vts_ifo)
> +        ifoClose(c->vts_ifo);
> +
> +    if (c->vmg_ifo)
> +        ifoClose(c->vmg_ifo);
> +
> +    if (c->dvdread)
> +        DVDClose(c->dvdread);
> +}
> +
> +static int dvdvideo_pgc_open(AVFormatContext *s)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    dvdnav_status_t dvdnav_open_status;
> +
> +    title_info_t title_info;
> +    int cur_title, cur_pgcn, cur_pgn;
> +
> +    int32_t disc_region_mask;
> +    int32_t player_region_mask;
> +
> +    c->dvdread = DVDOpen(s->url);
> +    if (!c->dvdread)
> +        goto end_fail_external;
> +
> +    if (!(c->vmg_ifo = ifoOpen(c->dvdread, 0)))
> +        goto end_fail_external;
> +
> +    if (c->opt_title > c->vmg_ifo->tt_srpt->nr_of_srpts) {
> +        av_log(s, AV_LOG_ERROR, "Title not found\n");
> +
> +        return AVERROR_STREAM_NOT_FOUND;
> +    }
> +
> +    title_info = c->vmg_ifo->tt_srpt->title[c->opt_title - 1];
> +    if (c->opt_angle > title_info.nr_of_angles) {
> +        av_log(s, AV_LOG_ERROR, "Angle not found\n");
> +
> +        return AVERROR_STREAM_NOT_FOUND;
> +    }
> +
> +    if (title_info.nr_of_ptts < 1) {
> +        av_log(s, AV_LOG_ERROR, "Title invalid\n");
> +
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    if (!(c->vts_ifo = ifoOpen(c->dvdread, title_info.title_set_nr)))
> +        goto end_fail_external;
> +
> +    if (title_info.vts_ttn < 1
> +            || title_info.vts_ttn > 99
> +            || title_info.vts_ttn > c->vts_ifo->vts_ptt_srpt->nr_of_srpts
> +            || c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams > 8
> +            || c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams > 32) {
> +        av_log(s, AV_LOG_ERROR, "Title invalid in VTS\n");
> +
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    dvdnav_open_status = dvdnav_open(&c->dvdnav, s->url);
> +    if (!c->dvdnav)
> +        goto end_fail_external;
> +
> +    if (dvdnav_open_status != DVDNAV_STATUS_OK
> +            || dvdnav_set_readahead_flag(c->dvdnav, 0) != DVDNAV_STATUS_OK
> +            || dvdnav_set_PGC_positioning_flag(c->dvdnav, 1) !=
> DVDNAV_STATUS_OK
> +            || dvdnav_get_region_mask(c->dvdnav, &disc_region_mask) !=
> DVDNAV_STATUS_OK)
> +        goto end_fail_external;
> +
> +    player_region_mask = c->opt_region > 0 ? (1 << (c->opt_region - 1)) :
> disc_region_mask;
> +    if (dvdnav_set_region_mask(c->dvdnav, player_region_mask) !=
> DVDNAV_STATUS_OK)
> +        goto end_fail_external;
> +
> +    if (c->opt_pgc > 0 && c->opt_pg > 0) {
> +        if (dvdnav_program_play(c->dvdnav, c->opt_title, c->opt_pgc,
> c->opt_pg) != DVDNAV_STATUS_OK)
> +            goto end_fail_external;
> +    } else {
> +        if (dvdnav_part_play(c->dvdnav, c->opt_title, c->opt_ptt) !=
> DVDNAV_STATUS_OK
> +                || dvdnav_current_title_program(c->dvdnav, &cur_title,
> &cur_pgcn, &cur_pgn) != DVDNAV_STATUS_OK)
> +            goto end_fail_external;
> +
> +        c->opt_pgc = cur_pgcn;
> +        c->opt_pg = cur_pgn;
> +    }
> +
> +    if (dvdnav_angle_change(c->dvdnav, c->opt_angle) != DVDNAV_STATUS_OK)
> +        goto end_fail_external;
> +
> +    /* update the context */
> +    c->play_vtsn = title_info.title_set_nr;
> +    c->play_pgc = c->vts_ifo->vts_pgcit->pgci_srp[c->opt_pgc - 1].pgc;
> +
> +    if (c->play_pgc->pg_playback_mode != 0) {
> +        av_log(s, AV_LOG_ERROR, "Sorry, non-sequential PGCs are not
> supported\n");
> +
> +        return AVERROR_PATCHWELCOME;
> +    }
> +
> +    return 0;
> +
> +end_fail_external:
> +    av_log(s, AV_LOG_ERROR, "Unable to start DVD playback at
> coordinates\n");
> +
> +    return AVERROR_EXTERNAL;
> +}
> +
> +static int dvdvideo_pgc_chapters_setup(AVFormatContext *s)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    uint64_t duration;
> +    uint64_t *times;
> +    uint64_t time_prev = 0;
> +    int nb_chapters = 0;
> +
> +    /* as a side effect of dvdnav_describe_title_chapters(), beneficial
> safety checks are done */
> +    nb_chapters = dvdnav_describe_title_chapters(c->dvdnav, c->opt_title,
> &times, &duration);
> +    if (!nb_chapters)
> +        return AVERROR_EXTERNAL;
> +
> +    for (int i = c->opt_ptt - 1; i < nb_chapters - 1; i++) {
> +        uint64_t time_effective = times[i];
> +
> +        if (!avpriv_new_chapter(s, i, DVDVIDEO_TIME_BASE_Q, time_prev,
> time_effective, NULL)) {
> +            av_log(s, AV_LOG_ERROR, "Unable to allocate chapter\n");
> +            return AVERROR(ENOMEM);
> +        }
> +
> +        time_prev = time_effective;
> +    }
> +
> +    free(times);
> +
> +    s->duration = av_rescale_q(duration, DVDVIDEO_TIME_BASE_Q,
> AV_TIME_BASE_Q);
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_video_stream_analyze(video_attr_t video_attr,
> +        DVDVideoVTSVideoStreamEntry *entry)
> +{
> +    AVRational framerate;
> +    int height = 0;
> +    int width = 0;
> +    int is_pal = video_attr.video_format == 1;
> +
> +    framerate = is_pal ? (AVRational) { 25, 1 } : (AVRational) { 30000,
> 1001 };
> +    height = is_pal ? 576 : 480;
> +
> +    if (height > 0) {
> +        switch (video_attr.picture_size) {
> +            case 0: /* D1 */
> +                width = 720;
> +                break;
> +            case 1: /* 4CIF */
> +                width = 704;
> +                break;
> +            case 2: /* Half D1 */
> +                width = 352;
> +                break;
> +            case 3: /* CIF */
> +                width = 352;
> +                height /= 2;
> +                break;
> +        }
> +    }
> +
> +    if (!width || !height) {
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    entry->startcode = 0x1E0;
> +    entry->codec_id = !video_attr.mpeg_version ? AV_CODEC_ID_MPEG1VIDEO :
> AV_CODEC_ID_MPEG2VIDEO;
> +    entry->width = width;
> +    entry->height = height;
> +    entry->dar = video_attr.display_aspect_ratio ? (AVRational) { 16, 9 }
> : (AVRational) { 4, 3 };
> +    entry->framerate = framerate;
> +    entry->has_cc = video_attr.line21_cc_1 || video_attr.line21_cc_2;
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_video_stream_add(AVFormatContext *s,
> +        DVDVideoVTSVideoStreamEntry *entry, enum AVStreamParseType
> need_parsing)
> +{
> +    AVStream *st;
> +    FFStream *sti;
> +
> +    st = avformat_new_stream(s, NULL);
> +    if (!st) {
> +        return AVERROR(ENOMEM);
> +    }
> +
> +    st->id = entry->startcode;
> +    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
> +    st->codecpar->codec_id = entry->codec_id;
> +    st->codecpar->width = entry->width;
> +    st->codecpar->height = entry->height;
> +    st->codecpar->format = AV_PIX_FMT_YUV420P;
> +    st->codecpar->color_range = AVCOL_RANGE_MPEG;
> +
> +    st->codecpar->framerate = entry->framerate;
> +#if FF_API_R_FRAME_RATE
> +    st->r_frame_rate = entry->framerate;
> +#endif
> +    st->avg_frame_rate = entry->framerate;
> +
> +    sti = ffstream(st);
> +    sti->request_probe = 0;
> +    sti->need_parsing = need_parsing;
> +    sti->display_aspect_ratio = entry->dar;
> +    sti->avctx->framerate = entry->framerate;
> +
> +    if (entry->has_cc)
> +        sti->avctx->properties |= FF_CODEC_PROPERTY_CLOSED_CAPTIONS;
> +
> +    avpriv_set_pts_info(st, DVDVIDEO_PTS_WRAP_BITS,
> +        DVDVIDEO_TIME_BASE_Q.num, DVDVIDEO_TIME_BASE_Q.den);
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_video_stream_setup(AVFormatContext *s)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    DVDVideoVTSVideoStreamEntry *entry;
> +    int ret;
> +
> +    entry = av_malloc(sizeof(DVDVideoVTSVideoStreamEntry));
> +
> +    if ((ret =
> dvdvideo_video_stream_analyze(c->vts_ifo->vtsi_mat->vts_video_attr, entry))
> < 0
> +            || (ret = dvdvideo_video_stream_add(c->mpeg_ctx, entry,
> AVSTREAM_PARSE_FULL)) < 0
> +            || (ret = dvdvideo_video_stream_add(s, entry,
> AVSTREAM_PARSE_NONE)) < 0) {
> +        av_free(entry);
> +        av_log(s, AV_LOG_ERROR, "Unable to allocate video stream\n");
> +
> +        return ret;
> +    }
> +
> +    av_free(entry);
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_audio_stream_analyze(audio_attr_t audio_attr,
> uint16_t audio_control,
> +        DVDVideoPGCAudioStreamEntry *entry)
> +{
> +    int startcode = 0;
> +    enum AVCodecID codec_id = AV_CODEC_ID_NONE;
> +    int sample_fmt = AV_SAMPLE_FMT_NONE;
> +    int sample_rate = 0;
> +    int bit_depth = 0;
> +    int nb_channels = 0;
> +    AVChannelLayout ch_layout;
> +    char lang_dvd[3] = {0};
> +
> +    int position = (audio_control & 0x7F00) >> 8;
> +
> +    switch (audio_attr.audio_format) {
> +        case 0:
> +            codec_id = AV_CODEC_ID_AC3;
> +            sample_fmt = AV_SAMPLE_FMT_FLTP;
> +            sample_rate = 48000;
> +            startcode = 0x80 + position;
> +            break;
> +        case 2:
> +            codec_id = AV_CODEC_ID_MP1;
> +            sample_fmt = audio_attr.quantization ? AV_SAMPLE_FMT_S32 :
> AV_SAMPLE_FMT_S16;
> +            sample_rate = 48000;
> +            bit_depth = audio_attr.quantization ? 20 : 16;
> +            startcode = 0x1C0 + position;
> +            break;
> +        case 3:
> +            codec_id = AV_CODEC_ID_MP2;
> +            sample_fmt = audio_attr.quantization ? AV_SAMPLE_FMT_S32 :
> AV_SAMPLE_FMT_S16;
> +            sample_rate = 48000;
> +            bit_depth = audio_attr.quantization ? 20 : 16;
> +            startcode = 0x1C0 + position;
> +            break;
> +        case 4:
> +            codec_id = audio_attr.quantization ? AV_CODEC_ID_PCM_S32LE :
> AV_CODEC_ID_PCM_S16LE;
> +            sample_fmt = audio_attr.quantization ? AV_SAMPLE_FMT_S32 :
> AV_SAMPLE_FMT_S16;
> +            sample_rate = audio_attr.sample_frequency ? 96000 : 48000;
> +            bit_depth = audio_attr.quantization == 2 ? 24 :
> (audio_attr.quantization ? 20 : 16);
> +            startcode = 0xA0 + position;
> +            break;
> +        case 6:
> +            codec_id = AV_CODEC_ID_DTS;
> +            sample_fmt = AV_SAMPLE_FMT_FLTP;
> +            sample_rate = 48000;
> +            bit_depth = audio_attr.quantization == 2 ? 24 :
> (audio_attr.quantization ? 20 : 16);
> +            startcode = 0x88 + position;
> +            break;
> +    }
> +
> +    nb_channels = audio_attr.channels + 1;
> +
> +    if (codec_id == AV_CODEC_ID_NONE || startcode == 0 || sample_fmt ==
> AV_SAMPLE_FMT_NONE
> +            || sample_rate == 0 || nb_channels == 0) {
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    if (nb_channels == 2)
> +        ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
> +    else if (nb_channels == 6)
> +        ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_5POINT1;
> +    else if (nb_channels == 8)
> +        ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_7POINT1;
> +    else
> +        ch_layout = (AVChannelLayout) { 0 };
> +
> +    AV_WB16(lang_dvd, audio_attr.lang_code);
> +
> +    entry->startcode = startcode;
> +    entry->codec_id = codec_id;
> +    entry->sample_rate = sample_rate;
> +    entry->bit_depth = bit_depth;
> +    entry->nb_channels = nb_channels;
> +    entry->ch_layout = ch_layout;
> +    entry->lang_iso = (char *) ff_convert_lang_to(lang_dvd,
> AV_LANG_ISO639_2_BIBL);
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_audio_stream_add(AVFormatContext *s,
> DVDVideoPGCAudioStreamEntry *entry,
> +        enum AVStreamParseType need_parsing)
> +{
> +    AVStream *st;
> +    FFStream *sti;
> +
> +    st = avformat_new_stream(s, NULL);
> +    if (!st) {
> +        return AVERROR(ENOMEM);
> +    }
> +
> +    st->id = entry->startcode;
> +    st->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
> +    st->codecpar->codec_id = entry->codec_id;
> +    st->codecpar->format = entry->sample_fmt;
> +    st->codecpar->sample_rate = entry->sample_rate;
> +    st->codecpar->bits_per_coded_sample = entry->bit_depth;
> +    st->codecpar->bits_per_raw_sample = entry->bit_depth;
> +    st->codecpar->ch_layout = entry->ch_layout;
> +    st->codecpar->ch_layout.nb_channels = entry->nb_channels;
> +
> +    if (entry->lang_iso) {
> +        av_dict_set(&st->metadata, "language", entry->lang_iso, 0);
> +    }
> +
> +    sti = ffstream(st);
> +    sti->request_probe = 0;
> +    sti->need_parsing = need_parsing;
> +
> +    avpriv_set_pts_info(st, DVDVIDEO_PTS_WRAP_BITS,
> +            DVDVIDEO_TIME_BASE_Q.num, DVDVIDEO_TIME_BASE_Q.den);
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_audio_stream_add_all(AVFormatContext *s)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    int ret;
> +
> +    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams;
> i++) {
> +        DVDVideoPGCAudioStreamEntry *entry;
> +
> +        if (!(c->play_pgc->audio_control[i] & 0x8000))
> +            continue;
> +
> +        entry = av_malloc(sizeof(DVDVideoPGCAudioStreamEntry));
> +        if (!entry)
> +            return AVERROR(ENOMEM);
> +
> +        if ((ret =
> dvdvideo_audio_stream_analyze(c->vts_ifo->vtsi_mat->vts_audio_attr[i],
> +                c->play_pgc->audio_control[i], entry)) < 0)
> +            goto break_free_and_error;
> +
> +        if (c->vts_ifo->vtsi_mat->vts_audio_attr[i].application_mode == 1)
> +            av_log(s, AV_LOG_WARNING, "Audio %d: karaoke extension
> enabled\n", entry->startcode);
> +
> +        for (int j = 0; j < s->nb_streams; j++)
> +            if (s->streams[j]->id == entry->startcode)
> +                goto continue_free;
> +
> +        if ((ret = dvdvideo_audio_stream_add(c->mpeg_ctx, entry,
> AVSTREAM_PARSE_FULL)) < 0)
> +            goto break_free_and_error;
> +
> +        if ((ret = dvdvideo_audio_stream_add(s, entry,
> AVSTREAM_PARSE_NONE)) < 0)
> +            goto break_free_and_error;
> +
> +continue_free:
> +        av_free(entry);
> +        continue;
> +
> +break_free_and_error:
> +        av_free(entry);
> +        av_log(s, AV_LOG_ERROR, "Unable to allocate audio stream\n");
> +        return ret;
> +    }
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_subp_stream_analyze(AVFormatContext *s, uint32_t
> offset, subp_attr_t subp_attr,
> +        DVDVideoPGCSubtitleStreamEntry *entry)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    char lang_dvd[3] = {0};
> +
> +    entry->startcode = 0x20 + (offset & 0x1F);
> +
> +    entry->clut = av_mallocz(DVDVIDEO_SUBP_CLUT_SIZE);
> +    memcpy(entry->clut, c->play_pgc->palette, DVDVIDEO_SUBP_CLUT_SIZE);
> +
> +    // XXX: YUV2RGB conversion will be handled in upcoming dedicated patch
> +    // ff_dvdvideo_subp_clut_yuv_to_rgb(entry->clut,
> DVDVIDEO_SUBP_CLUT_SIZE);
> +
> +    AV_WB16(lang_dvd, subp_attr.lang_code);
> +    entry->lang_iso = (char *) ff_convert_lang_to(lang_dvd,
> AV_LANG_ISO639_2_BIBL);
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_subp_stream_add(AVFormatContext *s,
> +        DVDVideoPGCSubtitleStreamEntry *entry, enum AVStreamParseType
> need_parsing)
> +{
> +    AVStream *st;
> +    FFStream *sti;
> +
> +    st = avformat_new_stream(s, NULL);
> +    if (!st) {
> +        return AVERROR(ENOMEM);
> +    }
> +
> +    st->id = entry->startcode;
> +    st->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
> +    st->codecpar->codec_id = AV_CODEC_ID_DVD_SUBTITLE;
> +
> +    // XXX: Palettes will be fixed in dedicated patchset (with shared
> utility)
> +    // if ((ret = ff_alloc_extradata(st->codecpar,
> DVDVIDEO_SUBP_PALETTE_EXTRADATA_SIZE)) < 0)
> +    //     return ret;
> +    // ff_dvdvideo_subp_palette_extradata_cat(entry->clut,
> DVDVIDEO_SUBP_CLUT_SIZE,
> +    //         st->codecpar->extradata, st->codecpar->extradata_size);
> +
> +    if (entry->lang_iso)
> +        av_dict_set(&st->metadata, "language", entry->lang_iso, 0);
> +
> +    sti = ffstream(st);
> +    sti->request_probe = 0;
> +    sti->need_parsing = need_parsing;
> +
> +    avpriv_set_pts_info(st, DVDVIDEO_PTS_WRAP_BITS,
> +        DVDVIDEO_TIME_BASE_Q.num, DVDVIDEO_TIME_BASE_Q.den);
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_subp_stream_add_internal(AVFormatContext *s,
> +        uint32_t offset, subp_attr_t subp_attr)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    DVDVideoPGCSubtitleStreamEntry *entry;
> +    int ret = 0;
> +
> +    entry = av_malloc(sizeof(DVDVideoPGCSubtitleStreamEntry));
> +
> +    if ((ret = dvdvideo_subp_stream_analyze(s, offset, subp_attr, entry))
> < 0)
> +        goto end_free_error;
> +
> +    for (int i = 0; i < s->nb_streams; i++)
> +        if (s->streams[i]->id == entry->startcode)
> +            goto end_free;
> +
> +    if ((ret = dvdvideo_subp_stream_add(c->mpeg_ctx, entry,
> AVSTREAM_PARSE_FULL)) < 0
> +            || (ret = dvdvideo_subp_stream_add(s, entry,
> AVSTREAM_PARSE_NONE)) < 0)
> +        goto end_free_error;
> +
> +    goto end_free;
> +
> +end_free_error:
> +    av_log(s, AV_LOG_ERROR, "Unable to allocate subtitle stream\n");
> +
> +end_free:
> +    av_free(entry->clut);
> +    av_free(entry);
> +
> +    return ret;
> +}
> +
> +static int dvdvideo_subp_stream_add_all(AVFormatContext *s)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    if (!c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams)
> +        return 0;
> +
> +    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams;
> i++) {
> +        int ret;
> +        uint32_t subp_control;
> +        subp_attr_t subp_attr;
> +        video_attr_t video_attr;
> +
> +        subp_control = c->play_pgc->subp_control[i];
> +        if (!(subp_control & 0x80000000))
> +            continue;
> +
> +        /* there can be several presentations for one SPU */
> +        /* for now, be flexible with the DAR check due to weird authoring
> */
> +        video_attr = c->vts_ifo->vtsi_mat->vts_video_attr;
> +        subp_attr = c->vts_ifo->vtsi_mat->vts_subp_attr[i];
> +
> +        /* 4:3 */
> +        if (!video_attr.display_aspect_ratio) {
> +            if ((ret = dvdvideo_subp_stream_add_internal(s, subp_control
> >> 24, subp_attr)) < 0)
> +                return ret;
> +
> +            continue;
> +        }
> +
> +        /* 16:9 */
> +        if ((ret = dvdvideo_subp_stream_add_internal(s, subp_control >>
> 16, subp_attr)) < 0)
> +            return ret;
> +
> +        /* 16:9 letterbox */
> +        if (video_attr.permitted_df == 2 || video_attr.permitted_df == 0)
> +            if ((ret = dvdvideo_subp_stream_add_internal(s, subp_control
> >> 8, subp_attr)) < 0)
> +                return ret;
> +
> +        /* 16:9 pan-and-scan */
> +        if (video_attr.permitted_df == 1 || video_attr.permitted_df == 0)
> +            if ((ret = dvdvideo_subp_stream_add_internal(s, subp_control,
> subp_attr)) < 0)
> +                return ret;
> +    }
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_subdemux_read_data(void *opaque, uint8_t *buf, int
> buf_size)
> +{
> +    AVFormatContext *s = opaque;
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    uint8_t nav_buf[DVDVIDEO_BLOCK_SIZE] = {0};
> +    int nav_event;
> +    int nav_len;
> +
> +    dvdnav_vts_change_event_t *e_vts;
> +    dvdnav_cell_change_event_t *e_cell;
> +    int cur_title, cur_pgcn, cur_pgn, cur_angle, cur_title_unused,
> cur_ptt, cur_nb_angles;
> +    pci_t *p_pci;
> +
> +    if (buf_size != DVDVIDEO_BLOCK_SIZE) {
> +        av_log(s, AV_LOG_ERROR, "Invalid buffer size\n");
> +
> +        return AVERROR(ENOMEM);
> +    }
> +
> +    for (int i = 0; i < DVDVIDEO_MAX_PS_SEARCH_BLOCKS; i++) {
> +        if (ff_check_interrupt(&s->interrupt_callback))
> +            return AVERROR_EXIT;
> +
> +        if (dvdnav_get_next_block(c->dvdnav, nav_buf, &nav_event,
> &nav_len) != DVDNAV_STATUS_OK)
> +            return AVERROR_EXTERNAL;
> +
> +        if (nav_len > DVDVIDEO_BLOCK_SIZE)
> +            return AVERROR_INVALIDDATA;
> +
> +        if (dvdnav_current_title_program(c->dvdnav, &cur_title,
> &cur_pgcn, &cur_pgn) != DVDNAV_STATUS_OK)
> +            return AVERROR_EXTERNAL;
> +
> +        if (dvdnav_current_title_info(c->dvdnav, &cur_title_unused,
> &cur_ptt) != DVDNAV_STATUS_OK)
> +            return AVERROR_EXTERNAL;
> +
> +        if (dvdnav_get_angle_info(c->dvdnav, &cur_angle, &cur_nb_angles)
> != DVDNAV_STATUS_OK)
> +            return AVERROR_EXTERNAL;
> +
> +        av_log(s, AV_LOG_TRACE, "new block: i=%d nav_event=%d nav_len=%d "
> +                                "cur_title=%d cur_ptt=%d cur_angle=%d
> cur_celln=%d cur_pgcn=%d cur_pgn=%d "
> +                                "play_in_vts=%d play_in_pgc=%d
> play_in_ps=%d\n",
> +                                i, nav_event, nav_len,
> +                                cur_title, cur_ptt, cur_angle,
> c->play_celln, cur_pgcn, cur_pgn,
> +                                c->play_in_vts, c->play_in_pgc,
> c->play_in_ps);
> +
> +        if (c->play_in_pgc && (cur_pgcn != c->opt_pgc ||
> !dvdnav_is_domain_vts(c->dvdnav)))
> +            return AVERROR_EOF;
> +
> +        switch (nav_event) {
> +            case DVDNAV_VTS_CHANGE:
> +                if (c->play_in_vts)
> +                    return AVERROR_EOF;
> +
> +                e_vts = (dvdnav_vts_change_event_t *) nav_buf;
> +
> +                if (e_vts->new_vtsN == c->play_vtsn && e_vts->new_domain
> == DVD_DOMAIN_VTSTitle)
> +                    c->play_in_vts = 1;
> +
> +                continue;
> +            case DVDNAV_CELL_CHANGE:
> +                if (!c->play_in_vts)
> +                    continue;
> +
> +                e_cell = (dvdnav_cell_change_event_t *) nav_buf;
> +
> +                av_log(s, AV_LOG_TRACE, "new cell: prev=%d new=%d\n",
> c->play_celln, e_cell->cellN);
> +
> +                if (!c->play_in_ps && !c->play_in_pgc) {
> +                    if (cur_title == c->opt_title && cur_ptt == c->opt_ptt
> +                            && cur_pgcn == c->opt_pgc && cur_pgn ==
> c->opt_pg) {
> +                        c->play_in_pgc = 1;
> +                    }
> +                } else if (c->play_celln > e_cell->cellN || c->play_pgn >
> cur_pgn)
> +                    return AVERROR_EOF;
> +
> +                c->play_celln = e_cell->cellN;
> +                c->play_pgn = cur_pgn;
> +                c->play_skip_cell_last = c->play_skip_cell;
> +                /* XXX: should we be skipping restricted cells
> mid-stream? */
> +                c->play_skip_cell =
> c->play_pgc->cell_playback[e_cell->cellN - 1].restricted != 0;
> +
> +                if (c->play_skip_cell)
> +                    av_log(s, AV_LOG_TRACE, "cell skipped: cell=%d\n",
> e_cell->cellN);
> +
> +                continue;
> +            case DVDNAV_NAV_PACKET:
> +                if (!c->play_in_pgc)
> +                    continue;
> +
> +                c->play_ptt = cur_ptt;
> +
> +                p_pci = dvdnav_get_current_nav_pci(c->dvdnav);
> +
> +                if (!c->play_in_ps) {
> +                    /* XXX: this should shift start time to 0, but not
> working on all discs */
> +                    c->play_ts_offset -= p_pci->pci_gi.vobu_s_ptm;
> +                    c->play_in_ps = 1;
> +
> +                    continue;
> +                }
> +
> +                if (cur_ptt < c->play_ptt)
> +                    return AVERROR_EOF;
> +
> +                if (c->play_skip_cell)
> +                    c->play_ts_offset -= p_pci->pci_gi.vobu_e_ptm;
> +                else if (p_pci->pci_gi.vobu_s_ptm < c->play_vobu_e_ptm) {
> +                    av_log(s, AV_LOG_TRACE, "discontinuity: adjusting
> timestamps\n");
> +
> +                    c->play_ts_offset += c->play_vobu_e_ptm;
> +
> +                    if (!c->play_skip_cell_last) {
> +                        avio_flush(&c->mpeg_pb.pub);
> +                        ff_read_frame_flush(c->mpeg_ctx);
> +                    }
> +                }
> +
> +                c->play_vobu_e_ptm = p_pci->pci_gi.vobu_e_ptm;
> +
> +                continue;
> +            case DVDNAV_BLOCK_OK:
> +                if (!c->play_in_ps || c->play_skip_cell)
> +                    continue;
> +
> +                if (nav_len != DVDVIDEO_BLOCK_SIZE)
> +                    return AVERROR_INVALIDDATA;
> +
> +                if (cur_angle != c->opt_angle) {
> +                    av_log(s, AV_LOG_ERROR, "Unexpected angle change\n");
> +
> +                    return AVERROR_INPUT_CHANGED;
> +                }
> +
> +                memcpy(buf, &nav_buf, nav_len);
> +
> +                return nav_len;
> +            case DVDNAV_STILL_FRAME:
> +                if (c->play_in_ps)
> +                    return AVERROR_EOF;
> +
> +                dvdnav_still_skip(c->dvdnav);
> +
> +                continue;
> +            case DVDNAV_WAIT:
> +                if (c->play_in_ps)
> +                    return AVERROR_EOF;
> +
> +                dvdnav_wait_skip(c->dvdnav);
> +
> +                continue;
> +            case DVDNAV_HOP_CHANNEL:
> +            case DVDNAV_HIGHLIGHT:
> +                if (c->play_in_ps)
> +                    return AVERROR_EOF;
> +
> +                continue;
> +            case DVDNAV_STOP:
> +                return AVERROR_EOF;
> +            default:
> +                continue;
> +        }
> +    }
> +
> +    av_log(s, AV_LOG_ERROR, "Unable to find next program stream block\n");
> +
> +    return AVERROR_EOF;
> +}
> +
> +static void dvdvideo_subdemux_close(AVFormatContext *s)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    av_freep(&c->mpeg_pb.pub.buffer);
> +    av_freep(&c->mpeg_pb);
> +    avformat_close_input(&c->mpeg_ctx);
> +}
> +
> +static int dvdvideo_subdemux_open(AVFormatContext *s)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    int ret;
> +
> +    const AVInputFormat *mpeg_fmt = NULL;
> +    AVFormatContext *mpeg_ctx = NULL;
> +    uint8_t *mpeg_buf = NULL;
> +
> +    if (!(mpeg_fmt = av_find_input_format("mpeg")))
> +        return AVERROR_DEMUXER_NOT_FOUND;
> +
> +    if (!(mpeg_ctx = avformat_alloc_context()))
> +        return AVERROR(ENOMEM);
> +
> +    if (!(mpeg_buf = av_malloc(DVDVIDEO_BLOCK_SIZE))) {
> +        avformat_free_context(mpeg_ctx);
> +
> +        return AVERROR(ENOMEM);
> +    }
> +
> +    ffio_init_context(&c->mpeg_pb, mpeg_buf, DVDVIDEO_BLOCK_SIZE, 0, s,
> +            dvdvideo_subdemux_read_data, NULL, NULL);
> +    c->mpeg_pb.pub.seekable = 0;
> +
> +    if ((ret = ff_copy_whiteblacklists(mpeg_ctx, s)) < 0) {
> +        avformat_free_context(mpeg_ctx);
> +
> +        return ret;
> +    }
> +
> +    mpeg_ctx->flags = AVFMT_FLAG_CUSTOM_IO | AVFMT_FLAG_GENPTS;
> +    mpeg_ctx->ctx_flags |= AVFMTCTX_UNSEEKABLE;
> +    mpeg_ctx->probesize = 0;
> +    mpeg_ctx->max_analyze_duration = 0;
> +    mpeg_ctx->interrupt_callback = s->interrupt_callback;
> +    mpeg_ctx->pb = &c->mpeg_pb.pub;
> +    mpeg_ctx->io_open = NULL;
> +
> +    if ((ret = avformat_open_input(&mpeg_ctx, "", mpeg_fmt, NULL)) < 0) {
> +        avformat_free_context(mpeg_ctx);
> +
> +        return ret;
> +    }
> +
> +    c->mpeg_fmt = mpeg_fmt;
> +    c->mpeg_ctx = mpeg_ctx;
> +    c->mpeg_buf = mpeg_buf;
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_read_header(AVFormatContext *s)
> +{
> +    int ret;
> +
> +    if ((ret = dvdvideo_pgc_open(s)) < 0)
> +        return ret;
> +
> +    if ((ret = dvdvideo_pgc_chapters_setup(s)) < 0)
> +        return ret;
> +
> +    if ((ret = dvdvideo_subdemux_open(s)) < 0)
> +        return ret;
> +
> +    if ((ret = dvdvideo_video_stream_setup(s)) < 0)
> +        return ret;
> +
> +    if ((ret = dvdvideo_audio_stream_add_all(s)) < 0)
> +        return ret;
> +
> +    if ((ret = dvdvideo_subp_stream_add_all(s)) < 0)
> +        return ret;
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_read_packet(AVFormatContext *s, AVPacket *pkt)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +
> +    int ret;
> +
> +    ret = av_read_frame(c->mpeg_ctx, pkt);
> +
> +    if (ret >= 0) {
> +        if (c->mpeg_ctx->nb_streams != s->nb_streams) {
> +            av_log(s, AV_LOG_ERROR, "Unexpected new stream in
> playback\n");
> +
> +            return AVERROR_INPUT_CHANGED;
> +        }
> +
> +        if (pkt->pts != AV_NOPTS_VALUE)
> +            pkt->pts += c->play_ts_offset;
> +        if (pkt->dts != AV_NOPTS_VALUE)
> +            pkt->dts += c->play_ts_offset;
> +
> +        return 0;
> +    }
> +
> +    return ret;
> +}
> +
> +static int dvdvideo_read_seek(AVFormatContext *s, int stream_index,
> +        int64_t timestamp, int flags)
> +{
> +    return AVERROR_PATCHWELCOME;
> +}
> +
> +static int dvdvideo_close(AVFormatContext *s)
> +{
> +    dvdvideo_subdemux_close(s);
> +    dvdvideo_pgc_close(s);
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_probe(const AVProbeData *p)
> +{
> +    /* PATCHWELCOME: not implemented at this time */
> +    return 0;
> +}
> +
> +#define OFFSET(x) offsetof(DVDVideoDemuxContext, x)
> +static const AVOption dvdvideo_options[] = {
> +    {"title",   "Title Number",                     OFFSET(opt_title),
>   AV_OPT_TYPE_INT,    { .i64=1 },     1,          99,
> AV_OPT_FLAG_DECODING_PARAM },
> +    {"chapter", "Entry Chapter (PTT) Number",       OFFSET(opt_ptt),
>   AV_OPT_TYPE_INT,    { .i64=1 },     1,          99,
> AV_OPT_FLAG_DECODING_PARAM },
> +    {"pgc",     "Entry PGC Number (0=auto)",        OFFSET(opt_pgc),
>   AV_OPT_TYPE_INT,    { .i64=0 },     0,          32767,
>  AV_OPT_FLAG_DECODING_PARAM },
> +    {"pg",      "Entry PG Number (0=auto)",         OFFSET(opt_pg),
>    AV_OPT_TYPE_INT,    { .i64=0 },     0,          255,
>  AV_OPT_FLAG_DECODING_PARAM },
> +    {"angle",   "Video Angle Number",               OFFSET(opt_angle),
>   AV_OPT_TYPE_INT,    { .i64=1 },     1,          9,
>  AV_OPT_FLAG_DECODING_PARAM },
> +    {"region",  "Playback Region Number (0=free)",  OFFSET(opt_region),
>    AV_OPT_TYPE_INT,    { .i64=0 },     0,          8,
>  AV_OPT_FLAG_DECODING_PARAM },
> +    {NULL}
> +};
> +
> +static const AVClass dvdvideo_class = {
> +    .class_name = "DVD-Video demuxer",
> +    .item_name  = av_default_item_name,
> +    .option     = dvdvideo_options,
> +    .version    = LIBAVUTIL_VERSION_INT
> +};
> +
> +const AVInputFormat ff_dvdvideo_demuxer = {
> +    .name           = "dvdvideo",
> +    .long_name      = NULL_IF_CONFIG_SMALL("DVD-Video"),
> +    .priv_class     = &dvdvideo_class,
> +    .priv_data_size = sizeof(DVDVideoDemuxContext),
> +    .flags          = AVFMT_NOFILE | AVFMT_SHOW_IDS | AVFMT_TS_DISCONT |
> AVFMT_NO_BYTE_SEEK | AVFMT_NOGENSEARCH | AVFMT_NOBINSEARCH,
> +    .flags_internal = FF_FMT_INIT_CLEANUP,
> +    .read_probe     = dvdvideo_probe,
> +    .read_close     = dvdvideo_close,
> +    .read_header    = dvdvideo_read_header,
> +    .read_packet    = dvdvideo_read_packet,
> +    .read_seek      = dvdvideo_read_seek
> +};
> --
> 2.34.1
>
>
Nicolas George Jan. 10, 2024, 10:16 a.m. UTC | #2
Marth64 (12024-01-10):
> I will add that I still had to maintain a dependency on GENPTS for the
> inner MPEG demuxer. This is because dvdnav_get_current_time() does not
> provide exact stream-level PTS. However, the crashing/hanging is fixed (by
> parsing NAV packets), and the output is produced smoothly. GENPTS fills in
> the blanks.
> 
> If anyone can think of a better way, please let me know. Right now, it
> works well.

You could imitate what tools/dvd2concat does internally: use
libdvdread to get the structure of the DVD but let the concat demuxer
handle all the actual demuxing work.

Regards,
Marth64 Jan. 10, 2024, 4:25 p.m. UTC | #3
Thank you, Nicolas. I had tinkered with concat this way in the past as PGCs
are indeed just a bunch of segments mushed together, but I had run into
issues specifically with (1) concerns about forced subtitles that pop up
mid stream and (2) unable to figure out custom IO with it. Since dvdnav is
driving the navigation and reading of the disc, I do not have the luxury of
knowing VOB addresses for a perfect read of the title (without fully
playing back). So I don’t have a list of files and sectors to give concat.
As DVD also has cell commands that drive direction of the stream, this
complicates it further. The structure revealed by dvdread alone is not
enough to properly play back; the dvdnav VM plays a key role. Finally, I
had also worry about oddities when having demuxer->submuxer->subsubdemuxer.

In my approach (using mpeg demux directly) there doesn’t seem to be any
issue with concatenation/handling the gaps, and I am flushing the
subdemuxer when they occur. Just the GENPTS sticks out as being a non-ideal
thing to do.

That said, I will take another look and see if I misunderstood at the time.

Best,

On Wed, Jan 10, 2024 at 04:17 Nicolas George <george@nsup.org> wrote:

> Marth64 (12024-01-10):
> > I will add that I still had to maintain a dependency on GENPTS for the
> > inner MPEG demuxer. This is because dvdnav_get_current_time() does not
> > provide exact stream-level PTS. However, the crashing/hanging is fixed
> (by
> > parsing NAV packets), and the output is produced smoothly. GENPTS fills
> in
> > the blanks.
> >
> > If anyone can think of a better way, please let me know. Right now, it
> > works well.
>
> You could imitate what tools/dvd2concat does internally: use
> libdvdread to get the structure of the DVD but let the concat demuxer
> handle all the actual demuxing work.
>
> Regards,
>
> --
>   Nicolas George
>
Marth64 Jan. 10, 2024, 4:48 p.m. UTC | #4
To elaborate slightly: GENPTS is working within each segment, whose start
and end times I have from NAV packets

On Wed, Jan 10, 2024 at 10:25 Marth64 <marth64@proxyid.net> wrote:

> Thank you, Nicolas. I had tinkered with concat this way in the past as
> PGCs are indeed just a bunch of segments mushed together, but I had run
> into issues specifically with (1) concerns about forced subtitles that pop
> up mid stream and (2) unable to figure out custom IO with it. Since dvdnav
> is driving the navigation and reading of the disc, I do not have the luxury
> of knowing VOB addresses for a perfect read of the title (without fully
> playing back). So I don’t have a list of files and sectors to give concat.
> As DVD also has cell commands that drive direction of the stream, this
> complicates it further. The structure revealed by dvdread alone is not
> enough to properly play back; the dvdnav VM plays a key role. Finally, I
> had also worry about oddities when having demuxer->submuxer->subsubdemuxer.
>
> In my approach (using mpeg demux directly) there doesn’t seem to be any
> issue with concatenation/handling the gaps, and I am flushing the
> subdemuxer when they occur. Just the GENPTS sticks out as being a non-ideal
> thing to do.
>
> That said, I will take another look and see if I misunderstood at the time.
>
> Best,
>
> On Wed, Jan 10, 2024 at 04:17 Nicolas George <george@nsup.org> wrote:
>
>> Marth64 (12024-01-10):
>> > I will add that I still had to maintain a dependency on GENPTS for the
>> > inner MPEG demuxer. This is because dvdnav_get_current_time() does not
>> > provide exact stream-level PTS. However, the crashing/hanging is fixed
>> (by
>> > parsing NAV packets), and the output is produced smoothly. GENPTS fills
>> in
>> > the blanks.
>> >
>> > If anyone can think of a better way, please let me know. Right now, it
>> > works well.
>>
>> You could imitate what tools/dvd2concat does internally: use
>> libdvdread to get the structure of the DVD but let the concat demuxer
>> handle all the actual demuxing work.
>>
>> Regards,
>>
>> --
>>   Nicolas George
>>
>
diff mbox series

Patch

diff --git a/Changelog b/Changelog
index 5b2899d05b..1b377fed2f 100644
--- a/Changelog
+++ b/Changelog
@@ -18,6 +18,7 @@  version <next>:
 - lavu/eval: introduce randomi() function in expressions
 - VVC decoder
 - fsync filter
+- DVD-Video demuxer, powered by libdvdnav and libdvdread
 
 version 6.1:
 - libaribcaption decoder
diff --git a/configure b/configure
index e87a09ce83..1f21f4f1c2 100755
--- a/configure
+++ b/configure
@@ -227,6 +227,8 @@  External library support:
   --enable-libdavs2        enable AVS2 decoding via libdavs2 [no]
   --enable-libdc1394       enable IIDC-1394 grabbing using libdc1394
                            and libraw1394 [no]
+  --enable-libdvdnav       enable libdvdnav, needed for DVD demuxing [no]
+  --enable-libdvdread       enable libdvdread, needed for DVD demuxing [no]
   --enable-libfdk-aac      enable AAC de/encoding via libfdk-aac [no]
   --enable-libflite        enable flite (voice synthesis) support via libflite [no]
   --enable-libfontconfig   enable libfontconfig, useful for drawtext filter [no]
@@ -1806,6 +1808,8 @@  EXTERNAL_LIBRARY_GPL_LIST="
     frei0r
     libcdio
     libdavs2
+    libdvdnav
+    libdvdread
     librubberband
     libvidstab
     libx264
@@ -3519,6 +3523,8 @@  dts_demuxer_select="dca_parser"
 dtshd_demuxer_select="dca_parser"
 dv_demuxer_select="dvprofile"
 dv_muxer_select="dvprofile"
+dvdvideo_demuxer_select="mpegps_demuxer"
+dvdvideo_demuxer_deps="libdvdnav libdvdread"
 dxa_demuxer_select="riffdec"
 eac3_demuxer_select="ac3_parser"
 evc_demuxer_select="evc_frame_merge_bsf evc_parser"
@@ -6760,6 +6766,8 @@  enabled libdav1d          && require_pkg_config libdav1d "dav1d >= 0.5.0" "dav1d
 enabled libdavs2          && require_pkg_config libdavs2 "davs2 >= 1.6.0" davs2.h davs2_decoder_open
 enabled libdc1394         && require_pkg_config libdc1394 libdc1394-2 dc1394/dc1394.h dc1394_new
 enabled libdrm            && check_pkg_config libdrm libdrm xf86drm.h drmGetVersion
+enabled libdvdnav         && require_pkg_config libdvdnav "dvdnav >= 6.1.1" dvdnav/dvdnav.h dvdnav_open2
+enabled libdvdread        && require_pkg_config libdvdread "dvdread >= 6.1.2" dvdread/dvd_reader.h DVDOpen2 -ldvdread
 enabled libfdk_aac        && { check_pkg_config libfdk_aac fdk-aac "fdk-aac/aacenc_lib.h" aacEncOpen ||
                                { require libfdk_aac fdk-aac/aacenc_lib.h aacEncOpen -lfdk-aac &&
                                  warn "using libfdk without pkg-config"; } }
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 581e378d95..3c1cb21fe2 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -192,6 +192,7 @@  OBJS-$(CONFIG_DTS_MUXER)                 += rawenc.o
 OBJS-$(CONFIG_DV_MUXER)                  += dvenc.o
 OBJS-$(CONFIG_DVBSUB_DEMUXER)            += dvbsub.o rawdec.o
 OBJS-$(CONFIG_DVBTXT_DEMUXER)            += dvbtxt.o rawdec.o
+OBJS-$(CONFIG_DVDVIDEO_DEMUXER)          += dvdvideodec.o
 OBJS-$(CONFIG_DXA_DEMUXER)               += dxa.o
 OBJS-$(CONFIG_EA_CDATA_DEMUXER)          += eacdata.o
 OBJS-$(CONFIG_EA_DEMUXER)                += electronicarts.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index ce6be5f04d..ea88d4c094 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -150,6 +150,7 @@  extern const AVInputFormat  ff_dv_demuxer;
 extern const FFOutputFormat ff_dv_muxer;
 extern const AVInputFormat  ff_dvbsub_demuxer;
 extern const AVInputFormat  ff_dvbtxt_demuxer;
+extern const AVInputFormat  ff_dvdvideo_demuxer;
 extern const AVInputFormat  ff_dxa_demuxer;
 extern const AVInputFormat  ff_ea_demuxer;
 extern const AVInputFormat  ff_ea_cdata_demuxer;
diff --git a/libavformat/avlanguage.c b/libavformat/avlanguage.c
index 782a58adb2..202d9aa835 100644
--- a/libavformat/avlanguage.c
+++ b/libavformat/avlanguage.c
@@ -29,7 +29,7 @@  typedef struct LangEntry {
     uint16_t next_equivalent;
 } LangEntry;
 
-static const uint16_t lang_table_counts[] = { 484, 20, 184 };
+static const uint16_t lang_table_counts[] = { 484, 20, 190 };
 static const uint16_t lang_table_offsets[] = { 0, 484, 504 };
 
 static const LangEntry lang_table[] = {
@@ -539,7 +539,7 @@  static const LangEntry lang_table[] = {
     /*0501*/ { "slk",  647 },
     /*0502*/ { "sqi",  652 },
     /*0503*/ { "zho",  686 },
-    /*----- AV_LANG_ISO639_1 entries (184) -----*/
+    /*----- AV_LANG_ISO639_1 entries (190) -----*/
     /*0504*/ { "aa" ,    0 },
     /*0505*/ { "ab" ,    1 },
     /*0506*/ { "ae" ,   33 },
@@ -724,6 +724,12 @@  static const LangEntry lang_table[] = {
     /*0685*/ { "za" ,  478 },
     /*0686*/ { "zh" ,   78 },
     /*0687*/ { "zu" ,  480 },
+    /*0688*/ { "in" ,  195 }, /* deprecated */
+    /*0689*/ { "iw" ,  172 }, /* deprecated */
+    /*0690*/ { "ji" ,  472 }, /* deprecated */
+    /*0691*/ { "jw" ,  202 }, /* deprecated */
+    /*0692*/ { "mo" ,  358 }, /* deprecated */
+    /*0693*/ { "sh" ,  693 }, /* deprecated (no equivalent) */
     { "", 0 }
 };
 
diff --git a/libavformat/dvdvideodec.c b/libavformat/dvdvideodec.c
new file mode 100644
index 0000000000..c99adb0d70
--- /dev/null
+++ b/libavformat/dvdvideodec.c
@@ -0,0 +1,1022 @@ 
+/*
+ * DVD-Video demuxer, powered by libdvdnav and libdvdread
+ * Author: Marth64 <marth64@proxyid.net>
+ *
+ * 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
+ */
+
+/**
+ * DVD-Video is not a directly accessible, linear container format in the
+ * traditional sense. Instead, it allows for complex and programmatic
+ * playback of carefully muxed streams. A typical DVD player relies on
+ * user GUI interaction to drive the direction of the demuxing.
+ * Ultimately, the logical playback sequence is defined by a title's PGC
+ * and a user selected "angle". An additional layer of control is defined by
+ * NAV packets in the MPEG-PS, but as these are processed by libdvdnav,
+ * they are witheld from the output of this demuxer.
+ *
+ * Therefore, the high-level approach is as follows:
+ * 1) Open the volume with libdvdread
+ * 2) Gather information about the user-requested title and PGC coordinates
+ * 3) Request playback at the coordinates and chosen angle with libdvdnav
+ * 4) Seek playback to first cell at the coordinates (skipping stills, etc.)
+ * 5) Begin the playback (reading and demuxing) of MPEG-PS blocks
+ * 6) End playback if the PGC or angle change, or nav leads to a menu or backwards
+ * 7) Close resources
+ **/
+
+#include <dvdread/dvd_reader.h>
+#include <dvdread/ifo_read.h>
+#include <dvdread/ifo_types.h>
+#include <dvdread/nav_read.h>
+#include <dvdnav/dvdnav.h>
+
+#include "libavutil/avutil.h"
+#include "libavutil/intreadwrite.h"
+#include "libavutil/mem.h"
+#include "libavutil/opt.h"
+#include "libavutil/samplefmt.h"
+#include "libavutil/timestamp.h"
+
+#include "libavcodec/avcodec.h"
+#include "libavformat/avio_internal.h"
+#include "libavformat/avlanguage.h"
+#include "libavformat/avformat.h"
+#include "libavformat/demux.h"
+#include "libavformat/internal.h"
+#include "libavformat/url.h"
+
+#define DVDVIDEO_MAX_PS_SEARCH_BLOCKS                   128
+#define DVDVIDEO_BLOCK_SIZE                             2048
+#define DVDVIDEO_TIME_BASE_Q                            (AVRational) { 1, 90000 }
+#define DVDVIDEO_PTS_WRAP_BITS                          32 /* DVD uses 32 (PES allows 33) */
+
+#define DVDVIDEO_SUBP_CLUT_LEN                          16
+#define DVDVIDEO_SUBP_CLUT_SIZE                         DVDVIDEO_SUBP_CLUT_LEN * sizeof(uint32_t)
+
+typedef struct DVDVideoVTSVideoStreamEntry {
+    int startcode;
+    enum AVCodecID codec_id;
+    int width;
+    int height;
+    AVRational dar;
+    AVRational framerate;
+    int has_cc;
+} DVDVideoVTSVideoStreamEntry;
+
+typedef struct DVDVideoPGCAudioStreamEntry {
+    int startcode;
+    enum AVCodecID codec_id;
+    int sample_fmt;
+    int sample_rate;
+    int bit_depth;
+    int nb_channels;
+    AVChannelLayout ch_layout;
+    char *lang_iso;
+} DVDVideoPGCAudioStreamEntry;
+
+typedef struct DVDVideoPGCSubtitleStreamEntry {
+    int startcode;
+    uint32_t *clut;
+    char *lang_iso;
+} DVDVideoPGCSubtitleStreamEntry;
+
+typedef struct DVDVideoDemuxContext {
+    const AVClass               *class;
+
+    /* options */
+    int                         opt_title;          /* the user-provided title number (1-indexed) */
+    int                         opt_ptt;            /* the user-provided PTT number (1-indexed) */
+    int                         opt_pgc;            /* the user-provided PGC number (1-indexed) */
+    int                         opt_pg;             /* the user-provided PG number (1-indexed) */
+    int                         opt_angle;          /* the user-provided angle number (1-indexed) */
+    int                         opt_region;         /* the user-provided region digit */
+
+    /* subdemux */
+    const AVInputFormat         *mpeg_fmt;          /* inner MPEG-PS (VOB) demuxer */
+    AVFormatContext             *mpeg_ctx;          /* context for inner demuxer */
+    uint8_t                     *mpeg_buf;          /* buffer for inner demuxer */
+    FFIOContext                 mpeg_pb;            /* buffer context for inner demuxer */
+
+    /* volume */
+    dvd_reader_t                *dvdread;           /* handle to libdvdread */
+    ifo_handle_t                *vmg_ifo;           /* handle to the VMG (VIDEO_TS.IFO) */
+    ifo_handle_t                *vts_ifo;           /* handle to the active VTS (VTS_nn_n.IFO) */
+    dvdnav_t                    *dvdnav;            /* handle to libdvdnav */
+
+    /* playback control */
+    pgc_t                       *play_pgc;          /* handle to the active PGC */
+    int64_t                     play_ts_offset;     /* PTS discontinuity offset (e.g. VOB change) */
+    int64_t                     play_vobu_e_ptm;    /* end PTS of the current VOBU */
+    int                         play_vtsn;          /* number of the active VTS (video title set) */
+    int                         play_celln;         /* number of the active cell */
+    int                         play_pgn;           /* number of the active program */
+    int                         play_ptt;           /* number of the active PTT (chapter) */
+    int                         play_in_vts;        /* if our play state is in the VTS */
+    int                         play_in_pgc;        /* if our play state is in the PGC */
+    int                         play_in_ps;         /* if our play state is in the program stream */
+    int                         play_skip_cell;     /* if this cell is being skipped*/
+    int                         play_skip_cell_last;/* if last cell was skipped */
+
+} DVDVideoDemuxContext;
+
+static void dvdvideo_pgc_close(AVFormatContext *s)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    av_log(s, AV_LOG_TRACE, "closing DVD volume\n");
+
+    if (c->dvdnav)
+        dvdnav_close(c->dvdnav);
+
+    if (c->vts_ifo)
+        ifoClose(c->vts_ifo);
+
+    if (c->vmg_ifo)
+        ifoClose(c->vmg_ifo);
+
+    if (c->dvdread)
+        DVDClose(c->dvdread);
+}
+
+static int dvdvideo_pgc_open(AVFormatContext *s)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    dvdnav_status_t dvdnav_open_status;
+
+    title_info_t title_info;
+    int cur_title, cur_pgcn, cur_pgn;
+
+    int32_t disc_region_mask;
+    int32_t player_region_mask;
+
+    c->dvdread = DVDOpen(s->url);
+    if (!c->dvdread)
+        goto end_fail_external;
+
+    if (!(c->vmg_ifo = ifoOpen(c->dvdread, 0)))
+        goto end_fail_external;
+
+    if (c->opt_title > c->vmg_ifo->tt_srpt->nr_of_srpts) {
+        av_log(s, AV_LOG_ERROR, "Title not found\n");
+
+        return AVERROR_STREAM_NOT_FOUND;
+    }
+
+    title_info = c->vmg_ifo->tt_srpt->title[c->opt_title - 1];
+    if (c->opt_angle > title_info.nr_of_angles) {
+        av_log(s, AV_LOG_ERROR, "Angle not found\n");
+
+        return AVERROR_STREAM_NOT_FOUND;
+    }
+
+    if (title_info.nr_of_ptts < 1) {
+        av_log(s, AV_LOG_ERROR, "Title invalid\n");
+
+        return AVERROR_INVALIDDATA;
+    }
+
+    if (!(c->vts_ifo = ifoOpen(c->dvdread, title_info.title_set_nr)))
+        goto end_fail_external;
+
+    if (title_info.vts_ttn < 1
+            || title_info.vts_ttn > 99
+            || title_info.vts_ttn > c->vts_ifo->vts_ptt_srpt->nr_of_srpts
+            || c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams > 8
+            || c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams > 32) {
+        av_log(s, AV_LOG_ERROR, "Title invalid in VTS\n");
+
+        return AVERROR_INVALIDDATA;
+    }
+
+    dvdnav_open_status = dvdnav_open(&c->dvdnav, s->url);
+    if (!c->dvdnav)
+        goto end_fail_external;
+
+    if (dvdnav_open_status != DVDNAV_STATUS_OK
+            || dvdnav_set_readahead_flag(c->dvdnav, 0) != DVDNAV_STATUS_OK
+            || dvdnav_set_PGC_positioning_flag(c->dvdnav, 1) != DVDNAV_STATUS_OK
+            || dvdnav_get_region_mask(c->dvdnav, &disc_region_mask) != DVDNAV_STATUS_OK)
+        goto end_fail_external;
+
+    player_region_mask = c->opt_region > 0 ? (1 << (c->opt_region - 1)) : disc_region_mask;
+    if (dvdnav_set_region_mask(c->dvdnav, player_region_mask) != DVDNAV_STATUS_OK)
+        goto end_fail_external;
+
+    if (c->opt_pgc > 0 && c->opt_pg > 0) {
+        if (dvdnav_program_play(c->dvdnav, c->opt_title, c->opt_pgc, c->opt_pg) != DVDNAV_STATUS_OK)
+            goto end_fail_external;
+    } else {
+        if (dvdnav_part_play(c->dvdnav, c->opt_title, c->opt_ptt) != DVDNAV_STATUS_OK
+                || dvdnav_current_title_program(c->dvdnav, &cur_title, &cur_pgcn, &cur_pgn) != DVDNAV_STATUS_OK)
+            goto end_fail_external;
+
+        c->opt_pgc = cur_pgcn;
+        c->opt_pg = cur_pgn;
+    }
+
+    if (dvdnav_angle_change(c->dvdnav, c->opt_angle) != DVDNAV_STATUS_OK)
+        goto end_fail_external;
+
+    /* update the context */
+    c->play_vtsn = title_info.title_set_nr;
+    c->play_pgc = c->vts_ifo->vts_pgcit->pgci_srp[c->opt_pgc - 1].pgc;
+
+    if (c->play_pgc->pg_playback_mode != 0) {
+        av_log(s, AV_LOG_ERROR, "Sorry, non-sequential PGCs are not supported\n");
+
+        return AVERROR_PATCHWELCOME;
+    }
+
+    return 0;
+
+end_fail_external:
+    av_log(s, AV_LOG_ERROR, "Unable to start DVD playback at coordinates\n");
+
+    return AVERROR_EXTERNAL;
+}
+
+static int dvdvideo_pgc_chapters_setup(AVFormatContext *s)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    uint64_t duration;
+    uint64_t *times;
+    uint64_t time_prev = 0;
+    int nb_chapters = 0;
+
+    /* as a side effect of dvdnav_describe_title_chapters(), beneficial safety checks are done */
+    nb_chapters = dvdnav_describe_title_chapters(c->dvdnav, c->opt_title, &times, &duration);
+    if (!nb_chapters)
+        return AVERROR_EXTERNAL;
+
+    for (int i = c->opt_ptt - 1; i < nb_chapters - 1; i++) {
+        uint64_t time_effective = times[i];
+
+        if (!avpriv_new_chapter(s, i, DVDVIDEO_TIME_BASE_Q, time_prev, time_effective, NULL)) {
+            av_log(s, AV_LOG_ERROR, "Unable to allocate chapter\n");
+            return AVERROR(ENOMEM);
+        }
+
+        time_prev = time_effective;
+    }
+
+    free(times);
+
+    s->duration = av_rescale_q(duration, DVDVIDEO_TIME_BASE_Q, AV_TIME_BASE_Q);
+
+    return 0;
+}
+
+static int dvdvideo_video_stream_analyze(video_attr_t video_attr,
+        DVDVideoVTSVideoStreamEntry *entry)
+{
+    AVRational framerate;
+    int height = 0;
+    int width = 0;
+    int is_pal = video_attr.video_format == 1;
+
+    framerate = is_pal ? (AVRational) { 25, 1 } : (AVRational) { 30000, 1001 };
+    height = is_pal ? 576 : 480;
+
+    if (height > 0) {
+        switch (video_attr.picture_size) {
+            case 0: /* D1 */
+                width = 720;
+                break;
+            case 1: /* 4CIF */
+                width = 704;
+                break;
+            case 2: /* Half D1 */
+                width = 352;
+                break;
+            case 3: /* CIF */
+                width = 352;
+                height /= 2;
+                break;
+        }
+    }
+
+    if (!width || !height) {
+        return AVERROR_INVALIDDATA;
+    }
+
+    entry->startcode = 0x1E0;
+    entry->codec_id = !video_attr.mpeg_version ? AV_CODEC_ID_MPEG1VIDEO : AV_CODEC_ID_MPEG2VIDEO;
+    entry->width = width;
+    entry->height = height;
+    entry->dar = video_attr.display_aspect_ratio ? (AVRational) { 16, 9 } : (AVRational) { 4, 3 };
+    entry->framerate = framerate;
+    entry->has_cc = video_attr.line21_cc_1 || video_attr.line21_cc_2;
+
+    return 0;
+}
+
+static int dvdvideo_video_stream_add(AVFormatContext *s,
+        DVDVideoVTSVideoStreamEntry *entry, enum AVStreamParseType need_parsing)
+{
+    AVStream *st;
+    FFStream *sti;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st) {
+        return AVERROR(ENOMEM);
+    }
+
+    st->id = entry->startcode;
+    st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+    st->codecpar->codec_id = entry->codec_id;
+    st->codecpar->width = entry->width;
+    st->codecpar->height = entry->height;
+    st->codecpar->format = AV_PIX_FMT_YUV420P;
+    st->codecpar->color_range = AVCOL_RANGE_MPEG;
+
+    st->codecpar->framerate = entry->framerate;
+#if FF_API_R_FRAME_RATE
+    st->r_frame_rate = entry->framerate;
+#endif
+    st->avg_frame_rate = entry->framerate;
+
+    sti = ffstream(st);
+    sti->request_probe = 0;
+    sti->need_parsing = need_parsing;
+    sti->display_aspect_ratio = entry->dar;
+    sti->avctx->framerate = entry->framerate;
+
+    if (entry->has_cc)
+        sti->avctx->properties |= FF_CODEC_PROPERTY_CLOSED_CAPTIONS;
+
+    avpriv_set_pts_info(st, DVDVIDEO_PTS_WRAP_BITS,
+        DVDVIDEO_TIME_BASE_Q.num, DVDVIDEO_TIME_BASE_Q.den);
+
+    return 0;
+}
+
+static int dvdvideo_video_stream_setup(AVFormatContext *s)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    DVDVideoVTSVideoStreamEntry *entry;
+    int ret;
+
+    entry = av_malloc(sizeof(DVDVideoVTSVideoStreamEntry));
+
+    if ((ret = dvdvideo_video_stream_analyze(c->vts_ifo->vtsi_mat->vts_video_attr, entry)) < 0
+            || (ret = dvdvideo_video_stream_add(c->mpeg_ctx, entry, AVSTREAM_PARSE_FULL)) < 0
+            || (ret = dvdvideo_video_stream_add(s, entry, AVSTREAM_PARSE_NONE)) < 0) {
+        av_free(entry);
+        av_log(s, AV_LOG_ERROR, "Unable to allocate video stream\n");
+
+        return ret;
+    }
+
+    av_free(entry);
+
+    return 0;
+}
+
+static int dvdvideo_audio_stream_analyze(audio_attr_t audio_attr, uint16_t audio_control,
+        DVDVideoPGCAudioStreamEntry *entry)
+{
+    int startcode = 0;
+    enum AVCodecID codec_id = AV_CODEC_ID_NONE;
+    int sample_fmt = AV_SAMPLE_FMT_NONE;
+    int sample_rate = 0;
+    int bit_depth = 0;
+    int nb_channels = 0;
+    AVChannelLayout ch_layout;
+    char lang_dvd[3] = {0};
+
+    int position = (audio_control & 0x7F00) >> 8;
+
+    switch (audio_attr.audio_format) {
+        case 0:
+            codec_id = AV_CODEC_ID_AC3;
+            sample_fmt = AV_SAMPLE_FMT_FLTP;
+            sample_rate = 48000;
+            startcode = 0x80 + position;
+            break;
+        case 2:
+            codec_id = AV_CODEC_ID_MP1;
+            sample_fmt = audio_attr.quantization ? AV_SAMPLE_FMT_S32 : AV_SAMPLE_FMT_S16;
+            sample_rate = 48000;
+            bit_depth = audio_attr.quantization ? 20 : 16;
+            startcode = 0x1C0 + position;
+            break;
+        case 3:
+            codec_id = AV_CODEC_ID_MP2;
+            sample_fmt = audio_attr.quantization ? AV_SAMPLE_FMT_S32 : AV_SAMPLE_FMT_S16;
+            sample_rate = 48000;
+            bit_depth = audio_attr.quantization ? 20 : 16;
+            startcode = 0x1C0 + position;
+            break;
+        case 4:
+            codec_id = audio_attr.quantization ? AV_CODEC_ID_PCM_S32LE : AV_CODEC_ID_PCM_S16LE;
+            sample_fmt = audio_attr.quantization ? AV_SAMPLE_FMT_S32 : AV_SAMPLE_FMT_S16;
+            sample_rate = audio_attr.sample_frequency ? 96000 : 48000;
+            bit_depth = audio_attr.quantization == 2 ? 24 : (audio_attr.quantization ? 20 : 16);
+            startcode = 0xA0 + position;
+            break;
+        case 6:
+            codec_id = AV_CODEC_ID_DTS;
+            sample_fmt = AV_SAMPLE_FMT_FLTP;
+            sample_rate = 48000;
+            bit_depth = audio_attr.quantization == 2 ? 24 : (audio_attr.quantization ? 20 : 16);
+            startcode = 0x88 + position;
+            break;
+    }
+
+    nb_channels = audio_attr.channels + 1;
+
+    if (codec_id == AV_CODEC_ID_NONE || startcode == 0 || sample_fmt == AV_SAMPLE_FMT_NONE
+            || sample_rate == 0 || nb_channels == 0) {
+        return AVERROR_INVALIDDATA;
+    }
+
+    if (nb_channels == 2)
+        ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
+    else if (nb_channels == 6)
+        ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_5POINT1;
+    else if (nb_channels == 8)
+        ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_7POINT1;
+    else
+        ch_layout = (AVChannelLayout) { 0 };
+
+    AV_WB16(lang_dvd, audio_attr.lang_code);
+
+    entry->startcode = startcode;
+    entry->codec_id = codec_id;
+    entry->sample_rate = sample_rate;
+    entry->bit_depth = bit_depth;
+    entry->nb_channels = nb_channels;
+    entry->ch_layout = ch_layout;
+    entry->lang_iso = (char *) ff_convert_lang_to(lang_dvd, AV_LANG_ISO639_2_BIBL);
+
+    return 0;
+}
+
+static int dvdvideo_audio_stream_add(AVFormatContext *s, DVDVideoPGCAudioStreamEntry *entry,
+        enum AVStreamParseType need_parsing)
+{
+    AVStream *st;
+    FFStream *sti;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st) {
+        return AVERROR(ENOMEM);
+    }
+
+    st->id = entry->startcode;
+    st->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
+    st->codecpar->codec_id = entry->codec_id;
+    st->codecpar->format = entry->sample_fmt;
+    st->codecpar->sample_rate = entry->sample_rate;
+    st->codecpar->bits_per_coded_sample = entry->bit_depth;
+    st->codecpar->bits_per_raw_sample = entry->bit_depth;
+    st->codecpar->ch_layout = entry->ch_layout;
+    st->codecpar->ch_layout.nb_channels = entry->nb_channels;
+
+    if (entry->lang_iso) {
+        av_dict_set(&st->metadata, "language", entry->lang_iso, 0);
+    }
+
+    sti = ffstream(st);
+    sti->request_probe = 0;
+    sti->need_parsing = need_parsing;
+
+    avpriv_set_pts_info(st, DVDVIDEO_PTS_WRAP_BITS,
+            DVDVIDEO_TIME_BASE_Q.num, DVDVIDEO_TIME_BASE_Q.den);
+
+    return 0;
+}
+
+static int dvdvideo_audio_stream_add_all(AVFormatContext *s)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    int ret;
+
+    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams; i++) {
+        DVDVideoPGCAudioStreamEntry *entry;
+
+        if (!(c->play_pgc->audio_control[i] & 0x8000))
+            continue;
+
+        entry = av_malloc(sizeof(DVDVideoPGCAudioStreamEntry));
+        if (!entry)
+            return AVERROR(ENOMEM);
+
+        if ((ret = dvdvideo_audio_stream_analyze(c->vts_ifo->vtsi_mat->vts_audio_attr[i],
+                c->play_pgc->audio_control[i], entry)) < 0)
+            goto break_free_and_error;
+
+        if (c->vts_ifo->vtsi_mat->vts_audio_attr[i].application_mode == 1)
+            av_log(s, AV_LOG_WARNING, "Audio %d: karaoke extension enabled\n", entry->startcode);
+
+        for (int j = 0; j < s->nb_streams; j++)
+            if (s->streams[j]->id == entry->startcode)
+                goto continue_free;
+
+        if ((ret = dvdvideo_audio_stream_add(c->mpeg_ctx, entry, AVSTREAM_PARSE_FULL)) < 0)
+            goto break_free_and_error;
+
+        if ((ret = dvdvideo_audio_stream_add(s, entry, AVSTREAM_PARSE_NONE)) < 0)
+            goto break_free_and_error;
+
+continue_free:
+        av_free(entry);
+        continue;
+
+break_free_and_error:
+        av_free(entry);
+        av_log(s, AV_LOG_ERROR, "Unable to allocate audio stream\n");
+        return ret;
+    }
+
+    return 0;
+}
+
+static int dvdvideo_subp_stream_analyze(AVFormatContext *s, uint32_t offset, subp_attr_t subp_attr,
+        DVDVideoPGCSubtitleStreamEntry *entry)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    char lang_dvd[3] = {0};
+
+    entry->startcode = 0x20 + (offset & 0x1F);
+
+    entry->clut = av_mallocz(DVDVIDEO_SUBP_CLUT_SIZE);
+    memcpy(entry->clut, c->play_pgc->palette, DVDVIDEO_SUBP_CLUT_SIZE);
+
+    // XXX: YUV2RGB conversion will be handled in upcoming dedicated patch
+    // ff_dvdvideo_subp_clut_yuv_to_rgb(entry->clut, DVDVIDEO_SUBP_CLUT_SIZE);
+
+    AV_WB16(lang_dvd, subp_attr.lang_code);
+    entry->lang_iso = (char *) ff_convert_lang_to(lang_dvd, AV_LANG_ISO639_2_BIBL);
+
+    return 0;
+}
+
+static int dvdvideo_subp_stream_add(AVFormatContext *s,
+        DVDVideoPGCSubtitleStreamEntry *entry, enum AVStreamParseType need_parsing)
+{
+    AVStream *st;
+    FFStream *sti;
+
+    st = avformat_new_stream(s, NULL);
+    if (!st) {
+        return AVERROR(ENOMEM);
+    }
+
+    st->id = entry->startcode;
+    st->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
+    st->codecpar->codec_id = AV_CODEC_ID_DVD_SUBTITLE;
+
+    // XXX: Palettes will be fixed in dedicated patchset (with shared utility)
+    // if ((ret = ff_alloc_extradata(st->codecpar, DVDVIDEO_SUBP_PALETTE_EXTRADATA_SIZE)) < 0)
+    //     return ret;
+    // ff_dvdvideo_subp_palette_extradata_cat(entry->clut, DVDVIDEO_SUBP_CLUT_SIZE,
+    //         st->codecpar->extradata, st->codecpar->extradata_size);
+
+    if (entry->lang_iso)
+        av_dict_set(&st->metadata, "language", entry->lang_iso, 0);
+
+    sti = ffstream(st);
+    sti->request_probe = 0;
+    sti->need_parsing = need_parsing;
+
+    avpriv_set_pts_info(st, DVDVIDEO_PTS_WRAP_BITS,
+        DVDVIDEO_TIME_BASE_Q.num, DVDVIDEO_TIME_BASE_Q.den);
+
+    return 0;
+}
+
+static int dvdvideo_subp_stream_add_internal(AVFormatContext *s,
+        uint32_t offset, subp_attr_t subp_attr)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    DVDVideoPGCSubtitleStreamEntry *entry;
+    int ret = 0;
+
+    entry = av_malloc(sizeof(DVDVideoPGCSubtitleStreamEntry));
+
+    if ((ret = dvdvideo_subp_stream_analyze(s, offset, subp_attr, entry)) < 0)
+        goto end_free_error;
+
+    for (int i = 0; i < s->nb_streams; i++)
+        if (s->streams[i]->id == entry->startcode)
+            goto end_free;
+
+    if ((ret = dvdvideo_subp_stream_add(c->mpeg_ctx, entry, AVSTREAM_PARSE_FULL)) < 0
+            || (ret = dvdvideo_subp_stream_add(s, entry, AVSTREAM_PARSE_NONE)) < 0)
+        goto end_free_error;
+
+    goto end_free;
+
+end_free_error:
+    av_log(s, AV_LOG_ERROR, "Unable to allocate subtitle stream\n");
+
+end_free:
+    av_free(entry->clut);
+    av_free(entry);
+
+    return ret;
+}
+
+static int dvdvideo_subp_stream_add_all(AVFormatContext *s)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    if (!c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams)
+        return 0;
+
+    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams; i++) {
+        int ret;
+        uint32_t subp_control;
+        subp_attr_t subp_attr;
+        video_attr_t video_attr;
+
+        subp_control = c->play_pgc->subp_control[i];
+        if (!(subp_control & 0x80000000))
+            continue;
+
+        /* there can be several presentations for one SPU */
+        /* for now, be flexible with the DAR check due to weird authoring */
+        video_attr = c->vts_ifo->vtsi_mat->vts_video_attr;
+        subp_attr = c->vts_ifo->vtsi_mat->vts_subp_attr[i];
+
+        /* 4:3 */
+        if (!video_attr.display_aspect_ratio) {
+            if ((ret = dvdvideo_subp_stream_add_internal(s, subp_control >> 24, subp_attr)) < 0)
+                return ret;
+
+            continue;
+        }
+
+        /* 16:9 */
+        if ((ret = dvdvideo_subp_stream_add_internal(s, subp_control >> 16, subp_attr)) < 0)
+            return ret;
+
+        /* 16:9 letterbox */
+        if (video_attr.permitted_df == 2 || video_attr.permitted_df == 0)
+            if ((ret = dvdvideo_subp_stream_add_internal(s, subp_control >> 8, subp_attr)) < 0)
+                return ret;
+
+        /* 16:9 pan-and-scan */
+        if (video_attr.permitted_df == 1 || video_attr.permitted_df == 0)
+            if ((ret = dvdvideo_subp_stream_add_internal(s, subp_control, subp_attr)) < 0)
+                return ret;
+    }
+
+    return 0;
+}
+
+static int dvdvideo_subdemux_read_data(void *opaque, uint8_t *buf, int buf_size)
+{
+    AVFormatContext *s = opaque;
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    uint8_t nav_buf[DVDVIDEO_BLOCK_SIZE] = {0};
+    int nav_event;
+    int nav_len;
+
+    dvdnav_vts_change_event_t *e_vts;
+    dvdnav_cell_change_event_t *e_cell;
+    int cur_title, cur_pgcn, cur_pgn, cur_angle, cur_title_unused, cur_ptt, cur_nb_angles;
+    pci_t *p_pci;
+
+    if (buf_size != DVDVIDEO_BLOCK_SIZE) {
+        av_log(s, AV_LOG_ERROR, "Invalid buffer size\n");
+
+        return AVERROR(ENOMEM);
+    }
+
+    for (int i = 0; i < DVDVIDEO_MAX_PS_SEARCH_BLOCKS; i++) {
+        if (ff_check_interrupt(&s->interrupt_callback))
+            return AVERROR_EXIT;
+
+        if (dvdnav_get_next_block(c->dvdnav, nav_buf, &nav_event, &nav_len) != DVDNAV_STATUS_OK)
+            return AVERROR_EXTERNAL;
+
+        if (nav_len > DVDVIDEO_BLOCK_SIZE)
+            return AVERROR_INVALIDDATA;
+
+        if (dvdnav_current_title_program(c->dvdnav, &cur_title, &cur_pgcn, &cur_pgn) != DVDNAV_STATUS_OK)
+            return AVERROR_EXTERNAL;
+
+        if (dvdnav_current_title_info(c->dvdnav, &cur_title_unused, &cur_ptt) != DVDNAV_STATUS_OK)
+            return AVERROR_EXTERNAL;
+
+        if (dvdnav_get_angle_info(c->dvdnav, &cur_angle, &cur_nb_angles) != DVDNAV_STATUS_OK)
+            return AVERROR_EXTERNAL;
+
+        av_log(s, AV_LOG_TRACE, "new block: i=%d nav_event=%d nav_len=%d "
+                                "cur_title=%d cur_ptt=%d cur_angle=%d cur_celln=%d cur_pgcn=%d cur_pgn=%d "
+                                "play_in_vts=%d play_in_pgc=%d play_in_ps=%d\n",
+                                i, nav_event, nav_len,
+                                cur_title, cur_ptt, cur_angle, c->play_celln, cur_pgcn, cur_pgn,
+                                c->play_in_vts, c->play_in_pgc, c->play_in_ps);
+
+        if (c->play_in_pgc && (cur_pgcn != c->opt_pgc || !dvdnav_is_domain_vts(c->dvdnav)))
+            return AVERROR_EOF;
+
+        switch (nav_event) {
+            case DVDNAV_VTS_CHANGE:
+                if (c->play_in_vts)
+                    return AVERROR_EOF;
+
+                e_vts = (dvdnav_vts_change_event_t *) nav_buf;
+
+                if (e_vts->new_vtsN == c->play_vtsn && e_vts->new_domain == DVD_DOMAIN_VTSTitle)
+                    c->play_in_vts = 1;
+
+                continue;
+            case DVDNAV_CELL_CHANGE:
+                if (!c->play_in_vts)
+                    continue;
+
+                e_cell = (dvdnav_cell_change_event_t *) nav_buf;
+
+                av_log(s, AV_LOG_TRACE, "new cell: prev=%d new=%d\n", c->play_celln, e_cell->cellN);
+
+                if (!c->play_in_ps && !c->play_in_pgc) {
+                    if (cur_title == c->opt_title && cur_ptt == c->opt_ptt
+                            && cur_pgcn == c->opt_pgc && cur_pgn == c->opt_pg) {
+                        c->play_in_pgc = 1;
+                    }
+                } else if (c->play_celln > e_cell->cellN || c->play_pgn > cur_pgn)
+                    return AVERROR_EOF;
+
+                c->play_celln = e_cell->cellN;
+                c->play_pgn = cur_pgn;
+                c->play_skip_cell_last = c->play_skip_cell;
+                /* XXX: should we be skipping restricted cells mid-stream? */
+                c->play_skip_cell = c->play_pgc->cell_playback[e_cell->cellN - 1].restricted != 0;
+
+                if (c->play_skip_cell)
+                    av_log(s, AV_LOG_TRACE, "cell skipped: cell=%d\n", e_cell->cellN);
+
+                continue;
+            case DVDNAV_NAV_PACKET:
+                if (!c->play_in_pgc)
+                    continue;
+
+                c->play_ptt = cur_ptt;
+
+                p_pci = dvdnav_get_current_nav_pci(c->dvdnav);
+
+                if (!c->play_in_ps) {
+                    /* XXX: this should shift start time to 0, but not working on all discs */
+                    c->play_ts_offset -= p_pci->pci_gi.vobu_s_ptm;
+                    c->play_in_ps = 1;
+
+                    continue;
+                }
+
+                if (cur_ptt < c->play_ptt)
+                    return AVERROR_EOF;
+
+                if (c->play_skip_cell)
+                    c->play_ts_offset -= p_pci->pci_gi.vobu_e_ptm;
+                else if (p_pci->pci_gi.vobu_s_ptm < c->play_vobu_e_ptm) {
+                    av_log(s, AV_LOG_TRACE, "discontinuity: adjusting timestamps\n");
+
+                    c->play_ts_offset += c->play_vobu_e_ptm;
+
+                    if (!c->play_skip_cell_last) {
+                        avio_flush(&c->mpeg_pb.pub);
+                        ff_read_frame_flush(c->mpeg_ctx);
+                    }
+                }
+
+                c->play_vobu_e_ptm = p_pci->pci_gi.vobu_e_ptm;
+
+                continue;
+            case DVDNAV_BLOCK_OK:
+                if (!c->play_in_ps || c->play_skip_cell)
+                    continue;
+
+                if (nav_len != DVDVIDEO_BLOCK_SIZE)
+                    return AVERROR_INVALIDDATA;
+
+                if (cur_angle != c->opt_angle) {
+                    av_log(s, AV_LOG_ERROR, "Unexpected angle change\n");
+
+                    return AVERROR_INPUT_CHANGED;
+                }
+
+                memcpy(buf, &nav_buf, nav_len);
+
+                return nav_len;
+            case DVDNAV_STILL_FRAME:
+                if (c->play_in_ps)
+                    return AVERROR_EOF;
+
+                dvdnav_still_skip(c->dvdnav);
+
+                continue;
+            case DVDNAV_WAIT:
+                if (c->play_in_ps)
+                    return AVERROR_EOF;
+
+                dvdnav_wait_skip(c->dvdnav);
+
+                continue;
+            case DVDNAV_HOP_CHANNEL:
+            case DVDNAV_HIGHLIGHT:
+                if (c->play_in_ps)
+                    return AVERROR_EOF;
+
+                continue;
+            case DVDNAV_STOP:
+                return AVERROR_EOF;
+            default:
+                continue;
+        }
+    }
+
+    av_log(s, AV_LOG_ERROR, "Unable to find next program stream block\n");
+
+    return AVERROR_EOF;
+}
+
+static void dvdvideo_subdemux_close(AVFormatContext *s)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    av_freep(&c->mpeg_pb.pub.buffer);
+    av_freep(&c->mpeg_pb);
+    avformat_close_input(&c->mpeg_ctx);
+}
+
+static int dvdvideo_subdemux_open(AVFormatContext *s)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    int ret;
+
+    const AVInputFormat *mpeg_fmt = NULL;
+    AVFormatContext *mpeg_ctx = NULL;
+    uint8_t *mpeg_buf = NULL;
+
+    if (!(mpeg_fmt = av_find_input_format("mpeg")))
+        return AVERROR_DEMUXER_NOT_FOUND;
+
+    if (!(mpeg_ctx = avformat_alloc_context()))
+        return AVERROR(ENOMEM);
+
+    if (!(mpeg_buf = av_malloc(DVDVIDEO_BLOCK_SIZE))) {
+        avformat_free_context(mpeg_ctx);
+
+        return AVERROR(ENOMEM);
+    }
+
+    ffio_init_context(&c->mpeg_pb, mpeg_buf, DVDVIDEO_BLOCK_SIZE, 0, s,
+            dvdvideo_subdemux_read_data, NULL, NULL);
+    c->mpeg_pb.pub.seekable = 0;
+
+    if ((ret = ff_copy_whiteblacklists(mpeg_ctx, s)) < 0) {
+        avformat_free_context(mpeg_ctx);
+
+        return ret;
+    }
+
+    mpeg_ctx->flags = AVFMT_FLAG_CUSTOM_IO | AVFMT_FLAG_GENPTS;
+    mpeg_ctx->ctx_flags |= AVFMTCTX_UNSEEKABLE;
+    mpeg_ctx->probesize = 0;
+    mpeg_ctx->max_analyze_duration = 0;
+    mpeg_ctx->interrupt_callback = s->interrupt_callback;
+    mpeg_ctx->pb = &c->mpeg_pb.pub;
+    mpeg_ctx->io_open = NULL;
+
+    if ((ret = avformat_open_input(&mpeg_ctx, "", mpeg_fmt, NULL)) < 0) {
+        avformat_free_context(mpeg_ctx);
+
+        return ret;
+    }
+
+    c->mpeg_fmt = mpeg_fmt;
+    c->mpeg_ctx = mpeg_ctx;
+    c->mpeg_buf = mpeg_buf;
+
+    return 0;
+}
+
+static int dvdvideo_read_header(AVFormatContext *s)
+{
+    int ret;
+
+    if ((ret = dvdvideo_pgc_open(s)) < 0)
+        return ret;
+
+    if ((ret = dvdvideo_pgc_chapters_setup(s)) < 0)
+        return ret;
+
+    if ((ret = dvdvideo_subdemux_open(s)) < 0)
+        return ret;
+
+    if ((ret = dvdvideo_video_stream_setup(s)) < 0)
+        return ret;
+
+    if ((ret = dvdvideo_audio_stream_add_all(s)) < 0)
+        return ret;
+
+    if ((ret = dvdvideo_subp_stream_add_all(s)) < 0)
+        return ret;
+
+    return 0;
+}
+
+static int dvdvideo_read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+
+    int ret;
+
+    ret = av_read_frame(c->mpeg_ctx, pkt);
+
+    if (ret >= 0) {
+        if (c->mpeg_ctx->nb_streams != s->nb_streams) {
+            av_log(s, AV_LOG_ERROR, "Unexpected new stream in playback\n");
+
+            return AVERROR_INPUT_CHANGED;
+        }
+
+        if (pkt->pts != AV_NOPTS_VALUE)
+            pkt->pts += c->play_ts_offset;
+        if (pkt->dts != AV_NOPTS_VALUE)
+            pkt->dts += c->play_ts_offset;
+
+        return 0;
+    }
+
+    return ret;
+}
+
+static int dvdvideo_read_seek(AVFormatContext *s, int stream_index,
+        int64_t timestamp, int flags)
+{
+    return AVERROR_PATCHWELCOME;
+}
+
+static int dvdvideo_close(AVFormatContext *s)
+{
+    dvdvideo_subdemux_close(s);
+    dvdvideo_pgc_close(s);
+
+    return 0;
+}
+
+static int dvdvideo_probe(const AVProbeData *p)
+{
+    /* PATCHWELCOME: not implemented at this time */
+    return 0;
+}
+
+#define OFFSET(x) offsetof(DVDVideoDemuxContext, x)
+static const AVOption dvdvideo_options[] = {
+    {"title",   "Title Number",                     OFFSET(opt_title),      AV_OPT_TYPE_INT,    { .i64=1 },     1,          99,        AV_OPT_FLAG_DECODING_PARAM },
+    {"chapter", "Entry Chapter (PTT) Number",       OFFSET(opt_ptt),        AV_OPT_TYPE_INT,    { .i64=1 },     1,          99,        AV_OPT_FLAG_DECODING_PARAM },
+    {"pgc",     "Entry PGC Number (0=auto)",        OFFSET(opt_pgc),        AV_OPT_TYPE_INT,    { .i64=0 },     0,          32767,     AV_OPT_FLAG_DECODING_PARAM },
+    {"pg",      "Entry PG Number (0=auto)",         OFFSET(opt_pg),         AV_OPT_TYPE_INT,    { .i64=0 },     0,          255,       AV_OPT_FLAG_DECODING_PARAM },
+    {"angle",   "Video Angle Number",               OFFSET(opt_angle),      AV_OPT_TYPE_INT,    { .i64=1 },     1,          9,         AV_OPT_FLAG_DECODING_PARAM },
+    {"region",  "Playback Region Number (0=free)",  OFFSET(opt_region),     AV_OPT_TYPE_INT,    { .i64=0 },     0,          8,         AV_OPT_FLAG_DECODING_PARAM },
+    {NULL}
+};
+
+static const AVClass dvdvideo_class = {
+    .class_name = "DVD-Video demuxer",
+    .item_name  = av_default_item_name,
+    .option     = dvdvideo_options,
+    .version    = LIBAVUTIL_VERSION_INT
+};
+
+const AVInputFormat ff_dvdvideo_demuxer = {
+    .name           = "dvdvideo",
+    .long_name      = NULL_IF_CONFIG_SMALL("DVD-Video"),
+    .priv_class     = &dvdvideo_class,
+    .priv_data_size = sizeof(DVDVideoDemuxContext),
+    .flags          = AVFMT_NOFILE | AVFMT_SHOW_IDS | AVFMT_TS_DISCONT | AVFMT_NO_BYTE_SEEK | AVFMT_NOGENSEARCH | AVFMT_NOBINSEARCH,
+    .flags_internal = FF_FMT_INIT_CLEANUP,
+    .read_probe     = dvdvideo_probe,
+    .read_close     = dvdvideo_close,
+    .read_header    = dvdvideo_read_header,
+    .read_packet    = dvdvideo_read_packet,
+    .read_seek      = dvdvideo_read_seek
+};