From 5a5cc02793ad28ecdc50d3df66b7913a0b467ac6 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 13 Jul 2014 17:35:45 +0200 Subject: tmp: mruby support --- player/mruby.c | 506 +++++++++++++++++++++++++++++++++++++++++++++++ player/mruby/events.mrb | 221 +++++++++++++++++++++ player/mruby/example.mrb | 27 +++ player/mruby/logging.mrb | 21 ++ player/mruby/reply.mrb | 37 ++++ player/mruby/test.mrb | 83 ++++++++ player/mruby/test.sh | 2 + player/scripting.c | 4 + wscript | 4 + wscript_build.py | 11 +- 10 files changed, 915 insertions(+), 1 deletion(-) create mode 100644 player/mruby.c create mode 100644 player/mruby/events.mrb create mode 100644 player/mruby/example.mrb create mode 100644 player/mruby/logging.mrb create mode 100644 player/mruby/reply.mrb create mode 100644 player/mruby/test.mrb create mode 100755 player/mruby/test.sh 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 . + */ + +#include +#include +#include +#include +#include +#include +#include + +#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 @@ -49,6 +50,9 @@ static const struct mp_scripting *const scripting_backends[] = { #endif #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 @@ -313,6 +313,10 @@ iconv support use --disable-iconv.", 'name' : '--lua', 'desc' : 'Lua', 'func': check_lua, + }, { + 'name' : '--mruby', + 'desc' : 'mruby', + 'func': check_cc(lib='mruby'), }, { 'name' : '--javascript', 'desc' : 'Javascript (MuJS backend)', 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" ), -- cgit v1.2.3