mirror of
https://github.com/mpv-player/mpv
synced 2025-02-20 06:46:55 +00:00
tmp: mruby support
This commit is contained in:
parent
d9ca235c68
commit
5a5cc02793
506
player/mruby.c
Normal file
506
player/mruby.c
Normal file
@ -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,
|
||||
};
|
221
player/mruby/events.mrb
Normal file
221
player/mruby/events.mrb
Normal file
@ -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
|
27
player/mruby/example.mrb
Normal file
27
player/mruby/example.mrb
Normal file
@ -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"
|
21
player/mruby/logging.mrb
Normal file
21
player/mruby/logging.mrb
Normal file
@ -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
|
37
player/mruby/reply.mrb
Normal file
37
player/mruby/reply.mrb
Normal file
@ -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
|
83
player/mruby/test.mrb
Normal file
83
player/mruby/test.mrb
Normal file
@ -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
|
2
player/mruby/test.sh
Executable file
2
player/mruby/test.sh
Executable file
@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
build/mpv --script=player/mruby/test.mrb --idle --msg-level=all=error $1
|
@ -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
|
||||
};
|
||||
|
4
wscript
4
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)',
|
||||
|
@ -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" ),
|
||||
|
Loading…
Reference in New Issue
Block a user