summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--DOCS/man/en/lua.rst18
-rw-r--r--DOCS/man/en/mpv.rst2
-rw-r--r--DOCS/man/en/options.rst4
-rw-r--r--Makefile12
-rwxr-xr-xconfigure114
-rw-r--r--mpvcore/command.c72
-rw-r--r--mpvcore/command.h18
-rw-r--r--mpvcore/input/input.c5
-rw-r--r--mpvcore/input/input.h3
-rw-r--r--mpvcore/lua/assdraw.lua98
-rw-r--r--mpvcore/lua/defaults.lua82
-rw-r--r--mpvcore/mp_core.h7
-rw-r--r--mpvcore/mp_lua.c678
-rw-r--r--mpvcore/mp_lua.h14
-rw-r--r--mpvcore/mplayer.c38
-rw-r--r--mpvcore/options.c4
-rw-r--r--mpvcore/options.h1
-rw-r--r--sub/osd_dummy.c7
-rw-r--r--sub/osd_libass.c42
-rw-r--r--sub/sub.c11
-rw-r--r--sub/sub.h10
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)
diff --git a/Makefile b/Makefile
index 5199c7d28b..52741b8c7f 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/configure b/configure
index 482a34aecc..32525f9de9 100755
--- a/configure
+++ b/configure
@@ -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 ? " " : "");
+ 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