From 3897b3a579014b5bb293180f6e2a24fe3e6fb9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 28 Jun 2026 18:33:12 +0200 Subject: [PATCH] stream_dvdnav: add support for still frames DVDNAV_STILL_FRAME means the VM is holding a frame indefinitely (menu pages, some logos) or for a timed duration. Report the indefinite case via nav state so the player holds playback at EOF instead of ending the file, and skip timed stills, which would otherwise stall playback. --- player/core.h | 2 ++ player/discnav.c | 16 ++++++++++ player/playloop.c | 4 +++ stream/stream.h | 1 + stream/stream_dvdnav.c | 71 +++++++++++++++++++++++++++++++++++------- 5 files changed, 83 insertions(+), 11 deletions(-) diff --git a/player/core.h b/player/core.h index 5e961cbb56..2889d352bd 100644 --- a/player/core.h +++ b/player/core.h @@ -447,6 +447,8 @@ typedef struct MPContext { struct screenshot_ctx *screenshot_ctx; struct command_ctx *command_ctx; struct disc_nav_state *disc_nav; + // Demuxer parked on an indefinite still frame (DVD/BD menu). + bool disc_nav_still_frame; struct encode_lavc_context *encode_lavc_ctx; struct mp_option_callback *option_callbacks; diff --git a/player/discnav.c b/player/discnav.c index 3f69f83f92..66548bd3f0 100644 --- a/player/discnav.c +++ b/player/discnav.c @@ -46,6 +46,10 @@ struct disc_nav_state { uint32_t bd_last_change_id; struct mp_osd_res bd_last_vo_res; + // Last menu-overlay change_id we forced an OSD redraw for. + uint32_t last_overlay_change_id; + bool overlay_change_seen; + // Last observed disc-nav discontinuity counter (bumped by the stream // backend on user nav actions and on libbluray/libdvdnav-internal // playlist/title/cell transitions). @@ -441,6 +445,11 @@ void disc_nav_update(struct MPContext *mpctx) struct disc_nav_state *st = get_state(mpctx); struct stream_nav_state nav = {0}; bool have = s && stream_control(s, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1; + bool still = have && nav.still_active; + if (s && still != mpctx->disc_nav_still_frame) + MP_VERBOSE(s, "still_frame %d->%d\n", + mpctx->disc_nav_still_frame, still); + mpctx->disc_nav_still_frame = still; if (!have) { if (st->overlay_visible) { osd_set_bitmaps(mpctx->osd, OSDTYPE_DISC_MENU, NULL); @@ -450,6 +459,7 @@ void disc_nav_update(struct MPContext *mpctx) st->bd_last_change_id = 0; st->bd_last_vo_res = (struct mp_osd_res){0}; st->menu_selected_track = NULL; + st->overlay_change_seen = false; if (st->menu_active_seen && st->last_menu_active) { st->last_menu_active = false; mp_notify_property(mpctx, "disc-menu-active"); @@ -477,6 +487,12 @@ void disc_nav_update(struct MPContext *mpctx) ensure_menu_sub_selection(mpctx, nav.menu_active); } + if (!st->overlay_change_seen || nav.change_id != st->last_overlay_change_id) { + st->overlay_change_seen = true; + st->last_overlay_change_id = nav.change_id; + osd_changed(mpctx->osd); + } + if (visible != st->overlay_visible) { MP_VERBOSE(s, "overlay %s\n", visible ? "on" : "off"); if (visible) { diff --git a/player/playloop.c b/player/playloop.c index f472e40969..0e74b09619 100644 --- a/player/playloop.c +++ b/player/playloop.c @@ -1238,6 +1238,10 @@ static void handle_eof(struct MPContext *mpctx) bool prevent_eof = mpctx->paused && mpctx->video_out && vo_has_frame(mpctx->video_out) && !mpctx->vo_chain->is_coverart; + /* A disc menu parked on an infinite still frame reports EOF so the decoder + * drains and the menu frame is shown. Hold it until the user navigates, + * rather than ending the file. */ + prevent_eof |= mpctx->disc_nav_still_frame; /* It's possible for the user to simultaneously switch both audio * and video streams to "disabled" at runtime. Handle this by waiting * rather than immediately stopping playback due to EOF. diff --git a/stream/stream.h b/stream/stream.h index 5f2e4c7223..49fc3262ef 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -147,6 +147,7 @@ struct stream_nav_state { bool nav_active; // interactive disc navigation is enabled bool menu_active; // a selectable menu/highlight is currently visible bool has_popup; // disc supports a popup menu (BD only) + bool still_active; // holding an indefinite still frame int src_w, src_h; // dimensions of the coordinate space mouse uses // Highlight rectangle of the currently focused button (in src coords). int hl_x, hl_y, hl_w, hl_h; diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index 16c657eddb..74586d25c4 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -63,6 +63,7 @@ struct priv { char *filename; // path unsigned int duration; // in milliseconds int title; + bool still_active; // fill_buffer() is holding a still uint32_t spu_clut[16]; bool spu_clut_valid; bool had_initial_vts; @@ -391,7 +392,28 @@ static void update_highlight(struct priv *priv) } } -static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) +// Move the highlight to the spec-defined neighbour of the current button. We +// resolve the neighbour ourselves and use dvdnav_button_select() (rather than +// dvdnav_{upper,lower,left,right}_button_select()) so that auto-action buttons +// are activated only through update_highlight(), where we can observe it. +static void select_neighbour_button(struct priv *priv, pci_t *pci, + enum stream_nav_action action) +{ + int32_t cur = 0; + dvdnav_get_current_highlight(priv->dvdnav, &cur); + if (cur <= 0 || cur > pci->hli.hl_gi.btn_ns) + return; + btni_t b; + memcpy(&b, &pci->hli.btnit[cur - 1], sizeof(b)); + int target = action == STREAM_NAV_UP ? b.up : + action == STREAM_NAV_DOWN ? b.down : + action == STREAM_NAV_LEFT ? b.left : + action == STREAM_NAV_RIGHT ? b.right : 0; + if (target > 0) + dvdnav_button_select(priv->dvdnav, pci, target); +} + +static void do_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) { struct priv *priv = stream->priv; @@ -425,16 +447,10 @@ static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) 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); + select_neighbour_button(priv, pci, cmd->action); break; case STREAM_NAV_MOUSE_MOVE: dvdnav_mouse_select(priv->dvdnav, pci, cmd->x, cmd->y); @@ -452,6 +468,18 @@ static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) update_highlight(priv); } +static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) +{ + struct priv *priv = stream->priv; + + int prev_auto = priv->auto_actioned_button; + do_nav_cmd(stream, cmd); + bool activated = stream_nav_action_activates(cmd->action) || + priv->auto_actioned_button != prev_auto; + if (priv->still_active && activated) + priv->discontinuity_id++; +} + /** * \brief mp_dvdnav_lang_from_aid() returns the language corresponding to audio id 'aid' * \param stream: - stream pointer @@ -545,8 +573,12 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) } switch (event) { case DVDNAV_BLOCK_OK: + // Real data is flowing again: we are no longer holding a still. + priv->still_active = false; return len; case DVDNAV_STOP: + // End of disc: a real EOF, not a held still. + priv->still_active = false; return 0; case DVDNAV_NAV_PACKET: { pci_t *pnavpci = dvdnav_get_current_nav_pci(dvdnav); @@ -557,9 +589,25 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) update_highlight(priv); break; } - case DVDNAV_STILL_FRAME: - dvdnav_still_skip(dvdnav); - break; + case DVDNAV_STILL_FRAME: { + dvdnav_still_event_t *still = buf; + // We only honor indefinite (0xff) stills. Finite stills (studio + // logos / warnings shown for a few seconds before the menu) are + // not hold on screen. This avoids complexities with correctly + // timing the still frames, and there is little use-case for holding + // 10+ seconds on single still frame. + if (still->length != 0xFF) { + MP_VERBOSE(s, "skipping finite still (%d s)\n", + still->length); + dvdnav_still_skip(dvdnav); + break; + } + // Indefinite still: report EOF to the demuxer so the video decoder + // is drained and the last frame is actually pushed to screen. + MP_VERBOSE(s, "indefinite still -> EOF, hold last frame\n"); + priv->still_active = true; + return 0; + } case DVDNAV_WAIT: dvdnav_wait_skip(dvdnav); break; @@ -844,6 +892,7 @@ static int control(stream_t *stream, int cmd, void *arg) .no_audio = no_audio, .menu_active = priv->in_menu, .has_popup = false, + .still_active = priv->still_active, .src_w = priv->src_w, .src_h = priv->src_h, .hl_x = priv->btn_rect[0],