From b17feb1f7910fffb4e4dcb9dec603c30c8b7414b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 02:06:53 +0200 Subject: [PATCH] stream_dvdnav: implement DVD menu navigation Drive libdvdnav's menu state through the new disc-nav protocol so the player can show and interact with VMGM and VTSM menus. STREAM_CTRL_NAV_CMD maps the nav actions onto libdvdnav. STREAM_CTRL_GET_NAV_STATE reports whether a menu with buttons is on screen, the highlight rectangle and palette of the focused button, and the ids of the disc-selected audio/SPU streams. Menu mode is entered by opening with dvd://menu or --disc-menu, which starts at the disc's root menu instead of a title. --- demux/demux_lavf.c | 12 +- stream/stream_dvdnav.c | 322 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 306 insertions(+), 28 deletions(-) diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index 67a04777e0..6c4cfcb4a2 100644 --- a/demux/demux_lavf.c +++ b/demux/demux_lavf.c @@ -238,6 +238,7 @@ typedef struct lavf_priv { struct stream *stream; bool own_stream; bool is_dvd_bd; + bool is_dvd; char *filename; struct format_hack format_hack; const AVInputFormat *avif; @@ -888,6 +889,12 @@ static void handle_new_stream(demuxer_t *demuxer, int i) mp_tags_move_from_av_dictionary(sh->tags, &st->metadata); demux_add_sh_stream(demuxer, sh); + // DVD routes the menu's button-graphics through an SPU + // substream lavf only discovers once the first SPU PES arrives + // mid-playback. Select it immediately to avoid missing menu highlights. + if (priv->is_dvd && sh->type == STREAM_SUB) + demuxer_select_track(demuxer, sh, MP_NOPTS_VALUE, true); + // Unfortunately, there is no better way to detect PCM codecs, other // than listing them all manually. (Or other "frameless" codecs. Or // rather, codecs with frames so small libavformat will put multiple of @@ -1632,8 +1639,9 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check) if (priv->stream) { const char *sname = priv->stream->info->name; - priv->is_dvd_bd = strcmp(sname, "dvdnav") == 0 || - strcmp(sname, "ifo_dvdnav") == 0 || + priv->is_dvd = strcmp(sname, "dvdnav") == 0 || + strcmp(sname, "ifo_dvdnav") == 0; + priv->is_dvd_bd = priv->is_dvd || strcmp(sname, "bd") == 0 || strcmp(sname, "bdnav") == 0 || strcmp(sname, "bdmv/bluray") == 0; diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index 4727e3158f..eed917e57f 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -49,11 +49,15 @@ #include "osdep/timer.h" #include "stream.h" #include "demux/demux.h" +#include "video/csputils.h" #include "video/out/vo.h" #define TITLE_MENU -1 #define TITLE_LONGEST -2 +#define DVD_SRC_W_DEFAULT 720 +#define DVD_SRC_H_DEFAULT 576 + struct priv { dvdnav_t *dvdnav; // handle to libdvdnav stuff char *filename; // path @@ -68,6 +72,15 @@ struct priv { int track; char *device; + bool in_menu; + int current_button; // mirror of libdvdnav HL_BTNN_REG + int btn_rect[4]; // x, y, w, h in source coords + uint32_t hl_palette[4]; // 0xAARRGGBB for SPU pixel values 0..3 + uint32_t nav_change_id; + uint32_t discontinuity_id; // bumped on actions that may jump + int src_w, src_h; // video resolution in pixels + int auto_actioned_button; // last auto-activated button; 0 if none + struct dvd_opts *opts; }; @@ -208,6 +221,210 @@ static int dvd_probe(const char *path, const char *ext, const char *sig) return r; } +static bool in_menu_domain(dvdnav_t *dvdnav) +{ + return dvdnav_is_domain_vmgm(dvdnav) == 1 || + dvdnav_is_domain_vtsm(dvdnav) == 1 || + dvdnav_is_domain_fp(dvdnav) == 1; +} + +static void compute_button_rect(struct priv *priv, pci_t *pci, int btn) +{ + priv->btn_rect[0] = priv->btn_rect[1] = 0; + priv->btn_rect[2] = priv->btn_rect[3] = 0; + if (btn <= 0 || btn > pci->hli.hl_gi.btn_ns) + return; + // btni_t is packed and full of bitfields, memcpy to ensure correct alignment. + btni_t b; + memcpy(&b, &pci->hli.btnit[btn - 1], sizeof(b)); + int xs = b.x_start, xe = b.x_end; + int ys = b.y_start, ye = b.y_end; + priv->btn_rect[0] = xs; + priv->btn_rect[1] = ys; + priv->btn_rect[2] = xe > xs ? xe - xs : 0; + priv->btn_rect[3] = ye > ys ? ye - ys : 0; +} + +// Resolve the 4-entry "select-state" highlight palette for the focused button. +// btn_coli holds two packed words per color set ([select, action]); the select +// word encodes [Ci3 Ci2 Ci1 Ci0 A3 A2 A1 A0]: four CLUT indices and four 4-bit +// alphas. +static void compute_highlight_palette(struct priv *priv, pci_t *pci, int btn) +{ + memset(priv->hl_palette, 0, sizeof(priv->hl_palette)); + if (!priv->spu_clut_valid || btn <= 0 || btn > pci->hli.hl_gi.btn_ns) + return; + btni_t b; + memcpy(&b, &pci->hli.btnit[btn - 1], sizeof(b)); + if (b.btn_coln == 0) + return; + int coln = b.btn_coln - 1; + if (coln > 2) + coln = 2; + uint32_t coli; + memcpy(&coli, &pci->hli.btn_colit.btn_coli[coln][0], sizeof(coli)); // [0] = select state + + struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS; + struct pl_transform3x3 cmatrix; + mp_get_csp_matrix(&csp, &cmatrix); + + for (int i = 0; i < 4; i++) { + uint8_t ci = (coli >> (16 + i * 4)) & 0xF; + uint8_t a = (coli >> (i * 4)) & 0xF; + uint32_t entry = priv->spu_clut[ci]; + // CLUT entry is 0x00YYCrCb. mp_get_csp_matrix returns a YCbCr→RGB matrix + // expecting {Y, Cb, Cr}, reorder to match this. + int y[3] = {(entry >> 16) & 0xff, entry & 0xff, (entry >> 8) & 0xff}; + int c[3]; + mp_map_fixp_color(&cmatrix, 8, y, 8, c); + uint32_t alpha = (a << 4) | a; + priv->hl_palette[i] = (alpha << 24) | (c[0] << 16) | (c[1] << 8) | c[2]; + } +} + +static void refresh_video_resolution(struct priv *priv) +{ + uint32_t w = 0, h = 0; + if (dvdnav_get_video_resolution(priv->dvdnav, &w, &h) == DVDNAV_STATUS_OK && + w > 0 && h > 0) + { + priv->src_w = (int)w; + priv->src_h = (int)h; + } else { + priv->src_w = DVD_SRC_W_DEFAULT; + priv->src_h = DVD_SRC_H_DEFAULT; + } +} + +// Pull the current selection back from libdvdnav and refresh our overlay +// state. Called on NAV_PACKET/HIGHLIGHT events and after every nav command. +static void update_highlight(struct priv *priv) +{ + int prev_btn = priv->current_button; + int prev_x = priv->btn_rect[0], prev_y = priv->btn_rect[1]; + int prev_w = priv->btn_rect[2], prev_h = priv->btn_rect[3]; + bool prev_menu = priv->in_menu; + uint32_t prev_palette[4]; + memcpy(prev_palette, priv->hl_palette, sizeof(prev_palette)); + + priv->in_menu = in_menu_domain(priv->dvdnav); + pci_t *pci = priv->in_menu ? dvdnav_get_current_nav_pci(priv->dvdnav) : NULL; + bool has_buttons = pci && pci->hli.hl_gi.hli_ss != 0 && pci->hli.hl_gi.btn_ns > 0; + + // Suppress the visible highlight while we're inside the menu's intro. + // The PCI gives us both the current VOBU's start PTS and the highlight + // valid window; once vobu_s_ptm catches up to hli_s_ptm (and we're still + // inside hli_e_ptm), it's live. + bool highlight_live = false; + if (has_buttons) { + uint32_t now = pci->pci_gi.vobu_s_ptm; + uint32_t hs = pci->hli.hl_gi.hli_s_ptm; + uint32_t he = pci->hli.hl_gi.hli_e_ptm; + highlight_live = now >= hs && (he == 0 || now < he); + } + + int32_t btn = 0; + if (highlight_live) + dvdnav_get_current_highlight(priv->dvdnav, &btn); + + if (!highlight_live || btn <= 0 || btn > pci->hli.hl_gi.btn_ns) { + priv->current_button = 0; + priv->btn_rect[0] = priv->btn_rect[1] = 0; + priv->btn_rect[2] = priv->btn_rect[3] = 0; + memset(priv->hl_palette, 0, sizeof(priv->hl_palette)); + } else { + priv->current_button = btn; + compute_button_rect(priv, pci, btn); + compute_highlight_palette(priv, pci, btn); + } + + if (priv->in_menu != prev_menu || priv->current_button != prev_btn || + priv->btn_rect[0] != prev_x || priv->btn_rect[1] != prev_y || + priv->btn_rect[2] != prev_w || priv->btn_rect[3] != prev_h || + memcmp(prev_palette, priv->hl_palette, sizeof(prev_palette)) != 0) + { + priv->nav_change_id++; + } + + // When we leave the menu, clear the auto-action latch so the next entry + // can fire again on the same button number. + if (!priv->in_menu) + priv->auto_actioned_button = 0; + + // A btnit entry can request immediate activation when its button gets focus. + if (highlight_live && priv->current_button > 0 && + priv->current_button != priv->auto_actioned_button) + { + btni_t b; + memcpy(&b, &pci->hli.btnit[priv->current_button - 1], sizeof(b)); + if (b.auto_action_mode == 1) { + priv->auto_actioned_button = priv->current_button; + dvdnav_button_activate(priv->dvdnav, pci); + } + } +} + +static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) +{ + struct priv *priv = stream->priv; + + switch (cmd->action) { + case STREAM_NAV_MENU_ROOT: + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Root); + update_highlight(priv); + return; + case STREAM_NAV_MENU_TITLE: + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Title); + update_highlight(priv); + return; + case STREAM_NAV_MENU_POPUP: + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Part); + update_highlight(priv); + return; + case STREAM_NAV_PREV_MENU: + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Escape); + update_highlight(priv); + return; + default: + break; + } + + if (!in_menu_domain(priv->dvdnav)) + return; + + pci_t *pci = dvdnav_get_current_nav_pci(priv->dvdnav); + if (!pci || pci->hli.hl_gi.hli_ss == 0 || pci->hli.hl_gi.btn_ns == 0) + return; + + switch (cmd->action) { + case STREAM_NAV_UP: + dvdnav_upper_button_select(priv->dvdnav, pci); + break; + case STREAM_NAV_DOWN: + dvdnav_lower_button_select(priv->dvdnav, pci); + break; + case STREAM_NAV_LEFT: + dvdnav_left_button_select(priv->dvdnav, pci); + break; + case STREAM_NAV_RIGHT: + dvdnav_right_button_select(priv->dvdnav, pci); + break; + case STREAM_NAV_MOUSE_MOVE: + dvdnav_mouse_select(priv->dvdnav, pci, cmd->x, cmd->y); + break; + case STREAM_NAV_MOUSE_CLICK: + dvdnav_mouse_activate(priv->dvdnav, pci, cmd->x, cmd->y); + break; + case STREAM_NAV_SELECT: + dvdnav_button_activate(priv->dvdnav, pci); + break; + default: + break; + } + + update_highlight(priv); +} + /** * \brief mp_dvdnav_lang_from_aid() returns the language corresponding to audio id 'aid' * \param stream: - stream pointer @@ -308,15 +525,23 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) pci_t *pnavpci = dvdnav_get_current_nav_pci(dvdnav); uint32_t start_pts = pnavpci->pci_gi.vobu_s_ptm; MP_TRACE(s, "start pts = %"PRIu32"\n", start_pts); + // Each NAV packet can change the highlighted button or the + // available button set; keep our mirrored state in sync. + update_highlight(priv); break; } case DVDNAV_STILL_FRAME: dvdnav_still_skip(dvdnav); - return 0; + break; case DVDNAV_WAIT: dvdnav_wait_skip(dvdnav); - return 0; + break; + case DVDNAV_HOP_CHANNEL: + // Bump discontinuity_id so the playloop flushes the cache. + priv->discontinuity_id++; + break; case DVDNAV_HIGHLIGHT: + update_highlight(priv); break; case DVDNAV_VTS_CHANGE: { int tit = 0, part = 0; @@ -337,6 +562,9 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) if (priv->title > 0 && tit != priv->title) MP_WARN(s, "Requested title not found\n"); } + // VTS change is a title-set boundary, flush. + refresh_video_resolution(priv); + priv->discontinuity_id++; break; } case DVDNAV_CELL_CHANGE: { @@ -350,6 +578,7 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) case DVDNAV_SPU_CLUT_CHANGE: { memcpy(priv->spu_clut, buf, 16 * sizeof(uint32_t)); priv->spu_clut_valid = true; + update_highlight(priv); break; } } @@ -443,8 +672,21 @@ static int control(stream_t *stream, int cmd, void *arg) } case STREAM_CTRL_SET_CURRENT_TITLE: { int title = *((unsigned int *) arg); + int32_t num_titles = 0; + if (dvdnav_get_number_of_titles(priv->dvdnav, &num_titles) != DVDNAV_STATUS_OK) + break; + // demux_disc appends a synthetic "Disc Menu" edition at the end. + if (title == num_titles) { + if (dvdnav_menu_call(priv->dvdnav, DVD_MENU_Root) + != DVDNAV_STATUS_OK) + break; + priv->discontinuity_id++; + stream_drop_buffers(stream); + return STREAM_OK; + } if (dvdnav_title_play(priv->dvdnav, title + 1) != DVDNAV_STATUS_OK) break; + priv->discontinuity_id++; stream_drop_buffers(stream); return STREAM_OK; } @@ -530,6 +772,29 @@ static int control(stream_t *stream, int cmd, void *arg) *(char**)arg = talloc_strdup(NULL, volume); return STREAM_OK; } + case STREAM_CTRL_NAV_CMD: { + handle_nav_cmd(stream, arg); + return STREAM_OK; + } + case STREAM_CTRL_GET_NAV_STATE: { + struct stream_nav_state *st = arg; + if (priv->src_w <= 0 || priv->src_h <= 0) + refresh_video_resolution(priv); + *st = (struct stream_nav_state){ + .menu_active = priv->in_menu, + .has_popup = false, + .src_w = priv->src_w, + .src_h = priv->src_h, + .hl_x = priv->btn_rect[0], + .hl_y = priv->btn_rect[1], + .hl_w = priv->btn_rect[2], + .hl_h = priv->btn_rect[3], + .change_id = priv->nav_change_id, + .discontinuity_id = priv->discontinuity_id, + }; + memcpy(st->hl_palette, priv->hl_palette, sizeof(st->hl_palette)); + return STREAM_OK; + } } return STREAM_UNSUPPORTED; @@ -596,35 +861,39 @@ static int open_s_internal(stream_t *stream) goto err; } + int32_t num_titles = 0; + dvdnav_get_number_of_titles(priv->dvdnav, &num_titles); + if (p->track == TITLE_LONGEST) { // longest dvdnav_t *dvdnav = priv->dvdnav; uint64_t best_length = 0; int best_title = -1; - int32_t num_titles; - if (dvdnav_get_number_of_titles(dvdnav, &num_titles) == DVDNAV_STATUS_OK) { - MP_VERBOSE(stream, "List of available titles:\n"); - for (int n = 1; n <= num_titles; n++) { - uint64_t *parts = NULL, duration = 0; - dvdnav_describe_title_chapters(dvdnav, n, &parts, &duration); - if (parts) { - if (duration > best_length) { - best_length = duration; - best_title = n; - } - if (duration > 90000) { // arbitrarily ignore <1s titles - char *time = mp_format_time(duration / 90000, false); - MP_VERBOSE(stream, "title: %3d duration: %s\n", - n - 1, time); - talloc_free(time); - } - free(parts); + MP_VERBOSE(stream, "List of available titles:\n"); + for (int n = 1; n <= num_titles; n++) { + uint64_t *parts = NULL, duration = 0; + dvdnav_describe_title_chapters(dvdnav, n, &parts, &duration); + if (parts) { + if (duration > best_length) { + best_length = duration; + best_title = n; } + if (duration > 90000) { // arbitrarily ignore <1s titles + char *time = mp_format_time(duration / 90000, false); + MP_VERBOSE(stream, "title: %3d duration: %s\n", + n - 1, time); + talloc_free(time); + } + free(parts); } } p->track = best_title - 1; MP_INFO(stream, "Selecting title %d.\n", p->track); } + // demux_disc.c appends a synthetic "Disc Menu" edition at index num_titles. + if (p->track >= num_titles) + p->track = TITLE_MENU; + if (p->track >= 0) { priv->title = p->track; if (dvdnav_title_play(priv->dvdnav, p->track + 1) != DVDNAV_STATUS_OK) { @@ -634,9 +903,8 @@ static int open_s_internal(stream_t *stream) goto err; } } else { - MP_FATAL(stream, "DVD menu support has been removed.\n"); - ret = STREAM_ERROR; - goto err; + priv->title = 0; + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Root); } if (p->opts->angle > 1) dvdnav_angle_change(priv->dvdnav, p->opts->angle); @@ -662,12 +930,13 @@ static int open_s(stream_t *stream) bstr title, bdevice; bstr_split_tok(bstr0(stream->path), "/", &title, &bdevice); - priv->track = TITLE_LONGEST; - struct MPOpts *opts = mp_get_config_group(stream, stream->global, &mp_opt_root); int edition_id = opts->edition_id; + bool disc_menu = opts->disc_menu; talloc_free(opts); + priv->track = disc_menu ? TITLE_MENU : TITLE_LONGEST; + if (edition_id >= 0) { priv->track = edition_id; } else if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) { @@ -712,7 +981,8 @@ static int ifo_dvdnav_stream_open(stream_t *stream) goto unsupported; struct MPOpts *opts = mp_get_config_group(NULL, stream->global, &mp_opt_root); - priv->track = opts->edition_id >= 0 ? opts->edition_id : TITLE_LONGEST; + priv->track = opts->edition_id >= 0 ? opts->edition_id : + (opts->disc_menu ? TITLE_MENU : TITLE_LONGEST); talloc_free(opts); char *path = mp_file_get_path(priv, bstr0(stream->url));