diff --git a/meson.build b/meson.build
index e571904378..f3accb2170 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 ec6784411c..5e961cbb56 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,16 @@ 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_reset(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 0000000000..cc71f626f1
--- /dev/null
+++ b/player/discnav.c
@@ -0,0 +1,372 @@
+/*
+ * 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;
+}
+
+// Forget everything tied to the previous playlist entry. Stale state (e.g.
+// another disc's discontinuity counter) must not leak into the next file.
+void disc_nav_reset(struct MPContext *mpctx)
+{
+ 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");
+ }
+ mp_image_unrefp(&st->bd_image);
+ *st = (struct disc_nav_state){0};
+}
+
+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 = MPMIN((int)(fx * src_w), src_w - 1);
+ *out_y = MPMIN((int)(fy * src_h), src_h - 1);
+ 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);
+ }
+
+ // The OSD/VO may still hold references to the previous frame's pixels.
+ if (!mp_image_make_writeable(st->bd_image))
+ return;
+
+ 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,
+ .video_color_space = true,
+ };
+ 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(s, "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 *s,
+ 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(s, "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);
+ if (!s && !mpctx->disc_nav)
+ return;
+ struct disc_nav_state *st = get_state(mpctx);
+ struct stream_nav_state nav = {0};
+ bool have = s && stream_control(s, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1;
+ if (!have) {
+ 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;
+ }
+
+ check_async_discontinuity(mpctx, s, &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(s, "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/loadfile.c b/player/loadfile.c
index 3ded4e71c4..6c24a3246e 100644
--- a/player/loadfile.c
+++ b/player/loadfile.c
@@ -1784,6 +1784,7 @@ static void play_current_file(struct MPContext *mpctx)
// let get_current_time() show 0 as start time (before playback_pts is set)
mpctx->last_seek_pts = 0.0;
mpctx->seek = (struct seek_params){ 0 };
+ disc_nav_reset(mpctx);
mpctx->filter_root = mp_filter_create_root(mpctx->global);
mp_filter_graph_set_wakeup_cb(mpctx->filter_root, mp_wakeup_core_cb, mpctx);
mp_filter_graph_set_max_run_time(mpctx->filter_root, 0.1);
diff --git a/player/main.c b/player/main.c
index 6891c9d5a1..0ef3d7f6d6 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 d899fba123..f472e40969 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);