diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index b28f452675..3205c66c4a 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,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);