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/dvda.txt b/DOCS/interface-changes/dvda.txt new file mode 100644 index 0000000000000..4224a2e0f927b --- /dev/null +++ b/DOCS/interface-changes/dvda.txt @@ -0,0 +1,2 @@ +add `dvda://` protocol for DVD-Audio playback (requires libdvdread >= 7.1.0) +add `--dvda-device` option diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 2de04ff287d73..6758a1009f465 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/mpv.rst b/DOCS/man/mpv.rst index 58b5321b07e7a..9a3afc9a8dc83 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -1336,6 +1336,13 @@ PROTOCOLS ``dvdnav://`` is an old alias for ``dvd://`` and does exactly the same thing. +``dvda://[title][/device]`` ``--dvda-device=PATH`` + + Play the AUDIO_TS zone of a DVD-Audio disc. Titles correspond to the + disc's audio groups, tracks are exposed as chapters. If no title is + given, the longest title is auto-selected. Menus and still images are + not supported. + ``dvb://[cardnumber@]channel`` ``--dvbin-...`` Digital TV via DVB. (Linux only.) diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 0febce1bf86da..84ae8702840af 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 @@ -3888,6 +3894,16 @@ Disc Devices ``mpv dvd:// --dvd-device=/path/to/dvd/`` +``--dvda-device=`` + Specify the DVD-Audio device or .iso filename for ``dvda://`` playback. + You can also specify a directory that contains files previously copied + directly from a DVD-Audio disc. The default device path depends on + the OS. See the `OPTICAL DRIVES`_ section. + + .. admonition:: Example + + ``mpv dvda:// --dvda-device=/path/to/dvda/`` + ``--bluray-device=`` Specify the Blu-ray disc location. Must be a directory with Blu-ray structure. The default device path depends on the OS. See the diff --git a/demux/demux.c b/demux/demux.c index b63038b6c4c41..ba1a95d564bf1 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -209,6 +209,12 @@ 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; + // Set while interactive disc navigation (DVD/BD menus) is active. + bool nav_active; + // One-shot request to force a single read even when nothing is being + // consumed, so a disc-nav command can drive the VM forward. Mostly used in + // paused state. + bool nav_pump; size_t max_bytes; size_t max_bytes_bw; bool seekable_cache; @@ -241,6 +247,7 @@ struct demux_internal { bool back_any_need_recheck; // at least 1 ds->back_need_recheck set bool tracks_switched; // thread needs to inform demuxer of this + bool nav_refresh; // thread needs to call desc->nav_refresh bool seeking; // there's a seek queued int seek_flags; // flags for next seek (if seeking==true) @@ -900,13 +907,13 @@ static void update_stream_selection_state(struct demux_internal *in, any_streams |= s->selected; } - // Subtitles are only eagerly read if there are no other eagerly read - // streams. + // Subtitles and sparse still-image video are only eagerly read if there are + // no other eagerly read streams. if (any_av_streams) { for (int n = 0; n < in->num_streams; n++) { struct demux_stream *s = in->streams[n]->ds; - if (s->type == STREAM_SUB) + if (s->type == STREAM_SUB || s->still_image) s->eager = false; } } @@ -1246,6 +1253,33 @@ 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 = true; + in->reading = true; + mp_cond_signal(&in->wakeup); + mp_mutex_unlock(&in->lock); +} + +// Re-queue sticky packets (e.g. the DVD menu subpicture) for re-delivery. +void demux_nav_refresh(struct demuxer *demuxer) +{ + struct demux_internal *in = demuxer->in; + mp_assert(demuxer == in->d_user); + + mp_mutex_lock(&in->lock); + if (in->d_thread->desc->nav_refresh) { + in->nav_refresh = true; + in->reading = true; + mp_cond_signal(&in->wakeup); + } + mp_mutex_unlock(&in->lock); +} + const char *stream_type_name(enum stream_type type) { switch (type) { @@ -2301,6 +2335,12 @@ static bool read_packet(struct demux_internal *in) prefetch_more |= true; } + // While interactive disc navigation is active, never read ahead. It would + // advance the disc VM past what the user is watching. + if (in->nav_active) + prefetch_more = false; + prefetch_more |= in->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) { @@ -2354,6 +2394,7 @@ static bool read_packet(struct demux_internal *in) // Actually read a packet. Drop the lock while doing so, because waiting // for disk or network I/O can take time. in->reading = true; + in->nav_pump = false; in->after_seek = false; in->after_seek_to_start = false; mp_mutex_unlock(&in->lock); @@ -2518,6 +2559,18 @@ static void execute_trackswitch(struct demux_internal *in) mp_mutex_lock(&in->lock); } +static void execute_nav_refresh(struct demux_internal *in) +{ + in->nav_refresh = false; + + mp_mutex_unlock(&in->lock); + + if (in->d_thread->desc->nav_refresh) + in->d_thread->desc->nav_refresh(in->d_thread); + + mp_mutex_lock(&in->lock); +} + static void execute_seek(struct demux_internal *in) { int flags = in->seek_flags; @@ -2634,6 +2687,10 @@ static bool thread_work(struct demux_internal *in) execute_trackswitch(in); return true; } + if (in->nav_refresh) { + execute_nav_refresh(in); + return true; + } if (in->need_back_seek) { perform_backward_seek(in); return true; @@ -3072,7 +3129,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); } } } @@ -3178,6 +3235,55 @@ 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); +} + +// Tell the cache whether interactive disc navigation is active. +void demux_set_nav_active(demuxer_t *demuxer, bool active) +{ + mp_assert(demuxer == demuxer->in->d_thread); + struct demux_internal *in = demuxer->in; + + mp_mutex_lock(&in->lock); + if (in->nav_active != active) { + in->nav_active = active; + mp_cond_signal(&in->wakeup); + } + 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) { @@ -4248,6 +4354,7 @@ int demuxer_add_chapter(demuxer_t *demuxer, char *name, }; mp_tags_set_str(new.metadata, "TITLE", name); MP_TARRAY_APPEND(demuxer, demuxer->chapters, demuxer->num_chapters, new); + talloc_steal(demuxer->chapters, new.metadata); return demuxer->num_chapters - 1; } diff --git a/demux/demux.h b/demux/demux.h index 6fabf64b9940e..ca6ad70464ff9 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, }; @@ -146,6 +147,8 @@ typedef struct demuxer_desc { // will be repeated. bool (*read_packet)(struct demuxer *demuxer, struct demux_packet **pkt); void (*drop_buffers)(struct demuxer *demuxer); + // Optional. Re-queue retained sticky packets (DVD menu subpicture). + void (*nav_refresh)(struct demuxer *demuxer); void (*close)(struct demuxer *demuxer); void (*seek)(struct demuxer *demuxer, double rel_seek_secs, int flags); void (*switched_tracks)(struct demuxer *demuxer); @@ -320,6 +323,8 @@ 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); +void demux_nav_refresh(struct demuxer *demuxer); bool demux_cancel_test(struct demuxer *demuxer); @@ -348,6 +353,9 @@ 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_set_nav_active(demuxer_t *demuxer, bool active); +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 86557a711c2da..4980f08783c31 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -25,43 +25,96 @@ #include "stream/stream.h" #include "video/mp_image.h" #include "demux.h" +#include "packet.h" #include "stheader.h" #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]; + + // DVD-only: retain the last SPU packet per substream. The menu subpicture + // is one-shot on an unseekable stream; re-deliver it on (re)selection and + // demux_nav_refresh(), like a refresh seek for ordinary streams. + struct dvd_sub_hold { + struct demux_packet *pkt; // clone, with playback-rebased timestamps + struct sh_stream *sh; // outer stream it belongs to + bool pending; // re-deliver on next read + } dvd_sub_hold[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 nav_active; // last interactive-nav state pushed to the cache 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 clear_dvd_sub_holds(struct priv *p) +{ + for (int i = 0; i < MAX_DVD_SPU_STREAMS; i++) { + talloc_free(p->dvd_sub_hold[i].pkt); + p->dvd_sub_hold[i] = (struct dvd_sub_hold){0}; + } +} + +// Mark retained subpictures of selected streams for re-delivery. +static void arm_dvd_sub_holds(struct priv *p) +{ + for (int i = 0; i < MAX_DVD_SPU_STREAMS; i++) { + struct dvd_sub_hold *h = &p->dvd_sub_hold[i]; + if (h->pkt && h->sh && demux_stream_is_selected(h->sh)) + h->pending = true; + } +} 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)); } } + arm_dvd_sub_holds(p); +} + +static void d_nav_refresh(demuxer_t *demuxer) +{ + struct priv *p = demuxer->priv; + arm_dvd_sub_holds(p); } static void get_disc_lang(struct stream *stream, struct sh_stream *sh, bool dvd) @@ -82,15 +135,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,71 +171,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 old_num = p->num_streams; + int num_slave = demux_get_num_stream(p->slave); - for (int n = old_num; n < demux_get_num_stream(p->slave); n++) { + 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 = 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; - sh->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); - 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; } - // Mirror slave sh_stream_group onto the disc-level sh_streams. This is needed + // 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 = old_num; n < p->num_streams; n++) { - struct sh_stream *disc_sh = p->streams[n]; - if (!disc_sh || disc_sh->group) + 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(disc_sh, struct sh_stream_group); + 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 *sh = src->group->members[m]; - if (!sh || sh->index < 0 || sh->index >= p->num_streams) + struct sh_stream *member = src->group->members[m]; + if (!member || member->index < 0 || + member->index >= p->slave_to_outer_count) continue; - struct sh_stream *disc_member = p->streams[sh->index]; - if (!disc_member) + struct sh_stream *outer_member = p->slave_to_outer[member->index]; + if (!outer_member) continue; - MP_TARRAY_APPEND(grp, grp->members, grp->num_members, disc_member); - disc_member->group = grp; + 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) @@ -210,6 +320,8 @@ static void d_seek(demuxer_t *demuxer, double seek_pts, int flags) if (p->slave->desc->drop_buffers) p->slave->desc->drop_buffers(p->slave); + clear_dvd_sub_holds(p); + p->seek_reinit = true; } @@ -228,31 +340,190 @@ 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 void refresh_disc_metadata(struct demuxer *demuxer) +{ + 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); + + // Old chapter metadata is not freed here, they are parented to demuxer. + demuxer->chapters = NULL; + demuxer->num_chapters = 0; + add_stream_chapters(demuxer); + sync_initial_edition(demuxer); + demux_lists_changed(demuxer); +} + +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); + clear_dvd_sub_holds(p); + // 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_disc_metadata(demuxer); + + return true; +} + +// Handle a disc-nav discontinuity (title/menu/cell jump) +static bool process_discontinuity(struct demuxer *demuxer, uint32_t new_id) +{ + struct priv *p = demuxer->priv; + + if (!p->is_dvd) { + // BD needs a full reopen (codecs can change across titles). + if (!reopen_slave(demuxer)) + return false; + } else { + clear_dvd_sub_holds(p); + if (p->slave->desc->drop_buffers) + p->slave->desc->drop_buffers(p->slave); + refresh_disc_metadata(demuxer); + } + p->last_discontinuity_id = new_id; + p->seek_reinit = true; + return true; +} + static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt) { struct priv *p = demuxer->priv; + bool menu_active = false; + struct stream_nav_state nav = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) { + menu_active = nav.menu_active; + if (nav.nav_active != p->nav_active) { + p->nav_active = nav.nav_active; + demux_set_nav_active(demuxer, nav.nav_active); + } + if (!p->is_dvd && nav.discontinuity_id != p->last_discontinuity_id) { + MP_VERBOSE(demuxer, "discontinuity %u->%u, handling\n", + p->last_discontinuity_id, nav.discontinuity_id); + if (!process_discontinuity(demuxer, nav.discontinuity_id)) + return false; + } + } + + // Re-deliver a retained menu subpicture. + if (menu_active) { + for (int i = 0; i < MAX_DVD_SPU_STREAMS; i++) { + struct dvd_sub_hold *h = &p->dvd_sub_hold[i]; + if (!h->pending || !h->pkt || !h->sh || + !demux_stream_is_selected(h->sh)) + continue; + h->pending = false; + struct demux_packet *rp = demux_copy_packet(demuxer->packet_pool, + h->pkt); + if (!rp) + continue; + rp->stream = h->sh->index; + *out_pkt = rp; + return true; + } + } + struct demux_packet *pkt = demux_read_any_packet(p->slave); - if (!pkt) - return false; + if (!pkt) { + // EOF is either a real still (hold the frame) or the one-shot drain + // EOF at a jump boundary; in the latter case resync and continue. + 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, handling\n", + p->last_discontinuity_id, nav2.discontinuity_id); + if (!process_discontinuity(demuxer, nav2.discontinuity_id)) + return false; + pkt = demux_read_any_packet(p->slave); + } + if (!pkt) + return false; + } demux_update(p->slave, MP_NOPTS_VALUE); - if (p->seek_reinit) + if (p->seek_reinit) { reset_pts(demuxer); + double len; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_TIME_LENGTH, &len) >= 1) + demux_set_duration(demuxer, len); + } - 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; + bool dvd_sub = sh && p->is_dvd && sh->type == STREAM_SUB; + if (!sh || (!demux_stream_is_selected(sh) && !dvd_sub)) { 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) { @@ -275,7 +546,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; if (pkt->duration > 0) @@ -294,6 +565,23 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt MP_TRACE(demuxer, "opts: %d %f %f\n", sh->type, pkt->pts, pkt->dts); + if (dvd_sub) { + int idx = sh->demuxer_id - 0x20; + if (idx >= 0 && idx < MAX_DVD_SPU_STREAMS) { + struct dvd_sub_hold *h = &p->dvd_sub_hold[idx]; + talloc_free(h->pkt); + h->pkt = demux_copy_packet(demuxer->packet_pool, pkt); + if (h->pkt) + talloc_steal(p, h->pkt); + h->sh = sh; + h->pending = false; + } + if (!demux_stream_is_selected(sh)) { + talloc_free(pkt); + return true; + } + } + *out_pkt = pkt; return 1; } @@ -323,6 +611,18 @@ 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, if the disc has menu support. + struct stream_nav_state nav = {0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) < 1) + return; + 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) @@ -382,9 +682,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); @@ -392,9 +694,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; } @@ -402,6 +702,7 @@ static int d_open(demuxer_t *demuxer, enum demux_check check) static void d_close(demuxer_t *demuxer) { struct priv *p = demuxer->priv; + clear_dvd_sub_holds(p); demux_free(p->slave); } @@ -413,4 +714,5 @@ const demuxer_desc_t demuxer_desc_disc = { .close = d_close, .seek = d_seek, .switched_tracks = reselect_streams, + .nav_refresh = d_nav_refresh, }; diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index a5d7431068284..b878fad42f6db 100644 --- a/demux/demux_lavf.c +++ b/demux/demux_lavf.c @@ -67,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 { int64_t probesize; @@ -233,6 +238,7 @@ 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; @@ -641,7 +647,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; @@ -719,7 +725,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) @@ -759,7 +765,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)) { @@ -796,7 +802,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; } @@ -838,9 +844,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); @@ -881,6 +889,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 @@ -1639,8 +1653,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; @@ -1797,11 +1812,17 @@ static void reset_dovi_split_state(demuxer_t *demuxer) static void demux_drop_buffers_lavf(demuxer_t *demuxer) { lavf_priv_t *priv = demuxer->priv; - av_seek_frame(priv->avfc, -1, 0, 1); + if (!priv->stream || priv->stream->seekable) + av_seek_frame(priv->avfc, -1, 0, 1); demux_flush(demuxer); stream_drop_buffers(priv->stream); avio_flush(priv->avfc->pb); avformat_flush(priv->avfc); + // Clear sticky EOF/error to reuse this demuxer. + if (priv->avfc->pb) { + priv->avfc->pb->eof_reached = 0; + priv->avfc->pb->error = 0; + } reset_dovi_split_state(demuxer); } @@ -1918,8 +1939,6 @@ 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); diff --git a/demux/dovi_split.c b/demux/dovi_split.c index 2476fde7101b1..cfe27185da703 100644 --- a/demux/dovi_split.c +++ b/demux/dovi_split.c @@ -99,7 +99,7 @@ struct mp_dovi_split *mp_dovi_split_create(struct demuxer *demuxer, 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, par_out->extradata, + el->codec->extradata = talloc_memdup(el->codec, par_out->extradata, par_out->extradata_size); el->codec->extradata_size = par_out->extradata_size; } diff --git a/etc/input.conf b/etc/input.conf index 9519bdbe03f91..aa586b2161c6f 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/meson.build b/meson.build index 80bd39d7ec3bf..b3ca896aedfbe 100644 --- a/meson.build +++ b/meson.build @@ -170,6 +170,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', @@ -659,6 +660,17 @@ if features['dvbin'] 'stream/stream_dvb.c') endif +dvda_opt = get_option('dvda').require( + get_option('gpl'), + error_message: 'the build is not GPL!', +) +dvdread = dependency('dvdread', version: '>= 7.1.0', required: dvda_opt) +features += {'dvda': dvdread.found()} +if features['dvda'] + dependencies += dvdread + sources += files('stream/stream_dvda.c') +endif + dvdnav_opt = get_option('dvdnav').require( get_option('gpl'), error_message: 'the build is not GPL!', diff --git a/meson.options b/meson.options index 35e174cebaa94..416ee3b5edf09 100644 --- a/meson.options +++ b/meson.options @@ -11,6 +11,7 @@ option('disable-packet-pool', type: 'boolean', value: false, description: 'disab option('cdda', type: 'feature', value: 'auto', description: 'cdda support (libcdio)') option('cplugins', type: 'feature', value: 'auto', description: 'C plugins') option('dvbin', type: 'feature', value: 'auto', description: 'DVB input module') +option('dvda', type: 'feature', value: 'auto', description: 'DVD-Audio support (libdvdread)') option('dvdnav', type: 'feature', value: 'auto', description: 'dvdnav support') option('iconv', type: 'feature', value: 'auto', description: 'iconv') option('javascript', type: 'feature', value: 'auto', description: 'Javascript (MuJS backend)') diff --git a/options/options.c b/options/options.c index bab7e324ca635..093443e0ab4ee 100644 --- a/options/options.c +++ b/options/options.c @@ -94,6 +94,7 @@ extern const struct m_obj_list vo_obj_list; extern const struct m_sub_options ao_conf; extern const struct m_sub_options dvd_conf; +extern const struct m_sub_options dvda_conf; extern const struct m_sub_options clipboard_conf; extern const struct m_sub_options curl_conf; @@ -587,9 +588,13 @@ static const m_option_t mp_opts[] = { #if HAVE_DVDNAV {"dvd", OPT_SUBSTRUCT(dvd_opts, dvd_conf)}, +#endif +#if HAVE_DVDA + {"dvda", OPT_SUBSTRUCT(dvda_opts, dvda_conf)}, #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 d7b9542e27e25..a00f503347477 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; @@ -419,6 +420,7 @@ typedef struct MPOpts { struct wingl_opts *wingl_opts; struct cuda_opts *cuda_opts; struct dvd_opts *dvd_opts; + struct dvda_opts *dvda_opts; struct vaapi_opts *vaapi_opts; struct sws_opts *sws_opts; struct zimg_opts *zimg_opts; diff --git a/player/command.c b/player/command.c index a5b5e7e6f5a82..e12109904769c 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); } @@ -4643,6 +4672,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}, @@ -5363,7 +5393,7 @@ static void recreate_overlays(struct MPContext *mpctx) new->num_parts = 0; } - osd_set_external2(mpctx->osd, new); + osd_set_bitmaps(mpctx->osd, OSDTYPE_EXTERNAL2, new); mp_wakeup_core(mpctx); cmd->overlay_osd_current = overlay_next; } @@ -5501,7 +5531,7 @@ static void overlay_uninit(struct MPContext *mpctx) return; for (int id = 0; id < cmd->num_overlays; id++) replace_overlay(mpctx, id, &(struct overlay){0}); - osd_set_external2(mpctx->osd, NULL); + osd_set_bitmaps(mpctx->osd, OSDTYPE_EXTERNAL2, NULL); for (int n = 0; n < 2; n++) mp_image_unrefp(&cmd->overlay_osd[n].packed); } @@ -7413,6 +7443,52 @@ 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); + } + + mp_wakeup_core(mpctx); +} + static void cmd_flush_status_line(void *p) { struct mp_cmd_ctx *cmd = p; @@ -7969,6 +8045,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)} } }, @@ -8397,6 +8490,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; @@ -8412,7 +8509,7 @@ void mp_option_run_callback(struct MPContext *mpctx, struct mp_option_callback * if (mpctx->playback_initialized && demuxer && demuxer->stream && (!strcmp(demuxer->stream->info->name, "bd") || !strcmp(demuxer->stream->info->name, "bdmv/bluray"))) { - int angle = opts->stream_bluray_opts->angle - 1; + int angle = opts->stream_bluray_opts->angle; stream_control(demuxer->stream, STREAM_CTRL_SET_ANGLE, &angle); } } diff --git a/player/core.h b/player/core.h index ec6784411c969..2ebe91f9667db 100644 --- a/player/core.h +++ b/player/core.h @@ -446,6 +446,9 @@ typedef struct MPContext { struct screenshot_ctx *screenshot_ctx; struct command_ctx *command_ctx; + struct disc_nav_state *disc_nav; + // Disc is parked on an infinite still frame (DVD/BD menu). + bool disc_nav_still_frame; struct encode_lavc_context *encode_lavc_ctx; struct mp_option_callback *option_callbacks; @@ -671,6 +674,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..35408e71efc6e --- /dev/null +++ b/player/discnav.c @@ -0,0 +1,474 @@ +/* + * 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 "sub/osd_state.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_bitmaps (OSDTYPE_DISC_MENU). + struct mp_image *bd_image; + uint32_t bd_last_change_id; + struct mp_osd_res bd_last_vo_res; + + // Last menu-overlay change_id we forced an OSD redraw for. + uint32_t last_overlay_change_id; + bool overlay_change_seen; + + // 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: dvd_subtitle track we force-selected for the menu, and the + // selection it displaced (restored on menu close). + struct track *menu_selected_track; + struct track *menu_saved_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 bool is_dvd_sub_track(struct track *t) +{ + return t && t->type == STREAM_SUB && t->stream && t->stream->codec && + t->stream->codec->codec && + strcmp(t->stream->codec->codec, "dvd_subtitle") == 0; +} + +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; +} + +// Push the current menu/highlight state to the dvd_subtitle decoders. +static void push_dvd_overlay(struct MPContext *mpctx, struct stream_nav_state *nav) +{ + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *t = mpctx->tracks[n]; + if (!is_dvd_sub_track(t) || !t->d_sub) + continue; + sub_control(t->d_sub, SD_CTRL_APPLY_DVDNAV, nav); + } +} + +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_bitmaps(mpctx->osd, OSDTYPE_DISC_MENU, 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_bitmaps(mpctx->osd, OSDTYPE_DISC_MENU, &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); + // Re-queue the retained menu subpicture the flush may have destroyed. + demux_nav_refresh(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 pointers could be stale. Trust only what we + // can still see in mpctx->tracks. + for (int i = 0; i < 2; i++) { + struct track **slot = i ? &st->menu_saved_track : &st->menu_selected_track; + if (!*slot) + continue; + bool still_present = false; + for (int n = 0; n < mpctx->num_tracks; n++) { + if (mpctx->tracks[n] == *slot) { + still_present = true; + break; + } + } + if (!still_present) + *slot = NULL; + } + + if (menu_on) { + // Re-checked every frame; other selectors (stream auto-select, slave + // reopens) can change the sub under us. + if (st->menu_selected_track && cur == st->menu_selected_track) + return; + // A dvd_subtitle track is already active; nothing to force. + if (is_dvd_sub_track(cur)) + return; + if (mpctx->opts->stream_id[0][STREAM_SUB] == -2) + return; + struct track *pick = NULL; + for (int n = 0; n < mpctx->num_tracks; n++) { + if (is_dvd_sub_track(mpctx->tracks[n])) { + pick = mpctx->tracks[n]; + break; + } + } + if (!pick) + return; + // Remember the displaced selection only on the first override. + if (!st->menu_selected_track) + st->menu_saved_track = cur; + 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, st->menu_saved_track, 0); + st->menu_selected_track = NULL; + st->menu_saved_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; + bool still = have && nav.still_active; + if (still != mpctx->disc_nav_still_frame) + MP_VERBOSE(mpctx, "discnav: still_frame %d->%d\n", + mpctx->disc_nav_still_frame, still); + mpctx->disc_nav_still_frame = still; + if (!have) { + struct disc_nav_state *st = mpctx->disc_nav; + if (!st) + return; + if (st->overlay_visible) { + osd_set_bitmaps(mpctx->osd, OSDTYPE_DISC_MENU, 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; + st->menu_saved_track = NULL; + st->overlay_change_seen = false; + 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 || (mp_rect_w(nav.hl.rect) > 0 && mp_rect_h(nav.hl.rect) > 0)); + + if (is_bd) { + push_bd_overlay(mpctx, s, &nav, visible); + } else { + push_dvd_overlay(mpctx, &nav); + ensure_menu_sub_selection(mpctx, nav.menu_active); + } + + // The menu overlay updates independently of video. When it changes while + // the video isn't producing frames (held on a still, or paused) nothing + // would repaint it, so request an OSD redraw. + if (!st->overlay_change_seen || nav.change_id != st->last_overlay_change_id) { + st->overlay_change_seen = true; + st->last_overlay_change_id = nav.change_id; + osd_changed(mpctx->osd); + } + + 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 b2e57d9f111fb..865bcc364b627 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,24 @@ 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. + TA_FREEP(&mpctx->chapters); + 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; } 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..0e74b09619c4c 100644 --- a/player/playloop.c +++ b/player/playloop.c @@ -1238,6 +1238,10 @@ static void handle_eof(struct MPContext *mpctx) bool prevent_eof = mpctx->paused && mpctx->video_out && vo_has_frame(mpctx->video_out) && !mpctx->vo_chain->is_coverart; + /* A disc menu parked on an infinite still frame reports EOF so the decoder + * drains and the menu frame is shown. Hold it until the user navigates, + * rather than ending the file. */ + prevent_eof |= mpctx->disc_nav_still_frame; /* It's possible for the user to simultaneously switch both audio * and video streams to "disabled" at runtime. Handle this by waiting * rather than immediately stopping playback due to EOF. @@ -1288,6 +1292,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/stream/stream.c b/stream/stream.c index 5314eb3c93622..cc29f31a4b26c 100644 --- a/stream/stream.c +++ b/stream/stream.c @@ -56,6 +56,8 @@ extern const stream_info_t stream_info_slice; extern const stream_info_t stream_info_fd; extern const stream_info_t stream_info_ifo_dvdnav; extern const stream_info_t stream_info_dvdnav; +extern const stream_info_t stream_info_ifo_dvda; +extern const stream_info_t stream_info_dvda; extern const stream_info_t stream_info_bdmv_dir; extern const stream_info_t stream_info_bluray; extern const stream_info_t stream_info_edl; @@ -77,6 +79,10 @@ static const stream_info_t *const stream_list[] = { &stream_info_ifo_dvdnav, &stream_info_dvdnav, #endif +#if HAVE_DVDA + &stream_info_ifo_dvda, + &stream_info_dvda, +#endif #if HAVE_LIBBLURAY &stream_info_bdmv_dir, &stream_info_bluray, diff --git a/stream/stream.h b/stream/stream.h index 621fd5aaf70c4..9a91a8a77c111 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -25,6 +25,7 @@ #include #include +#include "common/common.h" #include "misc/bstr.h" // Minimum guaranteed buffer and seek-back size. For any reads <= of this size, @@ -93,6 +94,62 @@ 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_* +}; + +// A DVD subpicture button highlight. +struct mp_dvdnav_highlight { + struct mp_rect rect; // button rectangle in SPU/source coords + uint32_t palette[4]; // replacement colors for SPU pixel values 0..3 +}; + +// Snapshot of the stream's menu state. +struct stream_nav_state { + bool nav_active; // interactive disc navigation is enabled + bool menu_active; // a selectable menu/highlight is currently visible + bool has_popup; // disc supports a popup menu (BD only) + bool still_active; // holding an indefinite still frame + int src_w, src_h; // dimensions of the coordinate space mouse uses + struct mp_dvdnav_highlight hl; // focused button highlight (DVD only) + 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 b28f452675537..59c0a8732ac1b 100644 --- a/stream/stream_bluray.c +++ b/stream/stream_bluray.c @@ -41,14 +41,16 @@ #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" #include "sub/osd.h" #include "sub/img_convert.h" +#include "video/csputils.h" #include "video/mp_image.h" #define BLURAY_SECTOR_SIZE 6144 @@ -61,8 +63,13 @@ // 90khz ticks #define BD_TIMEBASE (90000) -#define BD_TIME_TO_MP(x) ((x) / (double)(BD_TIMEBASE)) -#define BD_TIME_FROM_MP(x) ((uint64_t)(x * BD_TIMEBASE)) +#define BD_TIME_TO_S(x) ((x) / (double)(BD_TIMEBASE)) +#define BD_TIME_FROM_S(x) ((uint64_t)(x * BD_TIMEBASE)) + +// Interval between read retries while navigation is idle +#define BLURAY_POLL_TIME_S 0.010 +// In menu mode, tolerate this many consecutive idle reads at the end of playlist +#define BLURAY_NAV_EOF_POLLS 100 // copied from aacs.h in libaacs #define AACS_ERROR_CORRUPTED_DISC -1 /* opening or reading of AACS files failed */ @@ -88,102 +95,602 @@ const struct m_sub_options stream_bluray_conf = { }, }; +// libbluray's support only global debug callback, without per-instance info. +static mp_static_mutex bluray_log_lock = MP_STATIC_MUTEX_INITIALIZER; +static struct mp_log *bluray_log; + +static void bluray_logger(const char *msg) +{ + mp_mutex_lock(&bluray_log_lock); + if (bluray_log) + mp_msg(bluray_log, MSGL_DEBUG, "%s", msg); + mp_mutex_unlock(&bluray_log_lock); +} + +// One overlay plane (BGRA, premultiplied alpha). +struct bd_overlay_plane { + uint32_t *work; + uint32_t *publish; + int w, h; // current allocation size (0 if unallocated) + bool visible; // publish has any non-zero alpha pixel +}; + struct bluray_priv_s { BLURAY *bd; + struct mp_log *bluray_log; BLURAY_TITLE_INFO *title_info; int num_titles; int current_angle; int current_title; int current_playlist; + // Cached map from filtered title index (0..num_titles-1) to mpls_id. + uint32_t *title_to_playlist; + int cfg_title; int cfg_playlist; char *cfg_device; struct mp_bluray_opts *opts; struct m_config_cache *opts_cache; + + // Disc-menu support (enabled when cfg_title == BLURAY_MENU_TITLE). + // The HDMV graphics controller emits IG-plane primitives through + // bd_register_overlay_proc (YUV+RLE). BD-J titles bypass it entirely + // and emit fully-rendered ARGB on both PG and IG planes through + // bd_register_argb_overlay_proc. Discs that mix HDMV first-play with + // BD-J menus (or vice versa) need both callbacks registered. + bool hdmv_mode; + struct bd_overlay_plane ig; + struct bd_overlay_plane pg; + // Guards state shared between the demuxer thread and the player thread. + mp_mutex overlay_lock; + + 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 + bool still_active; // holding an indefinite still. + + // 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; }; -inline static int play_playlist(struct bluray_priv_s *priv, int playlist) +// 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) { - return bd_select_playlist(priv->bd, playlist); + 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; } -inline static int play_title(struct bluray_priv_s *priv, int title) +static void bd_overlay_clear(struct bd_overlay_plane *p) { - return bd_select_title(priv->bd, title); + if (p->work) + memset(p->work, 0, (size_t)p->w * p->h * 4); } -static void bluray_stream_close(stream_t *s) +static void bd_overlay_hide(struct bluray_priv_s *priv, struct bd_overlay_plane *p) { - struct bluray_priv_s *priv = s->priv; - if (!priv) + 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++; +} - if (priv->title_info) - bd_free_title_info(priv->title_info); - if (priv->bd) - bd_close(priv->bd); +static enum pl_color_system bd_overlay_csp(struct bluray_priv_s *priv) +{ + const BLURAY_TITLE_INFO *ti = priv->title_info; + if (!ti || !ti->clip_count || !ti->clips[0].video_stream_count) + return PL_COLOR_SYSTEM_BT_709; + const BLURAY_STREAM_INFO *vs = &ti->clips[0].video_streams[0]; + +#if BLURAY_VERSION > BLURAY_VERSION_CODE(1, 4, 1) // not yet released, but next libbluray will have this + // HEVC on UHD BD (2160p or 1080p) can be either 2020 or 709 + if (vs->coding_type == BLURAY_STREAM_TYPE_VIDEO_HEVC || vs->format == BLURAY_VIDEO_FORMAT_2160P) + return vs->color_space == BLURAY_COLOR_SPACE_BT2020 ? PL_COLOR_SYSTEM_BT_2020_NC : PL_COLOR_SYSTEM_BT_709; +#endif + + switch (vs->format) { +#if BLURAY_VERSION <= BLURAY_VERSION_CODE(1, 4, 1) + case BLURAY_VIDEO_FORMAT_2160P: + return PL_COLOR_SYSTEM_BT_2020_NC; +#endif + case BLURAY_VIDEO_FORMAT_480I: // ITU-R BT.601 + case BLURAY_VIDEO_FORMAT_576I: + case BLURAY_VIDEO_FORMAT_480P: + case BLURAY_VIDEO_FORMAT_576P: + return PL_COLOR_SYSTEM_BT_601; + default: + return PL_COLOR_SYSTEM_BT_709; + } } -static void handle_event(stream_t *s, const BD_EVENT *ev) +static void bd_palette_to_bgra(const BD_PG_PALETTE_ENTRY *pg, uint32_t out[256], + enum pl_color_system csp) { - struct bluray_priv_s *b = s->priv; - switch (ev->event) { - case BD_EVENT_MENU: + struct mp_csp_params params = MP_CSP_PARAMS_DEFAULTS; + params.repr.sys = csp; + params.repr.levels = PL_COLOR_LEVELS_LIMITED; + params.levels_out = PL_COLOR_LEVELS_FULL; + struct pl_transform3x3 yuv2rgb; + mp_get_csp_matrix(¶ms, &yuv2rgb); + + for (int i = 0; i < 256; i++) { + int yuv[3] = { pg[i].Y, pg[i].Cb, pg[i].Cr }; + int rgb[3]; + mp_map_fixp_color(&yuv2rgb, 8, yuv, 8, rgb); + int T = pg[i].T; + // Pre-multiply RGB by alpha so the OSD layer can composite directly. + int r = rgb[0] * T / 255; + int g = rgb[1] * T / 255; + int b = rgb[2] * T / 255; + out[i] = ((uint32_t)T << 24) | ((uint32_t)r << 16) | + ((uint32_t)g << 8) | (uint32_t)b; + } + // palette index 0xFF is always transparent. + out[0xFF] = 0; +} + +// Composite one RLE-encoded sub-bitmap into the IG plane at (ov->x, ov->y). +static void bd_overlay_draw_rle(struct bd_overlay_plane *p, const BD_OVERLAY *ov, + enum pl_color_system csp) +{ + if (!ov->img || !ov->palette || ov->w <= 0 || ov->h <= 0) + return; + uint32_t pal[256]; + bd_palette_to_bgra(ov->palette, pal, csp); + + const BD_PG_RLE_ELEM *rle = ov->img; + for (int y = 0; y < ov->h; y++) { + int dst_y = ov->y + y; + bool in_plane = dst_y >= 0 && dst_y < p->h; + uint32_t *dst_row = in_plane ? p->work + (size_t)dst_y * p->w : NULL; + int x = 0; + 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_EVENT_STILL: + case BD_OVERLAY_CLOSE: + case BD_OVERLAY_HIDE: + bd_overlay_hide(priv, p); break; - case BD_EVENT_STILL_TIME: - bd_read_skip_still(b->bd); + case BD_OVERLAY_CLEAR: + bd_overlay_clear(p); break; - case BD_EVENT_END_OF_TITLE: + 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_EVENT_PLAYLIST: - b->current_playlist = 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 = bd_get_playlist_info(b->bd, b->current_playlist, - b->current_angle); + case BD_OVERLAY_DRAW: + if (p->work) + bd_overlay_draw_rle(p, ov, bd_overlay_csp(priv)); 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; - if (b->title_info) { - bd_free_title_info(b->title_info); - b->title_info = NULL; - } + case BD_OVERLAY_FLUSH: + bd_overlay_flush(priv, p); break; - case BD_EVENT_ANGLE: - b->current_angle = ev->param; - 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); - } + default: break; - case BD_EVENT_POPUP: + } + 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; -#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 5, 0) - case BD_EVENT_DISCONTINUITY: + 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; -#endif default: - MP_TRACE(s, "Unhandled event: %d %d\n", ev->event, ev->param); break; } + mp_mutex_unlock(&priv->overlay_lock); +} + +inline static int play_playlist(struct bluray_priv_s *priv, int playlist) +{ + return bd_select_playlist(priv->bd, playlist); +} + +inline static int play_title(struct bluray_priv_s *priv, int title) +{ + return bd_select_title(priv->bd, title); +} + +static void bluray_stream_close(stream_t *s) +{ + struct bluray_priv_s *priv = s->priv; + if (!priv) + return; + + if (priv->title_info) + bd_free_title_info(priv->title_info); + if (priv->bd) { + if (priv->hdmv_mode) { + bd_register_overlay_proc(priv->bd, NULL, NULL); + bd_register_argb_overlay_proc(priv->bd, NULL, NULL, NULL); + } + bd_close(priv->bd); + } + mp_mutex_lock(&bluray_log_lock); + // If we created the global log, unset it. + if (bluray_log == priv->bluray_log) + bluray_log = NULL; + mp_mutex_unlock(&bluray_log_lock); + mp_mutex_destroy(&priv->overlay_lock); +} + +static const char *bd_event_str(uint32_t event) +{ +#if BLURAY_VERSION >= BLURAY_VERSION_CODE(1, 3, 0) + const char *name = bd_event_name(event); + if (name) + return name; +#endif + return "?"; } static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len) { struct bluray_priv_s *b = s->priv; - BD_EVENT event; - while (bd_get_event(b->bd, &event)) - handle_event(s, &event); - return bd_read(b->bd, buf, len); + + uint32_t disc_id = b->discontinuity_id; + + int idle_reads = 0; + while (!mp_cancel_test(s->cancel)) { + BD_EVENT ev; + int n = bd_read_ext(b->bd, buf, len, &ev); + if (n < 0) { + MP_VERBOSE(s, "bd_read_ext() failed.\n"); + return -1; + } + + if (b->hdmv_mode && ev.event != BD_EVENT_NONE) + MP_DBG(s, "event %s(%u) param %u\n", bd_event_str(ev.event), ev.event, ev.param); + + bool stalled = false; // EOF unless the VM continues + bool bdj_idle = false; // BD-J title alive with no playlist playing + + switch (ev.event) { + case BD_EVENT_NONE: + stalled = n == 0; + break; + case BD_EVENT_ERROR: + MP_ERR(s, "Blu-ray navigation error (%u).\n", ev.param); + return -1; + case BD_EVENT_READ_ERROR: + MP_WARN(s, "Blu-ray read error, skipping unit.\n"); + break; + case BD_EVENT_END_OF_TITLE: + mp_mutex_lock(&b->overlay_lock); + b->still_active = false; + mp_mutex_unlock(&b->overlay_lock); + stalled = true; + break; + case BD_EVENT_IDLE: + bdj_idle = true; + break; + case BD_EVENT_STILL: + mp_mutex_lock(&b->overlay_lock); + b->still_active = ev.param != 0; + mp_mutex_unlock(&b->overlay_lock); + break; + case BD_EVENT_STILL_TIME: + // TODO: consider timed stills support + // param == 0 is an indefinite still + if (ev.param == 0) { + mp_mutex_lock(&b->overlay_lock); + b->still_active = true; + mp_mutex_unlock(&b->overlay_lock); + } else { + bd_read_skip_still(b->bd); + } + break; + case BD_EVENT_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_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; + case BD_EVENT_PLAYLIST: { + int playlist = ev.param; + int title = bd_get_current_title(b->bd); + if (b->title_to_playlist) { + for (int i = 0; i < b->num_titles; i++) { + if (b->title_to_playlist[i] == ev.param) { + title = i; + break; + } + } + } + BLURAY_TITLE_INFO *ti = bd_get_playlist_info(b->bd, playlist, b->current_angle); + mp_mutex_lock(&b->overlay_lock); + if (b->title_info) + bd_free_title_info(b->title_info); + b->title_info = ti; + b->current_playlist = playlist; + b->current_title = title; + if (b->hdmv_mode) + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + break; + } + case BD_EVENT_TITLE: { + int title = bd_get_current_title(b->bd); + mp_mutex_lock(&b->overlay_lock); + if (b->title_info) { + bd_free_title_info(b->title_info); + b->title_info = NULL; + } + b->current_title = title; + if (b->hdmv_mode) + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + break; + } + case BD_EVENT_ANGLE: { + int angle = ev.param; + BLURAY_TITLE_INFO *ti = b->title_info ? bd_get_playlist_info(b->bd, b->current_playlist, angle) + : NULL; + mp_mutex_lock(&b->overlay_lock); + b->current_angle = angle; + if (ti) { + bd_free_title_info(b->title_info); + b->title_info = ti; + } + if (b->hdmv_mode) + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + break; + } + case BD_EVENT_AUDIO_STREAM: + mp_mutex_lock(&b->overlay_lock); + b->audio_stream_num = ev.param; + if (b->hdmv_mode) + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + break; + case BD_EVENT_PG_TEXTST_STREAM: + mp_mutex_lock(&b->overlay_lock); + b->sub_stream_num = ev.param; + if (b->hdmv_mode) + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + break; + case BD_EVENT_PG_TEXTST: + mp_mutex_lock(&b->overlay_lock); + b->sub_visible = ev.param != 0; + if (b->hdmv_mode) + b->nav_change_id++; + mp_mutex_unlock(&b->overlay_lock); + break; + case BD_EVENT_DISCONTINUITY: + break; + default: + MP_TRACE(s, "Unhandled event: %s(%u) %u\n", + bd_event_str(ev.event), ev.event, ev.param); + break; + } + + if (n > 0) { + if (b->still_active) { + mp_mutex_lock(&b->overlay_lock); + b->still_active = false; + mp_mutex_unlock(&b->overlay_lock); + } + b->data_delivered = true; + return n; + } + + // Holding an indefinite still frame: report EOF. The player keeps + // showing the last frame; user interaction releases the still and + // resumes reading through a discontinuity_id bump. + if (b->still_active) + return 0; + // The play position jumped to another title/playlist after data was + // already delivered: report EOF so demux_disc reopens the slave + // demuxer before it parses data from the new playlist. + if (b->data_delivered && b->discontinuity_id != disc_id) + return 0; + + if (bdj_idle) { + idle_reads = 0; + mp_cancel_wait(s->cancel, BLURAY_POLL_TIME_S); + } else if (stalled) { + // Without menus there is nothing that could continue: plain EOF. + if (!b->hdmv_mode) + return 0; + // Retry for a while before concluding that playback ended. The + // first retry is immediate: a just-resumed VM progresses at once. + if (++idle_reads > BLURAY_NAV_EOF_POLLS) { + MP_VERBOSE(s, "Navigation stopped, EOF.\n"); + return 0; + } + if (idle_reads > 1) + mp_cancel_wait(s->cancel, BLURAY_POLL_TIME_S); + } else { + // The VM/title made progress; read again immediately. + idle_reads = 0; + } + } + return 0; +} + +static bool nav_action_activates(enum stream_nav_action a) +{ + switch (a) { + case STREAM_NAV_SELECT: + case STREAM_NAV_MOUSE_CLICK: + case STREAM_NAV_MENU_ROOT: + case STREAM_NAV_MENU_TITLE: + case STREAM_NAV_MENU_POPUP: + case STREAM_NAV_PREV_MENU: + return true; + default: + return false; + } } static int bluray_stream_control(stream_t *s, int cmd, void *arg) @@ -192,34 +699,52 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) switch (cmd) { case STREAM_CTRL_GET_NUM_CHAPTERS: { + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; - *((unsigned int *) arg) = ti->chapter_count; - return STREAM_OK; + if (ti) + *((unsigned int *) arg) = ti->chapter_count; + mp_mutex_unlock(&b->overlay_lock); + return ti ? STREAM_OK : STREAM_UNSUPPORTED; } case STREAM_CTRL_GET_CHAPTER_TIME: { - const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; int chapter = *(double *)arg; - double time = MP_NOPTS_VALUE; - if (chapter >= 0 && chapter < ti->chapter_count) - time = BD_TIME_TO_MP(ti->chapters[chapter].start); - if (time == MP_NOPTS_VALUE) - return STREAM_ERROR; - *(double *)arg = time; - return STREAM_OK; + mp_mutex_lock(&b->overlay_lock); + const BLURAY_TITLE_INFO *ti = b->title_info; + int rc = STREAM_UNSUPPORTED; + if (ti) { + double time = MP_NOPTS_VALUE; + if (chapter >= 0 && chapter < ti->chapter_count) + time = BD_TIME_TO_S(ti->chapters[chapter].start); + if (time != MP_NOPTS_VALUE) { + *(double *)arg = time; + rc = STREAM_OK; + } else { + rc = STREAM_ERROR; + } + } + mp_mutex_unlock(&b->overlay_lock); + return rc; } case STREAM_CTRL_SET_CURRENT_TITLE: { const uint32_t title = *((unsigned int*)arg); + // demux_disc appends a synthetic "Disc Menu" edition at index num_titles. + if (title == b->num_titles) { + if (!b->hdmv_mode) + return STREAM_UNSUPPORTED; + bd_menu_call(b->bd, -1); + return STREAM_OK; + } if (title >= b->num_titles || !play_title(b, title)) return STREAM_UNSUPPORTED; + mp_mutex_lock(&b->overlay_lock); b->current_title = title; + mp_mutex_unlock(&b->overlay_lock); return STREAM_OK; } case STREAM_CTRL_GET_CURRENT_TITLE: { + mp_mutex_lock(&b->overlay_lock); *((unsigned int *) arg) = b->current_title; + mp_mutex_unlock(&b->overlay_lock); return STREAM_OK; } case STREAM_CTRL_GET_NUM_TITLES: { @@ -227,43 +752,50 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) return STREAM_OK; } case STREAM_CTRL_GET_TIME_LENGTH: { + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; - *((double *) arg) = BD_TIME_TO_MP(ti->duration); - return STREAM_OK; + if (ti) + *((double *) arg) = BD_TIME_TO_S(ti->duration); + mp_mutex_unlock(&b->overlay_lock); + return ti ? STREAM_OK : STREAM_UNSUPPORTED; } case STREAM_CTRL_GET_CURRENT_TIME: { - *((double *) arg) = BD_TIME_TO_MP(bd_tell_time(b->bd)); + *((double *) arg) = BD_TIME_TO_S(bd_tell_time(b->bd)); return STREAM_OK; } case STREAM_CTRL_SEEK_TO_TIME: { double pts = *((double *) arg); - bd_seek_time(b->bd, BD_TIME_FROM_MP(pts)); + bd_seek_time(b->bd, BD_TIME_FROM_S(pts)); stream_drop_buffers(s); // API makes it hard to determine seeking success return STREAM_OK; } case STREAM_CTRL_GET_NUM_ANGLES: { + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; - *((int *) arg) = ti->angle_count; - return STREAM_OK; + if (ti) + *((int *) arg) = ti->angle_count; + mp_mutex_unlock(&b->overlay_lock); + return ti ? STREAM_OK : STREAM_UNSUPPORTED; } case STREAM_CTRL_GET_ANGLE: { - *((int *) arg) = b->current_angle; + mp_mutex_lock(&b->overlay_lock); + *((int *) arg) = b->current_angle + 1; + mp_mutex_unlock(&b->overlay_lock); return STREAM_OK; } case STREAM_CTRL_SET_ANGLE: { - const BLURAY_TITLE_INFO *ti = b->title_info; - if (!ti) - return STREAM_UNSUPPORTED; int angle = *((int *) arg); - if (angle < 0 || angle > ti->angle_count) + mp_mutex_lock(&b->overlay_lock); + const BLURAY_TITLE_INFO *ti = b->title_info; + bool ok = ti && angle >= 1 && angle <= ti->angle_count; + if (ok) + b->current_angle = angle - 1; + int cur = b->current_angle; + mp_mutex_unlock(&b->overlay_lock); + if (!ok) return STREAM_UNSUPPORTED; - b->current_angle = angle; - bd_seamless_angle_change(b->bd, angle); + bd_seamless_angle_change(b->bd, cur); return STREAM_OK; } case STREAM_CTRL_GET_TITLE_LENGTH: { @@ -273,7 +805,7 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) BLURAY_TITLE_INFO *ti = bd_get_title_info(b->bd, title, 0); if (!ti) return STREAM_UNSUPPORTED; - *(double *)arg = BD_TIME_TO_MP(ti->duration); + *(double *)arg = BD_TIME_TO_S(ti->duration); bd_free_title_info(ti); return STREAM_OK; } @@ -289,6 +821,8 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) return STREAM_OK; } case STREAM_CTRL_GET_LANG: { + int rc = STREAM_ERROR; + mp_mutex_lock(&b->overlay_lock); const BLURAY_TITLE_INFO *ti = b->title_info; if (ti && ti->clip_count) { struct stream_lang_req *req = arg; @@ -308,11 +842,13 @@ static int bluray_stream_control(stream_t *s, int cmd, void *arg) BLURAY_STREAM_INFO *i = &si[n]; if (i->pid == req->id) { snprintf(req->name, sizeof(req->name), "%.4s", i->lang); - return STREAM_OK; + rc = STREAM_OK; + break; } } } - return STREAM_ERROR; + mp_mutex_unlock(&b->overlay_lock); + return rc; } case STREAM_CTRL_GET_DISC_NAME: { const struct meta_dl *meta = bd_get_meta(b->bd); @@ -321,6 +857,165 @@ 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); + break; + case STREAM_NAV_MENU_POPUP: + key = BD_VK_POPUP; + break; + case STREAM_NAV_PREV_MENU: + // No dedicated "previous menu" key; popup-toggle is the closest + // equivalent and behaves like "dismiss current menu" on most + // discs when already in popup. + key = BD_VK_POPUP; + break; + case STREAM_NAV_MOUSE_MOVE: + b->mouse_x = nav->x; + b->mouse_y = nav->y; + bd_mouse_select(b->bd, -1, nav->x, nav->y); + break; + 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); + // If an activation just ran a button command, it may have released a + // held still, bump discontinuity_id. + if (nav_action_activates(nav->action)) { + mp_mutex_lock(&b->overlay_lock); + if (b->still_active) + b->discontinuity_id++; + mp_mutex_unlock(&b->overlay_lock); + } + return STREAM_OK; + } + case STREAM_CTRL_GET_NAV_STATE: { + struct stream_nav_state *st = arg; + int audio_pid = -1; + int sub_pid = -1; + mp_mutex_lock(&b->overlay_lock); + const BLURAY_TITLE_INFO *ti = b->title_info; + if (ti && ti->clip_count) { + const BLURAY_CLIP_INFO *ci = &ti->clips[0]; + 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, + }; + mp_mutex_unlock(&b->overlay_lock); + return STREAM_OK; + } + // 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){ + .nav_active = true, + .menu_active = visible, + .has_popup = b->popup_supported, + .still_active = b->still_active, + .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; } @@ -416,6 +1111,8 @@ static int bluray_stream_open_internal(stream_t *s) b->opts_cache = opts_cache; b->opts = opts_cache->opts; + mp_mutex_init(&b->overlay_lock); + int ret = 0; char *device = NULL; /* find the requested device */ @@ -433,8 +1130,25 @@ static int bluray_stream_open_internal(stream_t *s) goto err; } - if (!mp_msg_test(s->log, MSGL_DEBUG)) - bd_set_debug_mask(0); + mp_mutex_lock(&bluray_log_lock); + // libbluray log callback is global and there is no way to separate it per + // instance, just replace with new one if already present. + if (bluray_log) { + MP_WARN(s, "Replacing logger from previous instance."); + talloc_free(bluray_log); + } + uint32_t mask = 0; + if (mp_msg_test(s->log, MSGL_DEBUG)) + mask |= DBG_CRIT | DBG_BLURAY | DBG_HDMV; + // When all bits are set (-1) libbluray inits to DBG_CRIT, set all bits, + // except one, so we avoid this default fallback. + if (mp_msg_test(s->log, MSGL_TRACE)) + mask |= UINT32_MAX >> 1; + bd_set_debug_mask(mask); + if (mask) + b->bluray_log = bluray_log = mp_log_new(s, s->log, "/libbluray"); + bd_set_debug_handler(bluray_logger); + mp_mutex_unlock(&bluray_log_lock); /* open device */ char *device_tmp = mp_get_user_path(NULL, s->global, device); @@ -463,13 +1177,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 +1203,46 @@ 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; + } - if (!bd_select_angle(bd, b->opts->angle - 1)) - MP_WARN(s, "Couldn't select angle '%d'.\n", b->opts->angle - 1); + MP_VERBOSE(s, "bdnav: cfg_title=%d hdmv_mode=%d\n", b->cfg_title, b->hdmv_mode); + if (b->hdmv_mode) { + bd_register_overlay_proc(bd, b, bd_yuv_overlay_cb); + if (info->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)); + } + + // 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 +1273,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 +1366,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_dvda.c b/stream/stream_dvda.c new file mode 100644 index 0000000000000..c68469ac5f53b --- /dev/null +++ b/stream/stream_dvda.c @@ -0,0 +1,514 @@ +/* + * 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 "config.h" + +#if !HAVE_GPL +#error GPL only +#endif + +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "osdep/io.h" + +#include "common/common.h" +#include "common/msg.h" +#include "options/m_config.h" +#include "options/options.h" +#include "options/path.h" +#include "stream.h" + +#define TITLE_LONGEST -1 + +#define DVD_BLOCK_SIZE 2048 + +#define DVDA_TIMEBASE 90000 +#define DVDA_TIME_TO_S(x) ((x) / (double)(DVDA_TIMEBASE)) +#define DVDA_TIME_FROM_S(x) ((int64_t)((x) * DVDA_TIMEBASE)) + +// One audio track (mapped to an mpv chapter) within a title. +struct dvda_track { + int64_t time; // start time relative to title start (ticks) + int64_t duration; // 90 kHz PTS ticks + uint32_t start_sector; // within the ATS AOB space + uint32_t end_sector; // inclusive +}; + +// One ATS title (mapped to an mpv title/edition). +struct dvda_title { + int ats; // audio title set number (1-based) + int64_t duration; // 90 kHz PTS ticks + uint32_t start_sector; + uint32_t end_sector; // inclusive + struct dvda_track *tracks; + int num_tracks; +}; + +struct priv { + dvd_reader_t *dvd; + dvd_file_t *file; // AOBs of the currently open ATS + int open_ats; // ATS number the file handle belongs to + + struct dvda_title *titles; + int num_titles; + int title; // current title index + uint32_t cur_sector; + + int track; // requested title, or TITLE_LONGEST + char *device; + struct dvda_opts *opts; +}; + +struct dvda_opts { + char *device; +}; + +#define OPT_BASE_STRUCT struct dvda_opts + +const struct m_sub_options dvda_conf = { + .opts = (const struct m_option[]){ + {"device", OPT_STRING(device), .flags = M_OPT_FILE}, + {0} + }, + .size = sizeof(struct dvda_opts), +}; + +// Read the track layout of every title in every audio title set. +static bool read_disc_structure(stream_t *stream) +{ + struct priv *priv = stream->priv; + + ifo_handle_t *amg = ifoOpenVMGI(priv->dvd); + if (!amg || amg->ifo_format != IFO_AUDIO || !amg->amgi_mat) { + MP_ERR(stream, "Could not read AUDIO_TS.IFO.\n"); + if (amg) + ifoClose(amg); + return false; + } + int num_ats = amg->amgi_mat->amg_nr_of_title_sets; + ifoClose(amg); + + for (int ats = 1; ats <= num_ats; ats++) { + ifo_handle_t *ifo = ifoOpen(priv->dvd, ats); + if (!ifo) + continue; + if (ifo->ifo_format != IFO_AUDIO || !ifo->atsi_title_table) { + ifoClose(ifo); + continue; + } + atsi_title_table_t *tt = ifo->atsi_title_table; + for (int n = 0; n < tt->nr_titles; n++) { + atsi_title_record_t *rec = &tt->atsi_title_row_tables[n]; + int num_tracks = MPMIN(rec->nr_tracks, rec->nr_pointer_records); + if (num_tracks <= 0) + continue; + + struct dvda_title t = { + .ats = ats, + .duration = rec->length_pts, + .start_sector = rec->atsi_track_pointer_rows[0].start_sector, + .num_tracks = num_tracks, + .tracks = talloc_array(priv, struct dvda_track, num_tracks), + }; + t.end_sector = t.start_sector; + int64_t time = 0; + for (int i = 0; i < num_tracks; i++) { + atsi_track_timestamp_t *ts = &rec->atsi_track_timestamp_rows[i]; + atsi_track_pointer_t *ptr = &rec->atsi_track_pointer_rows[i]; + t.tracks[i] = (struct dvda_track){ + .time = time, + .duration = ts->length_pts_of_track, + .start_sector = ptr->start_sector, + .end_sector = ptr->end_sector, + }; + time += t.tracks[i].duration; + if (ptr->end_sector > t.end_sector) + t.end_sector = ptr->end_sector; + } + MP_DBG(stream, "title %d: ats=%d tracks=%d sectors=%"PRIu32 + "..%"PRIu32" duration=%.2f\n", priv->num_titles, ats, + num_tracks, t.start_sector, t.end_sector, DVDA_TIME_TO_S(t.duration)); + for (int i = 0; i < num_tracks; i++) + MP_DBG(stream, " track %d: t=%.2f dur=%.2f sectors=%"PRIu32 + "..%"PRIu32"\n", i, t.tracks[i].time, + DVDA_TIME_TO_S(t.tracks[i].duration), t.tracks[i].start_sector, + t.tracks[i].end_sector); + MP_TARRAY_APPEND(priv, priv->titles, priv->num_titles, t); + } + ifoClose(ifo); + } + + return priv->num_titles > 0; +} + +static bool play_title(stream_t *stream, int title) +{ + struct priv *priv = stream->priv; + + if (title < 0 || title >= priv->num_titles) + return false; + + struct dvda_title *t = &priv->titles[title]; + if (!priv->file || priv->open_ats != t->ats) { + if (priv->file) + DVDCloseFile(priv->file); + priv->file = DVDOpenFile(priv->dvd, t->ats, DVD_READ_TITLE_VOBS); + if (!priv->file) { + MP_ERR(stream, "Could not open AOB files of title set %d.\n", t->ats); + return false; + } + priv->open_ats = t->ats; + } + priv->title = title; + priv->cur_sector = t->start_sector; + return true; +} + +static int fill_buffer(stream_t *stream, void *buf, int max_len) +{ + struct priv *priv = stream->priv; + struct dvda_title *t = &priv->titles[priv->title]; + + if (max_len < DVD_BLOCK_SIZE) { + MP_FATAL(stream, "Short read size. Data corruption will follow. Please " + "provide a patch.\n"); + return -1; + } + + if (priv->cur_sector > t->end_sector) + return 0; // title end + + size_t blocks = MPMIN(max_len / DVD_BLOCK_SIZE, + t->end_sector - priv->cur_sector + 1); + ssize_t r = DVDReadBlocks(priv->file, priv->cur_sector, blocks, buf); + if (r <= 0) { + MP_ERR(stream, "Error reading sector %"PRIu32".\n", priv->cur_sector); + return 0; + } + priv->cur_sector += r; + return r * DVD_BLOCK_SIZE; +} + +// A track whose sectors fall outside the title's contiguous AOB range is a +// trailing marker. +static bool track_sectors_ok(struct dvda_title *t, struct dvda_track *tr) +{ + return tr->start_sector >= t->start_sector && + tr->end_sector <= t->end_sector && + tr->end_sector >= tr->start_sector; +} + +// Map a sector position to title-relative playback time via the track it falls +// in. Track durations (and thus start times) come from length_pts. +static double sector_to_time(struct dvda_title *t, uint32_t sector) +{ + for (int i = 0; i < t->num_tracks; i++) { + struct dvda_track *tr = &t->tracks[i]; + if (!track_sectors_ok(t, tr) || sector > tr->end_sector) + continue; + if (sector < tr->start_sector) + return tr->time; + return tr->time + av_rescale(sector - tr->start_sector, tr->duration, + tr->end_sector - tr->start_sector + 1); + } + return t->duration; +} + +static uint32_t time_to_sector(struct dvda_title *t, int64_t time) +{ + for (int i = t->num_tracks - 1; i >= 0; i--) { + struct dvda_track *tr = &t->tracks[i]; + if (time < tr->time && i > 0) + continue; + if (!track_sectors_ok(t, tr)) + return t->end_sector; + uint32_t span = tr->end_sector - tr->start_sector; + int64_t off = time - tr->time; + int64_t add = tr->duration > 0 && off > 0 ? av_rescale(off, span, tr->duration) : 0; + return tr->start_sector + MPMIN(add, span); + } + return t->start_sector; +} + +static int control(stream_t *stream, int cmd, void *arg) +{ + struct priv *priv = stream->priv; + struct dvda_title *t = &priv->titles[priv->title]; + + switch (cmd) { + case STREAM_CTRL_GET_NUM_CHAPTERS: + *(unsigned int *)arg = t->num_tracks; + return STREAM_OK; + case STREAM_CTRL_GET_CHAPTER_TIME: { + double *ch = arg; + int chapter = *ch; + if (chapter < 0 || chapter >= t->num_tracks) + break; + *ch = DVDA_TIME_TO_S(t->tracks[chapter].time); + return STREAM_OK; + } + case STREAM_CTRL_GET_TIME_LENGTH: + *(double *)arg = DVDA_TIME_TO_S(t->duration); + return STREAM_OK; + case STREAM_CTRL_GET_CURRENT_TIME: + *(double *)arg = DVDA_TIME_TO_S(sector_to_time(t, priv->cur_sector)); + return STREAM_OK; + case STREAM_CTRL_SEEK_TO_TIME: { + double *args = arg; + priv->cur_sector = time_to_sector(t, DVDA_TIME_FROM_S(args[0])); + stream_drop_buffers(stream); + return STREAM_OK; + } + case STREAM_CTRL_GET_NUM_TITLES: + *(unsigned int *)arg = priv->num_titles; + return STREAM_OK; + case STREAM_CTRL_GET_TITLE_LENGTH: { + int title = *(double *)arg; + if (title < 0 || title >= priv->num_titles) + break; + *(double *)arg = DVDA_TIME_TO_S(priv->titles[title].duration); + return STREAM_OK; + } + case STREAM_CTRL_GET_CURRENT_TITLE: + *(unsigned int *)arg = priv->title; + return STREAM_OK; + case STREAM_CTRL_SET_CURRENT_TITLE: { + int title = *(unsigned int *)arg; + if (!play_title(stream, title)) + break; + stream_drop_buffers(stream); + return STREAM_OK; + } + case STREAM_CTRL_GET_DISC_NAME: { + char volid[32] = {0}; + if (DVDUDFVolumeInfo(priv->dvd, volid, sizeof(volid), NULL, 0) < 0 && + DVDISOVolumeInfo(priv->dvd, volid, sizeof(volid), NULL, 0) < 0) + break; + if (!volid[0]) + break; + *(char **)arg = talloc_strdup(NULL, volid); + return STREAM_OK; + } + } + + return STREAM_UNSUPPORTED; +} + +static void stream_dvda_close(stream_t *stream) +{ + struct priv *priv = stream->priv; + if (priv->file) + DVDCloseFile(priv->file); + if (priv->dvd) + DVDClose(priv->dvd); +} + +static void dvda_log(void *priv, dvd_logger_level_t level, + const char *fmt, va_list va) +{ + int lvl; + switch (level) { + case DVD_LOGGER_LEVEL_ERROR: lvl = MSGL_ERR; break; + case DVD_LOGGER_LEVEL_WARN: lvl = MSGL_WARN; break; + case DVD_LOGGER_LEVEL_DEBUG: lvl = MSGL_DEBUG; break; + case DVD_LOGGER_LEVEL_INFO: + default: lvl = MSGL_V; break; + } + if (!mp_msg_test(priv, lvl)) + return; + mp_msg_va(priv, lvl, fmt, va); + mp_msg(priv, lvl, "\n"); +} + +static int open_s_internal(stream_t *stream) +{ + struct priv *priv = stream->priv; + char *filename; + + priv->opts = mp_get_config_group(stream, stream->global, &dvda_conf); + + if (priv->device && priv->device[0]) { + filename = priv->device; + } else if (priv->opts->device && priv->opts->device[0]) { + filename = priv->opts->device; + } else { + filename = DEFAULT_OPTICAL_DEVICE; + } + + char *path = mp_get_user_path(priv, stream->global, filename); + if (!path) + goto err; + + struct mp_log *log = mp_log_new(stream, stream->log, "/libdvdread"); + const dvd_logger_cb logger_cb = { .pf_log = dvda_log }; + priv->dvd = DVDOpenAudio(log, &logger_cb, path); + if (!priv->dvd) { + MP_ERR(stream, "Couldn't open DVD-Audio device: %s\n", path); + goto err; + } + + if (!read_disc_structure(stream)) { + MP_ERR(stream, "No DVD-Audio titles found: %s\n", path); + goto err; + } + + if (priv->track == TITLE_LONGEST || priv->track >= priv->num_titles) { + int64_t best_length = -1; + int best_title = 0; + for (int n = 0; n < priv->num_titles; n++) { + MP_VERBOSE(stream, "title: %3d tracks: %2d duration: %.1f\n", + n, priv->titles[n].num_tracks, + DVDA_TIME_TO_S(priv->titles[n].duration)); + if (priv->titles[n].duration > best_length) { + best_length = priv->titles[n].duration; + best_title = n; + } + } + priv->track = best_title; + MP_INFO(stream, "Selecting title %d.\n", priv->track); + } + + if (!play_title(stream, priv->track)) { + MP_ERR(stream, "Couldn't select title %d.\n", priv->track); + goto err; + } + + stream->fill_buffer = fill_buffer; + stream->control = control; + stream->close = stream_dvda_close; + stream->demuxer = "+disc"; + stream->lavf_type = "mpeg"; + + return STREAM_OK; + +err: + stream_dvda_close(stream); + return STREAM_ERROR; +} + +static int open_s(stream_t *stream) +{ + struct priv *priv = talloc_zero(stream, struct priv); + stream->priv = priv; + + bstr title, bdevice; + bstr_split_tok(bstr0(stream->path), "/", &title, &bdevice); + + struct MPOpts *opts = mp_get_config_group(stream, stream->global, &mp_opt_root); + int edition_id = opts->edition_id; + talloc_free(opts); + + priv->track = TITLE_LONGEST; + + if (edition_id >= 0) { + priv->track = edition_id; + } else if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) { + priv->track = TITLE_LONGEST; + } else if (title.len) { + bstr rest; + priv->track = bstrtoll(title, &rest, 10); + if (rest.len) { + MP_ERR(stream, "number expected: '%.*s'\n", BSTR_P(rest)); + return STREAM_ERROR; + } + } + + priv->device = bstrto0(priv, bdevice); + + return open_s_internal(stream); +} + +const stream_info_t stream_info_dvda = { + .name = "dvda", + .open = open_s, + .protocols = (const char *const[]){ "dvda", NULL }, + .stream_origin = STREAM_ORIGIN_UNSAFE, +}; + +// Check if this is likely to be AUDIO_TS.IFO. +static bool check_ifo(const char *path) +{ + if (strcasecmp(mp_basename(path), "audio_ts.ifo")) + return false; + + FILE *temp = fopen(path, "rb"); + if (!temp) + return false; + + char data[12]; + bool r = fread(data, sizeof(data), 1, temp) == 1 && + memcmp(data, "DVDAUDIO-AMG", 12) == 0; + + fclose(temp); + return r; +} + +static int ifo_dvda_stream_open(stream_t *stream) +{ + struct priv *priv = talloc_zero(stream, struct priv); + stream->priv = priv; + + if (!stream->access_references) + 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; + talloc_free(opts); + + char *path = mp_file_get_path(priv, bstr0(stream->url)); + if (!path) + goto unsupported; + + // We allow the path to point to a directory containing AUDIO_TS/, a + // directory containing AUDIO_TS.IFO, or that file itself. + if (!check_ifo(path)) { + // On UNIX, just assume the filename is always uppercase. + char *npath = mp_path_join(priv, path, "AUDIO_TS.IFO"); + if (!check_ifo(npath)) { + npath = mp_path_join(priv, path, "AUDIO_TS/AUDIO_TS.IFO"); + if (!check_ifo(npath)) + goto unsupported; + } + path = npath; + } + + priv->device = bstrto0(priv, mp_dirname(path)); + + MP_INFO(stream, ".IFO detected. Redirecting to dvda://\n"); + return open_s_internal(stream); + +unsupported: + talloc_free(priv); + stream->priv = NULL; + return STREAM_UNSUPPORTED; +} + +const stream_info_t stream_info_ifo_dvda = { + .name = "ifo_dvda", + .open = ifo_dvda_stream_open, + .protocols = (const char *const[]){ "file", "", NULL }, + .stream_origin = STREAM_ORIGIN_UNSAFE, +}; diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c index ad688f109b924..2f59210f08649 100644 --- a/stream/stream_dvdnav.c +++ b/stream/stream_dvdnav.c @@ -44,22 +44,32 @@ #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 +#define DVD_TIMEBASE 90000 +#define DVD_TIME_TO_S(x) ((x) / (double)(DVD_TIMEBASE)) +#define DVD_TIME_FROM_S(x) ((int64_t)((x) * DVD_TIMEBASE)) + +// 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 - unsigned int duration; // in milliseconds - int mousex, mousey; + int64_t duration; // in 90 kHz PTS ticks int title; + bool still_active; // fill_buffer() is holding a still uint32_t spu_clut[16]; bool spu_clut_valid; bool had_initial_vts; @@ -69,6 +79,20 @@ struct priv { int track; char *device; + bool in_menu; + int current_button; // mirror of libdvdnav HL_BTNN_REG + struct mp_dvdnav_highlight hl; // focused button rect + palette + uint32_t nav_change_id; + uint32_t discontinuity_id; // bumped on actions that may jump + bool pending_drain; // emit one EOF at the next jump boundary + 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 +233,276 @@ 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->hl.rect = (struct mp_rect){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->hl.rect = (struct mp_rect){ + .x0 = xs, .y0 = ys, + .x1 = xe > xs ? xe : xs, + .y1 = ye > ys ? ye : ys, + }; +} + +// 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][0], sizeof(coli)); // [0] = 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; + bool prev_menu = priv->in_menu; + struct mp_dvdnav_highlight prev_hl = priv->hl; + + 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->hl = (struct mp_dvdnav_highlight){0}; + } 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 || + memcmp(&prev_hl, &priv->hl, sizeof(prev_hl)) != 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 bool nav_action_activates(enum stream_nav_action a) +{ + switch (a) { + case STREAM_NAV_SELECT: + case STREAM_NAV_MOUSE_CLICK: + case STREAM_NAV_MENU_ROOT: + case STREAM_NAV_MENU_TITLE: + case STREAM_NAV_MENU_POPUP: + case STREAM_NAV_PREV_MENU: + return true; + default: + return false; + } +} + +// Move the highlight to the spec-defined neighbour of the current button. We +// resolve the neighbour ourselves and use dvdnav_button_select() (rather than +// dvdnav_{upper,lower,left,right}_button_select()) so that auto-action buttons +// are activated only through update_highlight(), where we can observe it. +static void select_neighbour_button(struct priv *priv, pci_t *pci, + enum stream_nav_action action) +{ + int32_t cur = 0; + dvdnav_get_current_highlight(priv->dvdnav, &cur); + if (cur <= 0 || cur > pci->hli.hl_gi.btn_ns) + return; + btni_t b; + memcpy(&b, &pci->hli.btnit[cur - 1], sizeof(b)); + int target = action == STREAM_NAV_UP ? b.up : + action == STREAM_NAV_DOWN ? b.down : + action == STREAM_NAV_LEFT ? b.left : + action == STREAM_NAV_RIGHT ? b.right : 0; + if (target > 0) + dvdnav_button_select(priv->dvdnav, pci, target); +} + +static void do_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: + case STREAM_NAV_DOWN: + case STREAM_NAV_LEFT: + case STREAM_NAV_RIGHT: + select_neighbour_button(priv, pci, cmd->action); + break; + case STREAM_NAV_MOUSE_MOVE: + dvdnav_mouse_select(priv->dvdnav, pci, cmd->x, cmd->y); + break; + case STREAM_NAV_MOUSE_CLICK: + 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); +} + +// Mark a source-position jump. +static void bump_discontinuity(struct priv *priv) +{ + priv->discontinuity_id++; + priv->pending_drain = true; +} + +static void handle_nav_cmd(stream_t *stream, struct stream_nav_cmd *cmd) +{ + struct priv *priv = stream->priv; + + int prev_auto = priv->auto_actioned_button; + do_nav_cmd(stream, cmd); + bool activated = nav_action_activates(cmd->action) || + priv->auto_actioned_button != prev_auto; + if (priv->still_active && activated) + bump_discontinuity(priv); +} + /** * \brief mp_dvdnav_lang_from_aid() returns the language corresponding to audio id 'aid' * \param stream: - stream pointer @@ -288,6 +582,10 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) } while (1) { + if (priv->pending_drain) { + priv->pending_drain = false; + return 0; + } int len = -1; int event = DVDNAV_NOP; if (dvdnav_get_next_block(dvdnav, buf, &event, &len) != DVDNAV_STATUS_OK) @@ -302,29 +600,57 @@ static int fill_buffer(stream_t *s, void *buf, int max_len) } switch (event) { case DVDNAV_BLOCK_OK: + // Real data is flowing again: we are no longer holding a still. + priv->still_active = false; return len; case DVDNAV_STOP: + // End of disc: a real EOF, not a held still. + priv->still_active = false; return 0; case DVDNAV_NAV_PACKET: { 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); + case DVDNAV_STILL_FRAME: { + dvdnav_still_event_t *still = buf; + // We only honor indefinite (0xff) stills. Finite stills (studio + // logos / warnings shown for a few seconds before the menu) are + // not hold on screen. This avoids complexities with correctly + // timing the still frames, and there is little use-case for holding + // 10+ seconds on single still frame. + if (still->length != 0xFF) { + MP_VERBOSE(s, "skipping finite still (%d s)\n", + still->length); + dvdnav_still_skip(dvdnav); + break; + } + // Indefinite still: report EOF to the demuxer so the video decoder + // is drained and the last frame is actually pushed to screen. + MP_VERBOSE(s, "indefinite still -> EOF, hold last frame\n"); + priv->still_active = true; return 0; + } case DVDNAV_WAIT: dvdnav_wait_skip(dvdnav); - return 0; + break; + case DVDNAV_HOP_CHANNEL: + // Bump discontinuity_id so the playloop flushes the cache. + bump_discontinuity(priv); + 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,19 +664,50 @@ 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. + bump_discontinuity(priv); break; } case DVDNAV_CELL_CHANGE: { dvdnav_cell_change_event_t *ev = (dvdnav_cell_change_event_t *)buf; if (ev->pgc_length) - priv->duration = ev->pgc_length / 90; + priv->duration = ev->pgc_length; break; } 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; } } @@ -388,13 +745,13 @@ static int control(stream_t *stream, int cmd, void *arg) free(parts); break; } - *ch = chapter > 0 ? parts[chapter - 1] / 90000.0 : 0; + *ch = chapter > 0 ? DVD_TIME_TO_S(parts[chapter - 1]) : 0; free(parts); return STREAM_OK; } case STREAM_CTRL_GET_TIME_LENGTH: { - if (priv->duration) { - *(double *)arg = (double)priv->duration / 1000.0; + if (priv->duration > 0) { + *(double *)arg = DVD_TIME_TO_S(priv->duration); return STREAM_OK; } break; @@ -405,10 +762,9 @@ static int control(stream_t *stream, int cmd, void *arg) return STREAM_OK; } case STREAM_CTRL_GET_CURRENT_TIME: { - double tm; - tm = dvdnav_get_current_time(dvdnav) / 90000.0f; + int64_t tm = dvdnav_get_current_time(dvdnav); if (tm != -1) { - *(double *)arg = tm; + *(double *)arg = DVD_TIME_TO_S(tm); return STREAM_OK; } break; @@ -433,7 +789,7 @@ static int control(stream_t *stream, int cmd, void *arg) if (!parts) break; free(parts); - *(double *)arg = duration / 90000.0; + *(double *)arg = DVD_TIME_TO_S(duration); return STREAM_OK; } case STREAM_CTRL_GET_CURRENT_TITLE: { @@ -444,8 +800,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; + bump_discontinuity(priv); + stream_drop_buffers(stream); + return STREAM_OK; + } if (dvdnav_title_play(priv->dvdnav, title + 1) != DVDNAV_STATUS_OK) break; + bump_discontinuity(priv); stream_drop_buffers(stream); return STREAM_OK; } @@ -455,11 +823,11 @@ static int control(stream_t *stream, int cmd, void *arg) int flags = args[1]; // from SEEK_* flags (demux.h) if (flags & SEEK_HR) d -= 10; // fudge offset; it's a hack, because fuck libdvd* - int64_t tm = (int64_t)(d * 90000); + int64_t tm = DVD_TIME_FROM_S(d); if (tm < 0) tm = 0; - if (priv->duration && tm >= (int64_t)priv->duration * 90) - tm = (int64_t)priv->duration * 90 - 1; + if (priv->duration > 0 && tm >= priv->duration) + tm = priv->duration - 1; uint32_t pos, len; if (dvdnav_get_position(dvdnav, &pos, &len) != DVDNAV_STATUS_OK) break; @@ -467,7 +835,7 @@ static int control(stream_t *stream, int cmd, void *arg) if (dvdnav_time_search(dvdnav, tm) != DVDNAV_STATUS_OK) break; stream_drop_buffers(stream); - d = dvdnav_get_current_time(dvdnav) / 90000.0f; + d = DVD_TIME_TO_S(dvdnav_get_current_time(dvdnav)); MP_VERBOSE(stream, "landed at: %f\n", d); if (dvdnav_get_position(dvdnav, &pos, &len) == DVDNAV_STATUS_OK) MP_VERBOSE(stream, "block: %lu\n", (unsigned long)pos); @@ -531,6 +899,34 @@ 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){ + .nav_active = true, + .menu_active = priv->in_menu, + .has_popup = false, + .still_active = priv->still_active, + .src_w = priv->src_w, + .src_h = priv->src_h, + .hl = priv->hl, + .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, + }; + return STREAM_OK; + } } return STREAM_UNSUPPORTED; @@ -546,6 +942,36 @@ static void stream_dvdnav_close(stream_t *s) dvd_set_speed(s, priv->filename, -1); } +#if DVDNAV_VERSION >= DVDNAV_VERSION_CODE(6, 1, 0) +static void dvdnav_log(void *priv, dvdnav_logger_level_t level, + const char *fmt, va_list va) +{ + int lvl; + switch (level) { + case DVDNAV_LOGGER_LEVEL_ERROR: lvl = MSGL_ERR; break; + case DVDNAV_LOGGER_LEVEL_WARN: lvl = MSGL_WARN; break; + case DVDNAV_LOGGER_LEVEL_DEBUG: lvl = MSGL_DEBUG; break; + case DVDNAV_LOGGER_LEVEL_INFO: + default: lvl = MSGL_V; break; + } + if (!mp_msg_test(priv, lvl)) + return; + mp_msg_va(priv, lvl, fmt, va); + mp_msg(priv, lvl, "\n"); +} +#endif + +static dvdnav_status_t nav_open(stream_t *stream, dvdnav_t **dest, const char *path) +{ +#if DVDNAV_VERSION >= DVDNAV_VERSION_CODE(6, 1, 0) + struct mp_log *log = mp_log_new(stream, stream->log, "/libdvdnav"); + const dvdnav_logger_cb logger_cb = { .pf_log = dvdnav_log }; + return dvdnav_open2(dest, log, &logger_cb, path); +#else + return dvdnav_open(dest, path); +#endif +} + static struct priv *new_dvdnav_stream(stream_t *stream, char *filename) { struct priv *priv = stream->priv; @@ -560,7 +986,7 @@ static struct priv *new_dvdnav_stream(stream_t *stream, char *filename) priv->dvd_speed = priv->opts->speed; dvd_set_speed(stream, priv->filename, priv->dvd_speed); - if (dvdnav_open(&(priv->dvdnav), priv->filename) != DVDNAV_STATUS_OK) + if (nav_open(stream, &priv->dvdnav, priv->filename) != DVDNAV_STATUS_OK) return NULL; if (!priv->dvdnav) @@ -582,6 +1008,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 +1027,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 +1069,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 +1098,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 +1149,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..63832cb68d794 100644 --- a/sub/dec_sub.h +++ b/sub/dec_sub.h @@ -5,6 +5,7 @@ #include #include "player/core.h" +#include "stream/stream.h" #include "osd.h" struct sh_stream; @@ -21,6 +22,7 @@ enum sd_ctrl { SD_CTRL_SET_VIDEO_DEF_FPS, SD_CTRL_RESET_SOFT, SD_CTRL_UPDATE_OPTS, + SD_CTRL_APPLY_DVDNAV, // const struct stream_nav_state * }; enum sd_text_type { diff --git a/sub/osd.c b/sub/osd.c index ef646b6775f42..0a073bd7113ef 100644 --- a/sub/osd.c +++ b/sub/osd.c @@ -192,7 +192,8 @@ void osd_free(struct osd_state *osd) if (!osd) return; osd_destroy_backend(osd); - talloc_free(osd->objs[OSDTYPE_EXTERNAL2]->external2); + talloc_free(osd->objs[OSDTYPE_EXTERNAL2]->image_overlay); + talloc_free(osd->objs[OSDTYPE_DISC_MENU]->image_overlay); mp_mutex_destroy(&osd->lock); talloc_free(osd); } @@ -274,12 +275,12 @@ void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s) mp_mutex_unlock(&osd->lock); } -void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs) +void osd_set_bitmaps(struct osd_state *osd, int type, struct sub_bitmaps *imgs) { mp_mutex_lock(&osd->lock); - struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL2]; - talloc_free(obj->external2); - obj->external2 = sub_bitmaps_copy(NULL, imgs); + struct osd_object *obj = osd->objs[type]; + talloc_free(obj->image_overlay); + obj->image_overlay = sub_bitmaps_copy(NULL, imgs); obj->vo_change_id += 1; osd->want_redraw_notification = true; mp_mutex_unlock(&osd->lock); @@ -306,7 +307,8 @@ static void check_obj_resize(struct osd_state *osd, struct mp_osd_res res, void osd_resize(struct osd_state *osd, struct mp_osd_res res) { mp_mutex_lock(&osd->lock); - int types[] = {OSDTYPE_OSD, OSDTYPE_EXTERNAL, OSDTYPE_EXTERNAL2, -1}; + int types[] = {OSDTYPE_OSD, OSDTYPE_EXTERNAL, OSDTYPE_EXTERNAL2, + OSDTYPE_DISC_MENU, -1}; for (int n = 0; types[n] >= 0; n++) check_obj_resize(osd, res, osd->objs[types[n]]); mp_mutex_unlock(&osd->lock); @@ -331,10 +333,11 @@ static struct sub_bitmaps *render_object(struct osd_state *osd, } else if (obj->type == OSDTYPE_SUB2) { if (obj->sub && sub_is_secondary_visible(obj->sub)) res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts); - } else if (obj->type == OSDTYPE_EXTERNAL2) { - if (obj->external2 && obj->external2->format) { - res = sub_bitmaps_copy(NULL, obj->external2); // need to be owner - obj->external2->change_id = 0; + } else if (obj->type == OSDTYPE_EXTERNAL2 || + obj->type == OSDTYPE_DISC_MENU) { + if (obj->image_overlay && obj->image_overlay->format) { + res = sub_bitmaps_copy(NULL, obj->image_overlay); // need to be owner + obj->image_overlay->change_id = 0; } } else { res = osd_object_get_bitmaps(osd, obj, format); diff --git a/sub/osd.h b/sub/osd.h index 44cf2edc1a3db..d69387220ec69 100644 --- a/sub/osd.h +++ b/sub/osd.h @@ -113,7 +113,7 @@ struct mp_osd_res { bool osd_res_equals(struct mp_osd_res a, struct mp_osd_res b); // 0 <= sub_bitmaps.render_index < MAX_OSD_PARTS -#define MAX_OSD_PARTS 5 +#define MAX_OSD_PARTS 6 // Start of OSD symbols in osd_font.pfb #define OSD_CODEPOINTS 0xE000 @@ -208,7 +208,7 @@ struct osd_progbar_state { }; void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s); -void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs); +void osd_set_bitmaps(struct osd_state *osd, int type, struct sub_bitmaps *imgs); enum mp_osd_draw_flags { OSD_DRAW_SUB_FILTER = (1 << 0), diff --git a/sub/osd_state.h b/sub/osd_state.h index cd4ca15a04e3d..b07373e9ce79a 100644 --- a/sub/osd_state.h +++ b/sub/osd_state.h @@ -7,6 +7,8 @@ #include "osdep/threads.h" enum mp_osdtype { + OSDTYPE_DISC_MENU, + OSDTYPE_SUB, OSDTYPE_SUB2, // IDs must be numerically successive @@ -44,8 +46,8 @@ struct osd_object { struct osd_external **externals; int num_externals; - // OSDTYPE_EXTERNAL2 - struct sub_bitmaps *external2; + // OSDTYPE_EXTERNAL2 / OSDTYPE_DISC_MENU + struct sub_bitmaps *image_overlay; // VO cache state int vo_change_id; diff --git a/sub/sd_lavc.c b/sub/sd_lavc.c index 5cbc6d0a21257..84de577ff6eea 100644 --- a/sub/sd_lavc.c +++ b/sub/sd_lavc.c @@ -71,6 +71,11 @@ struct sd_lavc_priv { struct seekpoint *seekpoints; int num_seekpoints; struct bitmap_packer *packer; + + // DVD-nav menu/highlight overlay state. + bool menu_active; // menu subpicture is persistent display state + struct mp_dvdnav_highlight hl; // focused button rect + palette + uint32_t hli_change_id; }; static int init(struct sd *sd) @@ -294,6 +299,22 @@ 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 + struct mp_rect hlr = priv->hl.rect; + bool hli_active = priv->menu_active && + mp_rect_w(hlr) > 0 && mp_rect_h(hlr) > 0 && + r->x < hlr.x1 && r->y < hlr.y1 && + r->x + r->w > hlr.x0 && r->y + r->h > hlr.y0; + uint32_t hli_pal[4] = {0}; + if (hli_active) { + memcpy(hli_pal, priv->hl.palette, sizeof(hli_pal)); + convert_pal(hli_pal, 4, opts->sub_gray); + } + int hli_x0 = hlr.x0 - r->x; + int hli_y0 = hlr.y0 - r->y; + int hli_x1 = hlr.x1 - r->x; + int hli_y1 = hlr.y1 - r->y; + 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; @@ -418,6 +460,16 @@ static void decode(struct sd *sd, struct demux_packet *packet) static struct sub *get_current(struct sd_lavc_priv *priv, double pts) { + // A menu subpicture is persistent display state, not bound to playback + // time. + if (priv->menu_active) { + for (int n = 0; n < MAX_QUEUE; n++) { + if (priv->subs[n].valid) + return &priv->subs[n]; + } + return NULL; + } + struct sub *current = NULL; for (int n = 0; n < MAX_QUEUE; n++) { struct sub *sub = &priv->subs[n]; @@ -589,6 +641,10 @@ static bool accepts_packet(struct sd *sd, double min_pts) { struct sd_lavc_priv *priv = sd->priv; + // Menu subpictures are not pts-ordered; never block delivery. + if (priv->menu_active) + return true; + double pts = priv->current_pts; if (min_pts != MP_NOPTS_VALUE) { // guard against bogus rendering PTS in the future. @@ -621,8 +677,12 @@ static void reset(struct sd *sd) { struct sd_lavc_priv *priv = sd->priv; - for (int n = 0; n < MAX_QUEUE; n++) - clear_sub(&priv->subs[n]); + // Keep the menu subpicture; timeline resets don't invalidate it and the + // disc won't re-send it. Purged when the menu closes. + if (!priv->menu_active) { + for (int n = 0; n < MAX_QUEUE; n++) + clear_sub(&priv->subs[n]); + } // lavc might not do this right for all codecs; may need close+reopen avcodec_flush_buffers(priv->avctx); @@ -715,6 +775,23 @@ 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 stream_nav_state *nav = arg; + bool menu_closed = priv->menu_active && !nav->menu_active; + bool changed = priv->hli_change_id != nav->change_id; + priv->menu_active = nav->menu_active; + priv->hl = nav->hl; + priv->hli_change_id = nav->change_id; + // Don't let menu subpictures leak into title playback. + if (menu_closed) { + for (int n = 0; n < MAX_QUEUE; n++) + clear_sub(&priv->subs[n]); + } + // Re-render any decoded subtitles, after style update. + if (changed) + rerender_queued_subs(sd); + return CONTROL_OK; + } default: return CONTROL_UNKNOWN; }