diff options
-rw-r--r-- | DOCS/edl-mpv.rst | 43 | ||||
-rw-r--r-- | demux/demux.h | 2 | ||||
-rw-r--r-- | demux/demux_edl.c | 138 | ||||
-rw-r--r-- | demux/demux_lavf.c | 45 | ||||
-rw-r--r-- | demux/demux_timeline.c | 98 | ||||
-rw-r--r-- | demux/timeline.c | 4 | ||||
-rw-r--r-- | demux/timeline.h | 4 | ||||
-rw-r--r-- | player/lua/ytdl_hook.lua | 12 |
8 files changed, 276 insertions, 70 deletions
diff --git a/DOCS/edl-mpv.rst b/DOCS/edl-mpv.rst index b0f7238307..341d5ffa50 100644 --- a/DOCS/edl-mpv.rst +++ b/DOCS/edl-mpv.rst @@ -51,7 +51,8 @@ The rest of the lines belong to one of these classes: 1) An empty or commented line. A comment starts with ``#``, which must be the first character in the line. The rest of the line (up until the next line break) is ignored. An empty line has 0 bytes between two line feed bytes. -2) A segment entry in all other cases. +2) A header entry if the line starts with ``!``. +3) A segment entry in all other cases. Each segment entry consists of a list of named or unnamed parameters. Parameters are separated with ``,``. Named parameters consist of a name, @@ -63,8 +64,8 @@ Syntax:: segment_entry ::= <param> ( <param> ',' )* param ::= [ <name> '=' ] ( <value> | '%' <number> '%' <valuebytes> ) -The ``name`` string can consist of any characters, except ``=%,;\n``. The -``value`` string can consist of any characters except of ``,;\n``. +The ``name`` string can consist of any characters, except ``=%,;\n!``. The +``value`` string can consist of any characters except of ``,;\n!``. The construct starting with ``%`` allows defining any value with arbitrary contents inline, where ``number`` is an integer giving the number of bytes in @@ -94,6 +95,42 @@ to ``20``, ``param3`` to ``value,escaped``, ``param4`` to ``value2``. Instead of line breaks, the character ``;`` can be used. Line feed bytes and ``;`` are treated equally. +Header entries start with ``!`` as first character after a line break. Header +entries affect all other file entries in the EDL file. Their format is highly +implementation specific. They should generally follow the file header, and come +before any file entries. + +MP4 DASH +======== + +This is a header that helps implementing DASH, although it only provides a low +level mechanism. + +If this header is set, the given url designates an mp4 init fragment. It's +downloaded, and every URL in the EDL is prefixed with the init fragment on the +byte stream level. This is mostly for use by mpv's internal ytdl support. The +ytdl script will call youtube-dl, which in turn actually processes DASH +manifests. It may work only for this very specific purpose and fail to be +useful in other scenarios. It can be removed ot changed in incompatible ways +at any times. + +Example:: + + !mp4_dash,init=url + +The ``url`` is encoded as parameter value as defined in the general EDL syntax. +It's expected to point to an "initialization fragment", which will be prefixed +to every entry in the EDL on the byte stream level. + +The current implementation will + +- ignore stream start times +- use durations as hint for seeking only +- not adjust source timestamps +- open and close segments (i.e. fragments) as needed +- not add segment boundaries as chapter points +- require full compatibility between all segments (same codec etc.) + Timestamp format ================ diff --git a/demux/demux.h b/demux/demux.h index 44bd2b9766..8a1da4e400 100644 --- a/demux/demux.h +++ b/demux/demux.h @@ -163,6 +163,8 @@ struct demuxer_params { struct timeline *timeline; bool disable_timeline; bool initial_readahead; + bstr init_fragment; + bool skip_lavf_probing; // -- demux_open_url() only int stream_flags; bool disable_cache; diff --git a/demux/demux_edl.c b/demux/demux_edl.c index 65a18a1a41..fad73f7644 100644 --- a/demux/demux_edl.c +++ b/demux/demux_edl.c @@ -44,6 +44,8 @@ struct tl_part { }; struct tl_parts { + bool dash; + char *init_fragment_url; struct tl_part *parts; int num_parts; }; @@ -65,6 +67,8 @@ static bool parse_time(bstr str, double *out_time) return true; } +#define MAX_PARAMS 10 + /* Returns a list of parts, or NULL on parse error. * Syntax (without file header or URI prefix): * url ::= <entry> ( (';' | '\n') <entry> )* @@ -79,7 +83,10 @@ static struct tl_parts *parse_edl(bstr str) bstr_split_tok(str, "\n", &(bstr){0}, &str); if (bstr_eatstart0(&str, "\n") || bstr_eatstart0(&str, ";")) continue; + bool is_header = bstr_eatstart0(&str, "!"); struct tl_part p = { .length = -1 }; + bstr param_names[MAX_PARAMS]; + bstr param_vals[MAX_PARAMS]; int nparam = 0; while (1) { bstr name, val; @@ -117,10 +124,25 @@ static struct tl_parts *parse_edl(bstr str) if (bstr_equals0(val, "chapters")) p.chapter_ts = true; } + if (nparam >= MAX_PARAMS) + goto error; + param_names[nparam] = name; + param_vals[nparam] = val; nparam++; if (!bstr_eatstart0(&str, ",")) break; } + if (is_header) { + if (tl->num_parts) + goto error; // can't have header once an entry was defined + bstr type = param_vals[0]; // value, because no "=" + if (bstr_equals0(type, "mp4_dash")) { + tl->dash = true; + if (bstr_equals0(param_names[1], "init")) + tl->init_fragment_url = bstrto0(tl, param_vals[1]); + } + continue; + } if (!p.filename) goto error; MP_TARRAY_APPEND(tl, tl->parts, tl->num_parts, p); @@ -140,7 +162,10 @@ static struct demuxer *open_source(struct timeline *tl, char *filename) if (strcmp(d->stream->url, filename) == 0) return d; } - struct demuxer *d = demux_open_url(filename, NULL, tl->cancel, tl->global); + struct demuxer_params params = { + .init_fragment = tl->init_fragment, + }; + struct demuxer *d = demux_open_url(filename, ¶ms, tl->cancel, tl->global); if (d) { MP_TARRAY_APPEND(tl, tl->sources, tl->num_sources, d); } else { @@ -203,63 +228,104 @@ static void resolve_timestamps(struct tl_part *part, struct demuxer *demuxer) static void build_timeline(struct timeline *tl, struct tl_parts *parts) { + tl->track_layout = NULL; + tl->dash = parts->dash; + + if (parts->init_fragment_url && parts->init_fragment_url[0]) { + MP_VERBOSE(tl, "Opening init fragment...\n"); + stream_t *s = stream_create(parts->init_fragment_url, STREAM_READ, + tl->cancel, tl->global); + if (s) + tl->init_fragment = stream_read_complete(s, tl, 1000000); + free_stream(s); + if (!tl->init_fragment.len) { + MP_ERR(tl, "Could not read init fragment.\n"); + goto error; + } + s = open_memory_stream(tl->init_fragment.start, tl->init_fragment.len); + tl->track_layout = demux_open(s, NULL, tl->global); + if (!tl->track_layout) { + free_stream(s); + MP_ERR(tl, "Could not demux init fragment.\n"); + goto error; + } + } + tl->parts = talloc_array_ptrtype(tl, tl->parts, parts->num_parts + 1); double starttime = 0; for (int n = 0; n < parts->num_parts; n++) { struct tl_part *part = &parts->parts[n]; - struct demuxer *source = open_source(tl, part->filename); - if (!source) - goto error; + struct demuxer *source = NULL; - resolve_timestamps(part, source); + if (tl->dash) { + part->offset = starttime; + if (part->length <= 0) + MP_WARN(tl, "Segment %d has unknown duration.\n", n); + if (part->offset_set) + MP_WARN(tl, "Offsets are ignored.\n"); + tl->demuxer->is_network = true; + } else { + MP_VERBOSE(tl, "Opening segment %d...\n", n); - double end_time = source_get_length(source); - if (end_time >= 0) - end_time += source->start_time; + source = open_source(tl, part->filename); + if (!source) + goto error; - // Unknown length => use rest of the file. If duration is unknown, make - // something up. - if (part->length < 0) { - if (end_time < 0) { - MP_WARN(tl, "EDL: source file '%s' has unknown duration.\n", - part->filename); - end_time = 1; - } - part->length = end_time - part->offset; - } else if (end_time >= 0) { - double end_part = part->offset + part->length; - if (end_part > end_time) { - MP_WARN(tl, "EDL: entry %d uses %f " - "seconds, but file has only %f seconds.\n", - n, end_part, end_time); + resolve_timestamps(part, source); + + double end_time = source_get_length(source); + if (end_time >= 0) + end_time += source->start_time; + + // Unknown length => use rest of the file. If duration is unknown, make + // something up. + if (part->length < 0) { + if (end_time < 0) { + MP_WARN(tl, "EDL: source file '%s' has unknown duration.\n", + part->filename); + end_time = 1; + } + part->length = end_time - part->offset; + } else if (end_time >= 0) { + double end_part = part->offset + part->length; + if (end_part > end_time) { + MP_WARN(tl, "EDL: entry %d uses %f " + "seconds, but file has only %f seconds.\n", + n, end_part, end_time); + } } - } - // Add a chapter between each file. - struct demux_chapter ch = { - .pts = starttime, - .metadata = talloc_zero(tl, struct mp_tags), - }; - mp_tags_set_str(ch.metadata, "title", part->filename); - MP_TARRAY_APPEND(tl, tl->chapters, tl->num_chapters, ch); + // Add a chapter between each file. + struct demux_chapter ch = { + .pts = starttime, + .metadata = talloc_zero(tl, struct mp_tags), + }; + mp_tags_set_str(ch.metadata, "title", part->filename); + MP_TARRAY_APPEND(tl, tl->chapters, tl->num_chapters, ch); - // Also copy the source file's chapters for the relevant parts - copy_chapters(&tl->chapters, &tl->num_chapters, source, part->offset, - part->length, starttime); + // Also copy the source file's chapters for the relevant parts + copy_chapters(&tl->chapters, &tl->num_chapters, source, part->offset, + part->length, starttime); + } tl->parts[n] = (struct timeline_part) { .start = starttime, .source_start = part->offset, .source = source, + .url = talloc_strdup(tl, part->filename), }; starttime += part->length; - tl->demuxer->is_network |= source->is_network; + if (source) { + tl->demuxer->is_network |= source->is_network; + + if (!tl->track_layout) + tl->track_layout = source; + } } tl->parts[parts->num_parts] = (struct timeline_part) {.start = starttime}; tl->num_parts = parts->num_parts; - tl->track_layout = tl->parts[0].source; return; error: diff --git a/demux/demux_lavf.c b/demux/demux_lavf.c index 50391a29da..1cfbdb16cd 100644 --- a/demux/demux_lavf.c +++ b/demux/demux_lavf.c @@ -183,6 +183,8 @@ typedef struct lavf_priv { AVInputFormat *avif; int avif_flags; AVFormatContext *avfc; + bstr init_fragment; + int64_t stream_pos; AVIOContext *pb; struct sh_stream **streams; // NULL for unknown streams int num_streams; @@ -218,7 +220,14 @@ static int mp_read(void *opaque, uint8_t *buf, int size) struct stream *stream = priv->stream; int ret; - ret = stream_read(stream, buf, size); + if (priv->stream_pos < priv->init_fragment.len) { + ret = MPMIN(size, priv->init_fragment.len - priv->stream_pos); + memcpy(buf, priv->init_fragment.start + priv->stream_pos, ret); + priv->stream_pos += ret; + } else { + ret = stream_read(stream, buf, size); + priv->stream_pos = priv->init_fragment.len + stream_tell(stream); + } MP_TRACE(demuxer, "%d=mp_read(%p, %p, %d), pos: %"PRId64", eof:%d\n", ret, stream, buf, size, stream_tell(stream), stream->eof); @@ -230,32 +239,44 @@ static int64_t mp_seek(void *opaque, int64_t pos, int whence) struct demuxer *demuxer = opaque; lavf_priv_t *priv = demuxer->priv; struct stream *stream = priv->stream; - int64_t current_pos; + MP_TRACE(demuxer, "mp_seek(%p, %"PRId64", %s)\n", stream, pos, whence == SEEK_END ? "end" : whence == SEEK_CUR ? "cur" : whence == SEEK_SET ? "set" : "size"); if (whence == SEEK_END || whence == AVSEEK_SIZE) { - int64_t end = stream_get_size(stream); + int64_t end = stream_get_size(stream) + priv->init_fragment.len; if (end < 0) return -1; if (whence == AVSEEK_SIZE) return end; pos += end; } else if (whence == SEEK_CUR) { - pos += stream_tell(stream); + pos += priv->stream_pos; } else if (whence != SEEK_SET) { return -1; } if (pos < 0) return -1; - current_pos = stream_tell(stream); - if (stream_seek(stream, pos) == 0) { + + int64_t stream_target = pos - priv->init_fragment.len; + bool seek_before = stream_target < 0; + if (seek_before) + stream_target = 0; // within init segment - seek real stream to 0 + + int64_t current_pos = stream_tell(stream); + if (stream_seek(stream, stream_target) == 0) { stream_seek(stream, current_pos); return -1; } + if (seek_before) { + priv->stream_pos = pos; + } else { + priv->stream_pos = priv->init_fragment.len + stream_tell(stream); + } + return pos; } @@ -771,6 +792,9 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check) if (lavf_check_file(demuxer, check) < 0) return -1; + if (demuxer->params) + priv->init_fragment = bstrdup(priv, demuxer->params->init_fragment); + avfc = avformat_alloc_context(); if (!avfc) return -1; @@ -864,9 +888,12 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check) av_dict_free(&dopts); priv->avfc = avfc; - if (avformat_find_stream_info(avfc, NULL) < 0) { - MP_ERR(demuxer, "av_find_stream_info() failed\n"); - return -1; + + if (!demuxer->params || !demuxer->params->skip_lavf_probing) { + if (avformat_find_stream_info(avfc, NULL) < 0) { + MP_ERR(demuxer, "av_find_stream_info() failed\n"); + return -1; + } } MP_VERBOSE(demuxer, "avformat_find_stream_info() finished after %"PRId64 diff --git a/demux/demux_timeline.c b/demux/demux_timeline.c index c640965809..4b3eb93d0d 100644 --- a/demux/demux_timeline.c +++ b/demux/demux_timeline.c @@ -24,11 +24,14 @@ #include "demux.h" #include "timeline.h" #include "stheader.h" +#include "stream/stream.h" struct segment { int index; double start, end; double d_start; + char *url; + bool lazy; struct demuxer *d; // stream_map[sh_stream.index] = index into priv.streams, where sh_stream // is a stream from the source d. It's used to map the streams of the @@ -51,6 +54,7 @@ struct priv { struct timeline *tl; double duration; + bool dash; struct segment **segments; int num_segments; @@ -79,6 +83,9 @@ static void associate_streams(struct demuxer *demuxer, struct segment *seg) { struct priv *p = demuxer->priv; + if (!seg->d || seg->stream_map) + return; + int counts[STREAM_TYPE_COUNT] = {0}; int num_streams = demux_get_num_stream(seg->d); @@ -118,6 +125,9 @@ static void reselect_streams(struct demuxer *demuxer) for (int n = 0; n < p->num_segments; n++) { struct segment *seg = p->segments[n]; for (int i = 0; i < seg->num_stream_map; i++) { + if (!seg->d) + continue; + struct sh_stream *sh = demux_get_stream(seg->d, i); bool selected = false; if (seg->stream_map[i] >= 0) @@ -130,8 +140,42 @@ static void reselect_streams(struct demuxer *demuxer) } } +static void close_lazy_segments(struct demuxer *demuxer) +{ + struct priv *p = demuxer->priv; + + // unload previous segment + for (int n = 0; n < p->num_segments; n++) { + struct segment *seg = p->segments[n]; + if (seg != p->current && seg->d && seg->lazy) { + free_demuxer_and_stream(seg->d); + seg->d = NULL; + } + } +} + +static void reopen_lazy_segments(struct demuxer *demuxer) +{ + struct priv *p = demuxer->priv; + + if (p->current->d) + return; + + close_lazy_segments(demuxer); + + struct demuxer_params params = { + .init_fragment = p->tl->init_fragment, + .skip_lavf_probing = true, + }; + p->current->d = demux_open_url(p->current->url, ¶ms, + demuxer->stream->cancel, demuxer->global); + if (!p->current->d) + MP_ERR(demuxer, "failed to load segment\n"); + associate_streams(demuxer, p->current); +} + static void switch_segment(struct demuxer *demuxer, struct segment *new, - double start_pts, int flags) + double start_pts, int flags, bool init) { struct priv *p = demuxer->priv; @@ -141,9 +185,14 @@ static void switch_segment(struct demuxer *demuxer, struct segment *new, MP_VERBOSE(demuxer, "switch to segment %d\n", new->index); p->current = new; + reopen_lazy_segments(demuxer); + if (!new->d) + return; reselect_streams(demuxer); - demux_set_ts_offset(new->d, new->start - new->d_start); - demux_seek(new->d, start_pts, flags); + if (!p->dash) + demux_set_ts_offset(new->d, new->start - new->d_start); + if (!p->dash || !init) + demux_seek(new->d, start_pts, flags); for (int n = 0; n < p->num_streams; n++) { struct virtual_stream *vs = &p->streams[n]; @@ -170,7 +219,7 @@ static void d_seek(struct demuxer *demuxer, double seek_pts, int flags) } } - switch_segment(demuxer, new, pts, flags); + switch_segment(demuxer, new, pts, flags, false); } static int d_fill_buffer(struct demuxer *demuxer) @@ -178,9 +227,11 @@ static int d_fill_buffer(struct demuxer *demuxer) struct priv *p = demuxer->priv; if (!p->current) - switch_segment(demuxer, p->segments[0], 0, 0); + switch_segment(demuxer, p->segments[0], 0, 0, true); struct segment *seg = p->current; + if (!seg || !seg->d) + return 0; struct demux_packet *pkt = demux_read_any_packet(seg->d); if (!pkt || pkt->pts >= seg->end) @@ -217,20 +268,21 @@ static int d_fill_buffer(struct demuxer *demuxer) } if (!next) return 0; - switch_segment(demuxer, next, next->start, 0); + switch_segment(demuxer, next, next->start, 0, true); return 1; // reader will retry } if (pkt->stream < 0 || pkt->stream > seg->num_stream_map) goto drop; - if (!pkt->codec) - pkt->codec = demux_get_stream(seg->d, pkt->stream)->codec; - - if (pkt->start == MP_NOPTS_VALUE || pkt->start < seg->start) - pkt->start = seg->start; - if (pkt->end == MP_NOPTS_VALUE || pkt->end > seg->end) - pkt->end = seg->end; + if (!p->dash) { + if (!pkt->codec) + pkt->codec = demux_get_stream(seg->d, pkt->stream)->codec; + if (pkt->start == MP_NOPTS_VALUE || pkt->start < seg->start) + pkt->start = seg->start; + if (pkt->end == MP_NOPTS_VALUE || pkt->end > seg->end) + pkt->end = seg->end; + } pkt->stream = seg->stream_map[pkt->stream]; if (pkt->stream < 0) @@ -255,7 +307,8 @@ static int d_fill_buffer(struct demuxer *demuxer) } } - pkt->new_segment |= vs->new_segment; + if (!p->dash) + pkt->new_segment |= vs->new_segment; vs->new_segment = false; demux_add_packet(vs->sh, pkt); @@ -273,9 +326,9 @@ static void print_timeline(struct demuxer *demuxer) MP_VERBOSE(demuxer, "Timeline segments:\n"); for (int n = 0; n < p->num_segments; n++) { struct segment *seg = p->segments[n]; - int src_num = -1; - for (int i = 0; i < p->tl->num_sources; i++) { - if (p->tl->sources[i] == seg->d) { + int src_num = n; + for (int i = 0; i < n - 1; i++) { + if (seg->d && p->segments[i]->d == seg->d) { src_num = i; break; } @@ -283,9 +336,12 @@ static void print_timeline(struct demuxer *demuxer) MP_VERBOSE(demuxer, " %2d: %12f [%12f] (", n, seg->start, seg->d_start); for (int i = 0; i < seg->num_stream_map; i++) MP_VERBOSE(demuxer, "%s%d", i ? " " : "", seg->stream_map[i]); - MP_VERBOSE(demuxer, ") %d:'%s'\n", src_num, seg->d->filename); + MP_VERBOSE(demuxer, ") %d:'%s'\n", src_num, seg->url); } MP_VERBOSE(demuxer, "Total duration: %f\n", p->duration); + + if (p->dash) + MP_VERBOSE(demuxer, "Durations and offsets are non-authoritative.\n"); } static int d_open(struct demuxer *demuxer, enum demux_check check) @@ -334,6 +390,8 @@ static int d_open(struct demuxer *demuxer, enum demux_check check) struct segment *seg = talloc_ptrtype(p, seg); *seg = (struct segment){ .d = part->source, + .url = part->source ? part->source->filename : part->url, + .lazy = !part->source, .d_start = part->source_start, .start = part->start, .end = next->start, @@ -345,6 +403,8 @@ static int d_open(struct demuxer *demuxer, enum demux_check check) MP_TARRAY_APPEND(p, p->segments, p->num_segments, seg); } + p->dash = p->tl->dash; + print_timeline(demuxer); demuxer->seekable = true; @@ -363,6 +423,8 @@ static void d_close(struct demuxer *demuxer) { struct priv *p = demuxer->priv; struct demuxer *master = p->tl->demuxer; + p->current = NULL; + close_lazy_segments(demuxer); timeline_destroy(p->tl); free_demuxer(master); } diff --git a/demux/timeline.c b/demux/timeline.c index 73f3ab79a2..700a6dfd05 100644 --- a/demux/timeline.c +++ b/demux/timeline.c @@ -33,8 +33,10 @@ void timeline_destroy(struct timeline *tl) return; for (int n = 0; n < tl->num_sources; n++) { struct demuxer *d = tl->sources[n]; - if (d != tl->demuxer) + if (d != tl->demuxer && d != tl->track_layout) free_demuxer_and_stream(d); } + if (tl->track_layout && tl->track_layout != tl->demuxer) + free_demuxer_and_stream(tl->track_layout); talloc_free(tl); } diff --git a/demux/timeline.h b/demux/timeline.h index edc6a2f7ae..21a6602ba8 100644 --- a/demux/timeline.h +++ b/demux/timeline.h @@ -4,6 +4,7 @@ struct timeline_part { double start; double source_start; + char *url; struct demuxer *source; }; @@ -15,6 +16,9 @@ struct timeline { // main source struct demuxer *demuxer; + bstr init_fragment; + bool dash; + // All referenced files. The source file must be at sources[0]. struct demuxer **sources; int num_sources; diff --git a/player/lua/ytdl_hook.lua b/player/lua/ytdl_hook.lua index 7349e579e3..fba5100822 100644 --- a/player/lua/ytdl_hook.lua +++ b/player/lua/ytdl_hook.lua @@ -90,11 +90,17 @@ end local function edl_track_joined(fragments) local edl = "edl://" - for i = 1, #fragments do + local offset = 1 + if fragments[1] and not fragments[1].duration then + -- if no duration, probably initialization segment + edl = edl .. "!mp4_dash,init=" .. edl_escape(fragments[1].url) + offset = 2 + end + for i = offset, #fragments do local fragment = fragments[i] - edl = edl .. edl_escape(fragment.url) if fragment.duration then - edl = edl .. ",length=" .. fragment.duration + edl = edl .. edl_escape(fragment.url) + edl = edl..",length="..fragment.duration end edl = edl .. ";" end |