diff --git a/DOCS/interface-changes/dvda.txt b/DOCS/interface-changes/dvda.txt index a6909b1149..9d5fdf54bb 100644 --- a/DOCS/interface-changes/dvda.txt +++ b/DOCS/interface-changes/dvda.txt @@ -1,2 +1,2 @@ add `dvda://` protocol for DVD-Audio playback -add `--dvda-device` option +add `--dvda-device` and `--dvda-page` options diff --git a/DOCS/man/mpv.rst b/DOCS/man/mpv.rst index 2134904637..9f7fd0b0eb 100644 --- a/DOCS/man/mpv.rst +++ b/DOCS/man/mpv.rst @@ -1359,8 +1359,11 @@ PROTOCOLS 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. + given, the longest title is auto-selected. Menus are not supported. + + Still images (ASVS), such as cover art or booklet pages a disc associates + with its tracks, are exposed as a video track and shown by default. Can be + disabled with ``--vid=no``. ``dvb://[cardnumber@]channel`` ``--dvbin-...`` diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index d7ad685c83..249f53aa71 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -3947,6 +3947,11 @@ Disc Devices ``mpv dvda:// --dvda-device=/path/to/dvda/`` +``--dvda-page=<-1|0-...>`` + Force the still picture page shown during DVD-Audio playback, instead of + the page scheduled for the current playback position (default: -1, use + the scheduled page). + ``--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_disc.c b/demux/demux_disc.c index d0086e9cb1..58731147e2 100644 --- a/demux/demux_disc.c +++ b/demux/demux_disc.c @@ -71,6 +71,11 @@ struct priv { bool nav_active; // last interactive-nav state pushed to the cache bool is_dvd, is_cdda, is_bd; + + // DVD-Audio still-image video track. + struct sh_stream *still_sh; + int last_still_id; + struct demux_packet *pending_pkt; // still or the audio deferred behind it }; static void clear_dvd_sub_holds(struct priv *p) @@ -364,6 +369,10 @@ static void d_seek(demuxer_t *demuxer, double seek_pts, int flags) clear_dvd_sub_holds(p); + // Re-inject the still for the new position. + p->last_still_id = -1; + TA_FREEP(&p->pending_pkt); + p->seek_reinit = true; } @@ -474,10 +483,58 @@ static bool process_discontinuity(struct demuxer *demuxer, uint32_t new_id) return true; } +// Create the DVD-Audio still-image video track, if the stream provides stills. +static void add_still_stream(struct demuxer *demuxer) +{ + struct priv *p = demuxer->priv; + struct stream_still_req sr = {.time = 0}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_STILL, &sr) != STREAM_OK) + return; + if (!sr.has_stills) + return; + struct sh_stream *sh = demux_alloc_sh_stream(STREAM_VIDEO); + sh->codec->codec = "mpeg2video"; + sh->title = talloc_strdup(sh, "Cover"); + sh->still_image = true; // sparse: one frame per track, read lazily + p->still_sh = sh; + p->last_still_id = -1; + demux_add_sh_stream(demuxer, sh); +} + +// Inject the still for playback time `time` as a video packet, when the still +// track is selected and the shown still changed. +static void inject_still(struct demuxer *demuxer, double time) +{ + struct priv *p = demuxer->priv; + if (!p->still_sh || p->pending_pkt || + !demux_stream_is_selected(p->still_sh)) + return; + struct stream_still_req sr = {.time = time}; + if (stream_control(demuxer->stream, STREAM_CTRL_GET_STILL, &sr) != STREAM_OK) + return; + if (sr.id < 0 || sr.id == p->last_still_id || !sr.data || sr.data_size <= 0) + return; + p->last_still_id = sr.id; + struct demux_packet *dp = + new_demux_packet_from(demuxer->packet_pool, sr.data, sr.data_size); + if (!dp) + return; + dp->stream = p->still_sh->index; + dp->pts = dp->dts = time; + dp->keyframe = true; + p->pending_pkt = dp; +} + static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt) { struct priv *p = demuxer->priv; + if (p->pending_pkt) { + *out_pkt = p->pending_pkt; + p->pending_pkt = NULL; + return true; + } + bool menu_active = false; struct stream_nav_state nav = {0}; if (stream_control(demuxer->stream, STREAM_CTRL_GET_NAV_STATE, &nav) >= 1) { @@ -614,6 +671,18 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt MP_TRACE(demuxer, "mapped pkt: type=%d pts=%f dts=%f\n", sh->type, pkt->pts, pkt->dts); + if (sh->type == STREAM_AUDIO && pkt->pts != MP_NOPTS_VALUE) { + inject_still(demuxer, pkt->pts); + // Deliver the still before the audio it belongs to, so the frame is + // already present when the player starts or restarts here. + if (p->pending_pkt) { + struct demux_packet *still = p->pending_pkt; + p->pending_pkt = pkt; + *out_pkt = still; + return 1; + } + } + if (dvd_sub) { int idx = sh->demuxer_id - 0x20; if (idx >= 0 && idx < MAX_DVD_SPU_STREAMS) { @@ -742,6 +811,7 @@ static int d_open(demuxer_t *demuxer, enum demux_check check) add_dvd_streams(demuxer); sync_streams(demuxer); + add_still_stream(demuxer); add_stream_chapters(demuxer); add_stream_editions(demuxer); @@ -758,6 +828,7 @@ static void d_close(demuxer_t *demuxer) { struct priv *p = demuxer->priv; clear_dvd_sub_holds(p); + TA_FREEP(&p->pending_pkt); demux_free(p->slave); } diff --git a/options/options.h b/options/options.h index a20f81dcde..26a791343f 100644 --- a/options/options.h +++ b/options/options.h @@ -164,6 +164,11 @@ struct mp_bluray_opts { int angle; }; +struct dvda_opts { + char *device; + int page; +}; + typedef struct MPOpts { bool property_print_help; bool use_terminal; diff --git a/player/command.c b/player/command.c index 9942576939..18220ad771 100644 --- a/player/command.c +++ b/player/command.c @@ -8544,6 +8544,18 @@ void mp_option_run_callback(struct MPContext *mpctx, struct mp_option_callback * } #endif +#if HAVE_DVDA + if (opt_ptr == &opts->dvda_opts->page) { + struct demuxer *demuxer = mpctx->demuxer; + if (mpctx->playback_initialized && demuxer && demuxer->stream && + (!strcmp(demuxer->stream->info->name, "dvda") || + !strcmp(demuxer->stream->info->name, "ifo_dvda"))) { + int page = opts->dvda_opts->page; + stream_control(demuxer->stream, STREAM_CTRL_SET_STILL_PAGE, &page); + } + } +#endif + if (opt_ptr == &opts->pause) set_pause_state(mpctx, opts->pause); diff --git a/stream/stream.h b/stream/stream.h index e103aa6bd3..ffc241ed51 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -97,6 +97,20 @@ enum stream_ctrl { 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* + STREAM_CTRL_GET_STILL, // struct stream_still_req* + STREAM_CTRL_SET_STILL_PAGE, // int*, force the shown still page +}; + +// Fetch the still image that a disc (DVD-Audio ASVS) associates with the given +// playback position, as an MPEG-2 video elementary stream. +struct stream_still_req { + double time; // input: title-relative playback time to query + int id; // output: id of the still valid at `time`, changes + // when the shown still should change; -1 if none + bool has_stills; // output: the current title has stills at all (used + // to decide whether to expose a video track) + uint8_t *data; // output: MPEG-2 video ES; owned by the stream and + int data_size; // valid until the next STREAM_CTRL_GET_STILL call }; // Fetch a BGRA snapshot of the disc-menu overlay (used for Blu-ray HDMV). diff --git a/stream/stream_dvda.c b/stream/stream_dvda.c index 9c47e0b980..db9dfa9266 100644 --- a/stream/stream_dvda.c +++ b/stream/stream_dvda.c @@ -31,8 +31,10 @@ #include #include #include +#include #include "osdep/io.h" +#include "osdep/threads.h" #include "common/common.h" #include "common/msg.h" @@ -49,12 +51,19 @@ #define DVDA_TIME_TO_S(x) ((x) / (double)(DVDA_TIMEBASE)) #define DVDA_TIME_FROM_S(x) ((int64_t)((x) * DVDA_TIMEBASE)) +struct dvda_still { + int pvob; // 0-based global still picture (P_VOB) index + int64_t onset; // display time, 90 kHz PTS ticks +}; + // 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 + struct dvda_still *stills; // still pictures shown during this track + int num_stills; }; // One ATS title (mapped to an mpv title/edition). @@ -77,25 +86,138 @@ struct priv { int title; // current title index uint32_t cur_sector; + dvd_file_t *sv_file; // AUDIO_SV.VOB + uint32_t *pvob_sectors; // still picture (P_VOB) -> sector in AUDIO_SV.VOB + int num_pvobs; // number of still pictures in the lookup + int *asvu_first_pvob; // 1-based first P_VOB of each ASVU + int num_asvus; + uint32_t pvobs_end; // end of the still-picture area (sectors) + uint8_t *still_data; // the most recently extracted still (MPEG-2 video ES) + int still_size; + int still_pvob; // P_VOB index of the cached still, -1 if none + int track; // requested title, or TITLE_LONGEST + + mp_mutex still_lock; // guards the still-picture state below + int page; // forced still page (--dvda-page), -1 = time-based 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}, + {"page", OPT_INT(page), M_RANGE(-1, 2048)}, {0} }, .size = sizeof(struct dvda_opts), + .defaults = &(const struct dvda_opts){ + .page = -1, + }, }; +// Compatibility with current libdvdread release (7.1.1). Drop this once +// 7.1.2+ is released and we can require it. There were many internal libdvdread +// fixes, the current release support is working, but limited. When ASV_MAX_NR +// is defined, we are at newer than 7.1.1 (master). +#if DVDREAD_VERSION > DVDREAD_VERSION_CODE(7, 1, 1) || defined(ASV_MAX_NR) +#define ASVS_ASV_EA(m) ((m)->asv_ea) +#define ASVU_FIRST_ABS_ASVN(gi) ((gi)->first_abs_asvn) +#define ASVU_ASV_NS(gi) ((gi)->asv_ns) +#define ASVU_SA(gi) ((gi)->asvu_sa) +#define ASV_SRPT_OFFSET(e) ((e).offset) +#else +#define ASVS_ASV_EA(m) ((m)->p_vobs_ea) +#define ASVU_FIRST_ABS_ASVN(gi) ((gi)->start_p_vob_number) +#define ASVU_ASV_NS(gi) ((gi)->p_vob_ns) +#define ASVU_SA(gi) ((gi)->ref_start_sector) +#define ASV_SRPT_OFFSET(e) ((e) & 0x3fff) +#endif + +// Build the global still-picture -> AUDIO_SV.VOB sector table and open the still +// VOB. The ASVS groups still pictures (P_VOBs) into units (ASVUs). Each asv_srpt +// entry gives a P_VOB's start sector as an offset from its unit's start sector +// (the top two bits are copy control and not part of the offset). +static void read_asvs(stream_t *stream) +{ + struct priv *priv = stream->priv; + + priv->sv_file = DVDOpenFile(priv->dvd, 1, DVD_READ_MENU_VOBS); + ifo_handle_t *asvs = ifoOpenASVS(priv->dvd); + if (!asvs || !asvs->asvs_mat || !priv->sv_file) { + if (asvs) + ifoClose(asvs); + return; + } + + asvs_mat_t *m = asvs->asvs_mat; + priv->pvobs_end = ASVS_ASV_EA(m); + int f = 0; + for (int g = 0; g < m->asvs_nr_of_asvus; g++) { + asvu_gi_t *grp = &m->asvu_gi[g]; + MP_TARRAY_APPEND(priv, priv->asvu_first_pvob, priv->num_asvus, + ASVU_FIRST_ABS_ASVN(grp)); + for (int j = 0; j < ASVU_ASV_NS(grp); j++, f++) { + uint32_t sector = ASVU_SA(grp) + ASV_SRPT_OFFSET(m->asv_srpt[f]); + MP_TARRAY_APPEND(priv, priv->pvob_sectors, priv->num_pvobs, sector); + } + } + + MP_VERBOSE(stream, "ASVS: %d still pictures in %d units\n", priv->num_pvobs, + priv->num_asvus); + ifoClose(asvs); +} + +// Map the per-title still-picture table to per-track stills. libdvdread parses +// it into a per-program playback-info table (ats_pg_asv_pbi_srp) and the still +// records (dlist) it points at. Each entry names the ASVU (index) the program's +// stills live in and the [start,end] byte span of its records; each record gives +// the still number within that unit (asv_number) and its display time. The +// global still is the unit's start_p_vob_number plus asv_number. +static void parse_title_stills(stream_t *stream, struct dvda_title *t, + atsi_title_record_t *rec) +{ + struct priv *priv = stream->priv; + if (!priv->num_pvobs || !rec->ats_pg_asv_pbi_srp || !rec->dlist) + return; + + int nr_tracks = rec->nr_tracks; + int recbase = nr_tracks * ATS_PG_ASV_PBI_SRP_SIZE; + int title_idx = priv->num_titles; // title currently being built + + for (int i = 0; i < t->num_tracks && i < nr_tracks; i++) { + ats_pg_asv_pbi_srp_t *rr = &rec->ats_pg_asv_pbi_srp[i]; + int asvu = rr->asvu_n - 1; + MP_TRACE(stream, "title %d track %d: stills in asvu %d, " + "records [%u,%u], timing mode %d\n", title_idx, i, + rr->asvu_n, rr->start_value, rr->end_value, rr->dmod.timing); + if (rr->end_value < rr->start_value || rr->start_value < recbase || + asvu < 0 || asvu >= priv->num_asvus) + continue; + struct dvda_track *tr = &t->tracks[i]; + for (int k = (rr->start_value - recbase) / ASV_DLIST_SIZE; + k <= (rr->end_value - recbase) / ASV_DLIST_SIZE; k++) + { + asv_dlist_t *fr = &rec->dlist[k]; + if (fr->asv_number == 0) + continue; + int pvob = priv->asvu_first_pvob[asvu] - 1 + fr->asv_number - 1; + MP_TRACE(stream, " rec[%d]: asv %d track %#x timing %d -> " + "still %d\n", k, fr->asv_number, fr->track_nr, + fr->display_timing, pvob); + // With a shared range, the per-record track number selects the track. + if (rr->dmod.timing == 0 && fr->track_nr != i + 1) + continue; + if (pvob < 0 || pvob >= priv->num_pvobs) + continue; + struct dvda_still s = { .pvob = pvob, .onset = fr->display_timing }; + MP_TARRAY_APPEND(priv, tr->stills, tr->num_stills, s); + } + } +} + // Read the track layout of every title in every audio title set. static bool read_disc_structure(stream_t *stream) { @@ -111,6 +233,8 @@ static bool read_disc_structure(stream_t *stream) int num_ats = amg->amgi_mat->amg_nr_of_title_sets; ifoClose(amg); + read_asvs(stream); + for (int ats = 1; ats <= num_ats; ats++) { ifo_handle_t *ifo = ifoOpen(priv->dvd, ats); if (!ifo) @@ -148,14 +272,19 @@ static bool read_disc_structure(stream_t *stream) 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++) + parse_title_stills(stream, &t, rec); + + MP_VERBOSE(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++) { + struct dvda_track *tr = &t.tracks[i]; MP_DBG(stream, " track %d: t=%.2f dur=%.2f sectors=%"PRIu32 - "..%"PRIu32"\n", i, DVDA_TIME_TO_S(t.tracks[i].time), - DVDA_TIME_TO_S(t.tracks[i].duration), t.tracks[i].start_sector, - t.tracks[i].end_sector); + "..%"PRIu32" stills=%d\n", i, DVDA_TIME_TO_S(tr->time), + DVDA_TIME_TO_S(tr->duration), tr->start_sector, + tr->end_sector, tr->num_stills); + } MP_TARRAY_APPEND(priv, priv->titles, priv->num_titles, t); } ifoClose(ifo); @@ -253,6 +382,108 @@ static uint32_t time_to_sector(struct dvda_title *t, int64_t time) return t->start_sector; } +static int current_still_pvob(stream_t *stream, struct dvda_title *t, double time) +{ + struct priv *priv = stream->priv; + 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 (tr->num_stills == 0) + return -1; + struct dvda_still *sel = NULL; + if (priv->page >= 0) { + sel = &tr->stills[MPMIN(priv->page, tr->num_stills - 1)]; + } else { + int64_t pos = time - tr->time; + int64_t best_onset = -1; + for (int s = 0; s < tr->num_stills; s++) { + if (tr->stills[s].onset <= pos && + tr->stills[s].onset > best_onset) + { + best_onset = tr->stills[s].onset; + sel = &tr->stills[s]; + } + } + } + return sel ? sel->pvob : -1; + } + return -1; +} + +// Extract a still's MPEG-2 video elementary stream from AUDIO_SV.VOB into the +// stream's cache. Stills tile the picture area contiguously in P_VOB order, so a +// still spans from its start sector to the next still's start. The MPEG-2 +// video (PES 0xE0) is demuxed out of the program stream and trimmed at its +// sequence_end_code to drop trailing padding. +static bool extract_still(stream_t *stream, int pvob) +{ + struct priv *priv = stream->priv; + if (pvob < 0 || pvob >= priv->num_pvobs || !priv->sv_file) + return false; + + uint32_t start = priv->pvob_sectors[pvob]; + uint32_t end = pvob + 1 < priv->num_pvobs ? priv->pvob_sectors[pvob + 1] + : priv->pvobs_end + 1; + // A still is a single MPEG-2 I-frame plus mux overhead. The sectors come + // from IFO data, so treat an absurd span as corruption. + if (end <= start || end - start > 16 * 1024 * 1024 / DVD_BLOCK_SIZE) { + MP_WARN(stream, "still pvob %d: bad sector range %"PRIu32"..%"PRIu32 + "; skipping...\n", pvob, start, end); + return false; + } + int nsec = end - start; + + uint8_t *ps = talloc_array(NULL, uint8_t, nsec * DVD_BLOCK_SIZE); + ssize_t r = DVDReadBlocks(priv->sv_file, start, nsec, ps); + if (r <= 0) { + talloc_free(ps); + return false; + } + int total = r * DVD_BLOCK_SIZE; + + // Demux the video elementary stream (PES stream id 0xE0) out of the MPEG + // program stream. + uint8_t *es = talloc_array(priv, uint8_t, total); + int es_len = 0; + for (int i = 0; i + 9 <= total; ) { + if (ps[i] || ps[i + 1] || ps[i + 2] != 1) { + i++; + continue; + } + int id = ps[i + 3]; + if (id == 0xBA) { // pack header (MPEG-2 is 14 bytes) + i += 14; + continue; + } + int len = (ps[i + 4] << 8) | ps[i + 5]; + if (id == 0xE0 && len >= 3) { // video PES packet + int hdrlen = ps[i + 8]; + int off = 3 + hdrlen; + if (off < len && i + 6 + len <= total) { + memcpy(es + es_len, &ps[i + 6 + off], len - off); + es_len += len - off; + } + } + i += 6 + len; + } + talloc_free(ps); + + // A still image ends with a sequence_end_code, drop any trailing data. + for (int j = 0; j + 4 <= es_len; j++) { + if (!es[j] && !es[j + 1] && es[j + 2] == 1 && es[j + 3] == 0xB7) { + es_len = j + 4; + break; + } + } + + talloc_free(priv->still_data); + priv->still_data = es; + priv->still_size = es_len; + priv->still_pvob = pvob; + return es_len > 0; +} + static int control(stream_t *stream, int cmd, void *arg) { struct priv *priv = stream->priv; @@ -262,6 +493,36 @@ static int control(stream_t *stream, int cmd, void *arg) case STREAM_CTRL_GET_NUM_CHAPTERS: *(unsigned int *)arg = t->num_tracks; return STREAM_OK; + case STREAM_CTRL_SET_STILL_PAGE: + mp_mutex_lock(&priv->still_lock); + priv->page = *(int *)arg; + priv->still_pvob = -1; // force re-extract of the shown still + mp_mutex_unlock(&priv->still_lock); + return STREAM_OK; + case STREAM_CTRL_GET_STILL: { + struct stream_still_req *req = arg; + if (!priv->num_pvobs) + return STREAM_UNSUPPORTED; + req->has_stills = false; + for (int i = 0; i < t->num_tracks; i++) + req->has_stills |= t->tracks[i].num_stills > 0; + mp_mutex_lock(&priv->still_lock); + req->id = current_still_pvob(stream, t, DVDA_TIME_FROM_S(req->time)); + req->data = NULL; + req->data_size = 0; + if (req->id < 0) { + mp_mutex_unlock(&priv->still_lock); + return STREAM_OK; + } + if (req->id != priv->still_pvob || !priv->still_data) + extract_still(stream, req->id); + if (priv->still_pvob == req->id && priv->still_data) { + req->data = priv->still_data; + req->data_size = priv->still_size; + } + mp_mutex_unlock(&priv->still_lock); + return STREAM_OK; + } case STREAM_CTRL_GET_CHAPTER_TIME: { double *ch = arg; int chapter = *ch; @@ -313,8 +574,11 @@ static int control(stream_t *stream, int cmd, void *arg) static void stream_dvda_close(stream_t *stream) { struct priv *priv = stream->priv; + mp_mutex_destroy(&priv->still_lock); if (priv->file) DVDCloseFile(priv->file); + if (priv->sv_file) + DVDCloseFile(priv->sv_file); if (priv->dvd) DVDClose(priv->dvd); } @@ -341,7 +605,10 @@ static int open_s_internal(stream_t *stream) struct priv *priv = stream->priv; char *filename; + mp_mutex_init(&priv->still_lock); + priv->opts = mp_get_config_group(stream, stream->global, &dvda_conf); + priv->page = priv->opts->page; if (priv->device && priv->device[0]) { filename = priv->device;