diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | DOCS/man/en/lua.rst | 18 | ||||
-rw-r--r-- | DOCS/man/en/mpv.rst | 2 | ||||
-rw-r--r-- | DOCS/man/en/options.rst | 4 | ||||
-rw-r--r-- | Makefile | 12 | ||||
-rwxr-xr-x | configure | 114 | ||||
-rw-r--r-- | mpvcore/command.c | 72 | ||||
-rw-r--r-- | mpvcore/command.h | 18 | ||||
-rw-r--r-- | mpvcore/input/input.c | 5 | ||||
-rw-r--r-- | mpvcore/input/input.h | 3 | ||||
-rw-r--r-- | mpvcore/lua/assdraw.lua | 98 | ||||
-rw-r--r-- | mpvcore/lua/defaults.lua | 82 | ||||
-rw-r--r-- | mpvcore/mp_core.h | 7 | ||||
-rw-r--r-- | mpvcore/mp_lua.c | 678 | ||||
-rw-r--r-- | mpvcore/mp_lua.h | 14 | ||||
-rw-r--r-- | mpvcore/mplayer.c | 38 | ||||
-rw-r--r-- | mpvcore/options.c | 4 | ||||
-rw-r--r-- | mpvcore/options.h | 1 | ||||
-rw-r--r-- | sub/osd_dummy.c | 7 | ||||
-rw-r--r-- | sub/osd_libass.c | 42 | ||||
-rw-r--r-- | sub/sub.c | 11 | ||||
-rw-r--r-- | sub/sub.h | 10 |
22 files changed, 1238 insertions, 3 deletions
diff --git a/.gitignore b/.gitignore index c00adcbbdd..07eab0dfbd 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ /demux/ebml_defs.c /demux/ebml_types.h /sub/osd_font.h +/mpvcore/lua/*.inc /DOCS/man/*/mpv.1 /DOCS/man/*/mpv.aux /DOCS/man/*/mpv.log diff --git a/DOCS/man/en/lua.rst b/DOCS/man/en/lua.rst new file mode 100644 index 0000000000..25d456cb46 --- /dev/null +++ b/DOCS/man/en/lua.rst @@ -0,0 +1,18 @@ +LUA SCRIPTING +============= + +mpv can load Lua scripts. These scripts can be used to control mpv in a similar +way to slave mode. mpv provides the builtin module ``mp`` (can be loaded +with ``require 'mpv'``), which provides functions to send commands to the +mpv core and to retrieve information about playback state, user settings, +file information, and so on. + +.. admonition:: Warning + + Lua scripting is work in progress, and it's in a very early stage. When + writing scripts, rely only on the features and functions documented here. + Everything else is subject to change. + +.. admonition:: Warning + + Nothing is finished and documented yet. diff --git a/DOCS/man/en/mpv.rst b/DOCS/man/en/mpv.rst index 08eef8bc45..5b61257999 100644 --- a/DOCS/man/en/mpv.rst +++ b/DOCS/man/en/mpv.rst @@ -441,6 +441,8 @@ OPTIONS .. include:: input.rst +.. include:: lua.rst + .. include:: changes.rst ENVIRONMENT VARIABLES diff --git a/DOCS/man/en/options.rst b/DOCS/man/en/options.rst index ca17c763c4..4c4d6ebc8d 100644 --- a/DOCS/man/en/options.rst +++ b/DOCS/man/en/options.rst @@ -1258,6 +1258,10 @@ looping. If several files are specified on command line, the entire playlist is looped. +``--lua=<filename>`` + Load a Lua script. You can load multiple scripts by separating them with + commas (``,``). + ``--mc=<seconds/frame>`` Maximum A-V sync correction per frame (in seconds) @@ -123,6 +123,8 @@ SOURCES-$(WAYLAND) += video/out/vo_wayland.c video/out/wayland_comm SOURCES-$(VF_LAVFI) += video/filter/vf_lavfi.c SOURCES-$(AF_LAVFI) += audio/filter/af_lavfi.c +SOURCES-$(LUA) += mpvcore/mp_lua.c + ifeq ($(HAVE_AVUTIL_REFCOUNTING),no) SOURCES-yes += video/decode/lavc_dr1.c endif @@ -397,6 +399,14 @@ sub/osd_libass.c: sub/osd_font.h sub/osd_font.h: TOOLS/file2string.pl sub/osd_font.otf ./$^ >$@ +mpvcore/mp_lua.c: mpvcore/lua/defaults.inc +mpvcore/lua/defaults.inc: TOOLS/file2string.pl mpvcore/lua/defaults.lua + ./$^ >$@ + +mpvcore/mp_lua.c: mpvcore/lua/assdraw.inc +mpvcore/lua/assdraw.inc: TOOLS/file2string.pl mpvcore/lua/assdraw.lua + ./$^ >$@ + # ./configure must be rerun if it changed config.mak: configure @echo "############################################################" @@ -495,6 +505,8 @@ clean: -$(RM) video/out/gl_video_shaders.h -$(RM) video/out/x11_icon.inc -$(RM) sub/osd_font.h + -$(RM) mpvcore/lua/defaults.inc + -$(RM) mpvcore/lua/assdraw.inc distclean: clean -$(RM) config.log config.mak config.h TAGS tags @@ -293,6 +293,9 @@ Installation directories: Optional features: --disable-encoding disable encoding functionality [enable] + --disable-lua disable Lua scripting support [autodetect] + --lua=LUA select Lua package which should be autodetected + Choices: 51 51deb 52 52deb luajit --disable-libguess disable libguess [autodetect] --enable-terminfo use terminfo database for key codes [autodetect] --enable-termcap use termcap database for key codes [autodetect] @@ -485,6 +488,7 @@ _pthreads=auto _ass=auto _libass_osd=auto _rpath=no +lua=auto libpostproc=auto libavfilter=auto vf_lavfi=auto @@ -679,6 +683,9 @@ for ac_option do --disable-libavresample) _disable_avresample=yes ;; --enable-libavresample) _disable_avresample=no ;; + --enable-lua) lua=yes ;; + --disable-lua) lua=no ;; + --lua=*) lua_pkg=$(echo $ac_option | cut -d '=' -f 2) ;; --enable-lirc) _lirc=yes ;; --disable-lirc) _lirc=no ;; --enable-lircc) _lircc=yes ;; @@ -2982,6 +2989,111 @@ fi echores "$_pvr" +# Note: Lua has no official .pc file, so there are different OS-specific ones. +# Also, we support luajit, which is compatible to 5.1. +# The situation is further complicated by distros supporting multiple Lua +# versions, without ensuring libraries linking to conflicting Lua libs don't +# cause issues. This is a real problem with libquvi. + +cat > $TMPC << EOF +#include <stdlib.h> +#include <lua.h> +#include <lualib.h> +#include <lauxlib.h> +EOF +# abuse $TMPH file as second temp file +cat > $TMPH << EOF +void test_lua(void) { + lua_State *L = luaL_newstate(); + lua_pushstring(L, "test"); + lua_setglobal(L, "test"); +} +void test_other(void) { +EOF + +# test all other Lua using packages (hack that gives us time) +if test "$_libquvi4" = yes ; then + +echo "#include <quvi/quvi.h>" >> $TMPC +cat >> $TMPH << EOF + quvi_t q; + if (quvi_init(&q) == QUVI_OK) + quvi_supported(q, "http://nope"); +EOF + +fi + +if test "$_libquvi9" = yes ; then + +echo "#include <quvi.h>" >> $TMPC +cat >> $TMPH << EOF + quvi_t q = quvi_new(); + if (quvi_ok(q)) + quvi_supports(q, "http://nope", QUVI_SUPPORTS_MODE_OFFLINE, QUVI_SUPPORTS_TYPE_MEDIA); +EOF + +fi + +cat >> $TMPH << EOF +} +int main(void) { + test_lua(); + test_other(); + return 0; +} +EOF + +cat $TMPH >> $TMPC + +test_lua() { + # changed by pkg_config_add + old_extra_cflags="$extra_cflags" + old_libs_mplayer="$libs_mplayer" + echocheck "Lua $2 ($1)" + if test "$lua" = yes ; then + echores "no" + return 1 + fi + if test "x$lua_pkg" != "x" && test "$lua_pkg" != "$1" ; then + echores "no" + return 1 + fi + if pkg_config_add "$2" ; then + if test $_cross_compile = no ; then + if cc_check && tmp_run ; then + echo > /dev/null # idiot NOP + else + extra_cflags="$old_extra_cflags" + libs_mplayer="$old_libs_mplayer" + echores "no - does not run" + return 1 + fi + fi + lua=yes + fi + echores "$lua" + test "$lua" = yes + return $? +} + +if test "$lua" = auto ; then + +lua=no +test_lua 51 "lua >= 5.1.0 lua < 5.2.0" +test_lua 51deb "lua5.1 >= 5.1.0" # debian +test_lua luajit "luajit >= 2.0.0" +# assume all our dependencies (libquvi in particular) link with 5.1 +test_lua 52 "lua >= 5.2.0" +test_lua 52deb "lua5.2 >= 5.2.0" # debian + +fi + +if test "$lua" = yes ; then + def_lua='#define CONFIG_LUA 1' +else + def_lua='#undef CONFIG_LUA' +fi + echocheck "encoding" if test "$_encoding" = yes ; then def_encoding="#define CONFIG_ENCODING 1" @@ -3155,6 +3267,7 @@ DUMMY_OSD = $_dummy_osd LIBBLURAY = $_bluray LIBBS2B = $_libbs2b LCMS2 = $_lcms2 +LUA = $lua LIBPOSTPROC = $libpostproc LIBAVDEVICE = $libavdevice LIBAVFILTER = $libavfilter @@ -3341,6 +3454,7 @@ $def_libquvi9 $def_libguess $def_lcms2 +$def_lua /* libvo options */ diff --git a/mpvcore/command.c b/mpvcore/command.c index b30a5299e0..35e72cb3ad 100644 --- a/mpvcore/command.c +++ b/mpvcore/command.c @@ -68,6 +68,8 @@ #include "mpvcore/mp_core.h" +#include "mp_lua.h" + static int edit_filters(struct MPContext *mpctx, enum stream_type mediatype, const char *cmd, const char *arg); static int set_filters(struct MPContext *mpctx, enum stream_type mediatype, @@ -1913,6 +1915,11 @@ static const m_option_t mp_properties[] = { {0}, }; +const struct m_option *mp_get_property_list(void) +{ + return mp_properties; +} + int mp_property_do(const char *name, int action, void *val, struct MPContext *ctx) { @@ -2729,6 +2736,15 @@ void run_command(MPContext *mpctx, mp_cmd_t *cmd) cmd->args[1].v.s, msg_osd); break; + case MP_CMD_SCRIPT_DISPATCH: + if (mpctx->lua_ctx) { +#ifdef CONFIG_LUA + mp_lua_script_dispatch(mpctx, cmd->args[0].v.s, cmd->args[1].v.i, + cmd->key_up_follows ? "keyup_follows" : "press"); +#endif + } + break; + case MP_CMD_COMMAND_LIST: { for (struct mp_cmd *sub = cmd->args[0].v.p; sub; sub = sub->queue_next) run_command(mpctx, sub); @@ -2755,3 +2771,59 @@ void run_command(MPContext *mpctx, mp_cmd_t *cmd) break; } } + +struct command_ctx { + int events; +}; + +void command_init(struct MPContext *mpctx) +{ + mpctx->command_ctx = talloc_zero(mpctx, struct command_ctx); +} + +// Notify that a property might have changed. +void mp_notify_property(struct MPContext *mpctx, const char *property) +{ + mp_notify(mpctx, MP_EVENT_PROPERTY, (void *)property); +} + +void mp_notify(struct MPContext *mpctx, enum mp_event event, void *arg) +{ + struct command_ctx *ctx = mpctx->command_ctx; + ctx->events |= 1u << event; +} + +static void handle_script_event(struct MPContext *mpctx, const char *name, + const char *arg) +{ +#ifdef CONFIG_LUA + mp_lua_event(mpctx, name, arg); +#endif +} + +void mp_flush_events(struct MPContext *mpctx) +{ + struct command_ctx *ctx = mpctx->command_ctx; + + ctx->events |= (1u << MP_EVENT_TICK); + + for (int n = 0; n < 16; n++) { + enum mp_event event = n; + unsigned mask = 1 << event; + if (ctx->events & mask) { + // The event handler could set event flags again; in this case let + // the next mp_flush_events() call handle it to avoid infinite loops. + ctx->events &= ~mask; + const char *name = NULL; + switch (event) { + case MP_EVENT_TICK: name = "tick"; break; + case MP_EVENT_TRACKS_CHANGED: name = "track-layout"; break; + case MP_EVENT_START_FILE: name = "start"; break; + case MP_EVENT_END_FILE: name = "end"; break; + default: ; + } + if (name) + handle_script_event(mpctx, name, ""); + } + } +} diff --git a/mpvcore/command.h b/mpvcore/command.h index 5ac8c3a8f0..1ac5b6f8f0 100644 --- a/mpvcore/command.h +++ b/mpvcore/command.h @@ -22,6 +22,8 @@ struct MPContext; struct mp_cmd; +void command_init(struct MPContext *mpctx); + void mp_get_osd_mouse_pos(struct MPContext *mpctx, float *x, float *y); void run_command(struct MPContext *mpctx, struct mp_cmd *cmd); @@ -30,4 +32,20 @@ void property_print_help(void); int mp_property_do(const char* name, int action, void* val, struct MPContext *mpctx); +const struct m_option *mp_get_property_list(void); + +enum mp_event { + MP_EVENT_NONE, + MP_EVENT_TICK, + MP_EVENT_PROPERTY, // char*, property that is changed + MP_EVENT_TRACKS_CHANGED, + MP_EVENT_START_FILE, + MP_EVENT_END_FILE, +}; + +void mp_notify(struct MPContext *mpctx, enum mp_event event, void *arg); +void mp_notify_property(struct MPContext *mpctx, const char *property); + +void mp_flush_events(struct MPContext *mpctx); + #endif /* MPLAYER_COMMAND_H */ diff --git a/mpvcore/input/input.c b/mpvcore/input/input.c index 902aa52b47..f624b8f5dd 100644 --- a/mpvcore/input/input.c +++ b/mpvcore/input/input.c @@ -229,6 +229,8 @@ static const mp_cmd_t mp_cmds[] = { { MP_CMD_VO_CMDLINE, "vo_cmdline", { ARG_STRING } }, + { MP_CMD_SCRIPT_DISPATCH, "script_dispatch", { ARG_STRING, ARG_INT } }, + {0} }; @@ -528,6 +530,7 @@ struct input_ctx { // Autorepeat stuff short ar_state; int64_t last_ar; + // Autorepeat config unsigned int ar_delay; unsigned int ar_rate; @@ -1455,6 +1458,8 @@ static void remove_key_down(struct input_ctx *ictx, int code) static bool key_updown_ok(enum mp_command_type cmd) { switch (cmd) { + case MP_CMD_SCRIPT_DISPATCH: + return true; default: return false; } diff --git a/mpvcore/input/input.h b/mpvcore/input/input.h index f3ea895395..639f069a9c 100644 --- a/mpvcore/input/input.h +++ b/mpvcore/input/input.h @@ -85,6 +85,9 @@ enum mp_command_type { /// Video output commands MP_CMD_VO_CMDLINE, + /// Internal for Lua scripts + MP_CMD_SCRIPT_DISPATCH, + // Internal MP_CMD_COMMAND_LIST, // list of sub-commands in args[0].v.p }; diff --git a/mpvcore/lua/assdraw.lua b/mpvcore/lua/assdraw.lua new file mode 100644 index 0000000000..fc3b727f57 --- /dev/null +++ b/mpvcore/lua/assdraw.lua @@ -0,0 +1,98 @@ +local ass_mt = {} +ass_mt.__index = ass_mt + +local function ass_new() + return setmetatable({ scale = 4, text = "" }, ass_mt) +end + +function ass_mt.new_event(ass) + -- osd_libass.c adds an event per line + if #ass.text > 0 then + ass.text = ass.text .. "\n" + end +end + +function ass_mt.draw_start(ass) + ass.text = string.format("%s{\\p%d}", ass.text, ass.scale) +end + +function ass_mt.draw_stop(ass) + ass.text = ass.text .. "{\\p0}" +end + +function ass_mt.coord(ass, x, y) + local scale = math.pow(2, ass.scale - 1) + local ix = math.ceil(x * scale) + local iy = math.ceil(y * scale) + ass.text = string.format("%s %d %d", ass.text, ix, iy) +end + +function ass_mt.append(ass, s) + ass.text = ass.text .. s +end + +function ass_mt.merge(ass1, ass2) + ass1.text = ass1.text .. ass2.text +end + +function ass_mt.pos(ass, x, y) + ass:append(string.format("{\\pos(%f,%f)}", x, y)) +end + +function ass_mt.an(ass, an) + ass:append(string.format("{\\an%d}", an)) +end + +function ass_mt.move_to(ass, x, y) + ass:append(" m") + ass:coord(x, y) +end + +function ass_mt.line_to(ass, x, y) + ass:append(" l") + ass:coord(x, y) +end + +function ass_mt.bezier_curve(ass, x1, y1, x2, y2, x3, y3) + ass:append(" b") + ass:coord(x1, y1) + ass:coord(x2, y2) + ass:coord(x3, y3) +end + + +function ass_mt.rect_ccw(ass, x0, y0, x1, y1) + ass:move_to(x0, y0) + ass:line_to(x0, y1) + ass:line_to(x1, y1) + ass:line_to(x1, y0) +end + +function ass_mt.rect_cw(ass, x0, y0, x1, y1) + ass:move_to(x0, y0) + ass:line_to(x1, y0) + ass:line_to(x1, y1) + ass:line_to(x0, y1) +end + +function ass_mt.round_rect_cw(ass, x0, y0, x1, y1, r) + ass:move_to(x0 + r, y0) + ass:line_to(x1 - r, y0) -- top line + if r > 0 then + ass:bezier_curve(x1, y0, x1, y0, x1, y0 + r) -- top right corner + end + ass:line_to(x1, y1 - r) -- right line + if r > 0 then + ass:bezier_curve(x1, y1, x1, y1, x1 - r, y1) -- bottom right corner + end + ass:line_to(x0 + r, y1) -- bottom line + if r > 0 then + ass:bezier_curve(x0, y1, x0, y1, x0, y1 - r) -- bottom left corner + end + ass:line_to(x0, y0 + r) -- left line + if r > 0 then + ass:bezier_curve(x0, y0, x0, y0, x0 + r, y0) -- top left corner + end +end + +return {ass_new = ass_new} diff --git a/mpvcore/lua/defaults.lua b/mpvcore/lua/defaults.lua new file mode 100644 index 0000000000..d24cda9cbe --- /dev/null +++ b/mpvcore/lua/defaults.lua @@ -0,0 +1,82 @@ + +local callbacks = {} +-- each script has its own section, so that they don't conflict +local default_section = "input_" .. mp.script_name + +-- Set the list of key bindings. These will override the user's bindings, so +-- you should use this sparingly. +-- A call to this function will remove all bindings previously set with this +-- function. For example, set_key_bindings({}) would remove all script defined +-- key bindings. +-- Note: the bindings are not active by default. Use enable_key_bindings(). +-- +-- list is an array of key bindings, where each entry is an array as follow: +-- {key, callback} +-- {key, callback, callback_down} +-- key is the key string as used in input.conf, like "ctrl+a" +-- callback is a Lua function that is called when the key binding is used. +-- callback_down can be given too, and is called when a mouse button is pressed +-- if the key is a mouse button. (The normal callback will be for mouse button +-- down.) +-- +-- callback can be a string too, in which case the following will be added like +-- an input.conf line: key .. " " .. callback +-- (And callback_down is ignored.) +function mp.set_key_bindings(list, section) + local cfg = "" + for i = 1, #list do + local entry = list[i] + local key = entry[1] + local cb = entry[2] + local cb_down = entry[3] + if type(cb) == "function" then + callbacks[#callbacks + 1] = {press=cb, before_press=cb_down} + cfg = cfg .. key .. " script_dispatch " .. mp.script_name + .. " " .. #callbacks .. "\n" + else + cfg = cfg .. key .. " " .. cb .. "\n" + end + end + mp.input_define_section(section or default_section, cfg) +end + +function mp.enable_key_bindings(section, flags) + mp.input_enable_section(section or default_section, flags) +end + +function mp.disable_key_bindings(section) + mp.input_disable_section(section or default_section) +end + +function mp.set_mouse_area(x0, y0, x1, y1, section) + mp.input_set_section_mouse_area(section or default_section, x0, y0, x1, y1) +end + +-- called by C on script_dispatch input command +function mp_script_dispatch(id, event) + local cb = callbacks[id] + if cb then + if event == "press" and cb.press then + cb.press() + elseif event == "keyup_follows" and cb.before_press then + cb.before_press() + end + end +end + +mp.msg = { + log = mp.log, + fatal = function(...) return mp.log("fatal", ...) end, + error = function(...) return mp.log("error", ...) end, + warn = function(...) return mp.log("warn", ...) end, + info = function(...) return mp.log("info", ...) end, + verbose = function(...) return mp.log("verbose", ...) end, + debug = function(...) return mp.log("debug", ...) end, +} + +_G.print = mp.msg.info + +package.loaded["mp"] = mp +package.loaded["mp.msg"] = mp.msg + +return {} diff --git a/mpvcore/mp_core.h b/mpvcore/mp_core.h index 4b8a5fcef9..e15ec0355e 100644 --- a/mpvcore/mp_core.h +++ b/mpvcore/mp_core.h @@ -166,6 +166,8 @@ typedef struct MPContext { struct track **tracks; int num_tracks; + char *track_layout_hash; + // Selected tracks. NULL if no track selected. struct track *current_track[STREAM_TYPE_COUNT]; @@ -291,10 +293,9 @@ typedef struct MPContext { bool drop_message_shown; struct screenshot_ctx *screenshot_ctx; - - char *track_layout_hash; - + struct command_ctx *command_ctx; struct encode_lavc_context *encode_lavc_ctx; + struct lua_ctx *lua_ctx; } MPContext; diff --git a/mpvcore/mp_lua.c b/mpvcore/mp_lua.c new file mode 100644 index 0000000000..ec63134673 --- /dev/null +++ b/mpvcore/mp_lua.c @@ -0,0 +1,678 @@ +#include <assert.h> +#include <string.h> + +#include <lua.h> +#include <lualib.h> +#include <lauxlib.h> + +#include "talloc.h" + +#include "mp_common.h" +#include "mp_lua.h" +#include "mp_core.h" +#include "mp_msg.h" +#include "m_property.h" +#include "m_option.h" +#include "command.h" +#include "input/input.h" +#include "sub/sub.h" +#include "osdep/timer.h" +#include "path.h" +#include "bstr.h" + +// List of builtin modules and their contents as strings. +// All these are generated from mpvcore/lua/*.lua +static const char *builtin_lua_scripts[][2] = { + {"mp.defaults", +# include "lua/defaults.inc" + }, + {"mp.assdraw", +# include "lua/assdraw.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) +{ + const char *err = lua_tostring(L, -1); + mp_msg(MSGT_CPLAYER, MSGL_WARN, "[lua] 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 ? " " : ""); |