stream_bluray: implement HDMV menu navigation

Add HDMV menu support to stream_bluray, activated by opening with
"bd://menu/..." or the global --disc-menu option.

The IG overlay plane arrives through the libbluray YUV overlay
callback. Decode the palettized RLE into a BGRA plane (premultiplied
alpha) and hand it to the player via STREAM_CTRL_GET_NAV_OVERLAY.
STREAM_CTRL_NAV_CMD maps arrow/select/menu/popup/mouse actions onto
bd_user_input() and bd_mouse_select(). Events relevant to the menu
state (title, playlist, menu on/off) are tracked so
STREAM_CTRL_GET_NAV_STATE can report an active menu, and playlist
changes bump discontinuity_id so the player flushes stale packets.
This commit is contained in:
Kacper Michajłow committed 2026-08-26 01:47:41 +02:00
1 parent b17feb1f79
commit 3a8b6995e9
1 file changed
+423 -11
+423 -11
View File
@@ -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,214 @@ 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
};
// 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;
int cols = ov->w;
while (x < ov->w && cols-- >= 0) {
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 +324,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 +360,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 +399,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 +421,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 +498,12 @@ 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 || !bd_menu_call(b->bd, -1))
return STREAM_UNSUPPORTED;
return STREAM_OK;
}
if (title >= b->num_titles || !play_title(b, title))
return STREAM_UNSUPPORTED;
b->current_title = title;
@@ -321,6 +612,99 @@ 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:
bd_mouse_select(b->bd, -1, nav->x, nav->y);
return STREAM_OK;
case STREAM_NAV_MOUSE_CLICK:
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 +847,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 +873,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 +925,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 +1018,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);