aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar wm4 <wm4@nowhere>2013-09-26 00:41:14 +0200
committerGravatar wm4 <wm4@nowhere>2013-09-26 01:28:58 +0200
commit6048f87e3c07d03df8a59da58c81608c5375f6dc (patch)
treebe79bfe51b5268f2c1494f456fa72538c391d54f
parentb0cc3c2cf4e342e33300adaea4a98565ad866e22 (diff)
Add initial Lua scripting support
This is preliminary. There are still tons of issues, and any aspect of scripting may change in the future. I decided to merge this (preliminary) work now because it makes it easier to develop it, not because it's done. lua.rst is clear enough about it (plus some sarcasm). This requires linking to Lua. Lua has no official pkg-config file, but there are distribution specific .pc files, all with different names. Adding a non-pkg-config based configure test was considered, but we'd rather not. One major complication is that libquvi links against Lua too, and if the Lua version is different from mpv's, you will get a crash as soon as libquvi uses Lua. (libquvi by design always runs when a file is opened.) I would consider this the problem of distros and whoever builds mpv, but to make things easier for users, we add a terrible runtime test to the configure script, which probes whether libquvi will crash. This is disabled when cross-compiling, but in that case we hope the user knows what he is doing.
-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 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_msg(MSGT_CPLAYER, MSGL_V,
+ "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(bstr0((char*)s), "<lua>");
+ if (!cmd)
+ luaL_error(L, "error parsing command");
+ mp_input_queue_cmd(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);
+ float px, py;
+ mp_get_osd_mouse_pos(mpctx, &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(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
+ 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;
+ }
+}
diff --git a/mpvcore/mp_lua.h b/mpvcore/mp_lua.h
new file mode 100644
index 0000000000..050548e2d2
--- /dev/null
+++ b/mpvcore/mp_lua.h
@@ -0,0 +1,14 @@
+#ifndef MP_LUA_H
+#define MP_LUA_H
+
+#include <stdbool.h>
+
+struct MPContext;
+
+void mp_lua_init(struct MPContext *mpctx);
+void mp_lua_uninit(struct MPContext *mpctx);
+void mp_lua_event(struct MPContext *mpctx, const char *name, const char *arg);
+void mp_lua_script_dispatch(struct MPContext *mpctx, char *script_name,
+ int id, char *event);
+
+#endif
diff --git a/mpvcore/mplayer.c b/mpvcore/mplayer.c
index fbba387681..50c3db10d7 100644
--- a/mpvcore/mplayer.c
+++ b/mpvcore/mplayer.c
@@ -149,6 +149,8 @@
#include "mpvcore/mp_core.h"
#include "mpvcore/options.h"
+#include "mp_lua.h"
+
const char mp_help_text[] = _(
"Usage: mpv [options] [url|path/]filename\n"
"\n"
@@ -548,6 +550,10 @@ static MP_NORETURN void exit_player(struct MPContext *mpctx,
mpctx->encode_lavc_ctx = NULL;
+#ifdef CONFIG_LUA
+ mp_lua_uninit(mpctx);
+#endif
+
#if defined(__MINGW32__) || defined(__CYGWIN__)
timeEndPeriod(1);
#endif
@@ -1028,6 +1034,8 @@ static struct track *add_stream_track(struct MPContext *mpctx,
demuxer_select_track(track->demuxer, stream, false);
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
return track;
}
@@ -1057,6 +1065,8 @@ static void add_dvd_tracks(struct MPContext *mpctx)
struct stream_lang_req req = {.type = STREAM_SUB, .id = n};
stream_control(stream, STREAM_CTRL_GET_LANG, &req);
track->lang = talloc_strdup(track, req.name);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
}
}
demuxer_enable_autoselect(demuxer);
@@ -2035,12 +2045,15 @@ void mp_switch_track(struct MPContext *mpctx, enum stream_type type,
if (type == STREAM_VIDEO) {
mpctx->opts->video_id = user_tid;
reinit_video_chain(mpctx);
+ mp_notify_property(mpctx, "vid");
} else if (type == STREAM_AUDIO) {
mpctx->opts->audio_id = user_tid;
reinit_audio_chain(mpctx);
+ mp_notify_property(mpctx, "aid");
} else if (type == STREAM_SUB) {
mpctx->opts->sub_id = user_tid;
reinit_subs(mpctx);
+ mp_notify_property(mpctx, "sid");
}
talloc_free(mpctx->track_layout_hash);
@@ -2083,6 +2096,9 @@ bool mp_remove_track(struct MPContext *mpctx, struct track *track)
}
mpctx->num_tracks--;
talloc_free(track);
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+
return true;
}
@@ -2761,6 +2777,8 @@ static double update_video(struct MPContext *mpctx, double endpts)
void pause_player(struct MPContext *mpctx)
{
+ mp_notify_property(mpctx, "pause");
+
mpctx->opts->pause = 1;
if (mpctx->video_out)
@@ -2790,6 +2808,8 @@ void pause_player(struct MPContext *mpctx)
void unpause_player(struct MPContext *mpctx)
{
+ mp_notify_property(mpctx, "pause");
+
mpctx->opts->pause = 0;
if (mpctx->video_out && mpctx->opts->stop_screensaver)
@@ -3418,6 +3438,8 @@ static void handle_cursor_autohide(struct MPContext *mpctx)
static void handle_input_and_seek_coalesce(struct MPContext *mpctx)
{
+ mp_flush_events(mpctx);
+
mp_cmd_t *cmd;
while ((cmd = mp_input_get_cmd(mpctx->input, 0, 1)) != NULL) {
/* Allow running consecutive seek commands to combine them,
@@ -4216,6 +4238,7 @@ static void idle_loop(struct MPContext *mpctx)
false)));
run_command(mpctx, cmd);
mp_cmd_free(cmd);
+ mp_flush_events(mpctx);
}
}
@@ -4269,6 +4292,10 @@ static void play_current_file(struct MPContext *mpctx)
double playback_start = -1e100;
mpctx->initialized_flags |= INITIALIZED_PLAYBACK;
+
+ mp_notify(mpctx, MP_EVENT_START_FILE, NULL);
+ mp_flush_events(mpctx);
+
mpctx->stop_play = 0;
mpctx->filename = NULL;
@@ -4612,6 +4639,10 @@ terminate_playback: // don't jump here after ao/vo/getch initialization!
(playback_start < 0 || mp_time_sec() - playback_start < 3.0);
if (mpctx->playlist->current && !mpctx->playlist->current_was_replaced)
mpctx->playlist->current->playback_failed = playback_failed;
+
+ mp_notify(mpctx, MP_EVENT_TRACKS_CHANGED, NULL);
+ mp_notify(mpctx, MP_EVENT_END_FILE, NULL);
+ mp_flush_events(mpctx);
}
// Determine the next file to play. Note that if this function returns non-NULL,
@@ -4840,6 +4871,7 @@ static int mpv_main(int argc, char *argv[])
GetCpuCaps(&gCpuCaps);
screenshot_init(mpctx);
mpctx->mixer = mixer_init(mpctx, opts);
+ command_init(mpctx);
// Preparse the command line
m_config_preparse_command_line(mpctx->mconfig, argc, argv);
@@ -4902,6 +4934,12 @@ static int mpv_main(int argc, char *argv[])
mpctx->osd = osd_create(opts, mpctx->ass_library);
+#ifdef CONFIG_LUA
+ // Lua user scripts can call arbitrary functions. Load them at a point
+ // where this is safe.
+ mp_lua_init(mpctx);
+#endif
+
if (opts->shuffle)
playlist_shuffle(mpctx->playlist);
diff --git a/mpvcore/options.c b/mpvcore/options.c
index 9200c1dcf8..f62ac89721 100644
--- a/mpvcore/options.c
+++ b/mpvcore/options.c
@@ -344,6 +344,10 @@ const m_option_t mp_opts[] = {
OPT_FLAG("config", load_config, CONF_GLOBAL | CONF_NOCFG | CONF_PRE_PARSE),
OPT_STRINGLIST("reset-on-next-file", reset_options, CONF_GLOBAL),
+#ifdef CONFIG_LUA
+ OPT_STRINGLIST("lua", lua_files, CONF_GLOBAL),
+#endif
+
// ------------------------- stream options --------------------
#ifdef CONFIG_STREAM_CACHE
diff --git a/mpvcore/options.h b/mpvcore/options.h
index 7a30c24b34..ce0be86757 100644
--- a/mpvcore/options.h
+++ b/mpvcore/options.h
@@ -45,6 +45,7 @@ typedef struct mp_vo_opts {
typedef struct MPOpts {
char **reset_options;
+ char **lua_files;
struct m_obj_settings *audio_driver_list;
int fixed_vo;
diff --git a/sub/osd_dummy.c b/sub/osd_dummy.c
index 782ce21942..a330c63982 100644
--- a/sub/osd_dummy.c
+++ b/sub/osd_dummy.c
@@ -23,3 +23,10 @@ void osd_object_get_bitmaps(struct osd_state *osd, struct osd_object *obj,
{
*out_imgs = (struct sub_bitmaps) {0};
}
+
+void osd_object_get_resolution(struct osd_state *osd, struct osd_object *obj,
+ int *out_w, int *out_h)
+{
+ *out_w = 0;
+ *out_h = 0;
+}
diff --git a/sub/osd_libass.c b/sub/osd_libass.c
index 36e2a0ca0f..45edbc7cc8 100644
--- a/sub/osd_libass.c
+++ b/sub/osd_libass.c
@@ -347,6 +347,38 @@ static void update_progbar(struct osd_state *osd, struct osd_object *obj)
ass_draw_reset(d);
}
+static void update_external(struct osd_state *osd, struct osd_object *obj)
+{
+ create_osd_ass_track(osd, obj);
+ clear_obj(obj);
+
+ if (osd->external_res_x < 1 || osd->external_res_y < 1)
+ return;
+
+ ASS_Track *track = obj->osd_track;
+
+ if (track->PlayResX != osd->external_res_x ||
+ track->PlayResY != osd->external_res_y)
+ {
+ track->PlayResX = osd->external_res_x;
+ track->PlayResY = osd->external_res_y;
+ // Force libass to clear its internal cache - it doesn't check for
+ // PlayRes changes itself.
+ ass_set_frame_size(osd->osd_render, 1, 1);
+ }
+
+ bstr t = bstr0(osd->external);
+ while (t.len) {
+ bstr line;
+ bstr_split_tok(t, "\n", &line, &t);
+ if (line.len) {
+ char *tmp = bstrdup0(NULL, line);
+ add_osd_ass_event(obj->osd_track, tmp);
+ talloc_free(tmp);
+ }
+ }
+}
+
static void update_sub(struct osd_state *osd, struct osd_object *obj)
{
struct MPOpts *opts = osd->opts;
@@ -386,6 +418,9 @@ static void update_object(struct osd_state *osd, struct osd_object *obj)
case OSDTYPE_PROGBAR:
update_progbar(osd, obj);
break;
+ case OSDTYPE_EXTERNAL:
+ update_external(osd, obj);
+ break;
}
}
@@ -405,3 +440,10 @@ void osd_object_get_bitmaps(struct osd_state *osd, struct osd_object *obj,
&obj->parts_cache, out_imgs);
talloc_steal(obj, obj->parts_cache);
}
+
+void osd_object_get_resolution(struct osd_state *osd, struct osd_object *obj,
+ int *out_w, int *out_h)
+{
+ *out_w = obj->osd_track ? obj->osd_track->PlayResX : 0;
+ *out_h = obj->osd_track ? obj->osd_track->PlayResY : 0;
+}
diff --git a/sub/sub.c b/sub/sub.c
index a5ff9e2593..1beb9e8138 100644
--- a/sub/sub.c
+++ b/sub/sub.c
@@ -304,3 +304,14 @@ void osd_changed_all(struct osd_state *osd)
for (int n = 0; n < MAX_OSD_PARTS; n++)
osd_changed(osd, n);
}
+
+// Scale factor to translate OSD coordinates to what the obj uses internally.
+// osd_coordinates * (sh, sh) = obj_coordinates
+void osd_object_get_scale_factor(struct osd_state *osd, struct osd_object *obj,
+ double *sw, double *sh)
+{
+ int nw, nh;
+ osd_object_get_resolution(osd, obj, &nw, &nh);
+ *sw = nw / (double)obj->vo_res.w;
+ *sh = nh / (double)obj->vo_res.h;
+}
diff --git a/sub/sub.h b/sub/sub.h
index 3cb140565c..35527f266b 100644
--- a/sub/sub.h
+++ b/sub/sub.h
@@ -90,6 +90,8 @@ enum mp_osdtype {
OSDTYPE_PROGBAR,
OSDTYPE_OSD,
+ OSDTYPE_EXTERNAL,
+
MAX_OSD_PARTS
};
@@ -137,6 +139,9 @@ struct osd_state {
float progbar_value; // range 0.0-1.0
float *progbar_stops; // used for chapter indicators (0.0-1.0 each)
int progbar_num_stops;
+ // OSDTYPE_EXTERNAL
+ char *external;
+ int external_res_x, external_res_y;
// OSDTYPE_SUB
struct dec_sub *dec_sub;
@@ -219,10 +224,15 @@ void osd_draw_on_image_p(struct osd_state *osd, struct mp_osd_res res,
double video_pts, int draw_flags,
struct mp_image_pool *pool, struct mp_image *dest);
+void osd_object_get_scale_factor(struct osd_state *osd, struct osd_object *obj,
+ double *sw, double *sh);
+
// defined in osd_libass.c and osd_dummy.c
void osd_object_get_bitmaps(struct osd_state *osd, struct osd_object *obj,
struct sub_bitmaps *out_imgs);
+void osd_object_get_resolution(struct osd_state *osd, struct osd_object *obj,
+ int *out_w, int *out_h);
void osd_get_function_sym(char *buffer, size_t buffer_size, int osd_function);
void osd_init_backend(struct osd_state *osd);
void osd_destroy_backend(struct osd_state *osd);