mirror of https://github.com/mpv-player/mpv
637 lines
22 KiB
C
637 lines
22 KiB
C
/*
|
|
* Original author: Uoti Urpala
|
|
*
|
|
* This file is part of mpv.
|
|
*
|
|
* mpv is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* mpv is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <inttypes.h>
|
|
#include <limits.h>
|
|
#include <math.h>
|
|
|
|
#include "mpv_talloc.h"
|
|
|
|
#include "demux.h"
|
|
#include "timeline.h"
|
|
#include "common/msg.h"
|
|
#include "options/path.h"
|
|
#include "misc/bstr.h"
|
|
#include "common/common.h"
|
|
#include "stream/stream.h"
|
|
|
|
#define HEADER "# mpv EDL v0\n"
|
|
|
|
struct tl_part {
|
|
char *filename; // what is stream_open()ed
|
|
double offset; // offset into the source file
|
|
bool offset_set;
|
|
bool chapter_ts;
|
|
bool is_layout;
|
|
double length; // length of the part (-1 if rest of the file)
|
|
char *title;
|
|
};
|
|
|
|
struct tl_parts {
|
|
bool disable_chapters;
|
|
bool dash, no_clip, delay_open;
|
|
char *init_fragment_url;
|
|
struct sh_stream **sh_meta;
|
|
int num_sh_meta;
|
|
struct tl_part *parts;
|
|
int num_parts;
|
|
struct tl_parts *next;
|
|
};
|
|
|
|
struct tl_root {
|
|
struct tl_parts **pars;
|
|
int num_pars;
|
|
};
|
|
|
|
struct priv {
|
|
bstr data;
|
|
};
|
|
|
|
// Static allocation out of laziness.
|
|
#define NUM_MAX_PARAMS 20
|
|
|
|
struct parse_ctx {
|
|
struct mp_log *log;
|
|
bool error;
|
|
bstr param_vals[NUM_MAX_PARAMS];
|
|
bstr param_names[NUM_MAX_PARAMS];
|
|
int num_params;
|
|
};
|
|
|
|
// This returns a value with bstr.start==NULL if nothing found. If the parameter
|
|
// was specified, bstr.str!=NULL, even if the string is empty (bstr.len==0).
|
|
// The parameter is removed from the list if found.
|
|
static bstr get_param(struct parse_ctx *ctx, const char *name)
|
|
{
|
|
bstr bname = bstr0(name);
|
|
for (int n = 0; n < ctx->num_params; n++) {
|
|
if (bstr_equals(ctx->param_names[n], bname)) {
|
|
bstr res = ctx->param_vals[n];
|
|
int count = ctx->num_params;
|
|
MP_TARRAY_REMOVE_AT(ctx->param_names, count, n);
|
|
count = ctx->num_params;
|
|
MP_TARRAY_REMOVE_AT(ctx->param_vals, count, n);
|
|
ctx->num_params -= 1;
|
|
if (!res.start)
|
|
res = bstr0(""); // keep guarantees
|
|
return res;
|
|
}
|
|
}
|
|
return (bstr){0};
|
|
}
|
|
|
|
// Same as get_param(), but return C string. Return NULL if missing.
|
|
static char *get_param0(struct parse_ctx *ctx, void *ta_ctx, const char *name)
|
|
{
|
|
return bstrdup0(ta_ctx, get_param(ctx, name));
|
|
}
|
|
|
|
// Optional int parameter. Returns the parsed integer, or def if the parameter
|
|
// is missing or on error (sets ctx.error on error).
|
|
static int get_param_int(struct parse_ctx *ctx, const char *name, int def)
|
|
{
|
|
bstr val = get_param(ctx, name);
|
|
if (val.start) {
|
|
bstr rest;
|
|
long long ival = bstrtoll(val, &rest, 0);
|
|
if (!val.len || rest.len || ival < INT_MIN || ival > INT_MAX) {
|
|
MP_ERR(ctx, "Invalid integer: '%.*s'\n", BSTR_P(val));
|
|
ctx->error = true;
|
|
return def;
|
|
}
|
|
return ival;
|
|
}
|
|
return def;
|
|
}
|
|
|
|
// Optional time parameter. Currently a number.
|
|
// Returns true: parameter was present and valid, *t is set
|
|
// Returns false: parameter was not present (or broken => ctx.error set)
|
|
static bool get_param_time(struct parse_ctx *ctx, const char *name, double *t)
|
|
{
|
|
bstr val = get_param(ctx, name);
|
|
if (val.start) {
|
|
bstr rest;
|
|
double time = bstrtod(val, &rest);
|
|
if (!val.len || rest.len || !isfinite(time)) {
|
|
MP_ERR(ctx, "Invalid time string: '%.*s'\n", BSTR_P(val));
|
|
ctx->error = true;
|
|
return false;
|
|
}
|
|
*t = time;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static struct tl_parts *add_part(struct tl_root *root)
|
|
{
|
|
struct tl_parts *tl = talloc_zero(root, struct tl_parts);
|
|
MP_TARRAY_APPEND(root, root->pars, root->num_pars, tl);
|
|
return tl;
|
|
}
|
|
|
|
static struct sh_stream *get_meta(struct tl_parts *tl, int index)
|
|
{
|
|
for (int n = 0; n < tl->num_sh_meta; n++) {
|
|
if (tl->sh_meta[n]->index == index)
|
|
return tl->sh_meta[n];
|
|
}
|
|
struct sh_stream *sh = demux_alloc_sh_stream(STREAM_TYPE_COUNT);
|
|
talloc_steal(tl, sh);
|
|
MP_TARRAY_APPEND(tl, tl->sh_meta, tl->num_sh_meta, sh);
|
|
return sh;
|
|
}
|
|
|
|
/* Returns a list of parts, or NULL on parse error.
|
|
* Syntax (without file header or URI prefix):
|
|
* url ::= <entry> ( (';' | '\n') <entry> )*
|
|
* entry ::= <param> ( <param> ',' )*
|
|
* param ::= [<string> '='] (<string> | '%' <number> '%' <bytes>)
|
|
*/
|
|
static struct tl_root *parse_edl(bstr str, struct mp_log *log)
|
|
{
|
|
struct tl_root *root = talloc_zero(NULL, struct tl_root);
|
|
struct tl_parts *tl = add_part(root);
|
|
while (str.len) {
|
|
if (bstr_eatstart0(&str, "#")) {
|
|
bstr_split_tok(str, "\n", &(bstr){0}, &str);
|
|
continue;
|
|
}
|
|
if (bstr_eatstart0(&str, "\n") || bstr_eatstart0(&str, ";"))
|
|
continue;
|
|
bool is_header = bstr_eatstart0(&str, "!");
|
|
struct parse_ctx ctx = { .log = log };
|
|
int nparam = 0;
|
|
while (1) {
|
|
bstr name, val;
|
|
// Check if it's of the form "name=..."
|
|
int next = bstrcspn(str, "=%,;\n");
|
|
if (next > 0 && next < str.len && str.start[next] == '=') {
|
|
name = bstr_splice(str, 0, next);
|
|
str = bstr_cut(str, next + 1);
|
|
} else if (is_header) {
|
|
const char *names[] = {"type"}; // implied name
|
|
name = bstr0(nparam < 1 ? names[nparam] : "-");
|
|
} else {
|
|
const char *names[] = {"file", "start", "length"}; // implied name
|
|
name = bstr0(nparam < 3 ? names[nparam] : "-");
|
|
}
|
|
if (bstr_eatstart0(&str, "%")) {
|
|
int len = bstrtoll(str, &str, 0);
|
|
if (!bstr_startswith0(str, "%") || (len > str.len - 1))
|
|
goto error;
|
|
val = bstr_splice(str, 1, len + 1);
|
|
str = bstr_cut(str, len + 1);
|
|
} else {
|
|
next = bstrcspn(str, ",;\n");
|
|
val = bstr_splice(str, 0, next);
|
|
str = bstr_cut(str, next);
|
|
}
|
|
if (ctx.num_params >= NUM_MAX_PARAMS) {
|
|
mp_err(log, "Too many parameters, ignoring '%.*s'.\n",
|
|
BSTR_P(name));
|
|
} else {
|
|
ctx.param_names[ctx.num_params] = name;
|
|
ctx.param_vals[ctx.num_params] = val;
|
|
ctx.num_params += 1;
|
|
}
|
|
nparam++;
|
|
if (!bstr_eatstart0(&str, ","))
|
|
break;
|
|
}
|
|
if (is_header) {
|
|
bstr f_type = get_param(&ctx, "type");
|
|
if (bstr_equals0(f_type, "mp4_dash")) {
|
|
tl->dash = true;
|
|
tl->init_fragment_url = get_param0(&ctx, tl, "init");
|
|
} else if (bstr_equals0(f_type, "no_clip")) {
|
|
tl->no_clip = true;
|
|
} else if (bstr_equals0(f_type, "new_stream")) {
|
|
// (Special case: ignore "redundant" headers at the start for
|
|
// general symmetry.)
|
|
if (root->num_pars > 1 || tl->num_parts)
|
|
tl = add_part(root);
|
|
} else if (bstr_equals0(f_type, "no_chapters")) {
|
|
tl->disable_chapters = true;
|
|
} else if (bstr_equals0(f_type, "track_meta")) {
|
|
int index = get_param_int(&ctx, "index", -1);
|
|
struct sh_stream *sh = index < 0 && tl->num_sh_meta
|
|
? tl->sh_meta[tl->num_sh_meta - 1]
|
|
: get_meta(tl, index);
|
|
sh->lang = get_param0(&ctx, sh, "lang");
|
|
sh->title = get_param0(&ctx, sh, "title");
|
|
sh->hls_bitrate = get_param_int(&ctx, "byterate", 0) * 8;
|
|
bstr flags = get_param(&ctx, "flags");
|
|
bstr flag;
|
|
while (bstr_split_tok(flags, "+", &flag, &flags) || flag.len) {
|
|
if (bstr_equals0(flag, "default")) {
|
|
sh->default_track = true;
|
|
} else if (bstr_equals0(flag, "forced")) {
|
|
sh->forced_track = true;
|
|
} else {
|
|
mp_warn(log, "Unknown flag: '%.*s'\n", BSTR_P(flag));
|
|
}
|
|
}
|
|
} else if (bstr_equals0(f_type, "delay_open")) {
|
|
struct sh_stream *sh = get_meta(tl, tl->num_sh_meta);
|
|
bstr mt = get_param(&ctx, "media_type");
|
|
if (bstr_equals0(mt, "video")) {
|
|
sh->type = sh->codec->type = STREAM_VIDEO;
|
|
} else if (bstr_equals0(mt, "audio")) {
|
|
sh->type = sh->codec->type = STREAM_AUDIO;
|
|
} else if (bstr_equals0(mt, "sub")) {
|
|
sh->type = sh->codec->type = STREAM_SUB;
|
|
} else {
|
|
mp_err(log, "Invalid or missing !delay_open media type.\n");
|
|
goto error;
|
|
}
|
|
sh->codec->codec = get_param0(&ctx, sh, "codec");
|
|
if (!sh->codec->codec)
|
|
sh->codec->codec = "null";
|
|
sh->codec->disp_w = get_param_int(&ctx, "w", 0);
|
|
sh->codec->disp_h = get_param_int(&ctx, "h", 0);
|
|
sh->codec->fps = get_param_int(&ctx, "fps", 0);
|
|
sh->codec->samplerate = get_param_int(&ctx, "samplerate", 0);
|
|
tl->delay_open = true;
|
|
} else {
|
|
mp_err(log, "Unknown header: '%.*s'\n", BSTR_P(f_type));
|
|
goto error;
|
|
}
|
|
} else {
|
|
struct tl_part p = { .length = -1 };
|
|
p.filename = get_param0(&ctx, tl, "file");
|
|
p.offset_set = get_param_time(&ctx, "start", &p.offset);
|
|
get_param_time(&ctx, "length", &p.length);
|
|
bstr ts = get_param(&ctx, "timestamps");
|
|
if (bstr_equals0(ts, "chapters")) {
|
|
p.chapter_ts = true;
|
|
} else if (ts.start && !bstr_equals0(ts, "seconds")) {
|
|
mp_warn(log, "Unknown timestamp type: '%.*s'\n", BSTR_P(ts));
|
|
}
|
|
p.title = get_param0(&ctx, tl, "title");
|
|
bstr layout = get_param(&ctx, "layout");
|
|
if (layout.start) {
|
|
if (bstr_equals0(layout, "this")) {
|
|
p.is_layout = true;
|
|
} else {
|
|
mp_warn(log, "Unknown layout param: '%.*s'\n", BSTR_P(layout));
|
|
}
|
|
}
|
|
if (!p.filename) {
|
|
mp_err(log, "Missing filename in segment.'\n");
|
|
goto error;
|
|
}
|
|
MP_TARRAY_APPEND(tl, tl->parts, tl->num_parts, p);
|
|
}
|
|
if (ctx.error)
|
|
goto error;
|
|
for (int n = 0; n < ctx.num_params; n++) {
|
|
mp_warn(log, "Unknown or duplicate parameter: '%.*s'\n",
|
|
BSTR_P(ctx.param_names[n]));
|
|
}
|
|
}
|
|
assert(root->num_pars);
|
|
for (int n = 0; n < root->num_pars; n++) {
|
|
if (root->pars[n]->num_parts < 1) {
|
|
mp_err(log, "EDL specifies no segments.'\n");
|
|
goto error;
|
|
}
|
|
}
|
|
return root;
|
|
error:
|
|
mp_err(log, "EDL parsing failed.\n");
|
|
talloc_free(root);
|
|
return NULL;
|
|
}
|
|
|
|
static struct demuxer *open_source(struct timeline *root,
|
|
struct timeline_par *tl, char *filename)
|
|
{
|
|
for (int n = 0; n < tl->num_parts; n++) {
|
|
struct demuxer *d = tl->parts[n].source;
|
|
if (d && d->filename && strcmp(d->filename, filename) == 0)
|
|
return d;
|
|
}
|
|
struct demuxer_params params = {
|
|
.init_fragment = tl->init_fragment,
|
|
.stream_flags = root->stream_origin,
|
|
};
|
|
struct demuxer *d = demux_open_url(filename, ¶ms, root->cancel,
|
|
root->global);
|
|
if (d) {
|
|
MP_TARRAY_APPEND(root, root->sources, root->num_sources, d);
|
|
} else {
|
|
MP_ERR(root, "EDL: Could not open source file '%s'.\n", filename);
|
|
}
|
|
return d;
|
|
}
|
|
|
|
static double demuxer_chapter_time(struct demuxer *demuxer, int n)
|
|
{
|
|
if (n < 0 || n >= demuxer->num_chapters)
|
|
return -1;
|
|
return demuxer->chapters[n].pts;
|
|
}
|
|
|
|
// Append all chapters from src to the chapters array.
|
|
// Ignore chapters outside of the given time range.
|
|
static void copy_chapters(struct demux_chapter **chapters, int *num_chapters,
|
|
struct demuxer *src, double start, double len,
|
|
double dest_offset)
|
|
{
|
|
for (int n = 0; n < src->num_chapters; n++) {
|
|
double time = demuxer_chapter_time(src, n);
|
|
if (time >= start && time <= start + len) {
|
|
struct demux_chapter ch = {
|
|
.pts = dest_offset + time - start,
|
|
.metadata = mp_tags_dup(*chapters, src->chapters[n].metadata),
|
|
};
|
|
MP_TARRAY_APPEND(NULL, *chapters, *num_chapters, ch);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void resolve_timestamps(struct tl_part *part, struct demuxer *demuxer)
|
|
{
|
|
if (part->chapter_ts) {
|
|
double start = demuxer_chapter_time(demuxer, part->offset);
|
|
double length = part->length;
|
|
double end = length;
|
|
if (end >= 0)
|
|
end = demuxer_chapter_time(demuxer, part->offset + part->length);
|
|
if (end >= 0 && start >= 0)
|
|
length = end - start;
|
|
part->offset = start;
|
|
part->length = length;
|
|
}
|
|
if (!part->offset_set)
|
|
part->offset = demuxer->start_time;
|
|
}
|
|
|
|
static struct timeline_par *build_timeline(struct timeline *root,
|
|
struct tl_parts *parts)
|
|
{
|
|
struct timeline_par *tl = talloc_zero(root, struct timeline_par);
|
|
MP_TARRAY_APPEND(root, root->pars, root->num_pars, tl);
|
|
|
|
tl->track_layout = NULL;
|
|
tl->dash = parts->dash;
|
|
tl->no_clip = parts->no_clip;
|
|
tl->delay_open = parts->delay_open;
|
|
|
|
// There is no copy function for sh_stream, so just steal it.
|
|
for (int n = 0; n < parts->num_sh_meta; n++) {
|
|
MP_TARRAY_APPEND(tl, tl->sh_meta, tl->num_sh_meta,
|
|
talloc_steal(tl, parts->sh_meta[n]));
|
|
parts->sh_meta[n] = NULL;
|
|
}
|
|
parts->num_sh_meta = 0;
|
|
|
|
if (parts->init_fragment_url && parts->init_fragment_url[0]) {
|
|
MP_VERBOSE(root, "Opening init fragment...\n");
|
|
stream_t *s = stream_create(parts->init_fragment_url,
|
|
STREAM_READ | root->stream_origin,
|
|
root->cancel, root->global);
|
|
if (s) {
|
|
root->is_network |= s->is_network;
|
|
root->is_streaming |= s->streaming;
|
|
tl->init_fragment = stream_read_complete(s, tl, 1000000);
|
|
}
|
|
free_stream(s);
|
|
if (!tl->init_fragment.len) {
|
|
MP_ERR(root, "Could not read init fragment.\n");
|
|
goto error;
|
|
}
|
|
struct demuxer_params params = {
|
|
.init_fragment = tl->init_fragment,
|
|
.stream_flags = root->stream_origin,
|
|
};
|
|
tl->track_layout = demux_open_url("memory://", ¶ms, root->cancel,
|
|
root->global);
|
|
if (!tl->track_layout) {
|
|
MP_ERR(root, "Could not demux init fragment.\n");
|
|
goto error;
|
|
}
|
|
MP_TARRAY_APPEND(root, root->sources, root->num_sources, tl->track_layout);
|
|
}
|
|
|
|
tl->parts = talloc_array_ptrtype(tl, tl->parts, parts->num_parts);
|
|
double starttime = 0;
|
|
for (int n = 0; n < parts->num_parts; n++) {
|
|
struct tl_part *part = &parts->parts[n];
|
|
struct demuxer *source = NULL;
|
|
|
|
if (tl->dash) {
|
|
part->offset = starttime;
|
|
if (part->length <= 0)
|
|
MP_WARN(root, "Segment %d has unknown duration.\n", n);
|
|
if (part->offset_set)
|
|
MP_WARN(root, "Offsets are ignored.\n");
|
|
|
|
if (!tl->track_layout)
|
|
tl->track_layout = open_source(root, tl, part->filename);
|
|
} else if (tl->delay_open) {
|
|
if (n == 0 && !part->offset_set) {
|
|
part->offset = starttime;
|
|
part->offset_set = true;
|
|
}
|
|
if (part->chapter_ts || (part->length < 0 && !tl->no_clip)) {
|
|
MP_ERR(root, "Invalid specification for delay_open stream.\n");
|
|
goto error;
|
|
}
|
|
} else {
|
|
MP_VERBOSE(root, "Opening segment %d...\n", n);
|
|
|
|
source = open_source(root, tl, part->filename);
|
|
if (!source)
|
|
goto error;
|
|
|
|
resolve_timestamps(part, source);
|
|
|
|
double end_time = source->duration;
|
|
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(root, "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(root, "EDL: entry %d uses %f "
|
|
"seconds, but file has only %f seconds.\n",
|
|
n, end_part, end_time);
|
|
}
|
|
}
|
|
|
|
if (!parts->disable_chapters) {
|
|
// 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->title ? part->title : part->filename);
|
|
MP_TARRAY_APPEND(root, root->chapters, root->num_chapters, ch);
|
|
|
|
// Also copy the source file's chapters for the relevant parts
|
|
copy_chapters(&root->chapters, &root->num_chapters, source,
|
|
part->offset, part->length, starttime);
|
|
}
|
|
}
|
|
|
|
tl->parts[n] = (struct timeline_part) {
|
|
.start = starttime,
|
|
.end = starttime + part->length,
|
|
.source_start = part->offset,
|
|
.source = source,
|
|
.url = talloc_strdup(tl, part->filename),
|
|
};
|
|
|
|
starttime = tl->parts[n].end;
|
|
|
|
if (source && !tl->track_layout && part->is_layout)
|
|
tl->track_layout = source;
|
|
|
|
tl->num_parts++;
|
|
}
|
|
|
|
if (tl->no_clip && tl->num_parts > 1)
|
|
MP_WARN(root, "Multiple parts with no_clip. Undefined behavior ahead.\n");
|
|
|
|
if (!tl->track_layout) {
|
|
// Use a heuristic to select the "broadest" part as layout.
|
|
for (int n = 0; n < parts->num_parts; n++) {
|
|
struct demuxer *s = tl->parts[n].source;
|
|
if (!s)
|
|
continue;
|
|
if (!tl->track_layout ||
|
|
demux_get_num_stream(s) > demux_get_num_stream(tl->track_layout))
|
|
tl->track_layout = s;
|
|
}
|
|
}
|
|
|
|
if (!tl->track_layout && !tl->delay_open)
|
|
goto error;
|
|
if (!root->meta)
|
|
root->meta = tl->track_layout;
|
|
|
|
assert(tl->num_parts == parts->num_parts);
|
|
return tl;
|
|
|
|
error:
|
|
root->num_pars = 0;
|
|
return NULL;
|
|
}
|
|
|
|
static void fix_filenames(struct tl_parts *parts, char *source_path)
|
|
{
|
|
if (bstr_equals0(mp_split_proto(bstr0(source_path), NULL), "edl"))
|
|
return;
|
|
struct bstr dirname = mp_dirname(source_path);
|
|
for (int n = 0; n < parts->num_parts; n++) {
|
|
struct tl_part *part = &parts->parts[n];
|
|
if (!mp_is_url(bstr0(part->filename))) {
|
|
part->filename =
|
|
mp_path_join_bstr(parts, dirname, bstr0(part->filename));
|
|
}
|
|
}
|
|
}
|
|
|
|
static void build_mpv_edl_timeline(struct timeline *tl)
|
|
{
|
|
struct priv *p = tl->demuxer->priv;
|
|
|
|
struct tl_root *root = parse_edl(p->data, tl->log);
|
|
if (!root) {
|
|
MP_ERR(tl, "Error in EDL.\n");
|
|
return;
|
|
}
|
|
|
|
bool all_dash = true;
|
|
bool all_no_clip = true;
|
|
bool all_single = true;
|
|
|
|
for (int n = 0; n < root->num_pars; n++) {
|
|
struct tl_parts *parts = root->pars[n];
|
|
fix_filenames(parts, tl->demuxer->filename);
|
|
struct timeline_par *par = build_timeline(tl, parts);
|
|
if (!par)
|
|
break;
|
|
all_dash &= par->dash;
|
|
all_no_clip &= par->no_clip;
|
|
all_single &= par->num_parts == 1;
|
|
}
|
|
|
|
if (all_dash) {
|
|
tl->format = "dash";
|
|
} else if (all_no_clip && all_single) {
|
|
tl->format = "multi";
|
|
} else {
|
|
tl->format = "edl";
|
|
}
|
|
|
|
talloc_free(root);
|
|
}
|
|
|
|
static int try_open_file(struct demuxer *demuxer, enum demux_check check)
|
|
{
|
|
if (!demuxer->access_references)
|
|
return -1;
|
|
|
|
struct priv *p = talloc_zero(demuxer, struct priv);
|
|
demuxer->priv = p;
|
|
demuxer->fully_read = true;
|
|
|
|
struct stream *s = demuxer->stream;
|
|
if (s->info && strcmp(s->info->name, "edl") == 0) {
|
|
p->data = bstr0(s->path);
|
|
return 0;
|
|
}
|
|
if (check >= DEMUX_CHECK_UNSAFE) {
|
|
char header[sizeof(HEADER) - 1];
|
|
int len = stream_read_peek(s, header, sizeof(header));
|
|
if (len != strlen(HEADER) || memcmp(header, HEADER, len) != 0)
|
|
return -1;
|
|
}
|
|
p->data = stream_read_complete(s, demuxer, 1000000);
|
|
if (p->data.start == NULL)
|
|
return -1;
|
|
bstr_eatstart0(&p->data, HEADER);
|
|
demux_close_stream(demuxer);
|
|
return 0;
|
|
}
|
|
|
|
const struct demuxer_desc demuxer_desc_edl = {
|
|
.name = "edl",
|
|
.desc = "Edit decision list",
|
|
.open = try_open_file,
|
|
.load_timeline = build_mpv_edl_timeline,
|
|
};
|