Files
2026-08-26 18:04:17 +02:00

540 lines
19 KiB
C

/*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <stdbool.h>
#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 menu-overlay change_id we forced an OSD redraw for.
uint32_t last_overlay_change_id;
bool overlay_change_seen;
bool drain_was_pending; // last poll saw a boundary awaiting resync
// 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;
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;
// Last audio id we logged as "not in tracks yet" (avoids log spam).
int last_missing_audio_id;
// Last menu_active pushed through property-change notification.
bool last_menu_active;
bool menu_active_seen;
};
static bool is_dvd_sub_track(struct track *t)
{
return t && t->type == STREAM_SUB && t->stream && t->stream->codec &&
t->stream->codec->codec &&
strcmp(t->stream->codec->codec, "dvd_subtitle") == 0;
}
static struct disc_nav_state *get_state(struct MPContext *mpctx)
{
if (!mpctx->disc_nav)
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;
}
// Push the current menu/highlight state to the dvd_subtitle decoders.
static void push_dvd_overlay(struct MPContext *mpctx, struct stream_nav_state *nav)
{
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, nav);
}
}
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 playlist/title jumps driven by the disc itself (menu buttons, HDMV
// bytecode, end of title, 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;
bool new_drain = nav->drain_pending && !st->drain_was_pending;
st->drain_was_pending = nav->drain_pending;
if (!new_drain)
return;
// The disc jumped on its own, resync through the regular seek path.
// demux_disc releases the held boundary instead of repositioning the VM.
double t = 0;
if (stream_control(s, STREAM_CTRL_GET_CURRENT_TIME, &t) < 1)
t = 0;
MP_VERBOSE(s, "disc jumped (id %u), resync seek to %f\n",
nav->discontinuity_id, t);
queue_seek(mpctx, MPSEEK_ABSOLUTE, t, MPSEEK_KEYFRAME, 0);
}
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->no_audio &&
mpctx->opts->stream_id[0][STREAM_AUDIO] != -2)
{
// The disc hopped into a domain without any audio stream. Deselect audio.
if (mpctx->current_track[0][STREAM_AUDIO])
mp_switch_track_n(mpctx, 0, STREAM_AUDIO, NULL, 0);
st->last_audio_id = -1;
} else if (!first && (hopped || nav->active_audio_id != st->last_audio_id) &&
mpctx->opts->stream_id[0][STREAM_AUDIO] == -1)
{
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(s, "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) {
if (st->last_missing_audio_id != nav->active_audio_id) {
st->last_missing_audio_id = nav->active_audio_id;
MP_VERBOSE(s, "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 (mpctx->opts->stream_id[0][STREAM_AUDIO] == -1) {
struct track *t = find_track_by_demuxer_id(mpctx, STREAM_AUDIO,
nav->active_audio_id);
if (t && mpctx->current_track[0][STREAM_AUDIO] != t) {
MP_VERBOSE(s, "initial disc audio -> demuxer_id 0x%x\n",
nav->active_audio_id);
mp_switch_track_n(mpctx, 0, STREAM_AUDIO, t, 0);
}
}
}
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] == -1)
{
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(s, "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(s, "disc sub 0x%x not in tracks yet\n",
nav->active_sub_id);
}
} else {
if (mpctx->current_track[0][STREAM_SUB]) {
MP_VERBOSE(s, "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(s, "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)
{
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 pointers could be stale. Trust only what we
// can still see in mpctx->tracks.
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] == *slot) {
still_present = true;
break;
}
}
if (!still_present)
*slot = NULL;
}
if (menu_on) {
// 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++) {
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 {
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, st->menu_saved_track, 0);
st->menu_selected_track = NULL;
st->menu_saved_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;
bool still = have && nav.still_active;
if (s && still != mpctx->disc_nav_still_frame)
MP_VERBOSE(s, "still_frame %d->%d\n",
mpctx->disc_nav_still_frame, still);
mpctx->disc_nav_still_frame = still;
// The BD-J VM progresses only on reads, pump them while playback idles
// at EOF or a held still, per libbluray's requirement. Our core wakes up
// at 1s intervals even at pause, but that's not enough for fluid button
// navigation. So, we always drive the VM at least at 20Hz.
if (have && mpctx->demuxer) {
bool bd_idle = (strcmp(s->info->name, "bd") == 0 ||
strcmp(s->info->name, "bdmv/bluray") == 0) &&
(still || (mpctx->video_status == STATUS_EOF &&
mpctx->audio_status == STATUS_EOF));
if (bd_idle || (nav.menu_active && mpctx->paused)) {
if (bd_idle)
demux_drive_nav(mpctx->demuxer);
mp_set_timeout(mpctx, 0.05);
}
}
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;
st->menu_saved_track = NULL;
st->overlay_change_seen = false;
if (st->menu_active_seen && st->last_menu_active) {
st->last_menu_active = false;
mp_notify_property(mpctx, "disc-menu-active");
}
return;
}
if (!st->menu_active_seen || nav.menu_active != st->last_menu_active) {
st->menu_active_seen = true;
st->last_menu_active = nav.menu_active;
mp_notify_property(mpctx, "disc-menu-active");
}
check_async_discontinuity(mpctx, s, &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 || (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);
ensure_menu_sub_selection(mpctx, nav.menu_active);
}
if (!st->overlay_change_seen || nav.change_id != st->last_overlay_change_id) {
st->overlay_change_seen = true;
st->last_overlay_change_id = nav.change_id;
osd_changed(mpctx->osd);
}
if (visible != st->overlay_visible) {
MP_VERBOSE(s, "overlay %s\n", visible ? "on" : "off");
if (visible) {
mp_input_enable_section(mpctx->input, "discnav",
MP_INPUT_ON_TOP | MP_INPUT_ALLOW_VO_DRAGGING | MP_INPUT_ALLOW_HIDE_CURSOR);
} else {
mp_input_disable_section(mpctx->input, "discnav");
}
st->overlay_visible = visible;
}
}