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