diff mbox series

[FFmpeg-devel,2/3] avformat/dvdvideodec: add menu demuxing support

Message ID 20240306071913.2735832-2-marth64@proxyid.net
State New
Headers show
Series [FFmpeg-devel,1/3] avformat/dvdvideodec: add CLUT utilities and subtitle palette support | expand

Checks

Context Check Description
yinshiyou/commit_msg_loongarch64 warning Please wrap lines in the body of the commit message between 60 and 72 characters.
andriy/commit_msg_x86 warning Please wrap lines in the body of the commit message between 60 and 72 characters.
andriy/make_x86 success Make finished
andriy/make_fate_x86 success Make fate finished

Commit Message

Marth64 March 6, 2024, 7:19 a.m. UTC
Many DVDs have valuable assets in their menu structures, including background
video or music. Some discs also abuse the menu feature to include actual
feature footage that needs to change aspect ratio (in order to trick the DVD player).

This patch allows extraction and archival of these assets, but does not enable
controllable playback (which needs a full-fledged player and nav VM).
Menus are processed directly with dvdread and the existing foundation of the demuxer.

Will eventually add option to list their coordinates as well, so users
do not have to rely on other tools to find them.

Signed-off-by: Marth64 <marth64@proxyid.net>
---
 doc/demuxers.texi         |  43 +++++-
 libavformat/dvdvideodec.c | 315 ++++++++++++++++++++++++++++++++++++--
 2 files changed, 339 insertions(+), 19 deletions(-)

Comments

Stefano Sabatini March 6, 2024, 3:41 p.m. UTC | #1
On date Wednesday 2024-03-06 01:19:12 -0600, Marth64 wrote:
> Many DVDs have valuable assets in their menu structures, including background
> video or music. Some discs also abuse the menu feature to include actual
> feature footage that needs to change aspect ratio (in order to trick the DVD player).
> 
> This patch allows extraction and archival of these assets, but does not enable
> controllable playback (which needs a full-fledged player and nav VM).
> Menus are processed directly with dvdread and the existing foundation of the demuxer.
> 
> Will eventually add option to list their coordinates as well, so users
> do not have to rely on other tools to find them.
> 
> Signed-off-by: Marth64 <marth64@proxyid.net>
> ---
>  doc/demuxers.texi         |  43 +++++-
>  libavformat/dvdvideodec.c | 315 ++++++++++++++++++++++++++++++++++++--
>  2 files changed, 339 insertions(+), 19 deletions(-)
> 
> diff --git a/doc/demuxers.texi b/doc/demuxers.texi
> index 1a17c6db16..e2ea66c1a5 100644
> --- a/doc/demuxers.texi
> +++ b/doc/demuxers.texi
> @@ -289,8 +289,10 @@ This demuxer accepts the following option:
>  
>  DVD-Video demuxer, powered by libdvdnav and libdvdread.
>  
> -Can directly ingest DVD titles, specifically sequential PGCs,
> -into a conversion pipeline. Menus and seeking are not supported at this time.
> +Can directly ingest DVD titles, specifically sequential PGCs, into
> +a conversion pipeline. Menu assets, such as background video or audio,
> +can also be demuxed given the menu's coordinates (at best effort).
> +Seeking is not supported at this time.
>  
>  Block devices (DVD drives), ISO files, and directory structures are accepted.
>  Activate with @code{-f dvdvideo} in front of one of these inputs.
> @@ -347,37 +349,56 @@ This demuxer accepts the following options:
>  
>  @item title @var{int}
>  The title number to play. Must be set if @option{pgc} and @option{pg} are not set.
> +Not applicable to menus.
>  Default is 0 (auto), which currently only selects the first available title (title 1)
>  and notifies the user about the implications.
>  
>  @item chapter_start @var{int}
> -The chapter, or PTT (part-of-title), number to start at. Default is 1.
> +The chapter, or PTT (part-of-title), number to start at. Not applicable to menus.
> +Default is 1.
>  
>  @item chapter_end @var{int}
> -The chapter, or PTT (part-of-title), number to end at. Default is 0,
> -which is a special value to signal end at the last possible chapter.
> +The chapter, or PTT (part-of-title), number to end at. Not applicable to menus.
> +Default is 0, which is a special value to signal end at the last possible chapter.
>  
>  @item angle @var{int}
>  The video angle number, referring to what is essentially an additional
>  video stream that is composed from alternate frames interleaved in the VOBs.
> +Not applicable to menus.
>  Default is 1.
>  
>  @item region @var{int}
>  The region code to use for playback. Some discs may use this to default playback
>  at a particular angle in different regions. This option will not affect the region code
> -of a real DVD drive, if used as an input. Default is 0, "world".
> +of a real DVD drive, if used as an input. Not applicable to menus.
> +Default is 0, "world".
> +

> +@item menu @var{bool}
> +Demux menu assets instead of navigating a title. Requires exact coordinates
> +of the menu (@option{menu_lu}, @option{menu_vts}, @option{pgc}, @option{pg}).
> +Default is false.
> +

