mirror of
https://github.com/mpv-player/mpv
synced 2025-01-02 21:12:23 +00:00
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:
parent
97680bf604
commit
61202bb364
@ -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
|
||||
================
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,11 +228,46 @@ 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);
|
||||
struct demuxer *source = NULL;
|
||||
|
||||
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);
|
||||
|
||||
source = open_source(tl, part->filename);
|
||||
if (!source)
|
||||
goto error;
|
||||
|
||||
@ -246,20 +306,26 @@ static void build_timeline(struct timeline *tl, struct tl_parts *parts)
|
||||
// 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;
|
||||
|
||||
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:
|
||||
|
@ -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;
|
||||
|
||||
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,10 +888,13 @@ static int demux_open_lavf(demuxer_t *demuxer, enum demux_check check)
|
||||
av_dict_free(&dopts);
|
||||
|
||||
priv->avfc = avfc;
|
||||
|
||||
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
|
||||
" bytes.\n", stream_tell(priv->stream));
|
||||
|
@ -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,8 +185,13 @@ 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);
|
||||
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++) {
|
||||
@ -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 (!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,6 +307,7 @@ static int d_fill_buffer(struct demuxer *demuxer)
|
||||
}
|
||||
}
|
||||
|
||||
if (!p->dash)
|
||||
pkt->new_segment |= vs->new_segment;
|
||||
vs->new_segment = false;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user