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:
Avi Halachmi (:avih) 2014-12-13 18:27:47 +02:00 committed by wm4
parent 82aa1ea87f
commit d223a63bc5
8 changed files with 2159 additions and 2 deletions

336
DOCS/man/javascript.rst Normal file
View File

@ -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``.

View File

@ -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

View File

@ -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 --------------------

1307
player/javascript.c Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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)

View File

@ -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
};

View File

@ -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',

View File

@ -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" ),