From 8356c7f674128fab9472204d760bc7d08ecc4ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 15:41:26 +0200 Subject: [PATCH] demux_disc: keep one-shot DVD menu subpictures deliverable Keep DVD SPU streams always enabled at the slave level and retain the last subpicture per substream. Re-deliver the retained copy when a sub track is (re)selected and when the player resets playback state. --- demux/demux.c | 32 ++++++++++++++++ demux/demux.h | 3 ++ demux/demux_disc.c | 96 ++++++++++++++++++++++++++++++++++++++++++++-- player/discnav.c | 56 +++++++++++++++++---------- player/playloop.c | 3 ++ 5 files changed, 167 insertions(+), 23 deletions(-) diff --git a/demux/demux.c b/demux/demux.c index 2314abfd1c..2e0eb0d2c4 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -243,6 +243,7 @@ struct demux_internal { bool back_any_need_recheck; // at least 1 ds->back_need_recheck set bool tracks_switched; // thread needs to inform demuxer of this + bool nav_refresh; // thread needs to call desc->nav_refresh atomic_bool seeking; // there's a seek queued; read unlocked // by demux_read_interrupted() @@ -1270,6 +1271,21 @@ void demux_drive_nav(struct demuxer *demuxer) mp_mutex_unlock(&in->lock); } +// Re-queue sticky packets (e.g. the DVD menu subpicture) for re-delivery. +void demux_nav_refresh(struct demuxer *demuxer) +{ + struct demux_internal *in = demuxer->in; + mp_assert(demuxer == in->d_user); + + mp_mutex_lock(&in->lock); + if (in->d_thread->desc->nav_refresh) { + in->nav_refresh = true; + in->reading = true; + mp_cond_signal(&in->wakeup); + } + mp_mutex_unlock(&in->lock); +} + const char *stream_type_name(enum stream_type type) { switch (type) { @@ -2550,6 +2566,18 @@ static void execute_trackswitch(struct demux_internal *in) mp_mutex_lock(&in->lock); } +static void execute_nav_refresh(struct demux_internal *in) +{ + in->nav_refresh = false; + + mp_mutex_unlock(&in->lock); + + if (in->d_thread->desc->nav_refresh) + in->d_thread->desc->nav_refresh(in->d_thread); + + mp_mutex_lock(&in->lock); +} + static void execute_seek(struct demux_internal *in) { int flags = in->seek_flags; @@ -2668,6 +2696,10 @@ static bool thread_work(struct demux_internal *in) execute_trackswitch(in); return true; } + if (in->nav_refresh) { + execute_nav_refresh(in); + return true; + } if (in->need_back_seek) { perform_backward_seek(in); return true; diff --git a/demux/demux.h b/demux/demux.h index 5159c39c26..0b81810d6f 100644 --- a/demux/demux.h +++ b/demux/demux.h @@ -147,6 +147,8 @@ typedef struct demuxer_desc { // will be repeated. bool (*read_packet)(struct demuxer *demuxer, struct demux_packet **pkt); void (*drop_buffers)(struct demuxer *demuxer); + // Optional. Re-queue retained sticky packets (DVD menu subpicture). + void (*nav_refresh)(struct demuxer *demuxer); void (*close)(struct demuxer *demuxer); void (*seek)(struct demuxer *demuxer, double rel_seek_secs, int flags); void (*switched_tracks)(struct demuxer *demuxer); @@ -326,6 +328,7 @@ void demux_stop_thread(struct demuxer *demuxer); void demux_set_wakeup_cb(struct demuxer *demuxer, void (*cb)(void *ctx), void *ctx); void demux_start_prefetch(struct demuxer *demuxer); void demux_drive_nav(struct demuxer *demuxer); +void demux_nav_refresh(struct demuxer *demuxer); bool demux_cancel_test(struct demuxer *demuxer); bool demux_read_interrupted(struct demuxer *demuxer); diff --git a/demux/demux_disc.c b/demux/demux_disc.c index aa163d66e3..3204c59b0e 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -25,6 +25,7 @@ #include "stream/stream.h" #include "video/mp_image.h" #include "demux.h" +#include "packet.h" #include "stheader.h" #include "video/csputils.h" @@ -52,6 +53,15 @@ struct priv { // 0x20, carrying the disc-level CLUT as extradata. struct sh_stream *dvd_subs[MAX_DVD_SPU_STREAMS]; + // DVD-only: retain the last SPU packet per substream. The menu subpicture + // is one-shot on an unseekable stream; re-deliver it on (re)selection and + // demux_nav_refresh(), like a refresh seek for ordinary streams. + struct dvd_sub_hold { + struct demux_packet *pkt; // clone, with playback-rebased timestamps + struct sh_stream *sh; // outer stream it belongs to + bool pending; // re-deliver on next read + } dvd_sub_hold[MAX_DVD_SPU_STREAMS]; + // Used to rewrite the raw MPEG timestamps to playback time. double base_time; // playback display start time of current segment double base_dts; // packet DTS that maps to base_time @@ -63,6 +73,33 @@ struct priv { bool is_dvd, is_cdda, is_bd; }; +static void clear_dvd_sub_holds(struct priv *p) +{ + for (int i = 0; i < MAX_DVD_SPU_STREAMS; i++) { + talloc_free(p->dvd_sub_hold[i].pkt); + p->dvd_sub_hold[i] = (struct dvd_sub_hold){0}; + } +} + +// Mark retained subpictures of selected streams for re-delivery. +static void arm_dvd_sub_holds(struct priv *p) +{ + for (int i = 0; i < MAX_DVD_SPU_STREAMS; i++) { + struct dvd_sub_hold *h = &p->dvd_sub_hold[i]; + if (h->pkt && h->sh && demux_stream_is_selected(h->sh)) + h->pending = true; + } +} + +// Whether the slave stream mapped to this outer stream should be enabled. +// DVD SPU streams always stay enabled: the one-shot menu subpicture must be +// captured into dvd_sub_hold even before any sub track is selected. +static bool slave_stream_enabled(struct priv *p, struct sh_stream *outer) +{ + return demux_stream_is_selected(outer) || + (p->is_dvd && outer->type == STREAM_SUB); +} + static void reselect_streams(demuxer_t *demuxer) { struct priv *p = demuxer->priv; @@ -72,8 +109,15 @@ static void reselect_streams(demuxer_t *demuxer) for (int n = 0; n < num_slave && n < p->slave_to_outer_count; n++) { struct sh_stream *outer = p->slave_to_outer[n]; demuxer_select_track(p->slave, demux_get_stream(p->slave, n), - MP_NOPTS_VALUE, outer && demux_stream_is_selected(outer)); + MP_NOPTS_VALUE, outer && slave_stream_enabled(p, outer)); } + arm_dvd_sub_holds(p); +} + +static void d_nav_refresh(demuxer_t *demuxer) +{ + struct priv *p = demuxer->priv; + arm_dvd_sub_holds(p); } static void get_disc_lang(struct stream *stream, struct sh_stream *sh, bool dvd) @@ -266,7 +310,7 @@ static void sync_streams(struct demuxer *demuxer) for (int n = 0; n < num_slave; n++) { struct sh_stream *outer = p->slave_to_outer[n]; demuxer_select_track(p->slave, demux_get_stream(p->slave, n), - MP_NOPTS_VALUE, outer && demux_stream_is_selected(outer)); + MP_NOPTS_VALUE, outer && slave_stream_enabled(p, outer)); } // Mirror slave sh_stream_group onto the outer sh_streams. This is needed @@ -318,6 +362,8 @@ static void d_seek(demuxer_t *demuxer, double seek_pts, int flags) if (p->slave && p->slave->desc->drop_buffers) p->slave->desc->drop_buffers(p->slave); + clear_dvd_sub_holds(p); + p->seek_reinit = true; } @@ -384,6 +430,7 @@ static bool reopen_slave(struct demuxer *demuxer) MP_VERBOSE(demuxer, "reopening slave demuxer\n"); demux_free(p->slave); + clear_dvd_sub_holds(p); // Discard anything the stream wrapper buffered before the disc-nav // discontinuity, and restart byte positions at 0: the post-jump data is // a new stream, and the fresh slave expects to probe it from the start @@ -417,6 +464,7 @@ static bool process_discontinuity(struct demuxer *demuxer, uint32_t new_id) if (!reopen_slave(demuxer)) return false; } else { + clear_dvd_sub_holds(p); if (p->slave->desc->drop_buffers) p->slave->desc->drop_buffers(p->slave); refresh_disc_metadata(demuxer); @@ -430,8 +478,10 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt { struct priv *p = demuxer->priv; + bool menu_active = false; struct stream_nav_state nav = {0}; if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) { + menu_active = nav.menu_active; if (nav.nav_active != p->nav_active) { p->nav_active = nav.nav_active; demux_set_nav_active(demuxer, nav.nav_active); @@ -444,6 +494,26 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt } } + // Re-deliver a retained menu subpicture. + if (menu_active) { + for (int i = 0; i < MAX_DVD_SPU_STREAMS; i++) { + struct dvd_sub_hold *h = &p->dvd_sub_hold[i]; + if (!h->pending || !h->pkt || !h->sh || + !demux_stream_is_selected(h->sh)) + continue; + h->pending = false; + struct demux_packet *rp = demux_copy_packet(demuxer->packet_pool, + h->pkt); + if (!rp) + continue; + MP_DBG(demuxer, "re-delivering held dvd sub 0x%x (pts=%f)\n", + h->sh->demuxer_id, rp->pts); + rp->stream = h->sh->index; + *out_pkt = rp; + return true; + } + } + // A discontinuity reopen failed and the retry above hasn't succeeded // yet; report EOF until the stream settles and the reopen goes through. if (!p->slave) @@ -487,7 +557,8 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt struct sh_stream *sh = slave_index < p->slave_to_outer_count ? p->slave_to_outer[slave_index] : NULL; - if (!sh || !demux_stream_is_selected(sh)) { + bool dvd_sub = sh && p->is_dvd && sh->type == STREAM_SUB; + if (!sh || (!demux_stream_is_selected(sh) && !dvd_sub)) { talloc_free(pkt); return true; } @@ -543,6 +614,23 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt MP_TRACE(demuxer, "mapped pkt: type=%d pts=%f dts=%f\n", sh->type, pkt->pts, pkt->dts); + if (dvd_sub) { + int idx = sh->demuxer_id - 0x20; + if (idx >= 0 && idx < MAX_DVD_SPU_STREAMS) { + struct dvd_sub_hold *h = &p->dvd_sub_hold[idx]; + talloc_free(h->pkt); + h->pkt = demux_copy_packet(demuxer->packet_pool, pkt); + h->sh = sh; + h->pending = false; + } + MP_DBG(demuxer, "dvd sub 0x%x packet: pts=%f selected=%d (held)\n", + sh->demuxer_id, pkt->pts, demux_stream_is_selected(sh)); + if (!demux_stream_is_selected(sh)) { + talloc_free(pkt); + return true; + } + } + *out_pkt = pkt; return 1; } @@ -665,6 +753,7 @@ static int d_open(demuxer_t *demuxer, enum demux_check check) static void d_close(demuxer_t *demuxer) { struct priv *p = demuxer->priv; + clear_dvd_sub_holds(p); demux_free(p->slave); } @@ -676,4 +765,5 @@ const demuxer_desc_t demuxer_desc_disc = { .close = d_close, .seek = d_seek, .switched_tracks = reselect_streams, + .nav_refresh = d_nav_refresh, }; diff --git a/player/discnav.c b/player/discnav.c index 66548bd3f0..afe9cec91a 100644 --- a/player/discnav.c +++ b/player/discnav.c @@ -56,11 +56,10 @@ struct disc_nav_state { uint32_t last_discontinuity_id; bool discontinuity_seen; - // DVD-only: when a menu opens with no DVD sub track selected, we - // transiently select one so the menu graphic renders through the normal - // sd_lavc path. menu_selected_track remembers what we selected so we - // can deselect it again when the menu closes. + // DVD-only: dvd_subtitle track we force-selected for the menu, and the + // selection it displaced (restored on menu close). struct track *menu_selected_track; + struct track *menu_saved_track; // Last disc-driven audio/sub/angle we acted on. int last_audio_id; @@ -76,6 +75,13 @@ struct disc_nav_state { bool menu_active_seen; }; +static bool is_dvd_sub_track(struct track *t) +{ + return t && t->type == STREAM_SUB && t->stream && t->stream->codec && + t->stream->codec->codec && + strcmp(t->stream->codec->codec, "dvd_subtitle") == 0; +} + static struct disc_nav_state *get_state(struct MPContext *mpctx) { if (!mpctx->disc_nav) @@ -146,12 +152,13 @@ bool disc_nav_mouse_pos_to_src(struct MPContext *mpctx, int src_w, int src_h, return true; } +// Push the current menu/highlight state to the dvd_subtitle decoders. static void push_dvd_overlay(struct MPContext *mpctx, struct stream_nav_state *nav, bool visible) { struct mp_dvdnav_hli hli = { .show = visible, - .change_id = visible ? nav->change_id : 0, + .change_id = nav->change_id, }; if (visible) { hli.x = nav->hl_x; @@ -162,9 +169,7 @@ static void push_dvd_overlay(struct MPContext *mpctx, } for (int n = 0; n < mpctx->num_tracks; n++) { struct track *t = mpctx->tracks[n]; - if (t->type != STREAM_SUB || !t->d_sub || !t->stream || - !t->stream->codec || - strcmp(t->stream->codec->codec, "dvd_subtitle") != 0) + if (!is_dvd_sub_track(t) || !t->d_sub) continue; sub_control(t->d_sub, SD_CTRL_APPLY_DVDNAV, &hli); } @@ -290,6 +295,8 @@ static void check_async_discontinuity(struct MPContext *mpctx, struct stream *s, st->last_discontinuity_id = nav->discontinuity_id; reset_playback_state(mpctx); demux_flush(mpctx->demuxer); + // Re-queue the retained menu subpicture the flush may have destroyed. + demux_nav_refresh(mpctx->demuxer); } static struct track *find_track_by_demuxer_id(struct MPContext *mpctx, @@ -393,38 +400,45 @@ static void ensure_menu_sub_selection(struct MPContext *mpctx, bool menu_on) struct track *cur = mpctx->current_track[0][STREAM_SUB]; // If the track list got rebuilt under us (e.g. another file/disc loaded - // mid-session) the cached pointer could be stale. Trust only what we + // mid-session) the cached pointers could be stale. Trust only what we // can still see in mpctx->tracks. - if (st->menu_selected_track) { + for (int i = 0; i < 2; i++) { + struct track **slot = i ? &st->menu_saved_track : &st->menu_selected_track; + if (!*slot) + continue; bool still_present = false; for (int n = 0; n < mpctx->num_tracks; n++) { - if (mpctx->tracks[n] == st->menu_selected_track) { + if (mpctx->tracks[n] == *slot) { still_present = true; break; } } if (!still_present) - st->menu_selected_track = NULL; + *slot = NULL; } if (menu_on) { - if (cur || st->menu_selected_track) + // Re-checked every frame; other selectors (stream auto-select, slave + // reopens) can change the sub under us. + if (st->menu_selected_track && cur == st->menu_selected_track) + return; + // A dvd_subtitle track is already active; nothing to force. + if (is_dvd_sub_track(cur)) return; if (mpctx->opts->stream_id[0][STREAM_SUB] == -2) return; struct track *pick = NULL; for (int n = 0; n < mpctx->num_tracks; n++) { - struct track *t = mpctx->tracks[n]; - if (t->type == STREAM_SUB && t->stream && t->stream->codec && - t->stream->codec->codec && - strcmp(t->stream->codec->codec, "dvd_subtitle") == 0) - { - pick = t; + if (is_dvd_sub_track(mpctx->tracks[n])) { + pick = mpctx->tracks[n]; break; } } if (!pick) return; + // Remember the displaced selection only on the first override. + if (!st->menu_selected_track) + st->menu_saved_track = cur; mp_switch_track_n(mpctx, 0, STREAM_SUB, pick, 0); st->menu_selected_track = pick; } else { @@ -432,8 +446,9 @@ static void ensure_menu_sub_selection(struct MPContext *mpctx, bool menu_on) return; // Only revert if our override is still the active selection. if (cur == st->menu_selected_track) - mp_switch_track_n(mpctx, 0, STREAM_SUB, NULL, 0); + mp_switch_track_n(mpctx, 0, STREAM_SUB, st->menu_saved_track, 0); st->menu_selected_track = NULL; + st->menu_saved_track = NULL; } } @@ -459,6 +474,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->menu_saved_track = NULL; st->overlay_change_seen = false; if (st->menu_active_seen && st->last_menu_active) { st->last_menu_active = false; diff --git a/player/playloop.c b/player/playloop.c index 0e74b09619..fe790f3f69 100644 --- a/player/playloop.c +++ b/player/playloop.c @@ -246,6 +246,9 @@ void reset_playback_state(struct MPContext *mpctx) reset_audio_state(mpctx); reset_subtitle_state(mpctx); + if (mpctx->demuxer) + demux_nav_refresh(mpctx->demuxer); + for (int n = 0; n < mpctx->num_tracks; n++) { struct track *t = mpctx->tracks[n]; // (Often, but not always, this is redundant and also done elsewhere.)