diff --git a/DOCS/edl-mpv.rst b/DOCS/edl-mpv.rst index 0563c28b0c..cfec52c1bc 100644 --- a/DOCS/edl-mpv.rst +++ b/DOCS/edl-mpv.rst @@ -131,6 +131,33 @@ The current implementation will - not add segment boundaries as chapter points - require full compatibility between all segments (same codec etc.) +Separate files for tracks +========================= + +The special ``new_stream`` header lets you specify separate parts and time +offsets for separate tracks. This can for example be used to source audio and +video track from separate files. + +Example:: + + # mpv EDL v0 + video.mkv + !new_stream + audio.mkv + +This adds all tracks from both files to the virtual track list. Upon playback, +the tracks will be played at the same time, instead of appending them. The files +can contain more than 1 stream; the apparent effect is the same as if the second +part after the ``!new_stream`` part were in a separate ``.edl`` file and added +with ``--external-file``. + +Note that all metadata between the stream sets created by ``new_stream`` is +disjoint. Global metadata is taken from the first part only. + +In context of mpv, this is redundant to the ``--audio-file`` and +``--external-file`` options, but (as of this writing) has the advantage that +this will use a unified cache for all streams. + Timestamp format ================ diff --git a/demux/demux_edl.c b/demux/demux_edl.c index c0225737e1..52b36ebfc9 100644 --- a/demux/demux_edl.c +++ b/demux/demux_edl.c @@ -49,6 +49,7 @@ struct tl_parts { char *init_fragment_url; struct tl_part *parts; int num_parts; + struct tl_parts *next; }; struct priv { @@ -79,6 +80,7 @@ static bool parse_time(bstr str, double *out_time) static struct tl_parts *parse_edl(bstr str) { struct tl_parts *tl = talloc_zero(NULL, struct tl_parts); + struct tl_parts *root = tl; while (str.len) { if (bstr_eatstart0(&str, "#")) { bstr_split_tok(str, "\n", &(bstr){0}, &str); @@ -138,13 +140,15 @@ static struct tl_parts *parse_edl(bstr 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 (nparam > 1 && bstr_equals0(param_names[1], "init")) tl->init_fragment_url = bstrto0(tl, param_vals[1]); + } else if (bstr_equals0(type, "new_stream")) { + struct tl_parts *ntl = talloc_zero(tl, struct tl_parts); + tl->next = ntl; + tl = ntl; } continue; } @@ -152,11 +156,11 @@ static struct tl_parts *parse_edl(bstr str) goto error; MP_TARRAY_APPEND(tl, tl->parts, tl->num_parts, p); } - if (!tl->num_parts) + if (!root->num_parts) goto error; - return tl; + return root; error: - talloc_free(tl); + talloc_free(root); return NULL; } @@ -260,11 +264,12 @@ static void build_timeline(struct timeline *tl, struct tl_parts *parts) 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; + if (tl->demuxer) + tl->demuxer->is_network = true; if (!tl->track_layout) { - source = open_source(tl, part->filename); - if (!source) + tl->track_layout = open_source(tl, part->filename); + if (!tl->track_layout) goto error; } } else { @@ -320,12 +325,8 @@ static void build_timeline(struct timeline *tl, struct tl_parts *parts) starttime += part->length; - if (source) { - tl->demuxer->is_network |= source->is_network; - - if (!tl->track_layout) - tl->track_layout = source; - } + if (source && !tl->track_layout) + tl->track_layout = source; } tl->parts[parts->num_parts] = (struct timeline_part) {.start = starttime}; tl->num_parts = parts->num_parts; @@ -352,16 +353,32 @@ static void build_mpv_edl_timeline(struct timeline *tl) { struct priv *p = tl->demuxer->priv; - struct tl_parts *parts = parse_edl(p->data); - if (!parts) { + struct timeline *root_tl = tl; + struct tl_parts *root = parse_edl(p->data); + if (!root) { MP_ERR(tl, "Error in EDL.\n"); return; } - MP_TARRAY_APPEND(tl, tl->sources, tl->num_sources, tl->demuxer); - if (!p->allow_any) - fix_filenames(parts, tl->demuxer->filename); - build_timeline(tl, parts); - talloc_free(parts); + + for (struct tl_parts *parts = root; parts; parts = parts->next) { + if (tl->demuxer) + MP_TARRAY_APPEND(tl, tl->sources, tl->num_sources, tl->demuxer); + if (!p->allow_any) + fix_filenames(parts, root_tl->demuxer->filename); + build_timeline(tl, parts); + + if (parts->next) { + struct timeline *ntl = talloc_zero(tl, struct timeline); + *ntl = (struct timeline) { + .global = tl->global, + .log = tl->log, + .cancel = tl->cancel, + }; + tl->next = ntl; + tl = ntl; + } + } + talloc_free(root); } static int try_open_file(struct demuxer *demuxer, enum demux_check check) diff --git a/demux/demux_timeline.c b/demux/demux_timeline.c index 49046222d9..3c5304c6dc 100644 --- a/demux/demux_timeline.c +++ b/demux/demux_timeline.c @@ -27,17 +27,18 @@ #include "stream/stream.h" struct segment { - int index; + int index; // index into virtual_source.segments[] (and timeline.parts[]) 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 + // stream_map[sh_stream.index] = virtual_stream, where sh_stream is a stream + // from the source d, and virtual_stream is a streamexported by the + // timeline demuxer (virtual_stream.sh). It's used to map the streams of the // source onto the set of streams of the virtual timeline. - // Uses -1 for streams that do not appear in the virtual timeline. - int *stream_map; + // Uses NULL for streams that do not appear in the virtual timeline. + struct virtual_stream **stream_map; int num_stream_map; }; @@ -47,68 +48,87 @@ struct virtual_stream { struct sh_stream *sh; // stream exported by demux_timeline bool selected; // ==demux_stream_is_selected(sh) int eos_packets; // deal with b-frame delay + struct virtual_source *src; // group this stream is part of }; -struct priv { +// This represents a single timeline source. (See timeline.next. For each +// timeline struct there is a virtual_source.) +struct virtual_source { struct timeline *tl; - double duration; bool dash; struct segment **segments; int num_segments; struct segment *current; - // As the demuxer user sees it. struct virtual_stream **streams; int num_streams; // Total number of packets received past end of segment. Used // to be clever about determining when to switch segments. int eos_packets; + + bool eof_reached; + double dts; // highest read DTS (or PTS if no DTS available) + bool any_selected; // at least one stream is actually selected }; -static bool target_stream_used(struct segment *seg, int target_index) +struct priv { + struct timeline *tl; + + double duration; + + // As the demuxer user sees it. + struct virtual_stream **streams; + int num_streams; + + struct virtual_source **sources; + int num_sources; +}; + +static void add_tl(struct demuxer *demuxer, struct timeline *tl); + +static bool target_stream_used(struct segment *seg, struct virtual_stream *vs) { for (int n = 0; n < seg->num_stream_map; n++) { - if (seg->stream_map[n] == target_index) + if (seg->stream_map[n] == vs) return true; } return false; } // Create mapping from segment streams to virtual timeline streams. -static void associate_streams(struct demuxer *demuxer, struct segment *seg) +static void associate_streams(struct demuxer *demuxer, + struct virtual_source *src, + 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); for (int n = 0; n < num_streams; n++) { struct sh_stream *sh = demux_get_stream(seg->d, n); - // Try associating by demuxer ID (supposedly useful for ordered chapters). - struct sh_stream *other = - demuxer_stream_by_demuxer_id(demuxer, sh->type, sh->demuxer_id); - if (!other || !target_stream_used(seg, other->index)) { - // Try to associate the first unused stream with matching media type. - for (int i = 0; i < p->num_streams; i++) { - struct sh_stream *cur = p->streams[i]->sh; - if (cur->type == sh->type && !target_stream_used(seg, cur->index)) - { - other = cur; - break; - } - } + struct virtual_stream *other = NULL; + for (int i = 0; i < src->num_streams; i++) { + struct virtual_stream *vs = src->streams[i]; + + // The stream must always have the same media type. Also, a stream + // can't be assigned multiple times. + if (sh->type != vs->sh->type || target_stream_used(seg, vs)) + continue; + + // By default pick the first matching stream. + if (!other) + other = vs; + + // Matching by demuxer ID is supposedly useful and preferable for + // ordered chapters. + if (sh->demuxer_id == vs->sh->demuxer_id) + other = vs; } - MP_TARRAY_APPEND(seg, seg->stream_map, seg->num_stream_map, - other ? other->index : -1); - - counts[sh->type] += 1; + MP_TARRAY_APPEND(seg, seg->stream_map, seg->num_stream_map, other); } } @@ -121,86 +141,100 @@ static void reselect_streams(struct demuxer *demuxer) vs->selected = demux_stream_is_selected(vs->sh); } - 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++) { + for (int x = 0; x < p->num_sources; x++) { + struct virtual_source *src = p->sources[x]; + + for (int n = 0; n < src->num_segments; n++) { + struct segment *seg = src->segments[n]; + if (!seg->d) continue; - struct sh_stream *sh = demux_get_stream(seg->d, i); - bool selected = false; - if (seg->stream_map[i] >= 0) - selected = p->streams[seg->stream_map[i]]->selected; - // This stops demuxer readahead for inactive segments. - if (!p->current || seg->d != p->current->d) - selected = false; - demuxer_select_track(seg->d, sh, MP_NOPTS_VALUE, selected); + for (int i = 0; i < seg->num_stream_map; i++) { + bool selected = + seg->stream_map[i] && seg->stream_map[i]->selected; + + // This stops demuxer readahead for inactive segments. + if (!src->current || seg->d != src->current->d) + selected = false; + struct sh_stream *sh = demux_get_stream(seg->d, i); + demuxer_select_track(seg->d, sh, MP_NOPTS_VALUE, selected); + } + } + + bool was_selected = src->any_selected; + src->any_selected = false; + + for (int n = 0; n < src->num_streams; n++) + src->any_selected |= src->streams[n]->selected; + + if (!was_selected && src->any_selected) { + src->eof_reached = false; + src->dts = MP_NOPTS_VALUE; } } } -static void close_lazy_segments(struct demuxer *demuxer) +static void close_lazy_segments(struct demuxer *demuxer, + struct virtual_source *src) { - 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) { + for (int n = 0; n < src->num_segments; n++) { + struct segment *seg = src->segments[n]; + if (seg != src->current && seg->d && seg->lazy) { demux_free(seg->d); seg->d = NULL; } } } -static void reopen_lazy_segments(struct demuxer *demuxer) +static void reopen_lazy_segments(struct demuxer *demuxer, + struct virtual_source *src) { - struct priv *p = demuxer->priv; - - if (p->current->d) + if (src->current->d) return; - close_lazy_segments(demuxer); + close_lazy_segments(demuxer, src); struct demuxer_params params = { - .init_fragment = p->tl->init_fragment, + .init_fragment = src->tl->init_fragment, .skip_lavf_probing = true, }; - p->current->d = demux_open_url(p->current->url, ¶ms, - demuxer->cancel, demuxer->global); - if (!p->current->d && !demux_cancel_test(demuxer)) + src->current->d = demux_open_url(src->current->url, ¶ms, + demuxer->cancel, demuxer->global); + if (!src->current->d && !demux_cancel_test(demuxer)) MP_ERR(demuxer, "failed to load segment\n"); - if (p->current->d) - demux_disable_cache(p->current->d); - associate_streams(demuxer, p->current); + if (src->current->d) + demux_disable_cache(src->current->d); + associate_streams(demuxer, src, src->current); } -static void switch_segment(struct demuxer *demuxer, struct segment *new, - double start_pts, int flags, bool init) +static void switch_segment(struct demuxer *demuxer, struct virtual_source *src, + struct segment *new, double start_pts, int flags, + bool init) { - struct priv *p = demuxer->priv; - if (!(flags & SEEK_FORWARD)) flags |= SEEK_HR; MP_VERBOSE(demuxer, "switch to segment %d\n", new->index); - p->current = new; - reopen_lazy_segments(demuxer); + src->current = new; + reopen_lazy_segments(demuxer, src); if (!new->d) return; reselect_streams(demuxer); - if (!p->dash) + if (!src->dash) demux_set_ts_offset(new->d, new->start - new->d_start); - if (!p->dash || !init) + if (!src->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]; + for (int n = 0; n < src->num_streams; n++) { + struct virtual_stream *vs = src->streams[n]; vs->eos_packets = 0; } - p->eos_packets = 0; + src->eof_reached = false; + src->eos_packets = 0; } static void d_seek(struct demuxer *demuxer, double seek_pts, int flags) @@ -211,40 +245,64 @@ static void d_seek(struct demuxer *demuxer, double seek_pts, int flags) flags &= SEEK_FORWARD | SEEK_HR; - struct segment *new = p->segments[p->num_segments - 1]; - for (int n = 0; n < p->num_segments; n++) { - if (pts < p->segments[n]->end) { - new = p->segments[n]; - break; - } - } + for (int x = 0; x < p->num_sources; x++) { + struct virtual_source *src = p->sources[x]; - switch_segment(demuxer, new, pts, flags, false); + struct segment *new = src->segments[src->num_segments - 1]; + for (int n = 0; n < src->num_segments; n++) { + if (pts < src->segments[n]->end) { + new = src->segments[n]; + break; + } + } + + switch_segment(demuxer, src, new, pts, flags, false); + + src->dts = MP_NOPTS_VALUE; + } } static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt) { struct priv *p = demuxer->priv; - if (!p->current) - switch_segment(demuxer, p->segments[0], 0, 0, true); + struct virtual_source *src = NULL; - struct segment *seg = p->current; - if (!seg || !seg->d) + for (int x = 0; x < p->num_sources; x++) { + struct virtual_source *cur = p->sources[x]; + + if (!cur->any_selected || cur->eof_reached) + continue; + + if (!cur->current) + switch_segment(demuxer, cur, cur->segments[0], 0, 0, true); + + if (!cur->any_selected || !cur->current || !cur->current->d) + continue; + + if (!src || cur->dts == MP_NOPTS_VALUE || + (src->dts != MP_NOPTS_VALUE && cur->dts < src->dts)) + src = cur; + } + + if (!src) return false; + struct segment *seg = src->current; + assert(seg && seg->d); + struct demux_packet *pkt = demux_read_any_packet(seg->d); if (!pkt || pkt->pts >= seg->end) - p->eos_packets += 1; + src->eos_packets += 1; // Test for EOF. Do this here to properly run into EOF even if other // streams are disabled etc. If it somehow doesn't manage to reach the end // after demuxing a high (bit arbitrary) number of packets, assume one of // the streams went EOF early. - bool eos_reached = p->eos_packets > 0; - if (eos_reached && p->eos_packets < 100) { - for (int n = 0; n < p->num_streams; n++) { - struct virtual_stream *vs = p->streams[n]; + bool eos_reached = src->eos_packets > 0; + if (eos_reached && src->eos_packets < 100) { + for (int n = 0; n < src->num_streams; n++) { + struct virtual_stream *vs = src->streams[n]; if (vs->selected) { int max_packets = 0; if (vs->sh->type == STREAM_AUDIO) @@ -256,26 +314,30 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt } } + src->eof_reached = false; + if (eos_reached || !pkt) { talloc_free(pkt); struct segment *next = NULL; - for (int n = 0; n < p->num_segments - 1; n++) { - if (p->segments[n] == seg) { - next = p->segments[n + 1]; + for (int n = 0; n < src->num_segments - 1; n++) { + if (src->segments[n] == seg) { + next = src->segments[n + 1]; break; } } - if (!next) + if (!next) { + src->eof_reached = true; return false; - switch_segment(demuxer, next, next->start, 0, true); + } + switch_segment(demuxer, src, next, next->start, 0, true); return true; // reader will retry } if (pkt->stream < 0 || pkt->stream >= seg->num_stream_map) goto drop; - if (!p->dash) { + if (!src->dash) { pkt->segmented = true; if (!pkt->codec) pkt->codec = demux_get_stream(seg->d, pkt->stream)->codec; @@ -285,8 +347,8 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt pkt->end = seg->end; } - int vs_index = seg->stream_map[pkt->stream]; - if (vs_index < 0) + struct virtual_stream *vs = seg->stream_map[pkt->stream]; + if (!vs) goto drop; // for refresh seeks, demux.c prefers monotonically increasing packet pos @@ -294,8 +356,6 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt if (pkt->pos >= 0) pkt->pos |= (seg->index & 0x7FFFULL) << 48; - struct virtual_stream *vs = p->streams[vs_index]; - if (pkt->pts != MP_NOPTS_VALUE && pkt->pts >= seg->end) { // Trust the keyframe flag. Might not always be a good idea, but will // be sufficient at least with mkv. The problem is that this flag is @@ -308,6 +368,10 @@ static bool d_read_packet(struct demuxer *demuxer, struct demux_packet **out_pkt } } + double dts = pkt->dts != MP_NOPTS_VALUE ? pkt->dts : pkt->pts; + if (src->dts == MP_NOPTS_VALUE || (dts != MP_NOPTS_VALUE && dts > src->dts)) + src->dts = dts; + pkt->stream = vs->sh->index; *out_pkt = pkt; return true; @@ -322,24 +386,34 @@ static void print_timeline(struct demuxer *demuxer) struct priv *p = demuxer->priv; MP_VERBOSE(demuxer, "Timeline segments:\n"); - for (int n = 0; n < p->num_segments; n++) { - struct segment *seg = p->segments[n]; - 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; + for (int x = 0; x < p->num_sources; x++) { + struct virtual_source *src = p->sources[x]; + + if (x >= 1) + MP_VERBOSE(demuxer, " --- new parallel stream ---\n"); + + for (int n = 0; n < src->num_segments; n++) { + struct segment *seg = src->segments[n]; + int src_num = n; + for (int i = 0; i < n; i++) { + if (seg->d && src->segments[i]->d == seg->d) { + src_num = i; + break; + } } + MP_VERBOSE(demuxer, " %2d: %12f [%12f] (", n, seg->start, seg->d_start); + for (int i = 0; i < seg->num_stream_map; i++) { + struct virtual_stream *vs = seg->stream_map[i]; + MP_VERBOSE(demuxer, "%s%d", i ? " " : "", + vs ? vs->sh->index : -1); + } + MP_VERBOSE(demuxer, ") %d:'%s'\n", src_num, seg->url); } - 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->url); + + if (src->dash) + MP_VERBOSE(demuxer, " (Using pseudo-DASH mode.)\n"); } 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) @@ -349,8 +423,6 @@ static int d_open(struct demuxer *demuxer, enum demux_check check) if (!p->tl || p->tl->num_parts < 1) return -1; - p->duration = p->tl->parts[p->tl->num_parts].start; - demuxer->chapters = p->tl->chapters; demuxer->num_chapters = p->tl->num_chapters; @@ -361,8 +433,42 @@ static int d_open(struct demuxer *demuxer, enum demux_check check) demuxer->editions = meta->editions; demuxer->num_editions = meta->num_editions; demuxer->edition = meta->edition; + + for (struct timeline *tl = p->tl; tl; tl = tl->next) + add_tl(demuxer, tl); + demuxer->duration = p->duration; + print_timeline(demuxer); + + demuxer->seekable = true; + demuxer->partially_seekable = false; + + demuxer->filetype = talloc_asprintf(p, "edl/%s", + meta->filetype ? meta->filetype : meta->desc->name); + + reselect_streams(demuxer); + + return 0; +} + +static void add_tl(struct demuxer *demuxer, struct timeline *tl) +{ + struct priv *p = demuxer->priv; + + struct virtual_source *src = talloc_ptrtype(p, src); + *src = (struct virtual_source){ + .tl = tl, + .dash = tl->dash, + .dts = MP_NOPTS_VALUE, + }; + + MP_TARRAY_APPEND(p, p->sources, p->num_sources, src); + + p->duration = MPMAX(p->duration, tl->parts[tl->num_parts].start); + + struct demuxer *meta = tl->track_layout; + int num_streams = demux_get_num_stream(meta); for (int n = 0; n < num_streams; n++) { struct sh_stream *sh = demux_get_stream(meta, n); @@ -379,21 +485,26 @@ static int d_open(struct demuxer *demuxer, enum demux_check check) demux_add_sh_stream(demuxer, new); struct virtual_stream *vs = talloc_ptrtype(p, vs); *vs = (struct virtual_stream){ + .src = src, .sh = new, }; MP_TARRAY_APPEND(p, p->streams, p->num_streams, vs); + assert(demux_get_stream(demuxer, p->num_streams - 1) == new); + MP_TARRAY_APPEND(src, src->streams, src->num_streams, vs); } - for (int n = 0; n < p->tl->num_parts; n++) { - struct timeline_part *part = &p->tl->parts[n]; - struct timeline_part *next = &p->tl->parts[n + 1]; + for (int n = 0; n < tl->num_parts; n++) { + struct timeline_part *part = &tl->parts[n]; + struct timeline_part *next = &tl->parts[n + 1]; // demux_timeline already does caching, doing it for the sub-demuxers // would be pointless and wasteful. - if (part->source) + if (part->source) { demux_disable_cache(part->source); + demuxer->is_network |= part->source->is_network; + } - struct segment *seg = talloc_ptrtype(p, seg); + struct segment *seg = talloc_ptrtype(src, seg); *seg = (struct segment){ .d = part->source, .url = part->source ? part->source->filename : part->url, @@ -403,34 +514,27 @@ static int d_open(struct demuxer *demuxer, enum demux_check check) .end = next->start, }; - associate_streams(demuxer, seg); + associate_streams(demuxer, src, seg); seg->index = n; - MP_TARRAY_APPEND(p, p->segments, p->num_segments, seg); + MP_TARRAY_APPEND(src, src->segments, src->num_segments, seg); } - p->dash = p->tl->dash; - - print_timeline(demuxer); - - demuxer->seekable = true; - demuxer->partially_seekable = false; - - demuxer->filetype = meta->filetype ? meta->filetype : meta->desc->name; - - demuxer->is_network = p->tl->demuxer->is_network; - - reselect_streams(demuxer); - - return 0; + demuxer->is_network |= tl->track_layout->is_network; } static void d_close(struct demuxer *demuxer) { struct priv *p = demuxer->priv; + + for (int x = 0; x < p->num_sources; x++) { + struct virtual_source *src = p->sources[x]; + + src->current = NULL; + close_lazy_segments(demuxer, src); + } + struct demuxer *master = p->tl->demuxer; - p->current = NULL; - close_lazy_segments(demuxer); timeline_destroy(p->tl); demux_free(master); } diff --git a/demux/timeline.c b/demux/timeline.c index c44f67b166..86e4921280 100644 --- a/demux/timeline.c +++ b/demux/timeline.c @@ -31,6 +31,8 @@ void timeline_destroy(struct timeline *tl) { if (!tl) return; + // (Sub timeline elements may depend on allocations in the parent one.) + timeline_destroy(tl->next); for (int n = 0; n < tl->num_sources; n++) { struct demuxer *d = tl->sources[n]; if (d != tl->demuxer && d != tl->track_layout) diff --git a/demux/timeline.h b/demux/timeline.h index 21a6602ba8..69d93a4a2e 100644 --- a/demux/timeline.h +++ b/demux/timeline.h @@ -19,7 +19,7 @@ struct timeline { bstr init_fragment; bool dash; - // All referenced files. The source file must be at sources[0]. + // All referenced files. struct demuxer **sources; int num_sources; @@ -33,6 +33,12 @@ struct timeline { // Which source defines the overall track list (over the full timeline). struct demuxer *track_layout; + + // For tracks which require a separate opened demuxer, such as separate + // audio tracks. (For example, for ordered chapters this would be NULL, + // because all streams demux from the same file at a given time, while + // for DASH-style video+audio, each track would have its own timeline.) + struct timeline *next; }; struct timeline *timeline_load(struct mpv_global *global, struct mp_log *log,