From 1c2f08cf127ec2894a247f369e07535574a9f9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 25 May 2026 04:55:39 +0200 Subject: [PATCH 01/44] demux_lavf: parent mp_codec_params members to it --- demux/demux.c | 2 +- demux/demux_lavf.c | 23 ++++++++++++++--------- demux/dovi_split.c | 2 +- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/demux/demux.c b/demux/demux.c index b63038b6c4c41..673d38a8c1aa6 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -3072,7 +3072,7 @@ static void demux_update_replaygain(demuxer_t *demuxer) if (!rg) rg = decode_rgain(demuxer->log, demuxer->metadata); if (rg) - sh->codec->replaygain_data = talloc_steal(in, rg); + sh->codec->replaygain_data = talloc_steal(sh->codec, rg); } } } diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index a5d7431068284..90cec4fbf0da0 100644 --- a/demux/demux_lavf.c +++ b/demux/demux_lavf.c @@ -67,6 +67,11 @@ // libavformat (almost) always reads data in blocks of this size. #define BIO_BUFFER_SIZE 32768 +static void avcodec_par_destructor(void *p) +{ + avcodec_parameters_free(p); +} + #define OPT_BASE_STRUCT struct demux_lavf_opts struct demux_lavf_opts { int64_t probesize; @@ -641,7 +646,7 @@ static void export_replaygain(demuxer_t *demuxer, struct sh_stream *sh, if (!track_data_available && !album_data_available) return; - struct replaygain_data *rgain = talloc_ptrtype(demuxer, rgain); + struct replaygain_data *rgain = talloc_ptrtype(sh->codec, rgain); rgain->track_gain = rgain->album_gain = 0; rgain->track_peak = rgain->album_peak = 1; @@ -719,7 +724,7 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh->codec->samplerate = codec->sample_rate; sh->codec->bitrate = codec->bit_rate; - sh->codec->format_name = talloc_strdup(sh, av_get_sample_fmt_name(codec->format)); + sh->codec->format_name = talloc_strdup(sh->codec, av_get_sample_fmt_name(codec->format)); double delay = 0; if (codec->sample_rate > 0) @@ -759,7 +764,7 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh->codec->disp_w = codec->width; sh->codec->disp_h = codec->height; sh->codec->bitrate = codec->bit_rate; - sh->codec->format_name = talloc_strdup(sh, av_get_pix_fmt_name(codec->format)); + sh->codec->format_name = talloc_strdup(sh->codec, av_get_pix_fmt_name(codec->format)); if (st->avg_frame_rate.num) sh->codec->fps = av_q2d(st->avg_frame_rate); if (is_image(st, sh->attached_picture, priv->avif)) { @@ -796,7 +801,7 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh = demux_alloc_sh_stream(STREAM_SUB); if (codec->extradata_size) { - sh->codec->extradata = talloc_size(sh, codec->extradata_size); + sh->codec->extradata = talloc_size(sh->codec, codec->extradata_size); memcpy(sh->codec->extradata, codec->extradata, codec->extradata_size); sh->codec->extradata_size = codec->extradata_size; } @@ -838,9 +843,11 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh->ff_index = st->index; mp_codec_info_from_avcodecpar(codec, sh->codec); sh->codec->codec_tag = codec->codec_tag; - sh->codec->lav_codecpar = avcodec_parameters_alloc(); - if (sh->codec->lav_codecpar) - avcodec_parameters_copy(sh->codec->lav_codecpar, codec); + AVCodecParameters **lavp = talloc_ptrtype(sh->codec, lavp); + talloc_set_destructor(lavp, avcodec_par_destructor); + *lavp = avcodec_parameters_alloc(); + if (*lavp && avcodec_parameters_copy(*lavp, codec) >= 0) + sh->codec->lav_codecpar = *lavp; sh->codec->native_tb_num = st->time_base.num; sh->codec->native_tb_den = st->time_base.den; sh->codec->duration = st->duration * av_q2d(st->time_base); @@ -1918,8 +1925,6 @@ static void demux_close_lavf(demuxer_t *demuxer) av_freep(&priv->pb); for (int n = 0; n < priv->num_streams; n++) { struct stream_info *info = priv->streams[n]; - if (info->sh) - avcodec_parameters_free(&info->sh->codec->lav_codecpar); TA_FREEP(&info->dovi_split); } TA_FREEP(&priv->pending_pkt); diff --git a/demux/dovi_split.c b/demux/dovi_split.c index 2476fde7101b1..cfe27185da703 100644 --- a/demux/dovi_split.c +++ b/demux/dovi_split.c @@ -99,7 +99,7 @@ struct mp_dovi_split *mp_dovi_split_create(struct demuxer *demuxer, el->codec->disp_w = par_out->width; el->codec->disp_h = par_out->height; if (par_out->extradata_size > 0) { - el->codec->extradata = talloc_memdup(el, par_out->extradata, + el->codec->extradata = talloc_memdup(el->codec, par_out->extradata, par_out->extradata_size); el->codec->extradata_size = par_out->extradata_size; } From 9ac4cd019d558cf1d31a3f16070f33414f5c8d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 10 Jul 2026 03:52:11 +0200 Subject: [PATCH 02/44] stream_dvdnav: remove unused mousex/mousey --- stream/stream_dvdnav.c | 1 - 1 file changed, 1 deletion(-) diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index ad688f109b924..2246867961477 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -58,7 +58,6 @@ struct priv { dvdnav_t *dvdnav; // handle to libdvdnav stuff char *filename; // path unsigned int duration; // in milliseconds - int mousex, mousey; int title; uint32_t spu_clut[16]; bool spu_clut_valid; From bf70f7f82902e73462e8706ac454a981b2eb696f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 5 Jul 2026 16:25:04 +0200 Subject: [PATCH 03/44] sub/osd: change osd_set_external2 into generic osd_set_bitmaps --- player/command.c | 4 ++-- sub/osd.c | 4 ++-- sub/osd.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/player/command.c b/player/command.c index a5b5e7e6f5a82..1c205570d28ca 100644 --- a/player/command.c +++ b/player/command.c @@ -5363,7 +5363,7 @@ static void recreate_overlays(struct MPContext *mpctx) new->num_parts = 0; } - osd_set_external2(mpctx->osd, new); + osd_set_bitmaps(mpctx->osd, OSDTYPE_EXTERNAL2, new); mp_wakeup_core(mpctx); cmd->overlay_osd_current = overlay_next; } @@ -5501,7 +5501,7 @@ static void overlay_uninit(struct MPContext *mpctx) return; for (int id = 0; id < cmd->num_overlays; id++) replace_overlay(mpctx, id, &(struct overlay){0}); - osd_set_external2(mpctx->osd, NULL); + osd_set_bitmaps(mpctx->osd, OSDTYPE_EXTERNAL2, NULL); for (int n = 0; n < 2; n++) mp_image_unrefp(&cmd->overlay_osd[n].packed); } diff --git a/sub/osd.c b/sub/osd.c index ef646b6775f42..f0509d87bc76d 100644 --- a/sub/osd.c +++ b/sub/osd.c @@ -274,10 +274,10 @@ void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s) mp_mutex_unlock(&osd->lock); } -void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs) +void osd_set_bitmaps(struct osd_state *osd, int type, struct sub_bitmaps *imgs) { mp_mutex_lock(&osd->lock); - struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL2]; + struct osd_object *obj = osd->objs[type]; talloc_free(obj->external2); obj->external2 = sub_bitmaps_copy(NULL, imgs); obj->vo_change_id += 1; diff --git a/sub/osd.h b/sub/osd.h index 44cf2edc1a3db..836d751c58d31 100644 --- a/sub/osd.h +++ b/sub/osd.h @@ -208,7 +208,7 @@ struct osd_progbar_state { }; void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s); -void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs); +void osd_set_bitmaps(struct osd_state *osd, int type, struct sub_bitmaps *imgs); enum mp_osd_draw_flags { OSD_DRAW_SUB_FILTER = (1 << 0), From a43f090f38b21d79084e52c85d2ea794efda0cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 02:31:51 +0200 Subject: [PATCH 04/44] stream_dvdnav: read VTS event payload from the right buffer DVDNAV_VTS_CHANGE casts the just-written event payload to dvdnav_vts_change_event_t. The cast was reading from s->buffer (the stream's ring buffer, untouched by this read), not buf (the demuxer's destination that dvdnav_get_next_block actually wrote into). While here, drop the message to MP_VERBOSE and fix its wording. "switched to title" is misleading because this is a VTS-change event. not a title change. --- stream/stream_dvdnav.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index 2246867961477..06ed4923295b3 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -321,9 +321,9 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) case DVDNAV_VTS_CHANGE: { int tit = 0, part = 0; dvdnav_vts_change_event_t *vts_event = - (dvdnav_vts_change_event_t *)s->buffer; - MP_INFO(s, "DVDNAV, switched to title: %d\n", - vts_event->new_vtsN); + (dvdnav_vts_change_event_t *)buf; + MP_VERBOSE(s, "DVDNAV, switched to VTS: %d\n", + vts_event->new_vtsN); if (!priv->had_initial_vts) { // dvdnav sends an initial VTS change before any data; don't // cause a blocking wait for the player, because the player in From 27bb6726431e04a39c1fc3093f6160a056865b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 25 May 2026 12:02:03 +0200 Subject: [PATCH 05/44] sd_lavc: add support for DVD sub highlight This is connected to standard DVD subs renderer, with additional state passed for highlight color. --- sub/dec_sub.h | 8 ++++++++ sub/sd_lavc.c | 56 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/sub/dec_sub.h b/sub/dec_sub.h index d8f153968533e..b236aa9da1941 100644 --- a/sub/dec_sub.h +++ b/sub/dec_sub.h @@ -21,6 +21,14 @@ enum sd_ctrl { SD_CTRL_SET_VIDEO_DEF_FPS, SD_CTRL_RESET_SOFT, SD_CTRL_UPDATE_OPTS, + SD_CTRL_APPLY_DVDNAV, // struct mp_dvdnav_hli * +}; + +struct mp_dvdnav_hli { + bool show; + int x, y, w, h; // button rect in SPU/source coords + uint32_t palette[4]; // 0xAARRGGBB, straight alpha + uint32_t change_id; // bumped on any visible change }; enum sd_text_type { diff --git a/sub/sd_lavc.c b/sub/sd_lavc.c index 5cbc6d0a21257..eec2f1ef2c3ae 100644 --- a/sub/sd_lavc.c +++ b/sub/sd_lavc.c @@ -71,6 +71,10 @@ struct sd_lavc_priv { struct seekpoint *seekpoints; int num_seekpoints; struct bitmap_packer *packer; + + // DVD-nav per-pixel highlight overlay state. + struct mp_dvdnav_hli hli; + uint32_t hli_change_id; }; static int init(struct sd *sd) @@ -294,6 +298,23 @@ static void read_sub_bitmaps(struct sd *sd, struct sub *sub) memcpy(pal, data[1], r->nb_colors * 4); convert_pal(pal, 256, opts->sub_gray); + // DVD navigation highlight + bool hli_active = priv->hli.show && + priv->hli.w > 0 && priv->hli.h > 0 && + r->x < priv->hli.x + priv->hli.w && + r->y < priv->hli.y + priv->hli.h && + r->x + r->w > priv->hli.x && + r->y + r->h > priv->hli.y; + uint32_t hli_pal[4] = {0}; + if (hli_active) { + memcpy(hli_pal, priv->hli.palette, sizeof(hli_pal)); + convert_pal(hli_pal, 4, opts->sub_gray); + } + int hli_x0 = priv->hli.x - r->x; + int hli_y0 = priv->hli.y - r->y; + int hli_x1 = hli_x0 + priv->hli.w; + int hli_y1 = hli_y0 + priv->hli.h; + for (int y = -padding; y < b->h + padding; y++) { uint32_t *out = (uint32_t*)((char*)b->bitmap + y * b->stride); int start = 0; @@ -301,8 +322,14 @@ static void read_sub_bitmaps(struct sd *sd, struct sub *sub) out[x] = 0; if (y >= 0 && y < b->h) { uint8_t *in = data[0] + y * linesize[0]; - for (int x = 0; x < b->w; x++) - *out++ = pal[*in++]; + bool y_in_hli = hli_active && y >= hli_y0 && y < hli_y1; + for (int x = 0; x < b->w; x++) { + uint8_t pv = *in++; + if (y_in_hli && x >= hli_x0 && x < hli_x1 && pv < 4) + *out++ = hli_pal[pv]; + else + *out++ = pal[pv]; + } start = b->w; } for (int x = start; x < b->w + padding; x++) @@ -322,6 +349,21 @@ static void read_sub_bitmaps(struct sd *sd, struct sub *sub) } } +static void rerender_queued_subs(struct sd *sd) +{ + struct sd_lavc_priv *priv = sd->priv; + for (int n = 0; n < MAX_QUEUE; n++) { + struct sub *sub = &priv->subs[n]; + if (!sub->valid) + continue; + sub->count = 0; + sub->src_w = 0; + sub->src_h = 0; + sub->id = priv->new_id++; + read_sub_bitmaps(sd, sub); + } +} + static void decode(struct sd *sd, struct demux_packet *packet) { struct mp_subtitle_opts *opts = sd->opts; @@ -715,6 +757,16 @@ static int control(struct sd *sd, enum sd_ctrl cmd, void *arg) case SD_CTRL_SET_VIDEO_PARAMS: priv->video_params = *(struct mp_image_params *)arg; return CONTROL_OK; + case SD_CTRL_APPLY_DVDNAV: { + struct mp_dvdnav_hli *hli = arg; + if (priv->hli_change_id == hli->change_id) + return CONTROL_OK; + priv->hli = *hli; + priv->hli_change_id = hli->change_id; + // Re-render any decoded subtitles, after style update. + rerender_queued_subs(sd); + return CONTROL_OK; + } default: return CONTROL_UNKNOWN; } From 89cc134eb6b82b4c1a617ba8c3617e2e2d586862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 25 May 2026 12:12:42 +0200 Subject: [PATCH 06/44] demux_disc: drop "PTS discontinuity" logs to verbose They are expected in normal operation of libdvdread, where changing titles, causes natural jump in PTS. --- demux/demux_disc.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demux/demux_disc.c b/demux/demux_disc.c index 86557a711c2da..2bbc3a2a83562 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -275,7 +275,7 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt p->last_dts = pkt->dts; if (fabs(p->last_dts - pkt->dts) >= DTS_RESET_THRESHOLD) { - MP_WARN(demuxer, "PTS discontinuity: %f->%f\n", p->last_dts, pkt->dts); + MP_VERBOSE(demuxer, "PTS discontinuity: %f->%f\n", p->last_dts, pkt->dts); p->base_time += p->last_dts - p->base_dts; p->base_dts = pkt->dts; if (pkt->duration > 0) From 4f5024db5ffd4e4194f89986f6f1132da6725918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 01:11:41 +0200 Subject: [PATCH 07/44] demux: allow to update duration, chapters and editions during playback --- demux/demux.c | 35 +++++++++++++++++++++++++++++++++++ demux/demux.h | 3 +++ 2 files changed, 38 insertions(+) diff --git a/demux/demux.c b/demux/demux.c index 673d38a8c1aa6..5890fabaac5d8 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -3178,6 +3178,41 @@ void demux_metadata_changed(demuxer_t *demuxer) mp_mutex_unlock(&in->lock); } +// Updates the duration should it need to be changed. Used for demuxers that +// changes titles/playlists at runtime. +void demux_set_duration(demuxer_t *demuxer, double duration) +{ + mp_assert(demuxer == demuxer->in->d_thread); + struct demux_internal *in = demuxer->in; + + mp_mutex_lock(&in->lock); + in->duration = duration; + in->d_thread->duration = duration; + // Clear the high-water mark so subsequent packets can re-ratchet duration + // upward from the new playlist's PTS base without being shadowed by the + // previous title's value. + in->highest_av_pts = MP_NOPTS_VALUE; + in->events |= DEMUX_EVENT_DURATION; + mp_mutex_unlock(&in->lock); +} + +// Updates the chapters/editions should it need to be changed. Used for demuxers +// that changes titles/playlists at runtime. +void demux_lists_changed(demuxer_t *demuxer) +{ + mp_assert(demuxer == demuxer->in->d_thread); + struct demux_internal *in = demuxer->in; + + mp_mutex_lock(&in->lock); + in->d_user->chapters = in->d_thread->chapters; + in->d_user->num_chapters = in->d_thread->num_chapters; + in->d_user->editions = in->d_thread->editions; + in->d_user->num_editions = in->d_thread->num_editions; + in->d_user->edition = in->d_thread->edition; + in->events |= DEMUX_EVENT_LISTS; + mp_mutex_unlock(&in->lock); +} + // Called locked, with user demuxer. static void update_final_metadata(demuxer_t *demuxer, struct timed_metadata *tm) { diff --git a/demux/demux.h b/demux/demux.h index 6fabf64b9940e..093424c63ce1e 100644 --- a/demux/demux.h +++ b/demux/demux.h @@ -121,6 +121,7 @@ enum demux_event { DEMUX_EVENT_STREAMS = 1 << 1, // a stream was added DEMUX_EVENT_METADATA = 1 << 2, // metadata or stream_metadata changed DEMUX_EVENT_DURATION = 1 << 3, // duration updated + DEMUX_EVENT_LISTS = 1 << 4, // chapters / editions list changed DEMUX_EVENT_ALL = 0xFFFF, }; @@ -348,6 +349,8 @@ void demux_stream_tags_changed(struct demuxer *demuxer, struct sh_stream *sh, void demux_close_stream(struct demuxer *demuxer); void demux_metadata_changed(demuxer_t *demuxer); +void demux_set_duration(demuxer_t *demuxer, double duration); +void demux_lists_changed(demuxer_t *demuxer); void demux_update(demuxer_t *demuxer, double playback_pts); bool demux_cache_dump_set(struct demuxer *demuxer, double start, double end, From 63cb25b860c3866878bcf9b58047c8085af2d9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 01:54:13 +0200 Subject: [PATCH 08/44] stream: add disc-nav actions and states Define three new stream controls so disc backends (libdvdnav, libbluray) can expose their in-disc menu/highlight state to the player. STREAM_CTRL_NAV_CMD - send a navigation action STREAM_CTRL_GET_NAV_STATE - get current menu state STREAM_CTRL_GET_NAV_OVERLAY - get BGRA overlay that should be drawn (BD) Not used yet, will get use in next commits. --- stream/stream.h | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/stream/stream.h b/stream/stream.h index 621fd5aaf70c4..66017e85f510f 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -93,6 +93,52 @@ enum stream_ctrl { STREAM_CTRL_GET_LANG, STREAM_CTRL_GET_CURRENT_TITLE, STREAM_CTRL_SET_CURRENT_TITLE, + STREAM_CTRL_NAV_CMD, // struct stream_nav_cmd* + STREAM_CTRL_GET_NAV_STATE, // struct stream_nav_state* + STREAM_CTRL_GET_NAV_OVERLAY, // struct stream_nav_overlay_req* +}; + +// Fetch a BGRA snapshot of the disc-menu overlay (used for Blu-ray HDMV). +struct stream_nav_overlay_req { + int w, h; // input: caller's plane dimensions; output: actual + int stride; // input: dest row stride in bytes + uint8_t *dst; // input: dest buffer (caller-allocated, BGRA) + uint32_t change_id; // output: latched change_id of the snapshot +}; + +// In-disc navigation (DVD/BD menu) actions, used with STREAM_CTRL_NAV_CMD. +enum stream_nav_action { + STREAM_NAV_UP, + STREAM_NAV_DOWN, + STREAM_NAV_LEFT, + STREAM_NAV_RIGHT, + STREAM_NAV_SELECT, // activate currently highlighted button + STREAM_NAV_MENU_ROOT, // jump to the disc's root/title menu + STREAM_NAV_MENU_TITLE, // jump to the current title's menu + STREAM_NAV_MENU_POPUP, // BD popup menu (no-op for DVD) + STREAM_NAV_PREV_MENU, // return to previous menu / leave still + STREAM_NAV_MOUSE_MOVE, // mouse moved; .x,.y are video-space coords + STREAM_NAV_MOUSE_CLICK, // mouse button activated at .x,.y +}; + +struct stream_nav_cmd { + enum stream_nav_action action; + int x, y; // for MOUSE_* +}; + +// Snapshot of the stream's menu state. +struct stream_nav_state { + bool menu_active; // a selectable menu/highlight is currently visible + bool has_popup; // disc supports a popup menu (BD only) + 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; + // BGRA highlight palette (0xAARRGGBB, straight) to substitute for the + // four SPU pixel values inside the highlight rect. Zeroed when no highlight + // is active. + uint32_t hl_palette[4]; + uint32_t change_id; // Bumped whenever any of the above changes + uint32_t discontinuity_id; // Bumped when the stream's source position jumps }; struct stream_lang_req { From 4e146bb41c7a358ad01dabdca68dabd5a35da1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 25 May 2026 12:21:03 +0200 Subject: [PATCH 09/44] options: add `disc-menu` option --- DOCS/interface-changes/disc-menu.txt | 1 + DOCS/man/options.rst | 6 ++++++ options/options.c | 1 + options/options.h | 1 + 4 files changed, 9 insertions(+) create mode 100644 DOCS/interface-changes/disc-menu.txt diff --git a/DOCS/interface-changes/disc-menu.txt b/DOCS/interface-changes/disc-menu.txt new file mode 100644 index 0000000000000..3f8f558c5f29c --- /dev/null +++ b/DOCS/interface-changes/disc-menu.txt @@ -0,0 +1 @@ +and `--disc-menu` option diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 0febce1bf86da..43213263ea5df 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -131,6 +131,12 @@ Track Selection flat list. Note that depending on the file, tracks from different programs may be completely unrelated to each other. +``--disc-menu=`` + When set, opening ``dvd://`` or ``bd://`` boots into the disc's interactive + menu instead of automatically playing the longest title (default: ``no``). + The menu can also be reached at any time via the synthetic "Disc Menu" + entry in the editions/titles list, or with ``discnav menu`` command. + ``--show-dependent-tracks=`` Show dependent tracks in the track list (default: no). Dependent tracks carry coded data that is not independently decodable. For example, the diff --git a/options/options.c b/options/options.c index bab7e324ca635..34cf8d3b6dff4 100644 --- a/options/options.c +++ b/options/options.c @@ -590,6 +590,7 @@ static const m_option_t mp_opts[] = { #endif {"edition", OPT_CHOICE(edition_id, {"auto", -1}), M_RANGE(0, 8190)}, {"flatten-editions", OPT_BOOL(flatten_editions)}, + {"disc-menu", OPT_BOOL(disc_menu)}, {"show-dependent-tracks", OPT_BOOL(show_dependent_tracks)}, #if HAVE_LIBBLURAY {"bluray", OPT_SUBSTRUCT(stream_bluray_opts, stream_bluray_conf)}, diff --git a/options/options.h b/options/options.h index d7b9542e27e25..8c85a2bf765ce 100644 --- a/options/options.h +++ b/options/options.h @@ -255,6 +255,7 @@ typedef struct MPOpts { int hls_bitrate; int edition_id; bool flatten_editions; + bool disc_menu; bool show_dependent_tracks; bool initial_audio_sync; double sync_max_video_change; From dc5943030c6948c37bff10878a60ce963cfabd26 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 10/44] stream_dvdnav: implement DVD menu navigation Drive libdvdnav's menu state through the new disc-nav protocol so the player can show / interact with VMGM and VTSM menus: --- demux/demux_lavf.c | 12 +- stream/stream_dvdnav.c | 329 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 313 insertions(+), 28 deletions(-) diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index 90cec4fbf0da0..9fb475475efd5 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 @@ -1646,8 +1653,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 06ed4923295b3..2bb799a328936 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -44,16 +44,22 @@ #include "options/options.h" #include "common/msg.h" #include "input/input.h" +#include "misc/thread_tools.h" #include "options/m_config.h" #include "options/path.h" #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 +// Default source dimensions if dvdnav_get_video_resolution() fails. +#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 +74,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 +223,212 @@ 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; // spec: "no color" - button is invisible + 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; + + // Auto-action buttons: a btnit entry can request immediate activation + // when its button gets focus (auto_action_mode == 1). + // Fire it once per menu entry / button transition. + 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 +529,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 +566,11 @@ 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"); } + // Resolution can change across VTS (PAL vs. NTSC titles); refresh + // so mouse coordinate translation stays correct. + refresh_video_resolution(priv); + // VTS change is a title-set boundary, flush. + priv->discontinuity_id++; break; } case DVDNAV_CELL_CHANGE: { @@ -350,6 +584,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 +678,20 @@ 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; + dvdnav_get_number_of_titles(priv->dvdnav, &num_titles); + // 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 +777,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 +866,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 +908,10 @@ 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; + // Menu mode: don't pre-select any title; let dvdnav start with the + // disc's first-play / VMGM menu and drive everything via NAV events. + 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 +937,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 +988,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)); From bfded306fcadc8dc42dc519e4fc8175bd046f266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 02:08:10 +0200 Subject: [PATCH 11/44] stream_bluray: implement HDMV menu navigation Add HDMV menu support to stream_bluray. Activated by opening with "bd://menu/..." or by the global --disc-menu option Lots of plumbing the libbluary event loop. Menus are read though different function even. STREAM_CTRL_NAV_CMD maps arrow/select/menu/popup/mouse onto bd_user_input and bd_mouse_select. SELECT and MOUSE_CLICK queue an HDMV VM command (often PlayPlaylist) that won't execute until the next demuxer read, so we bump discontinuity_id pre-emptively; the player's post-action check then sees the change and flushes cached audio/video that would otherwise keep playing for seconds while the new playlist is already arriving. --- stream/stream_bluray.c | 440 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 429 insertions(+), 11 deletions(-) diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index b28f452675537..9d79a16e97ba5 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -41,9 +41,11 @@ #include "mpv_talloc.h" #include "common/common.h" #include "common/msg.h" +#include "misc/thread_tools.h" #include "options/m_config.h" #include "options/options.h" #include "options/path.h" +#include "osdep/threads.h" #include "stream.h" #include "osdep/io.h" #include "osdep/timer.h" @@ -96,14 +98,215 @@ struct bluray_priv_s { int current_title; int current_playlist; + // Cached map from filtered title index (0..num_titles-1) to mpls_id. + uint32_t *title_to_playlist; + int cfg_title; int cfg_playlist; char *cfg_device; struct mp_bluray_opts *opts; struct m_config_cache *opts_cache; + + // HDMV menu support (enabled when cfg_title == BLURAY_MENU_TITLE). + // libbluray delivers compressed YUV+RLE overlay primitives via + // bd_register_overlay_proc for the HDMV graphics controller. We decompress + // into ig_plane and snapshot to ig_publish on every FLUSH so the player + // thread can read a coherent image under overlay_lock. + // + // libbluray docs seems to suggest we should use bd_register_argb_overlay_proc + // if we can handle ARGB planes directly, but in practice that callback is + // only used for BD-J menus, and HDMV menus goes through the RLE callback. + // We support only the later, currently. + bool hdmv_mode; + uint32_t *ig_plane; // working BGRA plane, written from callback + uint32_t *ig_publish; // last FLUSHed snapshot, read by player + int plane_w, plane_h; // current allocation size (0 if unallocated) + mp_mutex overlay_lock; // guards ig_publish + visibility flags + + bool overlay_ig_visible; // IG plane was FLUSHED with non-empty content + bool menu_event_active; // BD_EVENT_MENU == 1 + bool popup_supported; // BD_EVENT_POPUP == 1 + uint32_t nav_change_id; // bumped on FLUSH/HIDE/MENU/POPUP events + uint32_t discontinuity_id; // bumped on actions that may hop (SELECT...) + bool data_delivered; // any byte returned from fill_buffer yet + + int mouse_x, mouse_y; }; +// Lazy (re-)allocation for the IG-plane working/publish buffers. +static bool bd_ensure_plane(struct bluray_priv_s *priv, int w, int h) +{ + if (w <= 0 || h <= 0) + return false; + if (priv->ig_plane && w <= priv->plane_w && h <= priv->plane_h) + return true; + int nw = MPMAX(w, priv->plane_w); + int nh = MPMAX(h, priv->plane_h); + size_t bytes = (size_t)nw * nh * 4; + uint32_t *plane = talloc_realloc(priv, priv->ig_plane, uint32_t, nw * nh); + uint32_t *publish = talloc_realloc(priv, priv->ig_publish, uint32_t, nw * nh); + if (!plane || !publish) + return false; + memset(plane, 0, bytes); + memset(publish, 0, bytes); + priv->ig_plane = plane; + priv->ig_publish = publish; + priv->plane_w = nw; + priv->plane_h = nh; + return true; +} + +static void bd_palette_to_bgra(const BD_PG_PALETTE_ENTRY *pg, uint32_t out[256]) +{ + for (int i = 0; i < 256; i++) { + int Y = pg[i].Y, Cb = pg[i].Cb, Cr = pg[i].Cr, T = pg[i].T; + // BT.709 limited->full. + int y_ = (Y - 16) * 1192; // 1.164 << 10 + int cr = Cr - 128; + int cb = Cb - 128; + int r = (y_ + 1836 * cr + 512) >> 10; // 1.793 + int g = (y_ - 547 * cr - 218 * cb + 512) >> 10; // 0.534 / 0.213 + int b = (y_ + 2166 * cb + 512) >> 10; // 2.115 + r = MPCLAMP(r, 0, 255); + g = MPCLAMP(g, 0, 255); + b = MPCLAMP(b, 0, 255); + // Pre-multiply RGB by alpha so the OSD layer can composite directly. + r = r * T / 255; + g = g * T / 255; + b = b * T / 255; + out[i] = ((uint32_t)T << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + // palette index 0xFF is always transparent. + out[0xFF] = 0; +} + +// Composite one RLE-encoded sub-bitmap into the IG plane at (ov->x, ov->y). +static void bd_overlay_draw_rle(struct bluray_priv_s *priv, const BD_OVERLAY *ov) +{ + if (!ov->img || !ov->palette || ov->w <= 0 || ov->h <= 0) + return; + uint32_t pal[256]; + bd_palette_to_bgra(ov->palette, pal); + + const BD_PG_RLE_ELEM *rle = ov->img; + for (int y = 0; y < ov->h; y++) { + int dst_y = ov->y + y; + bool in_plane = dst_y >= 0 && dst_y < priv->plane_h; + uint32_t *dst_row = in_plane + ? priv->ig_plane + (size_t)dst_y * priv->plane_w : NULL; + int x = 0; + while (x < ov->w) { + int len = rle->len; + int color = rle->color; + rle++; + if (len == 0) + continue; // stray EOL, skip + int dst_x = ov->x + x; + int run = len; + if (dst_x < 0) { + int skip = MPMIN(-dst_x, run); + run -= skip; + dst_x += skip; + } + if (dst_x + run > priv->plane_w) + run = priv->plane_w - dst_x; + if (dst_row && run > 0) { + uint32_t c = pal[color & 0xFF]; + for (int i = 0; i < run; i++) + dst_row[dst_x + i] = c; + } + x += len; + } + if (rle->len == 0) + rle++; + } +} + +// Snapshot the working plane to ig_publish and recompute visibility. +// Must be called with priv->overlay_lock held. +static void bd_publish_overlay_flush(struct bluray_priv_s *priv) +{ + if (!priv->ig_plane) + return; + bool any = false; + size_t n = (size_t)priv->plane_w * priv->plane_h; + memcpy(priv->ig_publish, priv->ig_plane, n * 4); + for (size_t i = 0; i < n; i++) { + if (priv->ig_publish[i] & 0xFF000000) { + any = true; + break; + } + } + priv->overlay_ig_visible = any; + priv->nav_change_id++; +} + +// Called by libbluray's HDMV graphics controller for every overlay primitive +// on either plane. We only render the IG (menu) plane; PG (subtitles) flows +// through the regular demuxer/sd_lavc pipeline. +static void bd_yuv_overlay_cb(void *handle, const struct bd_overlay_s *ov) +{ + struct bluray_priv_s *priv = handle; + if (!ov) + return; + if (ov->plane != BD_OVERLAY_IG) + return; + + mp_mutex_lock(&priv->overlay_lock); + switch (ov->cmd) { + case BD_OVERLAY_INIT: + bd_ensure_plane(priv, ov->w, ov->h); + if (priv->ig_plane) { + memset(priv->ig_plane, 0, + (size_t)priv->plane_w * priv->plane_h * 4); + } + priv->overlay_ig_visible = false; + priv->nav_change_id++; + break; + case BD_OVERLAY_CLOSE: + priv->overlay_ig_visible = false; + priv->nav_change_id++; + break; + case BD_OVERLAY_CLEAR: + if (priv->ig_plane) { + memset(priv->ig_plane, 0, + (size_t)priv->plane_w * priv->plane_h * 4); + } + break; + case BD_OVERLAY_WIPE: + if (priv->ig_plane) { + for (int y = 0; y < ov->h; y++) { + int dy = ov->y + y; + if (dy < 0 || dy >= priv->plane_h) + continue; + int dx = MPMAX(ov->x, 0); + int run = MPMIN(ov->w, priv->plane_w - dx); + if (run > 0) { + memset(priv->ig_plane + (size_t)dy * priv->plane_w + dx, + 0, run * 4); + } + } + } + break; + case BD_OVERLAY_DRAW: + if (priv->ig_plane) + bd_overlay_draw_rle(priv, ov); + break; + case BD_OVERLAY_HIDE: + priv->overlay_ig_visible = false; + priv->nav_change_id++; + break; + case BD_OVERLAY_FLUSH: + bd_publish_overlay_flush(priv); + break; + default: + break; + } + mp_mutex_unlock(&priv->overlay_lock); +} + inline static int play_playlist(struct bluray_priv_s *priv, int playlist) { return bd_select_playlist(priv->bd, playlist); @@ -122,17 +325,33 @@ static void bluray_stream_close(stream_t *s) if (priv->title_info) bd_free_title_info(priv->title_info); - if (priv->bd) + if (priv->bd) { + if (priv->hdmv_mode) + bd_register_overlay_proc(priv->bd, NULL, NULL); bd_close(priv->bd); + } + if (priv->hdmv_mode) + mp_mutex_destroy(&priv->overlay_lock); } static void handle_event(stream_t *s, const BD_EVENT *ev) { struct bluray_priv_s *b = s->priv; + if (b->hdmv_mode) + MP_VERBOSE(s, "bdnav: event %d param %u\n", ev->event, ev->param); switch (ev->event) { case BD_EVENT_MENU: + // ev->param: 1 if the disc is currently in an HDMV menu, 0 otherwise. + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->menu_event_active = ev->param != 0; + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; case BD_EVENT_STILL: + if (ev->param) + bd_read_skip_still(b->bd); break; case BD_EVENT_STILL_TIME: bd_read_skip_still(b->bd); @@ -142,20 +361,35 @@ static void handle_event(stream_t *s, const BD_EVENT *ev) case BD_EVENT_PLAYLIST: b->current_playlist = ev->param; b->current_title = bd_get_current_title(b->bd); + if (b->title_to_playlist) { + for (int i = 0; i < b->num_titles; i++) { + if (b->title_to_playlist[i] == (uint32_t)ev->param) { + b->current_title = i; + break; + } + } + } if (b->title_info) bd_free_title_info(b->title_info); b->title_info = bd_get_playlist_info(b->bd, b->current_playlist, b->current_angle); + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; case BD_EVENT_TITLE: - if (ev->param == BLURAY_TITLE_FIRST_PLAY) { - b->current_title = bd_get_current_title(b->bd); - } else - b->current_title = ev->param; + b->current_title = bd_get_current_title(b->bd); if (b->title_info) { bd_free_title_info(b->title_info); b->title_info = NULL; } + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; case BD_EVENT_ANGLE: b->current_angle = ev->param; @@ -166,6 +400,13 @@ static void handle_event(stream_t *s, const BD_EVENT *ev) } break; case BD_EVENT_POPUP: + // ev->param: 1 if popup menu is currently available, 0 otherwise. + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->popup_supported = ev->param != 0; + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; #if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 5, 0) case BD_EVENT_DISCONTINUITY: @@ -181,6 +422,51 @@ static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len) { struct bluray_priv_s *b = s->priv; BD_EVENT event; + + if (b->hdmv_mode) { + // bd_read() doesn't drive the HDMV VM, so the disc's first-play + // bytecode would never run and we'd be stuck with "no valid title" + // forever. bd_read_ext() runs the VM between event drains and also + // delivers one event per call, which we hand off to handle_event. + while (bd_get_event(b->bd, &event)) + handle_event(s, &event); + int total = 0; + int events_seen = 0; + // Loop briefly to absorb event-only returns (where bd_read_ext + // returns 0 with a freshly produced event) before reporting EOF. + // If an event bumps discontinuity_id (PLAYLIST/TITLE) *after* we + // have already delivered data to the slave demuxer, stop here even + // if no data was read: the next bd_read_ext would deliver data from + // the new playlist, but the slave must be reopened first so it + // parses with the correct codec context. + for (int i = 0; i < 200; i++) { + uint32_t disc_before = b->discontinuity_id; + int n = bd_read_ext(b->bd, (uint8_t *)buf + total, len - total, &event); + if (n < 0) { + MP_VERBOSE(s, "bdnav: bd_read_ext err iter=%d\n", i); + return -1; + } + if (event.event != BD_EVENT_NONE) { + handle_event(s, &event); + events_seen++; + } + if (n > 0) { + total += n; + break; + } + if (b->data_delivered && b->discontinuity_id != disc_before) + break; + if (mp_cancel_test(s->cancel)) + return 0; + mp_sleep_ns(MP_TIME_MS_TO_NS(5)); + } + if (total > 0) + b->data_delivered = true; + if (total == 0) + MP_VERBOSE(s, "bdnav: fill returned 0 (events=%d)\n", events_seen); + return total; + } + while (bd_get_event(b->bd, &event)) handle_event(s, &event); return bd_read(b->bd, buf, len); @@ -213,6 +499,13 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) } case STREAM_CTRL_SET_CURRENT_TITLE: { const uint32_t title = *((unsigned int*)arg); + // demux_disc appends a synthetic "Disc Menu" edition at index num_titles. + if (title == b->num_titles) { + if (!b->hdmv_mode) + return STREAM_UNSUPPORTED; + bd_menu_call(b->bd, -1); + return STREAM_OK; + } if (title >= b->num_titles || !play_title(b, title)) return STREAM_UNSUPPORTED; b->current_title = title; @@ -321,6 +614,103 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) *(char**)arg = talloc_strdup(NULL, meta->di_name); return STREAM_OK; } + case STREAM_CTRL_NAV_CMD: { + if (!b->hdmv_mode) + return STREAM_UNSUPPORTED; + struct stream_nav_cmd *nav = arg; + uint32_t key = BD_VK_NONE; + switch (nav->action) { + case STREAM_NAV_UP: + key = BD_VK_UP; + break; + case STREAM_NAV_DOWN: + key = BD_VK_DOWN; + break; + case STREAM_NAV_LEFT: + key = BD_VK_LEFT; + break; + case STREAM_NAV_RIGHT: + key = BD_VK_RIGHT; + break; + case STREAM_NAV_SELECT: + key = BD_VK_ENTER; + break; + case STREAM_NAV_MENU_ROOT: + case STREAM_NAV_MENU_TITLE: + // BD doesn't distinguish "title menu", both map to disc root. + bd_menu_call(b->bd, -1); + return STREAM_OK; + case STREAM_NAV_MENU_POPUP: + key = BD_VK_POPUP; + break; + case STREAM_NAV_PREV_MENU: + // No dedicated "previous menu" key; popup-toggle is the closest + // equivalent and behaves like "dismiss current menu" on most + // discs when already in popup. + key = BD_VK_POPUP; + break; + case STREAM_NAV_MOUSE_MOVE: + b->mouse_x = nav->x; + b->mouse_y = nav->y; + bd_mouse_select(b->bd, -1, nav->x, nav->y); + return STREAM_OK; + case STREAM_NAV_MOUSE_CLICK: + b->mouse_x = nav->x; + b->mouse_y = nav->y; + bd_mouse_select(b->bd, -1, nav->x, nav->y); + key = BD_VK_MOUSE_ACTIVATE; + break; + } + if (key != BD_VK_NONE) + bd_user_input(b->bd, -1, key); + return STREAM_OK; + } + case STREAM_CTRL_GET_NAV_STATE: { + struct stream_nav_state *st = arg; + if (!b->hdmv_mode) { + *st = (struct stream_nav_state){0}; + return STREAM_OK; + } + mp_mutex_lock(&b->overlay_lock); + bool visible = b->menu_event_active && b->overlay_ig_visible; + *st = (struct stream_nav_state){ + .menu_active = visible, + .has_popup = b->popup_supported, + .src_w = b->plane_w, + .src_h = b->plane_h, + .change_id = b->nav_change_id, + .discontinuity_id = b->discontinuity_id, + }; + mp_mutex_unlock(&b->overlay_lock); + return STREAM_OK; + } + case STREAM_CTRL_GET_NAV_OVERLAY: { + if (!b->hdmv_mode) + return STREAM_UNSUPPORTED; + struct stream_nav_overlay_req *req = arg; + if (!req->dst || req->w <= 0 || req->h <= 0) + return STREAM_ERROR; + mp_mutex_lock(&b->overlay_lock); + int copy_w = MPMIN(req->w, b->plane_w); + int copy_h = MPMIN(req->h, b->plane_h); + if (b->ig_publish && b->overlay_ig_visible) { + for (int y = 0; y < copy_h; y++) { + memcpy(req->dst + y * req->stride, + b->ig_publish + y * b->plane_w, + copy_w * 4); + } + } else { + // Plane is hidden / pre-init; clear the caller's buffer so a + // stale image doesn't linger after the menu closes. + for (int y = 0; y < copy_h; y++) + memset(req->dst + y * req->stride, 0, copy_w * 4); + } + req->change_id = b->nav_change_id; + req->w = copy_w; + req->h = copy_h; + mp_mutex_unlock(&b->overlay_lock); + return STREAM_OK; + } default: break; } @@ -463,13 +853,17 @@ static int bluray_stream_open_internal(stream_t *s) MP_INFO(s, "List of available titles:\n"); /* parse titles information */ + b->title_to_playlist = talloc_array(b, uint32_t, b->num_titles); for (int i = 0; i < b->num_titles; i++) { + b->title_to_playlist[i] = (uint32_t)-1; /* the information we're accessing (duration, playlist, angle count) * doesn't depend on the angle */ BLURAY_TITLE_INFO *ti = bd_get_title_info(bd, i, 0); if (!ti) continue; + b->title_to_playlist[i] = ti->playlist; + char *time = mp_format_time(ti->duration / 90000, false); MP_INFO(s, "idx: %3d duration: %s angles: %2d (playlist: %05d.mpls)\n", i, time, ti->angle_count, ti->playlist); @@ -485,10 +879,28 @@ static int bluray_stream_open_internal(stream_t *s) // initialize libbluray event queue bd_get_event(bd, NULL); - select_initial_title(s, bd_get_main_title(bd)); + b->hdmv_mode = b->cfg_title == BLURAY_MENU_TITLE; + MP_VERBOSE(s, "bdnav: cfg_title=%d hdmv_mode=%d\n", b->cfg_title, b->hdmv_mode); + if (b->hdmv_mode) { + mp_mutex_init(&b->overlay_lock); + bd_register_overlay_proc(bd, b, bd_yuv_overlay_cb); + if (!bd_play(bd)) { + MP_ERR(s, "Couldn't start Blu-ray HDMV playback.\n"); + ret = STREAM_UNSUPPORTED; + goto err; + } + b->current_title = bd_get_current_title(bd); + MP_VERBOSE(s, "bdnav: HDMV entered; current title=%d\n", + b->current_title); + } else { + select_initial_title(s, bd_get_main_title(bd)); + } - if (!bd_select_angle(bd, b->opts->angle - 1)) - MP_WARN(s, "Couldn't select angle '%d'.\n", b->opts->angle - 1); + // Angle selection is only valid once a playlist has been picked. + if (!b->hdmv_mode) { + if (!bd_select_angle(bd, b->opts->angle - 1)) + MP_WARN(s, "Couldn't select angle '%d'.\n", b->opts->angle - 1); + } b->current_angle = bd_get_current_angle(bd); @@ -519,12 +931,15 @@ static int bluray_stream_open(stream_t *s) struct MPOpts *opts = mp_get_config_group(s, s->global, &mp_opt_root); int edition_id = opts->edition_id; + bool disc_menu = opts->disc_menu; talloc_free(opts); if (edition_id >= 0) { b->cfg_title = edition_id; - } else if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) { - b->cfg_title = BLURAY_DEFAULT_TITLE; + } else if (title.len == 0 || bstr_equals0(title, "longest") || + bstr_equals0(title, "first")) + { + b->cfg_title = disc_menu ? BLURAY_MENU_TITLE : BLURAY_DEFAULT_TITLE; } else if (bstr_equals0(title, "menu")) { b->cfg_title = BLURAY_MENU_TITLE; } else if (bstr_equals0(title, "mpls")) { @@ -609,8 +1024,11 @@ static int bdmv_dir_stream_open(stream_t *stream) struct bluray_priv_s *priv = talloc_ptrtype(stream, priv); stream->priv = priv; struct MPOpts *opts = mp_get_config_group(NULL, stream->global, &mp_opt_root); + int default_title = opts->edition_id >= 0 ? opts->edition_id + : opts->disc_menu ? BLURAY_MENU_TITLE + : BLURAY_DEFAULT_TITLE; *priv = (struct bluray_priv_s){ - .cfg_title = opts->edition_id >= 0 ? opts->edition_id : BLURAY_DEFAULT_TITLE, + .cfg_title = default_title, }; talloc_free(opts); From 6957d4fc8384f1f54d2747a36d33a38a1dcd3f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 02:10:55 +0200 Subject: [PATCH 12/44] demux_disc: reopen slave demuxer on disc-nav hops Playing a disc through the menu means crossing playlist boundaries at runtime: menu -> title (and back), sub-menu -> sub-menu, title -> title on a "Play next" button. Each new playlist can have a different stream count and/or a different codec mix (a MPEG-2 menu into an H.264 main feature is common on BD-Video). The previous slave demuxer was probed once at open and never refreshed, so after a hop the player either kept decoding stale streams or saw nothing at all. Tear down and re-open the slave on every nav discontinuity. add_stream_editions() now appends a synthetic "Disc Menu" entry at index num_titles. --- demux/demux_disc.c | 344 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 269 insertions(+), 75 deletions(-) diff --git a/demux/demux_disc.c b/demux/demux_disc.c index 2bbc3a2a83562..0805abdfc6297 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -29,37 +29,54 @@ #include "video/csputils.h" +// DVD-Video has 32 subpicture (SPU) streams, mapped to PES substream IDs 0x20..0x3F. +#define MAX_DVD_SPU_STREAMS 32 + +// If the timestamp difference between subsequent packets is this big, assume +// a reset. It should be big enough to account for 1. low video framerates and +// large audio frames, and 2. bad interleaving. +#define DTS_RESET_THRESHOLD 5.0 + struct priv { struct demuxer *slave; - // streams[slave_stream_index] == our_stream - struct sh_stream **streams; - int num_streams; - // This contains each DVD sub stream, or NULL. Needed because DVD packets - // can come arbitrarily late in the MPEG stream, so the slave demuxer - // might add the streams only later. - struct sh_stream *dvd_subs[32]; + + // All outer sh_streams we have ever surfaced to the parent demuxer. + struct sh_stream **outer_streams; + int num_outer_streams; + + // Maps the current slave's stream index to its matching outer sh_stream. + struct sh_stream **slave_to_outer; + int slave_to_outer_count; + + // Per slave-stream-index flag: when set, the next packet from that slave + // stream is tagged segmented + pkt->codec=outer->codec so the decoder + // wrapper reinitialises for the freshly-refreshed codec. + bool *needs_segment_marker; + int needs_segment_marker_count; + + // DVD-only: pre-registered sub streams keyed by PES substream ID minus + // 0x20, carrying the disc-level CLUT as extradata. + struct sh_stream *dvd_subs[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 double last_dts; // DTS of previously demuxed packet bool seek_reinit; // needs reinit after seek + uint32_t last_discontinuity_id; // Last source-position-jump id seen from the stream. bool is_dvd, is_cdda; }; -// If the timestamp difference between subsequent packets is this big, assume -// a reset. It should be big enough to account for 1. low video framerates and -// large audio frames, and 2. bad interleaving. -#define DTS_RESET_THRESHOLD 5.0 - static void reselect_streams(demuxer_t *demuxer) { struct priv *p = demuxer->priv; int num_slave = demux_get_num_stream(p->slave); - for (int n = 0; n < MPMIN(num_slave, p->num_streams); n++) { - if (p->streams[n]) { + for (int n = 0; n < num_slave && n < p->slave_to_outer_count; n++) { + struct sh_stream *outer = p->slave_to_outer[n]; + if (outer) { demuxer_select_track(p->slave, demux_get_stream(p->slave, n), - MP_NOPTS_VALUE, demux_stream_is_selected(p->streams[n])); + MP_NOPTS_VALUE, demux_stream_is_selected(outer)); } } } @@ -82,15 +99,13 @@ static void add_dvd_streams(demuxer_t *demuxer) return; struct stream_dvd_info_req info; if (stream_control(stream, STREAM_CTRL_GET_DVD_INFO, &info) > 0) { - for (int n = 0; n < MPMIN(32, info.num_subs); n++) { + for (int n = 0; n < MPMIN(MAX_DVD_SPU_STREAMS, info.num_subs); n++) { struct sh_stream *sh = demux_alloc_sh_stream(STREAM_SUB); sh->demuxer_id = n + 0x20; sh->codec->codec = "dvd_subtitle"; get_disc_lang(stream, sh, true); - // p->streams _must_ match with p->slave->streams, so we can't add - // it yet - it has to be done when the real stream appears, which - // could be right on start, or any time later. p->dvd_subs[n] = sh; + MP_TARRAY_APPEND(p, p->outer_streams, p->num_outer_streams, sh); // emulate the extradata struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS; @@ -120,71 +135,130 @@ static void add_dvd_streams(demuxer_t *demuxer) } } -static void add_streams(demuxer_t *demuxer) +// Take ownership of a slave sh_stream's codec params into the outer demuxer +// so it survives a slave reopen. +static void adopt_codec_params(struct sh_stream *outer, struct sh_stream *src) +{ + if (outer->codec != src->codec) { + if (!outer->ds) + talloc_free(outer->codec); + outer->codec = src->codec; + talloc_steal(outer, outer->codec); + } + outer->codec->first_packet = NULL; + outer->codec->decoder = NULL; + outer->codec->decoder_desc = NULL; +} + +static struct sh_stream *find_outer_for_slave(struct priv *p, + struct sh_stream *src) +{ + if (src->type == STREAM_SUB && src->demuxer_id >= 0x20 && + src->demuxer_id <= 0x3F) + { + struct sh_stream *sub = p->dvd_subs[src->demuxer_id - 0x20]; + if (sub) + return sub; + } + for (int i = 0; i < p->num_outer_streams; i++) { + struct sh_stream *sh = p->outer_streams[i]; + if (sh && sh->type == src->type && sh->demuxer_id == src->demuxer_id) + return sh; + } + return NULL; +} + +// Build / rebuild the slave-index -> outer-sh map. For each slave stream reuse +// or register a fresh outer sh_stream as follows and expose it to the parent demuxer. +static void sync_streams(struct demuxer *demuxer) { struct priv *p = demuxer->priv; - int old_num = p->num_streams; + int num_slave = demux_get_num_stream(p->slave); - for (int n = old_num; n < demux_get_num_stream(p->slave); n++) { + if (num_slave > p->slave_to_outer_count) { + MP_TARRAY_GROW(p, p->slave_to_outer, num_slave - 1); + MP_TARRAY_GROW(p, p->needs_segment_marker, num_slave - 1); + for (int n = p->slave_to_outer_count; n < num_slave; n++) { + p->slave_to_outer[n] = NULL; + p->needs_segment_marker[n] = false; + } + p->slave_to_outer_count = num_slave; + p->needs_segment_marker_count = num_slave; + } + + for (int n = 0; n < num_slave; n++) { struct sh_stream *src = demux_get_stream(p->slave, n); - if (src->type == STREAM_SUB) { - struct sh_stream *sub = NULL; - if (src->demuxer_id >= 0x20 && src->demuxer_id <= 0x3F) - sub = p->dvd_subs[src->demuxer_id - 0x20]; - if (sub) { - mp_assert(p->num_streams == n); // directly mapped - MP_TARRAY_APPEND(p, p->streams, p->num_streams, sub); - continue; + struct sh_stream *outer = find_outer_for_slave(p, src); + + if (!outer) { + outer = demux_alloc_sh_stream(src->type); + adopt_codec_params(outer, src); + outer->demuxer_id = src->demuxer_id; + outer->dependent_track = src->dependent_track; + if (src->type == STREAM_VIDEO) { + double ar; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_ASPECT_RATIO, &ar) + == STREAM_OK) + { + struct mp_image_params f = {.w = src->codec->disp_w, + .h = src->codec->disp_h}; + mp_image_params_set_dsize(&f, 1728 * ar, 1728); + outer->codec->par_w = f.p_w; + outer->codec->par_h = f.p_h; + } } - } - struct sh_stream *sh = demux_alloc_sh_stream(src->type); - mp_assert(p->num_streams == n); // directly mapped - MP_TARRAY_APPEND(p, p->streams, p->num_streams, sh); - // Copy all stream fields that might be relevant - *sh->codec = *src->codec; - sh->demuxer_id = src->demuxer_id; - sh->dependent_track = src->dependent_track; - if (src->type == STREAM_VIDEO) { - double ar; - if (stream_control(demuxer->stream, STREAM_CTRL_GET_ASPECT_RATIO, &ar) - == STREAM_OK) - { - struct mp_image_params f = {.w = src->codec->disp_w, - .h = src->codec->disp_h}; - mp_image_params_set_dsize(&f, 1728 * ar, 1728); - sh->codec->par_w = f.p_w; - sh->codec->par_h = f.p_h; + get_disc_lang(demuxer->stream, outer, p->is_dvd); + MP_TARRAY_APPEND(p, p->outer_streams, p->num_outer_streams, outer); + demux_add_sh_stream(demuxer, outer); + } else if (outer->type != STREAM_SUB && outer->codec && src->codec) { + // Codec change on a reused outer, mostly useful for BD menus, which + // may be MPEG-2 while the video track is H.264. + const char *new_codec = src->codec->codec; + const char *cur_codec = outer->codec->codec; + if (new_codec && cur_codec && strcmp(new_codec, cur_codec) != 0) { + MP_VERBOSE(demuxer, "stream %d codec changed: %s -> %s\n", + n, cur_codec, new_codec); + adopt_codec_params(outer, src); + p->needs_segment_marker[n] = true; } } - get_disc_lang(demuxer->stream, sh, p->is_dvd); - demux_add_sh_stream(demuxer, sh); + + p->slave_to_outer[n] = outer; + } + + // Propagate outer selection state to the slave. + for (int n = 0; n < num_slave; n++) { + struct sh_stream *outer = p->slave_to_outer[n]; + if (outer) { + demuxer_select_track(p->slave, demux_get_stream(p->slave, n), + MP_NOPTS_VALUE, demux_stream_is_selected(outer)); + } } - // Mirror slave sh_stream_group onto the disc-level sh_streams. This is needed + // Mirror slave sh_stream_group onto the outer sh_streams. This is needed // for the Dolby Vision BL+EL group, it's detected well by lavf. We could use // the libbluray `dv_streams[]` info, but it's not available yet in release // version, and mapping it through lavf is less code. - for (int n = old_num; n < p->num_streams; n++) { - struct sh_stream *disc_sh = p->streams[n]; - if (!disc_sh || disc_sh->group) + for (int n = 0; n < num_slave; n++) { + struct sh_stream *outer = p->slave_to_outer[n]; + if (!outer || outer->group) continue; struct sh_stream *src = demux_get_stream(p->slave, n); if (!src || !src->group) continue; - struct sh_stream_group *grp = talloc_zero(disc_sh, struct sh_stream_group); + struct sh_stream_group *grp = talloc_zero(outer, struct sh_stream_group); for (int m = 0; m < src->group->num_members; m++) { - struct sh_stream *sh = src->group->members[m]; - if (!sh || sh->index < 0 || sh->index >= p->num_streams) + struct sh_stream *member = src->group->members[m]; + if (!member || member->index < 0 || + member->index >= p->slave_to_outer_count) continue; - struct sh_stream *disc_member = p->streams[sh->index]; - if (!disc_member) + struct sh_stream *outer_member = p->slave_to_outer[member->index]; + if (!outer_member) continue; - MP_TARRAY_APPEND(grp, grp->members, grp->num_members, disc_member); - disc_member->group = grp; + MP_TARRAY_APPEND(grp, grp->members, grp->num_members, outer_member); + outer_member->group = grp; } } - - reselect_streams(demuxer); } static void d_seek(demuxer_t *demuxer, double seek_pts, int flags) @@ -228,31 +302,144 @@ static void reset_pts(demuxer_t *demuxer) p->seek_reinit = false; } +static void add_stream_chapters(struct demuxer *demuxer); + +// Sync demuxer->edition with the disc's current playback position. The disc +// nav state takes precedence: if a menu is active, point at the synthetic +// "Disc Menu" entry add_stream_editions() appended at num_editions - 1; +// otherwise mirror the stream's GET_CURRENT_TITLE. +static void sync_initial_edition(struct demuxer *demuxer) +{ + unsigned title; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_CURRENT_TITLE, &title) >= 1) + demuxer->edition = title; + struct stream_nav_state nav = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1 + && nav.menu_active && demuxer->num_editions > 0) + { + demuxer->edition = demuxer->num_editions - 1; + } +} + +static bool reopen_slave(struct demuxer *demuxer) +{ + struct priv *p = demuxer->priv; + + struct demuxer_params params = { + .force_format = "+lavf", + .external_stream = demuxer->stream, + .stream_flags = demuxer->stream_origin, + .depth = demuxer->depth + 1, + }; + if (p->is_cdda) + params.force_format = "+rawaudio"; + + demux_free(p->slave); + // Discard anything the stream wrapper buffered before the disc-nav + // discontinuity. + stream_drop_buffers(demuxer->stream); + p->slave = demux_open_url("-", ¶ms, demuxer->cancel, demuxer->global); + if (!p->slave) { + MP_ERR(demuxer, "Failed to reopen slave demuxer after discontinuity\n"); + return false; + } + + for (int n = 0; n < p->slave_to_outer_count; n++) { + p->slave_to_outer[n] = NULL; + p->needs_segment_marker[n] = false; + } + + sync_streams(demuxer); + + // Refresh duration / chapters / edition for the new playlist. + double len; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1) + demux_set_duration(demuxer, len); + else + demux_set_duration(demuxer, -1); + + for (int n = 0; n < demuxer->num_chapters; n++) + talloc_free(demuxer->chapters[n].metadata); + demuxer->num_chapters = 0; + add_stream_chapters(demuxer); + + sync_initial_edition(demuxer); + + demux_lists_changed(demuxer); + + return true; +} + static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt) { struct priv *p = demuxer->priv; + struct stream_nav_state nav = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1 && + nav.discontinuity_id != p->last_discontinuity_id) + { + MP_VERBOSE(demuxer, "discontinuity %u->%u, reopening slave\n", + p->last_discontinuity_id, nav.discontinuity_id); + if (!reopen_slave(demuxer)) + return false; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) + p->last_discontinuity_id = nav.discontinuity_id; + p->seek_reinit = true; + } + struct demux_packet *pkt = demux_read_any_packet(p->slave); - if (!pkt) - return false; + if (!pkt) { + // The slave can hit EOF mid-playback when the stream layer breaks + // its read at a disc-driven discontinuity. + struct stream_nav_state nav2 = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav2) >= 1 + && nav2.discontinuity_id != p->last_discontinuity_id) + { + MP_VERBOSE(demuxer, "discontinuity %u->%u at EOF, reopening slave\n", + p->last_discontinuity_id, nav2.discontinuity_id); + if (!reopen_slave(demuxer)) + return false; + p->last_discontinuity_id = nav2.discontinuity_id; + p->seek_reinit = true; + pkt = demux_read_any_packet(p->slave); + } + if (!pkt) + return false; + } demux_update(p->slave, MP_NOPTS_VALUE); if (p->seek_reinit) reset_pts(demuxer); - add_streams(demuxer); - if (pkt->stream >= p->num_streams) { // out of memory? - talloc_free(pkt); - return true; + int slave_index = pkt->stream; + if (demux_get_num_stream(p->slave) > p->slave_to_outer_count || + slave_index >= p->slave_to_outer_count || + !p->slave_to_outer[slave_index]) + { + sync_streams(demuxer); } - struct sh_stream *sh = p->streams[pkt->stream]; - if (!demux_stream_is_selected(sh)) { + 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)) { talloc_free(pkt); return true; } + // First packet from a slave stream whose matched outer just had its + // codec refreshed gets tagged as a new segment so f_decoder_wrapper + // drains and reinits the decoder. + if (slave_index < p->needs_segment_marker_count && + p->needs_segment_marker[slave_index]) + { + p->needs_segment_marker[slave_index] = false; + pkt->segmented = true; + pkt->codec = sh->codec; + pkt->start = MP_NOPTS_VALUE; + pkt->end = MP_NOPTS_VALUE; + } + pkt->stream = sh->index; if (p->is_cdda) { @@ -323,6 +510,15 @@ static void add_stream_editions(struct demuxer *demuxer) mp_tprintf(42, "title: %u (%s)", title + 1, time)); talloc_free(time); } + + // Append a synthetic "Disc Menu" entry. + struct demux_edition menu = { + .demuxer_id = titles, + .default_edition = false, + .metadata = talloc_zero(demuxer, struct mp_tags), + }; + MP_TARRAY_APPEND(demuxer, demuxer->editions, demuxer->num_editions, menu); + mp_tags_set_str(menu.metadata, "TITLE", "Disc Menu"); } static void add_stream_chapters(struct demuxer *demuxer) @@ -384,7 +580,7 @@ static int d_open(demuxer_t *demuxer, enum demux_check check) demuxer->seekable = true; add_dvd_streams(demuxer); - add_streams(demuxer); + sync_streams(demuxer); add_stream_chapters(demuxer); add_stream_editions(demuxer); @@ -392,9 +588,7 @@ static int d_open(demuxer_t *demuxer, enum demux_check check) if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1) demuxer->duration = len; - unsigned title; - if (stream_control(demuxer->stream, STREAM_CTRL_GET_CURRENT_TITLE, &title) >= 1) - demuxer->edition = title; + sync_initial_edition(demuxer); return 0; } From 658bde7be608089b0c31a8bc30193aaf15afe47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 5 Jul 2026 16:33:17 +0200 Subject: [PATCH 13/44] sub/osd: add OSDTYPE_DISC_MENU --- sub/osd.c | 7 +++++-- sub/osd.h | 2 +- sub/osd_state.h | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sub/osd.c b/sub/osd.c index f0509d87bc76d..1aaa06e887802 100644 --- a/sub/osd.c +++ b/sub/osd.c @@ -193,6 +193,7 @@ void osd_free(struct osd_state *osd) return; osd_destroy_backend(osd); talloc_free(osd->objs[OSDTYPE_EXTERNAL2]->external2); + talloc_free(osd->objs[OSDTYPE_DISC_MENU]->external2); mp_mutex_destroy(&osd->lock); talloc_free(osd); } @@ -306,7 +307,8 @@ static void check_obj_resize(struct osd_state *osd, struct mp_osd_res res, void osd_resize(struct osd_state *osd, struct mp_osd_res res) { mp_mutex_lock(&osd->lock); - int types[] = {OSDTYPE_OSD, OSDTYPE_EXTERNAL, OSDTYPE_EXTERNAL2, -1}; + int types[] = {OSDTYPE_OSD, OSDTYPE_EXTERNAL, OSDTYPE_EXTERNAL2, + OSDTYPE_DISC_MENU, -1}; for (int n = 0; types[n] >= 0; n++) check_obj_resize(osd, res, osd->objs[types[n]]); mp_mutex_unlock(&osd->lock); @@ -331,7 +333,8 @@ static struct sub_bitmaps *render_object(struct osd_state *osd, } else if (obj->type == OSDTYPE_SUB2) { if (obj->sub && sub_is_secondary_visible(obj->sub)) res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts); - } else if (obj->type == OSDTYPE_EXTERNAL2) { + } else if (obj->type == OSDTYPE_EXTERNAL2 || + obj->type == OSDTYPE_DISC_MENU) { if (obj->external2 && obj->external2->format) { res = sub_bitmaps_copy(NULL, obj->external2); // need to be owner obj->external2->change_id = 0; diff --git a/sub/osd.h b/sub/osd.h index 836d751c58d31..d69387220ec69 100644 --- a/sub/osd.h +++ b/sub/osd.h @@ -113,7 +113,7 @@ struct mp_osd_res { bool osd_res_equals(struct mp_osd_res a, struct mp_osd_res b); // 0 <= sub_bitmaps.render_index < MAX_OSD_PARTS -#define MAX_OSD_PARTS 5 +#define MAX_OSD_PARTS 6 // Start of OSD symbols in osd_font.pfb #define OSD_CODEPOINTS 0xE000 diff --git a/sub/osd_state.h b/sub/osd_state.h index cd4ca15a04e3d..1e19b0d0eafe6 100644 --- a/sub/osd_state.h +++ b/sub/osd_state.h @@ -7,6 +7,8 @@ #include "osdep/threads.h" enum mp_osdtype { + OSDTYPE_DISC_MENU, + OSDTYPE_SUB, OSDTYPE_SUB2, // IDs must be numerically successive @@ -44,7 +46,7 @@ struct osd_object { struct osd_external **externals; int num_externals; - // OSDTYPE_EXTERNAL2 + // OSDTYPE_EXTERNAL2 / OSDTYPE_DISC_MENU struct sub_bitmaps *external2; // VO cache state From 73e346c7123363dc4d95cb2c56ecd0c49c1fb85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 02:12:05 +0200 Subject: [PATCH 14/44] player: add disc-menu state holder and overlay support New player/discnav.c owns the state for the in-disc menu UI. Used to display menu and control the DVD / BD playback. This is common part between stream_bluray.c and stream_dvdnav.c. --- meson.build | 1 + player/core.h | 10 ++ player/discnav.c | 354 ++++++++++++++++++++++++++++++++++++++++++++++ player/main.c | 1 + player/playloop.c | 2 + 5 files changed, 368 insertions(+) create mode 100644 player/discnav.c diff --git a/meson.build b/meson.build index 80bd39d7ec3bf..40254deb1a743 100644 --- a/meson.build +++ b/meson.build @@ -170,6 +170,7 @@ sources = files( 'player/client.c', 'player/command.c', 'player/configfiles.c', + 'player/discnav.c', 'player/external_files.c', 'player/loadfile.c', 'player/main.c', diff --git a/player/core.h b/player/core.h index ec6784411c969..83b4cd1b327a8 100644 --- a/player/core.h +++ b/player/core.h @@ -446,6 +446,7 @@ typedef struct MPContext { struct screenshot_ctx *screenshot_ctx; struct command_ctx *command_ctx; + struct disc_nav_state *disc_nav; struct encode_lavc_context *encode_lavc_ctx; struct mp_option_callback *option_callbacks; @@ -671,6 +672,15 @@ void uninit_sub_all(struct MPContext *mpctx); void update_osd_msg(struct MPContext *mpctx); bool update_subtitles(struct MPContext *mpctx, double video_pts); +// discnav.c +struct stream; +struct stream_nav_state; +void disc_nav_update(struct MPContext *mpctx); +void disc_nav_destroy(struct MPContext *mpctx); +struct stream *disc_nav_get_stream(struct MPContext *mpctx); +bool disc_nav_mouse_pos_to_src(struct MPContext *mpctx, int src_w, int src_h, + int *out_x, int *out_y); + // video.c void reset_video_state(struct MPContext *mpctx); int init_video_decoder(struct MPContext *mpctx, struct track *track); diff --git a/player/discnav.c b/player/discnav.c new file mode 100644 index 0000000000000..7a3cee8aaa857 --- /dev/null +++ b/player/discnav.c @@ -0,0 +1,354 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include +#include + +#include "mpv_talloc.h" + +#include "common/common.h" +#include "common/msg.h" +#include "input/input.h" +#include "player/command.h" + +#include "stream/stream.h" +#include "demux/demux.h" +#include "sub/dec_sub.h" +#include "sub/osd.h" +#include "sub/osd_state.h" +#include "video/mp_image.h" +#include "video/out/vo.h" + +#include "core.h" + +struct disc_nav_state { + // True while the disc-menu input section is enabled (a menu is on screen). + bool overlay_visible; + + // Blu-ray HDMV menu staging. libbluray hands us a pre-composited BGRA IG + // plane via STREAM_CTRL_GET_NAV_OVERLAY; we copy it into bd_image and + // forward through osd_set_bitmaps (OSDTYPE_DISC_MENU). + struct mp_image *bd_image; + uint32_t bd_last_change_id; + struct mp_osd_res bd_last_vo_res; + + // Last observed disc-nav discontinuity counter (bumped by the stream + // backend on user nav actions and on libbluray/libdvdnav-internal + // playlist/title/cell transitions). + 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. + struct track *menu_selected_track; +}; + +static struct disc_nav_state *get_state(struct MPContext *mpctx) +{ + if (!mpctx->disc_nav) + mpctx->disc_nav = talloc_zero(mpctx, struct disc_nav_state); + return mpctx->disc_nav; +} + +void disc_nav_destroy(struct MPContext *mpctx) +{ + if (!mpctx->disc_nav) + return; + mp_image_unrefp(&mpctx->disc_nav->bd_image); + TA_FREEP(&mpctx->disc_nav); +} + +struct stream *disc_nav_get_stream(struct MPContext *mpctx) +{ + if (!mpctx->demuxer || !mpctx->demuxer->stream) + return NULL; + struct stream *s = mpctx->demuxer->stream; + if (!s->info || !s->info->name) + return NULL; + const char *n = s->info->name; + if (strcmp(n, "dvdnav") == 0 || strcmp(n, "ifo_dvdnav") == 0 || + strcmp(n, "bd") == 0 || strcmp(n, "bdmv/bluray") == 0) + { + return s; + } + return NULL; +} + +bool disc_nav_mouse_pos_to_src(struct MPContext *mpctx, int src_w, int src_h, + int *out_x, int *out_y) +{ + struct vo *vo = mpctx->video_out; + if (!vo || !vo->config_ok || src_w <= 0 || src_h <= 0) + return false; + int wx, wy, hover; + mp_input_get_mouse_pos(mpctx->input, &wx, &wy, &hover); + struct mp_rect src, dst; + struct mp_osd_res osd; // mandatory out param; ignored + vo_get_src_dst_rects(vo, &src, &dst, &osd); + int dw = dst.x1 - dst.x0; + int dh = dst.y1 - dst.y0; + if (dw <= 0 || dh <= 0) + return false; + double fx = (wx - dst.x0) / (double)dw; + double fy = (wy - dst.y0) / (double)dh; + if (fx < 0 || fx > 1 || fy < 0 || fy > 1) + return false; + *out_x = (int)(fx * src_w); + *out_y = (int)(fy * src_h); + return true; +} + +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, + }; + if (visible) { + hli.x = nav->hl_x; + hli.y = nav->hl_y; + hli.w = nav->hl_w; + hli.h = nav->hl_h; + memcpy(hli.palette, nav->hl_palette, sizeof(hli.palette)); + } + 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) + continue; + sub_control(t->d_sub, SD_CTRL_APPLY_DVDNAV, &hli); + } +} + +static void push_bd_overlay(struct MPContext *mpctx, struct stream *s, + struct stream_nav_state *nav, bool visible) +{ + struct disc_nav_state *st = get_state(mpctx); + + if (!visible) { + if (st->overlay_visible) + osd_set_bitmaps(mpctx->osd, OSDTYPE_DISC_MENU, NULL); + st->bd_last_change_id = 0; + st->bd_last_vo_res = (struct mp_osd_res){0}; + return; + } + + if (nav->src_w <= 0 || nav->src_h <= 0) + return; + + // Skip the work when nothing the renderer cares about changed. + struct mp_osd_res vo_res = osd_get_vo_res(mpctx->osd); + if (st->overlay_visible && + st->bd_last_change_id == nav->change_id && + osd_res_equals(vo_res, st->bd_last_vo_res)) + return; + + if (!st->bd_image || + st->bd_image->w != nav->src_w || st->bd_image->h != nav->src_h) + { + mp_image_unrefp(&st->bd_image); + st->bd_image = mp_image_alloc(IMGFMT_BGRA, nav->src_w, nav->src_h); + if (!st->bd_image) + return; + talloc_steal(st, st->bd_image); + } + + struct mp_image *img = st->bd_image; + struct stream_nav_overlay_req req = { + .w = img->w, + .h = img->h, + .stride = img->stride[0], + .dst = img->planes[0], + }; + if (stream_control(s, STREAM_CTRL_GET_NAV_OVERLAY, &req) < 1) + return; + + struct sub_bitmap part = { + .bitmap = img->planes[0], + .stride = img->stride[0], + .w = req.w, + .h = req.h, + .dw = req.w, + .dh = req.h, + }; + struct sub_bitmaps imgs = { + .format = SUBBITMAP_BGRA, + .parts = &part, + .num_parts = 1, + .packed = img, + .packed_w = img->w, + .packed_h = img->h, + .change_id = nav->change_id ? (int)nav->change_id : 1, + }; + osd_rescale_bitmaps(&imgs, nav->src_w, nav->src_h, vo_res, 0); + osd_set_bitmaps(mpctx->osd, OSDTYPE_DISC_MENU, &imgs); + st->bd_last_change_id = nav->change_id; + st->bd_last_vo_res = vo_res; +} + +// Sync demuxer->edition with what the disc is actually playing. +static void sync_current_edition(struct MPContext *mpctx, struct stream *s, + struct stream_nav_state *nav) +{ + struct demuxer *demuxer = mpctx->demuxer; + if (!demuxer || demuxer->num_editions <= 0 || !demuxer->desc || + strcmp(demuxer->desc->name, "disc") != 0) + return; + + int desired = demuxer->edition; + if (nav->menu_active) { + desired = demuxer->num_editions - 1; + } else { + unsigned title; + if (stream_control(s, STREAM_CTRL_GET_CURRENT_TITLE, &title) >= 1 && + (int)title < demuxer->num_editions - 1) + { + desired = (int)title; + } + } + if (desired != demuxer->edition) { + MP_VERBOSE(mpctx, "discnav: current-edition %d->%d " + "(menu_active=%d)\n", + demuxer->edition, desired, nav->menu_active); + demuxer->edition = desired; + mp_notify_property(mpctx, "current-edition"); + } +} + +// Catch async playlist/title hops driven by the disc itself (HDMV bytecode, +// dvdnav HOP_CHANNEL, etc.). +static void check_async_discontinuity(struct MPContext *mpctx, + struct stream_nav_state *nav) +{ + struct disc_nav_state *st = get_state(mpctx); + if (!mpctx->demuxer) + return; + if (!st->discontinuity_seen) { + st->last_discontinuity_id = nav->discontinuity_id; + st->discontinuity_seen = true; + return; + } + if (nav->discontinuity_id == st->last_discontinuity_id) + return; + MP_VERBOSE(mpctx, "discnav: async discontinuity %u->%u, flushing\n", + st->last_discontinuity_id, nav->discontinuity_id); + st->last_discontinuity_id = nav->discontinuity_id; + reset_playback_state(mpctx); + demux_flush(mpctx->demuxer); +} + +// Make sure a dvd_subtitle track is selected while a menu is visible, +// so the SPU graphics decoded by sd_lavc reach the OSD. +static void ensure_menu_sub_selection(struct MPContext *mpctx, bool menu_on) +{ + struct disc_nav_state *st = get_state(mpctx); + 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 + // can still see in mpctx->tracks. + if (st->menu_selected_track) { + bool still_present = false; + for (int n = 0; n < mpctx->num_tracks; n++) { + if (mpctx->tracks[n] == st->menu_selected_track) { + still_present = true; + break; + } + } + if (!still_present) + st->menu_selected_track = NULL; + } + + if (menu_on) { + if (cur || st->menu_selected_track) + 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; + break; + } + } + if (!pick) + return; + mp_switch_track_n(mpctx, 0, STREAM_SUB, pick, 0); + st->menu_selected_track = pick; + } else { + if (!st->menu_selected_track) + 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); + st->menu_selected_track = NULL; + } +} + +void disc_nav_update(struct MPContext *mpctx) +{ + struct stream *s = disc_nav_get_stream(mpctx); + struct stream_nav_state nav = {0}; + bool have = s && stream_control(s, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1; + if (!have) { + struct disc_nav_state *st = mpctx->disc_nav; + if (!st) + return; + if (st->overlay_visible) { + osd_set_bitmaps(mpctx->osd, OSDTYPE_DISC_MENU, NULL); + mp_input_disable_section(mpctx->input, "discnav"); + st->overlay_visible = false; + } + st->bd_last_change_id = 0; + st->bd_last_vo_res = (struct mp_osd_res){0}; + st->menu_selected_track = NULL; + return; + } + + struct disc_nav_state *st = get_state(mpctx); + + check_async_discontinuity(mpctx, &nav); + sync_current_edition(mpctx, s, &nav); + + bool is_bd = strcmp(s->info->name, "bd") == 0 || strcmp(s->info->name, "bdmv/bluray") == 0; + bool visible = nav.menu_active && (is_bd || (nav.hl_w > 0 && nav.hl_h > 0)); + + if (is_bd) { + push_bd_overlay(mpctx, s, &nav, visible); + } else { + push_dvd_overlay(mpctx, &nav, visible); + ensure_menu_sub_selection(mpctx, nav.menu_active); + } + + if (visible != st->overlay_visible) { + MP_VERBOSE(mpctx, "discnav: overlay %s\n", visible ? "on" : "off"); + if (visible) { + mp_input_enable_section(mpctx->input, "discnav", MP_INPUT_ON_TOP); + } else { + mp_input_disable_section(mpctx->input, "discnav"); + } + st->overlay_visible = visible; + } +} diff --git a/player/main.c b/player/main.c index 6891c9d5a1d38..0ef3d7f6d6581 100644 --- a/player/main.c +++ b/player/main.c @@ -190,6 +190,7 @@ void mp_destroy(struct MPContext *mpctx) mpctx->encode_lavc_ctx = NULL; command_uninit(mpctx); + disc_nav_destroy(mpctx); mp_clients_destroy(mpctx); diff --git a/player/playloop.c b/player/playloop.c index d899fba1236df..f472e40969f17 100644 --- a/player/playloop.c +++ b/player/playloop.c @@ -1288,6 +1288,8 @@ void run_playloop(struct MPContext *mpctx) handle_update_subtitles(mpctx); + disc_nav_update(mpctx); + handle_each_frame_screenshot(mpctx); handle_eof(mpctx); From 4abcb2da532a4b79001e06ad1f65f1961185da83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 02:14:46 +0200 Subject: [PATCH 15/44] player/loadfile: auto-select default tracks added after initial open play_current_file picks default tracks once, then leaves them alone. That leaves later-surfacing streams without a default selection. --- player/loadfile.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/player/loadfile.c b/player/loadfile.c index b2e57d9f111fb..f2e481b2d085d 100644 --- a/player/loadfile.c +++ b/player/loadfile.c @@ -367,6 +367,23 @@ void update_demuxer_properties(struct MPContext *mpctx) add_demuxer_tracks(mpctx, tracks); print_track_list(mpctx, NULL); tracks->events &= ~DEMUX_EVENT_STREAMS; + + // Streams surfaced after play_current_file's selection phase (e.g. + // disc-nav playlist hop, or lavf identifying a PES stream only once + // its first packet arrives) wouldn't otherwise get auto-selected. + if (mpctx->playback_initialized && mpctx->opts->stream_auto_sel) { + for (int t = 0; t < STREAM_TYPE_COUNT; t++) { + for (int i = 0; i < num_ptracks[t]; i++) { + if (mpctx->current_track[i][t]) + continue; + if (mpctx->opts->stream_id[i][t] == -2) + continue; + struct track *sel = select_default_track(mpctx, i, t); + if (sel) + mp_switch_track_n(mpctx, i, t, sel, 0); + } + } + } } if (events & DEMUX_EVENT_METADATA) { struct mp_tags *info = From b45e8d5798cf1283eb6cfbfda4771b076612d908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 02:15:45 +0200 Subject: [PATCH 16/44] player/loadfile: react to runtime chapter/edition list updates --- player/loadfile.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/player/loadfile.c b/player/loadfile.c index f2e481b2d085d..865bcc364b627 100644 --- a/player/loadfile.c +++ b/player/loadfile.c @@ -427,6 +427,24 @@ void update_demuxer_properties(struct MPContext *mpctx) } if (events & DEMUX_EVENT_DURATION) mp_notify(mpctx, MP_EVENT_DURATION_UPDATE, NULL); + if (events & DEMUX_EVENT_LISTS) { + // Demuxer just published a new chapter / edition list at runtime. + TA_FREEP(&mpctx->chapters); + mpctx->num_chapters = demuxer->num_chapters; + if (demuxer->num_chapters > 0) { + mpctx->chapters = demux_copy_chapter_data(demuxer->chapters, + demuxer->num_chapters); + if (mpctx->opts->rebase_start_time) { + for (int n = 0; n < mpctx->num_chapters; n++) + mpctx->chapters[n].pts -= demuxer->start_time; + } + } + mp_notify(mpctx, MP_EVENT_CHAPTER_CHANGE, NULL); + mp_notify_property(mpctx, "chapter-list"); + mp_notify_property(mpctx, "edition"); + mp_notify_property(mpctx, "current-edition"); + mp_notify_property(mpctx, "editions"); + } demuxer->events = 0; } From 4d2d6d9377af1fe334a52c1043ef093171f850a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 25 May 2026 12:17:16 +0200 Subject: [PATCH 17/44] player/command: add `disc-menu-active` property --- DOCS/interface-changes/disc-menu.txt | 1 + DOCS/man/input.rst | 6 ++++++ player/command.c | 14 ++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/DOCS/interface-changes/disc-menu.txt b/DOCS/interface-changes/disc-menu.txt index 3f8f558c5f29c..941b7b2ff1e1d 100644 --- a/DOCS/interface-changes/disc-menu.txt +++ b/DOCS/interface-changes/disc-menu.txt @@ -1 +1,2 @@ and `--disc-menu` option +add `disc-menu-active` property diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 2de04ff287d73..23d0bac5f3e21 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -2423,6 +2423,12 @@ Property list between having no editions and a single edition, which will be reflected by the property, although in practice it does not matter.) +``disc-menu-active`` + ``yes`` when the current optical-disc stream (DVD or Blu-ray) is showing + an interactive menu with a selectable button highlight, and ``no`` + otherwise. Unavailable when the currently playing source is not an + optical disc. + ``chapters`` Number of chapters. diff --git a/player/command.c b/player/command.c index 1c205570d28ca..5101f7115d916 100644 --- a/player/command.c +++ b/player/command.c @@ -1130,6 +1130,19 @@ static int mp_property_current_edition(void *ctx, struct m_property *prop, return m_property_int_ro(action, arg, demuxer->edition); } +static int mp_property_disc_menu_active(void *ctx, struct m_property *prop, + int action, void *arg) +{ + MPContext *mpctx = ctx; + struct stream *s = disc_nav_get_stream(mpctx); + if (!s) + return M_PROPERTY_UNAVAILABLE; + struct stream_nav_state st = {0}; + if (stream_control(s, STREAM_CTRL_GET_NAV_STATE, &st) < 1) + return M_PROPERTY_UNAVAILABLE; + return m_property_bool_ro(action, arg, st.menu_active); +} + static int mp_property_edition(void *ctx, struct m_property *prop, int action, void *arg) { @@ -4643,6 +4656,7 @@ static const struct m_property mp_properties_base[] = { {"chapter", mp_property_chapter}, {"edition", mp_property_edition}, {"current-edition", mp_property_current_edition}, + {"disc-menu-active", mp_property_disc_menu_active}, {"chapters", mp_property_chapters}, {"editions", mp_property_editions}, {"metadata", mp_property_metadata}, From 3c7fbc24d01935bf1100456f69098c5f390cdd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 02:17:22 +0200 Subject: [PATCH 18/44] player/command: add discnav command and bind default keys User-facing entry point for in-disc menu navigation. The command forwards an action (up/down/left/right/select/menu/title-menu/popup/ prev/mouse-move/mouse-click) to the current disc stream handler The new `discnav` input section is defined to allow binding specific controls only for disc menu. --- DOCS/interface-changes/disc-menu.txt | 1 + DOCS/man/input.rst | 39 ++++++++++++++ etc/input.conf | 16 ++++++ player/command.c | 80 ++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/DOCS/interface-changes/disc-menu.txt b/DOCS/interface-changes/disc-menu.txt index 941b7b2ff1e1d..c3af55ce17be8 100644 --- a/DOCS/interface-changes/disc-menu.txt +++ b/DOCS/interface-changes/disc-menu.txt @@ -1,2 +1,3 @@ and `--disc-menu` option add `disc-menu-active` property +add `discnav` command diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 23d0bac5f3e21..6758a1009f465 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -1753,6 +1753,45 @@ Miscellaneous Commands ``context-menu`` Show context menu on the video window. See `Context Menu`_ section for details. +``discnav [ ]`` + Send a navigation command to the optical-disc stream that is currently + playing (DVD via ``dvdnav``, Blu-ray via ``libbluray``). The command does + nothing when no disc is being played. + + ```` is one of: + + up, down, left, right + Move the on-disc button selection. + select + Activate the currently highlighted button. + menu + Jump to the disc's root menu (DVD: VMGM root menu, BD: HDMV top menu). + title-menu + Jump to the current title's menu (DVD only; treated like ``menu`` on BD). + popup + Show / dismiss the Blu-ray popup menu. For DVDs, falls back to the + chapter menu where available. + prev + Return from a menu to playback, or to the previous menu domain. + mouse-move + Forward a mouse position to the disc, so it can update the focused + button under the cursor. With no `` `` arguments, the position + is read from the ``mouse-pos`` property and mapped from window into + source-video coordinates automatically. + mouse-click + Like ``mouse-move`` but also activates the button under the cursor. + + ```` and ```` are optional and only meaningful for the ``mouse-*`` + actions. They should normalized cormalized coordinates (top-left at ``0,0``). + Those values are used directly, instead of the live ``mouse-pos``. + + mpv ships default bindings for this command in the ``{discnav}`` input + section (``UP``/``DOWN``/``LEFT``/``RIGHT`` for navigation, ``ENTER`` to + select, ``ESC`` / ``BS`` to leave, ``MBTN_LEFT`` / ``MOUSE_MOVE`` for + mouse). The player enables and disables that section automatically as + the menu appears and disappears, so the bindings only shadow the normal + ones while a menu is actually on screen. + ``update-clipboard [timeout]`` Update the clipboard content so that the ``clipboard`` property reflects up-to-date value. This command is required to update the ``clipboard`` diff --git a/etc/input.conf b/etc/input.conf index 9519bdbe03f91..aa586b2161c6f 100644 --- a/etc/input.conf +++ b/etc/input.conf @@ -234,6 +234,22 @@ #Ctrl+KP_UP add video-align-y -0.01 # align video up #Ctrl+KP_PGUP add video-align-x 0.01; add video-align-y -0.01 # align video right and up +# +# The "{discnav}" section is enabled only while a disc menu with a button +# highlight is actually on screens. +# +#UP {discnav} discnav up +#DOWN {discnav} discnav down +#LEFT {discnav} discnav left +#RIGHT {discnav} discnav right +#ENTER {discnav} discnav select +#ESC {discnav} discnav prev +#BS {discnav} discnav prev +#MOUSE_MOVE {discnav} discnav mouse-move +#MBTN_LEFT {discnav} discnav mouse-click +#Ctrl+m discnav menu # jump to the disc's root menu +#Ctrl+M discnav popup # show/hide Blu-ray popup menu + # # Legacy bindings (may or may not be removed in the future) # diff --git a/player/command.c b/player/command.c index 5101f7115d916..788bb2169f68d 100644 --- a/player/command.c +++ b/player/command.c @@ -1178,6 +1178,22 @@ static int mp_property_edition(void *ctx, struct m_property *prop, } return M_PROPERTY_OK; } + case M_PROPERTY_SET: { + // For disc demuxers, jump the title directly, we sometimes need to + // react even if the actual edition "value" doesn't change. + if (disc_nav_get_stream(mpctx)) { + int new_ed = *(int *)arg; + if (new_ed < 0 || new_ed >= demuxer->num_editions) + return M_PROPERTY_ERROR; + unsigned new_title = new_ed; + stream_control(demuxer->stream, STREAM_CTRL_SET_CURRENT_TITLE, &new_title); + mpctx->opts->edition_id = new_ed; + mp_notify_property(mpctx, "edition"); + mp_wakeup_core(mpctx); + return M_PROPERTY_OK; + } + return mp_property_generic_option(mpctx, prop, action, arg); + } default: return mp_property_generic_option(mpctx, prop, action, arg); } @@ -7427,6 +7443,49 @@ static void cmd_context_menu(void *p) vo_control(vo, VOCTRL_SHOW_MENU, NULL); } +static void cmd_discnav(void *p) +{ + struct mp_cmd_ctx *cmd = p; + struct MPContext *mpctx = cmd->mpctx; + int action = cmd->args[0].v.i; + + struct stream *s = disc_nav_get_stream(mpctx); + if (!s) { + cmd->success = false; + return; + } + + struct stream_nav_cmd nc = { .action = action }; + + struct stream_nav_state pre = {0}; + stream_control(s, STREAM_CTRL_GET_NAV_STATE, &pre); + + if (action == STREAM_NAV_MOUSE_MOVE || action == STREAM_NAV_MOUSE_CLICK) { + double nx = cmd->args[1].v.d; + double ny = cmd->args[2].v.d; + if (nx >= 0 && ny >= 0) { + if (pre.src_w <= 0 || pre.src_h <= 0 || + nx < 0 || nx > 1 || ny < 0 || ny > 1) + { + cmd->success = false; + return; + } + nc.x = (int)(nx * pre.src_w); + nc.y = (int)(ny * pre.src_h); + } else if (!disc_nav_mouse_pos_to_src(mpctx, pre.src_w, pre.src_h, + &nc.x, &nc.y)) + { + cmd->success = false; + return; + } + } + + if (stream_control(s, STREAM_CTRL_NAV_CMD, &nc) < 1) + cmd->success = false; + + mp_wakeup_core(mpctx); +} + static void cmd_flush_status_line(void *p) { struct mp_cmd_ctx *cmd = p; @@ -7983,6 +8042,23 @@ const struct mp_cmd_def mp_cmds[] = { { "context-menu", cmd_context_menu }, + { "discnav", cmd_discnav, + { {"action", OPT_CHOICE(v.i, + {"up", STREAM_NAV_UP}, + {"down", STREAM_NAV_DOWN}, + {"left", STREAM_NAV_LEFT}, + {"right", STREAM_NAV_RIGHT}, + {"select", STREAM_NAV_SELECT}, + {"menu", STREAM_NAV_MENU_ROOT}, + {"title-menu", STREAM_NAV_MENU_TITLE}, + {"popup", STREAM_NAV_MENU_POPUP}, + {"prev", STREAM_NAV_PREV_MENU}, + {"mouse-move", STREAM_NAV_MOUSE_MOVE}, + {"mouse-click", STREAM_NAV_MOUSE_CLICK})}, + {"x", OPT_DOUBLE(v.d), OPTDEF_DOUBLE(-1)}, + {"y", OPT_DOUBLE(v.d), OPTDEF_DOUBLE(-1)} }, + }, + { "flush-status-line", cmd_flush_status_line, { {"clear", OPT_BOOL(v.b)} } }, { "notify-property", cmd_notify_property, { {"property", OPT_STRING(v.s)} } }, @@ -8411,6 +8487,10 @@ void mp_option_run_callback(struct MPContext *mpctx, struct mp_option_callback * mp_notify_property(mpctx, "current-edition"); print_track_list(mpctx, mp_tprintf(42, "Selected edition %d:", demuxer->edition)); + } else if (disc_nav_get_stream(mpctx)) { + unsigned new_title = opts->edition_id; + stream_control(demuxer->stream, + STREAM_CTRL_SET_CURRENT_TITLE, &new_title); } else { if (!mpctx->stop_play) mpctx->stop_play = PT_CURRENT_ENTRY; From 143198cb6e8b81f2806e93a935be0de2cde61a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 20:56:35 +0200 Subject: [PATCH 19/44] demux_disc: set partially_seekable to true Fixes track switching on some discs. --- demux/demux_disc.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demux/demux_disc.c b/demux/demux_disc.c index 0805abdfc6297..2bc3177aa9334 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -578,6 +578,8 @@ static int d_open(demuxer_t *demuxer, enum demux_check check) // Can be seekable even if the stream isn't. demuxer->seekable = true; + // Partially seekable to refresh seek on track changes. + demuxer->partially_seekable = true; add_dvd_streams(demuxer); sync_streams(demuxer); From bd29020e2300ae9c443ae87d6ba4776f2f563630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 6 Jun 2026 21:16:51 +0200 Subject: [PATCH 20/44] discnav: sync track selection state with VM --- player/discnav.c | 96 ++++++++++++++++++++++++++++++++++++++++++ stream/stream.h | 7 +++ stream/stream_bluray.c | 69 +++++++++++++++++++++++++++++- stream/stream_dvdnav.c | 63 +++++++++++++++++++++++++++ 4 files changed, 234 insertions(+), 1 deletion(-) diff --git a/player/discnav.c b/player/discnav.c index 7a3cee8aaa857..4e63eba29a64b 100644 --- a/player/discnav.c +++ b/player/discnav.c @@ -57,6 +57,15 @@ struct disc_nav_state { // sd_lavc path. menu_selected_track remembers what we selected so we // can deselect it again when the menu closes. struct track *menu_selected_track; + + // Last disc-driven audio/sub/angle we acted on. + int last_audio_id; + int last_sub_id; + bool last_sub_visible; + int last_angle; + bool track_sync_seen; + // Last disc-discontinuity id we acted on for track sync. + uint32_t last_track_disc_id; }; static struct disc_nav_state *get_state(struct MPContext *mpctx) @@ -255,6 +264,92 @@ static void check_async_discontinuity(struct MPContext *mpctx, demux_flush(mpctx->demuxer); } +static struct track *find_track_by_demuxer_id(struct MPContext *mpctx, + enum stream_type type, int id) +{ + if (id < 0) + return NULL; + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *t = mpctx->tracks[n]; + if (t->type == type && t->stream && t->stream->demuxer_id == id) + return t; + } + return NULL; +} + +static void sync_disc_track_selection(struct MPContext *mpctx, struct stream *s, + struct stream_nav_state *nav) +{ + struct disc_nav_state *st = get_state(mpctx); + bool first = !st->track_sync_seen; + bool hopped = nav->discontinuity_id != st->last_track_disc_id; + st->last_track_disc_id = nav->discontinuity_id; + st->track_sync_seen = true; + + if (!first && (hopped || nav->active_audio_id != st->last_audio_id) && + mpctx->opts->stream_id[0][STREAM_AUDIO] != -2) + { + struct track *t = find_track_by_demuxer_id(mpctx, STREAM_AUDIO, + nav->active_audio_id); + if (t) { + if (mpctx->current_track[0][STREAM_AUDIO] != t) { + MP_VERBOSE(mpctx, "discnav: disc audio -> demuxer_id 0x%x\n", + nav->active_audio_id); + mp_switch_track_n(mpctx, 0, STREAM_AUDIO, t, 0); + } + st->last_audio_id = nav->active_audio_id; + } else if (nav->active_audio_id >= 0) { + MP_TRACE(mpctx, "discnav: disc audio 0x%x not in tracks yet\n", + nav->active_audio_id); + } + } else if (first) { + st->last_audio_id = nav->active_audio_id; + } + + if (!first && (hopped || + nav->active_sub_id != st->last_sub_id || + nav->sub_visible != st->last_sub_visible) && + !nav->menu_active && !st->menu_selected_track && + mpctx->opts->stream_id[0][STREAM_SUB] != -2) + { + if (nav->sub_visible) { + struct track *t = find_track_by_demuxer_id(mpctx, STREAM_SUB, + nav->active_sub_id); + if (t) { + if (mpctx->current_track[0][STREAM_SUB] != t) { + MP_VERBOSE(mpctx, "discnav: disc sub -> demuxer_id 0x%x\n", + nav->active_sub_id); + mp_switch_track_n(mpctx, 0, STREAM_SUB, t, 0); + } + st->last_sub_id = nav->active_sub_id; + st->last_sub_visible = true; + } else if (nav->active_sub_id >= 0) { + MP_TRACE(mpctx, "discnav: disc sub 0x%x not in tracks yet\n", + nav->active_sub_id); + } + } else { + if (mpctx->current_track[0][STREAM_SUB]) { + MP_VERBOSE(mpctx, "discnav: disc sub -> off\n"); + mp_switch_track_n(mpctx, 0, STREAM_SUB, NULL, 0); + } + st->last_sub_id = nav->active_sub_id; + st->last_sub_visible = false; + } + } else if (first) { + st->last_sub_id = nav->active_sub_id; + st->last_sub_visible = nav->sub_visible; + } + + if (first) { + st->last_angle = nav->angle; + } else if (nav->angle > 0 && nav->angle != st->last_angle) { + MP_VERBOSE(mpctx, "discnav: disc angle %d -> %d (of %d)\n", + st->last_angle, nav->angle, nav->num_angles); + st->last_angle = nav->angle; + mp_notify_property(mpctx, "angle"); + } +} + // Make sure a dvd_subtitle track is selected while a menu is visible, // so the SPU graphics decoded by sd_lavc reach the OSD. static void ensure_menu_sub_selection(struct MPContext *mpctx, bool menu_on) @@ -331,6 +426,7 @@ void disc_nav_update(struct MPContext *mpctx) check_async_discontinuity(mpctx, &nav); sync_current_edition(mpctx, s, &nav); + sync_disc_track_selection(mpctx, s, &nav); bool is_bd = strcmp(s->info->name, "bd") == 0 || strcmp(s->info->name, "bdmv/bluray") == 0; bool visible = nav.menu_active && (is_bd || (nav.hl_w > 0 && nav.hl_h > 0)); diff --git a/stream/stream.h b/stream/stream.h index 66017e85f510f..69275bcd8f151 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -139,6 +139,13 @@ struct stream_nav_state { uint32_t hl_palette[4]; uint32_t change_id; // Bumped whenever any of the above changes uint32_t discontinuity_id; // Bumped when the stream's source position jumps + + // Disc-driven track selection. + int active_audio_id; + int active_sub_id; + bool sub_visible; // disc says subs should be displayed + int angle; // 1-based current angle (0 if unknown) + int num_angles; // total angle count (0 if unknown or always 1) }; struct stream_lang_req { diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index 9d79a16e97ba5..30b21c590ff72 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -131,6 +131,14 @@ struct bluray_priv_s { uint32_t discontinuity_id; // bumped on actions that may hop (SELECT...) bool data_delivered; // any byte returned from fill_buffer yet + // Disc-driven audio/sub selection, mirrored from BD_EVENT_AUDIO_STREAM + // and BD_EVENT_PG_TEXTST{,_STREAM}. The numbers are 1-based libbluray + // stream indices; we resolve to MPEG-TS PIDs via title_info on demand. + // 0 = unknown (no event seen yet), 0xff/0xfff = "none" sentinel from BD. + int audio_stream_num; + int sub_stream_num; + bool sub_visible; + int mouse_x, mouse_y; }; @@ -398,6 +406,35 @@ static void handle_event(stream_t *s, const BD_EVENT *ev) b->title_info = bd_get_playlist_info(b->bd, b->current_playlist, b->current_angle); } + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } + break; + case BD_EVENT_AUDIO_STREAM: + b->audio_stream_num = ev->param; + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } + break; + case BD_EVENT_PG_TEXTST_STREAM: + b->sub_stream_num = ev->param; + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } + break; + case BD_EVENT_PG_TEXTST: + b->sub_visible = ev->param != 0; + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; case BD_EVENT_POPUP: // ev->param: 1 if popup menu is currently available, 0 otherwise. @@ -667,8 +704,33 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) } case STREAM_CTRL_GET_NAV_STATE: { struct stream_nav_state *st = arg; + int audio_pid = -1; + int sub_pid = -1; + const BLURAY_TITLE_INFO *ti = b->title_info; + if (ti && ti->clip_count) { + const BLURAY_CLIP_INFO *ci = &ti->clips[0]; + if (b->audio_stream_num >= 1 && + b->audio_stream_num <= ci->audio_stream_count) + { + audio_pid = ci->audio_streams[b->audio_stream_num - 1].pid; + } + if (b->sub_stream_num >= 1 && + b->sub_stream_num <= ci->pg_stream_count) + { + sub_pid = ci->pg_streams[b->sub_stream_num - 1].pid; + } + } if (!b->hdmv_mode) { - *st = (struct stream_nav_state){0}; + // Even outside HDMV we can carry disc-driven audio/sub/angle so + // the player tracks the disc author's defaults on a plain-title + // playback. + *st = (struct stream_nav_state){ + .active_audio_id = audio_pid, + .active_sub_id = sub_pid, + .sub_visible = b->sub_visible, + .angle = b->current_angle + 1, + .num_angles = ti ? ti->angle_count : 0, + }; return STREAM_OK; } mp_mutex_lock(&b->overlay_lock); @@ -680,6 +742,11 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) .src_h = b->plane_h, .change_id = b->nav_change_id, .discontinuity_id = b->discontinuity_id, + .active_audio_id = audio_pid, + .active_sub_id = sub_pid, + .sub_visible = b->sub_visible, + .angle = b->current_angle + 1, + .num_angles = ti ? ti->angle_count : 0, }; mp_mutex_unlock(&b->overlay_lock); return STREAM_OK; diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index 2bb799a328936..b948b51024ed2 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -83,6 +83,11 @@ struct priv { int src_w, src_h; // video resolution in pixels int auto_actioned_button; // last auto-activated button; 0 if none + // Disc-driven audio/sub/angle state. + int audio_physical; // 0..7 from DVDNAV_AUDIO_STREAM_CHANGE + int sub_physical; // 0..31 from DVDNAV_SPU_STREAM_CHANGE + bool sub_visible; // SPU "on" flag from same event + struct dvd_opts *opts; }; @@ -284,6 +289,28 @@ static void compute_highlight_palette(struct priv *priv, pci_t *pci, int btn) } } +// Map a libdvdnav physical audio stream number (0..7) to the corresponding +// MPEG-PS substream byte that demux_lavf assigns to AVStream->id. +static int dvd_physical_audio_to_substream(struct priv *priv, int physical) +{ + if (physical < 0 || physical > 7) + return -1; + uint16_t fmt = dvdnav_audio_stream_format(priv->dvdnav, physical); + switch (fmt) { + case DVD_AUDIO_FORMAT_AC3: + return 0x80 + physical; + case DVD_AUDIO_FORMAT_DTS: + return 0x88 + physical; + case DVD_AUDIO_FORMAT_LPCM: + return 0xa0 + physical; + case DVD_AUDIO_FORMAT_MPEG: + case DVD_AUDIO_FORMAT_MPEG2_EXT: + return 0xc0 + physical; + default: + return -1; + } +} + static void refresh_video_resolution(struct priv *priv) { uint32_t w = 0, h = 0; @@ -587,6 +614,31 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) update_highlight(priv); break; } + case DVDNAV_AUDIO_STREAM_CHANGE: { + dvdnav_audio_stream_change_event_t *ev = buf; + // physical: 0..7 = active audio stream, -1 = SPU/audio off. + MP_VERBOSE(s, "DVDNAV, audio change phys=%d log=%d\n", ev->physical, ev->logical); + if (priv->audio_physical != ev->physical) { + priv->audio_physical = ev->physical; + priv->nav_change_id++; + } + break; + } + case DVDNAV_SPU_STREAM_CHANGE: { + dvdnav_spu_stream_change_event_t *ev = buf; + int raw = ev->physical_wide; + bool visible = raw >= 0 && !(raw & 0x80); + int phys = raw >= 0 ? (raw & 0x1F) : -1; + MP_VERBOSE(s, "DVDNAV, sub change phys_wide=0x%x lb=0x%x ps=0x%x log=%d\n", + ev->physical_wide, ev->physical_letterbox, + ev->physical_pan_scan, ev->logical); + if (priv->sub_physical != phys || priv->sub_visible != visible) { + priv->sub_physical = phys; + priv->sub_visible = visible; + priv->nav_change_id++; + } + break; + } } } return 0; @@ -785,6 +837,8 @@ static int control(stream_t *stream, int cmd, void *arg) struct stream_nav_state *st = arg; if (priv->src_w <= 0 || priv->src_h <= 0) refresh_video_resolution(priv); + uint32_t cur_angle = 0, num_angles = 0; + dvdnav_get_angle_info(dvdnav, &cur_angle, &num_angles); *st = (struct stream_nav_state){ .menu_active = priv->in_menu, .has_popup = false, @@ -796,6 +850,11 @@ static int control(stream_t *stream, int cmd, void *arg) .hl_h = priv->btn_rect[3], .change_id = priv->nav_change_id, .discontinuity_id = priv->discontinuity_id, + .active_audio_id = dvd_physical_audio_to_substream(priv, priv->audio_physical), + .active_sub_id = priv->sub_physical >= 0 ? 0x20 + priv->sub_physical : -1, + .sub_visible = priv->sub_visible, + .angle = cur_angle, + .num_angles = num_angles, }; memcpy(st->hl_palette, priv->hl_palette, sizeof(st->hl_palette)); return STREAM_OK; @@ -851,6 +910,10 @@ static int open_s_internal(stream_t *stream) char *filename; int ret = 0; + priv->audio_physical = -1; + priv->sub_physical = -1; + priv->sub_visible = false; + p->opts = mp_get_config_group(stream, stream->global, &dvd_conf); if (p->device && p->device[0]) From daa1d9706501849e3969f982ae15bcf4a665afd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 7 Jun 2026 03:01:16 +0200 Subject: [PATCH 21/44] stream_bluray: add BD-J menu support Adds support for ARGB callback from libbluray. For some reason they don't have unified support, and sometimes it uses ARGB (for BD-J) and RLE (for HDMV), while they could convery RLE encoded menus into ARGB bitmaps, if only one is supproted. We have to support both. When Java VM is not available, fallbacks to non-menu playback. --- stream/stream_bluray.c | 371 ++++++++++++++++++++++++++++------------- 1 file changed, 258 insertions(+), 113 deletions(-) diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index 30b21c590ff72..00ef02fb355d3 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -51,6 +51,7 @@ #include "osdep/timer.h" #include "sub/osd.h" #include "sub/img_convert.h" +#include "video/csputils.h" #include "video/mp_image.h" #define BLURAY_SECTOR_SIZE 6144 @@ -90,6 +91,14 @@ const struct m_sub_options stream_bluray_conf = { }, }; +// One overlay plane (BGRA, premultiplied alpha). +struct bd_overlay_plane { + uint32_t *work; + uint32_t *publish; + int w, h; // current allocation size (0 if unallocated) + bool visible; // publish has any non-zero alpha pixel +}; + struct bluray_priv_s { BLURAY *bd; BLURAY_TITLE_INFO *title_info; @@ -108,23 +117,17 @@ struct bluray_priv_s { struct mp_bluray_opts *opts; struct m_config_cache *opts_cache; - // HDMV menu support (enabled when cfg_title == BLURAY_MENU_TITLE). - // libbluray delivers compressed YUV+RLE overlay primitives via - // bd_register_overlay_proc for the HDMV graphics controller. We decompress - // into ig_plane and snapshot to ig_publish on every FLUSH so the player - // thread can read a coherent image under overlay_lock. - // - // libbluray docs seems to suggest we should use bd_register_argb_overlay_proc - // if we can handle ARGB planes directly, but in practice that callback is - // only used for BD-J menus, and HDMV menus goes through the RLE callback. - // We support only the later, currently. + // Disc-menu support (enabled when cfg_title == BLURAY_MENU_TITLE). + // The HDMV graphics controller emits IG-plane primitives through + // bd_register_overlay_proc (YUV+RLE). BD-J titles bypass it entirely + // and emit fully-rendered ARGB on both PG and IG planes through + // bd_register_argb_overlay_proc. Discs that mix HDMV first-play with + // BD-J menus (or vice versa) need both callbacks registered. bool hdmv_mode; - uint32_t *ig_plane; // working BGRA plane, written from callback - uint32_t *ig_publish; // last FLUSHed snapshot, read by player - int plane_w, plane_h; // current allocation size (0 if unallocated) - mp_mutex overlay_lock; // guards ig_publish + visibility flags + struct bd_overlay_plane ig; + struct bd_overlay_plane pg; + mp_mutex overlay_lock; // guards plane fields + visibility flags - bool overlay_ig_visible; // IG plane was FLUSHED with non-empty content bool menu_event_active; // BD_EVENT_MENU == 1 bool popup_supported; // BD_EVENT_POPUP == 1 uint32_t nav_change_id; // bumped on FLUSH/HIDE/MENU/POPUP events @@ -142,47 +145,106 @@ struct bluray_priv_s { int mouse_x, mouse_y; }; -// Lazy (re-)allocation for the IG-plane working/publish buffers. -static bool bd_ensure_plane(struct bluray_priv_s *priv, int w, int h) +// Lazy (re-)allocation for an overlay plane's working+publish buffer pair. +static bool bd_overlay_ensure(struct bluray_priv_s *priv, + struct bd_overlay_plane *p, int w, int h) { if (w <= 0 || h <= 0) return false; - if (priv->ig_plane && w <= priv->plane_w && h <= priv->plane_h) + if (p->work && w <= p->w && h <= p->h) return true; - int nw = MPMAX(w, priv->plane_w); - int nh = MPMAX(h, priv->plane_h); + int nw = MPMAX(w, p->w); + int nh = MPMAX(h, p->h); size_t bytes = (size_t)nw * nh * 4; - uint32_t *plane = talloc_realloc(priv, priv->ig_plane, uint32_t, nw * nh); - uint32_t *publish = talloc_realloc(priv, priv->ig_publish, uint32_t, nw * nh); - if (!plane || !publish) + uint32_t *work = talloc_realloc(priv, p->work, uint32_t, nw * nh); + uint32_t *pub = talloc_realloc(priv, p->publish, uint32_t, nw * nh); + if (!work || !pub) return false; - memset(plane, 0, bytes); - memset(publish, 0, bytes); - priv->ig_plane = plane; - priv->ig_publish = publish; - priv->plane_w = nw; - priv->plane_h = nh; + memset(work, 0, bytes); + memset(pub, 0, bytes); + p->work = work; + p->publish = pub; + p->w = nw; + p->h = nh; return true; } -static void bd_palette_to_bgra(const BD_PG_PALETTE_ENTRY *pg, uint32_t out[256]) +static void bd_overlay_clear(struct bd_overlay_plane *p) +{ + if (p->work) + memset(p->work, 0, (size_t)p->w * p->h * 4); +} + +static void bd_overlay_hide(struct bluray_priv_s *priv, struct bd_overlay_plane *p) +{ + p->visible = false; + priv->nav_change_id++; +} + +static void bd_overlay_flush(struct bluray_priv_s *priv, struct bd_overlay_plane *p) +{ + if (!p->work) + return; + size_t n = (size_t)p->w * p->h; + memcpy(p->publish, p->work, n * 4); + bool any = false; + for (size_t i = 0; i < n; i++) { + if (p->publish[i] & 0xFF000000) { + any = true; + break; + } + } + p->visible = any; + priv->nav_change_id++; +} + +static enum pl_color_system bd_overlay_csp(struct bluray_priv_s *priv) { + const BLURAY_TITLE_INFO *ti = priv->title_info; + if (!ti || !ti->clip_count || !ti->clips[0].video_stream_count) + return PL_COLOR_SYSTEM_BT_709; + const BLURAY_STREAM_INFO *vs = &ti->clips[0].video_streams[0]; + +#if BLURAY_VERSION > BLURAY_VERSION_CODE(1, 4, 1) // not yet released, but next libbluray will have this + // HEVC on UHD BD (2160p or 1080p) can be either 2020 or 709 + if (vs->coding_type == BLURAY_STREAM_TYPE_VIDEO_HEVC || vs->format == BLURAY_VIDEO_FORMAT_2160P) + return vs->color_space == BLURAY_COLOR_SPACE_BT2020 ? PL_COLOR_SYSTEM_BT_2020_NC : PL_COLOR_SYSTEM_BT_709; +#endif + + switch (vs->format) { +#if BLURAY_VERSION <= BLURAY_VERSION_CODE(1, 4, 1) + case BLURAY_VIDEO_FORMAT_2160P: + return PL_COLOR_SYSTEM_BT_2020_NC; +#endif + case BLURAY_VIDEO_FORMAT_480I: // ITU-R BT.601 + case BLURAY_VIDEO_FORMAT_576I: + case BLURAY_VIDEO_FORMAT_480P: + case BLURAY_VIDEO_FORMAT_576P: + return PL_COLOR_SYSTEM_BT_601; + default: + return PL_COLOR_SYSTEM_BT_709; + } +} + +static void bd_palette_to_bgra(const BD_PG_PALETTE_ENTRY *pg, uint32_t out[256], + enum pl_color_system csp) +{ + struct mp_csp_params params = MP_CSP_PARAMS_DEFAULTS; + params.repr.sys = csp; + params.repr.levels = PL_COLOR_LEVELS_LIMITED; + params.levels_out = PL_COLOR_LEVELS_FULL; + struct pl_transform3x3 yuv2rgb; + mp_get_csp_matrix(¶ms, &yuv2rgb); + for (int i = 0; i < 256; i++) { - int Y = pg[i].Y, Cb = pg[i].Cb, Cr = pg[i].Cr, T = pg[i].T; - // BT.709 limited->full. - int y_ = (Y - 16) * 1192; // 1.164 << 10 - int cr = Cr - 128; - int cb = Cb - 128; - int r = (y_ + 1836 * cr + 512) >> 10; // 1.793 - int g = (y_ - 547 * cr - 218 * cb + 512) >> 10; // 0.534 / 0.213 - int b = (y_ + 2166 * cb + 512) >> 10; // 2.115 - r = MPCLAMP(r, 0, 255); - g = MPCLAMP(g, 0, 255); - b = MPCLAMP(b, 0, 255); + int yuv[3] = { pg[i].Y, pg[i].Cb, pg[i].Cr }; + int rgb[3]; + mp_map_fixp_color(&yuv2rgb, 8, yuv, 8, rgb); + int T = pg[i].T; // Pre-multiply RGB by alpha so the OSD layer can composite directly. - r = r * T / 255; - g = g * T / 255; - b = b * T / 255; + int r = rgb[0] * T / 255; + int g = rgb[1] * T / 255; + int b = rgb[2] * T / 255; out[i] = ((uint32_t)T << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; } @@ -191,19 +253,19 @@ static void bd_palette_to_bgra(const BD_PG_PALETTE_ENTRY *pg, uint32_t out[256]) } // Composite one RLE-encoded sub-bitmap into the IG plane at (ov->x, ov->y). -static void bd_overlay_draw_rle(struct bluray_priv_s *priv, const BD_OVERLAY *ov) +static void bd_overlay_draw_rle(struct bd_overlay_plane *p, const BD_OVERLAY *ov, + enum pl_color_system csp) { if (!ov->img || !ov->palette || ov->w <= 0 || ov->h <= 0) return; uint32_t pal[256]; - bd_palette_to_bgra(ov->palette, pal); + bd_palette_to_bgra(ov->palette, pal, csp); const BD_PG_RLE_ELEM *rle = ov->img; for (int y = 0; y < ov->h; y++) { int dst_y = ov->y + y; - bool in_plane = dst_y >= 0 && dst_y < priv->plane_h; - uint32_t *dst_row = in_plane - ? priv->ig_plane + (size_t)dst_y * priv->plane_w : NULL; + bool in_plane = dst_y >= 0 && dst_y < p->h; + uint32_t *dst_row = in_plane ? p->work + (size_t)dst_y * p->w : NULL; int x = 0; while (x < ov->w) { int len = rle->len; @@ -218,8 +280,8 @@ static void bd_overlay_draw_rle(struct bluray_priv_s *priv, const BD_OVERLAY *ov run -= skip; dst_x += skip; } - if (dst_x + run > priv->plane_w) - run = priv->plane_w - dst_x; + if (dst_x + run > p->w) + run = p->w - dst_x; if (dst_row && run > 0) { uint32_t c = pal[color & 0xFF]; for (int i = 0; i < run; i++) @@ -232,25 +294,6 @@ static void bd_overlay_draw_rle(struct bluray_priv_s *priv, const BD_OVERLAY *ov } } -// Snapshot the working plane to ig_publish and recompute visibility. -// Must be called with priv->overlay_lock held. -static void bd_publish_overlay_flush(struct bluray_priv_s *priv) -{ - if (!priv->ig_plane) - return; - bool any = false; - size_t n = (size_t)priv->plane_w * priv->plane_h; - memcpy(priv->ig_publish, priv->ig_plane, n * 4); - for (size_t i = 0; i < n; i++) { - if (priv->ig_publish[i] & 0xFF000000) { - any = true; - break; - } - } - priv->overlay_ig_visible = any; - priv->nav_change_id++; -} - // Called by libbluray's HDMV graphics controller for every overlay primitive // on either plane. We only render the IG (menu) plane; PG (subtitles) flows // through the regular demuxer/sd_lavc pipeline. @@ -262,52 +305,112 @@ static void bd_yuv_overlay_cb(void *handle, const struct bd_overlay_s *ov) if (ov->plane != BD_OVERLAY_IG) return; + struct bd_overlay_plane *p = &priv->ig; mp_mutex_lock(&priv->overlay_lock); switch (ov->cmd) { case BD_OVERLAY_INIT: - bd_ensure_plane(priv, ov->w, ov->h); - if (priv->ig_plane) { - memset(priv->ig_plane, 0, - (size_t)priv->plane_w * priv->plane_h * 4); - } - priv->overlay_ig_visible = false; - priv->nav_change_id++; + bd_overlay_ensure(priv, p, ov->w, ov->h); + bd_overlay_clear(p); + bd_overlay_hide(priv, p); break; case BD_OVERLAY_CLOSE: - priv->overlay_ig_visible = false; - priv->nav_change_id++; + case BD_OVERLAY_HIDE: + bd_overlay_hide(priv, p); break; case BD_OVERLAY_CLEAR: - if (priv->ig_plane) { - memset(priv->ig_plane, 0, - (size_t)priv->plane_w * priv->plane_h * 4); - } + bd_overlay_clear(p); break; case BD_OVERLAY_WIPE: - if (priv->ig_plane) { + if (p->work) { for (int y = 0; y < ov->h; y++) { int dy = ov->y + y; - if (dy < 0 || dy >= priv->plane_h) + if (dy < 0 || dy >= p->h) continue; int dx = MPMAX(ov->x, 0); - int run = MPMIN(ov->w, priv->plane_w - dx); - if (run > 0) { - memset(priv->ig_plane + (size_t)dy * priv->plane_w + dx, - 0, run * 4); - } + int run = MPMIN(ov->w, p->w - dx); + if (run > 0) + memset(p->work + (size_t)dy * p->w + dx, 0, run * 4); } } break; case BD_OVERLAY_DRAW: - if (priv->ig_plane) - bd_overlay_draw_rle(priv, ov); - break; - case BD_OVERLAY_HIDE: - priv->overlay_ig_visible = false; - priv->nav_change_id++; + if (p->work) + bd_overlay_draw_rle(p, ov, bd_overlay_csp(priv)); break; case BD_OVERLAY_FLUSH: - bd_publish_overlay_flush(priv); + bd_overlay_flush(priv, p); + break; + default: + break; + } + mp_mutex_unlock(&priv->overlay_lock); +} + +static inline uint32_t bd_argb_premul(uint32_t src) +{ + uint32_t a = (src >> 24) & 0xFF; + if (a == 0) + return 0; + if (a == 255) + return src; + uint32_t r = (src >> 16) & 0xFF; + uint32_t g = (src >> 8) & 0xFF; + uint32_t b = src & 0xFF; + r = (r * a + 127) / 255; + g = (g * a + 127) / 255; + b = (b * a + 127) / 255; + return (a << 24) | (r << 16) | (g << 8) | b; +} + +static void bd_overlay_draw_argb(struct bd_overlay_plane *p, + const BD_ARGB_OVERLAY *ov) +{ + if (!ov->argb || ov->w <= 0 || ov->h <= 0) + return; + int sx = 0, sy = 0; + int dx = ov->x, dy = ov->y; + int w = ov->w, h = ov->h; + if (dx < 0) { sx -= dx; w += dx; dx = 0; } + if (dy < 0) { sy -= dy; h += dy; dy = 0; } + if (dx + w > p->w) w = p->w - dx; + if (dy + h > p->h) h = p->h - dy; + if (w <= 0 || h <= 0) + return; + for (int y = 0; y < h; y++) { + const uint32_t *src = ov->argb + (size_t)(sy + y) * ov->stride + sx; + uint32_t *dst = p->work + (size_t)(dy + y) * p->w + dx; + for (int x = 0; x < w; x++) + dst[x] = bd_argb_premul(src[x]); + } +} + +// Called by libbluray when a BD-J title paints into either the PG or IG plane. +static void bd_argb_overlay_cb(void *handle, const BD_ARGB_OVERLAY *ov) +{ + struct bluray_priv_s *priv = handle; + if (!ov) + return; + if (ov->plane != BD_OVERLAY_PG && ov->plane != BD_OVERLAY_IG) + return; + + struct bd_overlay_plane *p = ov->plane == BD_OVERLAY_IG ? &priv->ig + : &priv->pg; + mp_mutex_lock(&priv->overlay_lock); + switch (ov->cmd) { + case BD_ARGB_OVERLAY_INIT: + bd_overlay_ensure(priv, p, ov->w, ov->h); + bd_overlay_clear(p); + bd_overlay_hide(priv, p); + break; + case BD_ARGB_OVERLAY_CLOSE: + bd_overlay_hide(priv, p); + break; + case BD_ARGB_OVERLAY_DRAW: + if (p->work) + bd_overlay_draw_argb(p, ov); + break; + case BD_ARGB_OVERLAY_FLUSH: + bd_overlay_flush(priv, p); break; default: break; @@ -334,8 +437,10 @@ static void bluray_stream_close(stream_t *s) if (priv->title_info) bd_free_title_info(priv->title_info); if (priv->bd) { - if (priv->hdmv_mode) + if (priv->hdmv_mode) { bd_register_overlay_proc(priv->bd, NULL, NULL); + bd_register_argb_overlay_proc(priv->bd, NULL, NULL, NULL); + } bd_close(priv->bd); } if (priv->hdmv_mode) @@ -734,12 +839,15 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) return STREAM_OK; } mp_mutex_lock(&b->overlay_lock); - bool visible = b->menu_event_active && b->overlay_ig_visible; + // BD_EVENT_MENU isn't fired by BD-J (it's HDMV-only), so treat any + // visible BD-J plane as an active menu too. + bool any_overlay = b->ig.visible || b->pg.visible; + bool visible = (b->menu_event_active && b->ig.visible) || any_overlay; *st = (struct stream_nav_state){ .menu_active = visible, .has_popup = b->popup_supported, - .src_w = b->plane_w, - .src_h = b->plane_h, + .src_w = MPMAX(b->ig.w, b->pg.w), + .src_h = MPMAX(b->ig.h, b->pg.h), .change_id = b->nav_change_id, .discontinuity_id = b->discontinuity_id, .active_audio_id = audio_pid, @@ -758,19 +866,37 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) if (!req->dst || req->w <= 0 || req->h <= 0) return STREAM_ERROR; mp_mutex_lock(&b->overlay_lock); - int copy_w = MPMIN(req->w, b->plane_w); - int copy_h = MPMIN(req->h, b->plane_h); - if (b->ig_publish && b->overlay_ig_visible) { - for (int y = 0; y < copy_h; y++) { - memcpy(req->dst + y * req->stride, - b->ig_publish + y * b->plane_w, - copy_w * 4); + int copy_w = MPMIN(req->w, MPMAX(b->ig.w, b->pg.w)); + int copy_h = MPMIN(req->h, MPMAX(b->ig.h, b->pg.h)); + const uint32_t *ig_src = b->ig.visible ? b->ig.publish : NULL; + const uint32_t *pg_src = b->pg.visible ? b->pg.publish : NULL; + for (int y = 0; y < copy_h; y++) { + uint32_t *dst = (uint32_t *)(req->dst + y * req->stride); + const uint32_t *ig_row = (ig_src && y < b->ig.h) + ? ig_src + (size_t)y * b->ig.w : NULL; + const uint32_t *pg_row = (pg_src && y < b->pg.h) + ? pg_src + (size_t)y * b->pg.w : NULL; + int ig_lim = ig_row ? b->ig.w : 0; + int pg_lim = pg_row ? b->pg.w : 0; + for (int x = 0; x < copy_w; x++) { + uint32_t ig = (x < ig_lim) ? ig_row[x] : 0; + uint32_t pg = (x < pg_lim) ? pg_row[x] : 0; + uint32_t ia = (ig >> 24) & 0xFF; + if (ia == 0) { + dst[x] = pg; + } else if (ia == 0xFF || !pg) { + dst[x] = ig; + } else { + // out = ig + pg * (1 - ig.a) + uint32_t inv = 255 - ia; + uint32_t na = ia + ((pg >> 24) & 0xFF) * inv / 255; + uint32_t nr = ((ig >> 16) & 0xFF) + ((pg >> 16) & 0xFF) * inv / 255; + uint32_t ng = ((ig >> 8) & 0xFF) + ((pg >> 8) & 0xFF) * inv / 255; + uint32_t nb = ( ig & 0xFF) + ( pg & 0xFF) * inv / 255; + dst[x] = (MPMIN(na, 255u) << 24) | (MPMIN(nr, 255u) << 16) | + (MPMIN(ng, 255u) << 8) | MPMIN(nb, 255u); + } } - } else { - // Plane is hidden / pre-init; clear the caller's buffer so a - // stale image doesn't linger after the menu closes. - for (int y = 0; y < copy_h; y++) - memset(req->dst + y * req->stride, 0, copy_w * 4); } req->change_id = b->nav_change_id; req->w = copy_w; @@ -946,11 +1072,30 @@ static int bluray_stream_open_internal(stream_t *s) // initialize libbluray event queue bd_get_event(bd, NULL); + const BLURAY_DISC_INFO *info = bd_get_disc_info(bd); + MP_VERBOSE(s, "First play: %i, Top menu: %i, " + "HDMV Titles: %i, BD-J Titles: %i, Other: %i\n", + info->first_play_supported, info->top_menu_supported, + info->num_hdmv_titles, info->num_bdj_titles, + info->num_unsupported_titles); + b->hdmv_mode = b->cfg_title == BLURAY_MENU_TITLE; + + // BD-J menus require a usable Java VM and libbluray.jar. + if (b->hdmv_mode && info->bdj_detected && !info->bdj_handled) { + MP_WARN(s, "BD-J menus not supported. Playing without menus. " + "Java VM: %d, libbluray.jar: %d\n", + info->libjvm_detected, info->bdj_handled); + b->hdmv_mode = false; + b->cfg_title = BLURAY_DEFAULT_TITLE; + } + MP_VERBOSE(s, "bdnav: cfg_title=%d hdmv_mode=%d\n", b->cfg_title, b->hdmv_mode); if (b->hdmv_mode) { mp_mutex_init(&b->overlay_lock); bd_register_overlay_proc(bd, b, bd_yuv_overlay_cb); + if (info->num_bdj_titles) + bd_register_argb_overlay_proc(bd, b, bd_argb_overlay_cb, NULL); if (!bd_play(bd)) { MP_ERR(s, "Couldn't start Blu-ray HDMV playback.\n"); ret = STREAM_UNSUPPORTED; From 3808ebbb6247c701c745bfd544b22d5e8297997e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 7 Jun 2026 23:02:32 +0200 Subject: [PATCH 22/44] demux: in interactive mode disable forward cache VM events are processed in fill_buffer callback, which are called by the demuxer thread. The should be processed close to what user is seeing on screen, else VM will think that we are at different point and may trigger action before user sees whole cache. --- demux/demux.c | 39 +++++++++++++++++++++++++++++++++++++++ demux/demux.h | 2 ++ demux/demux_disc.c | 25 +++++++++++++++---------- player/command.c | 5 ++++- stream/stream.h | 1 + stream/stream_bluray.c | 1 + stream/stream_dvdnav.c | 1 + 7 files changed, 63 insertions(+), 11 deletions(-) diff --git a/demux/demux.c b/demux/demux.c index 5890fabaac5d8..dc1a78cba493b 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -209,6 +209,12 @@ struct demux_internal { double hyst_secs; // stop reading till there's hyst_secs remaining size_t hyst_bytes; // stop reading till there's hyst_bytes remaining bool hyst_active; + // Set while interactive disc navigation (DVD/BD menus) is active. + bool nav_active; + // One-shot request to force a single read even when nothing is being + // consumed, so a disc-nav command can drive the VM forward. Mostly used in + // paused state. + bool nav_pump; size_t max_bytes; size_t max_bytes_bw; bool seekable_cache; @@ -1246,6 +1252,18 @@ void demux_start_prefetch(struct demuxer *demuxer) mp_mutex_unlock(&in->lock); } +void demux_drive_nav(struct demuxer *demuxer) +{ + struct demux_internal *in = demuxer->in; + mp_assert(demuxer == in->d_user); + + mp_mutex_lock(&in->lock); + in->nav_pump = 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) { @@ -2301,6 +2319,12 @@ static bool read_packet(struct demux_internal *in) prefetch_more |= true; } + // While interactive disc navigation is active, never read ahead. It would + // advance the disc VM past what the user is watching. + if (in->nav_active) + prefetch_more = false; + prefetch_more |= in->nav_pump; + MP_TRACE(in, "bytes=%zd, read_more=%d prefetch_more=%d, refresh_more=%d\n", (size_t)total_fw_bytes, read_more, prefetch_more, refresh_more); if (total_fw_bytes >= in->max_bytes) { @@ -2354,6 +2378,7 @@ static bool read_packet(struct demux_internal *in) // Actually read a packet. Drop the lock while doing so, because waiting // for disk or network I/O can take time. in->reading = true; + in->nav_pump = false; in->after_seek = false; in->after_seek_to_start = false; mp_mutex_unlock(&in->lock); @@ -3196,6 +3221,20 @@ void demux_set_duration(demuxer_t *demuxer, double duration) mp_mutex_unlock(&in->lock); } +// Tell the cache whether interactive disc navigation is active. +void demux_set_nav_active(demuxer_t *demuxer, bool active) +{ + mp_assert(demuxer == demuxer->in->d_thread); + struct demux_internal *in = demuxer->in; + + mp_mutex_lock(&in->lock); + if (in->nav_active != active) { + in->nav_active = active; + mp_cond_signal(&in->wakeup); + } + mp_mutex_unlock(&in->lock); +} + // Updates the chapters/editions should it need to be changed. Used for demuxers // that changes titles/playlists at runtime. void demux_lists_changed(demuxer_t *demuxer) diff --git a/demux/demux.h b/demux/demux.h index 093424c63ce1e..eb53cd711dd4d 100644 --- a/demux/demux.h +++ b/demux/demux.h @@ -321,6 +321,7 @@ void demux_start_thread(struct demuxer *demuxer); 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); bool demux_cancel_test(struct demuxer *demuxer); @@ -350,6 +351,7 @@ void demux_close_stream(struct demuxer *demuxer); void demux_metadata_changed(demuxer_t *demuxer); void demux_set_duration(demuxer_t *demuxer, double duration); +void demux_set_nav_active(demuxer_t *demuxer, bool active); void demux_lists_changed(demuxer_t *demuxer); void demux_update(demuxer_t *demuxer, double playback_pts); diff --git a/demux/demux_disc.c b/demux/demux_disc.c index 2bc3177aa9334..55a2bc70afc99 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -64,6 +64,7 @@ struct priv { double last_dts; // DTS of previously demuxed packet bool seek_reinit; // needs reinit after seek uint32_t last_discontinuity_id; // Last source-position-jump id seen from the stream. + bool nav_active; // last interactive-nav state pushed to the cache bool is_dvd, is_cdda; }; @@ -375,16 +376,20 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt struct priv *p = demuxer->priv; struct stream_nav_state nav = {0}; - if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1 && - nav.discontinuity_id != p->last_discontinuity_id) - { - MP_VERBOSE(demuxer, "discontinuity %u->%u, reopening slave\n", - p->last_discontinuity_id, nav.discontinuity_id); - if (!reopen_slave(demuxer)) - return false; - if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) - p->last_discontinuity_id = nav.discontinuity_id; - p->seek_reinit = true; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) { + if (nav.nav_active != p->nav_active) { + p->nav_active = nav.nav_active; + demux_set_nav_active(demuxer, nav.nav_active); + } + if (nav.discontinuity_id != p->last_discontinuity_id) { + MP_VERBOSE(demuxer, "discontinuity %u->%u, reopening slave\n", + p->last_discontinuity_id, nav.discontinuity_id); + if (!reopen_slave(demuxer)) + return false; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) + p->last_discontinuity_id = nav.discontinuity_id; + p->seek_reinit = true; + } } struct demux_packet *pkt = demux_read_any_packet(p->slave); diff --git a/player/command.c b/player/command.c index 788bb2169f68d..5fcb0f0be3372 100644 --- a/player/command.c +++ b/player/command.c @@ -7480,8 +7480,11 @@ static void cmd_discnav(void *p) } } - if (stream_control(s, STREAM_CTRL_NAV_CMD, &nc) < 1) + if (stream_control(s, STREAM_CTRL_NAV_CMD, &nc) < 1) { cmd->success = false; + } else if (mpctx->demuxer) { + demux_drive_nav(mpctx->demuxer); + } mp_wakeup_core(mpctx); } diff --git a/stream/stream.h b/stream/stream.h index 69275bcd8f151..290dea7b89b0a 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -128,6 +128,7 @@ struct stream_nav_cmd { // Snapshot of the stream's menu state. 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) int src_w, src_h; // dimensions of the coordinate space mouse uses diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index 00ef02fb355d3..baaa64b9d41b6 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -844,6 +844,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) bool any_overlay = b->ig.visible || b->pg.visible; bool visible = (b->menu_event_active && b->ig.visible) || any_overlay; *st = (struct stream_nav_state){ + .nav_active = true, .menu_active = visible, .has_popup = b->popup_supported, .src_w = MPMAX(b->ig.w, b->pg.w), diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index b948b51024ed2..316ddf8886ae5 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -840,6 +840,7 @@ static int control(stream_t *stream, int cmd, void *arg) uint32_t cur_angle = 0, num_angles = 0; dvdnav_get_angle_info(dvdnav, &cur_angle, &num_angles); *st = (struct stream_nav_state){ + .nav_active = true, .menu_active = priv->in_menu, .has_popup = false, .src_w = priv->src_w, From 49f2ec2b6a39087d99e3050badbfa3c5b932c24f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 8 Jun 2026 04:51:17 +0200 Subject: [PATCH 23/44] stream_bluray: use 1-based angle, same as in DVD --- player/command.c | 2 +- stream/stream_bluray.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/player/command.c b/player/command.c index 5fcb0f0be3372..e12109904769c 100644 --- a/player/command.c +++ b/player/command.c @@ -8509,7 +8509,7 @@ void mp_option_run_callback(struct MPContext *mpctx, struct mp_option_callback * if (mpctx->playback_initialized && demuxer && demuxer->stream && (!strcmp(demuxer->stream->info->name, "bd") || !strcmp(demuxer->stream->info->name, "bdmv/bluray"))) { - int angle = opts->stream_bluray_opts->angle - 1; + int angle = opts->stream_bluray_opts->angle; stream_control(demuxer->stream, STREAM_CTRL_SET_ANGLE, &angle); } } diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index baaa64b9d41b6..6fe81203a378a 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -687,7 +687,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) return STREAM_OK; } case STREAM_CTRL_GET_ANGLE: { - *((int *) arg) = b->current_angle; + *((int *) arg) = b->current_angle + 1; return STREAM_OK; } case STREAM_CTRL_SET_ANGLE: { @@ -695,10 +695,10 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) if (!ti) return STREAM_UNSUPPORTED; int angle = *((int *) arg); - if (angle < 0 || angle > ti->angle_count) + if (angle < 1 || angle > ti->angle_count) return STREAM_UNSUPPORTED; - b->current_angle = angle; - bd_seamless_angle_change(b->bd, angle); + b->current_angle = angle - 1; + bd_seamless_angle_change(b->bd, b->current_angle); return STREAM_OK; } case STREAM_CTRL_GET_TITLE_LENGTH: { From 0b4ea226329d82e5c19699159733400ddb0fa138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 8 Jun 2026 04:45:17 +0200 Subject: [PATCH 24/44] stream_bluray: lock all controls that read cached disc state We cache some state like title properties, in some cases during deinit player thread may actually try to read it during demuxer thead read. Just add a lock, it shouldn't be called frequently to cause too much synchronization. --- stream/stream_bluray.c | 170 ++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 72 deletions(-) diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index 6fe81203a378a..58789e391a6a3 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -126,7 +126,8 @@ struct bluray_priv_s { bool hdmv_mode; struct bd_overlay_plane ig; struct bd_overlay_plane pg; - mp_mutex overlay_lock; // guards plane fields + visibility flags + // Guards state shared between the demuxer thread and the player thread. + mp_mutex overlay_lock; bool menu_event_active; // BD_EVENT_MENU == 1 bool popup_supported; // BD_EVENT_POPUP == 1 @@ -443,8 +444,7 @@ static void bluray_stream_close(stream_t *s) } bd_close(priv->bd); } - if (priv->hdmv_mode) - mp_mutex_destroy(&priv->overlay_lock); + mp_mutex_destroy(&priv->overlay_lock); } static void handle_event(stream_t *s, const BD_EVENT *ev) @@ -471,75 +471,77 @@ static void handle_event(stream_t *s, const BD_EVENT *ev) break; case BD_EVENT_END_OF_TITLE: break; - case BD_EVENT_PLAYLIST: - b->current_playlist = ev->param; - b->current_title = bd_get_current_title(b->bd); + case BD_EVENT_PLAYLIST: { + int playlist = ev->param; + int title = bd_get_current_title(b->bd); if (b->title_to_playlist) { for (int i = 0; i < b->num_titles; i++) { if (b->title_to_playlist[i] == (uint32_t)ev->param) { - b->current_title = i; + title = i; break; } } } + BLURAY_TITLE_INFO *ti = bd_get_playlist_info(b->bd, playlist, b->current_angle); + mp_mutex_lock(&b->overlay_lock); if (b->title_info) bd_free_title_info(b->title_info); - b->title_info = bd_get_playlist_info(b->bd, b->current_playlist, - b->current_angle); - if (b->hdmv_mode) { - mp_mutex_lock(&b->overlay_lock); + b->title_info = ti; + b->current_playlist = playlist; + b->current_title = title; + if (b->hdmv_mode) b->discontinuity_id++; - mp_mutex_unlock(&b->overlay_lock); - } + mp_mutex_unlock(&b->overlay_lock); break; - case BD_EVENT_TITLE: - b->current_title = bd_get_current_title(b->bd); + } + case BD_EVENT_TITLE: { + int title = bd_get_current_title(b->bd); + mp_mutex_lock(&b->overlay_lock); if (b->title_info) { bd_free_title_info(b->title_info); b->title_info = NULL; } - if (b->hdmv_mode) { - mp_mutex_lock(&b->overlay_lock); + b->current_title = title; + if (b->hdmv_mode) b->discontinuity_id++; - mp_mutex_unlock(&b->overlay_lock); - } + mp_mutex_unlock(&b->overlay_lock); break; - case BD_EVENT_ANGLE: - b->current_angle = ev->param; - if (b->title_info) { + } + case BD_EVENT_ANGLE: { + int angle = ev->param; + BLURAY_TITLE_INFO *ti = b->title_info ? bd_get_playlist_info(b->bd, b->current_playlist, angle) + : NULL; + mp_mutex_lock(&b->overlay_lock); + b->current_angle = angle; + if (ti) { bd_free_title_info(b->title_info); - b->title_info = bd_get_playlist_info(b->bd, b->current_playlist, - b->current_angle); + b->title_info = ti; } - if (b->hdmv_mode) { - mp_mutex_lock(&b->overlay_lock); + if (b->hdmv_mode) b->nav_change_id++; - mp_mutex_unlock(&b->overlay_lock); - } + mp_mutex_unlock(&b->overlay_lock); break; + } case BD_EVENT_AUDIO_STREAM: + mp_mutex_lock(&b->overlay_lock); b->audio_stream_num = ev->param; - if (b->hdmv_mode) { - mp_mutex_lock(&b->overlay_lock); + if (b->hdmv_mode) b->nav_change_id++; - mp_mutex_unlock(&b->overlay_lock); - } + mp_mutex_unlock(&b->overlay_lock); break; case BD_EVENT_PG_TEXTST_STREAM: + mp_mutex_lock(&b->overlay_lock); b->sub_stream_num = ev->param; - if (b->hdmv_mode) { - mp_mutex_lock(&b->overlay_lock); + if (b->hdmv_mode) b->nav_change_id++; - mp_mutex_unlock(&b->overlay_lock); - } + mp_mutex_unlock(&b->overlay_lock); break; case BD_EVENT_PG_TEXTST: + mp_mutex_lock(&b->overlay_lock); b->sub_visible = ev->param != 0; - if (b->hdmv_mode) { - mp_mutex_lock(&b->overlay_lock); + if (b->hdmv_mode) b->nav_change_id++; - mp_mutex_unlock(&b->overlay_lock); - } + mp_mutex_unlock(&b->overlay_lock); break; case BD_EVENT_POPUP: // ev->param: 1 if popup menu is currently available, 0 otherwise. @@ -620,24 +622,31 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) switch (cmd) { case STREAM_CTRL_GET_NUM_CHAPTERS: { + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; - *((unsigned int *) arg) = ti->chapter_count; - return STREAM_OK; + if (ti) + *((unsigned int *) arg) = ti->chapter_count; + mp_mutex_unlock(&b->overlay_lock); + return ti ? STREAM_OK : STREAM_UNSUPPORTED; } case STREAM_CTRL_GET_CHAPTER_TIME: { - const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; int chapter = *(double *)arg; - double time = MP_NOPTS_VALUE; - if (chapter >= 0 && chapter < ti->chapter_count) - time = BD_TIME_TO_MP(ti->chapters[chapter].start); - if (time == MP_NOPTS_VALUE) - return STREAM_ERROR; - *(double *)arg = time; - return STREAM_OK; + mp_mutex_lock(&b->overlay_lock); + const BLURAY_TITLE_INFO *ti = b->title_info; + int rc = STREAM_UNSUPPORTED; + if (ti) { + double time = MP_NOPTS_VALUE; + if (chapter >= 0 && chapter < ti->chapter_count) + time = BD_TIME_TO_MP(ti->chapters[chapter].start); + if (time != MP_NOPTS_VALUE) { + *(double *)arg = time; + rc = STREAM_OK; + } else { + rc = STREAM_ERROR; + } + } + mp_mutex_unlock(&b->overlay_lock); + return rc; } case STREAM_CTRL_SET_CURRENT_TITLE: { const uint32_t title = *((unsigned int*)arg); @@ -650,11 +659,15 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) } if (title >= b->num_titles || !play_title(b, title)) return STREAM_UNSUPPORTED; + mp_mutex_lock(&b->overlay_lock); b->current_title = title; + mp_mutex_unlock(&b->overlay_lock); return STREAM_OK; } case STREAM_CTRL_GET_CURRENT_TITLE: { + mp_mutex_lock(&b->overlay_lock); *((unsigned int *) arg) = b->current_title; + mp_mutex_unlock(&b->overlay_lock); return STREAM_OK; } case STREAM_CTRL_GET_NUM_TITLES: { @@ -662,11 +675,12 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) return STREAM_OK; } case STREAM_CTRL_GET_TIME_LENGTH: { + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; - *((double *) arg) = BD_TIME_TO_MP(ti->duration); - return STREAM_OK; + if (ti) + *((double *) arg) = BD_TIME_TO_MP(ti->duration); + mp_mutex_unlock(&b->overlay_lock); + return ti ? STREAM_OK : STREAM_UNSUPPORTED; } case STREAM_CTRL_GET_CURRENT_TIME: { *((double *) arg) = BD_TIME_TO_MP(bd_tell_time(b->bd)); @@ -680,25 +694,31 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) return STREAM_OK; } case STREAM_CTRL_GET_NUM_ANGLES: { + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; - *((int *) arg) = ti->angle_count; - return STREAM_OK; + if (ti) + *((int *) arg) = ti->angle_count; + mp_mutex_unlock(&b->overlay_lock); + return ti ? STREAM_OK : STREAM_UNSUPPORTED; } case STREAM_CTRL_GET_ANGLE: { + mp_mutex_lock(&b->overlay_lock); *((int *) arg) = b->current_angle + 1; + mp_mutex_unlock(&b->overlay_lock); return STREAM_OK; } case STREAM_CTRL_SET_ANGLE: { - const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; int angle = *((int *) arg); - if (angle < 1 || angle > ti->angle_count) + mp_mutex_lock(&b->overlay_lock); + const BLURAY_TITLE_INFO *ti = b->title_info; + bool ok = ti && angle >= 1 && angle <= ti->angle_count; + if (ok) + b->current_angle = angle - 1; + int cur = b->current_angle; + mp_mutex_unlock(&b->overlay_lock); + if (!ok) return STREAM_UNSUPPORTED; - b->current_angle = angle - 1; - bd_seamless_angle_change(b->bd, b->current_angle); + bd_seamless_angle_change(b->bd, cur); return STREAM_OK; } case STREAM_CTRL_GET_TITLE_LENGTH: { @@ -724,6 +744,8 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) return STREAM_OK; } case STREAM_CTRL_GET_LANG: { + int rc = STREAM_ERROR; + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; if (ti && ti->clip_count) { struct stream_lang_req *req = arg; @@ -743,11 +765,13 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) BLURAY_STREAM_INFO *i = &si[n]; if (i->pid == req->id) { snprintf(req->name, sizeof(req->name), "%.4s", i->lang); - return STREAM_OK; + rc = STREAM_OK; + break; } } } - return STREAM_ERROR; + mp_mutex_unlock(&b->overlay_lock); + return rc; } case STREAM_CTRL_GET_DISC_NAME: { const struct meta_dl *meta = bd_get_meta(b->bd); @@ -811,6 +835,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) struct stream_nav_state *st = arg; int audio_pid = -1; int sub_pid = -1; + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; if (ti && ti->clip_count) { const BLURAY_CLIP_INFO *ci = &ti->clips[0]; @@ -836,9 +861,9 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) .angle = b->current_angle + 1, .num_angles = ti ? ti->angle_count : 0, }; + mp_mutex_unlock(&b->overlay_lock); return STREAM_OK; } - mp_mutex_lock(&b->overlay_lock); // BD_EVENT_MENU isn't fired by BD-J (it's HDMV-only), so treat any // visible BD-J plane as an active menu too. bool any_overlay = b->ig.visible || b->pg.visible; @@ -1000,6 +1025,8 @@ static int bluray_stream_open_internal(stream_t *s) b->opts_cache = opts_cache; b->opts = opts_cache->opts; + mp_mutex_init(&b->overlay_lock); + int ret = 0; char *device = NULL; /* find the requested device */ @@ -1093,7 +1120,6 @@ static int bluray_stream_open_internal(stream_t *s) MP_VERBOSE(s, "bdnav: cfg_title=%d hdmv_mode=%d\n", b->cfg_title, b->hdmv_mode); if (b->hdmv_mode) { - mp_mutex_init(&b->overlay_lock); bd_register_overlay_proc(bd, b, bd_yuv_overlay_cb); if (info->num_bdj_titles) bd_register_argb_overlay_proc(bd, b, bd_argb_overlay_cb, NULL); From 01d065efee4f5ce1398be05f684c9b0ff0303ca9 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 25/44] strema_dvdnav: add support for still frames This is mostly used for menus. --- player/core.h | 2 + player/discnav.c | 22 +++++++++++ player/playloop.c | 4 ++ stream/stream.h | 1 + stream/stream_dvdnav.c | 86 ++++++++++++++++++++++++++++++++++++------ 5 files changed, 104 insertions(+), 11 deletions(-) diff --git a/player/core.h b/player/core.h index 83b4cd1b327a8..2ebe91f9667db 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; + // Disc is parked on an infinite 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 4e63eba29a64b..3acb411d7e5ea 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). @@ -407,6 +411,11 @@ void disc_nav_update(struct MPContext *mpctx) struct stream *s = disc_nav_get_stream(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 (still != mpctx->disc_nav_still_frame) + MP_VERBOSE(mpctx, "discnav: still_frame %d->%d\n", + mpctx->disc_nav_still_frame, still); + mpctx->disc_nav_still_frame = still; if (!have) { struct disc_nav_state *st = mpctx->disc_nav; if (!st) @@ -419,6 +428,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; return; } @@ -438,6 +448,18 @@ void disc_nav_update(struct MPContext *mpctx) ensure_menu_sub_selection(mpctx, nav.menu_active); } + // The menu overlay updates independently of video. When it changes while + // the video isn't producing frames (held on a still, or paused) nothing + // would repaint it, so request an OSD redraw; handle_osd_redraw() honors it + // at EOF/paused and the next video frame consumes it during playback. (BD's + // osd_set_external2() already flags want_redraw, but covering both keeps the + // DVD highlight, which goes through the SPU path, in sync.) + 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(mpctx, "discnav: overlay %s\n", visible ? "on" : "off"); if (visible) { diff --git a/player/playloop.c b/player/playloop.c index f472e40969f17..0e74b09619c4c 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 290dea7b89b0a..cf8a10a12829d 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -131,6 +131,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 316ddf8886ae5..5b0baf2058ca3 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -65,6 +65,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; @@ -395,7 +396,43 @@ static void update_highlight(struct priv *priv) } } -static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) +static bool nav_action_activates(enum stream_nav_action a) +{ + switch (a) { + case STREAM_NAV_SELECT: + case STREAM_NAV_MOUSE_CLICK: + case STREAM_NAV_MENU_ROOT: + case STREAM_NAV_MENU_TITLE: + case STREAM_NAV_MENU_POPUP: + case STREAM_NAV_PREV_MENU: + return true; + default: + return false; + } +} + +// 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; @@ -429,16 +466,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); @@ -456,6 +487,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 = 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 @@ -549,8 +592,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); @@ -561,9 +608,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; @@ -843,6 +906,7 @@ static int control(stream_t *stream, int cmd, void *arg) .nav_active = true, .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], From c901f3ff5584340ee472e26fe13e30e8635f9f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 28 Jun 2026 18:33:44 +0200 Subject: [PATCH 26/44] strema_bluray: add support for still frames --- stream/stream_bluray.c | 50 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index 58789e391a6a3..dfa0e0df7d8a7 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -134,6 +134,7 @@ struct bluray_priv_s { uint32_t nav_change_id; // bumped on FLUSH/HIDE/MENU/POPUP events uint32_t discontinuity_id; // bumped on actions that may hop (SELECT...) bool data_delivered; // any byte returned from fill_buffer yet + bool still_active; // holding an indefinite still. // Disc-driven audio/sub selection, mirrored from BD_EVENT_AUDIO_STREAM // and BD_EVENT_PG_TEXTST{,_STREAM}. The numbers are 1-based libbluray @@ -463,13 +464,23 @@ static void handle_event(stream_t *s, const BD_EVENT *ev) } break; case BD_EVENT_STILL: - if (ev->param) - bd_read_skip_still(b->bd); + mp_mutex_lock(&b->overlay_lock); + b->still_active = ev->param != 0; + mp_mutex_unlock(&b->overlay_lock); break; case BD_EVENT_STILL_TIME: - bd_read_skip_still(b->bd); + if (ev->param == 0) { + mp_mutex_lock(&b->overlay_lock); + b->still_active = true; + mp_mutex_unlock(&b->overlay_lock); + } else { + bd_read_skip_still(b->bd); + } break; case BD_EVENT_END_OF_TITLE: + mp_mutex_lock(&b->overlay_lock); + b->still_active = false; + mp_mutex_unlock(&b->overlay_lock); break; case BD_EVENT_PLAYLIST: { int playlist = ev->param; @@ -574,6 +585,8 @@ static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len) // delivers one event per call, which we hand off to handle_event. while (bd_get_event(b->bd, &event)) handle_event(s, &event); + if (b->still_active) + return 0; int total = 0; int events_seen = 0; // Loop briefly to absorb event-only returns (where bd_read_ext @@ -596,8 +609,11 @@ static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len) } if (n > 0) { total += n; + b->still_active = false; break; } + if (b->still_active) + break; if (b->data_delivered && b->discontinuity_id != disc_before) break; if (mp_cancel_test(s->cancel)) @@ -616,6 +632,21 @@ static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len) return bd_read(b->bd, buf, len); } +static bool nav_action_activates(enum stream_nav_action a) +{ + switch (a) { + case STREAM_NAV_SELECT: + case STREAM_NAV_MOUSE_CLICK: + case STREAM_NAV_MENU_ROOT: + case STREAM_NAV_MENU_TITLE: + case STREAM_NAV_MENU_POPUP: + case STREAM_NAV_PREV_MENU: + return true; + default: + return false; + } +} + static int bluray_stream_control(stream_t *s, int cmd, void *arg) { struct bluray_priv_s *b = s->priv; @@ -805,7 +836,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) case STREAM_NAV_MENU_TITLE: // BD doesn't distinguish "title menu", both map to disc root. bd_menu_call(b->bd, -1); - return STREAM_OK; + break; case STREAM_NAV_MENU_POPUP: key = BD_VK_POPUP; break; @@ -819,7 +850,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) b->mouse_x = nav->x; b->mouse_y = nav->y; bd_mouse_select(b->bd, -1, nav->x, nav->y); - return STREAM_OK; + break; case STREAM_NAV_MOUSE_CLICK: b->mouse_x = nav->x; b->mouse_y = nav->y; @@ -829,6 +860,14 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) } if (key != BD_VK_NONE) bd_user_input(b->bd, -1, key); + // If an activation just ran a button command, it may have released a + // held still, bump discontinuity_id. + if (nav_action_activates(nav->action)) { + mp_mutex_lock(&b->overlay_lock); + if (b->still_active) + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + } return STREAM_OK; } case STREAM_CTRL_GET_NAV_STATE: { @@ -872,6 +911,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) .nav_active = true, .menu_active = visible, .has_popup = b->popup_supported, + .still_active = b->still_active, .src_w = MPMAX(b->ig.w, b->pg.w), .src_h = MPMAX(b->ig.h, b->pg.h), .change_id = b->nav_change_id, From 35dee94512ecbfefcec6e43f36dca647ee16ff6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 9 Jul 2026 07:40:50 +0200 Subject: [PATCH 27/44] stream_bluray: always use bd_read_ext in fill_buffer This is mostly cleanup after adding menu support, where _ext was added, but I also left old bd_read(), which made things unnecessary complex. Also remove constant event drain and do it only in idle mode. --- stream/stream_bluray.c | 352 ++++++++++++++++++++++------------------- 1 file changed, 190 insertions(+), 162 deletions(-) diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index dfa0e0df7d8a7..fd73f5162bdb0 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -48,7 +48,6 @@ #include "osdep/threads.h" #include "stream.h" #include "osdep/io.h" -#include "osdep/timer.h" #include "sub/osd.h" #include "sub/img_convert.h" #include "video/csputils.h" @@ -67,6 +66,11 @@ #define BD_TIME_TO_MP(x) ((x) / (double)(BD_TIMEBASE)) #define BD_TIME_FROM_MP(x) ((uint64_t)(x * BD_TIMEBASE)) +// Interval between read retries while navigation is idle +#define BLURAY_POLL_TIME_S 0.010 +// In menu mode, tolerate this many consecutive idle reads at the end of playlist +#define BLURAY_NAV_EOF_POLLS 100 + // copied from aacs.h in libaacs #define AACS_ERROR_CORRUPTED_DISC -1 /* opening or reading of AACS files failed */ #define AACS_ERROR_NO_CONFIG -2 /* missing config file */ @@ -448,188 +452,212 @@ static void bluray_stream_close(stream_t *s) mp_mutex_destroy(&priv->overlay_lock); } -static void handle_event(stream_t *s, const BD_EVENT *ev) +static const char *bd_event_str(uint32_t event) +{ +#if BLURAY_VERSION >= BLURAY_VERSION_CODE(1, 3, 0) + const char *name = bd_event_name(event); + if (name) + return name; +#endif + return "?"; +} + +static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len) { struct bluray_priv_s *b = s->priv; - if (b->hdmv_mode) - MP_VERBOSE(s, "bdnav: event %d param %u\n", ev->event, ev->param); - switch (ev->event) { - case BD_EVENT_MENU: - // ev->param: 1 if the disc is currently in an HDMV menu, 0 otherwise. - if (b->hdmv_mode) { + + uint32_t disc_id = b->discontinuity_id; + + int idle_reads = 0; + while (!mp_cancel_test(s->cancel)) { + BD_EVENT ev; + int n = bd_read_ext(b->bd, buf, len, &ev); + if (n < 0) { + MP_VERBOSE(s, "bd_read_ext() failed.\n"); + return -1; + } + + if (b->hdmv_mode && ev.event != BD_EVENT_NONE) + MP_DBG(s, "event %s(%u) param %u\n", bd_event_str(ev.event), ev.event, ev.param); + + bool stalled = false; // EOF unless the VM continues + bool bdj_idle = false; // BD-J title alive with no playlist playing + + switch (ev.event) { + case BD_EVENT_NONE: + stalled = n == 0; + break; + case BD_EVENT_ERROR: + MP_ERR(s, "Blu-ray navigation error (%u).\n", ev.param); + return -1; + case BD_EVENT_READ_ERROR: + MP_WARN(s, "Blu-ray read error, skipping unit.\n"); + break; + case BD_EVENT_END_OF_TITLE: mp_mutex_lock(&b->overlay_lock); - b->menu_event_active = ev->param != 0; - b->nav_change_id++; + b->still_active = false; mp_mutex_unlock(&b->overlay_lock); - } - break; - case BD_EVENT_STILL: - mp_mutex_lock(&b->overlay_lock); - b->still_active = ev->param != 0; - mp_mutex_unlock(&b->overlay_lock); - break; - case BD_EVENT_STILL_TIME: - if (ev->param == 0) { + stalled = true; + break; + case BD_EVENT_IDLE: + bdj_idle = true; + break; + case BD_EVENT_STILL: mp_mutex_lock(&b->overlay_lock); - b->still_active = true; + b->still_active = ev.param != 0; mp_mutex_unlock(&b->overlay_lock); - } else { - bd_read_skip_still(b->bd); - } - break; - case BD_EVENT_END_OF_TITLE: - mp_mutex_lock(&b->overlay_lock); - b->still_active = false; - mp_mutex_unlock(&b->overlay_lock); - break; - case BD_EVENT_PLAYLIST: { - int playlist = ev->param; - int title = bd_get_current_title(b->bd); - if (b->title_to_playlist) { - for (int i = 0; i < b->num_titles; i++) { - if (b->title_to_playlist[i] == (uint32_t)ev->param) { - title = i; - break; + break; + case BD_EVENT_STILL_TIME: + // TODO: consider timed stills support + // param == 0 is an indefinite still + if (ev.param == 0) { + mp_mutex_lock(&b->overlay_lock); + b->still_active = true; + mp_mutex_unlock(&b->overlay_lock); + } else { + bd_read_skip_still(b->bd); + } + break; + case BD_EVENT_MENU: + // ev.param: 1 if the disc is currently in an HDMV menu, 0 otherwise. + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->menu_event_active = ev.param != 0; + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } + break; + case BD_EVENT_POPUP: + // ev.param: 1 if popup menu is currently available, 0 otherwise. + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->popup_supported = ev.param != 0; + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } + break; + case BD_EVENT_PLAYLIST: { + int playlist = ev.param; + int title = bd_get_current_title(b->bd); + if (b->title_to_playlist) { + for (int i = 0; i < b->num_titles; i++) { + if (b->title_to_playlist[i] == ev.param) { + title = i; + break; + } } } + BLURAY_TITLE_INFO *ti = bd_get_playlist_info(b->bd, playlist, b->current_angle); + mp_mutex_lock(&b->overlay_lock); + if (b->title_info) + bd_free_title_info(b->title_info); + b->title_info = ti; + b->current_playlist = playlist; + b->current_title = title; + if (b->hdmv_mode) + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + break; } - BLURAY_TITLE_INFO *ti = bd_get_playlist_info(b->bd, playlist, b->current_angle); - mp_mutex_lock(&b->overlay_lock); - if (b->title_info) - bd_free_title_info(b->title_info); - b->title_info = ti; - b->current_playlist = playlist; - b->current_title = title; - if (b->hdmv_mode) - b->discontinuity_id++; - mp_mutex_unlock(&b->overlay_lock); - break; - } - case BD_EVENT_TITLE: { - int title = bd_get_current_title(b->bd); - mp_mutex_lock(&b->overlay_lock); - if (b->title_info) { - bd_free_title_info(b->title_info); - b->title_info = NULL; + case BD_EVENT_TITLE: { + int title = bd_get_current_title(b->bd); + mp_mutex_lock(&b->overlay_lock); + if (b->title_info) { + bd_free_title_info(b->title_info); + b->title_info = NULL; + } + b->current_title = title; + if (b->hdmv_mode) + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + break; } - b->current_title = title; - if (b->hdmv_mode) - b->discontinuity_id++; - mp_mutex_unlock(&b->overlay_lock); - break; - } - case BD_EVENT_ANGLE: { - int angle = ev->param; - BLURAY_TITLE_INFO *ti = b->title_info ? bd_get_playlist_info(b->bd, b->current_playlist, angle) - : NULL; - mp_mutex_lock(&b->overlay_lock); - b->current_angle = angle; - if (ti) { - bd_free_title_info(b->title_info); - b->title_info = ti; + case BD_EVENT_ANGLE: { + int angle = ev.param; + BLURAY_TITLE_INFO *ti = b->title_info ? bd_get_playlist_info(b->bd, b->current_playlist, angle) + : NULL; + mp_mutex_lock(&b->overlay_lock); + b->current_angle = angle; + if (ti) { + bd_free_title_info(b->title_info); + b->title_info = ti; + } + if (b->hdmv_mode) + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + break; } - if (b->hdmv_mode) - b->nav_change_id++; - mp_mutex_unlock(&b->overlay_lock); - break; - } - case BD_EVENT_AUDIO_STREAM: - mp_mutex_lock(&b->overlay_lock); - b->audio_stream_num = ev->param; - if (b->hdmv_mode) - b->nav_change_id++; - mp_mutex_unlock(&b->overlay_lock); - break; - case BD_EVENT_PG_TEXTST_STREAM: - mp_mutex_lock(&b->overlay_lock); - b->sub_stream_num = ev->param; - if (b->hdmv_mode) - b->nav_change_id++; - mp_mutex_unlock(&b->overlay_lock); - break; - case BD_EVENT_PG_TEXTST: - mp_mutex_lock(&b->overlay_lock); - b->sub_visible = ev->param != 0; - if (b->hdmv_mode) - b->nav_change_id++; - mp_mutex_unlock(&b->overlay_lock); - break; - case BD_EVENT_POPUP: - // ev->param: 1 if popup menu is currently available, 0 otherwise. - if (b->hdmv_mode) { + case BD_EVENT_AUDIO_STREAM: + mp_mutex_lock(&b->overlay_lock); + b->audio_stream_num = ev.param; + if (b->hdmv_mode) + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + break; + case BD_EVENT_PG_TEXTST_STREAM: + mp_mutex_lock(&b->overlay_lock); + b->sub_stream_num = ev.param; + if (b->hdmv_mode) + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + break; + case BD_EVENT_PG_TEXTST: mp_mutex_lock(&b->overlay_lock); - b->popup_supported = ev->param != 0; - b->nav_change_id++; + b->sub_visible = ev.param != 0; + if (b->hdmv_mode) + b->nav_change_id++; mp_mutex_unlock(&b->overlay_lock); + break; + case BD_EVENT_DISCONTINUITY: + break; + default: + MP_TRACE(s, "Unhandled event: %s(%u) %u\n", + bd_event_str(ev.event), ev.event, ev.param); + break; } - break; -#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 5, 0) - case BD_EVENT_DISCONTINUITY: - break; -#endif - default: - MP_TRACE(s, "Unhandled event: %d %d\n", ev->event, ev->param); - break; - } -} -static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len) -{ - struct bluray_priv_s *b = s->priv; - BD_EVENT event; + if (n > 0) { + if (b->still_active) { + mp_mutex_lock(&b->overlay_lock); + b->still_active = false; + mp_mutex_unlock(&b->overlay_lock); + } + b->data_delivered = true; + return n; + } - if (b->hdmv_mode) { - // bd_read() doesn't drive the HDMV VM, so the disc's first-play - // bytecode would never run and we'd be stuck with "no valid title" - // forever. bd_read_ext() runs the VM between event drains and also - // delivers one event per call, which we hand off to handle_event. - while (bd_get_event(b->bd, &event)) - handle_event(s, &event); + // Holding an indefinite still frame: report EOF. The player keeps + // showing the last frame; user interaction releases the still and + // resumes reading through a discontinuity_id bump. if (b->still_active) return 0; - int total = 0; - int events_seen = 0; - // Loop briefly to absorb event-only returns (where bd_read_ext - // returns 0 with a freshly produced event) before reporting EOF. - // If an event bumps discontinuity_id (PLAYLIST/TITLE) *after* we - // have already delivered data to the slave demuxer, stop here even - // if no data was read: the next bd_read_ext would deliver data from - // the new playlist, but the slave must be reopened first so it - // parses with the correct codec context. - for (int i = 0; i < 200; i++) { - uint32_t disc_before = b->discontinuity_id; - int n = bd_read_ext(b->bd, (uint8_t *)buf + total, len - total, &event); - if (n < 0) { - MP_VERBOSE(s, "bdnav: bd_read_ext err iter=%d\n", i); - return -1; - } - if (event.event != BD_EVENT_NONE) { - handle_event(s, &event); - events_seen++; - } - if (n > 0) { - total += n; - b->still_active = false; - break; - } - if (b->still_active) - break; - if (b->data_delivered && b->discontinuity_id != disc_before) - break; - if (mp_cancel_test(s->cancel)) + // The play position jumped to another title/playlist after data was + // already delivered: report EOF so demux_disc reopens the slave + // demuxer before it parses data from the new playlist. + if (b->data_delivered && b->discontinuity_id != disc_id) + return 0; + + if (bdj_idle) { + idle_reads = 0; + mp_cancel_wait(s->cancel, BLURAY_POLL_TIME_S); + } else if (stalled) { + // Without menus there is nothing that could continue: plain EOF. + if (!b->hdmv_mode) return 0; - mp_sleep_ns(MP_TIME_MS_TO_NS(5)); + // Retry for a while before concluding that playback ended. The + // first retry is immediate: a just-resumed VM progresses at once. + if (++idle_reads > BLURAY_NAV_EOF_POLLS) { + MP_VERBOSE(s, "Navigation stopped, EOF.\n"); + return 0; + } + if (idle_reads > 1) + mp_cancel_wait(s->cancel, BLURAY_POLL_TIME_S); + } else { + // The VM/title made progress; read again immediately. + idle_reads = 0; } - if (total > 0) - b->data_delivered = true; - if (total == 0) - MP_VERBOSE(s, "bdnav: fill returned 0 (events=%d)\n", events_seen); - return total; } - - while (bd_get_event(b->bd, &event)) - handle_event(s, &event); - return bd_read(b->bd, buf, len); + return 0; } static bool nav_action_activates(enum stream_nav_action a) From e71bf70922b967512509a1adca0d5e3d890ed7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 15:33:20 +0200 Subject: [PATCH 28/44] demux_lavf: clear sticky avio EOF in drop_buffers A stream can report a one-shot EOF and then continue producing data (disc-nav jump boundaries). --- demux/demux_lavf.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index 9fb475475efd5..8570690ef5b7c 100644 --- a/demux/demux_lavf.c +++ b/demux/demux_lavf.c @@ -1817,6 +1817,11 @@ static void demux_drop_buffers_lavf(demuxer_t *demuxer) stream_drop_buffers(priv->stream); avio_flush(priv->avfc->pb); avformat_flush(priv->avfc); + // Clear sticky EOF/error to reuse this demuxer. + if (priv->avfc->pb) { + priv->avfc->pb->eof_reached = 0; + priv->avfc->pb->error = 0; + } reset_dovi_split_state(demuxer); } From a0bb6a07ede85be85e4c88320584065234bb4963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 15:33:43 +0200 Subject: [PATCH 29/44] stream_dvdnav: emit a one-shot EOF at source jump boundaries Menu activations, HOP_CHANNEL and VTS changes jump the source position. On this strictly forward stream the demuxer cannot tell where the old content ends, so buffered pre-jump data would mix with post-jump data. Return a single EOF at the boundary so the demuxer drains and resets before any new content flows. --- stream/stream_dvdnav.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index 5b0baf2058ca3..46a09fa29a99d 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -81,6 +81,7 @@ struct priv { 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 + bool pending_drain; // emit one EOF at the next jump boundary int src_w, src_h; // video resolution in pixels int auto_actioned_button; // last auto-activated button; 0 if none @@ -487,6 +488,13 @@ static void do_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) update_highlight(priv); } +// Mark a source-position jump. +static void bump_discontinuity(struct priv *priv) +{ + priv->discontinuity_id++; + priv->pending_drain = true; +} + static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) { struct priv *priv = stream->priv; @@ -496,7 +504,7 @@ static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) bool activated = nav_action_activates(cmd->action) || priv->auto_actioned_button != prev_auto; if (priv->still_active && activated) - priv->discontinuity_id++; + bump_discontinuity(priv); } /** @@ -578,6 +586,10 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) } while (1) { + if (priv->pending_drain) { + priv->pending_drain = false; + return 0; + } int len = -1; int event = DVDNAV_NOP; if (dvdnav_get_next_block(dvdnav, buf, &event, &len) != DVDNAV_STATUS_OK) @@ -632,7 +644,7 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) break; case DVDNAV_HOP_CHANNEL: // Bump discontinuity_id so the playloop flushes the cache. - priv->discontinuity_id++; + bump_discontinuity(priv); break; case DVDNAV_HIGHLIGHT: update_highlight(priv); @@ -660,7 +672,7 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) // so mouse coordinate translation stays correct. refresh_video_resolution(priv); // VTS change is a title-set boundary, flush. - priv->discontinuity_id++; + bump_discontinuity(priv); break; } case DVDNAV_CELL_CHANGE: { @@ -800,13 +812,13 @@ static int control(stream_t *stream, int cmd, void *arg) if (dvdnav_menu_call(priv->dvdnav, DVD_MENU_Root) != DVDNAV_STATUS_OK) break; - priv->discontinuity_id++; + bump_discontinuity(priv); stream_drop_buffers(stream); return STREAM_OK; } if (dvdnav_title_play(priv->dvdnav, title + 1) != DVDNAV_STATUS_OK) break; - priv->discontinuity_id++; + bump_discontinuity(priv); stream_drop_buffers(stream); return STREAM_OK; } From 6c9d6e504d074ddc4c1556ae3d941bfce7e0dae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 15:35:12 +0200 Subject: [PATCH 30/44] demux_disc: soft-reload the slave demuxer on DVD discontinuities A full slave reopen makes libavformat re-probe the stream, which eats packets around the jump (losing the one-shot menu SPU) and re-bases the timestamps. DVD cannot change codecs across a jump, so flushing the slave and refreshing duration/chapters/edition is enough. BD keeps the full reopen because codecs can change across titles. --- demux/demux_disc.c | 73 +++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/demux/demux_disc.c b/demux/demux_disc.c index 55a2bc70afc99..c0941cb07c198 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -322,6 +322,22 @@ static void sync_initial_edition(struct demuxer *demuxer) } } +static void refresh_disc_metadata(struct demuxer *demuxer) +{ + double len; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1) + demux_set_duration(demuxer, len); + else + demux_set_duration(demuxer, -1); + + // Old chapter metadata is not freed here, they are parented to demuxer. + demuxer->chapters = NULL; + demuxer->num_chapters = 0; + add_stream_chapters(demuxer); + sync_initial_edition(demuxer); + demux_lists_changed(demuxer); +} + static bool reopen_slave(struct demuxer *demuxer) { struct priv *p = demuxer->priv; @@ -351,23 +367,27 @@ static bool reopen_slave(struct demuxer *demuxer) } sync_streams(demuxer); + refresh_disc_metadata(demuxer); - // Refresh duration / chapters / edition for the new playlist. - double len; - if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1) - demux_set_duration(demuxer, len); - else - demux_set_duration(demuxer, -1); - - for (int n = 0; n < demuxer->num_chapters; n++) - talloc_free(demuxer->chapters[n].metadata); - demuxer->num_chapters = 0; - add_stream_chapters(demuxer); - - sync_initial_edition(demuxer); + return true; +} - demux_lists_changed(demuxer); +// Handle a disc-nav discontinuity (title/menu/cell jump) +static bool process_discontinuity(struct demuxer *demuxer, uint32_t new_id) +{ + struct priv *p = demuxer->priv; + if (!p->is_dvd) { + // BD needs a full reopen (codecs can change across titles). + if (!reopen_slave(demuxer)) + return false; + } else { + if (p->slave->desc->drop_buffers) + p->slave->desc->drop_buffers(p->slave); + refresh_disc_metadata(demuxer); + } + p->last_discontinuity_id = new_id; + p->seek_reinit = true; return true; } @@ -381,31 +401,26 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt p->nav_active = nav.nav_active; demux_set_nav_active(demuxer, nav.nav_active); } - if (nav.discontinuity_id != p->last_discontinuity_id) { - MP_VERBOSE(demuxer, "discontinuity %u->%u, reopening slave\n", + if (!p->is_dvd && nav.discontinuity_id != p->last_discontinuity_id) { + MP_VERBOSE(demuxer, "discontinuity %u->%u, handling\n", p->last_discontinuity_id, nav.discontinuity_id); - if (!reopen_slave(demuxer)) + if (!process_discontinuity(demuxer, nav.discontinuity_id)) return false; - if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) - p->last_discontinuity_id = nav.discontinuity_id; - p->seek_reinit = true; } } struct demux_packet *pkt = demux_read_any_packet(p->slave); if (!pkt) { - // The slave can hit EOF mid-playback when the stream layer breaks - // its read at a disc-driven discontinuity. + // EOF is either a real still (hold the frame) or the one-shot drain + // EOF at a jump boundary; in the latter case resync and continue. struct stream_nav_state nav2 = {0}; if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav2) >= 1 && nav2.discontinuity_id != p->last_discontinuity_id) { - MP_VERBOSE(demuxer, "discontinuity %u->%u at EOF, reopening slave\n", + MP_VERBOSE(demuxer, "discontinuity %u->%u at EOF, handling\n", p->last_discontinuity_id, nav2.discontinuity_id); - if (!reopen_slave(demuxer)) + if (!process_discontinuity(demuxer, nav2.discontinuity_id)) return false; - p->last_discontinuity_id = nav2.discontinuity_id; - p->seek_reinit = true; pkt = demux_read_any_packet(p->slave); } if (!pkt) @@ -414,8 +429,12 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt demux_update(p->slave, MP_NOPTS_VALUE); - if (p->seek_reinit) + if (p->seek_reinit) { reset_pts(demuxer); + double len; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1) + demux_set_duration(demuxer, len); + } int slave_index = pkt->stream; if (demux_get_num_stream(p->slave) > p->slave_to_outer_count || From 61a0343575fc11f6f1bd8d27fd6c75b0256987d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 15:36:13 +0200 Subject: [PATCH 31/44] demux: parent chapter metadata to the chapter list --- demux/demux.c | 1 + 1 file changed, 1 insertion(+) diff --git a/demux/demux.c b/demux/demux.c index dc1a78cba493b..ef901ba3b5292 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -4322,6 +4322,7 @@ int demuxer_add_chapter(demuxer_t *demuxer, char *name, }; mp_tags_set_str(new.metadata, "TITLE", name); MP_TARRAY_APPEND(demuxer, demuxer->chapters, demuxer->num_chapters, new); + talloc_steal(demuxer->chapters, new.metadata); return demuxer->num_chapters - 1; } From 0d77b11aea8da66a050f1f517f6f8414fb320192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 15:38:06 +0200 Subject: [PATCH 32/44] sd_lavc: treat the DVD menu subpicture as persistent display state A menu subpicture is not timed subtitle data: its display command keeps it on screen until it is stopped or replaced, independent of the playback time, which is frozen on stills and rebased across menu jumps. --- sub/dec_sub.h | 3 ++- sub/sd_lavc.c | 34 +++++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/sub/dec_sub.h b/sub/dec_sub.h index b236aa9da1941..c7b1e2f3b5020 100644 --- a/sub/dec_sub.h +++ b/sub/dec_sub.h @@ -25,7 +25,8 @@ enum sd_ctrl { }; struct mp_dvdnav_hli { - bool show; + bool show; // render the button highlight + bool menu_active; // menu subpicture is persistent display state int x, y, w, h; // button rect in SPU/source coords uint32_t palette[4]; // 0xAARRGGBB, straight alpha uint32_t change_id; // bumped on any visible change diff --git a/sub/sd_lavc.c b/sub/sd_lavc.c index eec2f1ef2c3ae..b5277a597791b 100644 --- a/sub/sd_lavc.c +++ b/sub/sd_lavc.c @@ -460,6 +460,16 @@ static void decode(struct sd *sd, struct demux_packet *packet) static struct sub *get_current(struct sd_lavc_priv *priv, double pts) { + // A menu subpicture is persistent display state, not bound to playback + // time. + if (priv->hli.menu_active) { + for (int n = 0; n < MAX_QUEUE; n++) { + if (priv->subs[n].valid) + return &priv->subs[n]; + } + return NULL; + } + struct sub *current = NULL; for (int n = 0; n < MAX_QUEUE; n++) { struct sub *sub = &priv->subs[n]; @@ -631,6 +641,10 @@ static bool accepts_packet(struct sd *sd, double min_pts) { struct sd_lavc_priv *priv = sd->priv; + // Menu subpictures are not pts-ordered; never block delivery. + if (priv->hli.menu_active) + return true; + double pts = priv->current_pts; if (min_pts != MP_NOPTS_VALUE) { // guard against bogus rendering PTS in the future. @@ -663,8 +677,12 @@ static void reset(struct sd *sd) { struct sd_lavc_priv *priv = sd->priv; - for (int n = 0; n < MAX_QUEUE; n++) - clear_sub(&priv->subs[n]); + // Keep the menu subpicture; timeline resets don't invalidate it and the + // disc won't re-send it. Purged when the menu closes. + if (!priv->hli.menu_active) { + for (int n = 0; n < MAX_QUEUE; n++) + clear_sub(&priv->subs[n]); + } // lavc might not do this right for all codecs; may need close+reopen avcodec_flush_buffers(priv->avctx); @@ -759,12 +777,18 @@ static int control(struct sd *sd, enum sd_ctrl cmd, void *arg) return CONTROL_OK; case SD_CTRL_APPLY_DVDNAV: { struct mp_dvdnav_hli *hli = arg; - if (priv->hli_change_id == hli->change_id) - return CONTROL_OK; + bool menu_closed = priv->hli.menu_active && !hli->menu_active; + bool changed = priv->hli_change_id != hli->change_id; priv->hli = *hli; priv->hli_change_id = hli->change_id; + // Don't let menu subpictures leak into title playback. + if (menu_closed) { + for (int n = 0; n < MAX_QUEUE; n++) + clear_sub(&priv->subs[n]); + } // Re-render any decoded subtitles, after style update. - rerender_queued_subs(sd); + if (changed) + rerender_queued_subs(sd); return CONTROL_OK; } default: From 354509cf315cda74c8d97f608ec95ff1b8c39c9e 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 33/44] demux_disc: retain and re-deliver DVD menu subpictures The menu SPU is a single packet on a stream that cannot be re-read. If the sub stream is not selected when it flows by, or the player flushes it before the decoder consumed it, the menu stays blank forever. Retain the last SPU per substream and re-deliver it when the stream is (re)selected. This arguably is getting bit of hacky mess, but the alternative would be to either not use double demuxer or use separate subtitle decoder. Both those solutions carry a lot of code duplication for demuxing and decoding. So we try to plug things togheter in current design. It still not perfect, but works well enough to keep the highlights from disapearing in some corner cases. --- demux/demux.c | 32 ++++++++++++++++++ demux/demux.h | 3 ++ demux/demux_disc.c | 81 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/demux/demux.c b/demux/demux.c index ef901ba3b5292..4c698d69d067f 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -247,6 +247,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 bool seeking; // there's a seek queued int seek_flags; // flags for next seek (if seeking==true) @@ -1264,6 +1265,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) { @@ -2543,6 +2559,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; @@ -2659,6 +2687,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 eb53cd711dd4d..ca6ad70464ff9 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); @@ -322,6 +324,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); diff --git a/demux/demux_disc.c b/demux/demux_disc.c index c0941cb07c198..940b47849d5e7 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" @@ -58,6 +59,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 @@ -69,6 +79,24 @@ struct priv { bool is_dvd, is_cdda; }; +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; + } +} + static void reselect_streams(demuxer_t *demuxer) { struct priv *p = demuxer->priv; @@ -80,6 +108,13 @@ static void reselect_streams(demuxer_t *demuxer) MP_NOPTS_VALUE, demux_stream_is_selected(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) @@ -285,6 +320,8 @@ static void d_seek(demuxer_t *demuxer, double seek_pts, int flags) if (p->slave->desc->drop_buffers) p->slave->desc->drop_buffers(p->slave); + clear_dvd_sub_holds(p); + p->seek_reinit = true; } @@ -352,6 +389,7 @@ static bool reopen_slave(struct demuxer *demuxer) params.force_format = "+rawaudio"; demux_free(p->slave); + clear_dvd_sub_holds(p); // Discard anything the stream wrapper buffered before the disc-nav // discontinuity. stream_drop_buffers(demuxer->stream); @@ -382,6 +420,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); @@ -395,8 +434,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); @@ -409,6 +450,24 @@ 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; + rp->stream = h->sh->index; + *out_pkt = rp; + return true; + } + } + struct demux_packet *pkt = demux_read_any_packet(p->slave); if (!pkt) { // EOF is either a real still (hold the frame) or the one-shot drain @@ -446,7 +505,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; } @@ -505,6 +565,23 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt MP_TRACE(demuxer, "opts: %d %f %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); + if (h->pkt) + talloc_steal(p, h->pkt); + h->sh = sh; + h->pending = false; + } + if (!demux_stream_is_selected(sh)) { + talloc_free(pkt); + return true; + } + } + *out_pkt = pkt; return 1; } @@ -622,6 +699,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); } @@ -633,4 +711,5 @@ const demuxer_desc_t demuxer_desc_disc = { .close = d_close, .seek = d_seek, .switched_tracks = reselect_streams, + .nav_refresh = d_nav_refresh, }; From a5efa837089f2549f9fdbcc97ab2ee5c44441629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 15:46:06 +0200 Subject: [PATCH 34/44] discnav: rework DVD menu subpicture handling Push menu_active to the dvd_subtitle decoders so the menu subpicture is treated as persistent display state, and always pass the real change_id so highlight removal re-renders too. Re-queue the retained menu SPU after the discontinuity flush, closing the race where the playback reset destroyed the subpicture between demuxer and decoder. --- player/discnav.c | 62 +++++++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/player/discnav.c b/player/discnav.c index 3acb411d7e5ea..8053a37d8c59c 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; @@ -72,6 +71,13 @@ struct disc_nav_state { uint32_t last_track_disc_id; }; +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) @@ -127,12 +133,14 @@ 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, + .menu_active = nav->menu_active, + .change_id = nav->change_id, }; if (visible) { hli.x = nav->hl_x; @@ -143,9 +151,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); } @@ -266,6 +272,8 @@ static void check_async_discontinuity(struct MPContext *mpctx, 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, @@ -362,38 +370,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 { @@ -401,8 +416,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; } } @@ -428,6 +444,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; return; } @@ -450,10 +467,7 @@ void disc_nav_update(struct MPContext *mpctx) // The menu overlay updates independently of video. When it changes while // the video isn't producing frames (held on a still, or paused) nothing - // would repaint it, so request an OSD redraw; handle_osd_redraw() honors it - // at EOF/paused and the next video frame consumes it during playback. (BD's - // osd_set_external2() already flags want_redraw, but covering both keeps the - // DVD highlight, which goes through the SPU path, in sync.) + // would repaint it, so request an OSD redraw. 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; From d8bbf267c88b9a99b5508978c6e3a893c19f3050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 19:35:35 +0200 Subject: [PATCH 35/44] demux_disc: only add the menu edition for streams with menu support --- demux/demux_disc.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/demux/demux_disc.c b/demux/demux_disc.c index 940b47849d5e7..4980f08783c31 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -612,7 +612,10 @@ static void add_stream_editions(struct demuxer *demuxer) talloc_free(time); } - // Append a synthetic "Disc Menu" entry. + // Append a synthetic "Disc Menu" entry, if the disc has menu support. + struct stream_nav_state nav = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) < 1) + return; struct demux_edition menu = { .demuxer_id = titles, .default_edition = false, From efc5dfd8c0937307de3469f6fc72610db35df3b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sun, 5 Jul 2026 16:12:40 +0200 Subject: [PATCH 36/44] demux_lavf: seek avfc only on seekable streams in drop_buffers This avoids bogus errors when using demux_disc which is seekable by in higher layer only. --- demux/demux_lavf.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index 8570690ef5b7c..b878fad42f6db 100644 --- a/demux/demux_lavf.c +++ b/demux/demux_lavf.c @@ -1812,7 +1812,8 @@ static void reset_dovi_split_state(demuxer_t *demuxer) static void demux_drop_buffers_lavf(demuxer_t *demuxer) { lavf_priv_t *priv = demuxer->priv; - av_seek_frame(priv->avfc, -1, 0, 1); + if (!priv->stream || priv->stream->seekable) + av_seek_frame(priv->avfc, -1, 0, 1); demux_flush(demuxer); stream_drop_buffers(priv->stream); avio_flush(priv->avfc->pb); From dd26b3e51ac7a5a0c128387161ce6d7bf95e86bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 9 Jul 2026 20:34:47 +0200 Subject: [PATCH 37/44] stream_bluray: rename BD_TIME_TO_MP to BD_TIME_TO_S --- stream/stream_bluray.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index fd73f5162bdb0..abc08fef98a1a 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -63,8 +63,8 @@ // 90khz ticks #define BD_TIMEBASE (90000) -#define BD_TIME_TO_MP(x) ((x) / (double)(BD_TIMEBASE)) -#define BD_TIME_FROM_MP(x) ((uint64_t)(x * BD_TIMEBASE)) +#define BD_TIME_TO_S(x) ((x) / (double)(BD_TIMEBASE)) +#define BD_TIME_FROM_S(x) ((uint64_t)(x * BD_TIMEBASE)) // Interval between read retries while navigation is idle #define BLURAY_POLL_TIME_S 0.010 @@ -696,7 +696,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) if (ti) { double time = MP_NOPTS_VALUE; if (chapter >= 0 && chapter < ti->chapter_count) - time = BD_TIME_TO_MP(ti->chapters[chapter].start); + time = BD_TIME_TO_S(ti->chapters[chapter].start); if (time != MP_NOPTS_VALUE) { *(double *)arg = time; rc = STREAM_OK; @@ -737,17 +737,17 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; if (ti) - *((double *) arg) = BD_TIME_TO_MP(ti->duration); + *((double *) arg) = BD_TIME_TO_S(ti->duration); mp_mutex_unlock(&b->overlay_lock); return ti ? STREAM_OK : STREAM_UNSUPPORTED; } case STREAM_CTRL_GET_CURRENT_TIME: { - *((double *) arg) = BD_TIME_TO_MP(bd_tell_time(b->bd)); + *((double *) arg) = BD_TIME_TO_S(bd_tell_time(b->bd)); return STREAM_OK; } case STREAM_CTRL_SEEK_TO_TIME: { double pts = *((double *) arg); - bd_seek_time(b->bd, BD_TIME_FROM_MP(pts)); + bd_seek_time(b->bd, BD_TIME_FROM_S(pts)); stream_drop_buffers(s); // API makes it hard to determine seeking success return STREAM_OK; @@ -787,7 +787,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) BLURAY_TITLE_INFO *ti = bd_get_title_info(b->bd, title, 0); if (!ti) return STREAM_UNSUPPORTED; - *(double *)arg = BD_TIME_TO_MP(ti->duration); + *(double *)arg = BD_TIME_TO_S(ti->duration); bd_free_title_info(ti); return STREAM_OK; } From 654fa430f37bf69efacc84a0d9588ae39a88c63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 9 Jul 2026 20:39:39 +0200 Subject: [PATCH 38/44] stream_dvdnav: keep cached duration in native time base --- stream/stream_dvdnav.c | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index 46a09fa29a99d..0c3116a6721dd 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -56,6 +56,10 @@ #define TITLE_MENU -1 #define TITLE_LONGEST -2 +#define DVD_TIMEBASE 90000 +#define DVD_TIME_TO_S(x) ((x) / (double)(DVD_TIMEBASE)) +#define DVD_TIME_FROM_S(x) ((int64_t)((x) * DVD_TIMEBASE)) + // Default source dimensions if dvdnav_get_video_resolution() fails. #define DVD_SRC_W_DEFAULT 720 #define DVD_SRC_H_DEFAULT 576 @@ -63,7 +67,7 @@ struct priv { dvdnav_t *dvdnav; // handle to libdvdnav stuff char *filename; // path - unsigned int duration; // in milliseconds + int64_t duration; // in 90 kHz PTS ticks int title; bool still_active; // fill_buffer() is holding a still uint32_t spu_clut[16]; @@ -679,7 +683,7 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) dvdnav_cell_change_event_t *ev = (dvdnav_cell_change_event_t *)buf; if (ev->pgc_length) - priv->duration = ev->pgc_length / 90; + priv->duration = ev->pgc_length; break; } @@ -749,13 +753,13 @@ static int control(stream_t *stream, int cmd, void *arg) free(parts); break; } - *ch = chapter > 0 ? parts[chapter - 1] / 90000.0 : 0; + *ch = chapter > 0 ? DVD_TIME_TO_S(parts[chapter - 1]) : 0; free(parts); return STREAM_OK; } case STREAM_CTRL_GET_TIME_LENGTH: { - if (priv->duration) { - *(double *)arg = (double)priv->duration / 1000.0; + if (priv->duration > 0) { + *(double *)arg = DVD_TIME_TO_S(priv->duration); return STREAM_OK; } break; @@ -766,10 +770,9 @@ static int control(stream_t *stream, int cmd, void *arg) return STREAM_OK; } case STREAM_CTRL_GET_CURRENT_TIME: { - double tm; - tm = dvdnav_get_current_time(dvdnav) / 90000.0f; + int64_t tm = dvdnav_get_current_time(dvdnav); if (tm != -1) { - *(double *)arg = tm; + *(double *)arg = DVD_TIME_TO_S(tm); return STREAM_OK; } break; @@ -794,7 +797,7 @@ static int control(stream_t *stream, int cmd, void *arg) if (!parts) break; free(parts); - *(double *)arg = duration / 90000.0; + *(double *)arg = DVD_TIME_TO_S(duration); return STREAM_OK; } case STREAM_CTRL_GET_CURRENT_TITLE: { @@ -828,11 +831,11 @@ static int control(stream_t *stream, int cmd, void *arg) int flags = args[1]; // from SEEK_* flags (demux.h) if (flags & SEEK_HR) d -= 10; // fudge offset; it's a hack, because fuck libdvd* - int64_t tm = (int64_t)(d * 90000); + int64_t tm = DVD_TIME_FROM_S(d); if (tm < 0) tm = 0; - if (priv->duration && tm >= (int64_t)priv->duration * 90) - tm = (int64_t)priv->duration * 90 - 1; + if (priv->duration > 0 && tm >= priv->duration) + tm = priv->duration - 1; uint32_t pos, len; if (dvdnav_get_position(dvdnav, &pos, &len) != DVDNAV_STATUS_OK) break; @@ -840,7 +843,7 @@ static int control(stream_t *stream, int cmd, void *arg) if (dvdnav_time_search(dvdnav, tm) != DVDNAV_STATUS_OK) break; stream_drop_buffers(stream); - d = dvdnav_get_current_time(dvdnav) / 90000.0f; + d = DVD_TIME_TO_S(dvdnav_get_current_time(dvdnav)); MP_VERBOSE(stream, "landed at: %f\n", d); if (dvdnav_get_position(dvdnav, &pos, &len) == DVDNAV_STATUS_OK) MP_VERBOSE(stream, "block: %lu\n", (unsigned long)pos); From 739511227001dc758a0ff603e3203aec2ca89fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 9 Jul 2026 21:16:26 +0200 Subject: [PATCH 39/44] stream_dvdnav: route logs from libdvdnav through mp_log --- stream/stream_dvdnav.c | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index 0c3116a6721dd..ddca976f5ee51 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -954,6 +954,36 @@ static void stream_dvdnav_close(stream_t *s) dvd_set_speed(s, priv->filename, -1); } +#if DVDNAV_VERSION >= DVDNAV_VERSION_CODE(6, 1, 0) +static void dvdnav_log(void *priv, dvdnav_logger_level_t level, + const char *fmt, va_list va) +{ + int lvl; + switch (level) { + case DVDNAV_LOGGER_LEVEL_ERROR: lvl = MSGL_ERR; break; + case DVDNAV_LOGGER_LEVEL_WARN: lvl = MSGL_WARN; break; + case DVDNAV_LOGGER_LEVEL_DEBUG: lvl = MSGL_DEBUG; break; + case DVDNAV_LOGGER_LEVEL_INFO: + default: lvl = MSGL_V; break; + } + if (!mp_msg_test(priv, lvl)) + return; + mp_msg_va(priv, lvl, fmt, va); + mp_msg(priv, lvl, "\n"); +} +#endif + +static dvdnav_status_t nav_open(stream_t *stream, dvdnav_t **dest, const char *path) +{ +#if DVDNAV_VERSION >= DVDNAV_VERSION_CODE(6, 1, 0) + struct mp_log *log = mp_log_new(stream, stream->log, "/libdvdnav"); + const dvdnav_logger_cb logger_cb = { .pf_log = dvdnav_log }; + return dvdnav_open2(dest, log, &logger_cb, path); +#else + return dvdnav_open(dest, path); +#endif +} + static struct priv *new_dvdnav_stream(stream_t *stream, char *filename) { struct priv *priv = stream->priv; @@ -968,7 +998,7 @@ static struct priv *new_dvdnav_stream(stream_t *stream, char *filename) priv->dvd_speed = priv->opts->speed; dvd_set_speed(stream, priv->filename, priv->dvd_speed); - if (dvdnav_open(&(priv->dvdnav), priv->filename) != DVDNAV_STATUS_OK) + if (nav_open(stream, &priv->dvdnav, priv->filename) != DVDNAV_STATUS_OK) return NULL; if (!priv->dvdnav) From e4a513fd090b41b08cff7f5e0cc41005429bb42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Thu, 9 Jul 2026 23:54:34 +0200 Subject: [PATCH 40/44] stream_bluray: route logs through mp_log This is awkward, because libbluray have single global log callback. libmpv can possibly spawn multiple players in single application, so this will conflict. The instance created last will override the log callback. This is similar to have ffmpeg logging works. In BD-J case this is bit spammy with some bogus java errors, even with mask DBG_CRIT. Allow logging only if our log level is > DEBUG. Since the log callback gives us only a string, not even log severity, we just print them as MSGL_DEBUG, and set mask according to our log level. --- stream/stream_bluray.c | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index abc08fef98a1a..59c0a8732ac1b 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -95,6 +95,18 @@ const struct m_sub_options stream_bluray_conf = { }, }; +// libbluray's support only global debug callback, without per-instance info. +static mp_static_mutex bluray_log_lock = MP_STATIC_MUTEX_INITIALIZER; +static struct mp_log *bluray_log; + +static void bluray_logger(const char *msg) +{ + mp_mutex_lock(&bluray_log_lock); + if (bluray_log) + mp_msg(bluray_log, MSGL_DEBUG, "%s", msg); + mp_mutex_unlock(&bluray_log_lock); +} + // One overlay plane (BGRA, premultiplied alpha). struct bd_overlay_plane { uint32_t *work; @@ -105,6 +117,7 @@ struct bd_overlay_plane { struct bluray_priv_s { BLURAY *bd; + struct mp_log *bluray_log; BLURAY_TITLE_INFO *title_info; int num_titles; int current_angle; @@ -449,6 +462,11 @@ static void bluray_stream_close(stream_t *s) } bd_close(priv->bd); } + mp_mutex_lock(&bluray_log_lock); + // If we created the global log, unset it. + if (bluray_log == priv->bluray_log) + bluray_log = NULL; + mp_mutex_unlock(&bluray_log_lock); mp_mutex_destroy(&priv->overlay_lock); } @@ -1112,8 +1130,25 @@ static int bluray_stream_open_internal(stream_t *s) goto err; } - if (!mp_msg_test(s->log, MSGL_DEBUG)) - bd_set_debug_mask(0); + mp_mutex_lock(&bluray_log_lock); + // libbluray log callback is global and there is no way to separate it per + // instance, just replace with new one if already present. + if (bluray_log) { + MP_WARN(s, "Replacing logger from previous instance."); + talloc_free(bluray_log); + } + uint32_t mask = 0; + if (mp_msg_test(s->log, MSGL_DEBUG)) + mask |= DBG_CRIT | DBG_BLURAY | DBG_HDMV; + // When all bits are set (-1) libbluray inits to DBG_CRIT, set all bits, + // except one, so we avoid this default fallback. + if (mp_msg_test(s->log, MSGL_TRACE)) + mask |= UINT32_MAX >> 1; + bd_set_debug_mask(mask); + if (mask) + b->bluray_log = bluray_log = mp_log_new(s, s->log, "/libbluray"); + bd_set_debug_handler(bluray_logger); + mp_mutex_unlock(&bluray_log_lock); /* open device */ char *device_tmp = mp_get_user_path(NULL, s->global, device); From 222249216cec69c40a5735157ad395cbf88983df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Tue, 7 Jul 2026 00:51:51 +0200 Subject: [PATCH 41/44] sub/osd_state: rename external2 to image_overlay --- sub/osd.c | 14 +++++++------- sub/osd_state.h | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sub/osd.c b/sub/osd.c index 1aaa06e887802..0a073bd7113ef 100644 --- a/sub/osd.c +++ b/sub/osd.c @@ -192,8 +192,8 @@ void osd_free(struct osd_state *osd) if (!osd) return; osd_destroy_backend(osd); - talloc_free(osd->objs[OSDTYPE_EXTERNAL2]->external2); - talloc_free(osd->objs[OSDTYPE_DISC_MENU]->external2); + talloc_free(osd->objs[OSDTYPE_EXTERNAL2]->image_overlay); + talloc_free(osd->objs[OSDTYPE_DISC_MENU]->image_overlay); mp_mutex_destroy(&osd->lock); talloc_free(osd); } @@ -279,8 +279,8 @@ void osd_set_bitmaps(struct osd_state *osd, int type, struct sub_bitmaps *imgs) { mp_mutex_lock(&osd->lock); struct osd_object *obj = osd->objs[type]; - talloc_free(obj->external2); - obj->external2 = sub_bitmaps_copy(NULL, imgs); + talloc_free(obj->image_overlay); + obj->image_overlay = sub_bitmaps_copy(NULL, imgs); obj->vo_change_id += 1; osd->want_redraw_notification = true; mp_mutex_unlock(&osd->lock); @@ -335,9 +335,9 @@ static struct sub_bitmaps *render_object(struct osd_state *osd, res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts); } else if (obj->type == OSDTYPE_EXTERNAL2 || obj->type == OSDTYPE_DISC_MENU) { - if (obj->external2 && obj->external2->format) { - res = sub_bitmaps_copy(NULL, obj->external2); // need to be owner - obj->external2->change_id = 0; + if (obj->image_overlay && obj->image_overlay->format) { + res = sub_bitmaps_copy(NULL, obj->image_overlay); // need to be owner + obj->image_overlay->change_id = 0; } } else { res = osd_object_get_bitmaps(osd, obj, format); diff --git a/sub/osd_state.h b/sub/osd_state.h index 1e19b0d0eafe6..b07373e9ce79a 100644 --- a/sub/osd_state.h +++ b/sub/osd_state.h @@ -47,7 +47,7 @@ struct osd_object { int num_externals; // OSDTYPE_EXTERNAL2 / OSDTYPE_DISC_MENU - struct sub_bitmaps *external2; + struct sub_bitmaps *image_overlay; // VO cache state int vo_change_id; From cb9b96182ab9ea4b26219589014f45b203156cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Fri, 10 Jul 2026 00:35:15 +0200 Subject: [PATCH 42/44] stream: add mp_dvdnav_highlight struct and use it Clean up usages of this duplicated fields. --- player/discnav.c | 22 +++++---------------- stream/stream.h | 14 ++++++++------ stream/stream_dvdnav.c | 38 +++++++++++++------------------------ sub/dec_sub.h | 11 ++--------- sub/sd_lavc.c | 43 +++++++++++++++++++++--------------------- 5 files changed, 50 insertions(+), 78 deletions(-) diff --git a/player/discnav.c b/player/discnav.c index 8053a37d8c59c..35408e71efc6e 100644 --- a/player/discnav.c +++ b/player/discnav.c @@ -134,26 +134,13 @@ bool disc_nav_mouse_pos_to_src(struct MPContext *mpctx, int src_w, int src_h, } // 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) +static void push_dvd_overlay(struct MPContext *mpctx, struct stream_nav_state *nav) { - struct mp_dvdnav_hli hli = { - .show = visible, - .menu_active = nav->menu_active, - .change_id = nav->change_id, - }; - if (visible) { - hli.x = nav->hl_x; - hli.y = nav->hl_y; - hli.w = nav->hl_w; - hli.h = nav->hl_h; - memcpy(hli.palette, nav->hl_palette, sizeof(hli.palette)); - } for (int n = 0; n < mpctx->num_tracks; n++) { struct track *t = mpctx->tracks[n]; if (!is_dvd_sub_track(t) || !t->d_sub) continue; - sub_control(t->d_sub, SD_CTRL_APPLY_DVDNAV, &hli); + sub_control(t->d_sub, SD_CTRL_APPLY_DVDNAV, nav); } } @@ -456,12 +443,13 @@ void disc_nav_update(struct MPContext *mpctx) sync_disc_track_selection(mpctx, s, &nav); bool is_bd = strcmp(s->info->name, "bd") == 0 || strcmp(s->info->name, "bdmv/bluray") == 0; - bool visible = nav.menu_active && (is_bd || (nav.hl_w > 0 && nav.hl_h > 0)); + bool visible = nav.menu_active && + (is_bd || (mp_rect_w(nav.hl.rect) > 0 && mp_rect_h(nav.hl.rect) > 0)); if (is_bd) { push_bd_overlay(mpctx, s, &nav, visible); } else { - push_dvd_overlay(mpctx, &nav, visible); + push_dvd_overlay(mpctx, &nav); ensure_menu_sub_selection(mpctx, nav.menu_active); } diff --git a/stream/stream.h b/stream/stream.h index cf8a10a12829d..9a91a8a77c111 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -25,6 +25,7 @@ #include #include +#include "common/common.h" #include "misc/bstr.h" // Minimum guaranteed buffer and seek-back size. For any reads <= of this size, @@ -126,6 +127,12 @@ struct stream_nav_cmd { int x, y; // for MOUSE_* }; +// A DVD subpicture button highlight. +struct mp_dvdnav_highlight { + struct mp_rect rect; // button rectangle in SPU/source coords + uint32_t palette[4]; // replacement colors for SPU pixel values 0..3 +}; + // Snapshot of the stream's menu state. struct stream_nav_state { bool nav_active; // interactive disc navigation is enabled @@ -133,12 +140,7 @@ struct stream_nav_state { 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; - // BGRA highlight palette (0xAARRGGBB, straight) to substitute for the - // four SPU pixel values inside the highlight rect. Zeroed when no highlight - // is active. - uint32_t hl_palette[4]; + struct mp_dvdnav_highlight hl; // focused button highlight (DVD only) uint32_t change_id; // Bumped whenever any of the above changes uint32_t discontinuity_id; // Bumped when the stream's source position jumps diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index ddca976f5ee51..2f59210f08649 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -81,8 +81,7 @@ struct priv { 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 + struct mp_dvdnav_highlight hl; // focused button rect + palette uint32_t nav_change_id; uint32_t discontinuity_id; // bumped on actions that may jump bool pending_drain; // emit one EOF at the next jump boundary @@ -243,8 +242,7 @@ static bool in_menu_domain(dvdnav_t *dvdnav) 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; + priv->hl.rect = (struct mp_rect){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. @@ -252,10 +250,11 @@ static void compute_button_rect(struct priv *priv, pci_t *pci, int btn) 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; + priv->hl.rect = (struct mp_rect){ + .x0 = xs, .y0 = ys, + .x1 = xe > xs ? xe : xs, + .y1 = ye > ys ? ye : ys, + }; } // Resolve the 4-entry "select-state" highlight palette for the focused button. @@ -264,7 +263,7 @@ static void compute_button_rect(struct priv *priv, pci_t *pci, int btn) // alphas. static void compute_highlight_palette(struct priv *priv, pci_t *pci, int btn) { - memset(priv->hl_palette, 0, sizeof(priv->hl_palette)); + 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; @@ -291,7 +290,7 @@ static void compute_highlight_palette(struct priv *priv, pci_t *pci, int btn) 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]; + priv->hl.palette[i] = (alpha << 24) | (c[0] << 16) | (c[1] << 8) | c[2]; } } @@ -336,11 +335,8 @@ static void refresh_video_resolution(struct priv *priv) 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)); + struct mp_dvdnav_highlight prev_hl = priv->hl; priv->in_menu = in_menu_domain(priv->dvdnav); pci_t *pci = priv->in_menu ? dvdnav_get_current_nav_pci(priv->dvdnav) : NULL; @@ -364,9 +360,7 @@ static void update_highlight(struct priv *priv) 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)); + priv->hl = (struct mp_dvdnav_highlight){0}; } else { priv->current_button = btn; compute_button_rect(priv, pci, btn); @@ -374,9 +368,7 @@ static void update_highlight(struct priv *priv) } 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) + memcmp(&prev_hl, &priv->hl, sizeof(prev_hl)) != 0) { priv->nav_change_id++; } @@ -924,10 +916,7 @@ static int control(stream_t *stream, int cmd, void *arg) .still_active = priv->still_active, .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], + .hl = priv->hl, .change_id = priv->nav_change_id, .discontinuity_id = priv->discontinuity_id, .active_audio_id = dvd_physical_audio_to_substream(priv, priv->audio_physical), @@ -936,7 +925,6 @@ static int control(stream_t *stream, int cmd, void *arg) .angle = cur_angle, .num_angles = num_angles, }; - memcpy(st->hl_palette, priv->hl_palette, sizeof(st->hl_palette)); return STREAM_OK; } } diff --git a/sub/dec_sub.h b/sub/dec_sub.h index c7b1e2f3b5020..63832cb68d794 100644 --- a/sub/dec_sub.h +++ b/sub/dec_sub.h @@ -5,6 +5,7 @@ #include #include "player/core.h" +#include "stream/stream.h" #include "osd.h" struct sh_stream; @@ -21,15 +22,7 @@ enum sd_ctrl { SD_CTRL_SET_VIDEO_DEF_FPS, SD_CTRL_RESET_SOFT, SD_CTRL_UPDATE_OPTS, - SD_CTRL_APPLY_DVDNAV, // struct mp_dvdnav_hli * -}; - -struct mp_dvdnav_hli { - bool show; // render the button highlight - bool menu_active; // menu subpicture is persistent display state - int x, y, w, h; // button rect in SPU/source coords - uint32_t palette[4]; // 0xAARRGGBB, straight alpha - uint32_t change_id; // bumped on any visible change + SD_CTRL_APPLY_DVDNAV, // const struct stream_nav_state * }; enum sd_text_type { diff --git a/sub/sd_lavc.c b/sub/sd_lavc.c index b5277a597791b..84de577ff6eea 100644 --- a/sub/sd_lavc.c +++ b/sub/sd_lavc.c @@ -72,8 +72,9 @@ struct sd_lavc_priv { int num_seekpoints; struct bitmap_packer *packer; - // DVD-nav per-pixel highlight overlay state. - struct mp_dvdnav_hli hli; + // DVD-nav menu/highlight overlay state. + bool menu_active; // menu subpicture is persistent display state + struct mp_dvdnav_highlight hl; // focused button rect + palette uint32_t hli_change_id; }; @@ -299,21 +300,20 @@ static void read_sub_bitmaps(struct sd *sd, struct sub *sub) convert_pal(pal, 256, opts->sub_gray); // DVD navigation highlight - bool hli_active = priv->hli.show && - priv->hli.w > 0 && priv->hli.h > 0 && - r->x < priv->hli.x + priv->hli.w && - r->y < priv->hli.y + priv->hli.h && - r->x + r->w > priv->hli.x && - r->y + r->h > priv->hli.y; + struct mp_rect hlr = priv->hl.rect; + bool hli_active = priv->menu_active && + mp_rect_w(hlr) > 0 && mp_rect_h(hlr) > 0 && + r->x < hlr.x1 && r->y < hlr.y1 && + r->x + r->w > hlr.x0 && r->y + r->h > hlr.y0; uint32_t hli_pal[4] = {0}; if (hli_active) { - memcpy(hli_pal, priv->hli.palette, sizeof(hli_pal)); + memcpy(hli_pal, priv->hl.palette, sizeof(hli_pal)); convert_pal(hli_pal, 4, opts->sub_gray); } - int hli_x0 = priv->hli.x - r->x; - int hli_y0 = priv->hli.y - r->y; - int hli_x1 = hli_x0 + priv->hli.w; - int hli_y1 = hli_y0 + priv->hli.h; + int hli_x0 = hlr.x0 - r->x; + int hli_y0 = hlr.y0 - r->y; + int hli_x1 = hlr.x1 - r->x; + int hli_y1 = hlr.y1 - r->y; for (int y = -padding; y < b->h + padding; y++) { uint32_t *out = (uint32_t*)((char*)b->bitmap + y * b->stride); @@ -462,7 +462,7 @@ static struct sub *get_current(struct sd_lavc_priv *priv, double pts) { // A menu subpicture is persistent display state, not bound to playback // time. - if (priv->hli.menu_active) { + if (priv->menu_active) { for (int n = 0; n < MAX_QUEUE; n++) { if (priv->subs[n].valid) return &priv->subs[n]; @@ -642,7 +642,7 @@ static bool accepts_packet(struct sd *sd, double min_pts) struct sd_lavc_priv *priv = sd->priv; // Menu subpictures are not pts-ordered; never block delivery. - if (priv->hli.menu_active) + if (priv->menu_active) return true; double pts = priv->current_pts; @@ -679,7 +679,7 @@ static void reset(struct sd *sd) // Keep the menu subpicture; timeline resets don't invalidate it and the // disc won't re-send it. Purged when the menu closes. - if (!priv->hli.menu_active) { + if (!priv->menu_active) { for (int n = 0; n < MAX_QUEUE; n++) clear_sub(&priv->subs[n]); } @@ -776,11 +776,12 @@ static int control(struct sd *sd, enum sd_ctrl cmd, void *arg) priv->video_params = *(struct mp_image_params *)arg; return CONTROL_OK; case SD_CTRL_APPLY_DVDNAV: { - struct mp_dvdnav_hli *hli = arg; - bool menu_closed = priv->hli.menu_active && !hli->menu_active; - bool changed = priv->hli_change_id != hli->change_id; - priv->hli = *hli; - priv->hli_change_id = hli->change_id; + struct stream_nav_state *nav = arg; + bool menu_closed = priv->menu_active && !nav->menu_active; + bool changed = priv->hli_change_id != nav->change_id; + priv->menu_active = nav->menu_active; + priv->hl = nav->hl; + priv->hli_change_id = nav->change_id; // Don't let menu subpictures leak into title playback. if (menu_closed) { for (int n = 0; n < MAX_QUEUE; n++) From 7dbd286a4d4fad0440efd1d8f1ba3c3072dc3960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 4 Jul 2026 19:36:04 +0200 Subject: [PATCH 43/44] stream_dvda: add DVD-Audio playback support Add a dvda:// protocol backed by libdvdread. Audio title sets are enumerated from the ATS IFOs. Each ATS title is exposed as an mpv title/edition and its tracks as chapters. Menus are not supports, they would need full VM implementation, and libdvdnav currently doesn't support DVD-Audio. Still images (ASVS) may be supported in the future. --- DOCS/interface-changes/dvda.txt | 2 + DOCS/man/mpv.rst | 7 + DOCS/man/options.rst | 10 + meson.build | 11 + meson.options | 1 + options/options.c | 4 + options/options.h | 1 + stream/stream.c | 6 + stream/stream_dvda.c | 514 ++++++++++++++++++++++++++++++++ 9 files changed, 556 insertions(+) create mode 100644 DOCS/interface-changes/dvda.txt create mode 100644 stream/stream_dvda.c diff --git a/DOCS/interface-changes/dvda.txt b/DOCS/interface-changes/dvda.txt new file mode 100644 index 0000000000000..4224a2e0f927b --- /dev/null +++ b/DOCS/interface-changes/dvda.txt @@ -0,0 +1,2 @@ +add `dvda://` protocol for DVD-Audio playback (requires libdvdread >= 7.1.0) +add `--dvda-device` option diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index 58b5321b07e7a..9a3afc9a8dc83 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -1336,6 +1336,13 @@ PROTOCOLS ``dvdnav://`` is an old alias for ``dvd://`` and does exactly the same thing. +``dvda://[title][/device]`` ``--dvda-device=PATH`` + + Play the AUDIO_TS zone of a DVD-Audio disc. Titles correspond to the + disc's audio groups, tracks are exposed as chapters. If no title is + given, the longest title is auto-selected. Menus and still images are + not supported. + ``dvb://[cardnumber@]channel`` ``--dvbin-...`` Digital TV via DVB. (Linux only.) diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 43213263ea5df..84ae8702840af 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -3894,6 +3894,16 @@ Disc Devices ``mpv dvd:// --dvd-device=/path/to/dvd/`` +``--dvda-device=`` + Specify the DVD-Audio device or .iso filename for ``dvda://`` playback. + You can also specify a directory that contains files previously copied + directly from a DVD-Audio disc. The default device path depends on + the OS. See the `OPTICAL DRIVES`_ section. + + .. admonition:: Example + + ``mpv dvda:// --dvda-device=/path/to/dvda/`` + ``--bluray-device=`` Specify the Blu-ray disc location. Must be a directory with Blu-ray structure. The default device path depends on the OS. See the diff --git a/meson.build b/meson.build index 40254deb1a743..b3ca896aedfbe 100644 --- a/meson.build +++ b/meson.build @@ -660,6 +660,17 @@ if features['dvbin'] 'stream/stream_dvb.c') endif +dvda_opt = get_option('dvda').require( + get_option('gpl'), + error_message: 'the build is not GPL!', +) +dvdread = dependency('dvdread', version: '>= 7.1.0', required: dvda_opt) +features += {'dvda': dvdread.found()} +if features['dvda'] + dependencies += dvdread + sources += files('stream/stream_dvda.c') +endif + dvdnav_opt = get_option('dvdnav').require( get_option('gpl'), error_message: 'the build is not GPL!', diff --git a/meson.options b/meson.options index 35e174cebaa94..416ee3b5edf09 100644 --- a/meson.options +++ b/meson.options @@ -11,6 +11,7 @@ option('disable-packet-pool', type: 'boolean', value: false, description: 'disab option('cdda', type: 'feature', value: 'auto', description: 'cdda support (libcdio)') option('cplugins', type: 'feature', value: 'auto', description: 'C plugins') option('dvbin', type: 'feature', value: 'auto', description: 'DVB input module') +option('dvda', type: 'feature', value: 'auto', description: 'DVD-Audio support (libdvdread)') option('dvdnav', type: 'feature', value: 'auto', description: 'dvdnav support') option('iconv', type: 'feature', value: 'auto', description: 'iconv') option('javascript', type: 'feature', value: 'auto', description: 'Javascript (MuJS backend)') diff --git a/options/options.c b/options/options.c index 34cf8d3b6dff4..093443e0ab4ee 100644 --- a/options/options.c +++ b/options/options.c @@ -94,6 +94,7 @@ extern const struct m_obj_list vo_obj_list; extern const struct m_sub_options ao_conf; extern const struct m_sub_options dvd_conf; +extern const struct m_sub_options dvda_conf; extern const struct m_sub_options clipboard_conf; extern const struct m_sub_options curl_conf; @@ -587,6 +588,9 @@ static const m_option_t mp_opts[] = { #if HAVE_DVDNAV {"dvd", OPT_SUBSTRUCT(dvd_opts, dvd_conf)}, +#endif +#if HAVE_DVDA + {"dvda", OPT_SUBSTRUCT(dvda_opts, dvda_conf)}, #endif {"edition", OPT_CHOICE(edition_id, {"auto", -1}), M_RANGE(0, 8190)}, {"flatten-editions", OPT_BOOL(flatten_editions)}, diff --git a/options/options.h b/options/options.h index 8c85a2bf765ce..a00f503347477 100644 --- a/options/options.h +++ b/options/options.h @@ -420,6 +420,7 @@ typedef struct MPOpts { struct wingl_opts *wingl_opts; struct cuda_opts *cuda_opts; struct dvd_opts *dvd_opts; + struct dvda_opts *dvda_opts; struct vaapi_opts *vaapi_opts; struct sws_opts *sws_opts; struct zimg_opts *zimg_opts; diff --git a/stream/stream.c b/stream/stream.c index 5314eb3c93622..cc29f31a4b26c 100644 --- a/stream/stream.c +++ b/stream/stream.c @@ -56,6 +56,8 @@ extern const stream_info_t stream_info_slice; extern const stream_info_t stream_info_fd; extern const stream_info_t stream_info_ifo_dvdnav; extern const stream_info_t stream_info_dvdnav; +extern const stream_info_t stream_info_ifo_dvda; +extern const stream_info_t stream_info_dvda; extern const stream_info_t stream_info_bdmv_dir; extern const stream_info_t stream_info_bluray; extern const stream_info_t stream_info_edl; @@ -77,6 +79,10 @@ static const stream_info_t *const stream_list[] = { &stream_info_ifo_dvdnav, &stream_info_dvdnav, #endif +#if HAVE_DVDA + &stream_info_ifo_dvda, + &stream_info_dvda, +#endif #if HAVE_LIBBLURAY &stream_info_bdmv_dir, &stream_info_bluray, diff --git a/stream/stream_dvda.c b/stream/stream_dvda.c new file mode 100644 index 0000000000000..c68469ac5f53b --- /dev/null +++ b/stream/stream_dvda.c @@ -0,0 +1,514 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include "config.h" + +#if !HAVE_GPL +#error GPL only +#endif + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "osdep/io.h" + +#include "common/common.h" +#include "common/msg.h" +#include "options/m_config.h" +#include "options/options.h" +#include "options/path.h" +#include "stream.h" + +#define TITLE_LONGEST -1 + +#define DVD_BLOCK_SIZE 2048 + +#define DVDA_TIMEBASE 90000 +#define DVDA_TIME_TO_S(x) ((x) / (double)(DVDA_TIMEBASE)) +#define DVDA_TIME_FROM_S(x) ((int64_t)((x) * DVDA_TIMEBASE)) + +// One audio track (mapped to an mpv chapter) within a title. +struct dvda_track { + int64_t time; // start time relative to title start (ticks) + int64_t duration; // 90 kHz PTS ticks + uint32_t start_sector; // within the ATS AOB space + uint32_t end_sector; // inclusive +}; + +// One ATS title (mapped to an mpv title/edition). +struct dvda_title { + int ats; // audio title set number (1-based) + int64_t duration; // 90 kHz PTS ticks + uint32_t start_sector; + uint32_t end_sector; // inclusive + struct dvda_track *tracks; + int num_tracks; +}; + +struct priv { + dvd_reader_t *dvd; + dvd_file_t *file; // AOBs of the currently open ATS + int open_ats; // ATS number the file handle belongs to + + struct dvda_title *titles; + int num_titles; + int title; // current title index + uint32_t cur_sector; + + int track; // requested title, or TITLE_LONGEST + char *device; + struct dvda_opts *opts; +}; + +struct dvda_opts { + char *device; +}; + +#define OPT_BASE_STRUCT struct dvda_opts + +const struct m_sub_options dvda_conf = { + .opts = (const struct m_option[]){ + {"device", OPT_STRING(device), .flags = M_OPT_FILE}, + {0} + }, + .size = sizeof(struct dvda_opts), +}; + +// Read the track layout of every title in every audio title set. +static bool read_disc_structure(stream_t *stream) +{ + struct priv *priv = stream->priv; + + ifo_handle_t *amg = ifoOpenVMGI(priv->dvd); + if (!amg || amg->ifo_format != IFO_AUDIO || !amg->amgi_mat) { + MP_ERR(stream, "Could not read AUDIO_TS.IFO.\n"); + if (amg) + ifoClose(amg); + return false; + } + int num_ats = amg->amgi_mat->amg_nr_of_title_sets; + ifoClose(amg); + + for (int ats = 1; ats <= num_ats; ats++) { + ifo_handle_t *ifo = ifoOpen(priv->dvd, ats); + if (!ifo) + continue; + if (ifo->ifo_format != IFO_AUDIO || !ifo->atsi_title_table) { + ifoClose(ifo); + continue; + } + atsi_title_table_t *tt = ifo->atsi_title_table; + for (int n = 0; n < tt->nr_titles; n++) { + atsi_title_record_t *rec = &tt->atsi_title_row_tables[n]; + int num_tracks = MPMIN(rec->nr_tracks, rec->nr_pointer_records); + if (num_tracks <= 0) + continue; + + struct dvda_title t = { + .ats = ats, + .duration = rec->length_pts, + .start_sector = rec->atsi_track_pointer_rows[0].start_sector, + .num_tracks = num_tracks, + .tracks = talloc_array(priv, struct dvda_track, num_tracks), + }; + t.end_sector = t.start_sector; + int64_t time = 0; + for (int i = 0; i < num_tracks; i++) { + atsi_track_timestamp_t *ts = &rec->atsi_track_timestamp_rows[i]; + atsi_track_pointer_t *ptr = &rec->atsi_track_pointer_rows[i]; + t.tracks[i] = (struct dvda_track){ + .time = time, + .duration = ts->length_pts_of_track, + .start_sector = ptr->start_sector, + .end_sector = ptr->end_sector, + }; + time += t.tracks[i].duration; + if (ptr->end_sector > t.end_sector) + t.end_sector = ptr->end_sector; + } + MP_DBG(stream, "title %d: ats=%d tracks=%d sectors=%"PRIu32 + "..%"PRIu32" duration=%.2f\n", priv->num_titles, ats, + num_tracks, t.start_sector, t.end_sector, DVDA_TIME_TO_S(t.duration)); + for (int i = 0; i < num_tracks; i++) + MP_DBG(stream, " track %d: t=%.2f dur=%.2f sectors=%"PRIu32 + "..%"PRIu32"\n", i, t.tracks[i].time, + DVDA_TIME_TO_S(t.tracks[i].duration), t.tracks[i].start_sector, + t.tracks[i].end_sector); + MP_TARRAY_APPEND(priv, priv->titles, priv->num_titles, t); + } + ifoClose(ifo); + } + + return priv->num_titles > 0; +} + +static bool play_title(stream_t *stream, int title) +{ + struct priv *priv = stream->priv; + + if (title < 0 || title >= priv->num_titles) + return false; + + struct dvda_title *t = &priv->titles[title]; + if (!priv->file || priv->open_ats != t->ats) { + if (priv->file) + DVDCloseFile(priv->file); + priv->file = DVDOpenFile(priv->dvd, t->ats, DVD_READ_TITLE_VOBS); + if (!priv->file) { + MP_ERR(stream, "Could not open AOB files of title set %d.\n", t->ats); + return false; + } + priv->open_ats = t->ats; + } + priv->title = title; + priv->cur_sector = t->start_sector; + return true; +} + +static int fill_buffer(stream_t *stream, void *buf, int max_len) +{ + struct priv *priv = stream->priv; + struct dvda_title *t = &priv->titles[priv->title]; + + if (max_len < DVD_BLOCK_SIZE) { + MP_FATAL(stream, "Short read size. Data corruption will follow. Please " + "provide a patch.\n"); + return -1; + } + + if (priv->cur_sector > t->end_sector) + return 0; // title end + + size_t blocks = MPMIN(max_len / DVD_BLOCK_SIZE, + t->end_sector - priv->cur_sector + 1); + ssize_t r = DVDReadBlocks(priv->file, priv->cur_sector, blocks, buf); + if (r <= 0) { + MP_ERR(stream, "Error reading sector %"PRIu32".\n", priv->cur_sector); + return 0; + } + priv->cur_sector += r; + return r * DVD_BLOCK_SIZE; +} + +// A track whose sectors fall outside the title's contiguous AOB range is a +// trailing marker. +static bool track_sectors_ok(struct dvda_title *t, struct dvda_track *tr) +{ + return tr->start_sector >= t->start_sector && + tr->end_sector <= t->end_sector && + tr->end_sector >= tr->start_sector; +} + +// Map a sector position to title-relative playback time via the track it falls +// in. Track durations (and thus start times) come from length_pts. +static double sector_to_time(struct dvda_title *t, uint32_t sector) +{ + for (int i = 0; i < t->num_tracks; i++) { + struct dvda_track *tr = &t->tracks[i]; + if (!track_sectors_ok(t, tr) || sector > tr->end_sector) + continue; + if (sector < tr->start_sector) + return tr->time; + return tr->time + av_rescale(sector - tr->start_sector, tr->duration, + tr->end_sector - tr->start_sector + 1); + } + return t->duration; +} + +static uint32_t time_to_sector(struct dvda_title *t, int64_t time) +{ + for (int i = t->num_tracks - 1; i >= 0; i--) { + struct dvda_track *tr = &t->tracks[i]; + if (time < tr->time && i > 0) + continue; + if (!track_sectors_ok(t, tr)) + return t->end_sector; + uint32_t span = tr->end_sector - tr->start_sector; + int64_t off = time - tr->time; + int64_t add = tr->duration > 0 && off > 0 ? av_rescale(off, span, tr->duration) : 0; + return tr->start_sector + MPMIN(add, span); + } + return t->start_sector; +} + +static int control(stream_t *stream, int cmd, void *arg) +{ + struct priv *priv = stream->priv; + struct dvda_title *t = &priv->titles[priv->title]; + + switch (cmd) { + case STREAM_CTRL_GET_NUM_CHAPTERS: + *(unsigned int *)arg = t->num_tracks; + return STREAM_OK; + case STREAM_CTRL_GET_CHAPTER_TIME: { + double *ch = arg; + int chapter = *ch; + if (chapter < 0 || chapter >= t->num_tracks) + break; + *ch = DVDA_TIME_TO_S(t->tracks[chapter].time); + return STREAM_OK; + } + case STREAM_CTRL_GET_TIME_LENGTH: + *(double *)arg = DVDA_TIME_TO_S(t->duration); + return STREAM_OK; + case STREAM_CTRL_GET_CURRENT_TIME: + *(double *)arg = DVDA_TIME_TO_S(sector_to_time(t, priv->cur_sector)); + return STREAM_OK; + case STREAM_CTRL_SEEK_TO_TIME: { + double *args = arg; + priv->cur_sector = time_to_sector(t, DVDA_TIME_FROM_S(args[0])); + stream_drop_buffers(stream); + return STREAM_OK; + } + case STREAM_CTRL_GET_NUM_TITLES: + *(unsigned int *)arg = priv->num_titles; + return STREAM_OK; + case STREAM_CTRL_GET_TITLE_LENGTH: { + int title = *(double *)arg; + if (title < 0 || title >= priv->num_titles) + break; + *(double *)arg = DVDA_TIME_TO_S(priv->titles[title].duration); + return STREAM_OK; + } + case STREAM_CTRL_GET_CURRENT_TITLE: + *(unsigned int *)arg = priv->title; + return STREAM_OK; + case STREAM_CTRL_SET_CURRENT_TITLE: { + int title = *(unsigned int *)arg; + if (!play_title(stream, title)) + break; + stream_drop_buffers(stream); + return STREAM_OK; + } + case STREAM_CTRL_GET_DISC_NAME: { + char volid[32] = {0}; + if (DVDUDFVolumeInfo(priv->dvd, volid, sizeof(volid), NULL, 0) < 0 && + DVDISOVolumeInfo(priv->dvd, volid, sizeof(volid), NULL, 0) < 0) + break; + if (!volid[0]) + break; + *(char **)arg = talloc_strdup(NULL, volid); + return STREAM_OK; + } + } + + return STREAM_UNSUPPORTED; +} + +static void stream_dvda_close(stream_t *stream) +{ + struct priv *priv = stream->priv; + if (priv->file) + DVDCloseFile(priv->file); + if (priv->dvd) + DVDClose(priv->dvd); +} + +static void dvda_log(void *priv, dvd_logger_level_t level, + const char *fmt, va_list va) +{ + int lvl; + switch (level) { + case DVD_LOGGER_LEVEL_ERROR: lvl = MSGL_ERR; break; + case DVD_LOGGER_LEVEL_WARN: lvl = MSGL_WARN; break; + case DVD_LOGGER_LEVEL_DEBUG: lvl = MSGL_DEBUG; break; + case DVD_LOGGER_LEVEL_INFO: + default: lvl = MSGL_V; break; + } + if (!mp_msg_test(priv, lvl)) + return; + mp_msg_va(priv, lvl, fmt, va); + mp_msg(priv, lvl, "\n"); +} + +static int open_s_internal(stream_t *stream) +{ + struct priv *priv = stream->priv; + char *filename; + + priv->opts = mp_get_config_group(stream, stream->global, &dvda_conf); + + if (priv->device && priv->device[0]) { + filename = priv->device; + } else if (priv->opts->device && priv->opts->device[0]) { + filename = priv->opts->device; + } else { + filename = DEFAULT_OPTICAL_DEVICE; + } + + char *path = mp_get_user_path(priv, stream->global, filename); + if (!path) + goto err; + + struct mp_log *log = mp_log_new(stream, stream->log, "/libdvdread"); + const dvd_logger_cb logger_cb = { .pf_log = dvda_log }; + priv->dvd = DVDOpenAudio(log, &logger_cb, path); + if (!priv->dvd) { + MP_ERR(stream, "Couldn't open DVD-Audio device: %s\n", path); + goto err; + } + + if (!read_disc_structure(stream)) { + MP_ERR(stream, "No DVD-Audio titles found: %s\n", path); + goto err; + } + + if (priv->track == TITLE_LONGEST || priv->track >= priv->num_titles) { + int64_t best_length = -1; + int best_title = 0; + for (int n = 0; n < priv->num_titles; n++) { + MP_VERBOSE(stream, "title: %3d tracks: %2d duration: %.1f\n", + n, priv->titles[n].num_tracks, + DVDA_TIME_TO_S(priv->titles[n].duration)); + if (priv->titles[n].duration > best_length) { + best_length = priv->titles[n].duration; + best_title = n; + } + } + priv->track = best_title; + MP_INFO(stream, "Selecting title %d.\n", priv->track); + } + + if (!play_title(stream, priv->track)) { + MP_ERR(stream, "Couldn't select title %d.\n", priv->track); + goto err; + } + + stream->fill_buffer = fill_buffer; + stream->control = control; + stream->close = stream_dvda_close; + stream->demuxer = "+disc"; + stream->lavf_type = "mpeg"; + + return STREAM_OK; + +err: + stream_dvda_close(stream); + return STREAM_ERROR; +} + +static int open_s(stream_t *stream) +{ + struct priv *priv = talloc_zero(stream, struct priv); + stream->priv = priv; + + bstr title, bdevice; + bstr_split_tok(bstr0(stream->path), "/", &title, &bdevice); + + struct MPOpts *opts = mp_get_config_group(stream, stream->global, &mp_opt_root); + int edition_id = opts->edition_id; + talloc_free(opts); + + priv->track = TITLE_LONGEST; + + if (edition_id >= 0) { + priv->track = edition_id; + } else if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) { + priv->track = TITLE_LONGEST; + } else if (title.len) { + bstr rest; + priv->track = bstrtoll(title, &rest, 10); + if (rest.len) { + MP_ERR(stream, "number expected: '%.*s'\n", BSTR_P(rest)); + return STREAM_ERROR; + } + } + + priv->device = bstrto0(priv, bdevice); + + return open_s_internal(stream); +} + +const stream_info_t stream_info_dvda = { + .name = "dvda", + .open = open_s, + .protocols = (const char *const[]){ "dvda", NULL }, + .stream_origin = STREAM_ORIGIN_UNSAFE, +}; + +// Check if this is likely to be AUDIO_TS.IFO. +static bool check_ifo(const char *path) +{ + if (strcasecmp(mp_basename(path), "audio_ts.ifo")) + return false; + + FILE *temp = fopen(path, "rb"); + if (!temp) + return false; + + char data[12]; + bool r = fread(data, sizeof(data), 1, temp) == 1 && + memcmp(data, "DVDAUDIO-AMG", 12) == 0; + + fclose(temp); + return r; +} + +static int ifo_dvda_stream_open(stream_t *stream) +{ + struct priv *priv = talloc_zero(stream, struct priv); + stream->priv = priv; + + if (!stream->access_references) + 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; + talloc_free(opts); + + char *path = mp_file_get_path(priv, bstr0(stream->url)); + if (!path) + goto unsupported; + + // We allow the path to point to a directory containing AUDIO_TS/, a + // directory containing AUDIO_TS.IFO, or that file itself. + if (!check_ifo(path)) { + // On UNIX, just assume the filename is always uppercase. + char *npath = mp_path_join(priv, path, "AUDIO_TS.IFO"); + if (!check_ifo(npath)) { + npath = mp_path_join(priv, path, "AUDIO_TS/AUDIO_TS.IFO"); + if (!check_ifo(npath)) + goto unsupported; + } + path = npath; + } + + priv->device = bstrto0(priv, mp_dirname(path)); + + MP_INFO(stream, ".IFO detected. Redirecting to dvda://\n"); + return open_s_internal(stream); + +unsupported: + talloc_free(priv); + stream->priv = NULL; + return STREAM_UNSUPPORTED; +} + +const stream_info_t stream_info_ifo_dvda = { + .name = "ifo_dvda", + .open = ifo_dvda_stream_open, + .protocols = (const char *const[]){ "file", "", NULL }, + .stream_origin = STREAM_ORIGIN_UNSAFE, +}; From 08960086431f7f474d42377cc27768a8fb4736d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Mon, 6 Jul 2026 01:51:23 +0200 Subject: [PATCH 44/44] demux: read sparse still-image streams lazily still_image streams (AV_DISPOSITION_STILL_IMAGE) consist of sparse frames that can be minutes apart. When such a stream was selected alongside an eager audio/video stream, its empty packet queue forced the demuxer to keep reading ahead, buffering the whole file trying to chase min_secs of "video". Read them lazily when other eager streams exist, so playback drives read-ahead instead. --- demux/demux.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/demux/demux.c b/demux/demux.c index 4c698d69d067f..ba1a95d564bf1 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -907,13 +907,13 @@ static void update_stream_selection_state(struct demux_internal *in, any_streams |= s->selected; } - // Subtitles are only eagerly read if there are no other eagerly read - // streams. + // Subtitles and sparse still-image video are only eagerly read if there are + // no other eagerly read streams. if (any_av_streams) { for (int n = 0; n < in->num_streams; n++) { struct demux_stream *s = in->streams[n]->ds; - if (s->type == STREAM_SUB) + if (s->type == STREAM_SUB || s->still_image) s->eager = false; } }