0
0
mirror of https://github.com/mpv-player/mpv.git synced 2024-09-20 12:02:23 +02:00
mpv/player/lua.c
wm4 18930ba7dd input, lua: add functions to take pre-split input commands
So you can pass a command as list of strings (each item is an argument),
instead of having to worry about escaping and such.

These functions also take an argument for the default command flags. In
particular, this allows setting saner defaults for commands sent by
program code.

Expose this to Lua as mp.send_commandv command (suggestions for a better
name welcome). The Lua version doesn't allow setting the default command
flags, but it can still use command prefixes. The default flags are
different from input.conf, and disable OSD and property expansion.
2013-12-20 18:01:04 +01:00

707 lines
20 KiB
C

#include <assert.h>
#include <string.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include "talloc.h"
#include "common/common.h"
#include "options/m_property.h"
#include "common/msg.h"
#include "options/m_option.h"
#include "input/input.h"
#include "options/path.h"
#include "bstr/bstr.h"
#include "osdep/timer.h"
#include "sub/osd.h"
#include "core.h"
#include "command.h"
#include "lua.h"
// List of builtin modules and their contents as strings.
// All these are generated from player/lua/*.lua
static const char *builtin_lua_scripts[][2] = {
{"mp.defaults",
# include "player/lua/defaults.inc"
},
{"mp.assdraw",
# include "player/lua/assdraw.inc"
},
{"@osc",
# include "player/lua/osc.inc"
},
{0}
};
// Represents a loaded script. Each has its own Lua state.
struct script_ctx {
const char *name;
lua_State *state;
struct mp_log *log;
struct MPContext *mpctx;
};
struct lua_ctx {
struct script_ctx **scripts;
int num_scripts;
};
static struct script_ctx *find_script(struct lua_ctx *lctx, const char *name)
{
for (int n = 0; n < lctx->num_scripts; n++) {
if (strcmp(lctx->scripts[n]->name, name) == 0)
return lctx->scripts[n];
}
return NULL;
}
static struct script_ctx *get_ctx(lua_State *L)
{
lua_getfield(L, LUA_REGISTRYINDEX, "ctx");
struct script_ctx *ctx = lua_touserdata(L, -1);
lua_pop(L, 1);
assert(ctx);
return ctx;
}
static struct MPContext *get_mpctx(lua_State *L)
{
return get_ctx(L)->mpctx;
}
static int wrap_cpcall(lua_State *L)
{
lua_CFunction fn = lua_touserdata(L, -1);
lua_pop(L, 1);
return fn(L);
}
// Call the given function fn under a Lua error handler (similar to lua_cpcall).
// Pass the given number of args from the Lua stack to fn.
// Returns 0 (and empty stack) on success.
// Returns LUA_ERR[RUN|MEM|ERR] otherwise, with the error value on the stack.
static int mp_cpcall(lua_State *L, lua_CFunction fn, int args)
{
// Don't use lua_pushcfunction() - it allocates memory on Lua 5.1.
// Instead, emulate C closures by making wrap_cpcall call fn.
lua_pushlightuserdata(L, fn); // args... fn
// Will always succeed if mp_lua_init() set it up correctly.
lua_getfield(L, LUA_REGISTRYINDEX, "wrap_cpcall"); // args... fn wrap_cpcall
lua_insert(L, -(args + 2)); // wrap_cpcall args... fn
return lua_pcall(L, args + 1, 0, 0);
}
static void report_error(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
const char *err = lua_tostring(L, -1);
MP_WARN(mpctx, "Error: %s\n", err ? err : "[unknown]");
lua_pop(L, 1);
}
static void add_functions(struct script_ctx *ctx);
static char *script_name_from_filename(void *talloc_ctx, struct lua_ctx *lctx,
const char *fname)
{
fname = mp_basename(fname);
if (fname[0] == '@')
fname += 1;
char *name = talloc_strdup(talloc_ctx, fname);
// Drop .lua extension
char *dot = strrchr(name, '.');
if (dot)
*dot = '\0';
// Turn it into a safe identifier - this is used with e.g. dispatching
// input via: "send scriptname ..."
for (int n = 0; name[n]; n++) {
char c = name[n];
if (!(c >= 'A' && c <= 'Z') && !(c >= 'a' && c <= 'z') &&
!(c >= '0' && c <= '9'))
name[n] = '_';
}
// Make unique (stupid but simple)
while (find_script(lctx, name))
name = talloc_strdup_append(name, "_");
return name;
}
static int load_file(struct script_ctx *ctx, const char *fname)
{
int r = 0;
lua_State *L = ctx->state;
if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0)) {
report_error(L);
r = -1;
}
assert(lua_gettop(L) == 0);
return r;
}
static int load_builtin(lua_State *L)
{
const char *name = luaL_checkstring(L, 1);
for (int n = 0; builtin_lua_scripts[n][0]; n++) {
if (strcmp(name, builtin_lua_scripts[n][0]) == 0) {
if (luaL_loadstring(L, builtin_lua_scripts[n][1]))
lua_error(L);
lua_call(L, 0, 1);
return 1;
}
}
return 0;
}
// Execute "require " .. name
static bool require(lua_State *L, const char *name)
{
char buf[80];
// Lazy, but better than calling the "require" function manually
snprintf(buf, sizeof(buf), "require '%s'", name);
if (luaL_loadstring(L, buf) || lua_pcall(L, 0, 0, 0)) {
report_error(L);
return false;
}
return true;
}
static void mp_lua_load_script(struct MPContext *mpctx, const char *fname)
{
struct lua_ctx *lctx = mpctx->lua_ctx;
struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
*ctx = (struct script_ctx) {
.mpctx = mpctx,
.name = script_name_from_filename(ctx, lctx, fname),
};
char *log_name = talloc_asprintf(ctx, "lua/%s", ctx->name);
ctx->log = mp_log_new(ctx, mpctx->log, log_name);
lua_State *L = ctx->state = luaL_newstate();
if (!L)
goto error_out;
// used by get_ctx()
lua_pushlightuserdata(L, ctx); // ctx
lua_setfield(L, LUA_REGISTRYINDEX, "ctx"); // -
lua_pushcfunction(L, wrap_cpcall); // closure
lua_setfield(L, LUA_REGISTRYINDEX, "wrap_cpcall"); // -
luaL_openlibs(L);
lua_newtable(L); // mp
lua_pushvalue(L, -1); // mp mp
lua_setglobal(L, "mp"); // mp
add_functions(ctx); // mp
lua_pushstring(L, ctx->name); // mp name
lua_setfield(L, -2, "script_name"); // mp
lua_pop(L, 1); // -
// Add a preloader for each builtin Lua module
lua_getglobal(L, "package"); // package
assert(lua_type(L, -1) == LUA_TTABLE);
lua_getfield(L, -1, "preload"); // package preload
assert(lua_type(L, -1) == LUA_TTABLE);
for (int n = 0; builtin_lua_scripts[n][0]; n++) {
lua_pushcfunction(L, load_builtin); // package preload load_builtin
lua_setfield(L, -2, builtin_lua_scripts[n][0]);
}
lua_pop(L, 2); // -
assert(lua_gettop(L) == 0);
if (!require(L, "mp.defaults")) {
report_error(L);
goto error_out;
}
assert(lua_gettop(L) == 0);
if (fname[0] == '@') {
if (!require(L, fname))
goto error_out;
} else {
if (load_file(ctx, fname) < 0)
goto error_out;
}
MP_TARRAY_APPEND(lctx, lctx->scripts, lctx->num_scripts, ctx);
return;
error_out:
if (ctx->state)
lua_close(ctx->state);
talloc_free(ctx);
}
static void kill_script(struct script_ctx *ctx)
{
if (!ctx)
return;
struct lua_ctx *lctx = ctx->mpctx->lua_ctx;
lua_close(ctx->state);
for (int n = 0; n < lctx->num_scripts; n++) {
if (lctx->scripts[n] == ctx) {
MP_TARRAY_REMOVE_AT(lctx->scripts, lctx->num_scripts, n);
break;
}
}
talloc_free(ctx);
}
static const char *log_level[] = {
[MSGL_FATAL] = "fatal",
[MSGL_ERR] = "error",
[MSGL_WARN] = "warn",
[MSGL_INFO] = "info",
[MSGL_V] = "verbose",
[MSGL_DBG2] = "debug",
};
static int script_log(lua_State *L)
{
struct script_ctx *ctx = get_ctx(L);
const char *level = luaL_checkstring(L, 1);
int msgl = -1;
for (int n = 0; n < MP_ARRAY_SIZE(log_level); n++) {
if (log_level[n] && strcasecmp(log_level[n], level) == 0) {
msgl = n;
break;
}
}
if (msgl < 0)
luaL_error(L, "Invalid log level '%s'", level);
int last = lua_gettop(L);
lua_getglobal(L, "tostring"); // args... tostring
for (int i = 2; i <= last; i++) {
lua_pushvalue(L, -1); // args... tostring tostring
lua_pushvalue(L, i); // args... tostring tostring args[i]
lua_call(L, 1, 1); // args... tostring str
const char *s = lua_tostring(L, -1);
if (s == NULL)
return luaL_error(L, "Invalid argument");
mp_msg_log(ctx->log, msgl, "%s%s", s, i > 0 ? " " : "");
lua_pop(L, 1); // args... tostring
}
mp_msg_log(ctx->log, msgl, "\n");
return 0;
}
static int script_find_config_file(lua_State *L)
{
const char *s = luaL_checkstring(L, 1);
char *path = mp_find_user_config_file(s);
if (path) {
lua_pushstring(L, path);
} else {
lua_pushnil(L);
}
talloc_free(path);
return 1;
}
static int run_event(lua_State *L)
{
lua_getglobal(L, "mp_event"); // name arg mp_event
if (lua_isnil(L, -1))
return 0;
lua_insert(L, -3); // mp_event name arg
lua_call(L, 2, 0);
return 0;
}
void mp_lua_event(struct MPContext *mpctx, const char *name, const char *arg)
{
// There is no proper subscription mechanism yet, so all scripts get it.
struct lua_ctx *lctx = mpctx->lua_ctx;
for (int n = 0; n < lctx->num_scripts; n++) {
struct script_ctx *ctx = lctx->scripts[n];
lua_State *L = ctx->state;
lua_pushstring(L, name);
if (arg) {
lua_pushstring(L, arg);
} else {
lua_pushnil(L);
}
if (mp_cpcall(L, run_event, 2) != 0)
report_error(L);
}
}
static int run_script_dispatch(lua_State *L)
{
int id = lua_tointeger(L, 1);
const char *event = lua_tostring(L, 2);
lua_getglobal(L, "mp_script_dispatch");
if (lua_isnil(L, -1))
return 0;
lua_pushinteger(L, id);
lua_pushstring(L, event);
lua_call(L, 2, 0);
return 0;
}
void mp_lua_script_dispatch(struct MPContext *mpctx, char *script_name,
int id, char *event)
{
struct script_ctx *ctx = find_script(mpctx->lua_ctx, script_name);
if (!ctx) {
MP_VERBOSE(mpctx, "Can't find script '%s' when handling input.\n",
script_name);
return;
}
lua_State *L = ctx->state;
lua_pushinteger(L, id);
lua_pushstring(L, event);
if (mp_cpcall(L, run_script_dispatch, 2) != 0)
report_error(L);
}
static int script_send_command(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
const char *s = luaL_checkstring(L, 1);
mp_cmd_t *cmd = mp_input_parse_cmd(mpctx->input, bstr0((char*)s), "<lua>");
if (!cmd)
luaL_error(L, "error parsing command");
mp_input_queue_cmd(mpctx->input, cmd);
return 0;
}
static int script_send_commandv(lua_State *L)
{
struct script_ctx *ctx = get_ctx(L);
int num = lua_gettop(L);
bstr args[50];
if (num > MP_ARRAY_SIZE(args))
luaL_error(L, "too many arguments");
for (int n = 1; n <= num; n++) {
size_t len;
const char *s = lua_tolstring(L, n, &len);
if (!s)
luaL_error(L, "argument %d is not a string", n);
args[n - 1] = (bstr){(char *)s, len};
}
mp_cmd_t *cmd = mp_input_parse_cmd_bstrv(ctx->log, 0, num, args, "<lua>");
if (!cmd)
luaL_error(L, "error parsing command");
mp_input_queue_cmd(ctx->mpctx->input, cmd);
return 0;
}
static int script_property_list(lua_State *L)
{
const struct m_option *props = mp_get_property_list();
lua_newtable(L);
for (int i = 0; props[i].name; i++) {
lua_pushinteger(L, i + 1);
lua_pushstring(L, props[i].name);
lua_settable(L, -3);
}
return 1;
}
static int script_property_string(lua_State *L)
{
const struct m_option *props = mp_get_property_list();
struct MPContext *mpctx = get_mpctx(L);
const char *name = luaL_checkstring(L, 1);
int type = lua_tointeger(L, lua_upvalueindex(1))
? M_PROPERTY_PRINT : M_PROPERTY_GET_STRING;
char *result = NULL;
if (m_property_do(props, name, type, &result, mpctx) >= 0 && result) {
lua_pushstring(L, result);
talloc_free(result);
return 1;
}
if (type == M_PROPERTY_PRINT) {
lua_pushstring(L, "");
return 1;
}
return 0;
}
static int script_set_osd_ass(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
int res_x = luaL_checkinteger(L, 1);
int res_y = luaL_checkinteger(L, 2);
const char *text = luaL_checkstring(L, 3);
if (!mpctx->osd->external ||
strcmp(mpctx->osd->external, text) != 0 ||
mpctx->osd->external_res_x != res_x ||
mpctx->osd->external_res_y != res_y)
{
talloc_free(mpctx->osd->external);
mpctx->osd->external = talloc_strdup(mpctx->osd, text);
mpctx->osd->external_res_x = res_x;
mpctx->osd->external_res_y = res_y;
osd_changed(mpctx->osd, OSDTYPE_EXTERNAL);
}
return 0;
}
static int script_get_osd_resolution(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
int w, h;
osd_object_get_resolution(mpctx->osd, mpctx->osd->objs[OSDTYPE_EXTERNAL],
&w, &h);
lua_pushnumber(L, w);
lua_pushnumber(L, h);
return 2;
}
static int script_get_screen_size(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
struct osd_object *obj = mpctx->osd->objs[OSDTYPE_EXTERNAL];
double aspect = 1.0 * obj->vo_res.w / MPMAX(obj->vo_res.h, 1) /
obj->vo_res.display_par;
lua_pushnumber(L, obj->vo_res.w);
lua_pushnumber(L, obj->vo_res.h);
lua_pushnumber(L, aspect);
return 3;
}
static int script_get_mouse_pos(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
int px, py;
mp_input_get_mouse_pos(mpctx->input, &px, &py);
double sw, sh;
osd_object_get_scale_factor(mpctx->osd, mpctx->osd->objs[OSDTYPE_EXTERNAL],
&sw, &sh);
lua_pushnumber(L, px * sw);
lua_pushnumber(L, py * sh);
return 2;
}
static int script_get_timer(lua_State *L)
{
lua_pushnumber(L, mp_time_sec());
return 1;
}
static int script_get_chapter_list(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
lua_newtable(L); // list
int num = get_chapter_count(mpctx);
for (int n = 0; n < num; n++) {
double time = chapter_start_time(mpctx, n);
char *name = chapter_display_name(mpctx, n);
lua_newtable(L); // list ch
lua_pushnumber(L, time); // list ch time
lua_setfield(L, -2, "time"); // list ch
lua_pushstring(L, name); // list ch name
lua_setfield(L, -2, "name"); // list ch
lua_pushinteger(L, n + 1); // list ch n1
lua_insert(L, -2); // list n1 ch
lua_settable(L, -3); // list
talloc_free(name);
}
return 1;
}
static const char *stream_type(enum stream_type t)
{
switch (t) {
case STREAM_VIDEO: return "video";
case STREAM_AUDIO: return "audio";
case STREAM_SUB: return "sub";
default: return "unknown";
}
}
static int script_get_track_list(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
lua_newtable(L); // list
for (int n = 0; n < mpctx->num_tracks; n++) {
struct track *track = mpctx->tracks[n];
lua_newtable(L); // list track
lua_pushstring(L, stream_type(track->type));
lua_setfield(L, -2, "type");
lua_pushinteger(L, track->user_tid);
lua_setfield(L, -2, "id");
lua_pushboolean(L, track->default_track);
lua_setfield(L, -2, "default");
lua_pushboolean(L, track->attached_picture);
lua_setfield(L, -2, "attached_picture");
if (track->lang) {
lua_pushstring(L, track->lang);
lua_setfield(L, -2, "language");
}
if (track->title) {
lua_pushstring(L, track->title);
lua_setfield(L, -2, "title");
}
lua_pushboolean(L, track->is_external);
lua_setfield(L, -2, "external");
if (track->external_filename) {
lua_pushstring(L, track->external_filename);
lua_setfield(L, -2, "external_filename");
}
lua_pushboolean(L, track->auto_loaded);
lua_setfield(L, -2, "auto_loaded");
lua_pushinteger(L, n + 1); // list track n1
lua_insert(L, -2); // list n1 track
lua_settable(L, -3); // list
}
return 1;
}
static int script_input_define_section(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
char *section = (char *)luaL_checkstring(L, 1);
char *contents = (char *)luaL_checkstring(L, 2);
mp_input_define_section(mpctx->input, section, "<script>", contents, true);
return 0;
}
static int script_input_enable_section(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
char *section = (char *)luaL_checkstring(L, 1);
char *sflags = (char *)luaL_optstring(L, 2, "");
bstr bflags = bstr0(sflags);
int flags = 0;
while (bflags.len) {
bstr val;
bstr_split_tok(bflags, "|", &val, &bflags);
if (bstr_equals0(val, "allow-hide-cursor")) {
flags |= MP_INPUT_ALLOW_HIDE_CURSOR;
} else if (bstr_equals0(val, "allow-vo-dragging")) {
flags |= MP_INPUT_ALLOW_VO_DRAGGING;
} else if (bstr_equals0(val, "exclusive")) {
flags |= MP_INPUT_EXCLUSIVE;
} else {
luaL_error(L, "invalid flag: '%.*s'", BSTR_P(val));
}
}
mp_input_enable_section(mpctx->input, section, flags);
return 0;
}
static int script_input_disable_section(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
char *section = (char *)luaL_checkstring(L, 1);
mp_input_disable_section(mpctx->input, section);
return 0;
}
static int script_input_set_section_mouse_area(lua_State *L)
{
struct MPContext *mpctx = get_mpctx(L);
double sw, sh;
struct osd_object *obj = mpctx->osd->objs[OSDTYPE_EXTERNAL];
osd_object_get_scale_factor(mpctx->osd, obj, &sw, &sh);
char *section = (char *)luaL_checkstring(L, 1);
int x0 = luaL_checkinteger(L, 2) / sw;
int y0 = luaL_checkinteger(L, 3) / sh;
int x1 = luaL_checkinteger(L, 4) / sw;
int y1 = luaL_checkinteger(L, 5) / sh;
mp_input_set_section_mouse_area(mpctx->input, section, x0, y0, x1, y1);
return 0;
}
static int script_format_time(lua_State *L)
{
double t = luaL_checknumber(L, 1);
const char *fmt = luaL_optstring(L, 2, "%H:%M:%S");
char *r = mp_format_time_fmt(fmt, t);
if (!r)
luaL_error(L, "Invalid time format string '%s'", fmt);
lua_pushstring(L, r);
talloc_free(r);
return 1;
}
struct fn_entry {
const char *name;
int (*fn)(lua_State *L);
};
#define FN_ENTRY(name) {#name, script_ ## name}
static struct fn_entry fn_list[] = {
FN_ENTRY(log),
FN_ENTRY(find_config_file),
FN_ENTRY(send_command),
FN_ENTRY(send_commandv),
FN_ENTRY(property_list),
FN_ENTRY(set_osd_ass),
FN_ENTRY(get_osd_resolution),
FN_ENTRY(get_screen_size),
FN_ENTRY(get_mouse_pos),
FN_ENTRY(get_timer),
FN_ENTRY(get_chapter_list),
FN_ENTRY(get_track_list),
FN_ENTRY(input_define_section),
FN_ENTRY(input_enable_section),
FN_ENTRY(input_disable_section),
FN_ENTRY(input_set_section_mouse_area),
FN_ENTRY(format_time),
};
// On stack: mp table
static void add_functions(struct script_ctx *ctx)
{
lua_State *L = ctx->state;
for (int n = 0; n < MP_ARRAY_SIZE(fn_list); n++) {
lua_pushcfunction(L, fn_list[n].fn);
lua_setfield(L, -2, fn_list[n].name);
}
lua_pushinteger(L, 0);
lua_pushcclosure(L, script_property_string, 1);
lua_setfield(L, -2, "property_get");
lua_pushinteger(L, 1);
lua_pushcclosure(L, script_property_string, 1);
lua_setfield(L, -2, "property_get_string");
}
void mp_lua_init(struct MPContext *mpctx)
{
mpctx->lua_ctx = talloc_zero(NULL, struct lua_ctx);
// Load scripts from options
if (mpctx->opts->lua_load_osc)
mp_lua_load_script(mpctx, "@osc");
char **files = mpctx->opts->lua_files;
for (int n = 0; files && files[n]; n++) {
if (files[n][0])
mp_lua_load_script(mpctx, files[n]);
}
}
void mp_lua_uninit(struct MPContext *mpctx)
{
if (mpctx->lua_ctx) {
while (mpctx->lua_ctx->num_scripts)
kill_script(mpctx->lua_ctx->scripts[0]);
talloc_free(mpctx->lua_ctx);
mpctx->lua_ctx = NULL;
}
}