/* * 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 "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_DEBUG] = "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(ctx->log, msgl, "%s%s", s, i > 0 ? " " : ""); lua_pop(L, 1); // args... tostring } mp_msg(ctx->log, msgl, "\n"); return 0; } static int script_find_config_file(lua_State *L) { struct MPContext *mpctx = get_mpctx(L); const char *s = luaL_checkstring(L, 1); char *path = mp_find_user_config_file(NULL, mpctx->global, 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), ""); 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, ""); 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) { 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 (mp_property_do(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, "