> +@item menu_lu @var{int}
> +The menu language to demux. In DVD, menus are grouped by language.
> +Default is 0, the first language unit.
> +
> +@item menu_vts @var{int}
> +The VTS where the menu lives, or 0 if it is a VMG menu (root-level).
> +Default is 0, VMG menu.
>  
>  @item pgc @var{int}
>  The entry PGC to start playback, in conjunction with @option{pg}.
>  Alternative to setting @option{title}.
>  Chapter markers are not supported at this time.
> +Must be explicitly set for menus.
>  Default is 0, automatically resolve from value of @option{title}.
>  
>  @item pg @var{int}
>  The entry PG to start playback, in conjunction with @option{pgc}.
>  Alternative to setting @option{title}.
>  Chapter markers are not supported at this time.
> -Default is 0, automatically resolve from value of @option{title}.
> +Default is 0, automatically resolve from value of @option{title}, or
> +start from the beginning (PG 1) of the menu.
>  
>  @item preindex @var{bool}
>  Enable this to have accurate chapter (PTT) markers and duration measurement,
> @@ -385,6 +406,7 @@ which requires a slow second pass read in order to index the chapter marker
>  timestamps from NAV packets. This is non-ideal extra work for real optical drives.
>  It is recommended and faster to use this option with a backup of the DVD structure
>  stored on a hard drive. Not compatible with @option{pgc} and @option{pg}.
> +Not applicable to menus.
>  Default is 0, false.
>  
>  @item trim @var{bool}
> @@ -392,6 +414,7 @@ Skip padding cells (i.e. cells shorter than 1 second) from the beginning.
>  There exist many discs with filler segments at the beginning of the PGC,
>  often with junk data intended for controlling a real DVD player's
>  buffering speed and with no other material data value.
> +Not applicable to menus.
>  Default is 1, true.
>  
>  @item clut_rgb @var{bool}
> @@ -421,6 +444,12 @@ Open only chapter 5 from title 1 from a given DVD structure:
>  @example
>  ffmpeg -f dvdvideo -chapter_start 5 -chapter_end 5 -title 1 -i <path to DVD> ...
>  @end example
> +
> +@item
> +Demux menu with language 1 from VTS 1, PGC 1, starting at PG 1:
> +@example
> +ffmpeg -f dvdvideo -menu 1 -menu_lu 1 -menu_vts 1 -pgc 1 -pg 1 -i <path to DVD> ...
> +@end example
>  @end itemize
>  
>  @section ea
> diff --git a/libavformat/dvdvideodec.c b/libavformat/dvdvideodec.c
> index 3bc76f5c65..2c7ffdd148 100644
> --- a/libavformat/dvdvideodec.c
> +++ b/libavformat/dvdvideodec.c
> @@ -57,9 +57,11 @@
>  #define DVDVIDEO_BLOCK_SIZE                             2048
>  #define DVDVIDEO_TIME_BASE_Q                            (AVRational) { 1, 90000 }
>  #define DVDVIDEO_PTS_WRAP_BITS                          64 /* VOBUs use 32 (PES allows 33) */
> -
>  #define DVDVIDEO_LIBDVDX_LOG_BUFFER_SIZE                1024
>  
> +#define PCI_START_BYTE                                  45 /* complement dvdread's DSI_START_BYTE */
> +static const uint8_t dvdvideo_nav_header[4] =           { 0x00, 0x00, 0x01, 0xBF };
> +
>  enum DVDVideoSubpictureViewport {
>      DVDVIDEO_SUBP_VIEWPORT_FULLSCREEN,
>      DVDVIDEO_SUBP_VIEWPORT_WIDESCREEN,
> @@ -121,6 +123,15 @@ typedef struct DVDVideoPlaybackState {
>      uint64_t                    *pgc_pg_times_est;  /* PG start times as reported by IFO */
>      pgc_t                       *pgc;               /* handle to the active PGC */
>      dvdnav_t                    *dvdnav;            /* handle to the dvdnav VM */
> +
> +    /* the following fields are only used for menu playback */
> +    int                         celln_start;        /* starting cell number */
> +    int                         celln_end;          /* ending cell number */
> +    int                         sector_offset;      /* current sector relative to the current VOB */
> +    uint32_t                    sector_end;         /* end sector relative to the current VOBU */
> +    uint32_t                    vobu_next;          /* the next VOBU pointer */
> +    uint32_t                    vobu_remaining;     /* remaining blocks for current VOBU */
> +    dvd_file_t                  *vob_file;          /* handle to the menu VOB (VMG or VTS) */
>  } DVDVideoPlaybackState;
>  
>  typedef struct DVDVideoDemuxContext {
> @@ -131,6 +142,9 @@ typedef struct DVDVideoDemuxContext {
>      int                         opt_chapter_end;    /* the user-provided exit PTT (0 for last) */
>      int                         opt_chapter_start;  /* the user-provided entry PTT (1-indexed) */
>      int                         opt_clut_rgb;       /* output subtitle palette (CLUT) as RGB */
> +    int                         opt_menu;           /* demux menu domain instead of title domain */
> +    int                         opt_menu_lu;        /* the menu language unit (logical grouping) */
> +    int                         opt_menu_vts;       /* the menu VTS, or 0 for VMG (main menu) */
>      int                         opt_pg;             /* the user-provided PG number (1-indexed) */
>      int                         opt_pgc;            /* the user-provided PGC number (1-indexed) */
>      int                         opt_preindex;       /* pre-indexing mode (2-pass read) */
> @@ -227,6 +241,16 @@ static int dvdvideo_ifo_open(AVFormatContext *s)
>          return AVERROR_EXTERNAL;
>      }
>  
> +    if (c->opt_menu) {
> +        if (c->opt_menu_vts > 0 && !(c->vts_ifo = ifoOpen(c->dvdread, c->opt_menu_vts))) {
> +            av_log(s, AV_LOG_ERROR, "Unable to open IFO structure for VTS %d\n", c->opt_menu_vts);
> +
> +            return AVERROR_EXTERNAL;
> +        }
> +
> +        return 0;
> +    }
> +
>      if (c->opt_title > c->vmg_ifo->tt_srpt->nr_of_srpts) {
>          av_log(s, AV_LOG_ERROR, "Title %d not found\n", c->opt_title);
>  
> @@ -290,6 +314,182 @@ static int dvdvideo_is_pgc_promising(AVFormatContext *s, pgc_t *pgc)
>      return 0;
>  }
>  
> +static void dvdvideo_menu_close(AVFormatContext *s, DVDVideoPlaybackState *state)
> +{
> +    if (state->vob_file)
> +        DVDCloseFile(state->vob_file);
> +}
> +
> +static int dvdvideo_menu_open(AVFormatContext *s, DVDVideoPlaybackState *state)
> +{
> +    DVDVideoDemuxContext *c = s->priv_data;
> +    pgci_ut_t *pgci_ut;
> +
> +    pgci_ut = c->opt_menu_vts ? c->vts_ifo->pgci_ut : c->vmg_ifo->pgci_ut;
> +    if (!pgci_ut) {
> +        av_log(s, AV_LOG_ERROR, "Invalid PGC table for menu [LU %d, PGC %d]\n",
> +                                c->opt_menu_lu, c->opt_pgc);
> +
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    if (c->opt_pgc < 1                      ||
> +        c->opt_menu_lu < 1                  ||
> +        c->opt_menu_lu > pgci_ut->nr_of_lus ||
> +        c->opt_pgc > pgci_ut->lu[c->opt_menu_lu - 1].pgcit->nr_of_pgci_srp) {
> +
> +        av_log(s, AV_LOG_ERROR, "Menu [LU %d, PGC %d] not found\n", c->opt_menu_lu, c->opt_pgc);
> +
> +        return AVERROR(EINVAL);
> +    }
> +
> +    /* make sure the PGC is valid */
> +    state->pgcn          = c->opt_pgc - 1;
> +    state->pgc           = pgci_ut->lu[c->opt_menu_lu - 1].pgcit->pgci_srp[c->opt_pgc - 1].pgc;
> +
> +    if (!state->pgc || !state->pgc->program_map || !state->pgc->cell_playback) {
> +        av_log(s, AV_LOG_ERROR, "Invalid PGC structure for menu [LU %d, PGC %d]\n",
> +                                c->opt_menu_lu, c->opt_pgc);
> +
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    /* make sure the PG is valid */
> +    state->entry_pgn     = c->opt_pg;
> +    if (state->entry_pgn < 1 || state->entry_pgn > state->pgc->nr_of_programs) {
> +        av_log(s, AV_LOG_ERROR, "Entry PG %d not found\n", state->entry_pgn);
> +
> +        return AVERROR(EINVAL);
> +    }
> +
> +    /* make sure the program map isn't leading us to nowhere */
> +    state->celln_start   = state->pgc->program_map[state->entry_pgn - 1];
> +    state->celln_end     = state->pgc->nr_of_cells;
> +    state->celln         = state->celln_start;
> +    if (state->celln_start > state->pgc->nr_of_cells) {
> +        av_log(s, AV_LOG_ERROR, "Invalid PGC structure: program map points to unknown cell\n");
> +
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    state->sector_end    = state->pgc->cell_playback[state->celln - 1].last_sector;
> +    state->vobu_next     = state->pgc->cell_playback[state->celln - 1].first_sector;
> +    state->sector_offset = state->vobu_next;
> +
> +    if (c->opt_menu_vts > 0)
> +        state->in_vts    = 1;
> +
> +    if (!(state->vob_file = DVDOpenFile(c->dvdread, c->opt_menu_vts, DVD_READ_MENU_VOBS))) {
> +        av_log(s, AV_LOG_ERROR, !c->opt_menu_vts ?
> +                                "Unable to open main menu VOB (VIDEO_TS.VOB)\n" :
> +                                "Unable to open menu VOBs for VTS %d\n", c->opt_menu_vts);
> +
> +        return AVERROR_EXTERNAL;
> +    }
> +
> +    return 0;
> +}
> +
> +static int dvdvideo_menu_next_ps_block(AVFormatContext *s, DVDVideoPlaybackState *state,
> +                                       uint8_t *buf, int buf_size,
> +                                       void (*flush_cb)(AVFormatContext *s))
> +{
> +    ssize_t blocks_read                   = 0;
> +    uint8_t read_buf[DVDVIDEO_BLOCK_SIZE] = {0};
> +    pci_t pci                             = (pci_t) {0};
> +    dsi_t dsi                             = (dsi_t) {0};
> +
> +    if (buf_size != DVDVIDEO_BLOCK_SIZE) {
> +        av_log(s, AV_LOG_ERROR, "Invalid buffer size (expected=%d actual=%d)\n",
> +                                DVDVIDEO_BLOCK_SIZE, buf_size);
> +
> +        return AVERROR(EINVAL);
> +    }
> +
> +    /* we were at the end of a vobu, so now go to the next one or EOF */
> +    if (!state->vobu_remaining && state->in_pgc) {
> +        if (state->vobu_next == SRI_END_OF_CELL) {
> +            if (state->celln == state->celln_end && state->sector_offset > state->sector_end)
> +                return AVERROR_EOF;
> +
> +            state->celln++;
> +            state->sector_offset = state->pgc->cell_playback[state->celln - 1].first_sector;
> +            state->sector_end    = state->pgc->cell_playback[state->celln - 1].last_sector;
> +        } else {
> +            state->sector_offset = state->vobu_next;
> +        }
> +    }
> +
> +    /* continue reading the VOBU */
> +    av_log(s, AV_LOG_TRACE, "reading block at offset %d\n", state->sector_offset);
> +
> +    blocks_read = DVDReadBlocks(state->vob_file, state->sector_offset, 1, read_buf);

> +    if (blocks_read != 1) {
> +        av_log(s, AV_LOG_ERROR, "Unable to read VOB block at offset %d\n", state->sector_offset);

nit: you might show blocks_read in case it is an error message to aid
debugging

> +
> +        return AVERROR_INVALIDDATA;
> +    }
> +
> +    /* we are at the start of a VOBU, so we are expecting a NAV packet */
> +    if (!state->vobu_remaining) {
> +        if (!memcmp(&read_buf[PCI_START_BYTE - 4], dvdvideo_nav_header, 4) ||
> +            !memcmp(&read_buf[DSI_START_BYTE - 4], dvdvideo_nav_header, 4) ||
> +            read_buf[PCI_START_BYTE - 1] != 0x00                           ||
> +            read_buf[DSI_START_BYTE - 1] != 0x01) {
> +
> +            av_log(s, AV_LOG_ERROR, "Invalid NAV packet at offset %d: PCI or DSI header mismatch\n",
> +                                    state->sector_offset);
> +
> +            return AVERROR_INVALIDDATA;
> +        }
> +
> +        navRead_PCI(&pci, &read_buf[PCI_START_BYTE]);
> +        navRead_DSI(&dsi, &read_buf[DSI_START_BYTE]);
> +
> +        if (!pci.pci_gi.vobu_s_ptm                          ||
> +            !pci.pci_gi.vobu_e_ptm                          ||
> +            pci.pci_gi.vobu_s_ptm > pci.pci_gi.vobu_e_ptm) {
> +
> +            av_log(s, AV_LOG_ERROR, "Invalid NAV packet at offset %d: PCI header is invalid\n",
> +                                    state->sector_offset);
> +
> +            return AVERROR_INVALIDDATA;
> +        }
> +
> +        state->vobu_remaining    = dsi.dsi_gi.vobu_ea;
> +        state->vobu_next         = dsi.vobu_sri.next_vobu == SRI_END_OF_CELL ? SRI_END_OF_CELL :
> +                                   dsi.dsi_gi.nv_pck_lbn + (dsi.vobu_sri.next_vobu & 0x7FFFFFFF);
> +        state->sector_offset++;
> +
> +        if (state->in_pgc) {
> +            if (state->vobu_e_ptm != pci.pci_gi.vobu_s_ptm) {
> +                if (flush_cb)
> +                    flush_cb(s);
> +
> +                state->ts_offset += state->vobu_e_ptm - pci.pci_gi.vobu_s_ptm;
> +            }
> +        } else {
> +            state->in_pgc        = 1;
> +        }
> +
> +        state->vobu_e_ptm        = pci.pci_gi.vobu_e_ptm;
> +
> +        av_log(s, AV_LOG_DEBUG, "NAV packet: sector=%d "
> +                                "vobu_s_ptm=%d vobu_e_ptm=%d ts_offset=%ld\n",
> +                                dsi.dsi_gi.nv_pck_lbn,
> +                                pci.pci_gi.vobu_s_ptm, pci.pci_gi.vobu_e_ptm, state->ts_offset);
> +
> +        return FFERROR_REDO;
> +    }
> +
> +    /* we are in the middle of a VOBU, so pass on the PS packet */
> +    memcpy(buf, &read_buf, DVDVIDEO_BLOCK_SIZE);
> +    state->sector_offset++;
> +    state->vobu_remaining--;
> +
> +    return DVDVIDEO_BLOCK_SIZE;
> +}
> +
>  static void dvdvideo_play_close(AVFormatContext *s, DVDVideoPlaybackState *state)
>  {
>      if (!state->dvdnav)
> @@ -716,7 +916,7 @@ static int dvdvideo_chapters_setup_preindex(AVFormatContext *s)
>          goto end_close;
>  
>      av_log(s, AV_LOG_INFO,
> -           "Indexing chapter markers, this will take a long time. Please wait...\n");

> +           "Indexing chapter markers, this may take a long time. Please wait...\n");

nit++: unrelated

>  
>      while (!(interrupt = ff_check_interrupt(&s->interrupt_callback))) {
>          ret = dvdvideo_play_next_ps_block(s, &state, nav_buf, DVDVIDEO_BLOCK_SIZE,
> @@ -858,8 +1058,15 @@ static int dvdvideo_video_stream_setup(AVFormatContext *s)
>  
>      int ret = 0;
>      DVDVideoVTSVideoStreamEntry entry = {0};
> +    video_attr_t video_attr;
>  
> -    if ((ret = dvdvideo_video_stream_analyze(s, c->vts_ifo->vtsi_mat->vts_video_attr, &entry)) < 0 ||
> +    if (c->opt_menu)
> +        video_attr = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->vmgm_video_attr :
> +                                        c->vts_ifo->vtsi_mat->vtsm_video_attr;
> +    else
> +        video_attr = c->vts_ifo->vtsi_mat->vts_video_attr;
> +
> +    if ((ret = dvdvideo_video_stream_analyze(s, video_attr, &entry)) < 0 ||
>          (ret = dvdvideo_video_stream_add(s, &entry, AVSTREAM_PARSE_HEADERS)) < 0) {
>  
>          av_log(s, AV_LOG_ERROR, "Unable to add video stream\n");
> @@ -1009,15 +1216,29 @@ static int dvdvideo_audio_stream_add_all(AVFormatContext *s)
>      DVDVideoDemuxContext *c = s->priv_data;
>  
>      int ret = 0;
> +    int nb_streams;
>  
> -    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams; i++) {
> +    if (c->opt_menu)
> +        nb_streams = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->nr_of_vmgm_audio_streams :
> +                                        c->vts_ifo->vtsi_mat->nr_of_vtsm_audio_streams;
> +    else
> +        nb_streams = c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams;
> +
> +    for (int i = 0; i < nb_streams; i++) {
>          DVDVideoPGCAudioStreamEntry entry = {0};
> +        audio_attr_t audio_attr;
> +
> +        if (c->opt_menu)
> +            audio_attr = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->vmgm_audio_attr :
> +                                            c->vts_ifo->vtsi_mat->vtsm_audio_attr;
> +        else
> +            audio_attr = c->vts_ifo->vtsi_mat->vts_audio_attr[i];
>  
>          if (!(c->play_state.pgc->audio_control[i] & 0x8000))
>              continue;
>  
> -        if ((ret = dvdvideo_audio_stream_analyze(s, c->vts_ifo->vtsi_mat->vts_audio_attr[i],
> -                                                 c->play_state.pgc->audio_control[i], &entry)) < 0)
> +        if ((ret = dvdvideo_audio_stream_analyze(s, audio_attr, c->play_state.pgc->audio_control[i],
> +                                                 &entry)) < 0)
>              goto break_error;
>  
>          /* IFO structures can declare duplicate entries for the same startcode */
> @@ -1127,7 +1348,16 @@ static int dvdvideo_subp_stream_add_all(AVFormatContext *s)
>  {
>      DVDVideoDemuxContext *c = s->priv_data;
>  
> -    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams; i++) {
> +    int nb_streams;
> +
> +    if (c->opt_menu)
> +        nb_streams = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->nr_of_vmgm_subp_streams :
> +                                        c->vts_ifo->vtsi_mat->nr_of_vtsm_subp_streams;
> +    else
> +        nb_streams = c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams;
> +
> +
> +    for (int i = 0; i < nb_streams; i++) {
>          int ret = 0;
>          uint32_t subp_control;
>          subp_attr_t subp_attr;
> @@ -1139,8 +1369,16 @@ static int dvdvideo_subp_stream_add_all(AVFormatContext *s)
>  
>          /* there can be several presentations for one SPU */
>          /* the DAR check is flexible in order to support weird authoring */
> -        video_attr = c->vts_ifo->vtsi_mat->vts_video_attr;
> -        subp_attr = c->vts_ifo->vtsi_mat->vts_subp_attr[i];
> +        if (c->opt_menu) {
> +            video_attr = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->vmgm_video_attr :
> +                                            c->vts_ifo->vtsi_mat->vtsm_video_attr;
> +
> +            subp_attr  = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->vmgm_subp_attr :
> +                                            c->vts_ifo->vtsi_mat->vtsm_subp_attr;
> +        } else {
> +            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) {
> @@ -1196,8 +1434,12 @@ static int dvdvideo_subdemux_read_data(void *opaque, uint8_t *buf, int buf_size)
>      if (c->play_end)
>          return AVERROR_EOF;
>  
> -    ret = dvdvideo_play_next_ps_block(opaque, &c->play_state, buf, buf_size,
> -                                      &nav_event, dvdvideo_subdemux_flush);
> +    if (c->opt_menu)
> +        ret = dvdvideo_menu_next_ps_block(s, &c->play_state, buf, buf_size,
> +                                          dvdvideo_subdemux_flush);
> +    else
> +        ret = dvdvideo_play_next_ps_block(opaque, &c->play_state, buf, buf_size,
> +                                          &nav_event, dvdvideo_subdemux_flush);
>  
>      if (ret == AVERROR_EOF) {
>          c->mpeg_pb.pub.eof_reached = 1;
> @@ -1261,6 +1503,47 @@ static int dvdvideo_read_header(AVFormatContext *s)
>  
>      int ret = 0;
>  
> +    if (c->opt_menu) {
> +        if (c->opt_region               ||
> +            c->opt_title > 1            ||
> +            c->opt_preindex             ||
> +            c->opt_chapter_start > 1    ||
> +            c->opt_chapter_end > 0) {

> +            av_log(s, AV_LOG_ERROR, "-menu is not compatible with the -region, -title, "
> +                                    "-preindex, or -chapter_start/-chapter_end options\n");

unrelated note: I wonder if we should use "-foo" for mentioning
options, since this in theory might be used also from the API,
probably we should just say: the menu option is not compatible with
... (this is not blocking and might be addressed by a further patch).

> +            return AVERROR(EINVAL);
> +        }
> +
> +        if (!c->opt_pgc) {
> +            av_log(s, AV_LOG_ERROR, "If -menu is enabled, -pgc must be set to a non-zero value\n");
> +
> +            return AVERROR(EINVAL);
> +        }
> +
> +        if (!c->opt_menu_lu) {
> +            av_log(s, AV_LOG_INFO, "Defaulting to menu language unit #1. "
> +                                   "This is not always desirable, validation suggested.\n");
> +
> +            c->opt_menu_lu = 1;
> +        }
> +
> +        if (!c->opt_pg) {
> +            av_log(s, AV_LOG_INFO, "Defaulting to menu PG #1. "
> +                                   "This is not always desirable, validation suggested.\n");
> +
> +            c->opt_pg = 1;
> +        }
> +
> +        if ((ret = dvdvideo_ifo_open(s)) < 0                    ||
> +            (ret = dvdvideo_menu_open(s, &c->play_state)) < 0   ||
> +            (ret = dvdvideo_subdemux_open(s)) < 0               ||
> +            (ret = dvdvideo_video_stream_setup(s)) < 0          ||
> +            (ret = dvdvideo_audio_stream_add_all(s)) < 0)
> +        return ret;
> +
> +        return 0;
> +    }
> +
>      if (c->opt_title == 0) {
>          av_log(s, AV_LOG_INFO, "Defaulting to title #1. "
>                                 "This is not always the main feature, validation suggested.\n");
> @@ -1376,7 +1659,12 @@ static int dvdvideo_close(AVFormatContext *s)
>      DVDVideoDemuxContext *c = s->priv_data;
>  
>      dvdvideo_subdemux_close(s);
> -    dvdvideo_play_close(s, &c->play_state);
> +
> +    if (c->opt_menu)
> +        dvdvideo_menu_close(s, &c->play_state);
> +    else
> +        dvdvideo_play_close(s, &c->play_state);
> +
>      dvdvideo_ifo_close(s);
>  
>      return 0;
> @@ -1388,6 +1676,9 @@ static const AVOption dvdvideo_options[] = {
>      {"chapter_end",     "exit chapter (PTT) number (0=end)",                        OFFSET(opt_chapter_end),    AV_OPT_TYPE_INT,    { .i64=0 },     0,          99,        AV_OPT_FLAG_DECODING_PARAM },
>      {"chapter_start",   "entry chapter (PTT) number",                               OFFSET(opt_chapter_start),  AV_OPT_TYPE_INT,    { .i64=1 },     1,          99,        AV_OPT_FLAG_DECODING_PARAM },
>      {"clut_rgb",        "output subtitle palette (CLUT) as RGB",                    OFFSET(opt_clut_rgb),       AV_OPT_TYPE_BOOL,   { .i64=1 },     0,          1,         AV_OPT_FLAG_DECODING_PARAM },
> +    {"menu",            "demux menu domain",                                        OFFSET(opt_menu),           AV_OPT_TYPE_BOOL,   { .i64=0 },     0,          1,         AV_OPT_FLAG_DECODING_PARAM },
> +    {"menu_lu",         "menu language unit (0=auto)",                              OFFSET(opt_menu_lu),        AV_OPT_TYPE_INT,    { .i64=0 },     0,          99,        AV_OPT_FLAG_DECODING_PARAM },
> +    {"menu_vts",        "menu VTS (0=VMG main menu)",                               OFFSET(opt_menu_vts),       AV_OPT_TYPE_INT,    { .i64=0 },     0,          99,        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 },
>      {"pgc",             "entry PGC number (0=auto)",                                OFFSET(opt_pgc),            AV_OPT_TYPE_INT,    { .i64=0 },     0,          999,       AV_OPT_FLAG_DECODING_PARAM },
>      {"preindex",        "enable for accurate chapter markers, slow (2-pass read)",  OFFSET(opt_preindex),       AV_OPT_TYPE_BOOL,   { .i64=0 },     0,          1,         AV_OPT_FLAG_DECODING_PARAM },

No more comments from me, nice feature!!
Marth64 March 6, 2024, 4:35 p.m. UTC | #2
Thank you Stefano for the review on the whole set. I will take the feedback
and come back with a better organized set in the next 2 or so days.

On Wed, Mar 6, 2024 at 09:42 Stefano Sabatini <stefasab@gmail.com> wrote:

> On date Wednesday 2024-03-06 01:19:12 -0600, Marth64 wrote:
> > Many DVDs have valuable assets in their menu structures, including
> background
> > video or music. Some discs also abuse the menu feature to include actual
> > feature footage that needs to change aspect ratio (in order to trick the
> DVD player).
> >
> > This patch allows extraction and archival of these assets, but does not
> enable
> > controllable playback (which needs a full-fledged player and nav VM).
> > Menus are processed directly with dvdread and the existing foundation of
> the demuxer.
> >
> > Will eventually add option to list their coordinates as well, so users
> > do not have to rely on other tools to find them.
> >
> > Signed-off-by: Marth64 <marth64@proxyid.net>
> > ---
> >  doc/demuxers.texi         |  43 +++++-
> >  libavformat/dvdvideodec.c | 315 ++++++++++++++++++++++++++++++++++++--
> >  2 files changed, 339 insertions(+), 19 deletions(-)
> >
> > diff --git a/doc/demuxers.texi b/doc/demuxers.texi
> > index 1a17c6db16..e2ea66c1a5 100644
> > --- a/doc/demuxers.texi
> > +++ b/doc/demuxers.texi
> > @@ -289,8 +289,10 @@ This demuxer accepts the following option:
> >
> >  DVD-Video demuxer, powered by libdvdnav and libdvdread.
> >
> > -Can directly ingest DVD titles, specifically sequential PGCs,
> > -into a conversion pipeline. Menus and seeking are not supported at this
> time.
> > +Can directly ingest DVD titles, specifically sequential PGCs, into
> > +a conversion pipeline. Menu assets, such as background video or audio,
> > +can also be demuxed given the menu's coordinates (at best effort).
> > +Seeking is not supported at this time.
> >
> >  Block devices (DVD drives), ISO files, and directory structures are
> accepted.
> >  Activate with @code{-f dvdvideo} in front of one of these inputs.
> > @@ -347,37 +349,56 @@ This demuxer accepts the following options:
> >
> >  @item title @var{int}
> >  The title number to play. Must be set if @option{pgc} and @option{pg}
> are not set.
> > +Not applicable to menus.
> >  Default is 0 (auto), which currently only selects the first available
> title (title 1)
> >  and notifies the user about the implications.
> >
> >  @item chapter_start @var{int}
> > -The chapter, or PTT (part-of-title), number to start at. Default is 1.
> > +The chapter, or PTT (part-of-title), number to start at. Not applicable
> to menus.
> > +Default is 1.
> >
> >  @item chapter_end @var{int}
> > -The chapter, or PTT (part-of-title), number to end at. Default is 0,
> > -which is a special value to signal end at the last possible chapter.
> > +The chapter, or PTT (part-of-title), number to end at. Not applicable
> to menus.
> > +Default is 0, which is a special value to signal end at the last
> possible chapter.
> >
> >  @item angle @var{int}
> >  The video angle number, referring to what is essentially an additional
> >  video stream that is composed from alternate frames interleaved in the
> VOBs.
> > +Not applicable to menus.
> >  Default is 1.
> >
> >  @item region @var{int}
> >  The region code to use for playback. Some discs may use this to default
> playback
> >  at a particular angle in different regions. This option will not affect
> the region code
> > -of a real DVD drive, if used as an input. Default is 0, "world".
> > +of a real DVD drive, if used as an input. Not applicable to menus.
> > +Default is 0, "world".
> > +
>
> > +@item menu @var{bool}
> > +Demux menu assets instead of navigating a title. Requires exact
> coordinates
> > +of the menu (@option{menu_lu}, @option{menu_vts}, @option{pgc},
> @option{pg}).
> > +Default is false.
> > +
>
> > +@item menu_lu @var{int}
> > +The menu language to demux. In DVD, menus are grouped by language.
> > +Default is 0, the first language unit.
> > +
> > +@item menu_vts @var{int}
> > +The VTS where the menu lives, or 0 if it is a VMG menu (root-level).
> > +Default is 0, VMG menu.
> >
> >  @item pgc @var{int}
> >  The entry PGC to start playback, in conjunction with @option{pg}.
> >  Alternative to setting @option{title}.
> >  Chapter markers are not supported at this time.
> > +Must be explicitly set for menus.
> >  Default is 0, automatically resolve from value of @option{title}.
> >
> >  @item pg @var{int}
> >  The entry PG to start playback, in conjunction with @option{pgc}.
> >  Alternative to setting @option{title}.
> >  Chapter markers are not supported at this time.
> > -Default is 0, automatically resolve from value of @option{title}.
> > +Default is 0, automatically resolve from value of @option{title}, or
> > +start from the beginning (PG 1) of the menu.
> >
> >  @item preindex @var{bool}
> >  Enable this to have accurate chapter (PTT) markers and duration
> measurement,
> > @@ -385,6 +406,7 @@ which requires a slow second pass read in order to
> index the chapter marker
> >  timestamps from NAV packets. This is non-ideal extra work for real
> optical drives.
> >  It is recommended and faster to use this option with a backup of the
> DVD structure
> >  stored on a hard drive. Not compatible with @option{pgc} and
> @option{pg}.
> > +Not applicable to menus.
> >  Default is 0, false.
> >
> >  @item trim @var{bool}
> > @@ -392,6 +414,7 @@ Skip padding cells (i.e. cells shorter than 1
> second) from the beginning.
> >  There exist many discs with filler segments at the beginning of the PGC,
> >  often with junk data intended for controlling a real DVD player's
> >  buffering speed and with no other material data value.
> > +Not applicable to menus.
> >  Default is 1, true.
> >
> >  @item clut_rgb @var{bool}
> > @@ -421,6 +444,12 @@ Open only chapter 5 from title 1 from a given DVD
> structure:
> >  @example
> >  ffmpeg -f dvdvideo -chapter_start 5 -chapter_end 5 -title 1 -i <path to
> DVD> ...
> >  @end example
> > +
> > +@item
> > +Demux menu with language 1 from VTS 1, PGC 1, starting at PG 1:
> > +@example
> > +ffmpeg -f dvdvideo -menu 1 -menu_lu 1 -menu_vts 1 -pgc 1 -pg 1 -i <path
> to DVD> ...
> > +@end example
> >  @end itemize
> >
> >  @section ea
> > diff --git a/libavformat/dvdvideodec.c b/libavformat/dvdvideodec.c
> > index 3bc76f5c65..2c7ffdd148 100644
> > --- a/libavformat/dvdvideodec.c
> > +++ b/libavformat/dvdvideodec.c
> > @@ -57,9 +57,11 @@
> >  #define DVDVIDEO_BLOCK_SIZE                             2048
> >  #define DVDVIDEO_TIME_BASE_Q                            (AVRational) {
> 1, 90000 }
> >  #define DVDVIDEO_PTS_WRAP_BITS                          64 /* VOBUs use
> 32 (PES allows 33) */
> > -
> >  #define DVDVIDEO_LIBDVDX_LOG_BUFFER_SIZE                1024
> >
> > +#define PCI_START_BYTE                                  45 /*
> complement dvdread's DSI_START_BYTE */
> > +static const uint8_t dvdvideo_nav_header[4] =           { 0x00, 0x00,
> 0x01, 0xBF };
> > +
> >  enum DVDVideoSubpictureViewport {
> >      DVDVIDEO_SUBP_VIEWPORT_FULLSCREEN,
> >      DVDVIDEO_SUBP_VIEWPORT_WIDESCREEN,
> > @@ -121,6 +123,15 @@ typedef struct DVDVideoPlaybackState {
> >      uint64_t                    *pgc_pg_times_est;  /* PG start times
> as reported by IFO */
> >      pgc_t                       *pgc;               /* handle to the
> active PGC */
> >      dvdnav_t                    *dvdnav;            /* handle to the
> dvdnav VM */
> > +
> > +    /* the following fields are only used for menu playback */
> > +    int                         celln_start;        /* starting cell
> number */
> > +    int                         celln_end;          /* ending cell
> number */
> > +    int                         sector_offset;      /* current sector
> relative to the current VOB */
> > +    uint32_t                    sector_end;         /* end sector
> relative to the current VOBU */
> > +    uint32_t                    vobu_next;          /* the next VOBU
> pointer */
> > +    uint32_t                    vobu_remaining;     /* remaining blocks
> for current VOBU */
> > +    dvd_file_t                  *vob_file;          /* handle to the
> menu VOB (VMG or VTS) */
> >  } DVDVideoPlaybackState;
> >
> >  typedef struct DVDVideoDemuxContext {
> > @@ -131,6 +142,9 @@ typedef struct DVDVideoDemuxContext {
> >      int                         opt_chapter_end;    /* the
> user-provided exit PTT (0 for last) */
> >      int                         opt_chapter_start;  /* the
> user-provided entry PTT (1-indexed) */
> >      int                         opt_clut_rgb;       /* output subtitle
> palette (CLUT) as RGB */
> > +    int                         opt_menu;           /* demux menu
> domain instead of title domain */
> > +    int                         opt_menu_lu;        /* the menu
> language unit (logical grouping) */
> > +    int                         opt_menu_vts;       /* the menu VTS, or
> 0 for VMG (main menu) */
> >      int                         opt_pg;             /* the
> user-provided PG number (1-indexed) */
> >      int                         opt_pgc;            /* the
> user-provided PGC number (1-indexed) */
> >      int                         opt_preindex;       /* pre-indexing
> mode (2-pass read) */
> > @@ -227,6 +241,16 @@ static int dvdvideo_ifo_open(AVFormatContext *s)
> >          return AVERROR_EXTERNAL;
> >      }
> >
> > +    if (c->opt_menu) {
> > +        if (c->opt_menu_vts > 0 && !(c->vts_ifo = ifoOpen(c->dvdread,
> c->opt_menu_vts))) {
> > +            av_log(s, AV_LOG_ERROR, "Unable to open IFO structure for
> VTS %d\n", c->opt_menu_vts);
> > +
> > +            return AVERROR_EXTERNAL;
> > +        }
> > +
> > +        return 0;
> > +    }
> > +
> >      if (c->opt_title > c->vmg_ifo->tt_srpt->nr_of_srpts) {
> >          av_log(s, AV_LOG_ERROR, "Title %d not found\n", c->opt_title);
> >
> > @@ -290,6 +314,182 @@ static int
> dvdvideo_is_pgc_promising(AVFormatContext *s, pgc_t *pgc)
> >      return 0;
> >  }
> >
> > +static void dvdvideo_menu_close(AVFormatContext *s,
> DVDVideoPlaybackState *state)
> > +{
> > +    if (state->vob_file)
> > +        DVDCloseFile(state->vob_file);
> > +}
> > +
> > +static int dvdvideo_menu_open(AVFormatContext *s, DVDVideoPlaybackState
> *state)
> > +{
> > +    DVDVideoDemuxContext *c = s->priv_data;
> > +    pgci_ut_t *pgci_ut;
> > +
> > +    pgci_ut = c->opt_menu_vts ? c->vts_ifo->pgci_ut :
> c->vmg_ifo->pgci_ut;
> > +    if (!pgci_ut) {
> > +        av_log(s, AV_LOG_ERROR, "Invalid PGC table for menu [LU %d, PGC
> %d]\n",
> > +                                c->opt_menu_lu, c->opt_pgc);
> > +
> > +        return AVERROR_INVALIDDATA;
> > +    }
> > +
> > +    if (c->opt_pgc < 1                      ||
> > +        c->opt_menu_lu < 1                  ||
> > +        c->opt_menu_lu > pgci_ut->nr_of_lus ||
> > +        c->opt_pgc > pgci_ut->lu[c->opt_menu_lu -
> 1].pgcit->nr_of_pgci_srp) {
> > +
> > +        av_log(s, AV_LOG_ERROR, "Menu [LU %d, PGC %d] not found\n",
> c->opt_menu_lu, c->opt_pgc);
> > +
> > +        return AVERROR(EINVAL);
> > +    }
> > +
> > +    /* make sure the PGC is valid */
> > +    state->pgcn          = c->opt_pgc - 1;
> > +    state->pgc           = pgci_ut->lu[c->opt_menu_lu -
> 1].pgcit->pgci_srp[c->opt_pgc - 1].pgc;
> > +
> > +    if (!state->pgc || !state->pgc->program_map ||
> !state->pgc->cell_playback) {
> > +        av_log(s, AV_LOG_ERROR, "Invalid PGC structure for menu [LU %d,
> PGC %d]\n",
> > +                                c->opt_menu_lu, c->opt_pgc);
> > +
> > +        return AVERROR_INVALIDDATA;
> > +    }
> > +
> > +    /* make sure the PG is valid */
> > +    state->entry_pgn     = c->opt_pg;
> > +    if (state->entry_pgn < 1 || state->entry_pgn >
> state->pgc->nr_of_programs) {
> > +        av_log(s, AV_LOG_ERROR, "Entry PG %d not found\n",
> state->entry_pgn);
> > +
> > +        return AVERROR(EINVAL);
> > +    }
> > +
> > +    /* make sure the program map isn't leading us to nowhere */
> > +    state->celln_start   = state->pgc->program_map[state->entry_pgn -
> 1];
> > +    state->celln_end     = state->pgc->nr_of_cells;
> > +    state->celln         = state->celln_start;
> > +    if (state->celln_start > state->pgc->nr_of_cells) {
> > +        av_log(s, AV_LOG_ERROR, "Invalid PGC structure: program map
> points to unknown cell\n");
> > +
> > +        return AVERROR_INVALIDDATA;
> > +    }
> > +
> > +    state->sector_end    = state->pgc->cell_playback[state->celln -
> 1].last_sector;
> > +    state->vobu_next     = state->pgc->cell_playback[state->celln -
> 1].first_sector;
> > +    state->sector_offset = state->vobu_next;
> > +
> > +    if (c->opt_menu_vts > 0)
> > +        state->in_vts    = 1;
> > +
> > +    if (!(state->vob_file = DVDOpenFile(c->dvdread, c->opt_menu_vts,
> DVD_READ_MENU_VOBS))) {
> > +        av_log(s, AV_LOG_ERROR, !c->opt_menu_vts ?
> > +                                "Unable to open main menu VOB
> (VIDEO_TS.VOB)\n" :
> > +                                "Unable to open menu VOBs for VTS
> %d\n", c->opt_menu_vts);
> > +
> > +        return AVERROR_EXTERNAL;
> > +    }
> > +
> > +    return 0;
> > +}
> > +
> > +static int dvdvideo_menu_next_ps_block(AVFormatContext *s,
> DVDVideoPlaybackState *state,
> > +                                       uint8_t *buf, int buf_size,
> > +                                       void (*flush_cb)(AVFormatContext
> *s))
> > +{
> > +    ssize_t blocks_read                   = 0;
> > +    uint8_t read_buf[DVDVIDEO_BLOCK_SIZE] = {0};
> > +    pci_t pci                             = (pci_t) {0};
> > +    dsi_t dsi                             = (dsi_t) {0};
> > +
> > +    if (buf_size != DVDVIDEO_BLOCK_SIZE) {
> > +        av_log(s, AV_LOG_ERROR, "Invalid buffer size (expected=%d
> actual=%d)\n",
> > +                                DVDVIDEO_BLOCK_SIZE, buf_size);
> > +
> > +        return AVERROR(EINVAL);
> > +    }
> > +
> > +    /* we were at the end of a vobu, so now go to the next one or EOF */
> > +    if (!state->vobu_remaining && state->in_pgc) {
> > +        if (state->vobu_next == SRI_END_OF_CELL) {
> > +            if (state->celln == state->celln_end &&
> state->sector_offset > state->sector_end)
> > +                return AVERROR_EOF;
> > +
> > +            state->celln++;
> > +            state->sector_offset =
> state->pgc->cell_playback[state->celln - 1].first_sector;
> > +            state->sector_end    =
> state->pgc->cell_playback[state->celln - 1].last_sector;
> > +        } else {
> > +            state->sector_offset = state->vobu_next;
> > +        }
> > +    }
> > +
> > +    /* continue reading the VOBU */
> > +    av_log(s, AV_LOG_TRACE, "reading block at offset %d\n",
> state->sector_offset);
> > +
> > +    blocks_read = DVDReadBlocks(state->vob_file, state->sector_offset,
> 1, read_buf);
>
> > +    if (blocks_read != 1) {
> > +        av_log(s, AV_LOG_ERROR, "Unable to read VOB block at offset
> %d\n", state->sector_offset);
>
> nit: you might show blocks_read in case it is an error message to aid
> debugging
>
> > +
> > +        return AVERROR_INVALIDDATA;
> > +    }
> > +
> > +    /* we are at the start of a VOBU, so we are expecting a NAV packet
> */
> > +    if (!state->vobu_remaining) {
> > +        if (!memcmp(&read_buf[PCI_START_BYTE - 4], dvdvideo_nav_header,
> 4) ||
> > +            !memcmp(&read_buf[DSI_START_BYTE - 4], dvdvideo_nav_header,
> 4) ||
> > +            read_buf[PCI_START_BYTE - 1] != 0x00
>    ||
> > +            read_buf[DSI_START_BYTE - 1] != 0x01) {
> > +
> > +            av_log(s, AV_LOG_ERROR, "Invalid NAV packet at offset %d:
> PCI or DSI header mismatch\n",
> > +                                    state->sector_offset);
> > +
> > +            return AVERROR_INVALIDDATA;
> > +        }
> > +
> > +        navRead_PCI(&pci, &read_buf[PCI_START_BYTE]);
> > +        navRead_DSI(&dsi, &read_buf[DSI_START_BYTE]);
> > +
> > +        if (!pci.pci_gi.vobu_s_ptm                          ||
> > +            !pci.pci_gi.vobu_e_ptm                          ||
> > +            pci.pci_gi.vobu_s_ptm > pci.pci_gi.vobu_e_ptm) {
> > +
> > +            av_log(s, AV_LOG_ERROR, "Invalid NAV packet at offset %d:
> PCI header is invalid\n",
> > +                                    state->sector_offset);
> > +
> > +            return AVERROR_INVALIDDATA;
> > +        }
> > +
> > +        state->vobu_remaining    = dsi.dsi_gi.vobu_ea;
> > +        state->vobu_next         = dsi.vobu_sri.next_vobu ==
> SRI_END_OF_CELL ? SRI_END_OF_CELL :
> > +                                   dsi.dsi_gi.nv_pck_lbn +
> (dsi.vobu_sri.next_vobu & 0x7FFFFFFF);
> > +        state->sector_offset++;
> > +
> > +        if (state->in_pgc) {
> > +            if (state->vobu_e_ptm != pci.pci_gi.vobu_s_ptm) {
> > +                if (flush_cb)
> > +                    flush_cb(s);
> > +
> > +                state->ts_offset += state->vobu_e_ptm -
> pci.pci_gi.vobu_s_ptm;
> > +            }
> > +        } else {
> > +            state->in_pgc        = 1;
> > +        }
> > +
> > +        state->vobu_e_ptm        = pci.pci_gi.vobu_e_ptm;
> > +
> > +        av_log(s, AV_LOG_DEBUG, "NAV packet: sector=%d "
> > +                                "vobu_s_ptm=%d vobu_e_ptm=%d
> ts_offset=%ld\n",
> > +                                dsi.dsi_gi.nv_pck_lbn,
> > +                                pci.pci_gi.vobu_s_ptm,
> pci.pci_gi.vobu_e_ptm, state->ts_offset);
> > +
> > +        return FFERROR_REDO;
> > +    }
> > +
> > +    /* we are in the middle of a VOBU, so pass on the PS packet */
> > +    memcpy(buf, &read_buf, DVDVIDEO_BLOCK_SIZE);
> > +    state->sector_offset++;
> > +    state->vobu_remaining--;
> > +
> > +    return DVDVIDEO_BLOCK_SIZE;
> > +}
> > +
> >  static void dvdvideo_play_close(AVFormatContext *s,
> DVDVideoPlaybackState *state)
> >  {
> >      if (!state->dvdnav)
> > @@ -716,7 +916,7 @@ static int
> dvdvideo_chapters_setup_preindex(AVFormatContext *s)
> >          goto end_close;
> >
> >      av_log(s, AV_LOG_INFO,
> > -           "Indexing chapter markers, this will take a long time.
> Please wait...\n");
>
> > +           "Indexing chapter markers, this may take a long time. Please
> wait...\n");
>
> nit++: unrelated
>
> >
> >      while (!(interrupt = ff_check_interrupt(&s->interrupt_callback))) {
> >          ret = dvdvideo_play_next_ps_block(s, &state, nav_buf,
> DVDVIDEO_BLOCK_SIZE,
> > @@ -858,8 +1058,15 @@ static int
> dvdvideo_video_stream_setup(AVFormatContext *s)
> >
> >      int ret = 0;
> >      DVDVideoVTSVideoStreamEntry entry = {0};
> > +    video_attr_t video_attr;
> >
> > -    if ((ret = dvdvideo_video_stream_analyze(s,
> c->vts_ifo->vtsi_mat->vts_video_attr, &entry)) < 0 ||
> > +    if (c->opt_menu)
> > +        video_attr = !c->opt_menu_vts ?
> c->vmg_ifo->vmgi_mat->vmgm_video_attr :
> > +
> c->vts_ifo->vtsi_mat->vtsm_video_attr;
> > +    else
> > +        video_attr = c->vts_ifo->vtsi_mat->vts_video_attr;
> > +
> > +    if ((ret = dvdvideo_video_stream_analyze(s, video_attr, &entry)) <
> 0 ||
> >          (ret = dvdvideo_video_stream_add(s, &entry,
> AVSTREAM_PARSE_HEADERS)) < 0) {
> >
> >          av_log(s, AV_LOG_ERROR, "Unable to add video stream\n");
> > @@ -1009,15 +1216,29 @@ static int
> dvdvideo_audio_stream_add_all(AVFormatContext *s)
> >      DVDVideoDemuxContext *c = s->priv_data;
> >
> >      int ret = 0;
> > +    int nb_streams;
> >
> > -    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams;
> i++) {
> > +    if (c->opt_menu)
> > +        nb_streams = !c->opt_menu_vts ?
> c->vmg_ifo->vmgi_mat->nr_of_vmgm_audio_streams :
> > +
> c->vts_ifo->vtsi_mat->nr_of_vtsm_audio_streams;
> > +    else
> > +        nb_streams = c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams;
> > +
> > +    for (int i = 0; i < nb_streams; i++) {
> >          DVDVideoPGCAudioStreamEntry entry = {0};
> > +        audio_attr_t audio_attr;
> > +
> > +        if (c->opt_menu)
> > +            audio_attr = !c->opt_menu_vts ?
> c->vmg_ifo->vmgi_mat->vmgm_audio_attr :
> > +
> c->vts_ifo->vtsi_mat->vtsm_audio_attr;
> > +        else
> > +            audio_attr = c->vts_ifo->vtsi_mat->vts_audio_attr[i];
> >
> >          if (!(c->play_state.pgc->audio_control[i] & 0x8000))
> >              continue;
> >
> > -        if ((ret = dvdvideo_audio_stream_analyze(s,
> c->vts_ifo->vtsi_mat->vts_audio_attr[i],
> > -
>  c->play_state.pgc->audio_control[i], &entry)) < 0)
> > +        if ((ret = dvdvideo_audio_stream_analyze(s, audio_attr,
> c->play_state.pgc->audio_control[i],
> > +                                                 &entry)) < 0)
> >              goto break_error;
> >
> >          /* IFO structures can declare duplicate entries for the same
> startcode */
> > @@ -1127,7 +1348,16 @@ static int
> dvdvideo_subp_stream_add_all(AVFormatContext *s)
> >  {
> >      DVDVideoDemuxContext *c = s->priv_data;
> >
> > -    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams;
> i++) {
> > +    int nb_streams;
> > +
> > +    if (c->opt_menu)
> > +        nb_streams = !c->opt_menu_vts ?
> c->vmg_ifo->vmgi_mat->nr_of_vmgm_subp_streams :
> > +
> c->vts_ifo->vtsi_mat->nr_of_vtsm_subp_streams;
> > +    else
> > +        nb_streams = c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams;
> > +
> > +
> > +    for (int i = 0; i < nb_streams; i++) {
> >          int ret = 0;
> >          uint32_t subp_control;
> >          subp_attr_t subp_attr;
> > @@ -1139,8 +1369,16 @@ static int
> dvdvideo_subp_stream_add_all(AVFormatContext *s)
> >
> >          /* there can be several presentations for one SPU */
> >          /* the DAR check is flexible in order to support weird
> authoring */
> > -        video_attr = c->vts_ifo->vtsi_mat->vts_video_attr;
> > -        subp_attr = c->vts_ifo->vtsi_mat->vts_subp_attr[i];
> > +        if (c->opt_menu) {
> > +            video_attr = !c->opt_menu_vts ?
> c->vmg_ifo->vmgi_mat->vmgm_video_attr :
> > +
> c->vts_ifo->vtsi_mat->vtsm_video_attr;
> > +
> > +            subp_attr  = !c->opt_menu_vts ?
> c->vmg_ifo->vmgi_mat->vmgm_subp_attr :
> > +
> c->vts_ifo->vtsi_mat->vtsm_subp_attr;
> > +        } else {
> > +            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) {
> > @@ -1196,8 +1434,12 @@ static int dvdvideo_subdemux_read_data(void
> *opaque, uint8_t *buf, int buf_size)
> >      if (c->play_end)
> >          return AVERROR_EOF;
> >
> > -    ret = dvdvideo_play_next_ps_block(opaque, &c->play_state, buf,
> buf_size,
> > -                                      &nav_event,
> dvdvideo_subdemux_flush);
> > +    if (c->opt_menu)
> > +        ret = dvdvideo_menu_next_ps_block(s, &c->play_state, buf,
> buf_size,
> > +                                          dvdvideo_subdemux_flush);
> > +    else
> > +        ret = dvdvideo_play_next_ps_block(opaque, &c->play_state, buf,
> buf_size,
> > +                                          &nav_event,
> dvdvideo_subdemux_flush);
> >
> >      if (ret == AVERROR_EOF) {
> >          c->mpeg_pb.pub.eof_reached = 1;
> > @@ -1261,6 +1503,47 @@ static int dvdvideo_read_header(AVFormatContext
> *s)
> >
> >      int ret = 0;
> >
> > +    if (c->opt_menu) {
> > +        if (c->opt_region               ||
> > +            c->opt_title > 1            ||
> > +            c->opt_preindex             ||
> > +            c->opt_chapter_start > 1    ||
> > +            c->opt_chapter_end > 0) {
>
> > +            av_log(s, AV_LOG_ERROR, "-menu is not compatible with the
> -region, -title, "
> > +                                    "-preindex, or
> -chapter_start/-chapter_end options\n");
>
> unrelated note: I wonder if we should use "-foo" for mentioning
> options, since this in theory might be used also from the API,
> probably we should just say: the menu option is not compatible with
> ... (this is not blocking and might be addressed by a further patch).
>
> > +            return AVERROR(EINVAL);
> > +        }
> > +
> > +        if (!c->opt_pgc) {
> > +            av_log(s, AV_LOG_ERROR, "If -menu is enabled, -pgc must be
> set to a non-zero value\n");
> > +
> > +            return AVERROR(EINVAL);
> > +        }
> > +
> > +        if (!c->opt_menu_lu) {
> > +            av_log(s, AV_LOG_INFO, "Defaulting to menu language unit
> #1. "
> > +                                   "This is not always desirable,
> validation suggested.\n");
> > +
> > +            c->opt_menu_lu = 1;
> > +        }
> > +
> > +        if (!c->opt_pg) {
> > +            av_log(s, AV_LOG_INFO, "Defaulting to menu PG #1. "
> > +                                   "This is not always desirable,
> validation suggested.\n");
> > +
> > +            c->opt_pg = 1;
> > +        }
> > +
> > +        if ((ret = dvdvideo_ifo_open(s)) < 0                    ||
> > +            (ret = dvdvideo_menu_open(s, &c->play_state)) < 0   ||
> > +            (ret = dvdvideo_subdemux_open(s)) < 0               ||
> > +            (ret = dvdvideo_video_stream_setup(s)) < 0          ||
> > +            (ret = dvdvideo_audio_stream_add_all(s)) < 0)
> > +        return ret;
> > +
> > +        return 0;
> > +    }
> > +
> >      if (c->opt_title == 0) {
> >          av_log(s, AV_LOG_INFO, "Defaulting to title #1. "
> >                                 "This is not always the main feature,
> validation suggested.\n");
> > @@ -1376,7 +1659,12 @@ static int dvdvideo_close(AVFormatContext *s)
> >      DVDVideoDemuxContext *c = s->priv_data;
> >
> >      dvdvideo_subdemux_close(s);
> > -    dvdvideo_play_close(s, &c->play_state);
> > +
> > +    if (c->opt_menu)
> > +        dvdvideo_menu_close(s, &c->play_state);
> > +    else
> > +        dvdvideo_play_close(s, &c->play_state);
> > +
> >      dvdvideo_ifo_close(s);
> >
> >      return 0;
> > @@ -1388,6 +1676,9 @@ static const AVOption dvdvideo_options[] = {
> >      {"chapter_end",     "exit chapter (PTT) number (0=end)",
>             OFFSET(opt_chapter_end),    AV_OPT_TYPE_INT,    { .i64=0 },
>  0,          99,        AV_OPT_FLAG_DECODING_PARAM },
> >      {"chapter_start",   "entry chapter (PTT) number",
>              OFFSET(opt_chapter_start),  AV_OPT_TYPE_INT,    { .i64=1 },
>  1,          99,        AV_OPT_FLAG_DECODING_PARAM },
> >      {"clut_rgb",        "output subtitle palette (CLUT) as RGB",
>             OFFSET(opt_clut_rgb),       AV_OPT_TYPE_BOOL,   { .i64=1 },
>  0,          1,         AV_OPT_FLAG_DECODING_PARAM },
> > +    {"menu",            "demux menu domain",
>             OFFSET(opt_menu),           AV_OPT_TYPE_BOOL,   { .i64=0 },
>  0,          1,         AV_OPT_FLAG_DECODING_PARAM },
> > +    {"menu_lu",         "menu language unit (0=auto)",
>             OFFSET(opt_menu_lu),        AV_OPT_TYPE_INT,    { .i64=0 },
>  0,          99,        AV_OPT_FLAG_DECODING_PARAM },
> > +    {"menu_vts",        "menu VTS (0=VMG main menu)",
>              OFFSET(opt_menu_vts),       AV_OPT_TYPE_INT,    { .i64=0 },
>  0,          99,        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 },
> >      {"pgc",             "entry PGC number (0=auto)",
>             OFFSET(opt_pgc),            AV_OPT_TYPE_INT,    { .i64=0 },
>  0,          999,       AV_OPT_FLAG_DECODING_PARAM },
> >      {"preindex",        "enable for accurate chapter markers, slow
> (2-pass read)",  OFFSET(opt_preindex),       AV_OPT_TYPE_BOOL,   { .i64=0
> },     0,          1,         AV_OPT_FLAG_DECODING_PARAM },
>
> No more comments from me, nice feature!!
>
diff mbox series

Patch

diff --git a/doc/demuxers.texi b/doc/demuxers.texi
index 1a17c6db16..e2ea66c1a5 100644
--- a/doc/demuxers.texi
+++ b/doc/demuxers.texi
@@ -289,8 +289,10 @@  This demuxer accepts the following option:
 
 DVD-Video demuxer, powered by libdvdnav and libdvdread.
 
-Can directly ingest DVD titles, specifically sequential PGCs,
-into a conversion pipeline. Menus and seeking are not supported at this time.
+Can directly ingest DVD titles, specifically sequential PGCs, into
+a conversion pipeline. Menu assets, such as background video or audio,
+can also be demuxed given the menu's coordinates (at best effort).
+Seeking is not supported at this time.
 
 Block devices (DVD drives), ISO files, and directory structures are accepted.
 Activate with @code{-f dvdvideo} in front of one of these inputs.
@@ -347,37 +349,56 @@  This demuxer accepts the following options:
 
 @item title @var{int}
 The title number to play. Must be set if @option{pgc} and @option{pg} are not set.
+Not applicable to menus.
 Default is 0 (auto), which currently only selects the first available title (title 1)
 and notifies the user about the implications.
 
 @item chapter_start @var{int}
-The chapter, or PTT (part-of-title), number to start at. Default is 1.
+The chapter, or PTT (part-of-title), number to start at. Not applicable to menus.
+Default is 1.
 
 @item chapter_end @var{int}
-The chapter, or PTT (part-of-title), number to end at. Default is 0,
-which is a special value to signal end at the last possible chapter.
+The chapter, or PTT (part-of-title), number to end at. Not applicable to menus.
+Default is 0, which is a special value to signal end at the last possible chapter.
 
 @item angle @var{int}
 The video angle number, referring to what is essentially an additional
 video stream that is composed from alternate frames interleaved in the VOBs.
+Not applicable to menus.
 Default is 1.
 
 @item region @var{int}
 The region code to use for playback. Some discs may use this to default playback
 at a particular angle in different regions. This option will not affect the region code
-of a real DVD drive, if used as an input. Default is 0, "world".
+of a real DVD drive, if used as an input. Not applicable to menus.
+Default is 0, "world".
+
+@item menu @var{bool}
+Demux menu assets instead of navigating a title. Requires exact coordinates
+of the menu (@option{menu_lu}, @option{menu_vts}, @option{pgc}, @option{pg}).
+Default is false.
+
+@item menu_lu @var{int}
+The menu language to demux. In DVD, menus are grouped by language.
+Default is 0, the first language unit.
+
+@item menu_vts @var{int}
+The VTS where the menu lives, or 0 if it is a VMG menu (root-level).
+Default is 0, VMG menu.
 
 @item pgc @var{int}
 The entry PGC to start playback, in conjunction with @option{pg}.
 Alternative to setting @option{title}.
 Chapter markers are not supported at this time.
+Must be explicitly set for menus.
 Default is 0, automatically resolve from value of @option{title}.
 
 @item pg @var{int}
 The entry PG to start playback, in conjunction with @option{pgc}.
 Alternative to setting @option{title}.
 Chapter markers are not supported at this time.
-Default is 0, automatically resolve from value of @option{title}.
+Default is 0, automatically resolve from value of @option{title}, or
+start from the beginning (PG 1) of the menu.
 
 @item preindex @var{bool}
 Enable this to have accurate chapter (PTT) markers and duration measurement,
@@ -385,6 +406,7 @@  which requires a slow second pass read in order to index the chapter marker
 timestamps from NAV packets. This is non-ideal extra work for real optical drives.
 It is recommended and faster to use this option with a backup of the DVD structure
 stored on a hard drive. Not compatible with @option{pgc} and @option{pg}.
+Not applicable to menus.
 Default is 0, false.
 
 @item trim @var{bool}
@@ -392,6 +414,7 @@  Skip padding cells (i.e. cells shorter than 1 second) from the beginning.
 There exist many discs with filler segments at the beginning of the PGC,
 often with junk data intended for controlling a real DVD player's
 buffering speed and with no other material data value.
+Not applicable to menus.
 Default is 1, true.
 
 @item clut_rgb @var{bool}
@@ -421,6 +444,12 @@  Open only chapter 5 from title 1 from a given DVD structure:
 @example
 ffmpeg -f dvdvideo -chapter_start 5 -chapter_end 5 -title 1 -i <path to DVD> ...
 @end example
+
+@item
+Demux menu with language 1 from VTS 1, PGC 1, starting at PG 1:
+@example
+ffmpeg -f dvdvideo -menu 1 -menu_lu 1 -menu_vts 1 -pgc 1 -pg 1 -i <path to DVD> ...
+@end example
 @end itemize
 
 @section ea
diff --git a/libavformat/dvdvideodec.c b/libavformat/dvdvideodec.c
index 3bc76f5c65..2c7ffdd148 100644
--- a/libavformat/dvdvideodec.c
+++ b/libavformat/dvdvideodec.c
@@ -57,9 +57,11 @@ 
 #define DVDVIDEO_BLOCK_SIZE                             2048
 #define DVDVIDEO_TIME_BASE_Q                            (AVRational) { 1, 90000 }
 #define DVDVIDEO_PTS_WRAP_BITS                          64 /* VOBUs use 32 (PES allows 33) */
-
 #define DVDVIDEO_LIBDVDX_LOG_BUFFER_SIZE                1024
 
+#define PCI_START_BYTE                                  45 /* complement dvdread's DSI_START_BYTE */
+static const uint8_t dvdvideo_nav_header[4] =           { 0x00, 0x00, 0x01, 0xBF };
+
 enum DVDVideoSubpictureViewport {
     DVDVIDEO_SUBP_VIEWPORT_FULLSCREEN,
     DVDVIDEO_SUBP_VIEWPORT_WIDESCREEN,
@@ -121,6 +123,15 @@  typedef struct DVDVideoPlaybackState {
     uint64_t                    *pgc_pg_times_est;  /* PG start times as reported by IFO */
     pgc_t                       *pgc;               /* handle to the active PGC */
     dvdnav_t                    *dvdnav;            /* handle to the dvdnav VM */
+
+    /* the following fields are only used for menu playback */
+    int                         celln_start;        /* starting cell number */
+    int                         celln_end;          /* ending cell number */
+    int                         sector_offset;      /* current sector relative to the current VOB */
+    uint32_t                    sector_end;         /* end sector relative to the current VOBU */
+    uint32_t                    vobu_next;          /* the next VOBU pointer */
+    uint32_t                    vobu_remaining;     /* remaining blocks for current VOBU */
+    dvd_file_t                  *vob_file;          /* handle to the menu VOB (VMG or VTS) */
 } DVDVideoPlaybackState;
 
 typedef struct DVDVideoDemuxContext {
@@ -131,6 +142,9 @@  typedef struct DVDVideoDemuxContext {
     int                         opt_chapter_end;    /* the user-provided exit PTT (0 for last) */
     int                         opt_chapter_start;  /* the user-provided entry PTT (1-indexed) */
     int                         opt_clut_rgb;       /* output subtitle palette (CLUT) as RGB */
+    int                         opt_menu;           /* demux menu domain instead of title domain */
+    int                         opt_menu_lu;        /* the menu language unit (logical grouping) */
+    int                         opt_menu_vts;       /* the menu VTS, or 0 for VMG (main menu) */
     int                         opt_pg;             /* the user-provided PG number (1-indexed) */
     int                         opt_pgc;            /* the user-provided PGC number (1-indexed) */
     int                         opt_preindex;       /* pre-indexing mode (2-pass read) */
@@ -227,6 +241,16 @@  static int dvdvideo_ifo_open(AVFormatContext *s)
         return AVERROR_EXTERNAL;
     }
 
+    if (c->opt_menu) {
+        if (c->opt_menu_vts > 0 && !(c->vts_ifo = ifoOpen(c->dvdread, c->opt_menu_vts))) {
+            av_log(s, AV_LOG_ERROR, "Unable to open IFO structure for VTS %d\n", c->opt_menu_vts);
+
+            return AVERROR_EXTERNAL;
+        }
+
+        return 0;
+    }
+
     if (c->opt_title > c->vmg_ifo->tt_srpt->nr_of_srpts) {
         av_log(s, AV_LOG_ERROR, "Title %d not found\n", c->opt_title);
 
@@ -290,6 +314,182 @@  static int dvdvideo_is_pgc_promising(AVFormatContext *s, pgc_t *pgc)
     return 0;
 }
 
+static void dvdvideo_menu_close(AVFormatContext *s, DVDVideoPlaybackState *state)
+{
+    if (state->vob_file)
+        DVDCloseFile(state->vob_file);
+}
+
+static int dvdvideo_menu_open(AVFormatContext *s, DVDVideoPlaybackState *state)
+{
+    DVDVideoDemuxContext *c = s->priv_data;
+    pgci_ut_t *pgci_ut;
+
+    pgci_ut = c->opt_menu_vts ? c->vts_ifo->pgci_ut : c->vmg_ifo->pgci_ut;
+    if (!pgci_ut) {
+        av_log(s, AV_LOG_ERROR, "Invalid PGC table for menu [LU %d, PGC %d]\n",
+                                c->opt_menu_lu, c->opt_pgc);
+
+        return AVERROR_INVALIDDATA;
+    }
+
+    if (c->opt_pgc < 1                      ||
+        c->opt_menu_lu < 1                  ||
+        c->opt_menu_lu > pgci_ut->nr_of_lus ||
+        c->opt_pgc > pgci_ut->lu[c->opt_menu_lu - 1].pgcit->nr_of_pgci_srp) {
+
+        av_log(s, AV_LOG_ERROR, "Menu [LU %d, PGC %d] not found\n", c->opt_menu_lu, c->opt_pgc);
+
+        return AVERROR(EINVAL);
+    }
+
+    /* make sure the PGC is valid */
+    state->pgcn          = c->opt_pgc - 1;
+    state->pgc           = pgci_ut->lu[c->opt_menu_lu - 1].pgcit->pgci_srp[c->opt_pgc - 1].pgc;
+
+    if (!state->pgc || !state->pgc->program_map || !state->pgc->cell_playback) {
+        av_log(s, AV_LOG_ERROR, "Invalid PGC structure for menu [LU %d, PGC %d]\n",
+                                c->opt_menu_lu, c->opt_pgc);
+
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* make sure the PG is valid */
+    state->entry_pgn     = c->opt_pg;
+    if (state->entry_pgn < 1 || state->entry_pgn > state->pgc->nr_of_programs) {
+        av_log(s, AV_LOG_ERROR, "Entry PG %d not found\n", state->entry_pgn);
+
+        return AVERROR(EINVAL);
+    }
+
+    /* make sure the program map isn't leading us to nowhere */
+    state->celln_start   = state->pgc->program_map[state->entry_pgn - 1];
+    state->celln_end     = state->pgc->nr_of_cells;
+    state->celln         = state->celln_start;
+    if (state->celln_start > state->pgc->nr_of_cells) {
+        av_log(s, AV_LOG_ERROR, "Invalid PGC structure: program map points to unknown cell\n");
+
+        return AVERROR_INVALIDDATA;
+    }
+
+    state->sector_end    = state->pgc->cell_playback[state->celln - 1].last_sector;
+    state->vobu_next     = state->pgc->cell_playback[state->celln - 1].first_sector;
+    state->sector_offset = state->vobu_next;
+
+    if (c->opt_menu_vts > 0)
+        state->in_vts    = 1;
+
+    if (!(state->vob_file = DVDOpenFile(c->dvdread, c->opt_menu_vts, DVD_READ_MENU_VOBS))) {
+        av_log(s, AV_LOG_ERROR, !c->opt_menu_vts ?
+                                "Unable to open main menu VOB (VIDEO_TS.VOB)\n" :
+                                "Unable to open menu VOBs for VTS %d\n", c->opt_menu_vts);
+
+        return AVERROR_EXTERNAL;
+    }
+
+    return 0;
+}
+
+static int dvdvideo_menu_next_ps_block(AVFormatContext *s, DVDVideoPlaybackState *state,
+                                       uint8_t *buf, int buf_size,
+                                       void (*flush_cb)(AVFormatContext *s))
+{
+    ssize_t blocks_read                   = 0;
+    uint8_t read_buf[DVDVIDEO_BLOCK_SIZE] = {0};
+    pci_t pci                             = (pci_t) {0};
+    dsi_t dsi                             = (dsi_t) {0};
+
+    if (buf_size != DVDVIDEO_BLOCK_SIZE) {
+        av_log(s, AV_LOG_ERROR, "Invalid buffer size (expected=%d actual=%d)\n",
+                                DVDVIDEO_BLOCK_SIZE, buf_size);
+
+        return AVERROR(EINVAL);
+    }
+
+    /* we were at the end of a vobu, so now go to the next one or EOF */
+    if (!state->vobu_remaining && state->in_pgc) {
+        if (state->vobu_next == SRI_END_OF_CELL) {
+            if (state->celln == state->celln_end && state->sector_offset > state->sector_end)
+                return AVERROR_EOF;
+
+            state->celln++;
+            state->sector_offset = state->pgc->cell_playback[state->celln - 1].first_sector;
+            state->sector_end    = state->pgc->cell_playback[state->celln - 1].last_sector;
+        } else {
+            state->sector_offset = state->vobu_next;
+        }
+    }
+
+    /* continue reading the VOBU */
+    av_log(s, AV_LOG_TRACE, "reading block at offset %d\n", state->sector_offset);
+
+    blocks_read = DVDReadBlocks(state->vob_file, state->sector_offset, 1, read_buf);
+    if (blocks_read != 1) {
+        av_log(s, AV_LOG_ERROR, "Unable to read VOB block at offset %d\n", state->sector_offset);
+
+        return AVERROR_INVALIDDATA;
+    }
+
+    /* we are at the start of a VOBU, so we are expecting a NAV packet */
+    if (!state->vobu_remaining) {
+        if (!memcmp(&read_buf[PCI_START_BYTE - 4], dvdvideo_nav_header, 4) ||
+            !memcmp(&read_buf[DSI_START_BYTE - 4], dvdvideo_nav_header, 4) ||
+            read_buf[PCI_START_BYTE - 1] != 0x00                           ||
+            read_buf[DSI_START_BYTE - 1] != 0x01) {
+
+            av_log(s, AV_LOG_ERROR, "Invalid NAV packet at offset %d: PCI or DSI header mismatch\n",
+                                    state->sector_offset);
+
+            return AVERROR_INVALIDDATA;
+        }
+
+        navRead_PCI(&pci, &read_buf[PCI_START_BYTE]);
+        navRead_DSI(&dsi, &read_buf[DSI_START_BYTE]);
+
+        if (!pci.pci_gi.vobu_s_ptm                          ||
+            !pci.pci_gi.vobu_e_ptm                          ||
+            pci.pci_gi.vobu_s_ptm > pci.pci_gi.vobu_e_ptm) {
+
+            av_log(s, AV_LOG_ERROR, "Invalid NAV packet at offset %d: PCI header is invalid\n",
+                                    state->sector_offset);
+
+            return AVERROR_INVALIDDATA;
+        }
+
+        state->vobu_remaining    = dsi.dsi_gi.vobu_ea;
+        state->vobu_next         = dsi.vobu_sri.next_vobu == SRI_END_OF_CELL ? SRI_END_OF_CELL :
+                                   dsi.dsi_gi.nv_pck_lbn + (dsi.vobu_sri.next_vobu & 0x7FFFFFFF);
+        state->sector_offset++;
+
+        if (state->in_pgc) {
+            if (state->vobu_e_ptm != pci.pci_gi.vobu_s_ptm) {
+                if (flush_cb)
+                    flush_cb(s);
+
+                state->ts_offset += state->vobu_e_ptm - pci.pci_gi.vobu_s_ptm;
+            }
+        } else {
+            state->in_pgc        = 1;
+        }
+
+        state->vobu_e_ptm        = pci.pci_gi.vobu_e_ptm;
+
+        av_log(s, AV_LOG_DEBUG, "NAV packet: sector=%d "
+                                "vobu_s_ptm=%d vobu_e_ptm=%d ts_offset=%ld\n",
+                                dsi.dsi_gi.nv_pck_lbn,
+                                pci.pci_gi.vobu_s_ptm, pci.pci_gi.vobu_e_ptm, state->ts_offset);
+
+        return FFERROR_REDO;
+    }
+
+    /* we are in the middle of a VOBU, so pass on the PS packet */
+    memcpy(buf, &read_buf, DVDVIDEO_BLOCK_SIZE);
+    state->sector_offset++;
+    state->vobu_remaining--;
+
+    return DVDVIDEO_BLOCK_SIZE;
+}
+
 static void dvdvideo_play_close(AVFormatContext *s, DVDVideoPlaybackState *state)
 {
     if (!state->dvdnav)
@@ -716,7 +916,7 @@  static int dvdvideo_chapters_setup_preindex(AVFormatContext *s)
         goto end_close;
 
     av_log(s, AV_LOG_INFO,
-           "Indexing chapter markers, this will take a long time. Please wait...\n");
+           "Indexing chapter markers, this may take a long time. Please wait...\n");
 
     while (!(interrupt = ff_check_interrupt(&s->interrupt_callback))) {
         ret = dvdvideo_play_next_ps_block(s, &state, nav_buf, DVDVIDEO_BLOCK_SIZE,
@@ -858,8 +1058,15 @@  static int dvdvideo_video_stream_setup(AVFormatContext *s)
 
     int ret = 0;
     DVDVideoVTSVideoStreamEntry entry = {0};
+    video_attr_t video_attr;
 
-    if ((ret = dvdvideo_video_stream_analyze(s, c->vts_ifo->vtsi_mat->vts_video_attr, &entry)) < 0 ||
+    if (c->opt_menu)
+        video_attr = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->vmgm_video_attr :
+                                        c->vts_ifo->vtsi_mat->vtsm_video_attr;
+    else
+        video_attr = c->vts_ifo->vtsi_mat->vts_video_attr;
+
+    if ((ret = dvdvideo_video_stream_analyze(s, video_attr, &entry)) < 0 ||
         (ret = dvdvideo_video_stream_add(s, &entry, AVSTREAM_PARSE_HEADERS)) < 0) {
 
         av_log(s, AV_LOG_ERROR, "Unable to add video stream\n");
@@ -1009,15 +1216,29 @@  static int dvdvideo_audio_stream_add_all(AVFormatContext *s)
     DVDVideoDemuxContext *c = s->priv_data;
 
     int ret = 0;
+    int nb_streams;
 
-    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams; i++) {
+    if (c->opt_menu)
+        nb_streams = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->nr_of_vmgm_audio_streams :
+                                        c->vts_ifo->vtsi_mat->nr_of_vtsm_audio_streams;
+    else
+        nb_streams = c->vts_ifo->vtsi_mat->nr_of_vts_audio_streams;
+
+    for (int i = 0; i < nb_streams; i++) {
         DVDVideoPGCAudioStreamEntry entry = {0};
+        audio_attr_t audio_attr;
+
+        if (c->opt_menu)
+            audio_attr = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->vmgm_audio_attr :
+                                            c->vts_ifo->vtsi_mat->vtsm_audio_attr;
+        else
+            audio_attr = c->vts_ifo->vtsi_mat->vts_audio_attr[i];
 
         if (!(c->play_state.pgc->audio_control[i] & 0x8000))
             continue;
 
-        if ((ret = dvdvideo_audio_stream_analyze(s, c->vts_ifo->vtsi_mat->vts_audio_attr[i],
-                                                 c->play_state.pgc->audio_control[i], &entry)) < 0)
+        if ((ret = dvdvideo_audio_stream_analyze(s, audio_attr, c->play_state.pgc->audio_control[i],
+                                                 &entry)) < 0)
             goto break_error;
 
         /* IFO structures can declare duplicate entries for the same startcode */
@@ -1127,7 +1348,16 @@  static int dvdvideo_subp_stream_add_all(AVFormatContext *s)
 {
     DVDVideoDemuxContext *c = s->priv_data;
 
-    for (int i = 0; i < c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams; i++) {
+    int nb_streams;
+
+    if (c->opt_menu)
+        nb_streams = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->nr_of_vmgm_subp_streams :
+                                        c->vts_ifo->vtsi_mat->nr_of_vtsm_subp_streams;
+    else
+        nb_streams = c->vts_ifo->vtsi_mat->nr_of_vts_subp_streams;
+
+
+    for (int i = 0; i < nb_streams; i++) {
         int ret = 0;
         uint32_t subp_control;
         subp_attr_t subp_attr;
@@ -1139,8 +1369,16 @@  static int dvdvideo_subp_stream_add_all(AVFormatContext *s)
 
         /* there can be several presentations for one SPU */
         /* the DAR check is flexible in order to support weird authoring */
-        video_attr = c->vts_ifo->vtsi_mat->vts_video_attr;
-        subp_attr = c->vts_ifo->vtsi_mat->vts_subp_attr[i];
+        if (c->opt_menu) {
+            video_attr = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->vmgm_video_attr :
+                                            c->vts_ifo->vtsi_mat->vtsm_video_attr;
+
+            subp_attr  = !c->opt_menu_vts ? c->vmg_ifo->vmgi_mat->vmgm_subp_attr :
+                                            c->vts_ifo->vtsi_mat->vtsm_subp_attr;
+        } else {
+            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) {
@@ -1196,8 +1434,12 @@  static int dvdvideo_subdemux_read_data(void *opaque, uint8_t *buf, int buf_size)
     if (c->play_end)
         return AVERROR_EOF;
 
-    ret = dvdvideo_play_next_ps_block(opaque, &c->play_state, buf, buf_size,
-                                      &nav_event, dvdvideo_subdemux_flush);
+    if (c->opt_menu)
+        ret = dvdvideo_menu_next_ps_block(s, &c->play_state, buf, buf_size,
+                                          dvdvideo_subdemux_flush);
+    else
+        ret = dvdvideo_play_next_ps_block(opaque, &c->play_state, buf, buf_size,
+                                          &nav_event, dvdvideo_subdemux_flush);
 
     if (ret == AVERROR_EOF) {
         c->mpeg_pb.pub.eof_reached = 1;
@@ -1261,6 +1503,47 @@  static int dvdvideo_read_header(AVFormatContext *s)
 
     int ret = 0;
 
+    if (c->opt_menu) {
+        if (c->opt_region               ||
+            c->opt_title > 1            ||
+            c->opt_preindex             ||
+            c->opt_chapter_start > 1    ||
+            c->opt_chapter_end > 0) {
+            av_log(s, AV_LOG_ERROR, "-menu is not compatible with the -region, -title, "
+                                    "-preindex, or -chapter_start/-chapter_end options\n");
+            return AVERROR(EINVAL);
+        }
+
+        if (!c->opt_pgc) {
+            av_log(s, AV_LOG_ERROR, "If -menu is enabled, -pgc must be set to a non-zero value\n");
+
+            return AVERROR(EINVAL);
+        }
+
+        if (!c->opt_menu_lu) {
+            av_log(s, AV_LOG_INFO, "Defaulting to menu language unit #1. "
+                                   "This is not always desirable, validation suggested.\n");
+
+            c->opt_menu_lu = 1;
+        }
+
+        if (!c->opt_pg) {
+            av_log(s, AV_LOG_INFO, "Defaulting to menu PG #1. "
+                                   "This is not always desirable, validation suggested.\n");
+
+            c->opt_pg = 1;
+        }
+
+        if ((ret = dvdvideo_ifo_open(s)) < 0                    ||
+            (ret = dvdvideo_menu_open(s, &c->play_state)) < 0   ||
+            (ret = dvdvideo_subdemux_open(s)) < 0               ||
+            (ret = dvdvideo_video_stream_setup(s)) < 0          ||
+            (ret = dvdvideo_audio_stream_add_all(s)) < 0)
+        return ret;
+
+        return 0;
+    }
+
     if (c->opt_title == 0) {
         av_log(s, AV_LOG_INFO, "Defaulting to title #1. "
                                "This is not always the main feature, validation suggested.\n");
@@ -1376,7 +1659,12 @@  static int dvdvideo_close(AVFormatContext *s)
     DVDVideoDemuxContext *c = s->priv_data;
 
     dvdvideo_subdemux_close(s);
-    dvdvideo_play_close(s, &c->play_state);
+
+    if (c->opt_menu)
+        dvdvideo_menu_close(s, &c->play_state);
+    else
+        dvdvideo_play_close(s, &c->play_state);
+
     dvdvideo_ifo_close(s);
 
     return 0;
@@ -1388,6 +1676,9 @@  static const AVOption dvdvideo_options[] = {
     {"chapter_end",     "exit chapter (PTT) number (0=end)",                        OFFSET(opt_chapter_end),    AV_OPT_TYPE_INT,    { .i64=0 },     0,          99,        AV_OPT_FLAG_DECODING_PARAM },
     {"chapter_start",   "entry chapter (PTT) number",                               OFFSET(opt_chapter_start),  AV_OPT_TYPE_INT,    { .i64=1 },     1,          99,        AV_OPT_FLAG_DECODING_PARAM },
     {"clut_rgb",        "output subtitle palette (CLUT) as RGB",                    OFFSET(opt_clut_rgb),       AV_OPT_TYPE_BOOL,   { .i64=1 },     0,          1,         AV_OPT_FLAG_DECODING_PARAM },
+    {"menu",            "demux menu domain",                                        OFFSET(opt_menu),           AV_OPT_TYPE_BOOL,   { .i64=0 },     0,          1,         AV_OPT_FLAG_DECODING_PARAM },
+    {"menu_lu",         "menu language unit (0=auto)",                              OFFSET(opt_menu_lu),        AV_OPT_TYPE_INT,    { .i64=0 },     0,          99,        AV_OPT_FLAG_DECODING_PARAM },
+    {"menu_vts",        "menu VTS (0=VMG main menu)",                               OFFSET(opt_menu_vts),       AV_OPT_TYPE_INT,    { .i64=0 },     0,          99,        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 },
     {"pgc",             "entry PGC number (0=auto)",                                OFFSET(opt_pgc),            AV_OPT_TYPE_INT,    { .i64=0 },     0,          999,       AV_OPT_FLAG_DECODING_PARAM },
     {"preindex",        "enable for accurate chapter markers, slow (2-pass read)",  OFFSET(opt_preindex),       AV_OPT_TYPE_BOOL,   { .i64=0 },     0,          1,         AV_OPT_FLAG_DECODING_PARAM },