History

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.

Avi Halachmi (:avih) committed 11 years ago

Commit d223a631 parent 82aa1ea8 files · +2159 −2Browse files

Showing the changes to player/javascript (1 of 8 files). Show all

Unified · Split

player/javascript/defaults.js+495−0
@@ -0,0 +1,495 @@
1+"use strict";
2+(function main_default_js(g) {
3+// - g is the global object.
4+// - User callbacks called without 'this', global only if callee is non-strict.
5+// - The names of function expressions are not required, but are used in stack
6+// traces. We name them where useful to show up (fname:#line always shows).
7+
8+mp.msg = { log: mp.log };
9+mp.msg.verbose = mp.log.bind(null, "v");
10+var levels = ["fatal", "error", "warn", "info", "debug"];
11+levels.forEach(function(l) { mp.msg[l] = mp.log.bind(null, l) });
12+
13+// same as {} but without inherited stuff, e.g. o["toString"] doesn't exist.
14+// used where we try to fetch items by keys which we don't absolutely trust.
15+function new_cache() {
16+ return Object.create(null, {});
17+}
18+
19+/**********************************************************************
20+ * event handlers, property observers, client messages, hooks
21+ *********************************************************************/
22+var ehandlers = new_cache() // items of event-name: array of {maybe cb: fn}
23+
24+mp.register_event = function(name, fn) {
25+ if (!ehandlers[name])
26+ ehandlers[name] = [];
27+ ehandlers[name] = ehandlers[name].concat([{cb: fn}]); // replaces the arr
28+ return mp._request_event(name, true);
29+}
30+
31+mp.unregister_event = function(fn) {
32+ for (var name in ehandlers) {
33+ ehandlers[name] = ehandlers[name].filter(function(h) {
34+ if (h.cb != fn)
35+ return true;
36+ delete h.cb; // dispatch could have a ref to h
37+ }); // replacing, not mutating the array
38+ if (!ehandlers[name].length) {
39+ delete ehandlers[name];
40+ mp._request_event(name, false);
41+ }
42+ }
43+}
44+
45+// call only pre-registered handlers, but not ones which got unregistered
46+function dispatch_event(e) {
47+ var handlers = ehandlers[e.event];
48+ if (handlers) {
49+ for (var len = handlers.length, i = 0; i < len; i++) {
50+ var cb = handlers[i].cb; // 'handlers' won't mutate, but unregister
51+ if (cb) // could remove cb from some items
52+ cb(e);
53+ }
54+ }
55+}
56+
57+// ----- property observers -----
58+var next_oid = 1,
59+ observers = new_cache(); // items of id: fn
60+
61+mp.observe_property = function(name, format, fn) {
62+ var id = next_oid++;
63+ observers[id] = fn;
64+ return mp._observe_property(id, name, format || undefined); // allow null
65+}
66+
67+mp.unobserve_property = function(fn) {
68+ for (var id in observers) {
69+ if (observers[id] == fn) {
70+ delete observers[id];
71+ mp._unobserve_property(id);
72+ }
73+ }
74+}
75+
76+function notify_observer(e) {
77+ var cb = observers[e.id];
78+ if (cb)
79+ cb(e.name, e.data);
80+}
81+
82+// ----- Client messages -----
83+var messages = new_cache(); // items of name: fn
84+
85+// overrides name. no libmpv API to reg/unreg specific messages.
86+mp.register_script_message = function(name, fn) {
87+ messages[name] = fn;
88+}
89+
90+mp.unregister_script_message = function(name) {
91+ delete messages[name];
92+}
93+
94+function dispatch_message(ev) {
95+ var cb = ev.args.length ? messages[ev.args[0]] : false;
96+ if (cb)
97+ cb.apply(null, ev.args.slice(1));
98+}
99+
100+// ----- hooks -----
101+var next_hid = 1,
102+ hooks = new_cache(); // items of id: fn
103+
104+function hook_run(id, cont) {
105+ var cb = hooks[id];
106+ if (cb)
107+ cb();
108+ mp.commandv("hook-ack", cont);
109+}
110+
111+mp.add_hook = function add_hook(name, pri, fn) {
112+ if (next_hid == 1) // doesn't really matter if we do it once or always
113+ mp.register_script_message("hook_run", hook_run);
114+ var id = next_hid++;
115+ hooks[id] = fn;
116+ return mp.commandv("hook-add", name, id, pri);
117+}
118+
119+/**********************************************************************
120+ * key bindings
121+ *********************************************************************/
122+// binds: items of (binding) name which are objects of:
123+// {cb: fn, forced: bool, maybe input: str, repeatable: bool, complex: bool}
124+var binds = new_cache();
125+
126+function dispatch_key_binding(name, state) {
127+ var cb = binds[name] ? binds[name].cb : false;
128+ if (cb) // "script-binding [<script_name>/]<name>" command was invoked
129+ cb(state);
130+}
131+
132+function update_input_sections() {
133+ var def = [], forced = [];
134+ for (var n in binds) // Array.join() will later skip undefined .input
135+ (binds[n].forced ? forced : def).push(binds[n].input);
136+
137+ var sect = "input_" + mp.script_name;
138+ mp.commandv("define-section", sect, def.join("\n"), "default");
139+ mp.commandv("enable-section", sect, "allow-hide-cursor+allow-vo-dragging");
140+
141+ sect = "input_forced_" + mp.script_name;
142+ mp.commandv("define-section", sect, forced.join("\n"), "force");
143+ mp.commandv("enable-section", sect, "allow-hide-cursor+allow-vo-dragging");
144+}
145+
146+// name/opts maybe omitted. opts: object with optional bool members: repeatable,
147+// complex, forced, or a string str which is evaluated as object {str: true}.
148+var next_bid = 1;
149+function add_binding(forced, key, name, fn, opts) {
150+ if (typeof name == "function") { // as if "name" is not part of the args
151+ opts = fn;
152+ fn = name;
153+ name = "__keybinding" + next_bid++; // new unique binding name
154+ }
155+ var key_data = {forced: forced};
156+ switch (typeof opts) { // merge opts into key_data
157+ case "string": key_data[opts] = true; break;
158+ case "object": for (var o in opts) key_data[o] = opts[o];
159+ }
160+
161+ if (key_data.complex) {
162+ mp.register_script_message(name, function msg_cb() {
163+ fn({event: "press", is_mouse: false});
164+ });
165+ var KEY_STATES = { u: "up", d: "down", r: "repeat", p: "press" };
166+ key_data.cb = function key_cb(state) {
167+ fn({
168+ event: KEY_STATES[state[0]] || "unknown",
169+ is_mouse: state[1] == "m"
170+ });
171+ }
172+ } else {
173+ mp.register_script_message(name, fn);
174+ key_data.cb = function key_cb(state) {
175+ // Emulate the semantics at input.c: mouse emits on up, kb on down.
176+ // Also, key repeat triggers the binding again.
177+ var e = state[0],
178+ emit = (state[1] == "m") ? (e == "u") : (e == "d");
179+ if (emit || e == "p" || e == "r" && key_data.repeatable)
180+ fn();
181+ }
182+ }
183+
184+ if (key)
185+ key_data.input = key + " script-binding " + mp.script_name + "/" + name;
186+ binds[name] = key_data; // used by user and/or our (key) script-binding
187+ update_input_sections();
188+}
189+
190+mp.add_key_binding = add_binding.bind(null, false);
191+mp.add_forced_key_binding = add_binding.bind(null, true);
192+
193+mp.remove_key_binding = function(name) {
194+ mp.unregister_script_message(name);
195+ delete binds[name];
196+ update_input_sections();
197+}
198+
199+/**********************************************************************
200+ Timers: compatible HTML5 WindowTimers - set/clear Timeout/Interval
201+ - Spec: https://www.w3.org/TR/html5/webappapis.html#timers
202+ - Guaranteed to callback a-sync to [re-]insertion (event-loop wise).
203+ - Guaranteed to callback by expiration order, or, if equal, by insertion order.
204+ - Not guaranteed schedule accuracy, though intervals should have good average.
205+ *********************************************************************/
206+
207+// pending 'timers' ordered by expiration: latest at index 0 (top fires first).
208+// Earlier timers are quicker to handle - just push/pop or fewer items to shift.
209+var next_tid = 1,
210+ timers = [], // while in process_timers, just insertion-ordered (push)
211+ tset_is_push = false, // signal set_timer that we're in process_timers
212+ tcanceled = false, // or object of items timer-id: true
213+ now = mp.get_time_ms; // just an alias
214+
215+function insert_sorted(arr, t) {
216+ for (var i = arr.length - 1; i >= 0 && t.when >= arr[i].when; i--)
217+ arr[i + 1] = arr[i]; // move up timers which fire earlier than t
218+ arr[i + 1] = t; // i is -1 or fires later than t
219+}
220+
221+// args (is "arguments"): fn_or_str [,duration [,user_arg1 [, user_arg2 ...]]]
222+function set_timer(repeat, args) {
223+ var fos = args[0],
224+ duration = Math.max(0, (args[1] || 0)), // minimum and default are 0
225+ t = {
226+ id: next_tid++,
227+ when: now() + duration,
228+ interval: repeat ? duration : -1,
229+ callback: (typeof fos == "function") ? fos : Function(fos),
230+ args: (args.length < 3) ? false : [].slice.call(args, 2),
231+ };
232+
233+ if (tset_is_push) {
234+ timers.push(t);
235+ } else {
236+ insert_sorted(timers, t);
237+ }
238+ return t.id;
239+}
240+
241+g.setTimeout = function setTimeout() { return set_timer(false, arguments) };
242+g.setInterval = function setInterval() { return set_timer(true, arguments) };
243+
244+g.clearTimeout = g.clearInterval = function(id) {
245+ if (id < next_tid) { // must ignore if not active timer id.
246+ if (!tcanceled)
247+ tcanceled = {};
248+ tcanceled[id] = true;
249+ }
250+}
251+
252+// arr: ordered timers array. ret: -1: no timers, 0: due, positive: ms to wait
253+function peek_wait(arr) {
254+ return arr.length ? Math.max(0, arr[arr.length - 1].when - now()) : -1;
255+}
256+
257+// Callback all due non-canceled timers which were inserted before calling us.
258+// Returns wait in ms till the next timer (possibly 0), or -1 if nothing pends.
259+function process_timers() {
260+ var wait = peek_wait(timers);
261+ if (wait != 0)
262+ return wait;
263+
264+ var actives = timers; // only process those already inserted by now
265+ timers = []; // we'll handle added new timers at the end of processing.
266+ tset_is_push = true; // signal set_timer to just push-insert
267+
268+ do {
269+ var t = actives.pop();
270+ if (tcanceled && tcanceled[t.id])
271+ continue;
272+
273+ if (t.args) {
274+ t.callback.apply(null, t.args);
275+ } else {
276+ (0, t.callback)(); // faster, nicer stack trace than t.cb.call()
277+ }
278+
279+ if (t.interval >= 0) {
280+ // allow 20 ms delay/clock-resolution/gc before we skip and reset
281+ t.when = Math.max(now() - 20, t.when + t.interval);
282+ timers.push(t); // insertion order only
283+ }
284+ } while (peek_wait(actives) == 0);
285+
286+ // new 'timers' are insertion-ordered. remains of actives are fully ordered
287+ timers.forEach(function(t) { insert_sorted(actives, t) });
288+ timers = actives; // now we're fully ordered again, and with all timers
289+ tset_is_push = false;
290+ if (tcanceled) {
291+ timers = timers.filter(function(t) { return !tcanceled[t.id] });
292+ tcanceled = false;
293+ }
294+ return peek_wait(timers);
295+}
296+
297+/**********************************************************************
298+ CommonJS module/require
299+
300+ Spec: http://wiki.commonjs.org/wiki/Modules/1.1.1
301+ - All the mandatory requirements are implemented, all the unit tests pass.
302+ - The implementation makes the following exception:
303+ - Allows the chars [~@:\\] in module id for meta-dir/builtin/dos-drive/UNC.
304+
305+ Implementation choices beyond the specification:
306+ - A module may assign to module.exports (rather than only to exports).
307+ - A module's 'this' is the global object, also if it sets strict mode.
308+ - No 'global'/'self'. Users can do "this.global = this;" before require(..)
309+ - A module has "privacy of its top scope", runs in its own function context.
310+ - No id identity with symlinks - a valid choice which others make too.
311+ - require("X") always maps to "X.js" -> require("foo.js") is file "foo.js.js".
312+ - Global modules search paths are 'scripts/modules.js/' in mpv config dirs.
313+ - A main script could e.g. require("./abc") to load a non-global module.
314+ - Module id supports mpv path enhancements, e.g. ~/foo, ~~/bar, ~~desktop/baz
315+ *********************************************************************/
316+
317+// Internal meta top-dirs. Users should not rely on these names.
318+var MODULES_META = "~~modules",
319+ SCRIPTDIR_META = "~~scriptdir", // relative script path -> meta absolute id
320+ main_script = mp.utils.split_path(mp.script_file); // -> [ path, file ]
321+
322+function resolve_module_file(id) {
323+ var sep = id.indexOf("/"),
324+ base = id.substring(0, sep),
325+ rest = id.substring(sep + 1) + ".js";
326+
327+ if (base == SCRIPTDIR_META)
328+ return mp.utils.join_path(main_script[0], rest);
329+
330+ if (base == MODULES_META) {
331+ var path = mp.find_config_file("scripts/modules.js/" + rest);
332+ if (!path)
333+ throw(Error("Cannot find module file '" + rest + "'"));
334+ return path;
335+ }
336+
337+ return id + ".js";
338+}
339+
340+// Delimiter '/', remove redundancies, prefix with modules meta-root if needed.
341+// E.g. c:\x -> c:/x, or ./x//y/../z -> ./x/z, or utils/x -> ~~modules/utils/x .
342+function canonicalize(id) {
343+ var path = id.replace(/\\/g,"/").split("/"),
344+ t = path[0],
345+ base = [];
346+
347+ // if not strictly relative then must be top-level. figure out base/rest
348+ if (t != "." && t != "..") {
349+ // global module if it's not fs-root/home/dos-drive/builtin/meta-dir
350+ if (!(t == "" || t == "~" || t[1] == ":" || t == "@" || t.match(/^~~/)))
351+ path.unshift(MODULES_META); // add an explicit modules meta-root
352+
353+ if (id.match(/^\\\\/)) // simple UNC handling, preserve leading \\srv
354+ path = ["\\\\" + path[2]].concat(path.slice(3)); // [ \\srv, shr..]
355+
356+ if (t[1] == ":" && t.length > 2) { // path: [ "c:relative", "path" ]
357+ path[0] = t.substring(2);
358+ path.unshift(t[0] + ":."); // -> [ "c:.", "relative", "path" ]
359+ }
360+ base = [path.shift()];
361+ }
362+
363+ // path is now logically relative. base, if not empty, is its [meta] root.
364+ // normalize the relative part - always id-based (spec Module Id, 1.3.6).
365+ var cr = []; // canonicalized relative
366+ for (var i = 0; i < path.length; i++) {
367+ if (path[i] == "." || path[i] == "")
368+ continue;
369+ if (path[i] == ".." && cr.length && cr[cr.length - 1] != "..") {
370+ cr.pop();
371+ continue;
372+ }
373+ cr.push(path[i]);
374+ }
375+
376+ if (!base.length && cr[0] != "..")
377+ base = ["."]; // relative and not ../<stuff> so must start with ./
378+ return base.concat(cr).join("/");
379+}
380+
381+function resolve_module_id(base_id, new_id) {
382+ new_id = canonicalize(new_id);
383+ if (!new_id.match(/^\.\/|^\.\.\//)) // doesn't start with ./ or ../
384+ return new_id; // not relative, we don't care about base_id
385+
386+ var combined = mp.utils.join_path(mp.utils.split_path(base_id)[0], new_id);
387+ return canonicalize(combined);
388+}
389+
390+var req_cache = new_cache(); // global for all instances of require
391+
392+// ret: a require function instance which uses base_id to resolve relative id's
393+function new_require(base_id) {
394+ return function require(id) {
395+ id = resolve_module_id(base_id, id); // id is now top-level
396+ if (req_cache[id])
397+ return req_cache[id].exports;
398+
399+ var new_module = {id: id, exports: {}};
400+ req_cache[id] = new_module;
401+ try {
402+ var filename = resolve_module_file(id);
403+ // we need dedicated free vars + filename in traces + allow strict
404+ var str = "mp._req = function(require, exports, module) {" +
405+ mp.utils.read_file(filename) +
406+ "\n;}";
407+ mp.utils.compile_js(filename, str)(); // only runs the assignment
408+ var tmp = mp._req; // we have mp._req, or else we'd have thrown
409+ delete mp._req;
410+ tmp.call(g, new_require(id), new_module.exports, new_module);
411+ } catch (e) {
412+ delete req_cache[id];
413+ throw(e);
414+ }
415+
416+ return new_module.exports;
417+ };
418+}
419+
420+g.require = new_require(SCRIPTDIR_META + "/" + main_script[1]);
421+
422+/**********************************************************************
423+ * various
424+ *********************************************************************/
425+g.print = mp.msg.info; // convenient alias
426+mp.get_script_name = function() { return mp.script_name };
427+mp.get_script_file = function() { return mp.script_file };
428+mp.get_time = function() { return mp.get_time_ms() / 1000 };
429+mp.utils.getcwd = function() { return mp.get_property("working-directory") };
430+mp.dispatch_event = dispatch_event;
431+mp.process_timers = process_timers;
432+
433+mp.get_opt = function(key, def) {
434+ var v = mp.get_property_native("options/script-opts")[key];
435+ return (typeof v != "undefined") ? v : def;
436+}
437+
438+mp.osd_message = function osd_message(text, duration) {
439+ mp.commandv("show_text", text, Math.round(1000 * (duration || -1)));
440+}
441+
442+// ----- dump: like print, but expands objects/arrays recursively -----
443+function replacer(k, v) {
444+ var t = typeof v;
445+ if (t == "function" || t == "undefined")
446+ return "<" + t + ">";
447+ if (Array.isArray(this) && t == "object" && v !== null) { // "safe" mode
448+ if (this.indexOf(v) >= 0)
449+ return "<VISITED>";
450+ this.push(v);
451+ }
452+ return v;
453+}
454+
455+function obj2str(v) {
456+ try { // can process objects more than once, but throws on cycles
457+ return JSON.stringify(v, replacer, 2);
458+ } catch (e) { // simple safe: exclude visited objects, even if not cyclic
459+ return JSON.stringify(v, replacer.bind([]), 2);
460+ }
461+}
462+
463+g.dump = function dump() {
464+ var toprint = [];
465+ for (var i = 0; i < arguments.length; i++) {
466+ var v = arguments[i];
467+ toprint.push((typeof v == "object") ? obj2str(v) : replacer(0, v));
468+ }
469+ print.apply(null, toprint);
470+}
471+
472+/**********************************************************************
473+ * main listeners and event loop
474+ *********************************************************************/
475+mp.keep_running = true;
476+g.exit = function() { mp.keep_running = false }; // user-facing too
477+mp.register_event("shutdown", g.exit);
478+mp.register_event("property-change", notify_observer);
479+mp.register_event("client-message", dispatch_message);
480+mp.register_script_message("key-binding", dispatch_key_binding);
481+
482+g.mp_event_loop = function mp_event_loop() {
483+ var wait = 0; // seconds
484+ do { // distapch events as long as they arrive, then do the timers
485+ var e = mp.wait_event(wait);
486+ if (e.event != "none") {
487+ dispatch_event(e);
488+ wait = 0; // poll the next one
489+ } else {
490+ wait = process_timers() / 1000;
491+ }
492+ } while (mp.keep_running);
493+};
494+
495+})(this)

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t