summaryrefslogtreecommitdiffstats
path: root/demux/demux_edl.c
diff options
context:
space:
mode:
Diffstat (limited to 'demux/demux_edl.c')
-rw-r--r--demux/demux_edl.c271
1 files changed, 221 insertions, 50 deletions
diff --git a/demux/demux_edl.c b/demux/demux_edl.c
index 9d276732fe..356b7eefb5 100644
--- a/demux/demux_edl.c
+++ b/demux/demux_edl.c
@@ -21,6 +21,7 @@
#include <stdbool.h>
#include <string.h>
#include <inttypes.h>
+#include <limits.h>
#include <math.h>
#include "mpv_talloc.h"
@@ -31,6 +32,7 @@
#include "options/path.h"
#include "misc/bstr.h"
#include "common/common.h"
+#include "common/tags.h"
#include "stream/stream.h"
#define HEADER "# mpv EDL v0\n"
@@ -47,8 +49,10 @@ struct tl_part {
struct tl_parts {
bool disable_chapters;
- bool dash, no_clip;
+ 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;
@@ -57,22 +61,88 @@ struct tl_parts {
struct tl_root {
struct tl_parts **pars;
int num_pars;
+ struct mp_tags *tags;
};
struct priv {
bstr data;
};
-// Parse a time (absolute file time or duration). Currently equivalent to a
-// number. Return false on failure.
-static bool parse_time(bstr str, double *out_time)
+// 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 rest;
- double time = bstrtod(str, &rest);
- if (!str.len || rest.len || !isfinite(time))
- return false;
- *out_time = time;
- return true;
+ 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)
@@ -82,15 +152,28 @@ static struct tl_parts *add_part(struct tl_root *root)
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)
+static struct tl_root *parse_edl(bstr str, struct mp_log *log)
{
struct tl_root *root = talloc_zero(NULL, struct tl_root);
+ root->tags = talloc_zero(root, struct mp_tags);
struct tl_parts *tl = add_part(root);
while (str.len) {
if (bstr_eatstart0(&str, "#")) {
@@ -100,9 +183,7 @@ static struct tl_root *parse_edl(bstr str)
if (bstr_eatstart0(&str, "\n") || bstr_eatstart0(&str, ";"))
continue;
bool is_header = bstr_eatstart0(&str, "!");
- bstr f_type = {0};
- bstr f_init = {0};
- struct tl_part p = { .length = -1 };
+ struct parse_ctx ctx = { .log = log };
int nparam = 0;
while (1) {
bstr name, val;
@@ -129,65 +210,125 @@ static struct tl_root *parse_edl(bstr str)
val = bstr_splice(str, 0, next);
str = bstr_cut(str, next);
}
- // Interpret parameters. Explicitly ignore unknown ones.
- if (is_header) {
- if (bstr_equals0(name, "type")) {
- f_type = val;
- } else if (bstr_equals0(name, "init")) {
- f_init = val;
- }
+ if (ctx.num_params >= NUM_MAX_PARAMS) {
+ mp_err(log, "Too many parameters, ignoring '%.*s'.\n",
+ BSTR_P(name));
} else {
- if (bstr_equals0(name, "file")) {
- p.filename = bstrto0(tl, val);
- } else if (bstr_equals0(name, "start")) {
- if (!parse_time(val, &p.offset))
- goto error;
- p.offset_set = true;
- } else if (bstr_equals0(name, "length")) {
- if (!parse_time(val, &p.length))
- goto error;
- } else if (bstr_equals0(name, "timestamps")) {
- if (bstr_equals0(val, "chapters"))
- p.chapter_ts = true;
- } else if (bstr_equals0(name, "title")) {
- p.title = bstrto0(tl, val);
- } else if (bstr_equals0(name, "layout")) {
- if (bstr_equals0(val, "this"))
- p.is_layout = true;
- }
+ 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;
- if (f_init.len)
- tl->init_fragment_url = bstrto0(tl, f_init);
+ 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")) {
- tl = add_part(root);
+ // (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 if (bstr_equals0(f_type, "global_tags")) {
+ for (int n = 0; n < ctx.num_params; n++) {
+ mp_tags_set_bstr(root->tags, ctx.param_names[n],
+ ctx.param_vals[n]);
+ }
+ ctx.num_params = 0;
} else {
+ mp_err(log, "Unknown header: '%.*s'\n", BSTR_P(f_type));
goto error;
}
} else {
- if (!p.filename)
+ 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]));
+ }
}
- if (!root->num_pars)
- goto error;
+ assert(root->num_pars);
for (int n = 0; n < root->num_pars; n++) {
- if (root->pars[n]->num_parts < 1)
+ 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;
}
@@ -257,6 +398,7 @@ static void resolve_timestamps(struct tl_part *part, struct demuxer *demuxer)
}
static struct timeline_par *build_timeline(struct timeline *root,
+ struct tl_root *edl_root,
struct tl_parts *parts)
{
struct timeline_par *tl = talloc_zero(root, struct timeline_par);
@@ -265,6 +407,15 @@ static struct timeline_par *build_timeline(struct timeline *root,
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");
@@ -309,6 +460,15 @@ static struct timeline_par *build_timeline(struct timeline *root,
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);
@@ -372,6 +532,9 @@ static struct timeline_par *build_timeline(struct timeline *root,
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++) {
@@ -384,11 +547,16 @@ static struct timeline_par *build_timeline(struct timeline *root,
}
}
- if (!tl->track_layout)
+ if (!tl->track_layout && !tl->delay_open)
goto error;
if (!root->meta)
root->meta = tl->track_layout;
+ // Not very sane, since demuxer fields are supposed to be treated read-only
+ // from outside, but happens to work in this case, so who cares.
+ if (root->meta)
+ mp_tags_merge(root->meta->metadata, edl_root->tags);
+
assert(tl->num_parts == parts->num_parts);
return tl;
@@ -404,7 +572,10 @@ static void fix_filenames(struct tl_parts *parts, char *source_path)
struct bstr dirname = mp_dirname(source_path);
for (int n = 0; n < parts->num_parts; n++) {
struct tl_part *part = &parts->parts[n];
- part->filename = mp_path_join_bstr(parts, dirname, bstr0(part->filename));
+ if (!mp_is_url(bstr0(part->filename))) {
+ part->filename =
+ mp_path_join_bstr(parts, dirname, bstr0(part->filename));
+ }
}
}
@@ -412,7 +583,7 @@ static void build_mpv_edl_timeline(struct timeline *tl)
{
struct priv *p = tl->demuxer->priv;
- struct tl_root *root = parse_edl(p->data);
+ struct tl_root *root = parse_edl(p->data, tl->log);
if (!root) {
MP_ERR(tl, "Error in EDL.\n");
return;
@@ -425,7 +596,7 @@ static void build_mpv_edl_timeline(struct timeline *tl)
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);
+ struct timeline_par *par = build_timeline(tl, root, parts);
if (!par)
break;
all_dash &= par->dash;