mirror of https://github.com/mpv-player/mpv
js: add javascript scripting support using MuJS
Implements JS with almost identical API to the Lua support. Key differences from Lua: - The global mp, mp.msg and mp.utils are always available. - Instead of returning x, error, return x and expose mp.last_error(). - Timers are JS standard set/clear Timeout/Interval. - Supports CommonJS modules/require. - Added at mp.utils: getenv, read_file, write_file and few more. - Global print and dump (expand objects) functions. - mp.options currently not supported. See DOCS/man/javascript.rst for more details.
This commit is contained in:
parent
82aa1ea87f
commit
d223a63bc5
|
@ -0,0 +1,336 @@
|
|||
JavaScript
|
||||
==========
|
||||
|
||||
JavaScript support in mpv is near identical to its Lua support. Use this section
|
||||
as reference on differences and availability of APIs, but otherwise you should
|
||||
refer to the Lua documentation for API details and general scripting in mpv.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
JavaScript code which leaves fullscreen mode when the player is paused:
|
||||
|
||||
::
|
||||
|
||||
function on_pause_change(name, value) {
|
||||
if (value == true)
|
||||
mp.set_property("fullscreen", "no");
|
||||
}
|
||||
mp.observe_property("pause", "bool", on_pause_change);
|
||||
|
||||
|
||||
Similarities with Lua
|
||||
---------------------
|
||||
|
||||
mpv tries to load a script file as JavaScript if it has a ``.js`` extension, but
|
||||
otherwise, the documented Lua options, script directories, loading, etc apply to
|
||||
JavaScript files too.
|
||||
|
||||
Script initialization and lifecycle is the same as with Lua, and most of the Lua
|
||||
functions at the modules ``mp``, ``mp.utils`` and ``mp.msg`` are available to
|
||||
JavaScript with identical APIs - including running commands, getting/setting
|
||||
properties, registering events/key-bindings/property-changes/hooks, etc.
|
||||
|
||||
Differences from Lua
|
||||
--------------------
|
||||
|
||||
No need to load modules. ``mp``, ``mp.utils`` and ``mp.msg`` are preloaded, and
|
||||
you can use e.g. ``var cwd = mp.utils.getcwd();`` without prior setup.
|
||||
``mp.options`` is currently not implemented, but ``mp.get_opt(...)`` is.
|
||||
|
||||
Errors are slightly different. Where the Lua APIs return ``nil`` for error,
|
||||
the JavaScript ones return ``undefined``. Where Lua returns ``something, error``
|
||||
JavaScript returns only ``something`` - and makes ``error`` available via
|
||||
``mp.last_error()``. Note that only some of the functions have this additional
|
||||
``error`` value - typically the same ones which have it in Lua.
|
||||
|
||||
Standard APIs are preferred. For instance ``setTimeout`` and ``JSON.stringify``
|
||||
are available, but ``mp.add_timeout`` and ``mp.utils.format_json`` are not.
|
||||
|
||||
No standard library. This means that interaction with anything outside of mpv is
|
||||
limited to the available APIs, typically via ``mp.utils``. However, some file
|
||||
functions were added, and CommonJS ``require`` is available too - where the
|
||||
loaded modules have the same privileges as normal scripts.
|
||||
|
||||
Language features - ECMAScript 5
|
||||
--------------------------------
|
||||
|
||||
The scripting backend which mpv currently uses is MuJS - a compatible minimal
|
||||
ES5 interpreter. As such, ``String.substring`` is implemented for instance,
|
||||
while the common but non-standard ``String.substr`` is not. Please consult the
|
||||
MuJS pages on language features and platform support - http://mujs.com .
|
||||
|
||||
Unsupported Lua APIs and their JS alternatives
|
||||
----------------------------------------------
|
||||
|
||||
``mp.add_timeout(seconds, fn)`` JS: ``id = setTimeout(fn, ms)``
|
||||
|
||||
``mp.add_periodic_timer(seconds, fn)`` JS: ``id = setInterval(fn, ms)``
|
||||
|
||||
``mp.register_idle(fn)`` JS: ``id = setTimeout(fn)``
|
||||
|
||||
``mp.unregister_idle(fn)`` JS: ``clearTimeout(id)``
|
||||
|
||||
``utils.parse_json(str [, trail])`` JS: ``JSON.parse(str)``
|
||||
|
||||
``utils.format_json(v)`` JS: ``JSON.stringify(v)``
|
||||
|
||||
``utils.to_string(v)`` see ``dump`` below.
|
||||
|
||||
``mp.suspend()`` JS: none (deprecated).
|
||||
|
||||
``mp.resume()`` JS: none (deprecated).
|
||||
|
||||
``mp.resume_all()`` JS: none (deprecated).
|
||||
|
||||
``mp.get_next_timeout()`` see event loop below.
|
||||
|
||||
``mp.dispatch_events([allow_wait])`` see event loop below.
|
||||
|
||||
``mp.options`` module is not implemented currently for JS.
|
||||
|
||||
Scripting APIs - identical to Lua
|
||||
---------------------------------
|
||||
|
||||
(LE) - Last-Error, indicates that ``mp.last_error()`` can be used after the
|
||||
call to test for success (empty string) or failure (non empty reason string).
|
||||
Otherwise, where the Lua APIs return ``nil`` on error, JS returns ``undefined``.
|
||||
|
||||
``mp.command(string)`` (LE)
|
||||
|
||||
``mp.commandv(arg1, arg2, ...)`` (LE)
|
||||
|
||||
``mp.command_native(table [,def])`` (LE)
|
||||
|
||||
``mp.get_property(name [,def])`` (LE)
|
||||
|
||||
``mp.get_property_osd(name [,def])`` (LE)
|
||||
|
||||
``mp.get_property_bool(name [,def])`` (LE)
|
||||
|
||||
``mp.get_property_number(name [,def])`` (LE)
|
||||
|
||||
``mp.get_property_native(name [,def])`` (LE)
|
||||
|
||||
``mp.set_property(name, value)`` (LE)
|
||||
|
||||
``mp.set_property_bool(name, value)`` (LE)
|
||||
|
||||
``mp.set_property_number(name, value)`` (LE)
|
||||
|
||||
``mp.set_property_native(name, value)`` (LE)
|
||||
|
||||
``mp.get_time()``
|
||||
|
||||
``mp.add_key_binding(key, name|fn [,fn [,flags]])``
|
||||
|
||||
``mp.add_forced_key_binding(...)``
|
||||
|
||||
``mp.remove_key_binding(name)``
|
||||
|
||||
``mp.register_event(name, fn)``
|
||||
|
||||
``mp.unregister_event(fn)``
|
||||
|
||||
``mp.observe_property(name, type, fn)``
|
||||
|
||||
``mp.unobserve_property(fn)``
|
||||
|
||||
``mp.get_opt(key)``
|
||||
|
||||
``mp.get_script_name()``
|
||||
|
||||
``mp.osd_message(text [,duration])``
|
||||
|
||||
``mp.get_wakeup_pipe()``
|
||||
|
||||
``mp.enable_messages(level)``
|
||||
|
||||
``mp.register_script_message(name, fn)``
|
||||
|
||||
``mp.unregister_script_message(name)``
|
||||
|
||||
``mp.msg.log(level, ...)``
|
||||
|
||||
``mp.msg.fatal(...)``
|
||||
|
||||
``mp.msg.error(...)``
|
||||
|
||||
``mp.msg.warn(...)``
|
||||
|
||||
``mp.msg.info(...)``
|
||||
|
||||
``mp.msg.verbose(...)``
|
||||
|
||||
``mp.msg.debug(...)``
|
||||
|
||||
``mp.utils.getcwd()`` (LE)
|
||||
|
||||
``mp.utils.readdir(path [, filter])`` (LE)
|
||||
|
||||
``mp.utils.split_path(path)``
|
||||
|
||||
``mp.utils.join_path(p1, p2)``
|
||||
|
||||
``mp.utils.subprocess(t)``
|
||||
|
||||
``mp.utils.subprocess_detached(t)``
|
||||
|
||||
``mp.add_hook(type, priority, fn)``
|
||||
|
||||
Additional utilities
|
||||
--------------------
|
||||
|
||||
``mp.last_error()``
|
||||
If used after an API call which updates last error, returns an empty string
|
||||
if the API call succeeded, or a non-empty error reason string otherwise.
|
||||
|
||||
``Error.stack`` (string)
|
||||
When using ``try { ... } catch(e) { ... }``, then ``e.stack`` is the stack
|
||||
trace of the error - if it was created using the ``Error(...)`` constructor.
|
||||
|
||||
``print`` (global)
|
||||
A convenient alias to ``mp.msg.info``.
|
||||
|
||||
``dump`` (global)
|
||||
Like ``print`` but also expands objects and arrays recursively.
|
||||
|
||||
``mp.utils.getenv(name)``
|
||||
Returns the value of the host environment variable ``name``, or empty str.
|
||||
|
||||
``mp.utils.get_user_path(path)``
|
||||
Expands (mpv) meta paths like ``~/x``, ``~~/y``, ``~~desktop/z`` etc.
|
||||
``read_file``, ``write_file`` and ``require`` already use this internaly.
|
||||
|
||||
``mp.utils.read_file(fname [,max])``
|
||||
Returns the content of file ``fname`` as string. If ``max`` is provided and
|
||||
not negative, limit the read to ``max`` bytes.
|
||||
|
||||
``mp.utils.write_file(fname, str)``
|
||||
(Over)write file ``fname`` with text content ``str``. ``fname`` must be
|
||||
prefixed with ``file://`` as simple protection against accidental arguments
|
||||
switch, e.g. ``mp.utils.write_file("file://~/abc.txt", "hello world")``.
|
||||
|
||||
Note: ``read_file`` and ``write_file`` throw on errors, allow text content only.
|
||||
|
||||
``mp.get_time_ms()``
|
||||
Same as ``mp.get_time()`` but in ms instead of seconds.
|
||||
|
||||
``mp.get_script_file()``
|
||||
Returns the file name of the current script.
|
||||
|
||||
``exit()`` (global)
|
||||
Make the script exit at the end of the current event loop iteration.
|
||||
Note: please reomve added key bindings before calling ``exit()``.
|
||||
|
||||
``mp.utils.compile_js(fname, content_str)``
|
||||
Compiles the JS code ``content_str`` as file name ``fname`` (without loading
|
||||
anything from the filesystem), and returns it as a function. Very similar
|
||||
to a ``Function`` constructor, but shows at stack traces as ``fname``.
|
||||
|
||||
Timers (global)
|
||||
---------------
|
||||
|
||||
The standard HTML/node.js timers are available:
|
||||
|
||||
``id = setTimeout(fn [,duration [,arg1 [,arg2...]]])``
|
||||
|
||||
``id = setTimeout(code_string [,duration])``
|
||||
|
||||
``clearTimeout(id)``
|
||||
|
||||
``id = setInterval(fn [,duration [,arg1 [,arg2...]]])``
|
||||
|
||||
``id = setInterval(code_string [,duration])``
|
||||
|
||||
``clearInterval(id)``
|
||||
|
||||
``setTimeout`` and ``setInterval`` return id, and later call ``fn`` (or execute
|
||||
``code_string``) after ``duration`` ms. Interval also repeat every ``duration``.
|
||||
|
||||
``duration`` has a minimum and default value of 0, ``code_string`` is
|
||||
a plain string which is evaluated as JS code, and ``[,arg1 [,arg2..]]`` are used
|
||||
as arguments (if provided) when calling back ``fn``.
|
||||
|
||||
The ``clear...(id)`` functions cancel timer ``id``, and are irreversible.
|
||||
|
||||
Note: timers always call back asynchronously, e.g. ``setTimeout(fn)`` will never
|
||||
call ``fn`` before returning. ``fn`` will be called either at the end of this
|
||||
event loop iteration or at a later event loop iteration. This is true also for
|
||||
intervals - which also never call back twice at the same event loop iteration.
|
||||
|
||||
Additionally, timers are processed after the event queue is empty, so it's valid
|
||||
to use ``setTimeout(fn)`` instead of Lua's ``mp.register_idle(fn)``.
|
||||
|
||||
CommonJS modules and ``require(id)``
|
||||
------------------------------------
|
||||
|
||||
CommonJS Modules are a standard system where scripts can export common functions
|
||||
for use by other scripts. A module is a script which adds properties (functions,
|
||||
etc) to its invisible ``exports`` object, which another script can access by
|
||||
loading it with ``require(module-id)`` - which returns that ``exports`` object.
|
||||
|
||||
Modules and ``require`` are supported, standard compliant, and generally similar
|
||||
to node.js. However, most node.js modules won't run due to missing modules such
|
||||
as ``fs``, ``process``, etc, but some node.js modules with minimal dependencies
|
||||
do work. In general, this is for mpv modules and not a node.js replacement.
|
||||
|
||||
A ``.js`` file extension is always added to ``id``, e.g. ``require("./foo")``
|
||||
will load the file ``./foo.js`` and return its ``exports`` object.
|
||||
|
||||
An id is relative (to the script which ``require``'d it) if it starts with
|
||||
``./`` or ``../``. Otherwise, it's considered a "top-level id" (CommonJS term).
|
||||
|
||||
Top level id is evaluated as absolute filesystem path if possible (e.g. ``/x/y``
|
||||
or ``~/x``). Otherwise, it's searched at ``scripts/modules.js/`` in mpv config
|
||||
dirs - in normal config search order. E.g. ``require("x")`` is searched as file
|
||||
``x.js`` at those dirs, and id ``foo/x`` is searched as file ``foo/x.js``.
|
||||
|
||||
No ``global`` variable, but a module's ``this`` at its top lexical scope is the
|
||||
global object - also in strict mode. If you have a module which needs ``global``
|
||||
as the global object, you could do ``this.global = this;`` before ``require``.
|
||||
|
||||
Functions and variables declared at a module don't pollute the global object.
|
||||
|
||||
The event loop
|
||||
--------------
|
||||
|
||||
The event loop poll/dispatch mpv events as long as the queue is not empty, then
|
||||
processes the timers, then waits for the next event, and repeats this forever.
|
||||
|
||||
You could put this code at your script to replace the built-in event loop, and
|
||||
also print every event which mpv sends to your script:
|
||||
|
||||
::
|
||||
|
||||
function mp_event_loop() {
|
||||
var wait = 0;
|
||||
do {
|
||||
var e = mp.wait_event(wait);
|
||||
dump(e); // there could be a lot of prints...
|
||||
if (e.event != "none") {
|
||||
mp.dispatch_event(e);
|
||||
wait = 0;
|
||||
} else {
|
||||
wait = mp.process_timers() / 1000;
|
||||
}
|
||||
} while (mp.keep_running);
|
||||
}
|
||||
|
||||
|
||||
``mp_event_loop`` is a name which mpv tries to call after the script loads.
|
||||
The internal implementation is similar to this (without ``dump`` though..).
|
||||
|
||||
``e = mp.wait_event(wait)`` returns when the next mpv event arrives, or after
|
||||
``wait`` seconds if positive and no mpv events arrived. ``wait`` value of 0
|
||||
returns immediately (with ``e.event == "none"`` if the queue is empty).
|
||||
|
||||
``mp.dispatch_event(e)`` calls back the handlers registered for ``e.event``,
|
||||
if there are such (event handlers, property observers, script messages, etc).
|
||||
|
||||
``mp.process_timers()`` calls back the already-added, non-canceled due timers,
|
||||
and returns the duration in ms till the next due timer (possibly 0), or -1 if
|
||||
there are no pending timers. Must not be called recursively.
|
||||
|
||||
Note: ``exit()`` is also registered for the ``shutdown`` event, and its
|
||||
implementation is a simple ``mp.keep_running = false``.
|
|
@ -847,6 +847,8 @@ works like in older mpv releases. The profiles are currently defined as follows:
|
|||
|
||||
.. include:: lua.rst
|
||||
|
||||
.. include:: javascript.rst
|
||||
|
||||
.. include:: ipc.rst
|
||||
|
||||
.. include:: changes.rst
|
||||
|
|
|
@ -308,14 +308,16 @@ const m_option_t mp_opts[] = {
|
|||
M_OPT_FIXED | CONF_NOCFG | CONF_PRE_PARSE | M_OPT_FILE),
|
||||
OPT_STRINGLIST("reset-on-next-file", reset_options, 0),
|
||||
|
||||
#if HAVE_LUA
|
||||
#if HAVE_LUA || HAVE_JAVASCRIPT
|
||||
OPT_STRINGLIST("script", script_files, M_OPT_FIXED | M_OPT_FILE),
|
||||
OPT_KEYVALUELIST("script-opts", script_opts, 0),
|
||||
OPT_FLAG("load-scripts", auto_load_scripts, 0),
|
||||
#endif
|
||||
#if HAVE_LUA
|
||||
OPT_FLAG("osc", lua_load_osc, UPDATE_BUILTIN_SCRIPTS),
|
||||
OPT_FLAG("ytdl", lua_load_ytdl, UPDATE_BUILTIN_SCRIPTS),
|
||||
OPT_STRING("ytdl-format", lua_ytdl_format, 0),
|
||||
OPT_KEYVALUELIST("ytdl-raw-options", lua_ytdl_raw_options, 0),
|
||||
OPT_FLAG("load-scripts", auto_load_scripts, 0),
|
||||
#endif
|
||||
|
||||
// ------------------------- stream options --------------------
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,495 @@
|
|||
"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"];
|
||||
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, client messages, hooks
|
||||
*********************************************************************/
|
||||
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 unregistered
|
||||
function 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----- 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 next_hid = 1,
|
||||
hooks = new_cache(); // items of id: fn
|
||||
|
||||
function hook_run(id, cont) {
|
||||
var cb = hooks[id];
|
||||
if (cb)
|
||||
cb();
|
||||
mp.commandv("hook-ack", cont);
|
||||
}
|
||||
|
||||
mp.add_hook = function add_hook(name, pri, fn) {
|
||||
if (next_hid == 1) // doesn't really matter if we do it once or always
|
||||
mp.register_script_message("hook_run", hook_run);
|
||||
var id = next_hid++;
|
||||
hooks[id] = fn;
|
||||
return mp.commandv("hook-add", name, id, pri);
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
* 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) {
|
||||
var cb = binds[name] ? binds[name].cb : false;
|
||||
if (cb) // "script-binding [<script_name>/]<name>" command was invoked
|
||||
cb(state);
|
||||
}
|
||||
|
||||
function update_input_sections() {
|
||||
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");
|
||||
}
|
||||
|
||||
// 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 = "__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) {
|
||||
fn({
|
||||
event: KEY_STATES[state[0]] || "unknown",
|
||||
is_mouse: state[1] == "m"
|
||||
});
|
||||
}
|
||||
} 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
|
||||
update_input_sections();
|
||||
}
|
||||
|
||||
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];
|
||||
update_input_sections();
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
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 wait
|
||||
function peek_wait(arr) {
|
||||
return arr.length ? Math.max(0, arr[arr.length - 1].when - now()) : -1;
|
||||
}
|
||||
|
||||
// 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
|
||||
*********************************************************************/
|
||||
|
||||
// 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) {
|
||||
var path = mp.find_config_file("scripts/modules.js/" + rest);
|
||||
if (!path)
|
||||
throw(Error("Cannot find module file '" + rest + "'"));
|
||||
return path;
|
||||
}
|
||||
|
||||
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's
|
||||
function 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]);
|
||||
|
||||
/**********************************************************************
|
||||
* various
|
||||
*********************************************************************/
|
||||
g.print = mp.msg.info; // convenient alias
|
||||
mp.get_script_name = function() { return mp.script_name };
|
||||
mp.get_script_file = function() { return mp.script_file };
|
||||
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.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)));
|
||||
}
|
||||
|
||||
// ----- 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, 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 too
|
||||
mp.register_event("shutdown", g.exit);
|
||||
mp.register_event("property-change", notify_observer);
|
||||
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
|
||||
var e = mp.wait_event(wait);
|
||||
if (e.event != "none") {
|
||||
dispatch_event(e);
|
||||
wait = 0; // poll the next one
|
||||
} else {
|
||||
wait = process_timers() / 1000;
|
||||
}
|
||||
} while (mp.keep_running);
|
||||
};
|
||||
|
||||
})(this)
|
|
@ -38,6 +38,7 @@
|
|||
|
||||
extern const struct mp_scripting mp_scripting_lua;
|
||||
extern const struct mp_scripting mp_scripting_cplugin;
|
||||
extern const struct mp_scripting mp_scripting_js;
|
||||
|
||||
static const struct mp_scripting *const scripting_backends[] = {
|
||||
#if HAVE_LUA
|
||||
|
@ -45,6 +46,9 @@ static const struct mp_scripting *const scripting_backends[] = {
|
|||
#endif
|
||||
#if HAVE_CPLUGINS
|
||||
&mp_scripting_cplugin,
|
||||
#endif
|
||||
#if HAVE_JAVASCRIPT
|
||||
&mp_scripting_js,
|
||||
#endif
|
||||
NULL
|
||||
};
|
||||
|
|
4
wscript
4
wscript
|
@ -289,6 +289,10 @@ iconv support use --disable-iconv.",
|
|||
'name' : '--lua',
|
||||
'desc' : 'Lua',
|
||||
'func': check_lua,
|
||||
}, {
|
||||
'name' : '--javascript',
|
||||
'desc' : 'Javascript (MuJS backend)',
|
||||
'func': check_statement('mujs.h', 'js_setreport(js_newstate(0, 0, 0), 0)', lib='mujs'),
|
||||
}, {
|
||||
'name': '--libass',
|
||||
'desc': 'SSA/ASS support',
|
||||
|
|
|
@ -92,6 +92,12 @@ def build(ctx):
|
|||
target = os.path.splitext(fn)[0] + ".inc",
|
||||
)
|
||||
|
||||
ctx(
|
||||
features = "file2string",
|
||||
source = "player/javascript/defaults.js",
|
||||
target = "player/javascript/defaults.js.inc",
|
||||
)
|
||||
|
||||
ctx(features = "ebml_header", target = "ebml_types.h")
|
||||
ctx(features = "ebml_definitions", target = "ebml_defs.c")
|
||||
|
||||
|
@ -237,6 +243,7 @@ def build(ctx):
|
|||
( "player/misc.c" ),
|
||||
( "player/lavfi.c" ),
|
||||
( "player/lua.c", "lua" ),
|
||||
( "player/javascript.c", "javascript" ),
|
||||
( "player/osd.c" ),
|
||||
( "player/playloop.c" ),
|
||||
( "player/screenshot.c" ),
|
||||
|
|
Loading…
Reference in New Issue