/* * 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 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; } }