summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorStefano Pigozzi <stefano.pigozzi@gmail.com>2014-07-13 17:35:45 +0200
committerStefano Pigozzi <stefano.pigozzi@gmail.com>2017-12-29 11:26:57 +0100
commit5a5cc02793ad28ecdc50d3df66b7913a0b467ac6 (patch)
tree75bce93cd8180f7cd26547f72db02b046fdcff48
parentd9ca235c68038629807fbcf21fd679cadbb8eba8 (diff)
downloadmpv-5a5cc02793ad28ecdc50d3df66b7913a0b467ac6.tar.bz2
mpv-5a5cc02793ad28ecdc50d3df66b7913a0b467ac6.tar.xz
tmp: mruby support
-rw-r--r--player/mruby.c506
-rw-r--r--player/mruby/events.mrb221
-rw-r--r--player/mruby/example.mrb27
-rw-r--r--player/mruby/logging.mrb21
-rw-r--r--player/mruby/reply.mrb37
-rw-r--r--player/mruby/test.mrb83
-rwxr-xr-xplayer/mruby/test.sh2
-rw-r--r--player/scripting.c4
-rw-r--r--wscript4
-rw-r--r--wscript_build.py11
10 files changed, 915 insertions, 1 deletions
diff --git a/player/mruby.c b/player/mruby.c
new file mode 100644
index 0000000000..f82fa54aaa
--- /dev/null
+++ b/player/mruby.c
@@ -0,0 +1,506 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <mruby.h>
+#include <mruby/array.h>
+#include <mruby/compile.h>
+#include <mruby/error.h>
+#include <mruby/hash.h>
+#include <mruby/string.h>
+#include <mruby/variable.h>
+
+#include "common/msg.h"
+#include "common/msg_control.h"
+#include "options/m_property.h"
+#include "options/path.h"
+#include "player/command.h"
+#include "player/core.h"
+#include "player/client.h"
+#include "libmpv/client.h"
+#include "mpv_talloc.h"
+
+static const char * const mruby_scripts[][2] = {
+ {"mpv/reply.mrb",
+# include "player/mruby/reply.inc"
+ },
+ {"mpv/logging.mrb",
+# include "player/mruby/logging.inc"
+ },
+ {"mpv/events.mrb",
+# include "player/mruby/events.inc"
+ },
+ {0}
+};
+
+struct script_ctx {
+ mrb_state *state;
+
+ const char *name;
+ const char *filename;
+ struct mp_log *log;
+ struct mpv_handle *client;
+ struct MPContext *mpctx;
+};
+
+static struct script_ctx *get_ctx(mrb_state *mrb)
+{
+ mrb_sym sym = mrb_intern_cstr(mrb, "mpctx");
+ mrb_value mrbctx = mrb_vm_const_get(mrb, sym);;
+ return mrb_cptr(mrbctx);
+}
+
+static int get_loglevel(char *level)
+{
+ for (int n = 0; n < MSGL_MAX; n++) {
+ if (mp_log_levels[n] && strcasecmp(mp_log_levels[n], level) == 0)
+ return n;
+ }
+ abort();
+}
+
+static mrb_value api_return(mrb_state *mrb, int err, mrb_value value)
+{
+ const char* status = mpv_error_string(err);
+ struct RClass *M = mrb_module_get(mrb, "M");
+ struct RClass *c = mrb_class_get_under(mrb, M, "Reply");
+ mrb_value init_args[2] = { value, mrb_str_new_cstr(mrb, status) };
+ return mrb_obj_new(mrb, c, MP_ARRAY_SIZE(init_args), init_args);
+}
+
+#define api_return_bool(mrb, err) api_return(mrb, err, mrb_bool_value(err >= 0))
+#define api_return_val(mrb, err, val) \
+ api_return(mrb, err, err >= 0 ? val : mrb_nil_value())
+
+static mrb_value _log(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ char *string;
+ char *level;
+ mrb_get_args(mrb, "zz", &level, &string);
+ mp_msg(ctx->log, get_loglevel(level), "%s", string);
+ return mrb_nil_value();
+}
+
+static mrb_value _find_config_file(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ char *s;
+ mrb_get_args(mrb, "z", &s);
+ char *path = mp_find_config_file(NULL, ctx->mpctx->global, s);
+ mrb_value r = path ? mrb_str_new_cstr(mrb, path) : mrb_nil_value();
+ talloc_free(path);
+ return api_return_val(mrb, 0, r);
+}
+
+static mrb_value mpv_to_mrb_root(mrb_state *mrb, mpv_node node, bool root)
+{
+ switch (node.format) {
+ case MPV_FORMAT_STRING:
+ return mrb_str_new_cstr(mrb, node.u.string);
+ case MPV_FORMAT_FLAG:
+ return mrb_bool_value(node.u.flag > 0);
+ case MPV_FORMAT_INT64:
+ return mrb_fixnum_value(node.u.int64);
+ case MPV_FORMAT_DOUBLE:
+ return mrb_float_value(mrb, node.u.double_);
+ case MPV_FORMAT_NODE_ARRAY: {
+ mrb_value ary = mrb_ary_new(mrb);
+ int ai = mrb_gc_arena_save(mrb);
+ for (int n = 0; n < node.u.list->num; n++) {
+ mrb_value item = mpv_to_mrb_root(mrb, node.u.list->values[n], false);
+ mrb_ary_push(mrb, ary, item);
+ }
+ if (root)
+ mrb_gc_arena_restore(mrb, ai);
+ return ary;
+ }
+ case MPV_FORMAT_NODE_MAP: {
+ mrb_value hash = mrb_hash_new(mrb);
+ int ai = mrb_gc_arena_save(mrb);
+ for (int n = 0; n < node.u.list->num; n++) {
+ mrb_value key = mrb_str_new_cstr(mrb, node.u.list->keys[n]);
+ mrb_value val = mpv_to_mrb_root(mrb, node.u.list->values[n], false);
+ mrb_hash_set(mrb, hash, key, val);
+ }
+ if (root)
+ mrb_gc_arena_restore(mrb, ai);
+ return hash;
+ }
+ default: {
+ struct script_ctx *ctx = get_ctx(mrb);
+ MP_ERR(ctx, "mpv_node mapping failed (format: %d).\n", node.format);
+ return mrb_nil_value();
+ }
+ }
+}
+
+#define mpv_to_mrb(mrb, node) mpv_to_mrb_root(mrb, node, true)
+
+static mrb_value _get_property(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ char *name;
+ mrb_get_args(mrb, "z", &name);
+ mpv_node node;
+ int err = mpv_get_property(ctx->client, name, MPV_FORMAT_NODE, &node);
+ return api_return_val(mrb, err, mpv_to_mrb(mrb, node));
+}
+
+static mpv_node mrb_to_mpv(void *ta_ctx, mrb_state *mrb, mrb_value value)
+{
+ mpv_node res;
+ switch (mrb_type(value)) {
+ case MRB_TT_TRUE:
+ res.format = MPV_FORMAT_FLAG;
+ res.u.flag = 1;
+ break;
+ case MRB_TT_FALSE: {
+ // MRB_TT_FALSE is used for both `nil` and `false`
+ if (mrb_nil_p(value)) {
+ res.format = MPV_FORMAT_NONE;
+ } else {
+ res.format = MPV_FORMAT_FLAG;
+ res.u.flag = 0;
+ }
+ break;
+ }
+ case MRB_TT_FIXNUM:
+ res.format = MPV_FORMAT_INT64;
+ res.u.int64 = mrb_fixnum(value);
+ break;
+ case MRB_TT_FLOAT:
+ res.format = MPV_FORMAT_DOUBLE;
+ res.u.double_ = mrb_float(value);
+ break;
+ case MRB_TT_STRING:
+ res.format = MPV_FORMAT_STRING;
+ res.u.string = talloc_strdup(ta_ctx, RSTRING_PTR(value));
+ break;
+ case MRB_TT_ARRAY: {
+ mpv_node_list *list = talloc_zero(ta_ctx, mpv_node_list);
+ res.format = MPV_FORMAT_NODE_ARRAY;
+ res.u.list = list;
+ mrb_int len = mrb_ary_len(mrb, value);
+ for (int i = 0; i < len; i++) {
+ MP_TARRAY_GROW(ta_ctx, list->values, list->num);
+ mrb_value item = mrb_ary_entry(value, i);
+ list->values[i] = mrb_to_mpv(ta_ctx, mrb, item);
+ list->num++;
+ }
+ break;
+ }
+ case MRB_TT_HASH: {
+ mpv_node_list *list = talloc_zero(ta_ctx, mpv_node_list);
+ res.format = MPV_FORMAT_NODE_MAP;
+ res.u.list = list;
+
+ mrb_value keys = mrb_hash_keys(mrb, value);
+ mrb_int len = mrb_ary_len(mrb, mrb_hash_keys(mrb, value));
+ for (int i = 0; i < len; i++) {
+ MP_TARRAY_GROW(ta_ctx, list->keys, list->num);
+ MP_TARRAY_GROW(ta_ctx, list->values, list->num);
+ mrb_value key = mrb_ary_entry(keys, i);
+ mrb_value skey = mrb_funcall(mrb, key, "to_s", 0);
+ mrb_value item = mrb_hash_get(mrb, value, key);
+ list->keys[i] = talloc_strdup(ta_ctx, RSTRING_PTR(skey));
+ list->values[i] = mrb_to_mpv(ta_ctx, mrb, item);
+ list->num++;
+ }
+ break;
+ }
+ default: {
+ struct script_ctx *ctx = get_ctx(mrb);
+ MP_ERR(ctx, "mrb_value mapping failed (class: %s).\n",
+ mrb_obj_classname(mrb, value));
+ }
+ }
+ return res;
+}
+
+static mrb_value _set_property(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ char *key;
+ mrb_value value;
+ mrb_get_args(mrb, "zo", &key, &value);
+
+ void *ta_ctx = talloc_new(NULL);
+ mpv_node node = mrb_to_mpv(ta_ctx, mrb, value);
+ int res = mpv_set_property(ctx->client, key, MPV_FORMAT_NODE, &node);
+ talloc_free(ta_ctx);
+ return api_return_bool(mrb, res);
+}
+
+#define mrb_hash_set_str(h, k, v) \
+ mrb_hash_set(mrb, h, mrb_str_new_cstr(mrb, k), mrb_str_new_cstr(mrb, v))
+
+static mrb_value _wait_event(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ mrb_float timeout;
+ mrb_get_args(mrb, "f", &timeout);
+ mpv_event *event = mpv_wait_event(ctx->client, timeout);
+
+ struct RClass *M = mrb_module_get(mrb, "M");
+ struct RClass *c = mrb_class_get_under(mrb, M, "Event");
+
+ mrb_value data = mrb_hash_new(mrb);
+
+ switch (event->event_id) {
+ case MPV_EVENT_LOG_MESSAGE: {
+ mpv_event_log_message *msg = event->data;
+ mrb_hash_set_str(data, "prefix", msg->prefix);
+ mrb_hash_set_str(data, "level", msg->level);
+ mrb_hash_set_str(data, "text", msg->text);
+ break;
+ }
+ case MPV_EVENT_SCRIPT_INPUT_DISPATCH: {
+ mpv_event_script_input_dispatch *msg = event->data;
+ mrb_value arg0 = mrb_fixnum_value(msg->arg0);
+ mrb_hash_set(mrb, data, mrb_str_new_cstr(mrb, "arg0"), arg0);
+ mrb_hash_set_str(data, "type", msg->type);
+ break;
+ }
+ case MPV_EVENT_CLIENT_MESSAGE: {
+ mpv_event_client_message *msg = event->data;
+ mrb_value args = mrb_ary_new(mrb);
+ for (int n = 0; n < msg->num_args; n++)
+ mrb_ary_push(mrb, args, mrb_str_new_cstr(mrb, msg->args[n]));
+ mrb_hash_set(mrb, data, mrb_str_new_cstr(mrb, "args"), args);
+ break;
+ }
+ case MPV_EVENT_PROPERTY_CHANGE: {
+ mpv_event_property *prop = event->data;
+ mrb_hash_set_str(data, "name", prop->name);
+ mpv_node node;
+
+ if (prop->format == MPV_FORMAT_NODE) {
+ node = *(mpv_node*)prop->data;
+ } else {
+ node = (mpv_node) { .format = MPV_FORMAT_NONE };
+ }
+
+ mrb_value value = mpv_to_mrb(mrb, node);
+ mrb_hash_set(mrb, data, mrb_str_new_cstr(mrb, "value"), value);
+ }
+ default: ;
+ }
+
+ mrb_value init_args[4] = {
+ mrb_fixnum_value(event->reply_userdata),
+ mrb_str_new_cstr(mrb, mpv_event_name(event->event_id)),
+ mrb_str_new_cstr(mrb, mpv_error_string(event->error)),
+ data
+ };
+
+ return mrb_obj_new(mrb, c, MP_ARRAY_SIZE(init_args), init_args);
+}
+
+static mrb_value _observe_property_raw(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ mrb_int id;
+ char *name;
+ mrb_get_args(mrb, "iz", &id, &name);
+ int err = mpv_observe_property(ctx->client, id, name, MPV_FORMAT_NODE);
+ return api_return_val(mrb, err, mrb_fixnum_value(id));
+}
+
+static mrb_value _unobserve_property_raw(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ mrb_int id;
+ mrb_get_args(mrb, "i", &id);
+ int err = mpv_unobserve_property(ctx->client, id);
+ return api_return_bool(mrb, err);
+}
+
+static mrb_value _request_event(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ char *event;
+ mrb_bool enable;
+ mrb_get_args(mrb, "zb", &event, &enable);
+
+ int event_id = -1;
+ for (int n = 0; n < 256; n++) {
+ const char *name = mpv_event_name(n);
+ if (name && strcmp(name, event) == 0) {
+ event_id = n;
+ break;
+ }
+ }
+
+ int err = mpv_request_event(ctx->client, event_id, enable);
+ return api_return_bool(mrb, err);
+}
+
+static mrb_value _command(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ char *command;
+ mrb_get_args(mrb, "z", &command);
+ return api_return_bool(mrb, mpv_command_string(ctx->client, command));
+}
+
+static mrb_value _commandv(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ mrb_value *commands;
+ mrb_int size;
+ mrb_get_args(mrb, "*", &commands, &size);
+ char **args = talloc_zero_array(NULL, char *, size + 1);
+ for (int i = 0; i < size; i++)
+ args[i] = talloc_strdup(args, RSTRING_PTR(commands[i]));
+ int err = mpv_command(ctx->client, (const char **)args);
+ talloc_free(args);
+ return api_return_bool(mrb, err);
+}
+
+static mrb_value _get_time(mrb_state *mrb, mrb_value self)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ const double secs = mpv_get_time_us(ctx->client) / (double)(1e6);
+ return mrb_float_value(mrb, secs);
+}
+
+#define MRB_FN(a,b) mrb_define_module_function(mrb, mod, #a, _ ## a, (b))
+static void define_module(mrb_state *mrb)
+{
+ struct RClass *mod = mrb_define_module(mrb, "M");
+ MRB_FN(log, MRB_ARGS_REQ(1));
+ MRB_FN(find_config_file, MRB_ARGS_REQ(1));
+ MRB_FN(get_property, MRB_ARGS_REQ(1));
+ MRB_FN(set_property, MRB_ARGS_REQ(2));
+ MRB_FN(wait_event, MRB_ARGS_REQ(1));
+ MRB_FN(observe_property_raw, MRB_ARGS_REQ(2));
+ MRB_FN(unobserve_property_raw, MRB_ARGS_REQ(1));
+ MRB_FN(request_event, MRB_ARGS_REQ(2));
+ MRB_FN(command, MRB_ARGS_REQ(1));
+ MRB_FN(commandv, MRB_ARGS_ANY());
+ MRB_FN(get_time, MRB_ARGS_NONE());
+}
+#undef MRB_FN
+
+static bool print_backtrace(mrb_state *mrb)
+{
+ if (!mrb->exc)
+ return true;
+
+ mrb_value exc = mrb_obj_value(mrb->exc);
+ mrb_value bt = mrb_exc_backtrace(mrb, exc);
+
+ int ai = mrb_gc_arena_save(mrb);
+
+ char *err = talloc_strdup(NULL, "");
+ mrb_value exc_str = mrb_inspect(mrb, exc);
+ err = talloc_asprintf_append(err, "%s\n", RSTRING_PTR(exc_str));
+
+ mrb_int bt_len = mrb_ary_len(mrb, bt);
+ err = talloc_asprintf_append(err, "backtrace:\n");
+ for (int i = 0; i < bt_len; i++) {
+ mrb_value s = mrb_ary_entry(bt, i);
+ err = talloc_asprintf_append(err, "\t[%d] => %s\n", i, RSTRING_PTR(s));
+ }
+
+ mrb_gc_arena_restore(mrb, ai);
+
+ struct script_ctx *ctx = get_ctx(mrb);
+ MP_ERR(ctx, "%s", err);
+ talloc_free(err);
+ return false;
+}
+
+typedef mrb_value (*runner)(mrb_state *, const void*, mrbc_context *);
+
+static bool run_script(mrb_state *mrb, runner runner,
+ const void *runee, const char *name)
+{
+ mrbc_context *mrb_ctx = mrbc_context_new(mrb);
+ mrbc_filename(mrb, mrb_ctx, name);
+ runner(mrb, runee, mrb_ctx);
+ bool err = print_backtrace(mrb);
+ mrbc_context_free(mrb, mrb_ctx);
+ return err;
+}
+
+static bool load_environment(mrb_state *mrb)
+{
+ for (int n = 0; mruby_scripts[n][0]; n++) {
+ const char *script = mruby_scripts[n][1];
+ const char *fname = mruby_scripts[n][0];
+ if (!run_script(mrb, (runner) mrb_load_string_cxt, script, fname))
+ return false;
+ }
+ return true;
+}
+
+static bool load_script(mrb_state *mrb, const char *fname)
+{
+ struct script_ctx *ctx = get_ctx(mrb);
+ char *file_path = mp_get_user_path(NULL, ctx->mpctx->global, fname);
+ FILE *fp = fopen(file_path, "r");
+ bool result = run_script(mrb, (runner) mrb_load_file_cxt, fp, fname);
+ fclose(fp);
+ talloc_free(file_path);
+ return result;
+}
+
+static int load_mruby(struct mpv_handle *client, const char *fname)
+{
+ struct MPContext *mpctx = mp_client_get_core(client);
+ int r = -1;
+
+ struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
+ *ctx = (struct script_ctx) {
+ .name = mpv_client_name(client),
+ .filename = fname,
+ .log = mp_client_get_log(client),
+ .client = client,
+ .mpctx = mpctx,
+ };
+
+ mrb_state *mrb = ctx->state = mrb_open();
+ mrb_sym sym = mrb_intern_cstr(mrb, "mpctx");
+ mrb_vm_const_set(mrb, sym, mrb_cptr_value(mrb, ctx));
+ define_module(mrb);
+
+ if (!mrb)
+ goto err_out;
+
+ if (!load_environment(mrb))
+ goto err_out;
+
+ if (!load_script(mrb, fname))
+ goto err_out;
+
+ if (!run_script(mrb, (runner) mrb_load_string_cxt, "M.run", "event_loop"))
+ goto err_out;
+
+ r = 0;
+
+err_out:
+ if (ctx->state)
+ mrb_close(ctx->state);
+ talloc_free(ctx);
+ return r;
+}
+
+const struct mp_scripting mp_scripting_mruby = {
+ .file_ext = "mrb",
+ .load = load_mruby,
+};
diff --git a/player/mruby/events.mrb b/player/mruby/events.mrb
new file mode 100644
index 0000000000..ee00e7df71
--- /dev/null
+++ b/player/mruby/events.mrb
@@ -0,0 +1,221 @@
+module M
+ class Event < Struct.new(:id, :type, :error, :data)
+ %w(shutdown property-change).each do |n|
+ mname = n.gsub('-', '_') + '?'
+ define_method(mname) do
+ type == n
+ end
+ end
+
+ def success?
+ error == "success"
+ end
+ end
+
+ class EventLoop
+ attr_reader :timers, :properties, :events
+
+ def initialize
+ @timers = Timers.new
+ @properties = PropertyObservers.new
+ @events = EventObservers.new
+ end
+
+ def run
+ loop do
+ timers.fire
+ wait_time = timers.wait_time || 1e20
+ event = M.wait_event(wait_time)
+ @properties.dispatch(event)
+ @events.dispatch(event)
+ break if event.shutdown?
+ end
+ end
+ end
+
+ class Timers
+ def initialize
+ @timers = []
+ end
+
+ def fire
+ @timers.each(&:fire)
+ end
+
+ def add(&block)
+ t = Timer.new(self, &block)
+ @timers.push(t)
+ t
+ end
+
+ def delete(t)
+ @timers.delete(t)
+ end
+
+ def wait_time
+ @timers.select(&:active?).map(&:wait_time).select{|t| t > 0}.min
+ end
+ end
+
+ class Timer
+ attr_accessor :executions
+
+ def initialize(timers, &block)
+ @timers = timers
+ @block = block
+ @active = false
+ self.executions = 0
+ end
+
+ def fire
+ return unless active?
+ return if wait_time > 0
+
+ self.executions += 1
+ @block.call(self)
+ reschedule
+ end
+
+ def cancel
+ @active = false
+ @timers.delete(self)
+ end
+
+ def every(secs)
+ @interval = secs
+ once(secs)
+ end
+
+ def once(secs)
+ @expire_time = now + secs
+ @active = true
+ end
+
+ def wait_time
+ @expire_time - now
+ end
+
+ def active?
+ @active
+ end
+
+ private
+ def interval?
+ !! @interval
+ end
+
+ def reschedule
+ every(@interval) if interval?
+ end
+
+ def now
+ M.get_time
+ end
+ end
+
+ class Observers
+ def initialize
+ @observers = {}
+ end
+
+ def dispatch(event)
+ if handle?(event) and o = @observers[event_key(event)]
+ o.call(*observer_args(event))
+ end
+ end
+
+ def observe(k, &block)
+ id = get_key(k)
+ raw_observe(k, id).tap do |result|
+ if result.success?
+ @observers[id] = block
+ end
+ end
+ end
+
+ def unobserve(id)
+ raw_unobserve(id).tap do |result|
+ if result.success?
+ @observers.delete(id)
+ end
+ end
+ end
+
+ def handle?(event)
+ @observers.include?(event_key(event))
+ end
+ end
+
+ class EventObservers < Observers
+ def handle?(event)
+ super and event.id == 0
+ end
+
+ def observer_args(event)
+ []
+ end
+
+ def get_key(k)
+ k
+ end
+
+ def event_key(event)
+ event.type
+ end
+
+ def raw_observe(k, id)
+ M.request_event(k, true)
+ end
+
+ def raw_unobserve(k)
+ M.request_event(k, false)
+ end
+ end
+
+ class PropertyObservers < Observers
+ def get_key(k)
+ @_id ||= 1337
+ @_id += 1
+ end
+
+ def observer_args(event)
+ [ event.data['value'] ]
+ end
+
+ def event_key(event)
+ event.id
+ end
+
+ def handle?(event)
+ super and event.id > 0 and event.property_change?
+ end
+
+ def raw_observe(k, id)
+ M.observe_property_raw(id, k)
+ end
+
+ def raw_unobserve(id)
+ M.unobserve_property_raw(id)
+ end
+ end
+
+ def self.event_loop
+ @event_loop ||= EventLoop.new
+ end
+
+ def self.properties
+ event_loop.properties
+ end
+
+ def self.events
+ event_loop.events
+ end
+
+ def self.timers
+ event_loop.timers
+ end
+
+ def self.run
+ event_loop.run
+ end
+end
diff --git a/player/mruby/example.mrb b/player/mruby/example.mrb
new file mode 100644
index 0000000000..6b71a95718
--- /dev/null
+++ b/player/mruby/example.mrb
@@ -0,0 +1,27 @@
+M.puts.error "hello from mruby!"
+
+boxes = %w(mute mut).map {|p| M.get_property(p)}
+boxes.each do |box|
+ box.unbox do |value|
+ # only executed if no errors
+ M.puts.warn "got #{value}"
+ end
+end
+
+M.puts.warn M.find_config_file("config")
+
+# M.events.observe 'tick' do
+# M.puts.error "tick"
+# M.events.unobserve "tick"
+# end
+
+M.properties.observe 'mute' do |val|
+ M.puts.error "got mute notification mute = #{val.inspect}"
+end
+
+M.timers.add do |t|
+ t.cancel and next if t.executions > 2
+ M.puts.error "timer called!"
+end.every(2)
+
+M.commandv "seek", "30"
diff --git a/player/mruby/logging.mrb b/player/mruby/logging.mrb
new file mode 100644
index 0000000000..ed81e6d80e
--- /dev/null
+++ b/player/mruby/logging.mrb
@@ -0,0 +1,21 @@
+module M
+ class Logger
+ def initialize(suffix="")
+ @suffix = suffix
+ end
+
+ %w(fatal error warn info v debug).each do |level|
+ define_method(level) do |message|
+ M.log(level, [message.to_s, @suffix].join)
+ end
+ end
+ end
+
+ def self.msg
+ @_msg ||= Logger.new
+ end
+
+ def self.puts
+ @_puts ||= Logger.new("\n")
+ end
+end
diff --git a/player/mruby/reply.mrb b/player/mruby/reply.mrb
new file mode 100644
index 0000000000..4ae65dbd3f
--- /dev/null
+++ b/player/mruby/reply.mrb
@@ -0,0 +1,37 @@
+module M
+ class ReplyError < StandardError; end
+ class Reply < Struct.new(:val, :status)
+ %w(b f i s).map{|type| "to_#{type}"}.map(&:intern).each do |method|
+ define_method(method) { unbox!.send(method) }
+ end
+
+ def unbox(default=nil, &block)
+ unbox!(&block)
+ rescue ReplyError
+ default
+ end
+
+ def unbox!(&block)
+ if success?
+ block_given? ? yield(val) : val
+ else
+ raise ReplyError, status
+ end
+ end
+
+ private :val
+
+ private
+ def success?
+ status == "success"
+ end
+
+ def method_missing(method, *args, &block)
+ if success? and val.respond_to?(method)
+ val.send(method, *args, &block)
+ else
+ super
+ end
+ end
+ end
+end
diff --git a/player/mruby/test.mrb b/player/mruby/test.mrb
new file mode 100644
index 0000000000..4fb68d76ea
--- /dev/null
+++ b/player/mruby/test.mrb
@@ -0,0 +1,83 @@
+def assert(name, &block)
+ @_tests ||= {}
+ @_tests[name] = block
+end
+
+def with_unbox(a, &block)
+ case [a.class]
+ when [M::Reply] then
+ a.unbox(&block)
+ else
+ yield(a)
+ end
+end
+
+def assert_equal(a, b)
+ raise "Expected #{a} to equal #{b}" unless with_unbox(a) {|v| v == b}
+end
+
+def assert_class(a, klass)
+ raise "Expected #{a} to be of class #{klass}" \
+ unless with_unbox(a) {|v| v.class == klass}
+end
+
+def assert_include(a, b)
+ raise "Expected:\n#{a}\nto include\n#{b}\n" unless a.include?(b)
+end
+
+def ok(s)
+ "\e[32m#{s}\e[0m"
+end
+
+def ko(s)
+ "\e[31m#{s}\e[0m"
+end
+
+def run
+ puts "\n\nRunning test suite..."
+ failed = false
+ @_tests.each do |name, block|
+ print name
+ begin
+ block.call
+ puts ok(" ~ ok")
+ rescue => e
+ puts ko(" ~ fail")
+ puts "\n"
+ puts e.inspect
+ puts "\n"
+ e.backtrace.map { |x| puts x }
+ failed = true
+ break
+ end
+ end
+ puts "\n done! All tests pass!\n\n" unless failed
+end
+
+assert ".property_list returns an array" do
+ assert_class(M.get_property('property-list').val, Array)
+end
+
+assert ".property_list contains options" do
+ assert_include(M.get_property('property-list'), "mute")
+end
+
+assert ".get_property returns proper values" do
+ assert_class(M.get_property("working-directory"), String)
+ assert_class(M.get_property("volume"), Float)
+ assert_class(M.get_property("osd-width"), Fixnum)
+ assert_class(M.get_property("vf"), Array)
+ assert_include([true, false], M.get_property("mute").unbox!)
+end
+
+assert ".set_property works on complex types" do
+ assert_equal(M.get_property("vf"), [])
+ M.set_property("vf", [{ name: "crop", params: { w: "400", h: "400" }}])
+ assert_equal(M.get_property("vf"), [{
+ "name" => "crop",
+ "enabled" => true,
+ "params" => { "w" => "400", "h" => "400" }
+ }])
+end
+
+run
diff --git a/player/mruby/test.sh b/player/mruby/test.sh
new file mode 100755
index 0000000000..3828dbd2f5
--- /dev/null
+++ b/player/mruby/test.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+build/mpv --script=player/mruby/test.mrb --idle --msg-level=all=error $1
diff --git a/player/scripting.c b/player/scripting.c
index 38e8809020..0fd52154f4 100644
--- a/player/scripting.c
+++ b/player/scripting.c
@@ -39,6 +39,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;
+extern const struct mp_scripting mp_scripting_mruby;
static const struct mp_scripting *const scripting_backends[] = {
#if HAVE_LUA
@@ -50,6 +51,9 @@ static const struct mp_scripting *const scripting_backends[] = {
#if HAVE_JAVASCRIPT
&mp_scripting_js,
#endif
+#if HAVE_MRUBY
+ &mp_scripting_mruby,
+#endif
NULL
};
diff --git a/wscript b/wscript
index bd4d7f128f..5182b78e33 100644
--- a/wscript
+++ b/wscript
@@ -314,6 +314,10 @@ iconv support use --disable-iconv.",
'desc' : 'Lua',
'func': check_lua,
}, {
+ 'name' : '--mruby',
+ 'desc' : 'mruby',
+ 'func': check_cc(lib='mruby'),
+ }, {
'name' : '--javascript',
'desc' : 'Javascript (MuJS backend)',
'func': check_pkg_config('mujs', '>= 1.0.0'),
diff --git a/wscript_build.py b/wscript_build.py
index bbe367963f..332bc9e596 100644
--- a/wscript_build.py
+++ b/wscript_build.py
@@ -101,7 +101,6 @@ def build(ctx):
lua_files = ["defaults.lua", "assdraw.lua", "options.lua", "osc.lua",
"ytdl_hook.lua", "stats.lua"]
-
for fn in lua_files:
fn = "player/lua/" + fn
ctx(
@@ -110,6 +109,15 @@ def build(ctx):
target = os.path.splitext(fn)[0] + ".inc",
)
+ mruby_files = ['events.mrb', 'logging.mrb', 'reply.mrb']
+ for fn in mruby_files:
+ fn = "player/mruby/" + fn
+ ctx(
+ features = "file2string",
+ source = fn,
+ target = os.path.splitext(fn)[0] + ".inc",
+ )
+
ctx(
features = "file2string",
source = "player/javascript/defaults.js",
@@ -294,6 +302,7 @@ def build(ctx):
( "player/lavfi.c" ),
( "player/lua.c", "lua" ),
( "player/javascript.c", "javascript" ),
+ ( "player/mruby.c", "mruby" ),
( "player/osd.c" ),
( "player/playloop.c" ),
( "player/screenshot.c" ),