summaryrefslogtreecommitdiffstats
path: root/player/scripting.c
diff options
context:
space:
mode:
Diffstat (limited to 'player/scripting.c')
-rw-r--r--player/scripting.c462
1 files changed, 462 insertions, 0 deletions
diff --git a/player/scripting.c b/player/scripting.c
new file mode 100644
index 0000000..0b20081
--- /dev/null
+++ b/player/scripting.c
@@ -0,0 +1,462 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+#include <strings.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#include "osdep/io.h"
+#include "osdep/subprocess.h"
+#include "osdep/threads.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "options/m_config.h"
+#include "options/parse_configfile.h"
+#include "options/path.h"
+#include "misc/bstr.h"
+#include "core.h"
+#include "client.h"
+#include "libmpv/client.h"
+#include "libmpv/render.h"
+#include "libmpv/stream_cb.h"
+
+extern const struct mp_scripting mp_scripting_lua;
+extern const struct mp_scripting mp_scripting_cplugin;
+extern const struct mp_scripting mp_scripting_js;
+extern const struct mp_scripting mp_scripting_run;
+
+static const struct mp_scripting *const scripting_backends[] = {
+#if HAVE_LUA
+ &mp_scripting_lua,
+#endif
+#if HAVE_CPLUGINS
+ &mp_scripting_cplugin,
+#endif
+#if HAVE_JAVASCRIPT
+ &mp_scripting_js,
+#endif
+ &mp_scripting_run,
+ NULL
+};
+
+static char *script_name_from_filename(void *talloc_ctx, const char *fname)
+{
+ fname = mp_basename(fname);
+ if (fname[0] == '@')
+ fname += 1;
+ char *name = talloc_strdup(talloc_ctx, fname);
+ // Drop file 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] = '_';
+ }
+ return talloc_asprintf(talloc_ctx, "%s", name);
+}
+
+static void run_script(struct mp_script_args *arg)
+{
+ char *name = talloc_asprintf(NULL, "%s/%s", arg->backend->name,
+ mpv_client_name(arg->client));
+ mp_thread_set_name(name);
+ talloc_free(name);
+
+ if (arg->backend->load(arg) < 0)
+ MP_ERR(arg, "Could not load %s script %s\n", arg->backend->name, arg->filename);
+
+ mpv_destroy(arg->client);
+ talloc_free(arg);
+}
+
+static MP_THREAD_VOID script_thread(void *p)
+{
+ struct mp_script_args *arg = p;
+ run_script(arg);
+
+ MP_THREAD_RETURN();
+}
+
+static int64_t mp_load_script(struct MPContext *mpctx, const char *fname)
+{
+ char *ext = mp_splitext(fname, NULL);
+ if (ext && strcasecmp(ext, "disable") == 0)
+ return 0;
+
+ void *tmp = talloc_new(NULL);
+
+ const char *path = NULL;
+ char *script_name = NULL;
+ const struct mp_scripting *backend = NULL;
+
+ struct stat s;
+ if (!stat(fname, &s) && S_ISDIR(s.st_mode)) {
+ path = fname;
+ fname = NULL;
+
+ for (int n = 0; scripting_backends[n]; n++) {
+ const struct mp_scripting *b = scripting_backends[n];
+ char *filename = mp_tprintf(80, "main.%s", b->file_ext);
+ fname = mp_path_join(tmp, path, filename);
+ if (!stat(fname, &s) && S_ISREG(s.st_mode)) {
+ backend = b;
+ break;
+ }
+ talloc_free((void *)fname);
+ fname = NULL;
+ }
+
+ if (!fname) {
+ MP_ERR(mpctx, "Cannot find main.* for any supported scripting "
+ "backend in: %s\n", path);
+ talloc_free(tmp);
+ return -1;
+ }
+
+ script_name = talloc_strdup(tmp, path);
+ mp_path_strip_trailing_separator(script_name);
+ script_name = mp_basename(script_name);
+ } else {
+ for (int n = 0; scripting_backends[n]; n++) {
+ const struct mp_scripting *b = scripting_backends[n];
+ if (ext && strcasecmp(ext, b->file_ext) == 0) {
+ backend = b;
+ break;
+ }
+ }
+ script_name = script_name_from_filename(tmp, fname);
+ }
+
+ if (!backend) {
+ MP_ERR(mpctx, "Can't load unknown script: %s\n", fname);
+ talloc_free(tmp);
+ return -1;
+ }
+
+ struct mp_script_args *arg = talloc_ptrtype(NULL, arg);
+ *arg = (struct mp_script_args){
+ .mpctx = mpctx,
+ .filename = talloc_strdup(arg, fname),
+ .path = talloc_strdup(arg, path),
+ .backend = backend,
+ // Create the client before creating the thread; otherwise a race
+ // condition could happen, where MPContext is destroyed while the
+ // thread tries to create the client.
+ .client = mp_new_client(mpctx->clients, script_name),
+ };
+
+ talloc_free(tmp);
+ fname = NULL; // might have been freed so don't touch anymore
+
+ if (!arg->client) {
+ MP_ERR(mpctx, "Failed to create client for script: %s\n", arg->filename);
+ talloc_free(arg);
+ return -1;
+ }
+
+ mp_client_set_weak(arg->client);
+ arg->log = mp_client_get_log(arg->client);
+ int64_t id = mpv_client_id(arg->client);
+
+ MP_DBG(arg, "Loading %s script %s...\n", backend->name, arg->filename);
+
+ if (backend->no_thread) {
+ run_script(arg);
+ } else {
+ mp_thread thread;
+ if (mp_thread_create(&thread, script_thread, arg)) {
+ mpv_destroy(arg->client);
+ talloc_free(arg);
+ return -1;
+ }
+ mp_thread_detach(thread);
+ }
+
+ return id;
+}
+
+int64_t mp_load_user_script(struct MPContext *mpctx, const char *fname)
+{
+ char *path = mp_get_user_path(NULL, mpctx->global, fname);
+ int64_t ret = mp_load_script(mpctx, path);
+ talloc_free(path);
+ return ret;
+}
+
+static int compare_filename(const void *pa, const void *pb)
+{
+ char *a = (char *)pa;
+ char *b = (char *)pb;
+ return strcmp(a, b);
+}
+
+static char **list_script_files(void *talloc_ctx, char *path)
+{
+ char **files = NULL;
+ int count = 0;
+ DIR *dp = opendir(path);
+ if (!dp)
+ return NULL;
+ struct dirent *ep;
+ while ((ep = readdir(dp))) {
+ if (ep->d_name[0] != '.') {
+ char *fname = mp_path_join(talloc_ctx, path, ep->d_name);
+ struct stat s;
+ if (!stat(fname, &s) && (S_ISREG(s.st_mode) || S_ISDIR(s.st_mode)))
+ MP_TARRAY_APPEND(talloc_ctx, files, count, fname);
+ }
+ }
+ closedir(dp);
+ if (files)
+ qsort(files, count, sizeof(char *), compare_filename);
+ MP_TARRAY_APPEND(talloc_ctx, files, count, NULL);
+ return files;
+}
+
+static void load_builtin_script(struct MPContext *mpctx, int slot, bool enable,
+ const char *fname)
+{
+ assert(slot < MP_ARRAY_SIZE(mpctx->builtin_script_ids));
+ int64_t *pid = &mpctx->builtin_script_ids[slot];
+ if (*pid > 0 && !mp_client_id_exists(mpctx, *pid))
+ *pid = 0; // died
+ if ((*pid > 0) != enable) {
+ if (enable) {
+ *pid = mp_load_script(mpctx, fname);
+ } else {
+ char *name = mp_tprintf(22, "@%"PRIi64, *pid);
+ mp_client_send_event(mpctx, name, 0, MPV_EVENT_SHUTDOWN, NULL);
+ }
+ }
+}
+
+void mp_load_builtin_scripts(struct MPContext *mpctx)
+{
+ load_builtin_script(mpctx, 0, mpctx->opts->lua_load_osc, "@osc.lua");
+ load_builtin_script(mpctx, 1, mpctx->opts->lua_load_ytdl, "@ytdl_hook.lua");
+ load_builtin_script(mpctx, 2, mpctx->opts->lua_load_stats, "@stats.lua");
+ load_builtin_script(mpctx, 3, mpctx->opts->lua_load_console, "@console.lua");
+ load_builtin_script(mpctx, 4, mpctx->opts->lua_load_auto_profiles,
+ "@auto_profiles.lua");
+}
+
+bool mp_load_scripts(struct MPContext *mpctx)
+{
+ bool ok = true;
+
+ // Load scripts from options
+ char **files = mpctx->opts->script_files;
+ for (int n = 0; files && files[n]; n++) {
+ if (files[n][0])
+ ok &= mp_load_user_script(mpctx, files[n]) >= 0;
+ }
+ if (!mpctx->opts->auto_load_scripts)
+ return ok;
+
+ // Load all scripts
+ void *tmp = talloc_new(NULL);
+ char **scriptsdir = mp_find_all_config_files(tmp, mpctx->global, "scripts");
+ for (int i = 0; scriptsdir && scriptsdir[i]; i++) {
+ files = list_script_files(tmp, scriptsdir[i]);
+ for (int n = 0; files && files[n]; n++)
+ ok &= mp_load_script(mpctx, files[n]) >= 0;
+ }
+ talloc_free(tmp);
+
+ return ok;
+}
+
+#if HAVE_CPLUGINS
+
+#if !HAVE_WIN32
+#include <dlfcn.h>
+#endif
+
+#define MPV_DLOPEN_FN "mpv_open_cplugin"
+typedef int (*mpv_open_cplugin)(mpv_handle *handle);
+
+static void init_sym_table(struct mp_script_args *args, void *lib) {
+#define INIT_SYM(name) \
+ { \
+ void **sym = (void **)dlsym(lib, "pfn_" #name); \
+ if (sym) { \
+ if (*sym && *sym != &name) \
+ MP_ERR(args, "Overriding already set function " #name "\n"); \
+ *sym = &name; \
+ } \
+ }
+
+ INIT_SYM(mpv_client_api_version);
+ INIT_SYM(mpv_error_string);
+ INIT_SYM(mpv_free);
+ INIT_SYM(mpv_client_name);
+ INIT_SYM(mpv_client_id);
+ INIT_SYM(mpv_create);
+ INIT_SYM(mpv_initialize);
+ INIT_SYM(mpv_destroy);
+ INIT_SYM(mpv_terminate_destroy);
+ INIT_SYM(mpv_create_client);
+ INIT_SYM(mpv_create_weak_client);
+ INIT_SYM(mpv_load_config_file);
+ INIT_SYM(mpv_get_time_us);
+ INIT_SYM(mpv_free_node_contents);
+ INIT_SYM(mpv_set_option);
+ INIT_SYM(mpv_set_option_string);
+ INIT_SYM(mpv_command);
+ INIT_SYM(mpv_command_node);
+ INIT_SYM(mpv_command_ret);
+ INIT_SYM(mpv_command_string);
+ INIT_SYM(mpv_command_async);
+ INIT_SYM(mpv_command_node_async);
+ INIT_SYM(mpv_abort_async_command);
+ INIT_SYM(mpv_set_property);
+ INIT_SYM(mpv_set_property_string);
+ INIT_SYM(mpv_del_property);
+ INIT_SYM(mpv_set_property_async);
+ INIT_SYM(mpv_get_property);
+ INIT_SYM(mpv_get_property_string);
+ INIT_SYM(mpv_get_property_osd_string);
+ INIT_SYM(mpv_get_property_async);
+ INIT_SYM(mpv_observe_property);
+ INIT_SYM(mpv_unobserve_property);
+ INIT_SYM(mpv_event_name);
+ INIT_SYM(mpv_event_to_node);
+ INIT_SYM(mpv_request_event);
+ INIT_SYM(mpv_request_log_messages);
+ INIT_SYM(mpv_wait_event);
+ INIT_SYM(mpv_wakeup);
+ INIT_SYM(mpv_set_wakeup_callback);
+ INIT_SYM(mpv_wait_async_requests);
+ INIT_SYM(mpv_hook_add);
+ INIT_SYM(mpv_hook_continue);
+ INIT_SYM(mpv_get_wakeup_pipe);
+
+ INIT_SYM(mpv_render_context_create);
+ INIT_SYM(mpv_render_context_set_parameter);
+ INIT_SYM(mpv_render_context_get_info);
+ INIT_SYM(mpv_render_context_set_update_callback);
+ INIT_SYM(mpv_render_context_update);
+ INIT_SYM(mpv_render_context_render);
+ INIT_SYM(mpv_render_context_report_swap);
+ INIT_SYM(mpv_render_context_free);
+
+ INIT_SYM(mpv_stream_cb_add_ro);
+
+#undef INIT_SYM
+}
+
+static int load_cplugin(struct mp_script_args *args)
+{
+ void *lib = dlopen(args->filename, RTLD_NOW | RTLD_LOCAL);
+ if (!lib)
+ goto error;
+ // Note: once loaded, we never unload, as unloading the libraries linked to
+ // the plugin can cause random serious problems.
+ mpv_open_cplugin sym = (mpv_open_cplugin)dlsym(lib, MPV_DLOPEN_FN);
+ if (!sym)
+ goto error;
+
+ init_sym_table(args, lib);
+
+ return sym(args->client) ? -1 : 0;
+error: ;
+ char *err = dlerror();
+ if (err)
+ MP_ERR(args, "C plugin error: '%s'\n", err);
+ return -1;
+}
+
+const struct mp_scripting mp_scripting_cplugin = {
+ .name = "cplugin",
+ #if HAVE_WIN32
+ .file_ext = "dll",
+ #else
+ .file_ext = "so",
+ #endif
+ .load = load_cplugin,
+};
+
+#endif
+
+static int load_run(struct mp_script_args *args)
+{
+ // The arg->client object might die and with it args->log, so duplicate it.
+ args->log = mp_log_new(args, args->log, NULL);
+
+ int fds[2];
+ if (!mp_ipc_start_anon_client(args->mpctx->ipc_ctx, args->client, fds))
+ return -1;
+ args->client = NULL; // ownership lost
+
+ char *fdopt = fds[1] >= 0 ? mp_tprintf(80, "--mpv-ipc-fd=%d:%d", fds[0], fds[1])
+ : mp_tprintf(80, "--mpv-ipc-fd=%d", fds[0]);
+
+ struct mp_subprocess_opts opts = {
+ .exe = (char *)args->filename,
+ .args = (char *[]){(char *)args->filename, fdopt, NULL},
+ .fds = {
+ // Keep terminal stuff
+ {.fd = 0, .src_fd = 0,},
+ {.fd = 1, .src_fd = 1,},
+ {.fd = 2, .src_fd = 2,},
+ // Just hope these don't step over each other (e.g. fds[1] could be
+ // below 4, if the std FDs are missing).
+ {.fd = fds[0], .src_fd = fds[0], },
+ {.fd = fds[1], .src_fd = fds[1], },
+ },
+ .num_fds = fds[1] >= 0 ? 5 : 4,
+ .detach = true,
+ };
+ struct mp_subprocess_result res;
+ mp_subprocess2(&opts, &res);
+
+ // Closing these will (probably) make the client exit, if it really died.
+ // They _should_ be CLOEXEC, but are not, because
+ // posix_spawn_file_actions_adddup2() may not clear the CLOEXEC flag
+ // properly if by coincidence fd==src_fd.
+ close(fds[0]);
+ if (fds[1] >= 0)
+ close(fds[1]);
+
+ if (res.error < 0) {
+ MP_ERR(args, "Starting '%s' failed: %s\n", args->filename,
+ mp_subprocess_err_str(res.error));
+ return -1;
+ }
+
+ return 0;
+}
+
+const struct mp_scripting mp_scripting_run = {
+ .name = "ipc",
+ .file_ext = "run",
+ .no_thread = true,
+ .load = load_run,
+};