Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
27 kB · 750 lines
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751"use strict";(function main_default_js(g) {// - g is the global object.// - User callbacks called without 'this', global only if callee is non-strict.// - The names of function expressions are not required, but are used in stack// traces. We name them where useful to show up (fname:#line always shows).
mp.msg = { log: mp.log };mp.msg.verbose = mp.log.bind(null, "v");var levels = ["fatal", "error", "warn", "info", "debug", "trace"];levels.forEach(function(l) { mp.msg[l] = mp.log.bind(null, l) });
// same as {} but without inherited stuff, e.g. o["toString"] doesn't exist.// used where we try to fetch items by keys which we don't absolutely trust.function new_cache() { return Object.create(null, {});}
/********************************************************************** * event handlers, property observers, idle, client messages, hooks, async *********************************************************************/var ehandlers = new_cache() // items of event-name: array of {maybe cb: fn}
mp.register_event = function(name, fn) { if (!ehandlers[name]) ehandlers[name] = []; ehandlers[name] = ehandlers[name].concat([{cb: fn}]); // replaces the arr return mp._request_event(name, true);}
mp.unregister_event = function(fn) { for (var name in ehandlers) { ehandlers[name] = ehandlers[name].filter(function(h) { if (h.cb != fn) return true; delete h.cb; // dispatch could have a ref to h }); // replacing, not mutating the array if (!ehandlers[name].length) { delete ehandlers[name]; mp._request_event(name, false); } }}
// call only pre-registered handlers, but not ones which got unregisteredfunction dispatch_event(e) { var handlers = ehandlers[e.event]; if (handlers) { for (var len = handlers.length, i = 0; i < len; i++) { var cb = handlers[i].cb; // 'handlers' won't mutate, but unregister if (cb) // could remove cb from some items cb(e); } }}
// ----- idle observers -----var iobservers = [], // array of callbacks ideleted = false;
mp.register_idle = function(fn) { iobservers.push(fn);}
mp.unregister_idle = function(fn) { iobservers.forEach(function(f, i) { if (f == fn) delete iobservers[i]; // -> same length but [more] sparse }); ideleted = true;}
function notify_idle_observers() { // forEach and filter skip deleted items and newly added items iobservers.forEach(function(f) { f() }); if (ideleted) { iobservers = iobservers.filter(function() { return true }); ideleted = false; }}
// ----- property observers -----var next_oid = 1, observers = new_cache(); // items of id: fn
mp.observe_property = function(name, format, fn) { var id = next_oid++; observers[id] = fn; return mp._observe_property(id, name, format || undefined); // allow null}
mp.unobserve_property = function(fn) { for (var id in observers) { if (observers[id] == fn) { delete observers[id]; mp._unobserve_property(id); } }}
function notify_observer(e) { var cb = observers[e.id]; if (cb) cb(e.name, e.data);}
// ----- Client messages -----var messages = new_cache(); // items of name: fn
// overrides name. no libmpv API to reg/unreg specific messages.mp.register_script_message = function(name, fn) { messages[name] = fn;}
mp.unregister_script_message = function(name) { delete messages[name];}
function dispatch_message(ev) { var cb = ev.args.length ? messages[ev.args[0]] : false; if (cb) cb.apply(null, ev.args.slice(1));}
// ----- hooks -----var hooks = []; // array of callbacks, id is index+1
function run_hook(ev) { var cb = ev.id > 0 && hooks[ev.id - 1]; if (cb) cb(); mp._hook_continue(ev.hook_id);}
mp.add_hook = function add_hook(name, pri, fn) { hooks.push(fn); // 50 (scripting docs default priority) maps to 0 (default in C API docs) return mp._hook_add(name, pri - 50, hooks.length);}
// ----- async commands -----var async_callbacks = new_cache(); // items of id: fnvar async_next_id = 1;
mp.command_native_async = function command_native_async(node, cb) { var id = async_next_id++; cb = cb || function dummy() {}; if (!mp._command_native_async(id, node)) { var le = mp.last_error(); setTimeout(cb, 0, false, undefined, le); /* callback async */ mp._set_last_error(le); return undefined; } async_callbacks[id] = cb; return id;}
function async_command_handler(ev) { var cb = async_callbacks[ev.id]; delete async_callbacks[ev.id]; if (ev.error) cb(false, undefined, ev.error); else cb(true, ev.result, "");}
mp.abort_async_command = function abort_async_command(id) { // cb will be invoked regardless, possibly with the abort result if (async_callbacks[id]) mp._abort_async_command(id);}
// shared-script-properties - always an object, even if without propertiesfunction shared_script_property_set(name, val) { if (arguments.length > 1) return mp.commandv("change-list", "shared-script-properties", "append", "" + name + "=" + val); else return mp.commandv("change-list", "shared-script-properties", "remove", name);}
function shared_script_property_get(name) { return mp.get_property_native("shared-script-properties")[name];}
function shared_script_property_observe(name, cb) { return mp.observe_property("shared-script-properties", "native", function shared_props_cb(_name, val) { cb(name, val[name]) } );}
mp.utils.shared_script_property_set = shared_script_property_set;mp.utils.shared_script_property_get = shared_script_property_get;mp.utils.shared_script_property_observe = shared_script_property_observe;
// osd-assvar next_assid = 1;mp.create_osd_overlay = function create_osd_overlay(format) { return { format: format || "ass-events", id: next_assid++, data: "", res_x: 0, res_y: 720, z: 0,
update: function ass_update() { mp.command_native({ name: "osd-overlay", format: this.format, id: this.id, data: this.data, res_x: Math.round(this.res_x), res_y: Math.round(this.res_y), z: this.z, }); return mp.last_error() ? undefined : true; },
remove: function ass_remove() { mp.command_native({ name: "osd-overlay", id: this.id, format: "none", data: "", }); return mp.last_error() ? undefined : true; }, };}
// osd-ass legacy APImp.set_osd_ass = function set_osd_ass(res_x, res_y, data) { if (!mp._legacy_overlay) mp._legacy_overlay = mp.create_osd_overlay("ass-events"); mp._legacy_overlay.res_x = res_x; mp._legacy_overlay.res_y = res_y; mp._legacy_overlay.data = data; return mp._legacy_overlay.update();}
// the following return undefined on error, null passthrough, or legacy objectmp.get_osd_size = function get_osd_size() { var d = mp.get_property_native("osd-dimensions"); return d && {width: d.w, height: d.h, aspect: d.aspect};}mp.get_osd_margins = function get_osd_margins() { var d = mp.get_property_native("osd-dimensions"); return d && {left: d.ml, right: d.mr, top: d.mt, bottom: d.mb};}
/********************************************************************** * key bindings *********************************************************************/// binds: items of (binding) name which are objects of:// {cb: fn, forced: bool, maybe input: str, repeatable: bool, complex: bool}var binds = new_cache();
function dispatch_key_binding(name, state, key_name) { var cb = binds[name] ? binds[name].cb : false; if (cb) // "script-binding [<script_name>/]<name>" command was invoked cb(state, key_name);}
var binds_tid = 0; // flush timer id. actual id's are always true-thymp.flush_key_bindings = function flush_key_bindings() { var def = [], forced = []; for (var n in binds) // Array.join() will later skip undefined .input (binds[n].forced ? forced : def).push(binds[n].input);
var sect = "input_" + mp.script_name; mp.commandv("define-section", sect, def.join("\n"), "default"); mp.commandv("enable-section", sect, "allow-hide-cursor+allow-vo-dragging");
sect = "input_forced_" + mp.script_name; mp.commandv("define-section", sect, forced.join("\n"), "force"); mp.commandv("enable-section", sect, "allow-hide-cursor+allow-vo-dragging");
clearTimeout(binds_tid); // cancel future flush if called directly binds_tid = 0;}
function sched_bindings_flush() { if (!binds_tid) binds_tid = setTimeout(mp.flush_key_bindings, 0); // fires on idle}
// name/opts maybe omitted. opts: object with optional bool members: repeatable,// complex, forced, or a string str which is evaluated as object {str: true}.var next_bid = 1;function add_binding(forced, key, name, fn, opts) { if (typeof name == "function") { // as if "name" is not part of the args opts = fn; fn = name; name = false; } if (!name) name = "__keybinding" + next_bid++; // new unique binding name var key_data = {forced: forced}; switch (typeof opts) { // merge opts into key_data case "string": key_data[opts] = true; break; case "object": for (var o in opts) key_data[o] = opts[o]; }
if (key_data.complex) { mp.register_script_message(name, function msg_cb() { fn({event: "press", is_mouse: false}); }); var KEY_STATES = { u: "up", d: "down", r: "repeat", p: "press" }; key_data.cb = function key_cb(state, key_name) { fn({ event: KEY_STATES[state[0]] || "unknown", is_mouse: state[1] == "m", key_name: key_name || undefined }); } } else { mp.register_script_message(name, fn); key_data.cb = function key_cb(state) { // Emulate the semantics at input.c: mouse emits on up, kb on down. // Also, key repeat triggers the binding again. var e = state[0], emit = (state[1] == "m") ? (e == "u") : (e == "d"); if (emit || e == "p" || e == "r" && key_data.repeatable) fn(); } }
if (key) key_data.input = key + " script-binding " + mp.script_name + "/" + name; binds[name] = key_data; // used by user and/or our (key) script-binding sched_bindings_flush();}
mp.add_key_binding = add_binding.bind(null, false);mp.add_forced_key_binding = add_binding.bind(null, true);
mp.remove_key_binding = function(name) { mp.unregister_script_message(name); delete binds[name]; sched_bindings_flush();}
/********************************************************************** Timers: compatible HTML5 WindowTimers - set/clear Timeout/Interval - Spec: https://www.w3.org/TR/html5/webappapis.html#timers - Guaranteed to callback a-sync to [re-]insertion (event-loop wise). - Guaranteed to callback by expiration order, or, if equal, by insertion order. - Not guaranteed schedule accuracy, though intervals should have good average. *********************************************************************/
// pending 'timers' ordered by expiration: latest at index 0 (top fires first).// Earlier timers are quicker to handle - just push/pop or fewer items to shift.var next_tid = 1, timers = [], // while in process_timers, just insertion-ordered (push) tset_is_push = false, // signal set_timer that we're in process_timers tcanceled = false, // or object of items timer-id: true now = mp.get_time_ms; // just an alias
function insert_sorted(arr, t) { for (var i = arr.length - 1; i >= 0 && t.when >= arr[i].when; i--) arr[i + 1] = arr[i]; // move up timers which fire earlier than t arr[i + 1] = t; // i is -1 or fires later than t}
// args (is "arguments"): fn_or_str [,duration [,user_arg1 [, user_arg2 ...]]]function set_timer(repeat, args) { var fos = args[0], duration = Math.max(0, (args[1] || 0)), // minimum and default are 0 t = { id: next_tid++, when: now() + duration, interval: repeat ? duration : -1, callback: (typeof fos == "function") ? fos : Function(fos), args: (args.length < 3) ? false : [].slice.call(args, 2), };
if (tset_is_push) { timers.push(t); } else { insert_sorted(timers, t); } return t.id;}
g.setTimeout = function setTimeout() { return set_timer(false, arguments) };g.setInterval = function setInterval() { return set_timer(true, arguments) };
g.clearTimeout = g.clearInterval = function(id) { if (id < next_tid) { // must ignore if not active timer id. if (!tcanceled) tcanceled = {}; tcanceled[id] = true; }}
// arr: ordered timers array. ret: -1: no timers, 0: due, positive: ms to waitfunction peek_wait(arr) { return arr.length ? Math.max(0, arr[arr.length - 1].when - now()) : -1;}
function peek_timers_wait() { return peek_wait(timers); // must not be called while in process_timers}
// Callback all due non-canceled timers which were inserted before calling us.// Returns wait in ms till the next timer (possibly 0), or -1 if nothing pends.function process_timers() { var wait = peek_wait(timers); if (wait != 0) return wait;
var actives = timers; // only process those already inserted by now timers = []; // we'll handle added new timers at the end of processing. tset_is_push = true; // signal set_timer to just push-insert
do { var t = actives.pop(); if (tcanceled && tcanceled[t.id]) continue;
if (t.args) { t.callback.apply(null, t.args); } else { (0, t.callback)(); // faster, nicer stack trace than t.cb.call() }
if (t.interval >= 0) { // allow 20 ms delay/clock-resolution/gc before we skip and reset t.when = Math.max(now() - 20, t.when + t.interval); timers.push(t); // insertion order only } } while (peek_wait(actives) == 0);
// new 'timers' are insertion-ordered. remains of actives are fully ordered timers.forEach(function(t) { insert_sorted(actives, t) }); timers = actives; // now we're fully ordered again, and with all timers tset_is_push = false; if (tcanceled) { timers = timers.filter(function(t) { return !tcanceled[t.id] }); tcanceled = false; } return peek_wait(timers);}
/********************************************************************** CommonJS module/require
Spec: http://wiki.commonjs.org/wiki/Modules/1.1.1 - All the mandatory requirements are implemented, all the unit tests pass. - The implementation makes the following exception: - Allows the chars [~@:\\] in module id for meta-dir/builtin/dos-drive/UNC.
Implementation choices beyond the specification: - A module may assign to module.exports (rather than only to exports). - A module's 'this' is the global object, also if it sets strict mode. - No 'global'/'self'. Users can do "this.global = this;" before require(..) - A module has "privacy of its top scope", runs in its own function context. - No id identity with symlinks - a valid choice which others make too. - require("X") always maps to "X.js" -> require("foo.js") is file "foo.js.js". - Global modules search paths are 'scripts/modules.js/' in mpv config dirs. - A main script could e.g. require("./abc") to load a non-global module. - Module id supports mpv path enhancements, e.g. ~/foo, ~~/bar, ~~desktop/baz *********************************************************************/
mp.module_paths = []; // global modules search pathsif (mp.script_path !== undefined) // loaded as a directory mp.module_paths.push(mp.utils.join_path(mp.script_path, "modules"));
// Internal meta top-dirs. Users should not rely on these names.var MODULES_META = "~~modules", SCRIPTDIR_META = "~~scriptdir", // relative script path -> meta absolute id main_script = mp.utils.split_path(mp.script_file); // -> [ path, file ]
function resolve_module_file(id) { var sep = id.indexOf("/"), base = id.substring(0, sep), rest = id.substring(sep + 1) + ".js";
if (base == SCRIPTDIR_META) return mp.utils.join_path(main_script[0], rest);
if (base == MODULES_META) { for (var i = 0; i < mp.module_paths.length; i++) { try { var f = mp.utils.join_path(mp.module_paths[i], rest); mp.utils.read_file(f, 1); // throws on any error return f; } catch (e) {} } throw(Error("Cannot find module file '" + rest + "'")); }
return id + ".js";}
// Delimiter '/', remove redundancies, prefix with modules meta-root if needed.// E.g. c:\x -> c:/x, or ./x//y/../z -> ./x/z, or utils/x -> ~~modules/utils/x .function canonicalize(id) { var path = id.replace(/\\/g,"/").split("/"), t = path[0], base = [];
// if not strictly relative then must be top-level. figure out base/rest if (t != "." && t != "..") { // global module if it's not fs-root/home/dos-drive/builtin/meta-dir if (!(t == "" || t == "~" || t[1] == ":" || t == "@" || t.match(/^~~/))) path.unshift(MODULES_META); // add an explicit modules meta-root
if (id.match(/^\\\\/)) // simple UNC handling, preserve leading \\srv path = ["\\\\" + path[2]].concat(path.slice(3)); // [ \\srv, shr..]
if (t[1] == ":" && t.length > 2) { // path: [ "c:relative", "path" ] path[0] = t.substring(2); path.unshift(t[0] + ":."); // -> [ "c:.", "relative", "path" ] } base = [path.shift()]; }
// path is now logically relative. base, if not empty, is its [meta] root. // normalize the relative part - always id-based (spec Module Id, 1.3.6). var cr = []; // canonicalized relative for (var i = 0; i < path.length; i++) { if (path[i] == "." || path[i] == "") continue; if (path[i] == ".." && cr.length && cr[cr.length - 1] != "..") { cr.pop(); continue; } cr.push(path[i]); }
if (!base.length && cr[0] != "..") base = ["."]; // relative and not ../<stuff> so must start with ./ return base.concat(cr).join("/");}
function resolve_module_id(base_id, new_id) { new_id = canonicalize(new_id); if (!new_id.match(/^\.\/|^\.\.\//)) // doesn't start with ./ or ../ return new_id; // not relative, we don't care about base_id
var combined = mp.utils.join_path(mp.utils.split_path(base_id)[0], new_id); return canonicalize(combined);}
var req_cache = new_cache(); // global for all instances of require
// ret: a require function instance which uses base_id to resolve relative id'sfunction new_require(base_id) { return function require(id) { id = resolve_module_id(base_id, id); // id is now top-level if (req_cache[id]) return req_cache[id].exports;
var new_module = {id: id, exports: {}}; req_cache[id] = new_module; try { var filename = resolve_module_file(id); // we need dedicated free vars + filename in traces + allow strict var str = "mp._req = function(require, exports, module) {" + mp.utils.read_file(filename) + "\n;}"; mp.utils.compile_js(filename, str)(); // only runs the assignment var tmp = mp._req; // we have mp._req, or else we'd have thrown delete mp._req; tmp.call(g, new_require(id), new_module.exports, new_module); } catch (e) { delete req_cache[id]; throw(e); }
return new_module.exports; };}
g.require = new_require(SCRIPTDIR_META + "/" + main_script[1]);
/********************************************************************** * mp.options *********************************************************************/function read_options(opts, id, on_update, conf_override) { id = String(id ? id : mp.get_script_name()); mp.msg.debug("reading options for " + id);
var conf, fname = "~~/script-opts/" + id + ".conf"; try { conf = arguments.length > 3 ? conf_override : mp.utils.read_file(fname); } catch (e) { mp.msg.verbose(fname + " not found."); }
// data as config file lines array, or empty array var data = conf ? conf.replace(/\r\n/g, "\n").split("\n") : [], conf_len = data.length; // before we append script-opts below
// Append relevant script-opts as <key-sans-id>=<value> to data var sopts = mp.get_property_native("options/script-opts"), prefix = id + "-"; for (var key in sopts) { if (key.indexOf(prefix) == 0) data.push(key.substring(prefix.length) + "=" + sopts[key]); }
// Update opts from data data.forEach(function(line, i) { if (line[0] == "#" || line.trim() == "") return;
var key = line.substring(0, line.indexOf("=")), val = line.substring(line.indexOf("=") + 1), type = typeof opts[key], info = i < conf_len ? fname + ":" + (i + 1) // 1-based line number : "script-opts:" + prefix + key;
if (!opts.hasOwnProperty(key)) mp.msg.warn(info, "Ignoring unknown key '" + key + "'"); else if (type == "string") opts[key] = val; else if (type == "boolean" && (val == "yes" || val == "no")) opts[key] = (val == "yes"); else if (type == "number" && val.trim() != "" && !isNaN(val)) opts[key] = Number(val); else mp.msg.error(info, "Error: can't convert '" + val + "' to " + type); });
if (on_update) { mp.observe_property("options/script-opts", "native", function(_n, _v) { var saved = JSON.parse(JSON.stringify(opts)); // clone var changelist = {}, changed = false; read_options(opts, id, 0, conf); // re-apply orig-file + script-opts for (var key in opts) { if (opts[key] != saved[key]) // type always stays the same changelist[key] = changed = true; } if (changed) on_update(changelist); }); }}
mp.options = { read_options: read_options };
/********************************************************************** * various *********************************************************************/g.print = mp.msg.info; // convenient aliasmp.get_script_name = function() { return mp.script_name };mp.get_script_file = function() { return mp.script_file };mp.get_script_directory = function() { return mp.script_path };mp.get_time = function() { return mp.get_time_ms() / 1000 };mp.utils.getcwd = function() { return mp.get_property("working-directory") };mp.dispatch_event = dispatch_event;mp.process_timers = process_timers;mp.notify_idle_observers = notify_idle_observers;mp.peek_timers_wait = peek_timers_wait;
mp.get_opt = function(key, def) { var v = mp.get_property_native("options/script-opts")[key]; return (typeof v != "undefined") ? v : def;}
mp.osd_message = function osd_message(text, duration) { mp.commandv("show_text", text, Math.round(1000 * (duration || -1)));}
mp.utils.subprocess = function subprocess(t) { var cmd = { name: "subprocess", capture_stdout: true }; var new_names = { cancellable: "playback_only", max_size: "capture_size" }; for (var k in t) cmd[new_names[k] || k] = t[k];
var rv = mp.command_native(cmd); if (mp.last_error()) /* typically on missing/incorrect args */ rv = { error_string: mp.last_error(), status: -1 }; if (rv.error_string) rv.error = rv.error_string; return rv;}
mp.utils.subprocess_detached = function subprocess_detached(t) { return mp.commandv.apply(null, ["run"].concat(t.args));}
// ----- dump: like print, but expands objects/arrays recursively -----function replacer(k, v) { var t = typeof v; if (t == "function" || t == "undefined") return "<" + t + ">"; if (Array.isArray(this) && t == "object" && v !== null) { // "safe" mode if (this.indexOf(v) >= 0) return "<VISITED>"; this.push(v); } return v;}
function obj2str(v) { try { // can process objects more than once, but throws on cycles return JSON.stringify(v, replacer.bind(null), 2); } catch (e) { // simple safe: exclude visited objects, even if not cyclic return JSON.stringify(v, replacer.bind([]), 2); }}
g.dump = function dump() { var toprint = []; for (var i = 0; i < arguments.length; i++) { var v = arguments[i]; toprint.push((typeof v == "object") ? obj2str(v) : replacer(0, v)); } print.apply(null, toprint);}
/********************************************************************** * main listeners and event loop *********************************************************************/mp.keep_running = true;g.exit = function() { mp.keep_running = false }; // user-facing toomp.register_event("shutdown", g.exit);mp.register_event("property-change", notify_observer);mp.register_event("hook", run_hook);mp.register_event("command-reply", async_command_handler);mp.register_event("client-message", dispatch_message);mp.register_script_message("key-binding", dispatch_key_binding);
g.mp_event_loop = function mp_event_loop() { var wait = 0; // seconds do { // distapch events as long as they arrive, then do the timers/idle var e = mp.wait_event(wait); if (e.event != "none") { dispatch_event(e); wait = 0; // poll the next one } else { wait = process_timers() / 1000; if (wait != 0) { notify_idle_observers(); // can add timers -> recalculate wait wait = peek_timers_wait() / 1000; } } } while (mp.keep_running);};
})(this)
try { // let the user extend us, e.g. for updating mp.module_paths require("~~/.init");} catch(e) {}