diff --git a/DOCS/interface-changes/disc-menu.txt b/DOCS/interface-changes/disc-menu.txt new file mode 100644 index 0000000000000..c3af55ce17be8 --- /dev/null +++ b/DOCS/interface-changes/disc-menu.txt @@ -0,0 +1,3 @@ +and `--disc-menu` option +add `disc-menu-active` property +add `discnav` command diff --git a/DOCS/interface-changes/el.txt b/DOCS/interface-changes/el.txt new file mode 100644 index 0000000000000..1486ba69713db --- /dev/null +++ b/DOCS/interface-changes/el.txt @@ -0,0 +1 @@ +add `vf=format=enhancement-layer` option diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 9d55d2c554bd8..990ff2b97978b 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -1753,6 +1753,45 @@ Miscellaneous Commands ``context-menu`` Show context menu on the video window. See `Context Menu`_ section for details. +``discnav [ ]`` + Send a navigation command to the optical-disc stream that is currently + playing (DVD via ``dvdnav``, Blu-ray via ``libbluray``). The command does + nothing when no disc is being played. + + ```` is one of: + + up, down, left, right + Move the on-disc button selection. + select + Activate the currently highlighted button. + menu + Jump to the disc's root menu (DVD: VMGM root menu, BD: HDMV top menu). + title-menu + Jump to the current title's menu (DVD only; treated like ``menu`` on BD). + popup + Show / dismiss the Blu-ray popup menu. For DVDs, falls back to the + chapter menu where available. + prev + Return from a menu to playback, or to the previous menu domain. + mouse-move + Forward a mouse position to the disc, so it can update the focused + button under the cursor. With no `` `` arguments, the position + is read from the ``mouse-pos`` property and mapped from window into + source-video coordinates automatically. + mouse-click + Like ``mouse-move`` but also activates the button under the cursor. + + ```` and ```` are optional and only meaningful for the ``mouse-*`` + actions. They should normalized cormalized coordinates (top-left at ``0,0``). + Those values are used directly, instead of the live ``mouse-pos``. + + mpv ships default bindings for this command in the ``{discnav}`` input + section (``UP``/``DOWN``/``LEFT``/``RIGHT`` for navigation, ``ENTER`` to + select, ``ESC`` / ``BS`` to leave, ``MBTN_LEFT`` / ``MOUSE_MOVE`` for + mouse). The player enables and disables that section automatically as + the menu appears and disappears, so the bindings only shadow the normal + ones while a menu is actually on screen. + ``update-clipboard [timeout]`` Update the clipboard content so that the ``clipboard`` property reflects up-to-date value. This command is required to update the ``clipboard`` @@ -2423,6 +2462,12 @@ Property list between having no editions and a single edition, which will be reflected by the property, although in practice it does not matter.) +``disc-menu-active`` + ``yes`` when the current optical-disc stream (DVD or Blu-ray) is showing + an interactive menu with a selectable button highlight, and ``no`` + otherwise. Unavailable when the currently playing source is not an + optical disc. + ``chapters`` Number of chapters. diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index d8c11bd995bcd..b4312107ee16d 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -131,6 +131,12 @@ Track Selection flat list. Note that depending on the file, tracks from different programs may be completely unrelated to each other. +``--disc-menu=`` + When set, opening ``dvd://`` or ``bd://`` boots into the disc's interactive + menu instead of automatically playing the longest title (default: ``no``). + The menu can also be reached at any time via the synthetic "Disc Menu" + entry in the editions/titles list, or with ``discnav menu`` command. + ``--show-dependent-tracks=`` Show dependent tracks in the track list (default: no). Dependent tracks carry coded data that is not independently decodable. For example, the diff --git a/DOCS/man/vf.rst b/DOCS/man/vf.rst index 3e4d9c7b2ea71..f0620ca905d23 100644 --- a/DOCS/man/vf.rst +++ b/DOCS/man/vf.rst @@ -323,6 +323,12 @@ Available mpv-only filters are: Whether or not to include HDR10+ metadata (default: yes). If disabled, any HDR10+ metadata will be stripped from frames. + ```` + Whether or not to apply the image enhancement layer (default: yes). + If disabled, the enhancement-layer frame paired with each base-layer + frame is discarded. Currently this controls Dolby Vision Profile 7 FEL + application. + ```` Set the minimum luminance value for the mastering display metadata. This is a float value in nits (cd/m²). diff --git a/demux/demux.c b/demux/demux.c index 69008829c4c49..f7379333fca1f 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -209,6 +209,8 @@ struct demux_internal { double hyst_secs; // stop reading till there's hyst_secs remaining size_t hyst_bytes; // stop reading till there's hyst_bytes remaining bool hyst_active; + int64_t nav_pump_deadline; // Deadline (mp_time_ns) until which reading is + // forced past the readahead size_t max_bytes; size_t max_bytes_bw; bool seekable_cache; @@ -1249,6 +1251,18 @@ void demux_start_prefetch(struct demuxer *demuxer) mp_mutex_unlock(&in->lock); } +void demux_drive_nav(struct demuxer *demuxer) +{ + struct demux_internal *in = demuxer->in; + mp_assert(demuxer == in->d_user); + + mp_mutex_lock(&in->lock); + in->nav_pump_deadline = mp_time_ns() + MP_TIME_S_TO_NS(2); + in->reading = true; + mp_cond_signal(&in->wakeup); + mp_mutex_unlock(&in->lock); +} + const char *stream_type_name(enum stream_type type) { switch (type) { @@ -2267,6 +2281,10 @@ static bool read_packet(struct demux_internal *in) if (!was_reading || in->blocked || demux_cancel_test(in->d_thread)) return false; + bool nav_pump = in->nav_pump_deadline && mp_time_ns() < in->nav_pump_deadline; + if (in->nav_pump_deadline && !nav_pump) + in->nav_pump_deadline = 0; + // Check if we need to read a new packet. We do this if all queues are below // the minimum, or if a stream explicitly needs new packets. Also includes // safe-guards against packet queue overflow. @@ -2304,6 +2322,11 @@ static bool read_packet(struct demux_internal *in) prefetch_more |= true; } + // Keep reading regardless of cache fill level so the stream's DVD/BD VM + // gets driven. This is important in cases, where we would receive + // discontinuity reset after VM control, but we have to pump the events first. + prefetch_more |= nav_pump; + MP_TRACE(in, "bytes=%zd, read_more=%d prefetch_more=%d, refresh_more=%d\n", (size_t)total_fw_bytes, read_more, prefetch_more, refresh_more); if (total_fw_bytes >= in->max_bytes) { @@ -3075,7 +3098,7 @@ static void demux_update_replaygain(demuxer_t *demuxer) if (!rg) rg = decode_rgain(demuxer->log, demuxer->metadata); if (rg) - sh->codec->replaygain_data = talloc_steal(in, rg); + sh->codec->replaygain_data = talloc_steal(sh->codec, rg); } } } @@ -3181,6 +3204,41 @@ void demux_metadata_changed(demuxer_t *demuxer) mp_mutex_unlock(&in->lock); } +// Updates the duration should it need to be changed. Used for demuxers that +// changes titles/playlists at runtime. +void demux_set_duration(demuxer_t *demuxer, double duration) +{ + mp_assert(demuxer == demuxer->in->d_thread); + struct demux_internal *in = demuxer->in; + + mp_mutex_lock(&in->lock); + in->duration = duration; + in->d_thread->duration = duration; + // Clear the high-water mark so subsequent packets can re-ratchet duration + // upward from the new playlist's PTS base without being shadowed by the + // previous title's value. + in->highest_av_pts = MP_NOPTS_VALUE; + in->events |= DEMUX_EVENT_DURATION; + mp_mutex_unlock(&in->lock); +} + +// Updates the chapters/editions should it need to be changed. Used for demuxers +// that changes titles/playlists at runtime. +void demux_lists_changed(demuxer_t *demuxer) +{ + mp_assert(demuxer == demuxer->in->d_thread); + struct demux_internal *in = demuxer->in; + + mp_mutex_lock(&in->lock); + in->d_user->chapters = in->d_thread->chapters; + in->d_user->num_chapters = in->d_thread->num_chapters; + in->d_user->editions = in->d_thread->editions; + in->d_user->num_editions = in->d_thread->num_editions; + in->d_user->edition = in->d_thread->edition; + in->events |= DEMUX_EVENT_LISTS; + mp_mutex_unlock(&in->lock); +} + // Called locked, with user demuxer. static void update_final_metadata(demuxer_t *demuxer, struct timed_metadata *tm) { @@ -4145,11 +4203,11 @@ void demuxer_select_track(struct demuxer *demuxer, struct sh_stream *stream, struct demux_internal *in = demuxer->in; mp_mutex_lock(&in->lock); bool changed = select_track(in, stream, ref_pts, selected); - if (stream->group) { + if (stream->group && !stream->dependent_track) { for (int i = 0; i < stream->group->num_members; i++) { struct sh_stream *m = stream->group->members[i]; mp_assert(m); - if (m != stream) + if (m != stream && m->dependent_track) changed |= select_track(in, m, ref_pts, selected); } } diff --git a/demux/demux.h b/demux/demux.h index 6fabf64b9940e..fd9eb6824c85d 100644 --- a/demux/demux.h +++ b/demux/demux.h @@ -121,6 +121,7 @@ enum demux_event { DEMUX_EVENT_STREAMS = 1 << 1, // a stream was added DEMUX_EVENT_METADATA = 1 << 2, // metadata or stream_metadata changed DEMUX_EVENT_DURATION = 1 << 3, // duration updated + DEMUX_EVENT_LISTS = 1 << 4, // chapters / editions list changed DEMUX_EVENT_ALL = 0xFFFF, }; @@ -320,6 +321,7 @@ void demux_start_thread(struct demuxer *demuxer); void demux_stop_thread(struct demuxer *demuxer); void demux_set_wakeup_cb(struct demuxer *demuxer, void (*cb)(void *ctx), void *ctx); void demux_start_prefetch(struct demuxer *demuxer); +void demux_drive_nav(struct demuxer *demuxer); bool demux_cancel_test(struct demuxer *demuxer); @@ -348,6 +350,8 @@ void demux_stream_tags_changed(struct demuxer *demuxer, struct sh_stream *sh, void demux_close_stream(struct demuxer *demuxer); void demux_metadata_changed(demuxer_t *demuxer); +void demux_set_duration(demuxer_t *demuxer, double duration); +void demux_lists_changed(demuxer_t *demuxer); void demux_update(demuxer_t *demuxer, double playback_pts); bool demux_cache_dump_set(struct demuxer *demuxer, double start, double end, diff --git a/demux/demux_disc.c b/demux/demux_disc.c index 377663c11a2f6..f4cd0581543c4 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -29,37 +29,54 @@ #include "video/csputils.h" +// DVD-Video has 32 subpicture (SPU) streams, mapped to PES substream IDs 0x20..0x3F. +#define MAX_DVD_SPU_STREAMS 32 + +// If the timestamp difference between subsequent packets is this big, assume +// a reset. It should be big enough to account for 1. low video framerates and +// large audio frames, and 2. bad interleaving. +#define DTS_RESET_THRESHOLD 5.0 + struct priv { struct demuxer *slave; - // streams[slave_stream_index] == our_stream - struct sh_stream **streams; - int num_streams; - // This contains each DVD sub stream, or NULL. Needed because DVD packets - // can come arbitrarily late in the MPEG stream, so the slave demuxer - // might add the streams only later. - struct sh_stream *dvd_subs[32]; + + // All outer sh_streams we have ever surfaced to the parent demuxer. + struct sh_stream **outer_streams; + int num_outer_streams; + + // Maps the current slave's stream index to its matching outer sh_stream. + struct sh_stream **slave_to_outer; + int slave_to_outer_count; + + // Per slave-stream-index flag: when set, the next packet from that slave + // stream is tagged segmented + pkt->codec=outer->codec so the decoder + // wrapper reinitialises for the freshly-refreshed codec. + bool *needs_segment_marker; + int needs_segment_marker_count; + + // DVD-only: pre-registered sub streams keyed by PES substream ID minus + // 0x20, carrying the disc-level CLUT as extradata. + struct sh_stream *dvd_subs[MAX_DVD_SPU_STREAMS]; + // Used to rewrite the raw MPEG timestamps to playback time. double base_time; // playback display start time of current segment double base_dts; // packet DTS that maps to base_time double last_dts; // DTS of previously demuxed packet bool seek_reinit; // needs reinit after seek + uint32_t last_discontinuity_id; // Last source-position-jump id seen from the stream. bool is_dvd, is_cdda; }; -// If the timestamp difference between subsequent packets is this big, assume -// a reset. It should be big enough to account for 1. low video framerates and -// large audio frames, and 2. bad interleaving. -#define DTS_RESET_THRESHOLD 5.0 - static void reselect_streams(demuxer_t *demuxer) { struct priv *p = demuxer->priv; int num_slave = demux_get_num_stream(p->slave); - for (int n = 0; n < MPMIN(num_slave, p->num_streams); n++) { - if (p->streams[n]) { + for (int n = 0; n < num_slave && n < p->slave_to_outer_count; n++) { + struct sh_stream *outer = p->slave_to_outer[n]; + if (outer) { demuxer_select_track(p->slave, demux_get_stream(p->slave, n), - MP_NOPTS_VALUE, demux_stream_is_selected(p->streams[n])); + MP_NOPTS_VALUE, demux_stream_is_selected(outer)); } } } @@ -82,15 +99,13 @@ static void add_dvd_streams(demuxer_t *demuxer) return; struct stream_dvd_info_req info; if (stream_control(stream, STREAM_CTRL_GET_DVD_INFO, &info) > 0) { - for (int n = 0; n < MPMIN(32, info.num_subs); n++) { + for (int n = 0; n < MPMIN(MAX_DVD_SPU_STREAMS, info.num_subs); n++) { struct sh_stream *sh = demux_alloc_sh_stream(STREAM_SUB); sh->demuxer_id = n + 0x20; sh->codec->codec = "dvd_subtitle"; get_disc_lang(stream, sh, true); - // p->streams _must_ match with p->slave->streams, so we can't add - // it yet - it has to be done when the real stream appears, which - // could be right on start, or any time later. p->dvd_subs[n] = sh; + MP_TARRAY_APPEND(p, p->outer_streams, p->num_outer_streams, sh); // emulate the extradata struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS; @@ -120,44 +135,130 @@ static void add_dvd_streams(demuxer_t *demuxer) } } -static void add_streams(demuxer_t *demuxer) +// Take ownership of a slave sh_stream's codec params into the outer demuxer +// so it survives a slave reopen. +static void adopt_codec_params(struct sh_stream *outer, struct sh_stream *src) +{ + if (outer->codec != src->codec) { + if (!outer->ds) + talloc_free(outer->codec); + outer->codec = src->codec; + talloc_steal(outer, outer->codec); + } + outer->codec->first_packet = NULL; + outer->codec->decoder = NULL; + outer->codec->decoder_desc = NULL; +} + +static struct sh_stream *find_outer_for_slave(struct priv *p, + struct sh_stream *src) +{ + if (src->type == STREAM_SUB && src->demuxer_id >= 0x20 && + src->demuxer_id <= 0x3F) + { + struct sh_stream *sub = p->dvd_subs[src->demuxer_id - 0x20]; + if (sub) + return sub; + } + for (int i = 0; i < p->num_outer_streams; i++) { + struct sh_stream *sh = p->outer_streams[i]; + if (sh && sh->type == src->type && sh->demuxer_id == src->demuxer_id) + return sh; + } + return NULL; +} + +// Build / rebuild the slave-index -> outer-sh map. For each slave stream reuse +// or register a fresh outer sh_stream as follows and expose it to the parent demuxer. +static void sync_streams(struct demuxer *demuxer) { struct priv *p = demuxer->priv; + int num_slave = demux_get_num_stream(p->slave); + + if (num_slave > p->slave_to_outer_count) { + MP_TARRAY_GROW(p, p->slave_to_outer, num_slave - 1); + MP_TARRAY_GROW(p, p->needs_segment_marker, num_slave - 1); + for (int n = p->slave_to_outer_count; n < num_slave; n++) { + p->slave_to_outer[n] = NULL; + p->needs_segment_marker[n] = false; + } + p->slave_to_outer_count = num_slave; + p->needs_segment_marker_count = num_slave; + } - for (int n = p->num_streams; n < demux_get_num_stream(p->slave); n++) { + for (int n = 0; n < num_slave; n++) { struct sh_stream *src = demux_get_stream(p->slave, n); - if (src->type == STREAM_SUB) { - struct sh_stream *sub = NULL; - if (src->demuxer_id >= 0x20 && src->demuxer_id <= 0x3F) - sub = p->dvd_subs[src->demuxer_id - 0x20]; - if (sub) { - mp_assert(p->num_streams == n); // directly mapped - MP_TARRAY_APPEND(p, p->streams, p->num_streams, sub); - continue; + struct sh_stream *outer = find_outer_for_slave(p, src); + + if (!outer) { + outer = demux_alloc_sh_stream(src->type); + adopt_codec_params(outer, src); + outer->demuxer_id = src->demuxer_id; + outer->dependent_track = src->dependent_track; + if (src->type == STREAM_VIDEO) { + double ar; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_ASPECT_RATIO, &ar) + == STREAM_OK) + { + struct mp_image_params f = {.w = src->codec->disp_w, + .h = src->codec->disp_h}; + mp_image_params_set_dsize(&f, 1728 * ar, 1728); + outer->codec->par_w = f.p_w; + outer->codec->par_h = f.p_h; + } } - } - struct sh_stream *sh = demux_alloc_sh_stream(src->type); - mp_assert(p->num_streams == n); // directly mapped - MP_TARRAY_APPEND(p, p->streams, p->num_streams, sh); - // Copy all stream fields that might be relevant - *sh->codec = *src->codec; - sh->demuxer_id = src->demuxer_id; - if (src->type == STREAM_VIDEO) { - double ar; - if (stream_control(demuxer->stream, STREAM_CTRL_GET_ASPECT_RATIO, &ar) - == STREAM_OK) - { - struct mp_image_params f = {.w = src->codec->disp_w, - .h = src->codec->disp_h}; - mp_image_params_set_dsize(&f, 1728 * ar, 1728); - sh->codec->par_w = f.p_w; - sh->codec->par_h = f.p_h; + get_disc_lang(demuxer->stream, outer, p->is_dvd); + MP_TARRAY_APPEND(p, p->outer_streams, p->num_outer_streams, outer); + demux_add_sh_stream(demuxer, outer); + } else if (outer->type != STREAM_SUB && outer->codec && src->codec) { + // Codec change on a reused outer, mostly useful for BD menus, which + // may be MPEG-2 while the video track is H.264. + const char *new_codec = src->codec->codec; + const char *cur_codec = outer->codec->codec; + if (new_codec && cur_codec && strcmp(new_codec, cur_codec) != 0) { + MP_VERBOSE(demuxer, "stream %d codec changed: %s -> %s\n", + n, cur_codec, new_codec); + adopt_codec_params(outer, src); + p->needs_segment_marker[n] = true; } } - get_disc_lang(demuxer->stream, sh, p->is_dvd); - demux_add_sh_stream(demuxer, sh); + + p->slave_to_outer[n] = outer; + } + + // Propagate outer selection state to the slave. + for (int n = 0; n < num_slave; n++) { + struct sh_stream *outer = p->slave_to_outer[n]; + if (outer) { + demuxer_select_track(p->slave, demux_get_stream(p->slave, n), + MP_NOPTS_VALUE, demux_stream_is_selected(outer)); + } + } + + // Mirror slave sh_stream_group onto the outer sh_streams. This is needed + // for the Dolby Vision BL+EL group, it's detected well by lavf. We could use + // the libbluray `dv_streams[]` info, but it's not available yet in release + // version, and mapping it through lavf is less code. + for (int n = 0; n < num_slave; n++) { + struct sh_stream *outer = p->slave_to_outer[n]; + if (!outer || outer->group) + continue; + struct sh_stream *src = demux_get_stream(p->slave, n); + if (!src || !src->group) + continue; + struct sh_stream_group *grp = talloc_zero(outer, struct sh_stream_group); + for (int m = 0; m < src->group->num_members; m++) { + struct sh_stream *member = src->group->members[m]; + if (!member || member->index < 0 || + member->index >= p->slave_to_outer_count) + continue; + struct sh_stream *outer_member = p->slave_to_outer[member->index]; + if (!outer_member) + continue; + MP_TARRAY_APPEND(grp, grp->members, grp->num_members, outer_member); + outer_member->group = grp; + } } - reselect_streams(demuxer); } static void d_seek(demuxer_t *demuxer, double seek_pts, int flags) @@ -201,31 +302,144 @@ static void reset_pts(demuxer_t *demuxer) p->seek_reinit = false; } +static void add_stream_chapters(struct demuxer *demuxer); + +// Sync demuxer->edition with the disc's current playback position. The disc +// nav state takes precedence: if a menu is active, point at the synthetic +// "Disc Menu" entry add_stream_editions() appended at num_editions - 1; +// otherwise mirror the stream's GET_CURRENT_TITLE. +static void sync_initial_edition(struct demuxer *demuxer) +{ + unsigned title; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_CURRENT_TITLE, &title) >= 1) + demuxer->edition = title; + struct stream_nav_state nav = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1 + && nav.menu_active && demuxer->num_editions > 0) + { + demuxer->edition = demuxer->num_editions - 1; + } +} + +static bool reopen_slave(struct demuxer *demuxer) +{ + struct priv *p = demuxer->priv; + + struct demuxer_params params = { + .force_format = "+lavf", + .external_stream = demuxer->stream, + .stream_flags = demuxer->stream_origin, + .depth = demuxer->depth + 1, + }; + if (p->is_cdda) + params.force_format = "+rawaudio"; + + demux_free(p->slave); + // Discard anything the stream wrapper buffered before the disc-nav + // discontinuity. + stream_drop_buffers(demuxer->stream); + p->slave = demux_open_url("-", ¶ms, demuxer->cancel, demuxer->global); + if (!p->slave) { + MP_ERR(demuxer, "Failed to reopen slave demuxer after discontinuity\n"); + return false; + } + + for (int n = 0; n < p->slave_to_outer_count; n++) { + p->slave_to_outer[n] = NULL; + p->needs_segment_marker[n] = false; + } + + sync_streams(demuxer); + + // Refresh duration / chapters / edition for the new playlist. + double len; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1) + demux_set_duration(demuxer, len); + else + demux_set_duration(demuxer, -1); + + for (int n = 0; n < demuxer->num_chapters; n++) + talloc_free(demuxer->chapters[n].metadata); + demuxer->num_chapters = 0; + add_stream_chapters(demuxer); + + sync_initial_edition(demuxer); + + demux_lists_changed(demuxer); + + return true; +} + static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt) { struct priv *p = demuxer->priv; + struct stream_nav_state nav = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1 && + nav.discontinuity_id != p->last_discontinuity_id) + { + MP_VERBOSE(demuxer, "discontinuity %u->%u, reopening slave\n", + p->last_discontinuity_id, nav.discontinuity_id); + if (!reopen_slave(demuxer)) + return false; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) + p->last_discontinuity_id = nav.discontinuity_id; + p->seek_reinit = true; + } + struct demux_packet *pkt = demux_read_any_packet(p->slave); - if (!pkt) - return false; + if (!pkt) { + // The slave can hit EOF mid-playback when the stream layer breaks + // its read at a disc-driven discontinuity. + struct stream_nav_state nav2 = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav2) >= 1 + && nav2.discontinuity_id != p->last_discontinuity_id) + { + MP_VERBOSE(demuxer, "discontinuity %u->%u at EOF, reopening slave\n", + p->last_discontinuity_id, nav2.discontinuity_id); + if (!reopen_slave(demuxer)) + return false; + p->last_discontinuity_id = nav2.discontinuity_id; + p->seek_reinit = true; + pkt = demux_read_any_packet(p->slave); + } + if (!pkt) + return false; + } demux_update(p->slave, MP_NOPTS_VALUE); if (p->seek_reinit) reset_pts(demuxer); - add_streams(demuxer); - if (pkt->stream >= p->num_streams) { // out of memory? - talloc_free(pkt); - return true; + int slave_index = pkt->stream; + if (demux_get_num_stream(p->slave) > p->slave_to_outer_count || + slave_index >= p->slave_to_outer_count || + !p->slave_to_outer[slave_index]) + { + sync_streams(demuxer); } - struct sh_stream *sh = p->streams[pkt->stream]; - if (!demux_stream_is_selected(sh)) { + struct sh_stream *sh = slave_index < p->slave_to_outer_count + ? p->slave_to_outer[slave_index] : NULL; + if (!sh || !demux_stream_is_selected(sh)) { talloc_free(pkt); return true; } + // First packet from a slave stream whose matched outer just had its + // codec refreshed gets tagged as a new segment so f_decoder_wrapper + // drains and reinits the decoder. + if (slave_index < p->needs_segment_marker_count && + p->needs_segment_marker[slave_index]) + { + p->needs_segment_marker[slave_index] = false; + pkt->segmented = true; + pkt->codec = sh->codec; + pkt->start = MP_NOPTS_VALUE; + pkt->end = MP_NOPTS_VALUE; + } + pkt->stream = sh->index; if (p->is_cdda) { @@ -248,7 +462,7 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt p->last_dts = pkt->dts; if (fabs(p->last_dts - pkt->dts) >= DTS_RESET_THRESHOLD) { - MP_WARN(demuxer, "PTS discontinuity: %f->%f\n", p->last_dts, pkt->dts); + MP_VERBOSE(demuxer, "PTS discontinuity: %f->%f\n", p->last_dts, pkt->dts); p->base_time += p->last_dts - p->base_dts; p->base_dts = pkt->dts - pkt->duration; } @@ -294,6 +508,15 @@ static void add_stream_editions(struct demuxer *demuxer) mp_tprintf(42, "title: %u (%s)", title + 1, time)); talloc_free(time); } + + // Append a synthetic "Disc Menu" entry. + struct demux_edition menu = { + .demuxer_id = titles, + .default_edition = false, + .metadata = talloc_zero(demuxer, struct mp_tags), + }; + MP_TARRAY_APPEND(demuxer, demuxer->editions, demuxer->num_editions, menu); + mp_tags_set_str(menu.metadata, "TITLE", "Disc Menu"); } static void add_stream_chapters(struct demuxer *demuxer) @@ -353,9 +576,11 @@ static int d_open(demuxer_t *demuxer, enum demux_check check) // Can be seekable even if the stream isn't. demuxer->seekable = true; + // Partially seekable to refresh seek on track changes. + demuxer->partially_seekable = true; add_dvd_streams(demuxer); - add_streams(demuxer); + sync_streams(demuxer); add_stream_chapters(demuxer); add_stream_editions(demuxer); @@ -363,9 +588,7 @@ static int d_open(demuxer_t *demuxer, enum demux_check check) if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1) demuxer->duration = len; - unsigned title; - if (stream_control(demuxer->stream, STREAM_CTRL_GET_CURRENT_TITLE, &title) >= 1) - demuxer->edition = title; + sync_initial_edition(demuxer); return 0; } diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index c8f134c47cae1..988f65899cf7a 100644 --- a/demux/demux_lavf.c +++ b/demux/demux_lavf.c @@ -52,6 +52,7 @@ #include "stream/stream_curl.h" #include "demux.h" +#include "dovi_split.h" #include "stheader.h" #include "options/m_config.h" #include "options/m_option.h" @@ -66,6 +67,11 @@ // libavformat (almost) always reads data in blocks of this size. #define BIO_BUFFER_SIZE 32768 +static void avcodec_par_destructor(void *p) +{ + avcodec_parameters_free(p); +} + #define OPT_BASE_STRUCT struct demux_lavf_opts struct demux_lavf_opts { int probesize; @@ -224,12 +230,14 @@ struct stream_info { double last_key_pts; double highest_pts; double ts_offset; + struct mp_dovi_split *dovi_split; }; typedef struct lavf_priv { struct stream *stream; bool own_stream; bool is_dvd_bd; + bool is_dvd; char *filename; struct format_hack format_hack; const AVInputFormat *avif; @@ -252,6 +260,8 @@ typedef struct lavf_priv { int retry_counter; + struct demux_packet *pending_pkt; + AVDictionary *av_opts; // Proxying nested streams. @@ -600,6 +610,12 @@ static void select_tracks(struct demuxer *demuxer, int start) AVStream *st = priv->avfc->streams[n]; bool selected = stream && demux_stream_is_selected(stream) && !stream->attached_picture; + if (!selected && priv->streams[n]->dovi_split) { + struct sh_stream *el = + mp_dovi_split_el_stream(priv->streams[n]->dovi_split); + if (el && demux_stream_is_selected(el)) + selected = true; + } st->discard = selected ? AVDISCARD_DEFAULT : AVDISCARD_ALL; } } @@ -630,7 +646,7 @@ static void export_replaygain(demuxer_t *demuxer, struct sh_stream *sh, if (!track_data_available && !album_data_available) return; - struct replaygain_data *rgain = talloc_ptrtype(demuxer, rgain); + struct replaygain_data *rgain = talloc_ptrtype(sh->codec, rgain); rgain->track_gain = rgain->album_gain = 0; rgain->track_peak = rgain->album_peak = 1; @@ -708,7 +724,7 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh->codec->samplerate = codec->sample_rate; sh->codec->bitrate = codec->bit_rate; - sh->codec->format_name = talloc_strdup(sh, av_get_sample_fmt_name(codec->format)); + sh->codec->format_name = talloc_strdup(sh->codec, av_get_sample_fmt_name(codec->format)); double delay = 0; if (codec->sample_rate > 0) @@ -748,7 +764,7 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh->codec->disp_w = codec->width; sh->codec->disp_h = codec->height; sh->codec->bitrate = codec->bit_rate; - sh->codec->format_name = talloc_strdup(sh, av_get_pix_fmt_name(codec->format)); + sh->codec->format_name = talloc_strdup(sh->codec, av_get_pix_fmt_name(codec->format)); if (st->avg_frame_rate.num) sh->codec->fps = av_q2d(st->avg_frame_rate); if (is_image(st, sh->attached_picture, priv->avif)) { @@ -773,6 +789,7 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh->codec->dovi = true; sh->codec->dv_profile = cfg->dv_profile; sh->codec->dv_level = cfg->dv_level; + sh->codec->dv_el_present = cfg->bl_present_flag && cfg->el_present_flag; } // This also applies to vfw-muxed mkv, but we can't detect these easily. @@ -784,7 +801,7 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh = demux_alloc_sh_stream(STREAM_SUB); if (codec->extradata_size) { - sh->codec->extradata = talloc_size(sh, codec->extradata_size); + sh->codec->extradata = talloc_size(sh->codec, codec->extradata_size); memcpy(sh->codec->extradata, codec->extradata, codec->extradata_size); sh->codec->extradata_size = codec->extradata_size; } @@ -826,9 +843,11 @@ static void handle_new_stream(demuxer_t *demuxer, int i) sh->ff_index = st->index; mp_codec_info_from_avcodecpar(codec, sh->codec); sh->codec->codec_tag = codec->codec_tag; - sh->codec->lav_codecpar = avcodec_parameters_alloc(); - if (sh->codec->lav_codecpar) - avcodec_parameters_copy(sh->codec->lav_codecpar, codec); + AVCodecParameters **lavp = talloc_ptrtype(sh->codec, lavp); + talloc_set_destructor(lavp, avcodec_par_destructor); + *lavp = avcodec_parameters_alloc(); + if (*lavp && avcodec_parameters_copy(*lavp, codec) >= 0) + sh->codec->lav_codecpar = *lavp; sh->codec->native_tb_num = st->time_base.num; sh->codec->native_tb_den = st->time_base.den; sh->codec->duration = st->duration * av_q2d(st->time_base); @@ -868,6 +887,12 @@ static void handle_new_stream(demuxer_t *demuxer, int i) mp_tags_move_from_av_dictionary(sh->tags, &st->metadata); demux_add_sh_stream(demuxer, sh); + // DVD routes the menu's button-graphics through an SPU + // substream lavf only discovers once the first SPU PES arrives + // mid-playback. Select it immediately to avoid missing menu highlights. + if (priv->is_dvd && sh->type == STREAM_SUB) + demuxer_select_track(demuxer, sh, MP_NOPTS_VALUE, true); + // Unfortunately, there is no better way to detect PCM codecs, other // than listing them all manually. (Or other "frameless" codecs. Or // rather, codecs with frames so small libavformat will put multiple of @@ -1266,6 +1291,42 @@ static void handle_lcevc_group(demuxer_t *demuxer, AVStreamGroup *stg) } #endif +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(62, 19, 100) +// Base layer + Enhancement layer separate track stream group +static void handle_layered_video_group(demuxer_t *demuxer, AVStreamGroup *stg) +{ + lavf_priv_t *priv = demuxer->priv; + AVStreamGroupLayeredVideo *layered = stg->params.layered_video; + + if (stg->nb_streams != 2 || layered->el_index >= stg->nb_streams) { + MP_WARN(demuxer, "Dolby Vision group %u: expected 2 streams with valid " + "el_index, got %u streams and el_index %u\n", + stg->index, stg->nb_streams, layered->el_index); + return; + } + + AVStream *el_st = stg->streams[layered->el_index]; + AVStream *bl_st = stg->streams[layered->el_index ? 0 : 1]; + + if ((size_t)el_st->index >= priv->num_streams || (size_t)bl_st->index >= priv->num_streams) + return; + + struct sh_stream *el_sh = priv->streams[el_st->index]->sh; + struct sh_stream *bl_sh = priv->streams[bl_st->index]->sh; + if (!el_sh || !bl_sh) + return; + + // Group storage is attached to the BL so its lifetime tracks the demuxer. + struct sh_stream_group *group = talloc_zero(bl_sh, struct sh_stream_group); + MP_TARRAY_APPEND(group, group->members, group->num_members, bl_sh); + MP_TARRAY_APPEND(group, group->members, group->num_members, el_sh); + + bl_sh->group = group; + el_sh->group = group; + el_sh->dependent_track = true; +} +#endif + static void handle_stream_groups(demuxer_t *demuxer) { lavf_priv_t *priv = demuxer->priv; @@ -1297,6 +1358,11 @@ static void handle_stream_groups(demuxer_t *demuxer) case AV_STREAM_GROUP_PARAMS_LCEVC: handle_lcevc_group(demuxer, stg); break; +#endif +#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(62, 19, 100) + case AV_STREAM_GROUP_PARAMS_DOLBY_VISION: + handle_layered_video_group(demuxer, stg); + break; #endif default: MP_VERBOSE(demuxer, "Unhandled stream group type %d (index %u)\n", @@ -1307,6 +1373,22 @@ static void handle_stream_groups(demuxer_t *demuxer) } #endif +static void detect_dovi_split_streams(demuxer_t *demuxer) +{ + lavf_priv_t *priv = demuxer->priv; + int snapshot_count = priv->num_streams; + for (int n = 0; n < snapshot_count; n++) { + struct stream_info *info = priv->streams[n]; + struct sh_stream *sh = info ? info->sh : NULL; + if (!sh || sh->type != STREAM_VIDEO || !sh->codec || + !sh->codec->dv_el_present || sh->group) + { + continue; + } + info->dovi_split = mp_dovi_split_create(demuxer, sh); + } +} + static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check) { AVFormatContext *avfc = NULL; @@ -1489,6 +1571,7 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check) #if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(60, 19, 100) handle_stream_groups(demuxer); #endif + detect_dovi_split_streams(demuxer); mp_tags_move_from_av_dictionary(demuxer->metadata, &avfc->metadata); @@ -1554,8 +1637,9 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check) if (priv->stream) { const char *sname = priv->stream->info->name; - priv->is_dvd_bd = strcmp(sname, "dvdnav") == 0 || - strcmp(sname, "ifo_dvdnav") == 0 || + priv->is_dvd = strcmp(sname, "dvdnav") == 0 || + strcmp(sname, "ifo_dvdnav") == 0; + priv->is_dvd_bd = priv->is_dvd || strcmp(sname, "bd") == 0 || strcmp(sname, "bdnav") == 0 || strcmp(sname, "bdmv/bluray") == 0; @@ -1576,6 +1660,13 @@ static bool demux_lavf_read_packet(struct demuxer *demux, { lavf_priv_t *priv = demux->priv; + // Companion EL packet queued by the Dolby Vision splitter on a prior call. + if (priv->pending_pkt) { + *mp_pkt = priv->pending_pkt; + priv->pending_pkt = NULL; + return true; + } + AVPacket *pkt = av_packet_alloc(); MP_HANDLE_OOM(pkt); int r = av_read_frame(priv->avfc, pkt); @@ -1604,7 +1695,14 @@ static bool demux_lavf_read_packet(struct demuxer *demux, struct sh_stream *stream = info->sh; AVStream *st = priv->avfc->streams[pkt->stream_index]; - if (!demux_stream_is_selected(stream)) { + // Keep BL packets flowing to feed the Dolby Vision splitter when its + // virtual EL is selected, even if the BL itself isn't selected. The + // unselected BL dp gets discarded by the demuxer queue downstream. + struct sh_stream *split_el = info->dovi_split + ? mp_dovi_split_el_stream(info->dovi_split) + : NULL; + bool need_for_split = split_el && demux_stream_is_selected(split_el); + if (!demux_stream_is_selected(stream) && !need_for_split) { av_packet_free(&pkt); return true; // don't signal EOF if skipping a packet } @@ -1666,6 +1764,13 @@ static bool demux_lavf_read_packet(struct demuxer *demux, } } + // Dispatch the EL view of this packet via the splitter. + if (info->dovi_split) { + struct sh_stream *el = mp_dovi_split_el_stream(info->dovi_split); + if (el && demux_stream_is_selected(el)) + priv->pending_pkt = mp_dovi_split_dispatch(info->dovi_split, dp); + } + if (st->event_flags & AVSTREAM_EVENT_FLAG_METADATA_UPDATED) { st->event_flags = 0; struct mp_tags *tags = talloc_zero(NULL, struct mp_tags); @@ -1678,6 +1783,16 @@ static bool demux_lavf_read_packet(struct demuxer *demux, return true; } +static void reset_dovi_split_state(demuxer_t *demuxer) +{ + lavf_priv_t *priv = demuxer->priv; + TA_FREEP(&priv->pending_pkt); + for (int n = 0; n < priv->num_streams; n++) { + if (priv->streams[n] && priv->streams[n]->dovi_split) + mp_dovi_split_reset(priv->streams[n]->dovi_split); + } +} + static void demux_drop_buffers_lavf(demuxer_t *demuxer) { lavf_priv_t *priv = demuxer->priv; @@ -1686,6 +1801,7 @@ static void demux_drop_buffers_lavf(demuxer_t *demuxer) stream_drop_buffers(priv->stream); avio_flush(priv->avfc->pb); avformat_flush(priv->avfc); + reset_dovi_split_state(demuxer); } static void demux_seek_lavf(demuxer_t *demuxer, double seek_pts, int flags) @@ -1769,6 +1885,7 @@ static void demux_seek_lavf(demuxer_t *demuxer, double seek_pts, int flags) av_strerror(r, buf, sizeof(buf)); MP_VERBOSE(demuxer, "Seek failed (%s)\n", buf); } + reset_dovi_split_state(demuxer); update_read_stats(demuxer); } @@ -1800,9 +1917,9 @@ static void demux_close_lavf(demuxer_t *demuxer) av_freep(&priv->pb); for (int n = 0; n < priv->num_streams; n++) { struct stream_info *info = priv->streams[n]; - if (info->sh) - avcodec_parameters_free(&info->sh->codec->lav_codecpar); + TA_FREEP(&info->dovi_split); } + TA_FREEP(&priv->pending_pkt); if (priv->own_stream) free_stream(priv->stream); if (priv->av_opts) diff --git a/demux/demux_mkv.c b/demux/demux_mkv.c index acbd75f6d929f..0349cb91ac4ed 100644 --- a/demux/demux_mkv.c +++ b/demux/demux_mkv.c @@ -55,6 +55,7 @@ #include "video/csputils.h" #include "video/mp_image.h" #include "demux.h" +#include "dovi_split.h" #include "packet_pool.h" #include "stheader.h" #include "ebml.h" @@ -163,6 +164,8 @@ typedef struct mkv_track { size_t last_index_entry; AVDOVIDecoderConfigurationRecord *dovi_config; + bstr hvce; + struct mp_dovi_split *dovi_split; } mkv_track_t; typedef struct mkv_index { @@ -837,9 +840,13 @@ static void parse_block_addition_mapping(struct demuxer *demuxer, switch (block_addition_mapping->block_add_id_type) { case MATROSKA_BLOCK_ADD_ID_TYPE_ITU_T_T35: break; - case MKBETAG('a','v','c','E'): case MKBETAG('h','v','c','E'): - MP_WARN(demuxer, "Dolby Vision enhancement-layer playback is not supported.\n"); + if (block_addition_mapping->n_block_add_id_extra_data) + track->hvce = bstrdup(track, block_addition_mapping->block_add_id_extra_data); + break; + case MKBETAG('a','v','c','E'): + MP_WARN(demuxer, "Dolby Vision enhancement-layer playback for AVC " + "is not supported.\n"); break; case MKBETAG('d','v','c','C'): case MKBETAG('d','v','v','C'): @@ -867,6 +874,7 @@ static void demux_mkv_free_trackentry(mkv_track_t *track) { talloc_free(track->parser_tmp); av_freep(&track->dovi_config); + TA_FREEP(&track->dovi_split); talloc_free(track); } @@ -1802,11 +1810,33 @@ static int demux_mkv_open_video(demuxer_t *demuxer, mkv_track_t *track) sh_v->dovi = true; sh_v->dv_level = track->dovi_config->dv_level; sh_v->dv_profile = track->dovi_config->dv_profile; + sh_v->dv_el_present = track->dovi_config->bl_present_flag && + track->dovi_config->el_present_flag; } +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(62, 35, 100) + if (track->hvce.len > 0) { + void *data = av_memdup(track->hvce.start, track->hvce.len); + MP_HANDLE_OOM(data); + if (!av_packet_side_data_add(&sh_v->lav_codecpar->coded_side_data, + &sh_v->lav_codecpar->nb_coded_side_data, + AV_PKT_DATA_HEVC_CONF, + data, track->hvce.len, 0)) + { + MP_ERR(demuxer, "Failed to attach hvcE configuration record to " + "codec parameters for track %d!\n", track->tnum); + av_free(data); + } + } +#endif + done: demux_add_sh_stream(demuxer, sh); + // Profile 7 NALU-interleaved + if (sh_v->dv_el_present) + track->dovi_split = mp_dovi_split_create(demuxer, sh); + return 0; } @@ -2250,6 +2280,51 @@ static int demux_mkv_open_sub(demuxer_t *demuxer, mkv_track_t *track) return 0; } +static void pair_dovi_tracks(demuxer_t *demuxer) +{ + mkv_demuxer_t *mkv_d = demuxer->priv; + mkv_track_t *bl_track = NULL, *el_track = NULL; + + for (int i = 0; i < mkv_d->num_tracks; i++) { + mkv_track_t *track = mkv_d->tracks[i]; + if (!track->stream || track->stream->type != STREAM_VIDEO || + !track->codec_id || strcmp(track->codec_id, "V_MPEGH/ISO/HEVC")) + continue; + + AVDOVIDecoderConfigurationRecord *dovi = track->dovi_config; + if (dovi && dovi->dv_profile == 7 && dovi->el_present_flag) { + // bl_present_flag is not checked, because the files in the + // wild set it to 1 for EL stream, while the expectation, based + // on Dolby spec for MPEG-TS would be that it's set to 0. + // Ignore this, if we have EL track and single other video track + // it's safe to assume it's BL. + if (el_track) + return; + el_track = track; + continue; + } + + if (bl_track) + return; + bl_track = track; + } + + if (!el_track || !bl_track) + return; + + struct sh_stream *bl_sh = bl_track->stream; + struct sh_stream *el_sh = el_track->stream; + + // Group storage is attached to the BL so its lifetime tracks the demuxer. + struct sh_stream_group *group = talloc_zero(bl_sh, struct sh_stream_group); + MP_TARRAY_APPEND(group, group->members, group->num_members, bl_sh); + MP_TARRAY_APPEND(group, group->members, group->num_members, el_sh); + + bl_sh->group = group; + el_sh->group = group; + el_sh->dependent_track = true; +} + // Workaround for broken files that don't set attached_picture static void probe_if_image(demuxer_t *demuxer) { @@ -2531,6 +2606,7 @@ static int demux_mkv_open(demuxer_t *demuxer, enum demux_check check) MP_VERBOSE(demuxer, "All headers are parsed!\n"); display_create_tracks(demuxer); + pair_dovi_tracks(demuxer); add_coverart(demuxer); process_tags(demuxer); @@ -2749,6 +2825,7 @@ static void mkv_seek_reset(demuxer_t *demuxer) av_parser_close(track->av_parser); track->av_parser = NULL; avcodec_free_context(&track->av_parser_codec); + mp_dovi_split_reset(track->dovi_split); } for (int n = 0; n < mkv_d->num_blocks; n++) @@ -2896,7 +2973,16 @@ static void mkv_parse_and_add_packet(demuxer_t *demuxer, mkv_track_t *track, } if (!track->parse || !track->av_parser || !track->av_parser_codec) { + struct demux_packet *el_dp = NULL; + struct sh_stream *el_sh = NULL; + if (track->dovi_split) { + el_sh = mp_dovi_split_el_stream(track->dovi_split); + if (el_sh && demux_stream_is_selected(el_sh)) + el_dp = mp_dovi_split_dispatch(track->dovi_split, dp); + } add_packet(demuxer, stream, dp); + if (el_dp) + add_packet(demuxer, el_sh, el_dp); return; } @@ -3034,7 +3120,13 @@ static int handle_block(demuxer_t *demuxer, struct block_info *block_info) struct sh_stream *stream = track->stream; bool use_this_block = tc >= mkv_d->skip_to_timecode; - if (!demux_stream_is_selected(stream)) + // Keep BL blocks flowing to feed the Dolby Vision splitter when its + // virtual EL is selected, even if the BL itself isn't selected. + struct sh_stream *split_el = track->dovi_split + ? mp_dovi_split_el_stream(track->dovi_split) + : NULL; + bool need_for_split = split_el && demux_stream_is_selected(split_el); + if (!demux_stream_is_selected(stream) && !need_for_split) return 0; current_pts = tc / 1e9 - track->codec_delay; diff --git a/demux/dovi_split.c b/demux/dovi_split.c new file mode 100644 index 0000000000000..cfe27185da703 --- /dev/null +++ b/demux/dovi_split.c @@ -0,0 +1,203 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include + +#include +#include +#include +#include + +#include "common/av_common.h" +#include "common/common.h" +#include "common/msg.h" +#include "demux.h" +#include "demux/packet.h" +#include "demux/packet_pool.h" +#include "demux/stheader.h" +#include "dovi_split.h" +#include "mpv_talloc.h" + +struct mp_dovi_split { + struct mp_log *log; + struct demuxer *demuxer; + struct sh_stream *bl; + struct sh_stream *el; + AVBSFContext *bsf; + AVPacket *staging; +}; + +static void mp_dovi_split_destructor(void *p) +{ + struct mp_dovi_split *s = p; + av_packet_free(&s->staging); + av_bsf_free(&s->bsf); +} + +struct mp_dovi_split *mp_dovi_split_create(struct demuxer *demuxer, + struct sh_stream *bl) +{ + if (!bl || bl->type != STREAM_VIDEO || !bl->codec || + !bl->codec->codec || strcmp(bl->codec->codec, "hevc") != 0) + return NULL; + + const AVBitStreamFilter *def = av_bsf_get_by_name("dovi_split"); + if (!def) { + MP_WARN(demuxer, "Dolby Vision EL: 'dovi_split' BSF not available in " + "libavcodec; rendering base layer only.\n"); + return NULL; + } + + struct mp_dovi_split *s = talloc_zero(demuxer, struct mp_dovi_split); + talloc_set_destructor(s, mp_dovi_split_destructor); + s->log = demuxer->log; + s->demuxer = demuxer; + s->bl = bl; + + s->staging = av_packet_alloc(); + if (!s->staging) + goto fail; + + int ret = av_bsf_alloc(def, &s->bsf); + if (ret < 0) + goto fail; + + AVCodecParameters *par = mp_codec_params_to_av(bl->codec); + if (par) { + avcodec_parameters_copy(s->bsf->par_in, par); + avcodec_parameters_free(&par); + } + s->bsf->time_base_in = mp_get_codec_timebase(bl->codec); + + if (av_opt_set(s->bsf, "mode", "el", AV_OPT_SEARCH_CHILDREN) < 0) + goto fail; + if (av_bsf_init(s->bsf) < 0) + goto fail; + + const AVCodecParameters *par_out = s->bsf->par_out; + + // Allocate the virtual EL sh_stream. + struct sh_stream *el = demux_alloc_sh_stream(STREAM_VIDEO); + el->codec->codec = "hevc"; + el->codec->native_tb_num = bl->codec->native_tb_num; + el->codec->native_tb_den = bl->codec->native_tb_den; + el->codec->fps = bl->codec->fps; + el->codec->disp_w = par_out->width; + el->codec->disp_h = par_out->height; + if (par_out->extradata_size > 0) { + el->codec->extradata = talloc_memdup(el->codec, par_out->extradata, + par_out->extradata_size); + el->codec->extradata_size = par_out->extradata_size; + } + for (int i = 0; i < par_out->nb_coded_side_data; i++) { + const AVPacketSideData *sd = &par_out->coded_side_data[i]; + if (sd->type != AV_PKT_DATA_DOVI_CONF) + continue; + const AVDOVIDecoderConfigurationRecord *cfg = (const void *)sd->data; + el->codec->dovi = true; + el->codec->dv_profile = cfg->dv_profile; + el->codec->dv_level = cfg->dv_level; + el->codec->dv_el_present = cfg->el_present_flag; + break; + } + el->title = talloc_strdup(el, "Dolby Vision enhancement layer"); + el->dependent_track = true; + + demux_add_sh_stream(demuxer, el); + s->el = el; + + // Bind BL and EL into a sh_stream_group. + struct sh_stream_group *group = talloc_zero(bl, struct sh_stream_group); + MP_TARRAY_APPEND(group, group->members, group->num_members, bl); + MP_TARRAY_APPEND(group, group->members, group->num_members, el); + bl->group = group; + el->group = group; + + MP_VERBOSE(demuxer, "Dolby Vision Profile 7 splitter: BL stream %d, " + "virtual EL stream %d (dependent_track).\n", + bl->index, el->index); + return s; + +fail: + talloc_free(s); + return NULL; +} + +void mp_dovi_split_reset(struct mp_dovi_split *s) +{ + if (!s || !s->bsf) + return; + av_bsf_flush(s->bsf); +} + +struct sh_stream *mp_dovi_split_el_stream(struct mp_dovi_split *s) +{ + return s ? s->el : NULL; +} + +struct demux_packet *mp_dovi_split_dispatch(struct mp_dovi_split *s, + struct demux_packet *bl_dp) +{ + if (!s || !s->bsf || !bl_dp || !bl_dp->buffer || bl_dp->len <= 0) + return NULL; + + // av_bsf_send_packet takes ownership of the packet's buffer, so copy it, + // to not steal it from caller. + AVPacket *copy = av_packet_alloc(); + if (!copy) + return NULL; + int ret = av_new_packet(copy, bl_dp->len); + if (ret < 0) { + av_packet_free(©); + return NULL; + } + memcpy(copy->data, bl_dp->buffer, bl_dp->len); + copy->flags = bl_dp->keyframe ? AV_PKT_FLAG_KEY : 0; + + ret = av_bsf_send_packet(s->bsf, copy); + av_packet_free(©); + if (ret < 0) { + MP_VERBOSE(s->demuxer, "dovi_split: BSF send failed: %s\n", + mp_strerror(AVUNERROR(ret))); + return NULL; + } + + ret = av_bsf_receive_packet(s->bsf, s->staging); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + // No EL NALs in this AU, nothing to emit. + return NULL; + } + if (ret < 0) { + MP_VERBOSE(s->demuxer, "dovi_split: BSF receive failed: %s; flushing.\n", + mp_strerror(AVUNERROR(ret))); + av_bsf_flush(s->bsf); + return NULL; + } + + struct demux_packet *dp = + new_demux_packet_from_avpacket(s->demuxer->packet_pool, s->staging); + if (dp) { + // Mirror the BL packet's timing so the pairing filter can match by PTS. + dp->pts = bl_dp->pts; + dp->dts = bl_dp->dts; + dp->duration = bl_dp->duration; + dp->keyframe = bl_dp->keyframe; + dp->stream = s->el->index; + } + av_packet_unref(s->staging); + return dp; +} diff --git a/demux/dovi_split.h b/demux/dovi_split.h new file mode 100644 index 0000000000000..cf22ded2378da --- /dev/null +++ b/demux/dovi_split.h @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +#pragma once + +struct demuxer; +struct sh_stream; +struct demux_packet; + +// Dolby Vision Profile 7 enhancement-layer splitter for HEVC streams that +// carry the EL bitstream interleaved as HEVC_NAL_UNSPEC63. +// +// Creates a virtual EL sh_stream and binds it to the BL via sh_stream_group +// so downstream code can treat same as separate track BL+EL. +struct mp_dovi_split; + +// Create a splitter on `bl`. Adds the virtual EL sh_stream to `demuxer`. +// +// Returns NULL if the BSF is unavailable or initialization fails. The +// returned context is talloc-attached to `demuxer`. +struct mp_dovi_split *mp_dovi_split_create(struct demuxer *demuxer, + struct sh_stream *bl); + +void mp_dovi_split_reset(struct mp_dovi_split *s); + +// Return the virtual EL sh_stream created by mp_dovi_split_create. The +// returned pointer remains valid for the lifetime of the splitter. +struct sh_stream *mp_dovi_split_el_stream(struct mp_dovi_split *s); + +// Apply the BSF to `bl_dp` and produce a companion EL demux_packet, if any. +// Caller owns the returned packet. Returns NULL if the access unit contained +// no EL NALs or on any non-fatal BSF error. +// +// The emitted packet inherits pts/dts/duration/keyframe from `bl_dp` so it +// lines up with the matching BL packet for PTS-based pairing downstream. +struct demux_packet *mp_dovi_split_dispatch(struct mp_dovi_split *s, + struct demux_packet *bl_dp); diff --git a/demux/stheader.h b/demux/stheader.h index 6722c07218673..01754746b2372 100644 --- a/demux/stheader.h +++ b/demux/stheader.h @@ -99,6 +99,23 @@ static inline bool sh_stream_has_program(const struct sh_stream *sh, int program return false; } +// Return the dependent twin of the given base track (e.g. a Dolby Vision +// enhancement-layer stream paired with the base layer), or NULL if none. Only +// twin-track groups (exactly 2 members) of matching type are supported. +static inline struct sh_stream *sh_stream_dependent_sibling(struct sh_stream *bl) +{ + if (!bl || !bl->group || bl->dependent_track) + return NULL; + if (bl->group->num_members != 2) + return NULL; + for (int i = 0; i < bl->group->num_members; i++) { + struct sh_stream *m = bl->group->members[i]; + if (m && m != bl && m->dependent_track && m->type == bl->type) + return m; + } + return NULL; +} + struct mp_codec_params { enum stream_type type; @@ -158,6 +175,7 @@ struct mp_codec_params { bool dovi; uint8_t dv_profile; uint8_t dv_level; + bool dv_el_present; // BL and EL interleaved in this stream (Profile 7) // STREAM_VIDEO + STREAM_AUDIO int bits_per_coded_sample; diff --git a/etc/input.conf b/etc/input.conf index 0b2fb46d4f9d5..a78156342546c 100644 --- a/etc/input.conf +++ b/etc/input.conf @@ -234,6 +234,22 @@ #Ctrl+KP_UP add video-align-y -0.01 # align video up #Ctrl+KP_PGUP add video-align-x 0.01; add video-align-y -0.01 # align video right and up +# +# The "{discnav}" section is enabled only while a disc menu with a button +# highlight is actually on screens. +# +#UP {discnav} discnav up +#DOWN {discnav} discnav down +#LEFT {discnav} discnav left +#RIGHT {discnav} discnav right +#ENTER {discnav} discnav select +#ESC {discnav} discnav prev +#BS {discnav} discnav prev +#MOUSE_MOVE {discnav} discnav mouse-move +#MBTN_LEFT {discnav} discnav mouse-click +#Ctrl+m discnav menu # jump to the disc's root menu +#Ctrl+M discnav popup # show/hide Blu-ray popup menu + # # Legacy bindings (may or may not be removed in the future) # diff --git a/filters/f_decoder_wrapper.c b/filters/f_decoder_wrapper.c index 08978e49f4540..2137b4b0d2152 100644 --- a/filters/f_decoder_wrapper.c +++ b/filters/f_decoder_wrapper.c @@ -1406,13 +1406,14 @@ struct mp_decoder_wrapper *mp_decoder_wrapper_create(struct mp_filter *parent, decf_reset(p->decf); + struct mp_pin *out_pin; if (p->queue) { struct mp_filter *f_in = mp_async_queue_create_filter(public_f, MP_PIN_OUT, p->queue); struct mp_filter *f_out = mp_async_queue_create_filter(p->decf, MP_PIN_IN, p->queue); - mp_pin_connect(public_f->ppins[0], f_in->pins[0]); mp_pin_connect(f_out->pins[0], p->decf->pins[0]); + out_pin = f_in->pins[0]; p->dec_thread_valid = true; if (mp_thread_create(&p->dec_thread, dec_thread, p)) { @@ -1420,9 +1421,11 @@ struct mp_decoder_wrapper *mp_decoder_wrapper_create(struct mp_filter *parent, goto error; } } else { - mp_pin_connect(public_f->ppins[0], p->decf->pins[0]); + out_pin = p->decf->pins[0]; } + mp_pin_connect(public_f->ppins[0], out_pin); + public_f_reset(public_f); return &p->public; diff --git a/filters/f_enhancement_pair.c b/filters/f_enhancement_pair.c new file mode 100644 index 0000000000000..18cea54466b33 --- /dev/null +++ b/filters/f_enhancement_pair.c @@ -0,0 +1,226 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include + +#include "common/common.h" +#include "common/msg.h" +#include "demux/stheader.h" +#include "f_decoder_wrapper.h" +#include "f_enhancement_pair.h" +#include "filter_internal.h" +#include "video/mp_image.h" + +// PTS-match tolerance, in seconds. +#define PTS_MATCH_TOLERANCE 1e-6 + +// Number of frames hold for matching. +#define QUEUE_MAX 16 + +struct priv { + struct mp_decoder_wrapper *el_dec; + struct mp_pin *el_in; // el_dec->f->pins[0] + + // BL/EL frames decoded but not yet emitted as a pair. + struct mp_image **bl_pending; + int num_bl_pending; + struct mp_image **el_pending; + int num_el_pending; + + bool bl_eof; + bool el_eof; +}; + +static int pts_cmp(double a, double b) +{ + if (a == MP_NOPTS_VALUE || b == MP_NOPTS_VALUE) + return 0; + if (a < b - PTS_MATCH_TOLERANCE) return -1; + if (a > b + PTS_MATCH_TOLERANCE) return 1; + return 0; +} + +// Pull available frames from `pin` into `queue` until either no data is +// ready or the queue is full. Sets *eof if the upstream signaled EOF. +static void drain_pin(struct mp_filter *f, struct mp_pin *pin, + struct mp_image ***queue, int *num, bool *eof) +{ + while (!*eof && *num < QUEUE_MAX) { + if (!mp_pin_out_request_data(pin)) + return; + struct mp_frame fr = mp_pin_out_read(pin); + if (fr.type == MP_FRAME_EOF) { + *eof = true; + return; + } + if (fr.type != MP_FRAME_VIDEO) { + mp_frame_unref(&fr); + continue; + } + MP_TARRAY_APPEND(f->priv, *queue, *num, fr.data); + } +} + +static void pop_head(struct mp_image ***queue, int *num) +{ + talloc_free((*queue)[0]); + int remain = *num - 1; + if (remain > 0) + memmove(&(*queue)[0], &(*queue)[1], remain * sizeof((*queue)[0])); + *num = remain; +} + +static struct mp_image *take_head(struct mp_image ***queue, int *num) +{ + struct mp_image *img = (*queue)[0]; + int remain = *num - 1; + if (remain > 0) + memmove(&(*queue)[0], &(*queue)[1], remain * sizeof((*queue)[0])); + *num = remain; + return img; +} + +// Downstream code expects the DV RPU and the related color/repr/HDR fields on +// the BL frame. In dual-track sources these are carried on the EL stream, so +// mirror them onto the BL here. This keeps DV bookkeeping local. +static void inherit_dovi_from_el(struct mp_image *bl, struct mp_image *el) +{ + if (bl->params.no_dovi || bl->dovi || !el->dovi) + return; + bl->dovi = av_buffer_ref(el->dovi); + if (!bl->dovi) + return; + bl->params.repr.dovi = (void *)bl->dovi->data; + bl->params.repr.sys = el->params.repr.sys; + bl->params.color.primaries = el->params.color.primaries; + bl->params.color.transfer = el->params.color.transfer; + bl->params.color.hdr.min_luma = el->params.color.hdr.min_luma; + bl->params.color.hdr.max_luma = el->params.color.hdr.max_luma; + bl->params.color.hdr.max_pq_y = el->params.color.hdr.max_pq_y; + bl->params.color.hdr.avg_pq_y = el->params.color.hdr.avg_pq_y; +} + +static void pair_process(struct mp_filter *f) +{ + struct priv *p = f->priv; + struct mp_pin *in = f->ppins[0]; + struct mp_pin *out = f->ppins[1]; + + drain_pin(f, in, &p->bl_pending, &p->num_bl_pending, &p->bl_eof); + drain_pin(f, p->el_in, &p->el_pending, &p->num_el_pending, &p->el_eof); + + while (mp_pin_in_needs_data(out)) { + if (p->num_bl_pending == 0) { + if (p->bl_eof) { + while (p->num_el_pending) + pop_head(&p->el_pending, &p->num_el_pending); + mp_pin_in_write(out, MP_EOF_FRAME); + } + return; + } + + struct mp_image *bl = p->bl_pending[0]; + int cmp = p->num_el_pending > 0 + ? pts_cmp(p->el_pending[0]->pts, bl->pts) : 0; + + // EL older than BL: its BL partner already left or never arrived. + if (p->num_el_pending > 0 && cmp < 0) { + pop_head(&p->el_pending, &p->num_el_pending); + continue; + } + + if (p->num_el_pending > 0 && cmp == 0) { + struct mp_image *el = take_head(&p->el_pending, &p->num_el_pending); + take_head(&p->bl_pending, &p->num_bl_pending); + inherit_dovi_from_el(bl, el); + if (bl->params.no_enhancement_layer) { + talloc_free(el); + } else { + bl->enhancement_layer = el; + } + mp_pin_in_write(out, MAKE_FRAME(MP_FRAME_VIDEO, bl)); + continue; + } + + // No EL match for the oldest BL. Hold BL unless we have affirmative + // evidence no EL is coming. + bool give_up = p->el_eof || + (p->num_el_pending > 0 && cmp > 0) || + p->num_bl_pending >= QUEUE_MAX; + if (!give_up) + return; + + take_head(&p->bl_pending, &p->num_bl_pending); + bl->enhancement_layer = NULL; + mp_pin_in_write(out, MAKE_FRAME(MP_FRAME_VIDEO, bl)); + } +} + +static void pair_reset(struct mp_filter *f) +{ + struct priv *p = f->priv; + while (p->num_bl_pending) + pop_head(&p->bl_pending, &p->num_bl_pending); + while (p->num_el_pending) + pop_head(&p->el_pending, &p->num_el_pending); + p->bl_eof = false; + p->el_eof = false; +} + +static void pair_destroy(struct mp_filter *f) +{ + struct priv *p = f->priv; + while (p->num_bl_pending) + pop_head(&p->bl_pending, &p->num_bl_pending); + while (p->num_el_pending) + pop_head(&p->el_pending, &p->num_el_pending); + // el_dec->f is a child filter, freed by the framework after this returns. +} + +static const struct mp_filter_info pair_filter = { + .name = "enhancement_pair", + .priv_size = sizeof(struct priv), + .process = pair_process, + .reset = pair_reset, + .destroy = pair_destroy, +}; + +struct mp_filter *mp_enhancement_pair_create(struct mp_filter *parent, + struct sh_stream *el_sh) +{ + if (!el_sh) + return NULL; + + struct mp_filter *f = mp_filter_create(parent, &pair_filter); + if (!f) + return NULL; + mp_filter_add_pin(f, MP_PIN_IN, "in"); + mp_filter_add_pin(f, MP_PIN_OUT, "out"); + + struct priv *p = f->priv; + p->el_dec = mp_decoder_wrapper_create(f, el_sh); + if (!p->el_dec || !mp_decoder_wrapper_reinit(p->el_dec)) { + MP_WARN(f, "Failed to set up enhancement-layer decoder; " + "rendering base layer only.\n"); + talloc_free(f); + return NULL; + } + p->el_in = p->el_dec->f->pins[0]; + mp_pin_set_manual_connection_for(p->el_in, f); + + return f; +} diff --git a/filters/f_enhancement_pair.h b/filters/f_enhancement_pair.h new file mode 100644 index 0000000000000..dc00e7ca14094 --- /dev/null +++ b/filters/f_enhancement_pair.h @@ -0,0 +1,38 @@ +/* + * 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 . + */ + +#pragma once + +#include "filter.h" + +struct sh_stream; + +// Enhancement-layer pairing filter. +// +// Reads base-layer (BL) mp_image frames from its input pin, decodes the +// enhancement-layer (EL) stream `el_sh` via an internal mp_decoder_wrapper, +// and attaches each decoded EL frame as `mpi->enhancement_layer` of the BL +// frame with the matching PTS. Unmatched BL frames are forwarded unchanged +// (BL-only fallback). +// +// `el_sh` must be a dependent sibling of the BL stream via sh_stream_group, +// it is auto-selected by the demuxer when the BL is selected. +// +// 1 input pin (BL frames), 1 output pin (paired BL frames). Returns NULL +// on init failure. The caller should fall back to BL-only rendering. +struct mp_filter *mp_enhancement_pair_create(struct mp_filter *parent, + struct sh_stream *el_sh); diff --git a/filters/f_output_chain.c b/filters/f_output_chain.c index 8ded23177da84..e512f613c804d 100644 --- a/filters/f_output_chain.c +++ b/filters/f_output_chain.c @@ -1,6 +1,7 @@ #include "audio/aframe.h" #include "audio/out/ao.h" #include "common/global.h" +#include "common/msg.h" #include "options/m_config.h" #include "options/m_option.h" #include "video/out/vo.h" @@ -9,6 +10,7 @@ #include "f_autoconvert.h" #include "f_auto_filters.h" +#include "f_enhancement_pair.h" #include "f_lavfi.h" #include "f_output_chain.h" #include "f_utils.h" @@ -42,6 +44,11 @@ struct chain { struct mp_user_filter *input, *output, *convert_wrapper; struct mp_autoconvert *convert; + // Enhancement-layer pair filter wrapper (tail of post_filters). NULL when + // no EL is paired. + struct mp_user_filter *el_pair; + struct sh_stream *el_sh; + struct vo *vo; struct ao *ao; @@ -392,6 +399,45 @@ void mp_output_chain_set_vo(struct mp_output_chain *c, struct vo *vo) update_output_caps(p); } +void mp_output_chain_set_el_stream(struct mp_output_chain *c, + struct sh_stream *el_sh) +{ + struct chain *p = c->f->priv; + + mp_assert(p->type == MP_OUTPUT_CHAIN_VIDEO); + + if (p->el_sh == el_sh && (!el_sh || p->el_pair)) + return; + + if (p->el_pair) { + for (int n = 0; n < p->num_post_filters; n++) { + if (p->post_filters[n] == p->el_pair) { + MP_TARRAY_REMOVE_AT(p->post_filters, p->num_post_filters, n); + break; + } + } + talloc_free(p->el_pair->wrapper); + p->el_pair = NULL; + p->el_sh = NULL; + } + + if (el_sh) { + struct mp_user_filter *u = create_wrapper_filter(p); + u->name = "el_pair"; + u->f = mp_enhancement_pair_create(u->wrapper, el_sh); + if (!u->f) { + MP_WARN(p, "Failed to set up enhancement-layer pairing.\n"); + talloc_free(u->wrapper); + } else { + MP_TARRAY_APPEND(p, p->post_filters, p->num_post_filters, u); + p->el_pair = u; + p->el_sh = el_sh; + } + } + + relink_filter_list(p); +} + void mp_output_chain_set_ao(struct mp_output_chain *c, struct ao *ao) { struct chain *p = c->f->priv; diff --git a/filters/f_output_chain.h b/filters/f_output_chain.h index f313f9a65f738..a9c0227783eb8 100644 --- a/filters/f_output_chain.h +++ b/filters/f_output_chain.h @@ -88,3 +88,9 @@ double mp_output_get_measured_total_delay(struct mp_output_chain *p); // Check if deinterlace user filter is inserted bool mp_output_chain_deinterlace_active(struct mp_output_chain *p); + +// Add an enhancement-layer pairing filter at the tail of the chain. +// Pass el_sh=NULL to remove. No-op if the same el_sh is already installed. +struct sh_stream; +void mp_output_chain_set_el_stream(struct mp_output_chain *p, + struct sh_stream *el_sh); diff --git a/meson.build b/meson.build index 1cd2570f0d7ef..215e08248f4b7 100644 --- a/meson.build +++ b/meson.build @@ -106,6 +106,7 @@ sources = files( 'demux/demux_playlist.c', 'demux/demux_raw.c', 'demux/demux_timeline.c', + 'demux/dovi_split.c', 'demux/ebml.c', 'demux/packet.c', 'demux/packet_pool.c', @@ -117,6 +118,7 @@ sources = files( 'filters/f_auto_filters.c', 'filters/f_decoder_wrapper.c', 'filters/f_demux_in.c', + 'filters/f_enhancement_pair.c', 'filters/f_hwtransfer.c', 'filters/f_lavfi.c', 'filters/f_output_chain.c', @@ -167,6 +169,7 @@ sources = files( 'player/client.c', 'player/command.c', 'player/configfiles.c', + 'player/discnav.c', 'player/external_files.c', 'player/loadfile.c', 'player/main.c', diff --git a/options/options.c b/options/options.c index 48baef13ff731..843c33c3fd05e 100644 --- a/options/options.c +++ b/options/options.c @@ -590,6 +590,7 @@ static const m_option_t mp_opts[] = { #endif {"edition", OPT_CHOICE(edition_id, {"auto", -1}), M_RANGE(0, 8190)}, {"flatten-editions", OPT_BOOL(flatten_editions)}, + {"disc-menu", OPT_BOOL(disc_menu)}, {"show-dependent-tracks", OPT_BOOL(show_dependent_tracks)}, #if HAVE_LIBBLURAY {"bluray", OPT_SUBSTRUCT(stream_bluray_opts, stream_bluray_conf)}, diff --git a/options/options.h b/options/options.h index 014e41de6d27e..c181dc6bbbda2 100644 --- a/options/options.h +++ b/options/options.h @@ -255,6 +255,7 @@ typedef struct MPOpts { int hls_bitrate; int edition_id; bool flatten_editions; + bool disc_menu; bool show_dependent_tracks; bool initial_audio_sync; double sync_max_video_change; diff --git a/player/command.c b/player/command.c index 0dea4bfc9420a..f82e8a3a78b8a 100644 --- a/player/command.c +++ b/player/command.c @@ -1130,6 +1130,19 @@ static int mp_property_current_edition(void *ctx, struct m_property *prop, return m_property_int_ro(action, arg, demuxer->edition); } +static int mp_property_disc_menu_active(void *ctx, struct m_property *prop, + int action, void *arg) +{ + MPContext *mpctx = ctx; + struct stream *s = disc_nav_get_stream(mpctx); + if (!s) + return M_PROPERTY_UNAVAILABLE; + struct stream_nav_state st = {0}; + if (stream_control(s, STREAM_CTRL_GET_NAV_STATE, &st) < 1) + return M_PROPERTY_UNAVAILABLE; + return m_property_bool_ro(action, arg, st.menu_active); +} + static int mp_property_edition(void *ctx, struct m_property *prop, int action, void *arg) { @@ -1165,6 +1178,22 @@ static int mp_property_edition(void *ctx, struct m_property *prop, } return M_PROPERTY_OK; } + case M_PROPERTY_SET: { + // For disc demuxers, jump the title directly, we sometimes need to + // react even if the actual edition "value" doesn't change. + if (disc_nav_get_stream(mpctx)) { + int new_ed = *(int *)arg; + if (new_ed < 0 || new_ed >= demuxer->num_editions) + return M_PROPERTY_ERROR; + unsigned new_title = new_ed; + stream_control(demuxer->stream, STREAM_CTRL_SET_CURRENT_TITLE, &new_title); + mpctx->opts->edition_id = new_ed; + mp_notify_property(mpctx, "edition"); + mp_wakeup_core(mpctx); + return M_PROPERTY_OK; + } + return mp_property_generic_option(mpctx, prop, action, arg); + } default: return mp_property_generic_option(mpctx, prop, action, arg); } @@ -4642,6 +4671,7 @@ static const struct m_property mp_properties_base[] = { {"chapter", mp_property_chapter}, {"edition", mp_property_edition}, {"current-edition", mp_property_current_edition}, + {"disc-menu-active", mp_property_disc_menu_active}, {"chapters", mp_property_chapters}, {"editions", mp_property_editions}, {"metadata", mp_property_metadata}, @@ -7412,6 +7442,51 @@ static void cmd_context_menu(void *p) vo_control(vo, VOCTRL_SHOW_MENU, NULL); } +static void cmd_discnav(void *p) +{ + struct mp_cmd_ctx *cmd = p; + struct MPContext *mpctx = cmd->mpctx; + int action = cmd->args[0].v.i; + + struct stream *s = disc_nav_get_stream(mpctx); + if (!s) { + cmd->success = false; + return; + } + + struct stream_nav_cmd nc = { .action = action }; + + struct stream_nav_state pre = {0}; + stream_control(s, STREAM_CTRL_GET_NAV_STATE, &pre); + + if (action == STREAM_NAV_MOUSE_MOVE || action == STREAM_NAV_MOUSE_CLICK) { + double nx = cmd->args[1].v.d; + double ny = cmd->args[2].v.d; + if (nx >= 0 && ny >= 0) { + if (pre.src_w <= 0 || pre.src_h <= 0 || + nx < 0 || nx > 1 || ny < 0 || ny > 1) + { + cmd->success = false; + return; + } + nc.x = (int)(nx * pre.src_w); + nc.y = (int)(ny * pre.src_h); + } else if (!disc_nav_mouse_pos_to_src(mpctx, pre.src_w, pre.src_h, + &nc.x, &nc.y)) + { + cmd->success = false; + return; + } + } + + if (stream_control(s, STREAM_CTRL_NAV_CMD, &nc) < 1) + cmd->success = false; + else if (mpctx->demuxer) + demux_drive_nav(mpctx->demuxer); // pump the menu VM past the cache + + mp_wakeup_core(mpctx); +} + static void cmd_flush_status_line(void *p) { struct mp_cmd_ctx *cmd = p; @@ -7968,6 +8043,23 @@ const struct mp_cmd_def mp_cmds[] = { { "context-menu", cmd_context_menu }, + { "discnav", cmd_discnav, + { {"action", OPT_CHOICE(v.i, + {"up", STREAM_NAV_UP}, + {"down", STREAM_NAV_DOWN}, + {"left", STREAM_NAV_LEFT}, + {"right", STREAM_NAV_RIGHT}, + {"select", STREAM_NAV_SELECT}, + {"menu", STREAM_NAV_MENU_ROOT}, + {"title-menu", STREAM_NAV_MENU_TITLE}, + {"popup", STREAM_NAV_MENU_POPUP}, + {"prev", STREAM_NAV_PREV_MENU}, + {"mouse-move", STREAM_NAV_MOUSE_MOVE}, + {"mouse-click", STREAM_NAV_MOUSE_CLICK})}, + {"x", OPT_DOUBLE(v.d), OPTDEF_DOUBLE(-1)}, + {"y", OPT_DOUBLE(v.d), OPTDEF_DOUBLE(-1)} }, + }, + { "flush-status-line", cmd_flush_status_line, { {"clear", OPT_BOOL(v.b)} } }, { "notify-property", cmd_notify_property, { {"property", OPT_STRING(v.s)} } }, @@ -8392,6 +8484,10 @@ void mp_option_run_callback(struct MPContext *mpctx, struct mp_option_callback * mp_notify_property(mpctx, "current-edition"); print_track_list(mpctx, mp_tprintf(42, "Selected edition %d:", demuxer->edition)); + } else if (disc_nav_get_stream(mpctx)) { + unsigned new_title = opts->edition_id; + stream_control(demuxer->stream, + STREAM_CTRL_SET_CURRENT_TITLE, &new_title); } else { if (!mpctx->stop_play) mpctx->stop_play = PT_CURRENT_ENTRY; diff --git a/player/core.h b/player/core.h index f17a8a9a3294f..83b4cd1b327a8 100644 --- a/player/core.h +++ b/player/core.h @@ -446,6 +446,7 @@ typedef struct MPContext { struct screenshot_ctx *screenshot_ctx; struct command_ctx *command_ctx; + struct disc_nav_state *disc_nav; struct encode_lavc_context *encode_lavc_ctx; struct mp_option_callback *option_callbacks; @@ -568,6 +569,7 @@ struct track *select_default_track(struct MPContext *mpctx, int order, enum stream_type type); void prefetch_next(struct MPContext *mpctx); void update_lavfi_complex(struct MPContext *mpctx); +void update_vo_chain_el_pair(struct MPContext *mpctx); // main.c int mp_initialize(struct MPContext *mpctx, char **argv); @@ -670,6 +672,15 @@ void uninit_sub_all(struct MPContext *mpctx); void update_osd_msg(struct MPContext *mpctx); bool update_subtitles(struct MPContext *mpctx, double video_pts); +// discnav.c +struct stream; +struct stream_nav_state; +void disc_nav_update(struct MPContext *mpctx); +void disc_nav_destroy(struct MPContext *mpctx); +struct stream *disc_nav_get_stream(struct MPContext *mpctx); +bool disc_nav_mouse_pos_to_src(struct MPContext *mpctx, int src_w, int src_h, + int *out_x, int *out_y); + // video.c void reset_video_state(struct MPContext *mpctx); int init_video_decoder(struct MPContext *mpctx, struct track *track); diff --git a/player/discnav.c b/player/discnav.c new file mode 100644 index 0000000000000..c04a0baa1966b --- /dev/null +++ b/player/discnav.c @@ -0,0 +1,449 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include +#include + +#include "mpv_talloc.h" + +#include "common/common.h" +#include "common/msg.h" +#include "input/input.h" +#include "player/command.h" + +#include "stream/stream.h" +#include "demux/demux.h" +#include "sub/dec_sub.h" +#include "sub/osd.h" +#include "video/mp_image.h" +#include "video/out/vo.h" + +#include "core.h" + +struct disc_nav_state { + // True while the disc-menu input section is enabled (a menu is on screen). + bool overlay_visible; + + // Blu-ray HDMV menu staging. libbluray hands us a pre-composited BGRA IG + // plane via STREAM_CTRL_GET_NAV_OVERLAY; we copy it into bd_image and + // forward through osd_set_external2. + struct mp_image *bd_image; + uint32_t bd_last_change_id; + struct mp_osd_res bd_last_vo_res; + + // Last observed disc-nav discontinuity counter (bumped by the stream + // backend on user nav actions and on libbluray/libdvdnav-internal + // playlist/title/cell transitions). + uint32_t last_discontinuity_id; + bool discontinuity_seen; + + // DVD-only: when a menu opens with no DVD sub track selected, we + // transiently select one so the menu graphic renders through the normal + // sd_lavc path. menu_selected_track remembers what we selected so we + // can deselect it again when the menu closes. + struct track *menu_selected_track; + + // Last disc-driven audio/sub/angle we acted on. + int last_audio_id; + int last_sub_id; + bool last_sub_visible; + int last_angle; + bool track_sync_seen; + // Last disc-discontinuity id we acted on for track sync. + uint32_t last_track_disc_id; +}; + +static struct disc_nav_state *get_state(struct MPContext *mpctx) +{ + if (!mpctx->disc_nav) + mpctx->disc_nav = talloc_zero(mpctx, struct disc_nav_state); + return mpctx->disc_nav; +} + +void disc_nav_destroy(struct MPContext *mpctx) +{ + if (!mpctx->disc_nav) + return; + mp_image_unrefp(&mpctx->disc_nav->bd_image); + TA_FREEP(&mpctx->disc_nav); +} + +struct stream *disc_nav_get_stream(struct MPContext *mpctx) +{ + if (!mpctx->demuxer || !mpctx->demuxer->stream) + return NULL; + struct stream *s = mpctx->demuxer->stream; + if (!s->info || !s->info->name) + return NULL; + const char *n = s->info->name; + if (strcmp(n, "dvdnav") == 0 || strcmp(n, "ifo_dvdnav") == 0 || + strcmp(n, "bd") == 0 || strcmp(n, "bdmv/bluray") == 0) + { + return s; + } + return NULL; +} + +bool disc_nav_mouse_pos_to_src(struct MPContext *mpctx, int src_w, int src_h, + int *out_x, int *out_y) +{ + struct vo *vo = mpctx->video_out; + if (!vo || !vo->config_ok || src_w <= 0 || src_h <= 0) + return false; + int wx, wy, hover; + mp_input_get_mouse_pos(mpctx->input, &wx, &wy, &hover); + struct mp_rect src, dst; + struct mp_osd_res osd; // mandatory out param; ignored + vo_get_src_dst_rects(vo, &src, &dst, &osd); + int dw = dst.x1 - dst.x0; + int dh = dst.y1 - dst.y0; + if (dw <= 0 || dh <= 0) + return false; + double fx = (wx - dst.x0) / (double)dw; + double fy = (wy - dst.y0) / (double)dh; + if (fx < 0 || fx > 1 || fy < 0 || fy > 1) + return false; + *out_x = (int)(fx * src_w); + *out_y = (int)(fy * src_h); + return true; +} + +static void push_dvd_overlay(struct MPContext *mpctx, + struct stream_nav_state *nav, bool visible) +{ + struct mp_dvdnav_hli hli = { + .show = visible, + .change_id = visible ? nav->change_id : 0, + }; + if (visible) { + hli.x = nav->hl_x; + hli.y = nav->hl_y; + hli.w = nav->hl_w; + hli.h = nav->hl_h; + memcpy(hli.palette, nav->hl_palette, sizeof(hli.palette)); + } + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *t = mpctx->tracks[n]; + if (t->type != STREAM_SUB || !t->d_sub || !t->stream || + !t->stream->codec || + strcmp(t->stream->codec->codec, "dvd_subtitle") != 0) + continue; + sub_control(t->d_sub, SD_CTRL_APPLY_DVDNAV, &hli); + } +} + +static void push_bd_overlay(struct MPContext *mpctx, struct stream *s, + struct stream_nav_state *nav, bool visible) +{ + struct disc_nav_state *st = get_state(mpctx); + + if (!visible) { + if (st->overlay_visible) + osd_set_external2(mpctx->osd, NULL); + st->bd_last_change_id = 0; + st->bd_last_vo_res = (struct mp_osd_res){0}; + return; + } + + if (nav->src_w <= 0 || nav->src_h <= 0) + return; + + // Skip the work when nothing the renderer cares about changed. + struct mp_osd_res vo_res = osd_get_vo_res(mpctx->osd); + if (st->overlay_visible && + st->bd_last_change_id == nav->change_id && + osd_res_equals(vo_res, st->bd_last_vo_res)) + return; + + if (!st->bd_image || + st->bd_image->w != nav->src_w || st->bd_image->h != nav->src_h) + { + mp_image_unrefp(&st->bd_image); + st->bd_image = mp_image_alloc(IMGFMT_BGRA, nav->src_w, nav->src_h); + if (!st->bd_image) + return; + talloc_steal(st, st->bd_image); + } + + struct mp_image *img = st->bd_image; + struct stream_nav_overlay_req req = { + .w = img->w, + .h = img->h, + .stride = img->stride[0], + .dst = img->planes[0], + }; + if (stream_control(s, STREAM_CTRL_GET_NAV_OVERLAY, &req) < 1) + return; + + struct sub_bitmap part = { + .bitmap = img->planes[0], + .stride = img->stride[0], + .w = req.w, + .h = req.h, + .dw = req.w, + .dh = req.h, + }; + struct sub_bitmaps imgs = { + .format = SUBBITMAP_BGRA, + .parts = &part, + .num_parts = 1, + .packed = img, + .packed_w = img->w, + .packed_h = img->h, + .change_id = nav->change_id ? (int)nav->change_id : 1, + }; + osd_rescale_bitmaps(&imgs, nav->src_w, nav->src_h, vo_res, 0); + osd_set_external2(mpctx->osd, &imgs); + st->bd_last_change_id = nav->change_id; + st->bd_last_vo_res = vo_res; +} + +// Sync demuxer->edition with what the disc is actually playing. +static void sync_current_edition(struct MPContext *mpctx, struct stream *s, + struct stream_nav_state *nav) +{ + struct demuxer *demuxer = mpctx->demuxer; + if (!demuxer || demuxer->num_editions <= 0 || !demuxer->desc || + strcmp(demuxer->desc->name, "disc") != 0) + return; + + int desired = demuxer->edition; + if (nav->menu_active) { + desired = demuxer->num_editions - 1; + } else { + unsigned title; + if (stream_control(s, STREAM_CTRL_GET_CURRENT_TITLE, &title) >= 1 && + (int)title < demuxer->num_editions - 1) + { + desired = (int)title; + } + } + if (desired != demuxer->edition) { + MP_VERBOSE(mpctx, "discnav: current-edition %d->%d " + "(menu_active=%d)\n", + demuxer->edition, desired, nav->menu_active); + demuxer->edition = desired; + mp_notify_property(mpctx, "current-edition"); + } +} + +// Catch async playlist/title hops driven by the disc itself (HDMV bytecode, +// dvdnav HOP_CHANNEL, etc.). +static void check_async_discontinuity(struct MPContext *mpctx, + struct stream_nav_state *nav) +{ + struct disc_nav_state *st = get_state(mpctx); + if (!mpctx->demuxer) + return; + if (!st->discontinuity_seen) { + st->last_discontinuity_id = nav->discontinuity_id; + st->discontinuity_seen = true; + return; + } + if (nav->discontinuity_id == st->last_discontinuity_id) + return; + MP_VERBOSE(mpctx, "discnav: async discontinuity %u->%u, flushing\n", + st->last_discontinuity_id, nav->discontinuity_id); + st->last_discontinuity_id = nav->discontinuity_id; + reset_playback_state(mpctx); + demux_flush(mpctx->demuxer); +} + +static struct track *find_track_by_demuxer_id(struct MPContext *mpctx, + enum stream_type type, int id) +{ + if (id < 0) + return NULL; + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *t = mpctx->tracks[n]; + if (t->type == type && t->stream && t->stream->demuxer_id == id) + return t; + } + return NULL; +} + +static void sync_disc_track_selection(struct MPContext *mpctx, struct stream *s, + struct stream_nav_state *nav) +{ + struct disc_nav_state *st = get_state(mpctx); + bool first = !st->track_sync_seen; + bool hopped = nav->discontinuity_id != st->last_track_disc_id; + st->last_track_disc_id = nav->discontinuity_id; + st->track_sync_seen = true; + + if (!first && (hopped || nav->active_audio_id != st->last_audio_id) && + mpctx->opts->stream_id[0][STREAM_AUDIO] != -2) + { + struct track *t = find_track_by_demuxer_id(mpctx, STREAM_AUDIO, + nav->active_audio_id); + if (t) { + if (mpctx->current_track[0][STREAM_AUDIO] != t) { + MP_VERBOSE(mpctx, "discnav: disc audio -> demuxer_id 0x%x\n", + nav->active_audio_id); + mp_switch_track_n(mpctx, 0, STREAM_AUDIO, t, 0); + } + st->last_audio_id = nav->active_audio_id; + } else if (nav->active_audio_id >= 0) { + MP_TRACE(mpctx, "discnav: disc audio 0x%x not in tracks yet\n", + nav->active_audio_id); + } + } else if (first) { + st->last_audio_id = nav->active_audio_id; + } + + if (!first && (hopped || + nav->active_sub_id != st->last_sub_id || + nav->sub_visible != st->last_sub_visible) && + !nav->menu_active && !st->menu_selected_track && + mpctx->opts->stream_id[0][STREAM_SUB] != -2) + { + if (nav->sub_visible) { + struct track *t = find_track_by_demuxer_id(mpctx, STREAM_SUB, + nav->active_sub_id); + if (t) { + if (mpctx->current_track[0][STREAM_SUB] != t) { + MP_VERBOSE(mpctx, "discnav: disc sub -> demuxer_id 0x%x\n", + nav->active_sub_id); + mp_switch_track_n(mpctx, 0, STREAM_SUB, t, 0); + } + st->last_sub_id = nav->active_sub_id; + st->last_sub_visible = true; + } else if (nav->active_sub_id >= 0) { + MP_TRACE(mpctx, "discnav: disc sub 0x%x not in tracks yet\n", + nav->active_sub_id); + } + } else { + if (mpctx->current_track[0][STREAM_SUB]) { + MP_VERBOSE(mpctx, "discnav: disc sub -> off\n"); + mp_switch_track_n(mpctx, 0, STREAM_SUB, NULL, 0); + } + st->last_sub_id = nav->active_sub_id; + st->last_sub_visible = false; + } + } else if (first) { + st->last_sub_id = nav->active_sub_id; + st->last_sub_visible = nav->sub_visible; + } + + if (first) { + st->last_angle = nav->angle; + } else if (nav->angle > 0 && nav->angle != st->last_angle) { + MP_VERBOSE(mpctx, "discnav: disc angle %d -> %d (of %d)\n", + st->last_angle, nav->angle, nav->num_angles); + st->last_angle = nav->angle; + mp_notify_property(mpctx, "angle"); + } +} + +// Make sure a dvd_subtitle track is selected while a menu is visible, +// so the SPU graphics decoded by sd_lavc reach the OSD. +static void ensure_menu_sub_selection(struct MPContext *mpctx, bool menu_on) +{ + struct disc_nav_state *st = get_state(mpctx); + struct track *cur = mpctx->current_track[0][STREAM_SUB]; + + // If the track list got rebuilt under us (e.g. another file/disc loaded + // mid-session) the cached pointer could be stale. Trust only what we + // can still see in mpctx->tracks. + if (st->menu_selected_track) { + bool still_present = false; + for (int n = 0; n < mpctx->num_tracks; n++) { + if (mpctx->tracks[n] == st->menu_selected_track) { + still_present = true; + break; + } + } + if (!still_present) + st->menu_selected_track = NULL; + } + + if (menu_on) { + if (cur || st->menu_selected_track) + return; + if (mpctx->opts->stream_id[0][STREAM_SUB] == -2) + return; + struct track *pick = NULL; + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *t = mpctx->tracks[n]; + if (t->type == STREAM_SUB && t->stream && t->stream->codec && + t->stream->codec->codec && + strcmp(t->stream->codec->codec, "dvd_subtitle") == 0) + { + pick = t; + break; + } + } + if (!pick) + return; + mp_switch_track_n(mpctx, 0, STREAM_SUB, pick, 0); + st->menu_selected_track = pick; + } else { + if (!st->menu_selected_track) + return; + // Only revert if our override is still the active selection. + if (cur == st->menu_selected_track) + mp_switch_track_n(mpctx, 0, STREAM_SUB, NULL, 0); + st->menu_selected_track = NULL; + } +} + +void disc_nav_update(struct MPContext *mpctx) +{ + struct stream *s = disc_nav_get_stream(mpctx); + struct stream_nav_state nav = {0}; + bool have = s && stream_control(s, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1; + if (!have) { + struct disc_nav_state *st = mpctx->disc_nav; + if (!st) + return; + if (st->overlay_visible) { + osd_set_external2(mpctx->osd, NULL); + mp_input_disable_section(mpctx->input, "discnav"); + st->overlay_visible = false; + } + st->bd_last_change_id = 0; + st->bd_last_vo_res = (struct mp_osd_res){0}; + st->menu_selected_track = NULL; + return; + } + + struct disc_nav_state *st = get_state(mpctx); + + check_async_discontinuity(mpctx, &nav); + sync_current_edition(mpctx, s, &nav); + sync_disc_track_selection(mpctx, s, &nav); + + bool is_bd = strcmp(s->info->name, "bd") == 0 || strcmp(s->info->name, "bdmv/bluray") == 0; + bool visible = nav.menu_active && (is_bd || (nav.hl_w > 0 && nav.hl_h > 0)); + + if (is_bd) { + push_bd_overlay(mpctx, s, &nav, visible); + } else { + push_dvd_overlay(mpctx, &nav, visible); + ensure_menu_sub_selection(mpctx, nav.menu_active); + } + + if (visible != st->overlay_visible) { + MP_VERBOSE(mpctx, "discnav: overlay %s\n", visible ? "on" : "off"); + if (visible) { + mp_input_enable_section(mpctx->input, "discnav", MP_INPUT_ON_TOP); + } else { + mp_input_disable_section(mpctx->input, "discnav"); + } + st->overlay_visible = visible; + } +} diff --git a/player/loadfile.c b/player/loadfile.c index 0b3e70d550d59..0b4f61d0ee8f6 100644 --- a/player/loadfile.c +++ b/player/loadfile.c @@ -367,6 +367,23 @@ void update_demuxer_properties(struct MPContext *mpctx) add_demuxer_tracks(mpctx, tracks); print_track_list(mpctx, NULL); tracks->events &= ~DEMUX_EVENT_STREAMS; + + // Streams surfaced after play_current_file's selection phase (e.g. + // disc-nav playlist hop, or lavf identifying a PES stream only once + // its first packet arrives) wouldn't otherwise get auto-selected. + if (mpctx->playback_initialized && mpctx->opts->stream_auto_sel) { + for (int t = 0; t < STREAM_TYPE_COUNT; t++) { + for (int i = 0; i < num_ptracks[t]; i++) { + if (mpctx->current_track[i][t]) + continue; + if (mpctx->opts->stream_id[i][t] == -2) + continue; + struct track *sel = select_default_track(mpctx, i, t); + if (sel) + mp_switch_track_n(mpctx, i, t, sel, 0); + } + } + } } if (events & DEMUX_EVENT_METADATA) { struct mp_tags *info = @@ -410,6 +427,25 @@ void update_demuxer_properties(struct MPContext *mpctx) } if (events & DEMUX_EVENT_DURATION) mp_notify(mpctx, MP_EVENT_DURATION_UPDATE, NULL); + if (events & DEMUX_EVENT_LISTS) { + // Demuxer just published a new chapter / edition list at runtime. + talloc_free(mpctx->chapters); + mpctx->chapters = NULL; + mpctx->num_chapters = demuxer->num_chapters; + if (demuxer->num_chapters > 0) { + mpctx->chapters = demux_copy_chapter_data(demuxer->chapters, + demuxer->num_chapters); + if (mpctx->opts->rebase_start_time) { + for (int n = 0; n < mpctx->num_chapters; n++) + mpctx->chapters[n].pts -= demuxer->start_time; + } + } + mp_notify(mpctx, MP_EVENT_CHAPTER_CHANGE, NULL); + mp_notify_property(mpctx, "chapter-list"); + mp_notify_property(mpctx, "edition"); + mp_notify_property(mpctx, "current-edition"); + mp_notify_property(mpctx, "editions"); + } demuxer->events = 0; } @@ -1576,8 +1612,12 @@ static int reinit_complex_filters(struct MPContext *mpctx, bool force_uninit) cleanup_deassociated_complex_filters(mpctx); if (mpctx->playback_initialized) { - for (int n = 0; n < mpctx->num_tracks; n++) - reselect_demux_stream(mpctx, mpctx->tracks[n], false); + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *t = mpctx->tracks[n]; + if (t->stream && t->stream->dependent_track && !t->selected) + continue; + reselect_demux_stream(mpctx, t, false); + } } mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL); @@ -1585,11 +1625,25 @@ static int reinit_complex_filters(struct MPContext *mpctx, bool force_uninit) return success ? 1 : -1; } +// Match the enhancement-layer pairing on the vo_chain to the currently +// selected video track. Idempotent. Decoupled from lavfi-complex internals. +void update_vo_chain_el_pair(struct MPContext *mpctx) +{ + if (!mpctx->vo_chain || !mpctx->vo_chain->filter) + return; + struct track *track = mpctx->current_track[0][STREAM_VIDEO]; + mp_output_chain_set_el_stream(mpctx->vo_chain->filter, + track ? sh_stream_dependent_sibling(track->stream) : NULL); +} + void update_lavfi_complex(struct MPContext *mpctx) { if (mpctx->playback_initialized) { - if (reinit_complex_filters(mpctx, false) != 0) + int r = reinit_complex_filters(mpctx, false); + if (r != 0) issue_refresh_seek(mpctx, MPSEEK_EXACT); + if (r > 0) + update_vo_chain_el_pair(mpctx); } } @@ -1919,10 +1973,18 @@ static void play_current_file(struct MPContext *mpctx) } process_hooks(mpctx, "on_loaded"); - for (int t = 0; t < STREAM_TYPE_COUNT; t++) - for (int n = 0; n < mpctx->num_tracks; n++) - if (mpctx->tracks[n]->type == t) - reselect_demux_stream(mpctx, mpctx->tracks[n], false); + for (int t = 0; t < STREAM_TYPE_COUNT; t++) { + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *track = mpctx->tracks[n]; + if (track->type != t) + continue; + // Only reselect dependent tracks when explicitly selected by user + if (track->stream && track->stream->dependent_track && + !track->selected) + continue; + reselect_demux_stream(mpctx, track, false); + } + } update_demuxer_properties(mpctx); @@ -1931,6 +1993,9 @@ static void play_current_file(struct MPContext *mpctx) reinit_video_chain(mpctx); reinit_audio_chain(mpctx); reinit_sub_all(mpctx); + // For lavfi-complex mode reinit_video_chain skips chain setup, so set up + // the enhancement-layer pairing here. No-op in non-lavfi-complex mode. + update_vo_chain_el_pair(mpctx); if (mpctx->encode_lavc_ctx) { if (mpctx->vo_chain) diff --git a/player/main.c b/player/main.c index 6891c9d5a1d38..0ef3d7f6d6581 100644 --- a/player/main.c +++ b/player/main.c @@ -190,6 +190,7 @@ void mp_destroy(struct MPContext *mpctx) mpctx->encode_lavc_ctx = NULL; command_uninit(mpctx); + disc_nav_destroy(mpctx); mp_clients_destroy(mpctx); diff --git a/player/playloop.c b/player/playloop.c index d899fba1236df..f472e40969f17 100644 --- a/player/playloop.c +++ b/player/playloop.c @@ -1288,6 +1288,8 @@ void run_playloop(struct MPContext *mpctx) handle_update_subtitles(mpctx); + disc_nav_update(mpctx); + handle_each_frame_screenshot(mpctx); handle_eof(mpctx); diff --git a/player/video.c b/player/video.c index 820bbfbf6c961..e2cc4801f28a0 100644 --- a/player/video.c +++ b/player/video.c @@ -279,6 +279,8 @@ void reinit_video_chain_src(struct MPContext *mpctx, struct track *track) mp_pin_connect(vo_c->filter->f->pins[0], vo_c->dec_src); } + update_vo_chain_el_pair(mpctx); + if (!recreate_video_filters(mpctx)) goto err_out; diff --git a/stream/stream.h b/stream/stream.h index 98b5dff355a6f..521c60034a02c 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -94,6 +94,59 @@ enum stream_ctrl { STREAM_CTRL_GET_LANG, STREAM_CTRL_GET_CURRENT_TITLE, STREAM_CTRL_SET_CURRENT_TITLE, + STREAM_CTRL_NAV_CMD, // struct stream_nav_cmd* + STREAM_CTRL_GET_NAV_STATE, // struct stream_nav_state* + STREAM_CTRL_GET_NAV_OVERLAY, // struct stream_nav_overlay_req* +}; + +// Fetch a BGRA snapshot of the disc-menu overlay (used for Blu-ray HDMV). +struct stream_nav_overlay_req { + int w, h; // input: caller's plane dimensions; output: actual + int stride; // input: dest row stride in bytes + uint8_t *dst; // input: dest buffer (caller-allocated, BGRA) + uint32_t change_id; // output: latched change_id of the snapshot +}; + +// In-disc navigation (DVD/BD menu) actions, used with STREAM_CTRL_NAV_CMD. +enum stream_nav_action { + STREAM_NAV_UP, + STREAM_NAV_DOWN, + STREAM_NAV_LEFT, + STREAM_NAV_RIGHT, + STREAM_NAV_SELECT, // activate currently highlighted button + STREAM_NAV_MENU_ROOT, // jump to the disc's root/title menu + STREAM_NAV_MENU_TITLE, // jump to the current title's menu + STREAM_NAV_MENU_POPUP, // BD popup menu (no-op for DVD) + STREAM_NAV_PREV_MENU, // return to previous menu / leave still + STREAM_NAV_MOUSE_MOVE, // mouse moved; .x,.y are video-space coords + STREAM_NAV_MOUSE_CLICK, // mouse button activated at .x,.y +}; + +struct stream_nav_cmd { + enum stream_nav_action action; + int x, y; // for MOUSE_* +}; + +// Snapshot of the stream's menu state. +struct stream_nav_state { + bool menu_active; // a selectable menu/highlight is currently visible + bool has_popup; // disc supports a popup menu (BD only) + int src_w, src_h; // dimensions of the coordinate space mouse uses + // Highlight rectangle of the currently focused button (in src coords). + int hl_x, hl_y, hl_w, hl_h; + // BGRA highlight palette (0xAARRGGBB, straight) to substitute for the + // four SPU pixel values inside the highlight rect. Zeroed when no highlight + // is active. + uint32_t hl_palette[4]; + uint32_t change_id; // Bumped whenever any of the above changes + uint32_t discontinuity_id; // Bumped when the stream's source position jumps + + // Disc-driven track selection. + int active_audio_id; + int active_sub_id; + bool sub_visible; // disc says subs should be displayed + int angle; // 1-based current angle (0 if unknown) + int num_angles; // total angle count (0 if unknown or always 1) }; struct stream_lang_req { diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c index ef048afd1f5d8..1a03ed8fcfeeb 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -41,9 +41,11 @@ #include "mpv_talloc.h" #include "common/common.h" #include "common/msg.h" +#include "misc/thread_tools.h" #include "options/m_config.h" #include "options/options.h" #include "options/path.h" +#include "osdep/threads.h" #include "stream.h" #include "osdep/io.h" #include "osdep/timer.h" @@ -88,6 +90,14 @@ const struct m_sub_options stream_bluray_conf = { }, }; +// 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) + bool visible; // publish has any non-zero alpha pixel +}; + struct bluray_priv_s { BLURAY *bd; BLURAY_TITLE_INFO *title_info; @@ -96,14 +106,287 @@ struct bluray_priv_s { int current_title; int current_playlist; + // Cached map from filtered title index (0..num_titles-1) to mpls_id. + uint32_t *title_to_playlist; + int cfg_title; int cfg_playlist; char *cfg_device; struct mp_bluray_opts *opts; struct m_config_cache *opts_cache; + + // 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; // guards plane fields + visibility flags + + bool menu_event_active; // BD_EVENT_MENU == 1 + bool popup_supported; // BD_EVENT_POPUP == 1 + uint32_t nav_change_id; // bumped on FLUSH/HIDE/MENU/POPUP events + uint32_t discontinuity_id; // bumped on actions that may hop (SELECT...) + bool data_delivered; // any byte returned from fill_buffer yet + + // 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; we resolve to MPEG-TS PIDs via title_info on demand. + // 0 = unknown (no event seen yet), 0xff/0xfff = "none" sentinel from BD. + int audio_stream_num; + int sub_stream_num; + bool sub_visible; + + int mouse_x, mouse_y; }; +// 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 void bd_palette_to_bgra(const BD_PG_PALETTE_ENTRY *pg, uint32_t out[256]) +{ + for (int i = 0; i < 256; i++) { + int Y = pg[i].Y, Cb = pg[i].Cb, Cr = pg[i].Cr, T = pg[i].T; + // BT.709 limited->full. + int y_ = (Y - 16) * 1192; // 1.164 << 10 + int cr = Cr - 128; + int cb = Cb - 128; + int r = (y_ + 1836 * cr + 512) >> 10; // 1.793 + int g = (y_ - 547 * cr - 218 * cb + 512) >> 10; // 0.534 / 0.213 + int b = (y_ + 2166 * cb + 512) >> 10; // 2.115 + r = MPCLAMP(r, 0, 255); + g = MPCLAMP(g, 0, 255); + b = MPCLAMP(b, 0, 255); + // Pre-multiply RGB by alpha so the OSD layer can composite directly. + r = r * T / 255; + g = g * T / 255; + b = b * T / 255; + out[i] = ((uint32_t)T << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + // palette index 0xFF is always transparent. + out[0xFF] = 0; +} + +// Composite one RLE-encoded sub-bitmap into the IG plane at (ov->x, ov->y). +static void bd_overlay_draw_rle(struct bd_overlay_plane *p, const BD_OVERLAY *ov) +{ + if (!ov->img || !ov->palette || ov->w <= 0 || ov->h <= 0) + return; + uint32_t pal[256]; + bd_palette_to_bgra(ov->palette, pal); + + const BD_PG_RLE_ELEM *rle = ov->img; + for (int y = 0; y < ov->h; y++) { + int dst_y = ov->y + y; + bool in_plane = dst_y >= 0 && dst_y < p->h; + uint32_t *dst_row = in_plane ? p->work + (size_t)dst_y * p->w : NULL; + int x = 0; + while (x < ov->w) { + 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: + bd_overlay_ensure(priv, p, ov->w, 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); + 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: + bd_overlay_ensure(priv, p, ov->w, 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); @@ -122,17 +405,35 @@ static void bluray_stream_close(stream_t *s) if (priv->title_info) bd_free_title_info(priv->title_info); - if (priv->bd) + if (priv->bd) { + if (priv->hdmv_mode) { + bd_register_overlay_proc(priv->bd, NULL, NULL); + bd_register_argb_overlay_proc(priv->bd, NULL, NULL, NULL); + } bd_close(priv->bd); + } + if (priv->hdmv_mode) + mp_mutex_destroy(&priv->overlay_lock); } static void handle_event(stream_t *s, const BD_EVENT *ev) { struct bluray_priv_s *b = s->priv; + if (b->hdmv_mode) + MP_VERBOSE(s, "bdnav: event %d param %u\n", ev->event, ev->param); switch (ev->event) { case BD_EVENT_MENU: + // ev->param: 1 if the disc is currently in an HDMV menu, 0 otherwise. + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->menu_event_active = ev->param != 0; + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; case BD_EVENT_STILL: + if (ev->param) + bd_read_skip_still(b->bd); break; case BD_EVENT_STILL_TIME: bd_read_skip_still(b->bd); @@ -142,20 +443,35 @@ static void handle_event(stream_t *s, const BD_EVENT *ev) case BD_EVENT_PLAYLIST: b->current_playlist = ev->param; b->current_title = bd_get_current_title(b->bd); + if (b->title_to_playlist) { + for (int i = 0; i < b->num_titles; i++) { + if (b->title_to_playlist[i] == (uint32_t)ev->param) { + b->current_title = i; + break; + } + } + } if (b->title_info) bd_free_title_info(b->title_info); b->title_info = bd_get_playlist_info(b->bd, b->current_playlist, b->current_angle); + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; case BD_EVENT_TITLE: - if (ev->param == BLURAY_TITLE_FIRST_PLAY) { - b->current_title = bd_get_current_title(b->bd); - } else - b->current_title = ev->param; + b->current_title = bd_get_current_title(b->bd); if (b->title_info) { bd_free_title_info(b->title_info); b->title_info = NULL; } + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; case BD_EVENT_ANGLE: b->current_angle = ev->param; @@ -164,8 +480,44 @@ static void handle_event(stream_t *s, const BD_EVENT *ev) b->title_info = bd_get_playlist_info(b->bd, b->current_playlist, b->current_angle); } + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } + break; + case BD_EVENT_AUDIO_STREAM: + b->audio_stream_num = ev->param; + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } + break; + case BD_EVENT_PG_TEXTST_STREAM: + b->sub_stream_num = ev->param; + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } + break; + case BD_EVENT_PG_TEXTST: + b->sub_visible = ev->param != 0; + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; case BD_EVENT_POPUP: + // ev->param: 1 if popup menu is currently available, 0 otherwise. + if (b->hdmv_mode) { + mp_mutex_lock(&b->overlay_lock); + b->popup_supported = ev->param != 0; + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + } break; #if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 5, 0) case BD_EVENT_DISCONTINUITY: @@ -181,6 +533,51 @@ static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len) { struct bluray_priv_s *b = s->priv; BD_EVENT event; + + if (b->hdmv_mode) { + // bd_read() doesn't drive the HDMV VM, so the disc's first-play + // bytecode would never run and we'd be stuck with "no valid title" + // forever. bd_read_ext() runs the VM between event drains and also + // delivers one event per call, which we hand off to handle_event. + while (bd_get_event(b->bd, &event)) + handle_event(s, &event); + int total = 0; + int events_seen = 0; + // Loop briefly to absorb event-only returns (where bd_read_ext + // returns 0 with a freshly produced event) before reporting EOF. + // If an event bumps discontinuity_id (PLAYLIST/TITLE) *after* we + // have already delivered data to the slave demuxer, stop here even + // if no data was read: the next bd_read_ext would deliver data from + // the new playlist, but the slave must be reopened first so it + // parses with the correct codec context. + for (int i = 0; i < 200; i++) { + uint32_t disc_before = b->discontinuity_id; + int n = bd_read_ext(b->bd, (uint8_t *)buf + total, len - total, &event); + if (n < 0) { + MP_VERBOSE(s, "bdnav: bd_read_ext err iter=%d\n", i); + return -1; + } + if (event.event != BD_EVENT_NONE) { + handle_event(s, &event); + events_seen++; + } + if (n > 0) { + total += n; + break; + } + if (b->data_delivered && b->discontinuity_id != disc_before) + break; + if (mp_cancel_test(s->cancel)) + return 0; + mp_sleep_ns(MP_TIME_MS_TO_NS(5)); + } + if (total > 0) + b->data_delivered = true; + if (total == 0) + MP_VERBOSE(s, "bdnav: fill returned 0 (events=%d)\n", events_seen); + return total; + } + while (bd_get_event(b->bd, &event)) handle_event(s, &event); return bd_read(b->bd, buf, len); @@ -213,6 +610,13 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) } case STREAM_CTRL_SET_CURRENT_TITLE: { const uint32_t title = *((unsigned int*)arg); + // demux_disc appends a synthetic "Disc Menu" edition at index num_titles. + if (title == b->num_titles) { + if (!b->hdmv_mode) + return STREAM_UNSUPPORTED; + bd_menu_call(b->bd, -1); + return STREAM_OK; + } if (title >= b->num_titles || !play_title(b, title)) return STREAM_UNSUPPORTED; b->current_title = title; @@ -321,6 +725,154 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) *(char**)arg = talloc_strdup(NULL, meta->di_name); return STREAM_OK; } + case STREAM_CTRL_NAV_CMD: { + if (!b->hdmv_mode) + return STREAM_UNSUPPORTED; + struct stream_nav_cmd *nav = arg; + uint32_t key = BD_VK_NONE; + switch (nav->action) { + case STREAM_NAV_UP: + key = BD_VK_UP; + break; + case STREAM_NAV_DOWN: + key = BD_VK_DOWN; + break; + case STREAM_NAV_LEFT: + key = BD_VK_LEFT; + break; + case STREAM_NAV_RIGHT: + key = BD_VK_RIGHT; + break; + case STREAM_NAV_SELECT: + key = BD_VK_ENTER; + break; + case STREAM_NAV_MENU_ROOT: + case STREAM_NAV_MENU_TITLE: + // BD doesn't distinguish "title menu", both map to disc root. + bd_menu_call(b->bd, -1); + return STREAM_OK; + case STREAM_NAV_MENU_POPUP: + key = BD_VK_POPUP; + break; + case STREAM_NAV_PREV_MENU: + // No dedicated "previous menu" key; popup-toggle is the closest + // equivalent and behaves like "dismiss current menu" on most + // discs when already in popup. + key = BD_VK_POPUP; + break; + case STREAM_NAV_MOUSE_MOVE: + b->mouse_x = nav->x; + b->mouse_y = nav->y; + bd_mouse_select(b->bd, -1, nav->x, nav->y); + return STREAM_OK; + case STREAM_NAV_MOUSE_CLICK: + b->mouse_x = nav->x; + b->mouse_y = nav->y; + bd_mouse_select(b->bd, -1, nav->x, nav->y); + key = BD_VK_MOUSE_ACTIVATE; + break; + } + if (key != BD_VK_NONE) + bd_user_input(b->bd, -1, key); + return STREAM_OK; + } + case STREAM_CTRL_GET_NAV_STATE: { + struct stream_nav_state *st = arg; + int audio_pid = -1; + int sub_pid = -1; + const BLURAY_TITLE_INFO *ti = b->title_info; + if (ti && ti->clip_count) { + const BLURAY_CLIP_INFO *ci = &ti->clips[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){ + .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, + }; + return STREAM_OK; + } + mp_mutex_lock(&b->overlay_lock); + // BD_EVENT_MENU isn't fired by BD-J (it's HDMV-only), so treat any + // visible BD-J plane as an active menu too. + bool any_overlay = b->ig.visible || b->pg.visible; + bool visible = (b->menu_event_active && b->ig.visible) || any_overlay; + *st = (struct stream_nav_state){ + .menu_active = visible, + .has_popup = b->popup_supported, + .src_w = MPMAX(b->ig.w, b->pg.w), + .src_h = MPMAX(b->ig.h, b->pg.h), + .change_id = b->nav_change_id, + .discontinuity_id = b->discontinuity_id, + .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.w, b->pg.w)); + int copy_h = MPMIN(req->h, MPMAX(b->ig.h, b->pg.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; } @@ -463,13 +1015,17 @@ static int bluray_stream_open_internal(stream_t *s) MP_INFO(s, "List of available titles:\n"); /* parse titles information */ + b->title_to_playlist = talloc_array(b, uint32_t, b->num_titles); for (int i = 0; i < b->num_titles; i++) { + b->title_to_playlist[i] = (uint32_t)-1; /* the information we're accessing (duration, playlist, angle count) * doesn't depend on the angle */ BLURAY_TITLE_INFO *ti = bd_get_title_info(bd, i, 0); if (!ti) continue; + b->title_to_playlist[i] = ti->playlist; + char *time = mp_format_time(ti->duration / 90000, false); MP_INFO(s, "idx: %3d duration: %s angles: %2d (playlist: %05d.mpls)\n", i, time, ti->angle_count, ti->playlist); @@ -485,10 +1041,47 @@ static int bluray_stream_open_internal(stream_t *s) // initialize libbluray event queue bd_get_event(bd, NULL); - select_initial_title(s, bd_get_main_title(bd)); + 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) { + mp_mutex_init(&b->overlay_lock); + bd_register_overlay_proc(bd, b, bd_yuv_overlay_cb); + if (info->num_bdj_titles) + 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)); + } - if (!bd_select_angle(bd, b->opts->angle - 1)) - MP_WARN(s, "Couldn't select angle '%d'.\n", b->opts->angle - 1); + // Angle selection is only valid once a playlist has been picked. + if (!b->hdmv_mode) { + if (!bd_select_angle(bd, b->opts->angle - 1)) + MP_WARN(s, "Couldn't select angle '%d'.\n", b->opts->angle - 1); + } b->current_angle = bd_get_current_angle(bd); @@ -519,12 +1112,15 @@ static int bluray_stream_open(stream_t *s) struct MPOpts *opts = mp_get_config_group(s, s->global, &mp_opt_root); int edition_id = opts->edition_id; + bool disc_menu = opts->disc_menu; talloc_free(opts); if (edition_id >= 0) { b->cfg_title = edition_id; - } else if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) { - b->cfg_title = BLURAY_DEFAULT_TITLE; + } else if (title.len == 0 || bstr_equals0(title, "longest") || + bstr_equals0(title, "first")) + { + b->cfg_title = disc_menu ? BLURAY_MENU_TITLE : BLURAY_DEFAULT_TITLE; } else if (bstr_equals0(title, "menu")) { b->cfg_title = BLURAY_MENU_TITLE; } else if (bstr_equals0(title, "mpls")) { @@ -609,8 +1205,11 @@ static int bdmv_dir_stream_open(stream_t *stream) struct bluray_priv_s *priv = talloc_ptrtype(stream, priv); stream->priv = priv; struct MPOpts *opts = mp_get_config_group(NULL, stream->global, &mp_opt_root); + int default_title = opts->edition_id >= 0 ? opts->edition_id + : opts->disc_menu ? BLURAY_MENU_TITLE + : BLURAY_DEFAULT_TITLE; *priv = (struct bluray_priv_s){ - .cfg_title = opts->edition_id >= 0 ? opts->edition_id : BLURAY_DEFAULT_TITLE, + .cfg_title = default_title, }; talloc_free(opts); diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index ad688f109b924..912ef8d135e52 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -44,16 +44,22 @@ #include "options/options.h" #include "common/msg.h" #include "input/input.h" +#include "misc/thread_tools.h" #include "options/m_config.h" #include "options/path.h" #include "osdep/timer.h" #include "stream.h" #include "demux/demux.h" +#include "video/csputils.h" #include "video/out/vo.h" #define TITLE_MENU -1 #define TITLE_LONGEST -2 +// Default source dimensions if dvdnav_get_video_resolution() fails. +#define DVD_SRC_W_DEFAULT 720 +#define DVD_SRC_H_DEFAULT 576 + struct priv { dvdnav_t *dvdnav; // handle to libdvdnav stuff char *filename; // path @@ -69,6 +75,20 @@ struct priv { int track; char *device; + bool in_menu; + int current_button; // mirror of libdvdnav HL_BTNN_REG + int btn_rect[4]; // x, y, w, h in source coords + uint32_t hl_palette[4]; // 0xAARRGGBB for SPU pixel values 0..3 + uint32_t nav_change_id; + uint32_t discontinuity_id; // bumped on actions that may jump + int src_w, src_h; // video resolution in pixels + int auto_actioned_button; // last auto-activated button; 0 if none + + // Disc-driven audio/sub/angle state. + int audio_physical; // 0..7 from DVDNAV_AUDIO_STREAM_CHANGE + int sub_physical; // 0..31 from DVDNAV_SPU_STREAM_CHANGE + bool sub_visible; // SPU "on" flag from same event + struct dvd_opts *opts; }; @@ -209,6 +229,238 @@ static int dvd_probe(const char *path, const char *ext, const char *sig) return r; } +static bool in_menu_domain(dvdnav_t *dvdnav) +{ + return dvdnav_is_domain_vmgm(dvdnav) == 1 || + dvdnav_is_domain_vtsm(dvdnav) == 1 || + dvdnav_is_domain_fp(dvdnav) == 1; +} + +static void compute_button_rect(struct priv *priv, pci_t *pci, int btn) +{ + priv->btn_rect[0] = priv->btn_rect[1] = 0; + priv->btn_rect[2] = priv->btn_rect[3] = 0; + if (btn <= 0 || btn > pci->hli.hl_gi.btn_ns) + return; + // btni_t is packed and full of bitfields, memcpy to ensure correct alignment. + btni_t b; + memcpy(&b, &pci->hli.btnit[btn - 1], sizeof(b)); + int xs = b.x_start, xe = b.x_end; + int ys = b.y_start, ye = b.y_end; + priv->btn_rect[0] = xs; + priv->btn_rect[1] = ys; + priv->btn_rect[2] = xe > xs ? xe - xs : 0; + priv->btn_rect[3] = ye > ys ? ye - ys : 0; +} + +// Resolve the 4-entry "select-state" highlight palette for the focused button. +// btn_coli holds two packed words per color set ([select, action]); the select +// word encodes [Ci3 Ci2 Ci1 Ci0 A3 A2 A1 A0]: four CLUT indices and four 4-bit +// alphas. +static void compute_highlight_palette(struct priv *priv, pci_t *pci, int btn) +{ + memset(priv->hl_palette, 0, sizeof(priv->hl_palette)); + if (!priv->spu_clut_valid || btn <= 0 || btn > pci->hli.hl_gi.btn_ns) + return; + btni_t b; + memcpy(&b, &pci->hli.btnit[btn - 1], sizeof(b)); + if (b.btn_coln == 0) + return; // spec: "no color" - button is invisible + int coln = b.btn_coln - 1; + if (coln > 2) + coln = 2; + uint32_t coli; + memcpy(&coli, &pci->hli.btn_colit.btn_coli[coln][1], sizeof(coli)); // [1] = select state + + struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS; + struct pl_transform3x3 cmatrix; + mp_get_csp_matrix(&csp, &cmatrix); + + for (int i = 0; i < 4; i++) { + uint8_t ci = (coli >> (16 + i * 4)) & 0xF; + uint8_t a = (coli >> (i * 4)) & 0xF; + uint32_t entry = priv->spu_clut[ci]; + // CLUT entry is 0x00YYCrCb. mp_get_csp_matrix returns a YCbCr→RGB matrix + // expecting {Y, Cb, Cr}, reorder to match this. + int y[3] = {(entry >> 16) & 0xff, entry & 0xff, (entry >> 8) & 0xff}; + int c[3]; + mp_map_fixp_color(&cmatrix, 8, y, 8, c); + uint32_t alpha = (a << 4) | a; + priv->hl_palette[i] = (alpha << 24) | (c[0] << 16) | (c[1] << 8) | c[2]; + } +} + +// Map a libdvdnav physical audio stream number (0..7) to the corresponding +// MPEG-PS substream byte that demux_lavf assigns to AVStream->id. +static int dvd_physical_audio_to_substream(struct priv *priv, int physical) +{ + if (physical < 0 || physical > 7) + return -1; + uint16_t fmt = dvdnav_audio_stream_format(priv->dvdnav, physical); + switch (fmt) { + case DVD_AUDIO_FORMAT_AC3: + return 0x80 + physical; + case DVD_AUDIO_FORMAT_DTS: + return 0x88 + physical; + case DVD_AUDIO_FORMAT_LPCM: + return 0xa0 + physical; + case DVD_AUDIO_FORMAT_MPEG: + case DVD_AUDIO_FORMAT_MPEG2_EXT: + return 0xc0 + physical; + default: + return -1; + } +} + +static void refresh_video_resolution(struct priv *priv) +{ + uint32_t w = 0, h = 0; + if (dvdnav_get_video_resolution(priv->dvdnav, &w, &h) == DVDNAV_STATUS_OK && + w > 0 && h > 0) + { + priv->src_w = (int)w; + priv->src_h = (int)h; + } else { + priv->src_w = DVD_SRC_W_DEFAULT; + priv->src_h = DVD_SRC_H_DEFAULT; + } +} + +// Pull the current selection back from libdvdnav and refresh our overlay +// state. Called on NAV_PACKET/HIGHLIGHT events and after every nav command. +static void update_highlight(struct priv *priv) +{ + int prev_btn = priv->current_button; + int prev_x = priv->btn_rect[0], prev_y = priv->btn_rect[1]; + int prev_w = priv->btn_rect[2], prev_h = priv->btn_rect[3]; + bool prev_menu = priv->in_menu; + uint32_t prev_palette[4]; + memcpy(prev_palette, priv->hl_palette, sizeof(prev_palette)); + + priv->in_menu = in_menu_domain(priv->dvdnav); + pci_t *pci = priv->in_menu ? dvdnav_get_current_nav_pci(priv->dvdnav) : NULL; + bool has_buttons = pci && pci->hli.hl_gi.hli_ss != 0 && pci->hli.hl_gi.btn_ns > 0; + + // Suppress the visible highlight while we're inside the menu's intro. + // The PCI gives us both the current VOBU's start PTS and the highlight + // valid window; once vobu_s_ptm catches up to hli_s_ptm (and we're still + // inside hli_e_ptm), it's live. + bool highlight_live = false; + if (has_buttons) { + uint32_t now = pci->pci_gi.vobu_s_ptm; + uint32_t hs = pci->hli.hl_gi.hli_s_ptm; + uint32_t he = pci->hli.hl_gi.hli_e_ptm; + highlight_live = now >= hs && (he == 0 || now < he); + } + + int32_t btn = 0; + if (highlight_live) + dvdnav_get_current_highlight(priv->dvdnav, &btn); + + if (!highlight_live || btn <= 0 || btn > pci->hli.hl_gi.btn_ns) { + priv->current_button = 0; + priv->btn_rect[0] = priv->btn_rect[1] = 0; + priv->btn_rect[2] = priv->btn_rect[3] = 0; + memset(priv->hl_palette, 0, sizeof(priv->hl_palette)); + } else { + priv->current_button = btn; + compute_button_rect(priv, pci, btn); + compute_highlight_palette(priv, pci, btn); + } + + if (priv->in_menu != prev_menu || priv->current_button != prev_btn || + priv->btn_rect[0] != prev_x || priv->btn_rect[1] != prev_y || + priv->btn_rect[2] != prev_w || priv->btn_rect[3] != prev_h || + memcmp(prev_palette, priv->hl_palette, sizeof(prev_palette)) != 0) + { + priv->nav_change_id++; + } + + // When we leave the menu, clear the auto-action latch so the next entry + // can fire again on the same button number. + if (!priv->in_menu) + priv->auto_actioned_button = 0; + + // Auto-action buttons: a btnit entry can request immediate activation + // when its button gets focus (auto_action_mode == 1). + // Fire it once per menu entry / button transition. + if (highlight_live && priv->current_button > 0 && + priv->current_button != priv->auto_actioned_button) + { + btni_t b; + memcpy(&b, &pci->hli.btnit[priv->current_button - 1], sizeof(b)); + if (b.auto_action_mode == 1) { + priv->auto_actioned_button = priv->current_button; + dvdnav_button_activate(priv->dvdnav, pci); + } + } +} + +static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) +{ + struct priv *priv = stream->priv; + + switch (cmd->action) { + case STREAM_NAV_MENU_ROOT: + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Root); + update_highlight(priv); + return; + case STREAM_NAV_MENU_TITLE: + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Title); + update_highlight(priv); + return; + case STREAM_NAV_MENU_POPUP: + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Part); + update_highlight(priv); + return; + case STREAM_NAV_PREV_MENU: + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Escape); + update_highlight(priv); + return; + default: + break; + } + + if (!in_menu_domain(priv->dvdnav)) + return; + + pci_t *pci = dvdnav_get_current_nav_pci(priv->dvdnav); + if (!pci || pci->hli.hl_gi.hli_ss == 0 || pci->hli.hl_gi.btn_ns == 0) + return; + + switch (cmd->action) { + case STREAM_NAV_UP: + dvdnav_upper_button_select(priv->dvdnav, pci); + break; + case STREAM_NAV_DOWN: + dvdnav_lower_button_select(priv->dvdnav, pci); + break; + case STREAM_NAV_LEFT: + dvdnav_left_button_select(priv->dvdnav, pci); + break; + case STREAM_NAV_RIGHT: + dvdnav_right_button_select(priv->dvdnav, pci); + break; + case STREAM_NAV_MOUSE_MOVE: + priv->mousex = cmd->x; + priv->mousey = cmd->y; + dvdnav_mouse_select(priv->dvdnav, pci, cmd->x, cmd->y); + break; + case STREAM_NAV_MOUSE_CLICK: + priv->mousex = cmd->x; + priv->mousey = cmd->y; + dvdnav_mouse_activate(priv->dvdnav, pci, cmd->x, cmd->y); + break; + case STREAM_NAV_SELECT: + dvdnav_button_activate(priv->dvdnav, pci); + break; + default: + break; + } + + update_highlight(priv); +} + /** * \brief mp_dvdnav_lang_from_aid() returns the language corresponding to audio id 'aid' * \param stream: - stream pointer @@ -309,22 +561,30 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) pci_t *pnavpci = dvdnav_get_current_nav_pci(dvdnav); uint32_t start_pts = pnavpci->pci_gi.vobu_s_ptm; MP_TRACE(s, "start pts = %"PRIu32"\n", start_pts); + // Each NAV packet can change the highlighted button or the + // available button set; keep our mirrored state in sync. + update_highlight(priv); break; } case DVDNAV_STILL_FRAME: dvdnav_still_skip(dvdnav); - return 0; + break; case DVDNAV_WAIT: dvdnav_wait_skip(dvdnav); - return 0; + break; + case DVDNAV_HOP_CHANNEL: + // Bump discontinuity_id so the playloop flushes the cache. + priv->discontinuity_id++; + break; case DVDNAV_HIGHLIGHT: + update_highlight(priv); break; case DVDNAV_VTS_CHANGE: { int tit = 0, part = 0; dvdnav_vts_change_event_t *vts_event = - (dvdnav_vts_change_event_t *)s->buffer; - MP_INFO(s, "DVDNAV, switched to title: %d\n", - vts_event->new_vtsN); + (dvdnav_vts_change_event_t *)buf; + MP_VERBOSE(s, "DVDNAV, switched to VTS: %d\n", + vts_event->new_vtsN); if (!priv->had_initial_vts) { // dvdnav sends an initial VTS change before any data; don't // cause a blocking wait for the player, because the player in @@ -338,6 +598,11 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) if (priv->title > 0 && tit != priv->title) MP_WARN(s, "Requested title not found\n"); } + // Resolution can change across VTS (PAL vs. NTSC titles); refresh + // so mouse coordinate translation stays correct. + refresh_video_resolution(priv); + // VTS change is a title-set boundary, flush. + priv->discontinuity_id++; break; } case DVDNAV_CELL_CHANGE: { @@ -351,6 +616,32 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) case DVDNAV_SPU_CLUT_CHANGE: { memcpy(priv->spu_clut, buf, 16 * sizeof(uint32_t)); priv->spu_clut_valid = true; + update_highlight(priv); + break; + } + case DVDNAV_AUDIO_STREAM_CHANGE: { + dvdnav_audio_stream_change_event_t *ev = buf; + // physical: 0..7 = active audio stream, -1 = SPU/audio off. + MP_VERBOSE(s, "DVDNAV, audio change phys=%d log=%d\n", ev->physical, ev->logical); + if (priv->audio_physical != ev->physical) { + priv->audio_physical = ev->physical; + priv->nav_change_id++; + } + break; + } + case DVDNAV_SPU_STREAM_CHANGE: { + dvdnav_spu_stream_change_event_t *ev = buf; + int raw = ev->physical_wide; + bool visible = raw >= 0 && !(raw & 0x80); + int phys = raw >= 0 ? (raw & 0x1F) : -1; + MP_VERBOSE(s, "DVDNAV, sub change phys_wide=0x%x lb=0x%x ps=0x%x log=%d\n", + ev->physical_wide, ev->physical_letterbox, + ev->physical_pan_scan, ev->logical); + if (priv->sub_physical != phys || priv->sub_visible != visible) { + priv->sub_physical = phys; + priv->sub_visible = visible; + priv->nav_change_id++; + } break; } } @@ -444,8 +735,20 @@ static int control(stream_t *stream, int cmd, void *arg) } case STREAM_CTRL_SET_CURRENT_TITLE: { int title = *((unsigned int *) arg); + int32_t num_titles = 0; + dvdnav_get_number_of_titles(priv->dvdnav, &num_titles); + // demux_disc appends a synthetic "Disc Menu" edition at the end. + if (title == num_titles) { + if (dvdnav_menu_call(priv->dvdnav, DVD_MENU_Root) + != DVDNAV_STATUS_OK) + break; + priv->discontinuity_id++; + stream_drop_buffers(stream); + return STREAM_OK; + } if (dvdnav_title_play(priv->dvdnav, title + 1) != DVDNAV_STATUS_OK) break; + priv->discontinuity_id++; stream_drop_buffers(stream); return STREAM_OK; } @@ -531,6 +834,36 @@ static int control(stream_t *stream, int cmd, void *arg) *(char**)arg = talloc_strdup(NULL, volume); return STREAM_OK; } + case STREAM_CTRL_NAV_CMD: { + handle_nav_cmd(stream, arg); + return STREAM_OK; + } + case STREAM_CTRL_GET_NAV_STATE: { + struct stream_nav_state *st = arg; + if (priv->src_w <= 0 || priv->src_h <= 0) + refresh_video_resolution(priv); + uint32_t cur_angle = 0, num_angles = 0; + dvdnav_get_angle_info(dvdnav, &cur_angle, &num_angles); + *st = (struct stream_nav_state){ + .menu_active = priv->in_menu, + .has_popup = false, + .src_w = priv->src_w, + .src_h = priv->src_h, + .hl_x = priv->btn_rect[0], + .hl_y = priv->btn_rect[1], + .hl_w = priv->btn_rect[2], + .hl_h = priv->btn_rect[3], + .change_id = priv->nav_change_id, + .discontinuity_id = priv->discontinuity_id, + .active_audio_id = dvd_physical_audio_to_substream(priv, priv->audio_physical), + .active_sub_id = priv->sub_physical >= 0 ? 0x20 + priv->sub_physical : -1, + .sub_visible = priv->sub_visible, + .angle = cur_angle, + .num_angles = num_angles, + }; + memcpy(st->hl_palette, priv->hl_palette, sizeof(st->hl_palette)); + return STREAM_OK; + } } return STREAM_UNSUPPORTED; @@ -582,6 +915,10 @@ static int open_s_internal(stream_t *stream) char *filename; int ret = 0; + priv->audio_physical = -1; + priv->sub_physical = -1; + priv->sub_visible = false; + p->opts = mp_get_config_group(stream, stream->global, &dvd_conf); if (p->device && p->device[0]) @@ -597,35 +934,39 @@ static int open_s_internal(stream_t *stream) goto err; } + int32_t num_titles = 0; + dvdnav_get_number_of_titles(priv->dvdnav, &num_titles); + if (p->track == TITLE_LONGEST) { // longest dvdnav_t *dvdnav = priv->dvdnav; uint64_t best_length = 0; int best_title = -1; - int32_t num_titles; - if (dvdnav_get_number_of_titles(dvdnav, &num_titles) == DVDNAV_STATUS_OK) { - MP_VERBOSE(stream, "List of available titles:\n"); - for (int n = 1; n <= num_titles; n++) { - uint64_t *parts = NULL, duration = 0; - dvdnav_describe_title_chapters(dvdnav, n, &parts, &duration); - if (parts) { - if (duration > best_length) { - best_length = duration; - best_title = n; - } - if (duration > 90000) { // arbitrarily ignore <1s titles - char *time = mp_format_time(duration / 90000, false); - MP_VERBOSE(stream, "title: %3d duration: %s\n", - n - 1, time); - talloc_free(time); - } - free(parts); + MP_VERBOSE(stream, "List of available titles:\n"); + for (int n = 1; n <= num_titles; n++) { + uint64_t *parts = NULL, duration = 0; + dvdnav_describe_title_chapters(dvdnav, n, &parts, &duration); + if (parts) { + if (duration > best_length) { + best_length = duration; + best_title = n; + } + if (duration > 90000) { // arbitrarily ignore <1s titles + char *time = mp_format_time(duration / 90000, false); + MP_VERBOSE(stream, "title: %3d duration: %s\n", + n - 1, time); + talloc_free(time); } + free(parts); } } p->track = best_title - 1; MP_INFO(stream, "Selecting title %d.\n", p->track); } + // demux_disc.c appends a synthetic "Disc Menu" edition at index num_titles. + if (p->track >= num_titles) + p->track = TITLE_MENU; + if (p->track >= 0) { priv->title = p->track; if (dvdnav_title_play(priv->dvdnav, p->track + 1) != DVDNAV_STATUS_OK) { @@ -635,9 +976,10 @@ static int open_s_internal(stream_t *stream) goto err; } } else { - MP_FATAL(stream, "DVD menu support has been removed.\n"); - ret = STREAM_ERROR; - goto err; + // Menu mode: don't pre-select any title; let dvdnav start with the + // disc's first-play / VMGM menu and drive everything via NAV events. + priv->title = 0; + dvdnav_menu_call(priv->dvdnav, DVD_MENU_Root); } if (p->opts->angle > 1) dvdnav_angle_change(priv->dvdnav, p->opts->angle); @@ -663,12 +1005,13 @@ static int open_s(stream_t *stream) bstr title, bdevice; bstr_split_tok(bstr0(stream->path), "/", &title, &bdevice); - priv->track = TITLE_LONGEST; - struct MPOpts *opts = mp_get_config_group(stream, stream->global, &mp_opt_root); int edition_id = opts->edition_id; + bool disc_menu = opts->disc_menu; talloc_free(opts); + priv->track = disc_menu ? TITLE_MENU : TITLE_LONGEST; + if (edition_id >= 0) { priv->track = edition_id; } else if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) { @@ -713,7 +1056,8 @@ static int ifo_dvdnav_stream_open(stream_t *stream) goto unsupported; struct MPOpts *opts = mp_get_config_group(NULL, stream->global, &mp_opt_root); - priv->track = opts->edition_id >= 0 ? opts->edition_id : TITLE_LONGEST; + priv->track = opts->edition_id >= 0 ? opts->edition_id : + (opts->disc_menu ? TITLE_MENU : TITLE_LONGEST); talloc_free(opts); char *path = mp_file_get_path(priv, bstr0(stream->url)); diff --git a/sub/dec_sub.h b/sub/dec_sub.h index d8f153968533e..b236aa9da1941 100644 --- a/sub/dec_sub.h +++ b/sub/dec_sub.h @@ -21,6 +21,14 @@ enum sd_ctrl { SD_CTRL_SET_VIDEO_DEF_FPS, SD_CTRL_RESET_SOFT, SD_CTRL_UPDATE_OPTS, + SD_CTRL_APPLY_DVDNAV, // struct mp_dvdnav_hli * +}; + +struct mp_dvdnav_hli { + bool show; + int x, y, w, h; // button rect in SPU/source coords + uint32_t palette[4]; // 0xAARRGGBB, straight alpha + uint32_t change_id; // bumped on any visible change }; enum sd_text_type { diff --git a/sub/sd_lavc.c b/sub/sd_lavc.c index 5cbc6d0a21257..eec2f1ef2c3ae 100644 --- a/sub/sd_lavc.c +++ b/sub/sd_lavc.c @@ -71,6 +71,10 @@ struct sd_lavc_priv { struct seekpoint *seekpoints; int num_seekpoints; struct bitmap_packer *packer; + + // DVD-nav per-pixel highlight overlay state. + struct mp_dvdnav_hli hli; + uint32_t hli_change_id; }; static int init(struct sd *sd) @@ -294,6 +298,23 @@ static void read_sub_bitmaps(struct sd *sd, struct sub *sub) memcpy(pal, data[1], r->nb_colors * 4); convert_pal(pal, 256, opts->sub_gray); + // DVD navigation highlight + bool hli_active = priv->hli.show && + priv->hli.w > 0 && priv->hli.h > 0 && + r->x < priv->hli.x + priv->hli.w && + r->y < priv->hli.y + priv->hli.h && + r->x + r->w > priv->hli.x && + r->y + r->h > priv->hli.y; + uint32_t hli_pal[4] = {0}; + if (hli_active) { + memcpy(hli_pal, priv->hli.palette, sizeof(hli_pal)); + convert_pal(hli_pal, 4, opts->sub_gray); + } + int hli_x0 = priv->hli.x - r->x; + int hli_y0 = priv->hli.y - r->y; + int hli_x1 = hli_x0 + priv->hli.w; + int hli_y1 = hli_y0 + priv->hli.h; + for (int y = -padding; y < b->h + padding; y++) { uint32_t *out = (uint32_t*)((char*)b->bitmap + y * b->stride); int start = 0; @@ -301,8 +322,14 @@ static void read_sub_bitmaps(struct sd *sd, struct sub *sub) out[x] = 0; if (y >= 0 && y < b->h) { uint8_t *in = data[0] + y * linesize[0]; - for (int x = 0; x < b->w; x++) - *out++ = pal[*in++]; + bool y_in_hli = hli_active && y >= hli_y0 && y < hli_y1; + for (int x = 0; x < b->w; x++) { + uint8_t pv = *in++; + if (y_in_hli && x >= hli_x0 && x < hli_x1 && pv < 4) + *out++ = hli_pal[pv]; + else + *out++ = pal[pv]; + } start = b->w; } for (int x = start; x < b->w + padding; x++) @@ -322,6 +349,21 @@ static void read_sub_bitmaps(struct sd *sd, struct sub *sub) } } +static void rerender_queued_subs(struct sd *sd) +{ + struct sd_lavc_priv *priv = sd->priv; + for (int n = 0; n < MAX_QUEUE; n++) { + struct sub *sub = &priv->subs[n]; + if (!sub->valid) + continue; + sub->count = 0; + sub->src_w = 0; + sub->src_h = 0; + sub->id = priv->new_id++; + read_sub_bitmaps(sd, sub); + } +} + static void decode(struct sd *sd, struct demux_packet *packet) { struct mp_subtitle_opts *opts = sd->opts; @@ -715,6 +757,16 @@ static int control(struct sd *sd, enum sd_ctrl cmd, void *arg) case SD_CTRL_SET_VIDEO_PARAMS: priv->video_params = *(struct mp_image_params *)arg; return CONTROL_OK; + case SD_CTRL_APPLY_DVDNAV: { + struct mp_dvdnav_hli *hli = arg; + if (priv->hli_change_id == hli->change_id) + return CONTROL_OK; + priv->hli = *hli; + priv->hli_change_id = hli->change_id; + // Re-render any decoded subtitles, after style update. + rerender_queued_subs(sd); + return CONTROL_OK; + } default: return CONTROL_UNKNOWN; } diff --git a/video/filter/vf_format.c b/video/filter/vf_format.c index ff64c6fce37b0..6b971902024b7 100644 --- a/video/filter/vf_format.c +++ b/video/filter/vf_format.c @@ -61,6 +61,7 @@ struct vf_format_opts { int force_scaler; bool dovi; bool hdr10plus; + bool enhancement_layer; float min_luma; float max_luma; float max_cll; @@ -185,6 +186,16 @@ static void vf_format_process(struct mp_filter *f) .clm = get_side_data(img, AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), .dhp = get_side_data(img, AV_FRAME_DATA_DYNAMIC_HDR_PLUS), }); + // Tell the f_enhancement_pair filter to not inherit DV metadata + // from the EL. + img->params.no_dovi = true; + } + + if (!priv->opts->enhancement_layer) { + // This is no-op, but just in case. f_enhancement_pair runs at the + // end of chain. + mp_image_unrefp(&img->enhancement_layer); + img->params.no_enhancement_layer = true; } if (!priv->opts->hdr10plus) { @@ -269,6 +280,7 @@ static const m_option_t vf_opts_fields[] = { {"dar", OPT_DOUBLE(dar)}, {"convert", OPT_BOOL(convert)}, {"dolbyvision", OPT_BOOL(dovi)}, + {"enhancement-layer", OPT_BOOL(enhancement_layer)}, {"hdr10plus", OPT_BOOL(hdr10plus)}, {"min-luma", OPT_FLOAT(min_luma), M_RANGE(0, 10000)}, {"max-luma", OPT_FLOAT(max_luma), M_RANGE(0, 10000)}, @@ -290,6 +302,7 @@ const struct mp_user_filter_entry vf_format = { .priv_defaults = &(const OPT_BASE_STRUCT){ .rotate = -1, .dovi = true, + .enhancement_layer = true, .hdr10plus = true, .film_grain = true, }, diff --git a/video/img_format.c b/video/img_format.c index 3dbd94fe62517..8d2b0d058b1e8 100644 --- a/video/img_format.c +++ b/video/img_format.c @@ -553,7 +553,7 @@ static bool get_native_desc(int mpfmt, struct mp_imgfmt_desc *desc) return true; } -int mp_imgfmt_desc_get_num_comps(struct mp_imgfmt_desc *desc) +int mp_imgfmt_desc_get_num_comps(const struct mp_imgfmt_desc *desc) { int flags = desc->flags; if (!(flags & MP_IMGFLAG_COLOR_MASK)) diff --git a/video/img_format.h b/video/img_format.h index 5f2044c944fd6..8cc7788d76db9 100644 --- a/video/img_format.h +++ b/video/img_format.h @@ -147,7 +147,7 @@ struct mp_imgfmt_desc { struct mp_imgfmt_desc mp_imgfmt_get_desc(int imgfmt); // Return the number of component types, or 0 if unknown. -int mp_imgfmt_desc_get_num_comps(struct mp_imgfmt_desc *desc); +int mp_imgfmt_desc_get_num_comps(const struct mp_imgfmt_desc *desc); // For MP_IMGFLAG_PACKED_SS_YUV formats (packed sub-sampled YUV): positions of // further luma samples. luma_offsets must be an array of align_x size, and the diff --git a/video/mp_image.c b/video/mp_image.c index 0c177d556ee6e..1ce05558070a4 100644 --- a/video/mp_image.c +++ b/video/mp_image.c @@ -237,6 +237,7 @@ static void mp_image_destructor(void *ptr) for (int n = 0; n < mpi->num_ff_side_data; n++) av_buffer_unref(&mpi->ff_side_data[n].buf); talloc_free(mpi->ff_side_data); + mp_image_unrefp(&mpi->enhancement_layer); } int mp_chroma_div_up(int size, int shift) @@ -374,6 +375,9 @@ struct mp_image *mp_image_new_ref(struct mp_image *img) for (int n = 0; n < new->num_ff_side_data; n++) ref_buffer(&new->ff_side_data[n].buf); + new->enhancement_layer = img->enhancement_layer + ? mp_image_new_ref(img->enhancement_layer) : NULL; + return new; } @@ -407,6 +411,7 @@ struct mp_image *mp_image_new_dummy_ref(struct mp_image *img) new->film_grain = NULL; new->num_ff_side_data = 0; new->ff_side_data = NULL; + new->enhancement_layer = NULL; return new; } @@ -556,6 +561,8 @@ void mp_image_copy_attributes(struct mp_image *dst, struct mp_image *src) dst->params.primaries_orig = src->params.primaries_orig; dst->params.transfer_orig = src->params.transfer_orig; dst->params.sys_orig = src->params.sys_orig; + dst->params.no_dovi = src->params.no_dovi; + dst->params.no_enhancement_layer = src->params.no_enhancement_layer; // ensure colorspace consistency enum pl_color_system dst_forced_csp = mp_image_params_get_forced_csp(&dst->params); @@ -587,6 +594,8 @@ void mp_image_copy_attributes(struct mp_image *dst, struct mp_image *src) dst->ff_side_data[n].buf = av_buffer_ref(src->ff_side_data[n].buf); MP_HANDLE_OOM(dst->ff_side_data[n].buf); } + + mp_image_setrefp(&dst->enhancement_layer, src->enhancement_layer); } // Crop the given image to (x0, y0)-(x1, y1) (bottom/right border exclusive) @@ -1125,6 +1134,8 @@ struct mp_image *mp_image_from_av_frame(struct AVFrame *src) dst->params.stereo3d = p->stereo3d; // Might be incorrect if colorspace changes. dst->params.light = p->light; + dst->params.no_dovi = p->no_dovi; + dst->params.no_enhancement_layer = p->no_enhancement_layer; #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(60, 11, 100) dst->params.repr.alpha = p->repr.alpha; #endif @@ -1174,12 +1185,13 @@ struct mp_image *mp_image_from_av_frame(struct AVFrame *src) if (sd) { #ifdef PL_HAVE_LAV_DOLBY_VISION const AVDOVIMetadata *metadata = (const AVDOVIMetadata *)sd->buf->data; -#if PL_API_VER >= 364 - if (pl_avdovi_metadata_supported(metadata)) { -#else +#if PL_API_VER < 364 const AVDOVIRpuDataHeader *header = av_dovi_get_header(metadata); - if (header->disable_residual_flag) { + if (header->disable_residual_flag) +#elif PL_API_VER < 370 + if (pl_avdovi_metadata_supported(metadata)) #endif + { dst->dovi = dovi = av_buffer_alloc(sizeof(struct pl_dovi_metadata)); MP_HANDLE_OOM(dovi); pl_map_avdovi_metadata(&dst->params.color, &dst->params.repr, diff --git a/video/mp_image.h b/video/mp_image.h index 5fe523dd64e96..f5c1562815a21 100644 --- a/video/mp_image.h +++ b/video/mp_image.h @@ -63,6 +63,9 @@ struct mp_image_params { int rotate; enum mp_stereo3d_mode stereo3d; // image is encoded with this mode struct mp_rect crop; // crop applied on image + // Flags for f_enhancement_pair.c to not inherit flags from EL. + bool no_dovi; + bool no_enhancement_layer; }; /* Memory management: @@ -126,6 +129,8 @@ typedef struct mp_image { // Other side data we don't care about. struct mp_ff_side_data *ff_side_data; int num_ff_side_data; + // Optional decoded enhancement-layer frame + struct mp_image *enhancement_layer; } mp_image_t; struct mp_ff_side_data { diff --git a/video/out/vo.h b/video/out/vo.h index a98b0f9496853..8e397618eb911 100644 --- a/video/out/vo.h +++ b/video/out/vo.h @@ -167,7 +167,7 @@ struct mp_pass_perf { }; #define VO_PASS_PERF_MAX 64 -#define VO_PASS_DESC_MAX_LEN 128 +#define VO_PASS_DESC_MAX_LEN 256 struct mp_frame_perf { int count; diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c index 451c25e576ce8..6875c352b5568 100644 --- a/video/out/vo_gpu_next.c +++ b/video/out/vo_gpu_next.c @@ -114,6 +114,8 @@ struct priv { struct ra_hwdec_mapper *hwdec_mapper; struct timer_pool *hwdec_timer; struct mp_pass_perf hwdec_perf; + struct ra_hwdec_mapper *el_hwdec_mapper; + struct timer_pool *el_hwdec_timer; struct timer_pool *sw_upload_timer; struct mp_pass_perf sw_upload_perf; @@ -466,6 +468,10 @@ struct frame_priv { struct osd_state subs; uint64_t osd_sync; struct ra_hwdec *hwdec; + // Optional Dolby Vision FEL. + struct ra_hwdec *el_hwdec; + pl_tex el_tex[4]; + struct pl_frame el_frame; }; static int plane_data_from_imgfmt(struct pl_plane_data out_data[4], @@ -561,38 +567,39 @@ static int plane_data_from_imgfmt(struct pl_plane_data out_data[4], return desc.num_planes; } -static bool hwdec_reconfig(struct priv *p, struct ra_hwdec *hwdec, +static bool hwdec_reconfig(struct priv *p, struct ra_hwdec_mapper **mapper, + struct timer_pool **timer, struct ra_hwdec *hwdec, const struct mp_image_params *par) { - if (p->hwdec_mapper) { - if (mp_image_params_static_equal(par, &p->hwdec_mapper->src_params)) { - p->hwdec_mapper->src_params.repr.dovi = par->repr.dovi; - p->hwdec_mapper->dst_params.repr.dovi = par->repr.dovi; - p->hwdec_mapper->src_params.color.hdr = par->color.hdr; - p->hwdec_mapper->dst_params.color.hdr = par->color.hdr; - return p->hwdec_mapper; + if (*mapper) { + if (mp_image_params_static_equal(par, &(*mapper)->src_params)) { + (*mapper)->src_params.repr.dovi = par->repr.dovi; + (*mapper)->dst_params.repr.dovi = par->repr.dovi; + (*mapper)->src_params.color.hdr = par->color.hdr; + (*mapper)->dst_params.color.hdr = par->color.hdr; + return true; } else { - ra_hwdec_mapper_free(&p->hwdec_mapper); - timer_pool_destroy(p->hwdec_timer); - p->hwdec_timer = NULL; + ra_hwdec_mapper_free(mapper); + timer_pool_destroy(*timer); + *timer = NULL; } } - p->hwdec_mapper = ra_hwdec_mapper_create(hwdec, par); - if (!p->hwdec_mapper) { + *mapper = ra_hwdec_mapper_create(hwdec, par); + if (!*mapper) { MP_ERR(p, "Initializing texture for hardware decoding failed.\n"); - return NULL; + return false; } - p->hwdec_timer = timer_pool_create(p->ra_ctx->ra); + *timer = timer_pool_create(p->ra_ctx->ra); - return p->hwdec_mapper; + return true; } -// For RAs not based on ra_pl, this creates a new pl_tex wrapper -static pl_tex hwdec_get_tex(struct priv *p, int n) +// For RAs not based on ra_pl, this creates a new pl_tex wrapper. +static pl_tex hwdec_get_tex(struct priv *p, struct ra_hwdec_mapper *mapper, int n) { - struct ra_tex *ratex = p->hwdec_mapper->tex[n]; - struct ra *ra = p->hwdec_mapper->ra; + struct ra_tex *ratex = mapper->tex[n]; + struct ra *ra = mapper->ra; if (ra_pl_get(ra)) return (pl_tex) ratex->priv; @@ -630,12 +637,37 @@ static pl_tex hwdec_get_tex(struct priv *p, int n) return false; } +// Fill `frame->num_planes` and per-plane component_mapping from an +// hwdec-mapped imgfmt description. +static void setup_hwdec_plane_mapping(struct pl_frame *frame, + const struct mp_imgfmt_desc *desc) +{ + frame->num_planes = desc->num_planes; + for (int n = 0; n < frame->num_planes; n++) { + struct pl_plane *plane = &frame->planes[n]; + int *map = plane->component_mapping; + for (int c = 0; c < mp_imgfmt_desc_get_num_comps(desc); c++) { + if (desc->comps[c].plane != n) + continue; + // Sort by component offset + uint8_t offset = desc->comps[c].offset; + int index = plane->components++; + while (index > 0 && desc->comps[map[index - 1]].offset > offset) { + map[index] = map[index - 1]; + index--; + } + map[index] = c; + } + } +} + static bool hwdec_acquire(pl_gpu gpu, struct pl_frame *frame) { struct mp_image *mpi = frame->user_data; struct frame_priv *fp = mpi->priv; struct priv *p = fp->vo->priv; - if (!hwdec_reconfig(p, fp->hwdec, &mpi->params)) + if (!hwdec_reconfig(p, &p->hwdec_mapper, &p->hwdec_timer, fp->hwdec, + &mpi->params)) return false; stats_time_start(p->stats, "hwdec-map"); @@ -648,7 +680,7 @@ static bool hwdec_acquire(pl_gpu gpu, struct pl_frame *frame) } for (int n = 0; n < frame->num_planes; n++) { - if (!(frame->planes[n].texture = hwdec_get_tex(p, n))) { + if (!(frame->planes[n].texture = hwdec_get_tex(p, p->hwdec_mapper, n))) { timer_pool_stop(p->hwdec_timer); stats_time_end(p->stats, "hwdec-map"); return false; @@ -675,6 +707,45 @@ static void hwdec_release(pl_gpu gpu, struct pl_frame *frame) ra_hwdec_mapper_unmap(p->hwdec_mapper); } +#if PL_API_VER >= 367 +static bool hwdec_acquire_el(pl_gpu gpu, struct pl_frame *frame) +{ + struct mp_image *bl_mpi = frame->user_data; + struct mp_image *el_mpi = bl_mpi->enhancement_layer; + struct frame_priv *fp = bl_mpi->priv; + struct priv *p = fp->vo->priv; + if (!hwdec_reconfig(p, &p->el_hwdec_mapper, &p->el_hwdec_timer, + fp->el_hwdec, &el_mpi->params)) + return false; + + if (ra_hwdec_mapper_map(p->el_hwdec_mapper, el_mpi) < 0) { + MP_ERR(p, "Mapping enhancement-layer hwdec surface failed.\n"); + return false; + } + + for (int n = 0; n < frame->num_planes; n++) { + if (!(frame->planes[n].texture = + hwdec_get_tex(p, p->el_hwdec_mapper, n))) + return false; + } + + return true; +} + +static void hwdec_release_el(pl_gpu gpu, struct pl_frame *frame) +{ + struct mp_image *bl_mpi = frame->user_data; + struct frame_priv *fp = bl_mpi->priv; + struct priv *p = fp->vo->priv; + if (!ra_pl_get(p->el_hwdec_mapper->ra)) { + for (int n = 0; n < frame->num_planes; n++) + pl_tex_destroy(p->gpu, &frame->planes[n].texture); + } + + ra_hwdec_mapper_unmap(p->el_hwdec_mapper); +} +#endif + static bool format_supported(struct vo *vo, int format, bool use_uint) { struct priv *p = vo->priv; @@ -692,6 +763,59 @@ static bool format_supported(struct vo *vo, int format, bool use_uint) return true; } +static bool upload_planes_sw(struct vo *vo, pl_gpu gpu, struct mp_image *mpi, + struct pl_frame *frame, pl_tex tex[4]) +{ + struct priv *p = vo->priv; + struct pl_plane_data data[4] = {0}; + + // At this point, we know that the format is supported, query_format() + // makes sure of that. Just check if we should use UINT as a fallback. + bool use_uint = !format_supported(vo, mpi->imgfmt, false); + int planes = plane_data_from_imgfmt(data, &frame->repr.bits, mpi->imgfmt, + use_uint); + if (!planes) + return false; + + frame->num_planes = planes; + for (int n = 0; n < planes; n++) { + struct pl_plane *plane = &frame->planes[n]; + data[n].width = mp_image_plane_w(mpi, n); + data[n].height = mp_image_plane_h(mpi, n); + if (mpi->stride[n] < 0) { + data[n].pixels = mpi->planes[n] + (data[n].height - 1) * mpi->stride[n]; + data[n].row_stride = -mpi->stride[n]; + plane->flipped = true; + } else { + data[n].pixels = mpi->planes[n]; + data[n].row_stride = mpi->stride[n]; + } + + pl_buf buf = get_dr_buf(p, data[n].pixels); + if (buf) { + data[n].buf = buf; + data[n].buf_offset = (uint8_t *) data[n].pixels - buf->data; + data[n].pixels = NULL; + } + // Keep the image alive until it's fully read. + if (gpu->limits.callbacks) { + data[n].callback = talloc_free; + data[n].priv = mp_image_new_ref(mpi); + } + + if (!pl_upload_plane(gpu, plane, &tex[n], &data[n])) { + talloc_free(data[n].priv); + return false; + } + + // Without async callback support, we have to poll... + if (!gpu->limits.callbacks && data[n].buf) + while (pl_buf_poll(gpu, data[n].buf, UINT64_MAX)); + } + + return true; +} + static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src, struct pl_frame *frame) { @@ -707,7 +831,8 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src // only reconfig the mapper here (potentially creating it) to access // `dst_params`. In practice, though, this should not matter unless the // image format changes mid-stream. - if (!hwdec_reconfig(p, fp->hwdec, &mpi->params)) { + if (!hwdec_reconfig(p, &p->hwdec_mapper, &p->hwdec_timer, fp->hwdec, + &mpi->params)) { talloc_free(mpi); return false; } @@ -745,90 +870,71 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(par.imgfmt); frame->acquire = hwdec_acquire; frame->release = hwdec_release; - frame->num_planes = desc.num_planes; - for (int n = 0; n < frame->num_planes; n++) { - struct pl_plane *plane = &frame->planes[n]; - int *map = plane->component_mapping; - for (int c = 0; c < mp_imgfmt_desc_get_num_comps(&desc); c++) { - if (desc.comps[c].plane != n) - continue; - - // Sort by component offset - uint8_t offset = desc.comps[c].offset; - int index = plane->components++; - while (index > 0 && desc.comps[map[index - 1]].offset > offset) { - map[index] = map[index - 1]; - index--; - } - map[index] = c; - } - } - + setup_hwdec_plane_mapping(frame, &desc); } else { // swdec p->hwdec_perf.count = 0; if (!p->sw_upload_timer) p->sw_upload_timer = timer_pool_create(p->ra_ctx->ra); - struct pl_plane_data data[4] = {0}; - bool use_uint = false; - - // At this point, we know that the format is supported, query_format() - // makes sure of that. Just check if we should use UINT as a fallback. - if (!format_supported(vo, mpi->imgfmt, false)) - use_uint = true; - - frame->num_planes = plane_data_from_imgfmt(data, &frame->repr.bits, mpi->imgfmt, use_uint); stats_time_start(p->stats, "swdec-upload"); timer_pool_start(p->sw_upload_timer); - for (int n = 0; n < frame->num_planes; n++) { - struct pl_plane *plane = &frame->planes[n]; - data[n].width = mp_image_plane_w(mpi, n); - data[n].height = mp_image_plane_h(mpi, n); - if (mpi->stride[n] < 0) { - data[n].pixels = mpi->planes[n] + (data[n].height - 1) * mpi->stride[n]; - data[n].row_stride = -mpi->stride[n]; - plane->flipped = true; - } else { - data[n].pixels = mpi->planes[n]; - data[n].row_stride = mpi->stride[n]; - } + bool ok = upload_planes_sw(vo, gpu, mpi, frame, tex); + timer_pool_stop(p->sw_upload_timer); + stats_time_end(p->stats, "swdec-upload"); + if (!ok) { + MP_ERR(vo, "Failed uploading frame!\n"); + talloc_free(mpi); + return false; + } + p->sw_upload_perf = timer_pool_measure(p->sw_upload_timer); + } - pl_buf buf = get_dr_buf(p, data[n].pixels); - if (buf) { - data[n].buf = buf; - data[n].buf_offset = (uint8_t *) data[n].pixels - buf->data; - data[n].pixels = NULL; - } - // Keep the image alive until it's fully read. - if (gpu->limits.callbacks) { - mp_assert(!data[n].callback); - data[n].callback = talloc_free; - mp_assert(!data[n].priv); - data[n].priv = mp_image_new_ref(mpi); - } + // Update chroma location, must be done after initializing planes + pl_frame_set_chroma_location(frame, par.chroma_location); - if (!pl_upload_plane(gpu, plane, &tex[n], &data[n])) { - MP_ERR(vo, "Failed uploading frame!\n"); - timer_pool_stop(p->sw_upload_timer); - stats_time_end(p->stats, "swdec-upload"); - talloc_free(data[n].priv); - talloc_free(mpi); - return false; +#if PL_API_VER >= 367 + if (mpi->enhancement_layer) { + struct mp_image *el = mpi->enhancement_layer; + fp->el_hwdec = ra_hwdec_get(&p->hwdec_ctx, el->imgfmt); + + struct mp_image_params el_par = el->params; + bool el_ok = true; + if (fp->el_hwdec) { + if (hwdec_reconfig(p, &p->el_hwdec_mapper, &p->el_hwdec_timer, + fp->el_hwdec, &el->params)) { + el_par = p->el_hwdec_mapper->dst_params; + } else { + fp->el_hwdec = NULL; + el_ok = false; } + } + mp_image_params_guess_csp(&el_par); + + fp->el_frame = (struct pl_frame) { + .color = el_par.color, + .repr = el_par.repr, + .user_data = mpi, // BL mpi + }; - // Without async callback support, we have to poll... - if (!gpu->limits.callbacks && data[n].buf) - while (pl_buf_poll(gpu, data[n].buf, UINT64_MAX)); + if (el_ok && fp->el_hwdec) { + struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(el_par.imgfmt); + fp->el_frame.acquire = hwdec_acquire_el; + fp->el_frame.release = hwdec_release_el; + setup_hwdec_plane_mapping(&fp->el_frame, &desc); + } else if (el_ok) { + el_ok = upload_planes_sw(vo, gpu, el, &fp->el_frame, fp->el_tex); } - timer_pool_stop(p->sw_upload_timer); - p->sw_upload_perf = timer_pool_measure(p->sw_upload_timer); - stats_time_end(p->stats, "swdec-upload"); + if (el_ok) { + pl_frame_set_chroma_location(&fp->el_frame, el_par.chroma_location); + frame->enhancement_layer = &fp->el_frame; + } else { + MP_WARN(vo, "Failed setting up enhancement layer; " + "rendering base layer only.\n"); + } } - - // Update chroma location, must be done after initializing planes - pl_frame_set_chroma_location(frame, par.chroma_location); +#endif if (mpi->film_grain) pl_film_grain_from_av(&frame->film_grain, (AVFilmGrainParams *) mpi->film_grain->data); @@ -857,6 +963,10 @@ static void unmap_frame(pl_gpu gpu, struct pl_frame *frame, if (tex) MP_TARRAY_APPEND(p, p->sub_tex, p->num_sub_tex, tex); } + for (int i = 0; i < MP_ARRAY_SIZE(fp->el_tex); i++) { + if (fp->el_tex[i]) + pl_tex_destroy(gpu, &fp->el_tex[i]); + } talloc_free(mpi); } @@ -2188,6 +2298,8 @@ static void uninit(struct vo *vo) if (vo->hwdec_devs) { ra_hwdec_mapper_free(&p->hwdec_mapper); timer_pool_destroy(p->hwdec_timer); + ra_hwdec_mapper_free(&p->el_hwdec_mapper); + timer_pool_destroy(p->el_hwdec_timer); ra_hwdec_ctx_uninit(&p->hwdec_ctx); hwdec_devices_set_loader(vo->hwdec_devs, NULL, NULL); hwdec_devices_destroy(vo->hwdec_devs);