Files
mpv/stream/stream_bluray.c
Kacper Michajłow a47452aac0 stream_bluray: treat unannounced repositions as jumps
BD-J applets seek their player and restart playlists without any event
reaching the read loop, menu loop points do this constantly. Compare
bd_tell against the end of the previous read and treat a mismatch as a
jump. Checking before the read keeps the new position's head out of
the old slave demuxer, the forward-only stream cannot re-read it. A
read delivered together with a jump event can already carry data of
the new position, the same check drops it.
2026-08-26 01:47:41 +02:00

1498 lines
51 KiB
C

/*
* Copyright (C) 2010 Benjamin Zores <ben@geexbox.org>
*
* 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/>.
*/
/*
* Blu-ray parser/reader using libbluray
* Use 'git clone git://git.videolan.org/libbluray' to get it.
*
* TODO:
* - Add descrambled keys database support (KEYDB.cfg)
*
*/
#include <string.h>
#include <assert.h>
#include <libbluray/bluray.h>
#include <libbluray/meta_data.h>
#include <libbluray/overlay.h>
#include <libbluray/keys.h>
#include <libbluray/bluray-version.h>
#include <libbluray/log_control.h>
#include <libavutil/common.h>
#include "config.h"
#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 "sub/osd.h"
#include "sub/img_convert.h"
#include "video/csputils.h"
#include "video/mp_image.h"
#define BLURAY_SECTOR_SIZE 6144
#define BLURAY_DEFAULT_ANGLE 0
#define BLURAY_DEFAULT_CHAPTER 0
#define BLURAY_PLAYLIST_TITLE -3
#define BLURAY_DEFAULT_TITLE -2
#define BLURAY_MENU_TITLE -1
// 90khz ticks
#define BD_TIMEBASE (90000)
#define BD_TIME_TO_S(x) ((x) / (double)(BD_TIMEBASE))
#define BD_TIME_FROM_S(x) ((uint64_t)(x * BD_TIMEBASE))
// Interval between read retries while navigation is idle
#define BLURAY_POLL_TIME_S 0.010
// In menu mode, tolerate this many consecutive idle reads at the end of playlist
#define BLURAY_NAV_EOF_POLLS 100
// copied from aacs.h in libaacs
#define AACS_ERROR_CORRUPTED_DISC -1 /* opening or reading of AACS files failed */
#define AACS_ERROR_NO_CONFIG -2 /* missing config file */
#define AACS_ERROR_NO_PK -3 /* no matching processing key */
#define AACS_ERROR_NO_CERT -4 /* no valid certificate */
#define AACS_ERROR_CERT_REVOKED -5 /* certificate has been revoked */
#define AACS_ERROR_MMC_OPEN -6 /* MMC open failed (no MMC drive ?) */
#define AACS_ERROR_MMC_FAILURE -7 /* MMC failed */
#define AACS_ERROR_NO_DK -8 /* no matching device key */
#define OPT_BASE_STRUCT struct mp_bluray_opts
const struct m_sub_options stream_bluray_conf = {
.opts = (const struct m_option[]) {
{"device", OPT_STRING(bluray_device), .flags = M_OPT_FILE},
{"angle", OPT_INT(angle), M_RANGE(1, 999)},
{0},
},
.size = sizeof(struct mp_bluray_opts),
.defaults = &(const struct mp_bluray_opts){
.angle = 1,
},
};
// libbluray's support only global debug callback, without per-instance info.
static mp_static_mutex bluray_log_lock = MP_STATIC_MUTEX_INITIALIZER;
static struct mp_log *bluray_log;
static void bluray_logger(const char *msg)
{
mp_mutex_lock(&bluray_log_lock);
if (bluray_log)
mp_msg(bluray_log, MSGL_DEBUG, "%s", msg);
mp_mutex_unlock(&bluray_log_lock);
}
// One overlay plane (BGRA, premultiplied alpha).
struct bd_overlay_plane {
uint32_t *work;
uint32_t *publish;
int w, h; // current allocation size (0 if unallocated)
int disp_w, disp_h; // plane size announced by BD_OVERLAY_INIT
bool visible; // publish has any non-zero alpha pixel
};
struct bluray_priv_s {
BLURAY *bd;
struct mp_log *bluray_log;
bool probing; // open is an .iso auto-detection probe
BLURAY_TITLE_INFO *title_info;
int num_titles;
int current_angle;
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;
// Disc-menu support (enabled when cfg_title == BLURAY_MENU_TITLE).
// The HDMV graphics controller emits IG-plane primitives through
// bd_register_overlay_proc (YUV+RLE). BD-J titles bypass it entirely
// and emit fully-rendered ARGB on both PG and IG planes through
// bd_register_argb_overlay_proc. Discs that mix HDMV first-play with
// BD-J menus (or vice versa) need both callbacks registered.
bool hdmv_mode;
struct bd_overlay_plane ig;
struct bd_overlay_plane pg;
mp_mutex overlay_lock;
uint32_t nav_change_id; // bumped on overlay FLUSH/HIDE events
uint32_t discontinuity_id; // bumped on actions that may hop (SELECT...)
bool data_delivered; // any byte returned from fill_buffer yet
bool still_active; // holding an indefinite still.
uint64_t next_read_pos; // expected bd_tell() of the next read
bool read_pos_known; // next_read_pos is valid
bool resync_owed; // jump settled, resync seek not acked yet
// Disc-driven audio/sub selection, mirrored from BD_EVENT_AUDIO_STREAM
// and BD_EVENT_PG_TEXTST{,_STREAM}. The numbers are 1-based libbluray
// stream indices.
int audio_stream_num;
int sub_stream_num;
bool sub_visible;
};
// Lazy (re-)allocation for an overlay plane's working+publish buffer pair.
static bool bd_overlay_ensure(struct bluray_priv_s *priv,
struct bd_overlay_plane *p, int w, int h)
{
if (w <= 0 || h <= 0)
return false;
if (p->work && w <= p->w && h <= p->h)
return true;
int nw = MPMAX(w, p->w);
int nh = MPMAX(h, p->h);
size_t bytes = (size_t)nw * nh * 4;
uint32_t *work = talloc_realloc(priv, p->work, uint32_t, nw * nh);
uint32_t *pub = talloc_realloc(priv, p->publish, uint32_t, nw * nh);
if (!work || !pub)
return false;
memset(work, 0, bytes);
memset(pub, 0, bytes);
p->work = work;
p->publish = pub;
p->w = nw;
p->h = nh;
return true;
}
static void bd_overlay_clear(struct bd_overlay_plane *p)
{
if (p->work)
memset(p->work, 0, (size_t)p->w * p->h * 4);
}
static void bd_overlay_hide(struct bluray_priv_s *priv, struct bd_overlay_plane *p)
{
p->visible = false;
priv->nav_change_id++;
}
static void bd_overlay_flush(struct bluray_priv_s *priv, struct bd_overlay_plane *p)
{
if (!p->work)
return;
size_t n = (size_t)p->w * p->h;
memcpy(p->publish, p->work, n * 4);
bool any = false;
for (size_t i = 0; i < n; i++) {
if (p->publish[i] & 0xFF000000) {
any = true;
break;
}
}
p->visible = any;
priv->nav_change_id++;
}
static enum pl_color_system bd_overlay_csp(struct bluray_priv_s *priv)
{
const BLURAY_TITLE_INFO *ti = priv->title_info;
if (!ti || !ti->clip_count || !ti->clips[0].video_stream_count)
return PL_COLOR_SYSTEM_BT_709;
const BLURAY_STREAM_INFO *vs = &ti->clips[0].video_streams[0];
#if BLURAY_VERSION >= BLURAY_VERSION_CODE(1, 5, 0)
// HEVC on UHD BD (2160p or 1080p) can be either 2020 or 709
if (vs->coding_type == BLURAY_STREAM_TYPE_VIDEO_HEVC || vs->format == BLURAY_VIDEO_FORMAT_2160P)
return vs->color_space == BLURAY_COLOR_SPACE_BT2020 ? PL_COLOR_SYSTEM_BT_2020_NC : PL_COLOR_SYSTEM_BT_709;
#endif
switch (vs->format) {
#if BLURAY_VERSION <= BLURAY_VERSION_CODE(1, 4, 1)
case BLURAY_VIDEO_FORMAT_2160P:
return PL_COLOR_SYSTEM_BT_2020_NC;
#endif
case BLURAY_VIDEO_FORMAT_480I: // ITU-R BT.601
case BLURAY_VIDEO_FORMAT_576I:
case BLURAY_VIDEO_FORMAT_480P:
case BLURAY_VIDEO_FORMAT_576P:
return PL_COLOR_SYSTEM_BT_601;
default:
return PL_COLOR_SYSTEM_BT_709;
}
}
static void bd_palette_to_bgra(const BD_PG_PALETTE_ENTRY *pg, uint32_t out[256],
enum pl_color_system csp)
{
struct mp_csp_params params = MP_CSP_PARAMS_DEFAULTS;
params.repr.sys = csp;
params.repr.levels = PL_COLOR_LEVELS_LIMITED;
params.levels_out = PL_COLOR_LEVELS_FULL;
struct pl_transform3x3 yuv2rgb;
mp_get_csp_matrix(&params, &yuv2rgb);
for (int i = 0; i < 256; i++) {
int yuv[3] = { pg[i].Y, pg[i].Cb, pg[i].Cr };
int rgb[3];
mp_map_fixp_color(&yuv2rgb, 8, yuv, 8, rgb);
int T = pg[i].T;
// Pre-multiply RGB by alpha so the OSD layer can composite directly.
int r = rgb[0] * T / 255;
int g = rgb[1] * T / 255;
int b = rgb[2] * 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 bd_overlay_plane *p, const BD_OVERLAY *ov,
enum pl_color_system csp)
{
if (!ov->img || !ov->palette || ov->w <= 0 || ov->h <= 0)
return;
uint32_t pal[256];
bd_palette_to_bgra(ov->palette, pal, csp);
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 < p->h;
uint32_t *dst_row = in_plane ? p->work + (size_t)dst_y * p->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 > p->w)
run = p->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++;
}
}
// 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;
struct bd_overlay_plane *p = &priv->ig;
mp_mutex_lock(&priv->overlay_lock);
switch (ov->cmd) {
case BD_OVERLAY_INIT:
if (bd_overlay_ensure(priv, p, ov->w, ov->h)) {
// The allocation only ever grows, but the announced size is the
// coordinate space of the current menu.
p->disp_w = ov->w;
p->disp_h = ov->h;
}
bd_overlay_clear(p);
bd_overlay_hide(priv, p);
break;
case BD_OVERLAY_CLOSE:
case BD_OVERLAY_HIDE:
bd_overlay_hide(priv, p);
break;
case BD_OVERLAY_CLEAR:
bd_overlay_clear(p);
break;
case BD_OVERLAY_WIPE:
if (p->work) {
for (int y = 0; y < ov->h; y++) {
int dy = ov->y + y;
if (dy < 0 || dy >= p->h)
continue;
int dx = MPMAX(ov->x, 0);
int run = MPMIN(ov->w, p->w - dx);
if (run > 0)
memset(p->work + (size_t)dy * p->w + dx, 0, run * 4);
}
}
break;
case BD_OVERLAY_DRAW:
if (p->work)
bd_overlay_draw_rle(p, ov, bd_overlay_csp(priv));
break;
case BD_OVERLAY_FLUSH:
bd_overlay_flush(priv, p);
break;
default:
break;
}
mp_mutex_unlock(&priv->overlay_lock);
}
static inline uint32_t bd_argb_premul(uint32_t src)
{
uint32_t a = (src >> 24) & 0xFF;
if (a == 0)
return 0;
if (a == 255)
return src;
uint32_t r = (src >> 16) & 0xFF;
uint32_t g = (src >> 8) & 0xFF;
uint32_t b = src & 0xFF;
r = (r * a + 127) / 255;
g = (g * a + 127) / 255;
b = (b * a + 127) / 255;
return (a << 24) | (r << 16) | (g << 8) | b;
}
static void bd_overlay_draw_argb(struct bd_overlay_plane *p,
const BD_ARGB_OVERLAY *ov)
{
if (!ov->argb || ov->w <= 0 || ov->h <= 0)
return;
int sx = 0, sy = 0;
int dx = ov->x, dy = ov->y;
int w = ov->w, h = ov->h;
if (dx < 0) { sx -= dx; w += dx; dx = 0; }
if (dy < 0) { sy -= dy; h += dy; dy = 0; }
if (dx + w > p->w) w = p->w - dx;
if (dy + h > p->h) h = p->h - dy;
if (w <= 0 || h <= 0)
return;
for (int y = 0; y < h; y++) {
const uint32_t *src = ov->argb + (size_t)(sy + y) * ov->stride + sx;
uint32_t *dst = p->work + (size_t)(dy + y) * p->w + dx;
for (int x = 0; x < w; x++)
dst[x] = bd_argb_premul(src[x]);
}
}
// Called by libbluray when a BD-J title paints into either the PG or IG plane.
static void bd_argb_overlay_cb(void *handle, const BD_ARGB_OVERLAY *ov)
{
struct bluray_priv_s *priv = handle;
if (!ov)
return;
if (ov->plane != BD_OVERLAY_PG && ov->plane != BD_OVERLAY_IG)
return;
struct bd_overlay_plane *p = ov->plane == BD_OVERLAY_IG ? &priv->ig
: &priv->pg;
mp_mutex_lock(&priv->overlay_lock);
switch (ov->cmd) {
case BD_ARGB_OVERLAY_INIT:
if (bd_overlay_ensure(priv, p, ov->w, ov->h)) {
// The allocation only ever grows, but the announced size is the
// coordinate space of the current menu.
p->disp_w = ov->w;
p->disp_h = ov->h;
}
bd_overlay_clear(p);
bd_overlay_hide(priv, p);
break;
case BD_ARGB_OVERLAY_CLOSE:
bd_overlay_hide(priv, p);
break;
case BD_ARGB_OVERLAY_DRAW:
if (p->work)
bd_overlay_draw_argb(p, ov);
break;
case BD_ARGB_OVERLAY_FLUSH:
bd_overlay_flush(priv, p);
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);
}
inline static int play_title(struct bluray_priv_s *priv, int title)
{
return bd_select_title(priv->bd, title);
}
static void bluray_stream_close(stream_t *s)
{
struct bluray_priv_s *priv = s->priv;
if (!priv)
return;
if (priv->title_info)
bd_free_title_info(priv->title_info);
if (priv->bd) {
if (priv->hdmv_mode) {
bd_register_overlay_proc(priv->bd, NULL, NULL);
bd_register_argb_overlay_proc(priv->bd, NULL, NULL, NULL);
}
bd_close(priv->bd);
}
mp_mutex_lock(&bluray_log_lock);
// If we created the global log, unset it.
if (bluray_log == priv->bluray_log)
bluray_log = NULL;
mp_mutex_unlock(&bluray_log_lock);
mp_mutex_destroy(&priv->overlay_lock);
}
static const char *bd_event_str(uint32_t event)
{
#if BLURAY_VERSION >= BLURAY_VERSION_CODE(1, 3, 0)
const char *name = bd_event_name(event);
if (name)
return name;
#endif
return "?";
}
static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len)
{
struct bluray_priv_s *b = s->priv;
mp_mutex_lock(&b->overlay_lock);
uint32_t disc_id = b->discontinuity_id;
bool resync_owed = b->resync_owed;
mp_mutex_unlock(&b->overlay_lock);
// Resync pending, skip the read.
if (resync_owed)
return 0;
int idle_reads = 0;
bool hopped = false;
while (!mp_cancel_test(s->cancel)) {
BD_EVENT ev;
// BD-J applets reposition the stream, sometimes with no event.
// Detect it before reading so the new position's head survives.
if (!hopped && b->read_pos_known && bd_tell(b->bd) != b->next_read_pos) {
MP_VERBOSE(s, "position warped (%"PRIu64" -> %"PRIu64"), "
"treating as a jump\n", b->next_read_pos, bd_tell(b->bd));
b->read_pos_known = false;
mp_mutex_lock(&b->overlay_lock);
if (b->discontinuity_id == disc_id)
b->discontinuity_id++;
mp_mutex_unlock(&b->overlay_lock);
hopped = true;
}
// Once a jump was detected, stop consuming data (it belongs to the
// new position and the slave demuxer must be reopened first); a
// zero-length read only polls the remaining events.
int n = bd_read_ext(b->bd, buf, hopped ? 0 : len, &ev);
if (n < 0) {
MP_VERBOSE(s, "bd_read_ext() failed.\n");
return -1;
}
if (ev.event != BD_EVENT_NONE)
MP_DBG(s, "event %s(%u) param %u\n", bd_event_str(ev.event), ev.event, ev.param);
bool stalled = false; // EOF unless the VM continues
bool bdj_idle = false; // BD-J title alive with no playlist playing
switch (ev.event) {
case BD_EVENT_NONE:
stalled = n == 0;
break;
case BD_EVENT_ERROR:
MP_ERR(s, "Blu-ray navigation error (%u).\n", ev.param);
return -1;
case BD_EVENT_READ_ERROR:
MP_WARN(s, "Blu-ray read error, skipping unit.\n");
break;
case BD_EVENT_END_OF_TITLE:
mp_mutex_lock(&b->overlay_lock);
b->still_active = false;
mp_mutex_unlock(&b->overlay_lock);
stalled = true;
break;
case BD_EVENT_IDLE:
bdj_idle = true;
break;
case BD_EVENT_STILL:
mp_mutex_lock(&b->overlay_lock);
b->still_active = ev.param != 0;
mp_mutex_unlock(&b->overlay_lock);
break;
case BD_EVENT_STILL_TIME:
// TODO: consider timed stills support
// param == 0 is an indefinite still
if (ev.param == 0) {
mp_mutex_lock(&b->overlay_lock);
b->still_active = true;
mp_mutex_unlock(&b->overlay_lock);
} else {
bd_read_skip_still(b->bd);
}
break;
case BD_EVENT_PLAYLIST: {
int playlist = ev.param;
int 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] == ev.param) {
title = i;
break;
}
}
}
mp_mutex_lock(&b->overlay_lock);
int cur_angle = b->current_angle;
mp_mutex_unlock(&b->overlay_lock);
BLURAY_TITLE_INFO *ti = bd_get_playlist_info(b->bd, playlist, cur_angle);
mp_mutex_lock(&b->overlay_lock);
if (b->title_info)
bd_free_title_info(b->title_info);
b->title_info = ti;
b->current_playlist = playlist;
b->current_title = title;
if (b->hdmv_mode)
b->discontinuity_id++;
mp_mutex_unlock(&b->overlay_lock);
break;
}
case BD_EVENT_TITLE: {
int title = bd_get_current_title(b->bd);
mp_mutex_lock(&b->overlay_lock);
if (b->title_info) {
bd_free_title_info(b->title_info);
b->title_info = NULL;
}
b->current_title = title;
if (b->hdmv_mode)
b->discontinuity_id++;
mp_mutex_unlock(&b->overlay_lock);
break;
}
case BD_EVENT_ANGLE: {
// The event value is 1-based, current_angle is 0-based.
int angle = MPMAX(0, (int)ev.param - 1);
BLURAY_TITLE_INFO *ti = b->title_info ? bd_get_playlist_info(b->bd, b->current_playlist, angle)
: NULL;
mp_mutex_lock(&b->overlay_lock);
b->current_angle = angle;
if (ti) {
bd_free_title_info(b->title_info);
b->title_info = ti;
}
if (b->hdmv_mode)
b->nav_change_id++;
mp_mutex_unlock(&b->overlay_lock);
break;
}
case BD_EVENT_AUDIO_STREAM:
mp_mutex_lock(&b->overlay_lock);
b->audio_stream_num = ev.param;
if (b->hdmv_mode)
b->nav_change_id++;
mp_mutex_unlock(&b->overlay_lock);
break;
case BD_EVENT_PG_TEXTST_STREAM:
mp_mutex_lock(&b->overlay_lock);
b->sub_stream_num = ev.param;
if (b->hdmv_mode)
b->nav_change_id++;
mp_mutex_unlock(&b->overlay_lock);
break;
case BD_EVENT_PG_TEXTST:
mp_mutex_lock(&b->overlay_lock);
b->sub_visible = ev.param != 0;
if (b->hdmv_mode)
b->nav_change_id++;
mp_mutex_unlock(&b->overlay_lock);
break;
case BD_EVENT_DISCONTINUITY:
break;
default:
MP_TRACE(s, "Unhandled event: %s(%u) %u\n",
bd_event_str(ev.event), ev.event, ev.param);
break;
}
if (n > 0) {
// A jump event popped by this read can mean the data already
// belongs to the new position. Drop non-contiguous reads and
// drain like any other jump.
uint64_t pos = bd_tell(b->bd);
if (b->read_pos_known && pos - n != b->next_read_pos) {
MP_VERBOSE(s, "read not contiguous (expected %"PRIu64", "
"got %"PRIu64"), treating as a jump\n",
b->next_read_pos, pos - n);
b->read_pos_known = false;
mp_mutex_lock(&b->overlay_lock);
if (b->discontinuity_id == disc_id)
b->discontinuity_id++;
mp_mutex_unlock(&b->overlay_lock);
hopped = true;
continue;
}
if (!b->read_pos_known)
MP_DBG(s, "reads resume at %"PRIu64"\n", pos - n);
b->next_read_pos = pos;
b->read_pos_known = true;
if (b->still_active) {
mp_mutex_lock(&b->overlay_lock);
b->still_active = false;
mp_mutex_unlock(&b->overlay_lock);
}
b->data_delivered = true;
return n;
}
mp_mutex_lock(&b->overlay_lock);
bool still_active = b->still_active;
if (!hopped && b->data_delivered && b->discontinuity_id != disc_id) {
MP_VERBOSE(s, "disc jumped (id %u -> %u), draining events\n",
disc_id, b->discontinuity_id);
hopped = true;
}
mp_mutex_unlock(&b->overlay_lock);
// The play position jumped to another title/playlist after data was
// already delivered: report EOF so demux_disc reopens the slave
// demuxer before it parses data from the new position. A single jump
// queues a burst of events (TITLE, PLAYLIST, ...) of which more than
// one can bump discontinuity_id, so hold the EOF until the burst is
// fully drained: with a bump still queued, the slave demuxer
// reopened for this jump would hit that bump's EOF right away while
// probing the new position, and fail to open.
if (hopped) {
if (ev.event != BD_EVENT_NONE)
continue;
b->read_pos_known = false;
mp_mutex_lock(&b->overlay_lock);
b->resync_owed = true;
mp_mutex_unlock(&b->overlay_lock);
MP_VERBOSE(s, "jump settled (id %u), EOF for demuxer resync\n",
b->discontinuity_id);
return 0;
}
// Holding an indefinite still frame: report EOF. The player keeps
// showing the last frame; user interaction releases the still and
// resumes reading through a discontinuity_id bump.
if (still_active) {
// Drain queued events first, the still may already be released.
// libbluray re-emits STILL_TIME on every read while holding, it
// is not progress.
if (ev.event != BD_EVENT_NONE && ev.event != BD_EVENT_STILL_TIME)
continue;
MP_VERBOSE(s, "holding still frame, EOF\n");
return 0;
}
if (bdj_idle) {
idle_reads = 0;
mp_cancel_wait(s->cancel, BLURAY_POLL_TIME_S);
} else if (stalled) {
// Without menus there is nothing that could continue: plain EOF.
if (!b->hdmv_mode)
return 0;
// Retry for a while before concluding that playback ended. The
// first retry is immediate: a just-resumed VM progresses at once.
if (++idle_reads > BLURAY_NAV_EOF_POLLS) {
MP_VERBOSE(s, "Navigation stopped, EOF.\n");
return 0;
}
if (idle_reads > 1)
mp_cancel_wait(s->cancel, BLURAY_POLL_TIME_S);
} else {
// The VM/title made progress; read again immediately.
idle_reads = 0;
}
}
return 0;
}
static int bluray_stream_control(stream_t *s, int cmd, void *arg)
{
struct bluray_priv_s *b = s->priv;
switch (cmd) {
case STREAM_CTRL_GET_NUM_CHAPTERS: {
mp_mutex_lock(&b->overlay_lock);
const BLURAY_TITLE_INFO *ti = b->title_info;
if (ti)
*((unsigned int *) arg) = ti->chapter_count;
mp_mutex_unlock(&b->overlay_lock);
return ti ? STREAM_OK : STREAM_UNSUPPORTED;
}
case STREAM_CTRL_GET_CHAPTER_TIME: {
int chapter = *(double *)arg;
mp_mutex_lock(&b->overlay_lock);
const BLURAY_TITLE_INFO *ti = b->title_info;
int rc = STREAM_UNSUPPORTED;
if (ti) {
double time = MP_NOPTS_VALUE;
if (chapter >= 0 && chapter < ti->chapter_count)
time = BD_TIME_TO_S(ti->chapters[chapter].start);
if (time != MP_NOPTS_VALUE) {
*(double *)arg = time;
rc = STREAM_OK;
} else {
rc = STREAM_ERROR;
}
}
mp_mutex_unlock(&b->overlay_lock);
return rc;
}
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;
mp_mutex_lock(&b->overlay_lock);
b->current_title = title;
mp_mutex_unlock(&b->overlay_lock);
return STREAM_OK;
}
case STREAM_CTRL_GET_CURRENT_TITLE: {
mp_mutex_lock(&b->overlay_lock);
*((unsigned int *) arg) = b->current_title;
mp_mutex_unlock(&b->overlay_lock);
return STREAM_OK;
}
case STREAM_CTRL_GET_NUM_TITLES: {
*((unsigned int *)arg) = b->num_titles;
return STREAM_OK;
}
case STREAM_CTRL_GET_TIME_LENGTH: {
mp_mutex_lock(&b->overlay_lock);
const BLURAY_TITLE_INFO *ti = b->title_info;
if (ti)
*((double *) arg) = BD_TIME_TO_S(ti->duration);
mp_mutex_unlock(&b->overlay_lock);
return ti ? STREAM_OK : STREAM_UNSUPPORTED;
}
case STREAM_CTRL_GET_CURRENT_TIME: {
*((double *) arg) = BD_TIME_TO_S(bd_tell_time(b->bd));
return STREAM_OK;
}
case STREAM_CTRL_SEEK_TO_TIME: {
double pts = *((double *) arg);
bd_seek_time(b->bd, BD_TIME_FROM_S(pts));
stream_drop_buffers(s);
b->read_pos_known = false;
// API makes it hard to determine seeking success
return STREAM_OK;
}
case STREAM_CTRL_NAV_DRAIN_ACK:
mp_mutex_lock(&b->overlay_lock);
b->resync_owed = false;
mp_mutex_unlock(&b->overlay_lock);
return STREAM_OK;
case STREAM_CTRL_GET_NUM_ANGLES: {
mp_mutex_lock(&b->overlay_lock);
const BLURAY_TITLE_INFO *ti = b->title_info;
if (ti)
*((int *) arg) = ti->angle_count;
mp_mutex_unlock(&b->overlay_lock);
return ti ? STREAM_OK : STREAM_UNSUPPORTED;
}
case STREAM_CTRL_GET_ANGLE: {
mp_mutex_lock(&b->overlay_lock);
*((int *) arg) = b->current_angle + 1;
mp_mutex_unlock(&b->overlay_lock);
return STREAM_OK;
}
case STREAM_CTRL_SET_ANGLE: {
int angle = *((int *) arg);
mp_mutex_lock(&b->overlay_lock);
const BLURAY_TITLE_INFO *ti = b->title_info;
bool ok = ti && angle >= 1 && angle <= ti->angle_count;
if (ok)
b->current_angle = angle - 1;
int cur = b->current_angle;
mp_mutex_unlock(&b->overlay_lock);
if (!ok)
return STREAM_UNSUPPORTED;
bd_seamless_angle_change(b->bd, cur);
return STREAM_OK;
}
case STREAM_CTRL_GET_TITLE_LENGTH: {
int title = *(double *)arg;
if (!b->bd || title < 0 || title >= b->num_titles)
return STREAM_UNSUPPORTED;
BLURAY_TITLE_INFO *ti = bd_get_title_info(b->bd, title, 0);
if (!ti)
return STREAM_UNSUPPORTED;
*(double *)arg = BD_TIME_TO_S(ti->duration);
bd_free_title_info(ti);
return STREAM_OK;
}
case STREAM_CTRL_GET_TITLE_PLAYLIST: {
int title = *(double *)arg;
if (!b->bd || title < 0 || title >= b->num_titles)
return STREAM_UNSUPPORTED;
BLURAY_TITLE_INFO *ti = bd_get_title_info(b->bd, title, 0);
if (!ti)
return STREAM_UNSUPPORTED;
*(double *)arg = ti->playlist;
bd_free_title_info(ti);
return STREAM_OK;
}
case STREAM_CTRL_GET_LANG: {
int rc = STREAM_ERROR;
mp_mutex_lock(&b->overlay_lock);
const BLURAY_TITLE_INFO *ti = b->title_info;
if (ti && ti->clip_count) {
struct stream_lang_req *req = arg;
BLURAY_STREAM_INFO *si = NULL;
int count = 0;
switch (req->type) {
case STREAM_AUDIO:
count = ti->clips[0].audio_stream_count;
si = ti->clips[0].audio_streams;
break;
case STREAM_SUB:
count = ti->clips[0].pg_stream_count;
si = ti->clips[0].pg_streams;
break;
}
for (int n = 0; n < count; n++) {
BLURAY_STREAM_INFO *i = &si[n];
if (i->pid == req->id) {
snprintf(req->name, sizeof(req->name), "%.4s", i->lang);
rc = STREAM_OK;
break;
}
}
}
mp_mutex_unlock(&b->overlay_lock);
return rc;
}
case STREAM_CTRL_GET_DISC_NAME: {
const struct meta_dl *meta = bd_get_meta(b->bd);
if (!meta || !meta->di_name || !meta->di_name[0])
break;
*(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);
break;
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);
break;
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);
// If an activation just ran a button command, it may have released a
// held still, bump discontinuity_id.
if (stream_nav_action_activates(nav->action)) {
mp_mutex_lock(&b->overlay_lock);
if (b->still_active)
b->discontinuity_id++;
mp_mutex_unlock(&b->overlay_lock);
}
return STREAM_OK;
}
case STREAM_CTRL_GET_NAV_STATE: {
struct stream_nav_state *st = arg;
int audio_pid = -1;
int sub_pid = -1;
bool no_audio = false;
mp_mutex_lock(&b->overlay_lock);
const BLURAY_TITLE_INFO *ti = b->title_info;
if (ti && ti->clip_count) {
const BLURAY_CLIP_INFO *ci = &ti->clips[0];
no_audio = ci->audio_stream_count == 0;
if (b->audio_stream_num >= 1 &&
b->audio_stream_num <= ci->audio_stream_count)
{
audio_pid = ci->audio_streams[b->audio_stream_num - 1].pid;
}
if (b->sub_stream_num >= 1 &&
b->sub_stream_num <= ci->pg_stream_count)
{
sub_pid = ci->pg_streams[b->sub_stream_num - 1].pid;
}
}
if (!b->hdmv_mode) {
// Even outside HDMV we can carry disc-driven audio/sub/angle so
// the player tracks the disc author's defaults on a plain-title
// playback.
*st = (struct stream_nav_state){
.no_audio = no_audio,
.active_audio_id = audio_pid,
.active_sub_id = sub_pid,
.sub_visible = b->sub_visible,
.angle = b->current_angle + 1,
.num_angles = ti ? ti->angle_count : 0,
};
mp_mutex_unlock(&b->overlay_lock);
return STREAM_OK;
}
// There is no reliable "menu on screen" signal that covers both HDMV
// and BD-J (BD_EVENT_MENU is HDMV-only). Any visible overlay plane is
// treated as an active menu.
*st = (struct stream_nav_state){
.nav_active = true,
.menu_active = b->ig.visible || b->pg.visible,
.still_active = b->still_active,
.src_w = MPMAX(b->ig.disp_w, b->pg.disp_w),
.src_h = MPMAX(b->ig.disp_h, b->pg.disp_h),
.change_id = b->nav_change_id,
.discontinuity_id = b->discontinuity_id,
.drain_pending = b->resync_owed,
.no_audio = no_audio,
.active_audio_id = audio_pid,
.active_sub_id = sub_pid,
.sub_visible = b->sub_visible,
.angle = b->current_angle + 1,
.num_angles = ti ? ti->angle_count : 0,
};
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, MPMAX(b->ig.disp_w, b->pg.disp_w));
int copy_h = MPMIN(req->h, MPMAX(b->ig.disp_h, b->pg.disp_h));
const uint32_t *ig_src = b->ig.visible ? b->ig.publish : NULL;
const uint32_t *pg_src = b->pg.visible ? b->pg.publish : NULL;
for (int y = 0; y < copy_h; y++) {
uint32_t *dst = (uint32_t *)(req->dst + y * req->stride);
const uint32_t *ig_row = (ig_src && y < b->ig.h)
? ig_src + (size_t)y * b->ig.w : NULL;
const uint32_t *pg_row = (pg_src && y < b->pg.h)
? pg_src + (size_t)y * b->pg.w : NULL;
int ig_lim = ig_row ? b->ig.w : 0;
int pg_lim = pg_row ? b->pg.w : 0;
for (int x = 0; x < copy_w; x++) {
uint32_t ig = (x < ig_lim) ? ig_row[x] : 0;
uint32_t pg = (x < pg_lim) ? pg_row[x] : 0;
uint32_t ia = (ig >> 24) & 0xFF;
if (ia == 0) {
dst[x] = pg;
} else if (ia == 0xFF || !pg) {
dst[x] = ig;
} else {
// out = ig + pg * (1 - ig.a)
uint32_t inv = 255 - ia;
uint32_t na = ia + ((pg >> 24) & 0xFF) * inv / 255;
uint32_t nr = ((ig >> 16) & 0xFF) + ((pg >> 16) & 0xFF) * inv / 255;
uint32_t ng = ((ig >> 8) & 0xFF) + ((pg >> 8) & 0xFF) * inv / 255;
uint32_t nb = ( ig & 0xFF) + ( pg & 0xFF) * inv / 255;
dst[x] = (MPMIN(na, 255u) << 24) | (MPMIN(nr, 255u) << 16) |
(MPMIN(ng, 255u) << 8) | MPMIN(nb, 255u);
}
}
}
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;
}
return STREAM_UNSUPPORTED;
}
static const char *aacs_strerr(int err)
{
switch (err) {
case AACS_ERROR_CORRUPTED_DISC: return "opening or reading of AACS files failed";
case AACS_ERROR_NO_CONFIG: return "missing config file";
case AACS_ERROR_NO_PK: return "no matching processing key";
case AACS_ERROR_NO_CERT: return "no valid certificate";
case AACS_ERROR_CERT_REVOKED: return "certificate has been revoked";
case AACS_ERROR_MMC_OPEN: return "MMC open failed (maybe no MMC drive?)";
case AACS_ERROR_MMC_FAILURE: return "MMC failed";
case AACS_ERROR_NO_DK: return "no matching device key";
default: return "unknown error";
}
}
static bool check_disc_info(stream_t *s)
{
struct bluray_priv_s *b = s->priv;
const BLURAY_DISC_INFO *info = bd_get_disc_info(b->bd);
// check Blu-ray
if (!info->bluray_detected) {
if (!b->probing)
MP_ERR(s, "Given stream is not a Blu-ray.\n");
return false;
}
b->probing = false;
// check AACS
if (info->aacs_detected) {
if (!info->libaacs_detected) {
MP_ERR(s, "AACS encryption detected but cannot find libaacs.\n");
return false;
}
if (!info->aacs_handled) {
MP_ERR(s, "AACS error: %s\n", aacs_strerr(info->aacs_error_code));
return false;
}
}
// check BD+
if (info->bdplus_detected) {
if (!info->libbdplus_detected) {
MP_ERR(s, "BD+ encryption detected but cannot find libbdplus.\n");
return false;
}
if (!info->bdplus_handled) {
MP_ERR(s, "Cannot decrypt BD+ encryption.\n");
return false;
}
}
return true;
}
static void select_initial_title(stream_t *s, int title_guess) {
struct bluray_priv_s *b = s->priv;
if (b->cfg_title == BLURAY_PLAYLIST_TITLE) {
if (!play_playlist(b, b->cfg_playlist))
MP_WARN(s, "Couldn't start playlist '%05d'.\n", b->cfg_playlist);
b->current_title = bd_get_current_title(b->bd);
} else {
int title = -1;
if (b->cfg_title != BLURAY_DEFAULT_TITLE )
title = b->cfg_title;
else
title = title_guess;
if (title < 0)
return;
if (play_title(b, title))
b->current_title = title;
else {
MP_WARN(s, "Couldn't start title '%d'.\n", title);
b->current_title = bd_get_current_title(b->bd);
}
}
}
static int bluray_stream_open_internal(stream_t *s)
{
struct bluray_priv_s *b = s->priv;
struct m_config_cache *opts_cache =
m_config_cache_alloc(s, s->global, &stream_bluray_conf);
b->opts_cache = opts_cache;
b->opts = opts_cache->opts;
mp_mutex_init(&b->overlay_lock);
int ret = 0;
char *device = NULL;
/* find the requested device */
if (b->cfg_device && b->cfg_device[0]) {
device = b->cfg_device;
} else if (b->opts->bluray_device && b->opts->bluray_device[0]) {
device = b->opts->bluray_device;
} else {
device = DEFAULT_OPTICAL_DEVICE;
}
if (!device || !device[0]) {
MP_ERR(s, "No Blu-ray device/location was specified ...\n");
ret = STREAM_UNSUPPORTED;
goto err;
}
mp_mutex_lock(&bluray_log_lock);
// libbluray log callback is global and there is no way to separate it per
// instance, just replace with a new one if already present. The previous
// log stays alive (it is freed with the stream owning it), so a logging
// callback racing this replacement never sees a dangling pointer.
if (bluray_log) {
MP_WARN(s, "Replacing logger from previous instance.\n");
bluray_log = NULL;
}
uint32_t mask = 0;
if (mp_msg_test(s->log, MSGL_DEBUG))
mask |= DBG_CRIT | DBG_BLURAY | DBG_HDMV;
// When all bits are set (-1) libbluray inits to DBG_CRIT, set all bits,
// except one, so we avoid this default fallback.
if (mp_msg_test(s->log, MSGL_TRACE))
mask |= UINT32_MAX >> 1;
bd_set_debug_mask(mask);
if (mask)
b->bluray_log = bluray_log = mp_log_new(s, s->log, "/libbluray");
bd_set_debug_handler(bluray_logger);
mp_mutex_unlock(&bluray_log_lock);
/* open device */
char *device_tmp = mp_get_user_path(NULL, s->global, device);
BLURAY *bd = bd_open(device_tmp, NULL);
talloc_free(device_tmp);
if (!bd) {
if (!b->probing)
MP_ERR(s, "Couldn't open Blu-ray device: %s\n", device);
ret = STREAM_UNSUPPORTED;
goto err;
}
b->bd = bd;
if (!check_disc_info(s)) {
ret = STREAM_UNSUPPORTED;
goto err;
}
/* check for available titles on disc */
b->num_titles = bd_get_titles(bd, TITLES_RELEVANT, 0);
if (!b->num_titles) {
MP_ERR(s, "Can't find any Blu-ray-compatible title here.\n");
ret = STREAM_UNSUPPORTED;
goto err;
}
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);
talloc_free(time);
bd_free_title_info(ti);
}
// these should be set before any callback
b->current_angle = -1;
b->current_title = -1;
// initialize libbluray event queue
bd_get_event(bd, NULL);
const BLURAY_DISC_INFO *info = bd_get_disc_info(bd);
MP_VERBOSE(s, "First play: %i, Top menu: %i, "
"HDMV Titles: %i, BD-J Titles: %i, Other: %i\n",
info->first_play_supported, info->top_menu_supported,
info->num_hdmv_titles, info->num_bdj_titles,
info->num_unsupported_titles);
b->hdmv_mode = b->cfg_title == BLURAY_MENU_TITLE;
// BD-J menus require a usable Java VM and libbluray.jar.
if (b->hdmv_mode && info->bdj_detected && !info->bdj_handled) {
MP_WARN(s, "BD-J menus not supported. Playing without menus. "
"Java VM: %d, libbluray.jar: %d\n",
info->libjvm_detected, info->bdj_handled);
b->hdmv_mode = false;
b->cfg_title = BLURAY_DEFAULT_TITLE;
}
MP_VERBOSE(s, "bdnav: cfg_title=%d hdmv_mode=%d\n", b->cfg_title, b->hdmv_mode);
if (b->hdmv_mode) {
bd_register_overlay_proc(bd, b, bd_yuv_overlay_cb);
if (info->bdj_detected)
bd_register_argb_overlay_proc(bd, b, bd_argb_overlay_cb, NULL);
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));
}
// 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);
s->fill_buffer = bluray_stream_fill_buffer;
s->close = bluray_stream_close;
s->control = bluray_stream_control;
s->priv = b;
s->demuxer = "+disc";
MP_VERBOSE(s, "Blu-ray successfully opened.\n");
return STREAM_OK;
err:
bluray_stream_close(s);
return ret;
}
static int bluray_stream_open(stream_t *s)
{
struct bluray_priv_s *b = talloc_zero(s, struct bluray_priv_s);
s->priv = b;
bstr title, bdevice, rest = { .len = 0 };
bstr_split_tok(bstr0(s->path), "/", &title, &bdevice);
b->cfg_title = BLURAY_DEFAULT_TITLE;
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 (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")) {
bstr_split_tok(bdevice, "/", &title, &bdevice);
long long pl = bstrtoll(title, &rest, 10);
if (rest.len) {
MP_ERR(s, "number expected: '%.*s'\n", BSTR_P(rest));
return STREAM_ERROR;
} else if (pl < 0 || 99999 < pl) {
MP_ERR(s, "invalid playlist: '%.*s', must be in the range 0-99999\n",
BSTR_P(title));
return STREAM_ERROR;
}
b->cfg_playlist = pl;
b->cfg_title = BLURAY_PLAYLIST_TITLE;
} else if (title.len) {
long long t = bstrtoll(title, &rest, 10);
if (rest.len) {
MP_ERR(s, "number expected: '%.*s'\n", BSTR_P(rest));
return STREAM_ERROR;
} else if (t < 0 || 99999 < t) {
MP_ERR(s, "invalid title: '%.*s', must be in the range 0-99999\n",
BSTR_P(title));
return STREAM_ERROR;
}
b->cfg_title = t;
}
b->cfg_device = bstrto0(b, bdevice);
return bluray_stream_open_internal(s);
}
const stream_info_t stream_info_bluray = {
.name = "bd",
.open = bluray_stream_open,
.protocols = (const char*const[]){ "bd", "br", "bluray", NULL },
.stream_origin = STREAM_ORIGIN_UNSAFE,
};
static bool check_bdmv(const char *path)
{
if (strcasecmp(mp_basename(path), "MovieObject.bdmv"))
return false;
FILE *temp = fopen(path, "rb");
if (!temp)
return false;
char data[50];
bool ret = false;
if (fread(data, 50, 1, temp) == 1) {
bstr bdata = {data, 50};
ret = bstr_startswith0(bdata, "MOBJ0100") || // AVCHD
bstr_startswith0(bdata, "MOBJ0200") || // Blu-ray
bstr_startswith0(bdata, "MOBJ0300"); // UHD BD
}
fclose(temp);
return ret;
}
// Destructively remove the current trailing path component.
static void remove_prefix(char *path)
{
size_t len = strlen(path);
#if HAVE_DOS_PATHS
const char *seps = "/\\";
#else
const char *seps = "/";
#endif
while (len > 0 && !strchr(seps, path[len - 1]))
len--;
while (len > 0 && strchr(seps, path[len - 1]))
len--;
path[len] = '\0';
}
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 = default_title,
};
talloc_free(opts);
if (!stream->access_references)
goto unsupported;
char *path = mp_file_get_path(priv, bstr0(stream->url));
if (!path)
goto unsupported;
// Hand the .iso to libbluray as the device. Opening validates the disc
// info, so it doubles as the probe (see priv->probing).
if (bstr_case_endswith(bstr0(path), bstr0(".iso"))) {
priv->cfg_device = talloc_strdup(priv, path);
priv->probing = stream->autoprobed;
int r = bluray_stream_open_internal(stream);
if (r != STREAM_OK) {
if (priv->probing)
goto unsupported;
return r;
}
MP_INFO(stream, "Blu-ray ISO image detected. Redirecting to bluray://\n");
return r;
}
// We allow the path to point to a directory containing BDMV/, a
// directory containing MovieObject.bdmv, or that file itself.
if (!check_bdmv(path)) {
// On UNIX, just assume the filename has always this case.
char *npath = mp_path_join(priv, path, "MovieObject.bdmv");
if (!check_bdmv(npath)) {
npath = mp_path_join(priv, path, "BDMV/MovieObject.bdmv");
if (!check_bdmv(npath))
goto unsupported;
}
path = npath;
}
// Go up by 2 levels.
remove_prefix(path);
remove_prefix(path);
priv->cfg_device = path;
if (strlen(priv->cfg_device) <= 1)
goto unsupported;
MP_INFO(stream, "BDMV detected. Redirecting to bluray://\n");
return bluray_stream_open_internal(stream);
unsupported:
talloc_free(priv);
stream->priv = NULL;
return STREAM_UNSUPPORTED;
}
const stream_info_t stream_info_bdmv_dir = {
.name = "bdmv/bluray",
.open = bdmv_dir_stream_open,
.protocols = (const char*const[]){ "file", "", NULL },
.stream_origin = STREAM_ORIGIN_UNSAFE,
};