ytdl_hook, edl: implement pseudo-DASH support

We use the metadata provided by youtube-dl to sort-of implement
fragmented DASH streaming.

This is all a bit hacky, but hopefully a makeshift solution until
libavformat has proper mechanisms. (Although in danger of being one
of those temporary hacks that become permanent.)
This commit is contained in:
wm4 2017-01-30 19:38:43 +01:00
parent 97680bf604
commit 61202bb364
8 changed files with 277 additions and 71 deletions

View File

@ -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
================

View File

@ -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;

View File

@ -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, &params, 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);
// Also copy the source file's chapters for the relevant parts
copy_chapters(&tl->chapters, &tl->num_chapters, source, part->offset,
part->length, starttime);
}
// 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);
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:

View File

@ -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

View File

@ -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, &params,
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);
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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