summaryrefslogtreecommitdiffstats
path: root/osdep
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
commit51de1d8436100f725f3576aefa24a2bd2057bc28 (patch)
treec6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /osdep
parentInitial commit. (diff)
downloadmpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz
mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'osdep')
-rw-r--r--osdep/android/strnlen.c40
-rw-r--r--osdep/android/strnlen.h33
-rw-r--r--osdep/apple_utils.c39
-rw-r--r--osdep/apple_utils.h28
-rw-r--r--osdep/compiler.h30
-rw-r--r--osdep/endian.h37
-rw-r--r--osdep/getpid.h29
-rw-r--r--osdep/glob-win.c162
-rw-r--r--osdep/io.c904
-rw-r--r--osdep/io.h232
-rw-r--r--osdep/language-apple.c45
-rw-r--r--osdep/language-posix.c72
-rw-r--r--osdep/language-win.c65
-rw-r--r--osdep/macOS_swift_bridge.h57
-rw-r--r--osdep/macos/libmpv_helper.swift250
-rw-r--r--osdep/macos/log_helper.swift47
-rw-r--r--osdep/macos/mpv_helper.swift156
-rw-r--r--osdep/macos/precise_timer.swift153
-rw-r--r--osdep/macos/remote_command_center.swift191
-rw-r--r--osdep/macos/swift_compat.swift36
-rw-r--r--osdep/macos/swift_extensions.swift58
-rw-r--r--osdep/macosx_application.h55
-rw-r--r--osdep/macosx_application.m375
-rw-r--r--osdep/macosx_application_objc.h40
-rw-r--r--osdep/macosx_events.h36
-rw-r--r--osdep/macosx_events.m408
-rw-r--r--osdep/macosx_events_objc.h45
-rw-r--r--osdep/macosx_menubar.h30
-rw-r--r--osdep/macosx_menubar.m853
-rw-r--r--osdep/macosx_menubar_objc.h25
-rw-r--r--osdep/macosx_touchbar.h46
-rw-r--r--osdep/macosx_touchbar.m334
-rw-r--r--osdep/main-fn-cocoa.c10
-rw-r--r--osdep/main-fn-unix.c6
-rw-r--r--osdep/main-fn-win.c93
-rw-r--r--osdep/main-fn.h1
-rw-r--r--osdep/meson.build51
-rw-r--r--osdep/mpv.exe.manifest41
-rw-r--r--osdep/mpv.rc50
-rw-r--r--osdep/path-darwin.c77
-rw-r--r--osdep/path-macosx.m34
-rw-r--r--osdep/path-unix.c100
-rw-r--r--osdep/path-uwp.c35
-rw-r--r--osdep/path-win.c113
-rw-r--r--osdep/path.h32
-rw-r--r--osdep/poll_wrapper.c89
-rw-r--r--osdep/poll_wrapper.h12
-rw-r--r--osdep/semaphore.h37
-rw-r--r--osdep/semaphore_osx.c117
-rw-r--r--osdep/strnlen.h31
-rw-r--r--osdep/subprocess-dummy.c7
-rw-r--r--osdep/subprocess-posix.c345
-rw-r--r--osdep/subprocess-win.c516
-rw-r--r--osdep/subprocess.c39
-rw-r--r--osdep/subprocess.h88
-rw-r--r--osdep/terminal-dummy.c35
-rw-r--r--osdep/terminal-unix.c573
-rw-r--r--osdep/terminal-win.c425
-rw-r--r--osdep/terminal.h60
-rw-r--r--osdep/threads-posix.c64
-rw-r--r--osdep/threads-posix.h247
-rw-r--r--osdep/threads-win32.h224
-rw-r--r--osdep/threads.h23
-rw-r--r--osdep/timer-darwin.c48
-rw-r--r--osdep/timer-linux.c64
-rw-r--r--osdep/timer-win32.c141
-rw-r--r--osdep/timer.c67
-rw-r--r--osdep/timer.h63
-rw-r--r--osdep/w32_keyboard.c123
-rw-r--r--osdep/w32_keyboard.h29
-rw-r--r--osdep/win32-console-wrapper.c89
-rw-r--r--osdep/windows_utils.c229
-rw-r--r--osdep/windows_utils.h49
73 files changed, 9388 insertions, 0 deletions
diff --git a/osdep/android/strnlen.c b/osdep/android/strnlen.c
new file mode 100644
index 0000000..c8c9d3d
--- /dev/null
+++ b/osdep/android/strnlen.c
@@ -0,0 +1,40 @@
+/*-
+ * Copyright (c) 2009 David Schultz <das@FreeBSD.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stddef.h>
+#include "osdep/android/strnlen.h"
+
+size_t
+freebsd_strnlen(const char *s, size_t maxlen)
+{
+ size_t len;
+
+ for (len = 0; len < maxlen; len++, s++) {
+ if (!*s)
+ break;
+ }
+ return (len);
+}
diff --git a/osdep/android/strnlen.h b/osdep/android/strnlen.h
new file mode 100644
index 0000000..c1f3391
--- /dev/null
+++ b/osdep/android/strnlen.h
@@ -0,0 +1,33 @@
+/*-
+ * Copyright (c) 2009 David Schultz <das@FreeBSD.org>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef MP_OSDEP_ANDROID_STRNLEN
+#define MP_OSDEP_ANDROID_STRNLEN
+
+size_t
+freebsd_strnlen(const char *s, size_t maxlen);
+
+#endif
diff --git a/osdep/apple_utils.c b/osdep/apple_utils.c
new file mode 100644
index 0000000..02cdfaa
--- /dev/null
+++ b/osdep/apple_utils.c
@@ -0,0 +1,39 @@
+/*
+ * Apple-specific utility functions
+ *
+ * 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 "apple_utils.h"
+
+#include "mpv_talloc.h"
+
+CFStringRef cfstr_from_cstr(const char *str)
+{
+ return CFStringCreateWithCString(NULL, str, kCFStringEncodingUTF8);
+}
+
+char *cfstr_get_cstr(const CFStringRef cfstr)
+{
+ if (!cfstr)
+ return NULL;
+ CFIndex size =
+ CFStringGetMaximumSizeForEncoding(
+ CFStringGetLength(cfstr), kCFStringEncodingUTF8) + 1;
+ char *buffer = talloc_zero_size(NULL, size);
+ CFStringGetCString(cfstr, buffer, size, kCFStringEncodingUTF8);
+ return buffer;
+}
diff --git a/osdep/apple_utils.h b/osdep/apple_utils.h
new file mode 100644
index 0000000..166937e
--- /dev/null
+++ b/osdep/apple_utils.h
@@ -0,0 +1,28 @@
+/*
+ * Apple-specific utility functions
+ *
+ * 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/>.
+ */
+
+#ifndef MPV_APPLE_UTILS
+#define MPV_APPLE_UTILS
+
+#include <CoreFoundation/CoreFoundation.h>
+
+CFStringRef cfstr_from_cstr(const char *str);
+char *cfstr_get_cstr(const CFStringRef cfstr);
+
+#endif /* MPV_APPLE_UTILS */
diff --git a/osdep/compiler.h b/osdep/compiler.h
new file mode 100644
index 0000000..f565897
--- /dev/null
+++ b/osdep/compiler.h
@@ -0,0 +1,30 @@
+#ifndef MPV_COMPILER_H
+#define MPV_COMPILER_H
+
+#define MP_EXPAND_ARGS(...) __VA_ARGS__
+
+#ifdef __GNUC__
+#define PRINTF_ATTRIBUTE(a1, a2) __attribute__ ((format(printf, a1, a2)))
+#define MP_NORETURN __attribute__((noreturn))
+#define MP_FALLTHROUGH __attribute__((fallthrough))
+#define MP_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
+#else
+#define PRINTF_ATTRIBUTE(a1, a2)
+#define MP_NORETURN
+#define MP_FALLTHROUGH do {} while (0)
+#define MP_WARN_UNUSED_RESULT
+#endif
+
+// Broken crap with __USE_MINGW_ANSI_STDIO
+#if defined(__MINGW32__) && defined(__GNUC__) && !defined(__clang__)
+#undef PRINTF_ATTRIBUTE
+#define PRINTF_ATTRIBUTE(a1, a2) __attribute__ ((format (gnu_printf, a1, a2)))
+#endif
+
+#ifdef __GNUC__
+#define MP_ASSERT_UNREACHABLE() (assert(!"unreachable"), __builtin_unreachable())
+#else
+#define MP_ASSERT_UNREACHABLE() (assert(!"unreachable"), abort())
+#endif
+
+#endif
diff --git a/osdep/endian.h b/osdep/endian.h
new file mode 100644
index 0000000..c6d1376
--- /dev/null
+++ b/osdep/endian.h
@@ -0,0 +1,37 @@
+#ifndef MP_ENDIAN_H_
+#define MP_ENDIAN_H_
+
+#include <sys/types.h>
+
+#if !defined(BYTE_ORDER)
+
+#if defined(__BYTE_ORDER)
+#define BYTE_ORDER __BYTE_ORDER
+#define LITTLE_ENDIAN __LITTLE_ENDIAN
+#define BIG_ENDIAN __BIG_ENDIAN
+#elif defined(__DARWIN_BYTE_ORDER)
+#define BYTE_ORDER __DARWIN_BYTE_ORDER
+#define LITTLE_ENDIAN __DARWIN_LITTLE_ENDIAN
+#define BIG_ENDIAN __DARWIN_BIG_ENDIAN
+#else
+#include <libavutil/bswap.h>
+#if AV_HAVE_BIGENDIAN
+#define BYTE_ORDER 1234
+#define LITTLE_ENDIAN 4321
+#define BIG_ENDIAN 1234
+#else
+#define BYTE_ORDER 1234
+#define LITTLE_ENDIAN 1234
+#define BIG_ENDIAN 4321
+#endif
+#endif
+
+#endif /* !defined(BYTE_ORDER) */
+
+#if BYTE_ORDER == BIG_ENDIAN
+#define MP_SELECT_LE_BE(LE, BE) BE
+#else
+#define MP_SELECT_LE_BE(LE, BE) LE
+#endif
+
+#endif
diff --git a/osdep/getpid.h b/osdep/getpid.h
new file mode 100644
index 0000000..ace5e29
--- /dev/null
+++ b/osdep/getpid.h
@@ -0,0 +1,29 @@
+/*
+ * getpid wrapper
+ *
+ * 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/>.
+ */
+
+#pragma once
+
+#ifdef _WIN32
+#include <windows.h>
+#define mp_getpid() GetCurrentProcessId()
+#else // POSIX
+#include <sys/types.h>
+#include <unistd.h>
+#define mp_getpid() getpid()
+#endif
diff --git a/osdep/glob-win.c b/osdep/glob-win.c
new file mode 100644
index 0000000..08fd90f
--- /dev/null
+++ b/osdep/glob-win.c
@@ -0,0 +1,162 @@
+/*
+ * 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 <windows.h>
+#include <stdbool.h>
+#include <string.h>
+#include "osdep/io.h"
+#include "mpv_talloc.h"
+
+#if HAVE_UWP
+// Missing from MinGW headers.
+WINBASEAPI HANDLE WINAPI FindFirstFileExW(LPCWSTR lpFileName,
+ FINDEX_INFO_LEVELS fInfoLevelId, LPVOID lpFindFileData,
+ FINDEX_SEARCH_OPS fSearchOp, LPVOID lpSearchFilter, DWORD dwAdditionalFlags);
+#endif
+
+static wchar_t *talloc_wcsdup(void *ctx, const wchar_t *wcs)
+{
+ size_t len = (wcslen(wcs) + 1) * sizeof(wchar_t);
+ return talloc_memdup(ctx, (void*)wcs, len);
+}
+
+static int compare_wcscoll(const void *v1, const void *v2)
+{
+ wchar_t * const* p1 = v1;
+ wchar_t * const* p2 = v2;
+ return wcscoll(*p1, *p2);
+}
+
+static bool exists(const char *filename)
+{
+ wchar_t *wfilename = mp_from_utf8(NULL, filename);
+ bool result = GetFileAttributesW(wfilename) != INVALID_FILE_ATTRIBUTES;
+ talloc_free(wfilename);
+ return result;
+}
+
+int mp_glob(const char *restrict pattern, int flags,
+ int (*errfunc)(const char*, int), mp_glob_t *restrict pglob)
+{
+ // This glob implementation never calls errfunc and doesn't understand any
+ // flags. These features are currently unused in mpv, however if new code
+ // were to use these them, it would probably break on Windows.
+
+ unsigned dirlen = 0;
+ bool wildcards = false;
+
+ // Check for drive relative paths eg. "C:*.flac"
+ if (pattern[0] != '\0' && pattern[1] == ':')
+ dirlen = 2;
+
+ // Split the directory and filename. All files returned by FindFirstFile
+ // will be in this directory. Also check the filename for wildcards.
+ for (unsigned i = 0; pattern[i]; i ++) {
+ if (pattern[i] == '?' || pattern[i] == '*')
+ wildcards = true;
+
+ if (pattern[i] == '\\' || pattern[i] == '/') {
+ dirlen = i + 1;
+ wildcards = false;
+ }
+ }
+
+ // FindFirstFile is unreliable with certain input (it returns weird results
+ // with paths like "." and "..", and presumably others.) If there are no
+ // wildcards in the filename, don't call it, just check if the file exists.
+ // The CRT globbing code does this too.
+ if (!wildcards) {
+ if (!exists(pattern)) {
+ pglob->gl_pathc = 0;
+ return GLOB_NOMATCH;
+ }
+
+ pglob->ctx = talloc_new(NULL);
+ pglob->gl_pathc = 1;
+ pglob->gl_pathv = talloc_array_ptrtype(pglob->ctx, pglob->gl_pathv, 2);
+ pglob->gl_pathv[0] = talloc_strdup(pglob->ctx, pattern);
+ pglob->gl_pathv[1] = NULL;
+ return 0;
+ }
+
+ wchar_t *wpattern = mp_from_utf8(NULL, pattern);
+ WIN32_FIND_DATAW data;
+ HANDLE find = FindFirstFileExW(wpattern, FindExInfoBasic, &data, FindExSearchNameMatch, NULL, 0);
+ talloc_free(wpattern);
+
+ // Assume an error means there were no matches. mpv doesn't check for
+ // glob() errors, so this should be fine for now.
+ if (find == INVALID_HANDLE_VALUE) {
+ pglob->gl_pathc = 0;
+ return GLOB_NOMATCH;
+ }
+
+ size_t pathc = 0;
+ void *tmp = talloc_new(NULL);
+ wchar_t **wnamev = NULL;
+
+ // Read a list of filenames. Unlike glob(), FindFirstFile doesn't return
+ // the full path, since all files are relative to the directory specified
+ // in the pattern.
+ do {
+ if (!wcscmp(data.cFileName, L".") || !wcscmp(data.cFileName, L".."))
+ continue;
+
+ wchar_t *wname = talloc_wcsdup(tmp, data.cFileName);
+ MP_TARRAY_APPEND(tmp, wnamev, pathc, wname);
+ } while (FindNextFileW(find, &data));
+ FindClose(find);
+
+ if (!wnamev) {
+ talloc_free(tmp);
+ pglob->gl_pathc = 0;
+ return GLOB_NOMATCH;
+ }
+
+ // POSIX glob() is supposed to sort paths according to LC_COLLATE.
+ // FindFirstFile just returns paths in the order they are read from the
+ // directory, so sort them manually with wcscoll.
+ qsort(wnamev, pathc, sizeof(wchar_t*), compare_wcscoll);
+
+ pglob->ctx = talloc_new(NULL);
+ pglob->gl_pathc = pathc;
+ pglob->gl_pathv = talloc_array_ptrtype(pglob->ctx, pglob->gl_pathv,
+ pathc + 1);
+
+ // Now convert all filenames to UTF-8 (they had to be in UTF-16 for
+ // sorting) and prepend the directory
+ for (unsigned i = 0; i < pathc; i ++) {
+ int namelen = WideCharToMultiByte(CP_UTF8, 0, wnamev[i], -1, NULL, 0,
+ NULL, NULL);
+ char *path = talloc_array(pglob->ctx, char, namelen + dirlen);
+
+ memcpy(path, pattern, dirlen);
+ WideCharToMultiByte(CP_UTF8, 0, wnamev[i], -1, path + dirlen,
+ namelen, NULL, NULL);
+ pglob->gl_pathv[i] = path;
+ }
+
+ // gl_pathv must be null terminated
+ pglob->gl_pathv[pathc] = NULL;
+ talloc_free(tmp);
+ return 0;
+}
+
+void mp_globfree(mp_glob_t *pglob)
+{
+ talloc_free(pglob->ctx);
+}
diff --git a/osdep/io.c b/osdep/io.c
new file mode 100644
index 0000000..bdf79f8
--- /dev/null
+++ b/osdep/io.c
@@ -0,0 +1,904 @@
+/*
+ * unicode/utf-8 I/O helpers and wrappers for Windows
+ *
+ * Contains parts based on libav code (http://libav.org).
+ *
+ * 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 <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <limits.h>
+#include <unistd.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+#include "misc/random.h"
+#include "osdep/io.h"
+#include "osdep/terminal.h"
+
+#if HAVE_UWP
+// Missing from MinGW headers.
+#include <windows.h>
+WINBASEAPI UINT WINAPI GetTempFileNameW(LPCWSTR lpPathName, LPCWSTR lpPrefixString,
+ UINT uUnique, LPWSTR lpTempFileName);
+WINBASEAPI DWORD WINAPI GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer);
+WINBASEAPI DWORD WINAPI GetFullPathNameW(LPCWSTR lpFileName, DWORD nBufferLength,
+ LPWSTR lpBuffer, LPWSTR *lpFilePart);
+#endif
+
+// Set the CLOEXEC flag on the given fd.
+// On error, false is returned (and errno set).
+bool mp_set_cloexec(int fd)
+{
+#if defined(F_SETFD)
+ if (fd >= 0) {
+ int flags = fcntl(fd, F_GETFD);
+ if (flags == -1)
+ return false;
+ if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1)
+ return false;
+ }
+#endif
+ return true;
+}
+
+#ifdef __MINGW32__
+int mp_make_cloexec_pipe(int pipes[2])
+{
+ pipes[0] = pipes[1] = -1;
+ return -1;
+}
+#else
+int mp_make_cloexec_pipe(int pipes[2])
+{
+ if (pipe(pipes) != 0) {
+ pipes[0] = pipes[1] = -1;
+ return -1;
+ }
+
+ for (int i = 0; i < 2; i++)
+ mp_set_cloexec(pipes[i]);
+ return 0;
+}
+#endif
+
+#ifdef __MINGW32__
+int mp_make_wakeup_pipe(int pipes[2])
+{
+ return mp_make_cloexec_pipe(pipes);
+}
+#else
+// create a pipe, and set it to non-blocking (and also set FD_CLOEXEC)
+int mp_make_wakeup_pipe(int pipes[2])
+{
+ if (mp_make_cloexec_pipe(pipes) < 0)
+ return -1;
+
+ for (int i = 0; i < 2; i++) {
+ int val = fcntl(pipes[i], F_GETFL) | O_NONBLOCK;
+ fcntl(pipes[i], F_SETFL, val);
+ }
+ return 0;
+}
+#endif
+
+void mp_flush_wakeup_pipe(int pipe_end)
+{
+#ifndef __MINGW32__
+ char buf[100];
+ (void)read(pipe_end, buf, sizeof(buf));
+#endif
+}
+
+#ifdef _WIN32
+
+#include <windows.h>
+#include <wchar.h>
+#include <stdio.h>
+#include <stddef.h>
+
+//copied and modified from libav
+//http://git.libav.org/?p=libav.git;a=blob;f=libavformat/os_support.c;h=a0fcd6c9ba2be4b0dbcc476f6c53587345cc1152;hb=HEADl30
+
+wchar_t *mp_from_utf8(void *talloc_ctx, const char *s)
+{
+ int count = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
+ if (count <= 0)
+ abort();
+ wchar_t *ret = talloc_array(talloc_ctx, wchar_t, count);
+ MultiByteToWideChar(CP_UTF8, 0, s, -1, ret, count);
+ return ret;
+}
+
+char *mp_to_utf8(void *talloc_ctx, const wchar_t *s)
+{
+ int count = WideCharToMultiByte(CP_UTF8, 0, s, -1, NULL, 0, NULL, NULL);
+ if (count <= 0)
+ abort();
+ char *ret = talloc_array(talloc_ctx, char, count);
+ WideCharToMultiByte(CP_UTF8, 0, s, -1, ret, count, NULL, NULL);
+ return ret;
+}
+
+#endif // _WIN32
+
+#ifdef __MINGW32__
+
+#include <io.h>
+#include <fcntl.h>
+#include "osdep/threads.h"
+
+static void set_errno_from_lasterror(void)
+{
+ // This just handles the error codes expected from CreateFile at the moment
+ switch (GetLastError()) {
+ case ERROR_FILE_NOT_FOUND:
+ errno = ENOENT;
+ break;
+ case ERROR_SHARING_VIOLATION:
+ case ERROR_ACCESS_DENIED:
+ errno = EACCES;
+ break;
+ case ERROR_FILE_EXISTS:
+ case ERROR_ALREADY_EXISTS:
+ errno = EEXIST;
+ break;
+ case ERROR_PIPE_BUSY:
+ errno = EAGAIN;
+ break;
+ default:
+ errno = EINVAL;
+ break;
+ }
+}
+
+static time_t filetime_to_unix_time(int64_t wintime)
+{
+ static const int64_t hns_per_second = 10000000ll;
+ static const int64_t win_to_unix_epoch = 11644473600ll;
+ return wintime / hns_per_second - win_to_unix_epoch;
+}
+
+static bool get_file_ids_win8(HANDLE h, dev_t *dev, ino_t *ino)
+{
+ FILE_ID_INFO ii;
+ if (!GetFileInformationByHandleEx(h, FileIdInfo, &ii, sizeof(ii)))
+ return false;
+ *dev = ii.VolumeSerialNumber;
+ // The definition of FILE_ID_128 differs between mingw-w64 and the Windows
+ // SDK, but we can ignore that by just memcpying it. This will also
+ // truncate the file ID on 32-bit Windows, which doesn't support __int128.
+ // 128-bit file IDs are only used for ReFS, so that should be okay.
+ assert(sizeof(*ino) <= sizeof(ii.FileId));
+ memcpy(ino, &ii.FileId, sizeof(*ino));
+ return true;
+}
+
+#if HAVE_UWP
+static bool get_file_ids(HANDLE h, dev_t *dev, ino_t *ino)
+{
+ return false;
+}
+#else
+static bool get_file_ids(HANDLE h, dev_t *dev, ino_t *ino)
+{
+ // GetFileInformationByHandle works on FAT partitions and Windows 7, but
+ // doesn't work in UWP and can produce non-unique IDs on ReFS
+ BY_HANDLE_FILE_INFORMATION bhfi;
+ if (!GetFileInformationByHandle(h, &bhfi))
+ return false;
+ *dev = bhfi.dwVolumeSerialNumber;
+ *ino = ((ino_t)bhfi.nFileIndexHigh << 32) | bhfi.nFileIndexLow;
+ return true;
+}
+#endif
+
+// Like fstat(), but with a Windows HANDLE
+static int hstat(HANDLE h, struct mp_stat *buf)
+{
+ // Handle special (or unknown) file types first
+ switch (GetFileType(h) & ~FILE_TYPE_REMOTE) {
+ case FILE_TYPE_PIPE:
+ *buf = (struct mp_stat){ .st_nlink = 1, .st_mode = _S_IFIFO | 0644 };
+ return 0;
+ case FILE_TYPE_CHAR: // character device
+ *buf = (struct mp_stat){ .st_nlink = 1, .st_mode = _S_IFCHR | 0644 };
+ return 0;
+ case FILE_TYPE_UNKNOWN:
+ errno = EBADF;
+ return -1;
+ }
+
+ struct mp_stat st = { 0 };
+
+ FILE_BASIC_INFO bi;
+ if (!GetFileInformationByHandleEx(h, FileBasicInfo, &bi, sizeof(bi))) {
+ errno = EBADF;
+ return -1;
+ }
+ st.st_atime = filetime_to_unix_time(bi.LastAccessTime.QuadPart);
+ st.st_mtime = filetime_to_unix_time(bi.LastWriteTime.QuadPart);
+ st.st_ctime = filetime_to_unix_time(bi.ChangeTime.QuadPart);
+
+ FILE_STANDARD_INFO si;
+ if (!GetFileInformationByHandleEx(h, FileStandardInfo, &si, sizeof(si))) {
+ errno = EBADF;
+ return -1;
+ }
+ st.st_nlink = si.NumberOfLinks;
+
+ // Here we pretend Windows has POSIX permissions by pretending all
+ // directories are 755 and regular files are 644
+ if (si.Directory) {
+ st.st_mode |= _S_IFDIR | 0755;
+ } else {
+ st.st_mode |= _S_IFREG | 0644;
+ st.st_size = si.EndOfFile.QuadPart;
+ }
+
+ if (!get_file_ids_win8(h, &st.st_dev, &st.st_ino)) {
+ // Fall back to the Windows 7 method (also used for FAT in Win8)
+ if (!get_file_ids(h, &st.st_dev, &st.st_ino)) {
+ errno = EBADF;
+ return -1;
+ }
+ }
+
+ *buf = st;
+ return 0;
+}
+
+int mp_stat(const char *path, struct mp_stat *buf)
+{
+ wchar_t *wpath = mp_from_utf8(NULL, path);
+ HANDLE h = CreateFileW(wpath, FILE_READ_ATTRIBUTES,
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
+ OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | SECURITY_SQOS_PRESENT |
+ SECURITY_IDENTIFICATION, NULL);
+ talloc_free(wpath);
+ if (h == INVALID_HANDLE_VALUE) {
+ set_errno_from_lasterror();
+ return -1;
+ }
+
+ int ret = hstat(h, buf);
+ CloseHandle(h);
+ return ret;
+}
+
+int mp_fstat(int fd, struct mp_stat *buf)
+{
+ HANDLE h = (HANDLE)_get_osfhandle(fd);
+ if (h == INVALID_HANDLE_VALUE) {
+ errno = EBADF;
+ return -1;
+ }
+ // Use mpv's hstat() function rather than MSVCRT's fstat() because mpv's
+ // supports directories and device/inode numbers.
+ return hstat(h, buf);
+}
+
+#if HAVE_UWP
+static int mp_vfprintf(FILE *stream, const char *format, va_list args)
+{
+ return vfprintf(stream, format, args);
+}
+#else
+static int mp_check_console(HANDLE wstream)
+{
+ if (wstream != INVALID_HANDLE_VALUE) {
+ unsigned int filetype = GetFileType(wstream);
+
+ if (!((filetype == FILE_TYPE_UNKNOWN) &&
+ (GetLastError() != ERROR_SUCCESS)))
+ {
+ filetype &= ~(FILE_TYPE_REMOTE);
+
+ if (filetype == FILE_TYPE_CHAR) {
+ DWORD ConsoleMode;
+ int ret = GetConsoleMode(wstream, &ConsoleMode);
+
+ if (!(!ret && (GetLastError() == ERROR_INVALID_HANDLE))) {
+ // This seems to be a console
+ return 1;
+ }
+ }
+ }
+ }
+
+ return 0;
+}
+
+static int mp_vfprintf(FILE *stream, const char *format, va_list args)
+{
+ int done = 0;
+
+ HANDLE wstream = INVALID_HANDLE_VALUE;
+
+ if (stream == stdout || stream == stderr) {
+ wstream = GetStdHandle(stream == stdout ?
+ STD_OUTPUT_HANDLE : STD_ERROR_HANDLE);
+ }
+
+ if (mp_check_console(wstream)) {
+ size_t len = vsnprintf(NULL, 0, format, args) + 1;
+ char *buf = talloc_array(NULL, char, len);
+
+ if (buf) {
+ done = vsnprintf(buf, len, format, args);
+ mp_write_console_ansi(wstream, buf);
+ }
+ talloc_free(buf);
+ } else {
+ done = vfprintf(stream, format, args);
+ }
+
+ return done;
+}
+#endif
+
+int mp_fprintf(FILE *stream, const char *format, ...)
+{
+ int res;
+ va_list args;
+ va_start(args, format);
+ res = mp_vfprintf(stream, format, args);
+ va_end(args);
+ return res;
+}
+
+int mp_printf(const char *format, ...)
+{
+ int res;
+ va_list args;
+ va_start(args, format);
+ res = mp_vfprintf(stdout, format, args);
+ va_end(args);
+ return res;
+}
+
+int mp_open(const char *filename, int oflag, ...)
+{
+ // Always use all share modes, which is useful for opening files that are
+ // open in other processes, and also more POSIX-like
+ static const DWORD share =
+ FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
+ // Setting FILE_APPEND_DATA and avoiding GENERIC_WRITE/FILE_WRITE_DATA
+ // will make the file handle use atomic append behavior
+ static const DWORD append =
+ FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA;
+
+ DWORD access = 0;
+ DWORD disposition = 0;
+ DWORD flags = 0;
+
+ switch (oflag & (_O_RDONLY | _O_RDWR | _O_WRONLY | _O_APPEND)) {
+ case _O_RDONLY:
+ access = GENERIC_READ;
+ flags |= FILE_FLAG_BACKUP_SEMANTICS; // For opening directories
+ break;
+ case _O_RDWR:
+ access = GENERIC_READ | GENERIC_WRITE;
+ break;
+ case _O_RDWR | _O_APPEND:
+ case _O_RDONLY | _O_APPEND:
+ access = GENERIC_READ | append;
+ break;
+ case _O_WRONLY:
+ access = GENERIC_WRITE;
+ break;
+ case _O_WRONLY | _O_APPEND:
+ access = append;
+ break;
+ default:
+ errno = EINVAL;
+ return -1;
+ }
+
+ switch (oflag & (_O_CREAT | _O_EXCL | _O_TRUNC)) {
+ case 0:
+ case _O_EXCL: // Like MSVCRT, ignore invalid use of _O_EXCL
+ disposition = OPEN_EXISTING;
+ break;
+ case _O_TRUNC:
+ case _O_TRUNC | _O_EXCL:
+ disposition = TRUNCATE_EXISTING;
+ break;
+ case _O_CREAT:
+ disposition = OPEN_ALWAYS;
+ flags |= FILE_ATTRIBUTE_NORMAL;
+ break;
+ case _O_CREAT | _O_TRUNC:
+ disposition = CREATE_ALWAYS;
+ break;
+ case _O_CREAT | _O_EXCL:
+ case _O_CREAT | _O_EXCL | _O_TRUNC:
+ disposition = CREATE_NEW;
+ flags |= FILE_ATTRIBUTE_NORMAL;
+ break;
+ }
+
+ // Opening a named pipe as a file can allow the pipe server to impersonate
+ // mpv's process, which could be a security issue. Set SQOS flags, so pipe
+ // servers can only identify the mpv process, not impersonate it.
+ if (disposition != CREATE_NEW)
+ flags |= SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION;
+
+ // Keep the same semantics for some MSVCRT-specific flags
+ if (oflag & _O_TEMPORARY) {
+ flags |= FILE_FLAG_DELETE_ON_CLOSE;
+ access |= DELETE;
+ }
+ if (oflag & _O_SHORT_LIVED)
+ flags |= FILE_ATTRIBUTE_TEMPORARY;
+ if (oflag & _O_SEQUENTIAL) {
+ flags |= FILE_FLAG_SEQUENTIAL_SCAN;
+ } else if (oflag & _O_RANDOM) {
+ flags |= FILE_FLAG_RANDOM_ACCESS;
+ }
+
+ // Open the Windows file handle
+ wchar_t *wpath = mp_from_utf8(NULL, filename);
+ HANDLE h = CreateFileW(wpath, access, share, NULL, disposition, flags, NULL);
+ talloc_free(wpath);
+ if (h == INVALID_HANDLE_VALUE) {
+ set_errno_from_lasterror();
+ return -1;
+ }
+
+ // Map the Windows file handle to a CRT file descriptor. Note: MSVCRT only
+ // cares about the following oflags.
+ oflag &= _O_APPEND | _O_RDONLY | _O_RDWR | _O_WRONLY;
+ oflag |= _O_NOINHERIT; // We never create inheritable handles
+ int fd = _open_osfhandle((intptr_t)h, oflag);
+ if (fd < 0) {
+ CloseHandle(h);
+ return -1;
+ }
+
+ return fd;
+}
+
+int mp_creat(const char *filename, int mode)
+{
+ return mp_open(filename, _O_CREAT | _O_WRONLY | _O_TRUNC, mode);
+}
+
+int mp_rename(const char *oldpath, const char *newpath)
+{
+ wchar_t *woldpath = mp_from_utf8(NULL, oldpath),
+ *wnewpath = mp_from_utf8(NULL, newpath);
+ BOOL ok = MoveFileExW(woldpath, wnewpath, MOVEFILE_REPLACE_EXISTING);
+ talloc_free(woldpath);
+ talloc_free(wnewpath);
+ if (!ok) {
+ set_errno_from_lasterror();
+ return -1;
+ }
+ return 0;
+}
+
+FILE *mp_fopen(const char *filename, const char *mode)
+{
+ if (!mode[0]) {
+ errno = EINVAL;
+ return NULL;
+ }
+
+ int rwmode;
+ int oflags = 0;
+ switch (mode[0]) {
+ case 'r':
+ rwmode = _O_RDONLY;
+ break;
+ case 'w':
+ rwmode = _O_WRONLY;
+ oflags |= _O_CREAT | _O_TRUNC;
+ break;
+ case 'a':
+ rwmode = _O_WRONLY;
+ oflags |= _O_CREAT | _O_APPEND;
+ break;
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ // Parse extra mode flags
+ for (const char *pos = mode + 1; *pos; pos++) {
+ switch (*pos) {
+ case '+': rwmode = _O_RDWR; break;
+ case 'x': oflags |= _O_EXCL; break;
+ // Ignore unknown flags (glibc does too)
+ default: break;
+ }
+ }
+
+ // Open a CRT file descriptor
+ int fd = mp_open(filename, rwmode | oflags);
+ if (fd < 0)
+ return NULL;
+
+ // Add 'b' to the mode so the CRT knows the file is opened in binary mode
+ char bmode[] = { mode[0], 'b', rwmode == _O_RDWR ? '+' : '\0', '\0' };
+ FILE *fp = fdopen(fd, bmode);
+ if (!fp) {
+ close(fd);
+ return NULL;
+ }
+
+ return fp;
+}
+
+// Windows' MAX_PATH/PATH_MAX/FILENAME_MAX is fixed to 260, but this limit
+// applies to unicode paths encoded with wchar_t (2 bytes on Windows). The UTF-8
+// version could end up bigger in memory. In the worst case each wchar_t is
+// encoded to 3 bytes in UTF-8, so in the worst case we have:
+// wcslen(wpath) * 3 <= strlen(utf8path)
+// Thus we need MP_PATH_MAX as the UTF-8/char version of PATH_MAX.
+// Also make sure there's free space for the terminating \0.
+// (For codepoints encoded as UTF-16 surrogate pairs, UTF-8 has the same length.)
+#define MP_PATH_MAX (FILENAME_MAX * 3 + 1)
+
+struct mp_dir {
+ DIR crap; // must be first member
+ _WDIR *wdir;
+ union {
+ struct dirent dirent;
+ // dirent has space only for FILENAME_MAX bytes. _wdirent has space for
+ // FILENAME_MAX wchar_t, which might end up bigger as UTF-8 in some
+ // cases. Guarantee we can always hold _wdirent.d_name converted to
+ // UTF-8 (see MP_PATH_MAX).
+ // This works because dirent.d_name is the last member of dirent.
+ char space[MP_PATH_MAX];
+ };
+};
+
+DIR* mp_opendir(const char *path)
+{
+ wchar_t *wpath = mp_from_utf8(NULL, path);
+ _WDIR *wdir = _wopendir(wpath);
+ talloc_free(wpath);
+ if (!wdir)
+ return NULL;
+ struct mp_dir *mpdir = talloc(NULL, struct mp_dir);
+ // DIR is supposed to be opaque, but unfortunately the MinGW headers still
+ // define it. Make sure nobody tries to use it.
+ memset(&mpdir->crap, 0xCD, sizeof(mpdir->crap));
+ mpdir->wdir = wdir;
+ return (DIR*)mpdir;
+}
+
+struct dirent* mp_readdir(DIR *dir)
+{
+ struct mp_dir *mpdir = (struct mp_dir*)dir;
+ struct _wdirent *wdirent = _wreaddir(mpdir->wdir);
+ if (!wdirent)
+ return NULL;
+ size_t buffersize = sizeof(mpdir->space) - offsetof(struct dirent, d_name);
+ WideCharToMultiByte(CP_UTF8, 0, wdirent->d_name, -1, mpdir->dirent.d_name,
+ buffersize, NULL, NULL);
+ mpdir->dirent.d_ino = 0;
+ mpdir->dirent.d_reclen = 0;
+ mpdir->dirent.d_namlen = strlen(mpdir->dirent.d_name);
+ return &mpdir->dirent;
+}
+
+int mp_closedir(DIR *dir)
+{
+ struct mp_dir *mpdir = (struct mp_dir*)dir;
+ int res = _wclosedir(mpdir->wdir);
+ talloc_free(mpdir);
+ return res;
+}
+
+int mp_mkdir(const char *path, int mode)
+{
+ wchar_t *wpath = mp_from_utf8(NULL, path);
+ int res = _wmkdir(wpath);
+ talloc_free(wpath);
+ return res;
+}
+
+char *mp_win32_getcwd(char *buf, size_t size)
+{
+ if (size >= SIZE_MAX / 3 - 1) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ size_t wbuffer = size * 3 + 1;
+ wchar_t *wres = talloc_array(NULL, wchar_t, wbuffer);
+ DWORD wlen = GetFullPathNameW(L".", wbuffer, wres, NULL);
+ if (wlen >= wbuffer || wlen == 0) {
+ talloc_free(wres);
+ errno = wlen ? ERANGE : ENOENT;
+ return NULL;
+ }
+ char *t = mp_to_utf8(NULL, wres);
+ talloc_free(wres);
+ size_t st = strlen(t);
+ if (st >= size) {
+ talloc_free(t);
+ errno = ERANGE;
+ return NULL;
+ }
+ memcpy(buf, t, st + 1);
+ talloc_free(t);
+ return buf;
+}
+
+static char **utf8_environ;
+static void *utf8_environ_ctx;
+
+static void free_env(void)
+{
+ talloc_free(utf8_environ_ctx);
+ utf8_environ_ctx = NULL;
+ utf8_environ = NULL;
+}
+
+// Note: UNIX getenv() returns static strings, and we try to do the same. Since
+// using putenv() is not multithreading safe, we don't expect env vars to change
+// at runtime, and converting/allocating them in advance is ok.
+static void init_getenv(void)
+{
+#if !HAVE_UWP
+ wchar_t *wenv = GetEnvironmentStringsW();
+ if (!wenv)
+ return;
+ utf8_environ_ctx = talloc_new(NULL);
+ int num_env = 0;
+ while (1) {
+ size_t len = wcslen(wenv);
+ if (!len)
+ break;
+ char *s = mp_to_utf8(utf8_environ_ctx, wenv);
+ MP_TARRAY_APPEND(utf8_environ_ctx, utf8_environ, num_env, s);
+ wenv += len + 1;
+ }
+ MP_TARRAY_APPEND(utf8_environ_ctx, utf8_environ, num_env, NULL);
+ // Avoid showing up in leak detectors etc.
+ atexit(free_env);
+#endif
+}
+
+char *mp_getenv(const char *name)
+{
+ static mp_once once_init_getenv = MP_STATIC_ONCE_INITIALIZER;
+ mp_exec_once(&once_init_getenv, init_getenv);
+ // Copied from musl, http://git.musl-libc.org/cgit/musl/tree/COPYRIGHT
+ // Copyright © 2005-2013 Rich Felker, standard MIT license
+ int i;
+ size_t l = strlen(name);
+ if (!utf8_environ || !*name || strchr(name, '=')) return NULL;
+ for (i=0; utf8_environ[i] && (strncmp(name, utf8_environ[i], l)
+ || utf8_environ[i][l] != '='); i++) {}
+ if (utf8_environ[i]) return utf8_environ[i] + l+1;
+ return NULL;
+}
+
+char ***mp_penviron(void)
+{
+ mp_getenv(""); // ensure init
+ return &utf8_environ; // `environ' should be an l-value
+}
+
+off_t mp_lseek(int fd, off_t offset, int whence)
+{
+ HANDLE h = (HANDLE)_get_osfhandle(fd);
+ if (h != INVALID_HANDLE_VALUE && GetFileType(h) != FILE_TYPE_DISK) {
+ errno = ESPIPE;
+ return (off_t)-1;
+ }
+ return _lseeki64(fd, offset, whence);
+}
+
+_Thread_local
+static struct {
+ DWORD errcode;
+ char *errstring;
+} mp_dl_result = {
+ .errcode = 0,
+ .errstring = NULL
+};
+
+static void mp_dl_free(void)
+{
+ if (mp_dl_result.errstring != NULL) {
+ talloc_free(mp_dl_result.errstring);
+ }
+}
+
+static void mp_dl_init(void)
+{
+ atexit(mp_dl_free);
+}
+
+void *mp_dlopen(const char *filename, int flag)
+{
+ wchar_t *wfilename = mp_from_utf8(NULL, filename);
+ HMODULE lib = LoadLibraryW(wfilename);
+ talloc_free(wfilename);
+ mp_dl_result.errcode = GetLastError();
+ return (void *)lib;
+}
+
+void *mp_dlsym(void *handle, const char *symbol)
+{
+ FARPROC addr = GetProcAddress((HMODULE)handle, symbol);
+ mp_dl_result.errcode = GetLastError();
+ return (void *)addr;
+}
+
+char *mp_dlerror(void)
+{
+ static mp_once once_init_dlerror = MP_STATIC_ONCE_INITIALIZER;
+ mp_exec_once(&once_init_dlerror, mp_dl_init);
+ mp_dl_free();
+
+ if (mp_dl_result.errcode == 0)
+ return NULL;
+
+ // convert error code to a string message
+ LPWSTR werrstring = NULL;
+ FormatMessageW(
+ FORMAT_MESSAGE_FROM_SYSTEM
+ | FORMAT_MESSAGE_IGNORE_INSERTS
+ | FORMAT_MESSAGE_ALLOCATE_BUFFER,
+ NULL,
+ mp_dl_result.errcode,
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL),
+ (LPWSTR) &werrstring,
+ 0,
+ NULL);
+ mp_dl_result.errcode = 0;
+
+ if (werrstring) {
+ mp_dl_result.errstring = mp_to_utf8(NULL, werrstring);
+ LocalFree(werrstring);
+ }
+
+ return mp_dl_result.errstring == NULL
+ ? "unknown error"
+ : mp_dl_result.errstring;
+}
+
+#if HAVE_UWP
+void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
+{
+ errno = ENOSYS;
+ return MAP_FAILED;
+}
+
+int munmap(void *addr, size_t length)
+{
+ errno = ENOSYS;
+ return -1;
+}
+
+int msync(void *addr, size_t length, int flags)
+{
+ errno = ENOSYS;
+ return -1;
+}
+#else
+// Limited mmap() wrapper, inspired by:
+// http://code.google.com/p/mman-win32/source/browse/trunk/mman.c
+
+void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
+{
+ assert(addr == NULL); // not implemented
+ assert(flags == MAP_SHARED); // not implemented
+
+ HANDLE osf = (HANDLE)_get_osfhandle(fd);
+ if (!osf) {
+ errno = EBADF;
+ return MAP_FAILED;
+ }
+
+ DWORD protect = 0;
+ DWORD access = 0;
+ if (prot & PROT_WRITE) {
+ protect = PAGE_READWRITE;
+ access = FILE_MAP_WRITE;
+ } else if (prot & PROT_READ) {
+ protect = PAGE_READONLY;
+ access = FILE_MAP_READ;
+ }
+
+ DWORD l_low = (uint32_t)length;
+ DWORD l_high = ((uint64_t)length) >> 32;
+ HANDLE map = CreateFileMapping(osf, NULL, protect, l_high, l_low, NULL);
+
+ if (!map) {
+ errno = EACCES; // something random
+ return MAP_FAILED;
+ }
+
+ DWORD o_low = (uint32_t)offset;
+ DWORD o_high = ((uint64_t)offset) >> 32;
+ void *p = MapViewOfFile(map, access, o_high, o_low, length);
+
+ CloseHandle(map);
+
+ if (!p) {
+ errno = EINVAL;
+ return MAP_FAILED;
+ }
+ return p;
+}
+
+int munmap(void *addr, size_t length)
+{
+ UnmapViewOfFile(addr);
+ return 0;
+}
+
+int msync(void *addr, size_t length, int flags)
+{
+ FlushViewOfFile(addr, length);
+ return 0;
+}
+#endif
+
+locale_t newlocale(int category, const char *locale, locale_t base)
+{
+ return (locale_t)1;
+}
+
+locale_t uselocale(locale_t locobj)
+{
+ return (locale_t)1;
+}
+
+void freelocale(locale_t locobj)
+{
+}
+
+#endif // __MINGW32__
+
+int mp_mkostemps(char *template, int suffixlen, int flags)
+{
+ size_t len = strlen(template);
+ char *t = len >= 6 + suffixlen ? &template[len - (6 + suffixlen)] : NULL;
+ if (!t || strncmp(t, "XXXXXX", 6) != 0) {
+ errno = EINVAL;
+ return -1;
+ }
+
+ for (size_t fuckshit = 0; fuckshit < UINT32_MAX; fuckshit++) {
+ // Using a random value may make it require fewer iterations (even if
+ // not truly random; just a counter would be sufficient).
+ size_t fuckmess = mp_rand_next();
+ char crap[7] = "";
+ snprintf(crap, sizeof(crap), "%06zx", fuckmess);
+ memcpy(t, crap, 6);
+
+ int res = open(template, O_RDWR | O_CREAT | O_EXCL | flags, 0600);
+ if (res >= 0 || errno != EEXIST)
+ return res;
+ }
+
+ errno = EEXIST;
+ return -1;
+}
diff --git a/osdep/io.h b/osdep/io.h
new file mode 100644
index 0000000..db711fb
--- /dev/null
+++ b/osdep/io.h
@@ -0,0 +1,232 @@
+/*
+ * unicode/utf-8 I/O helpers and wrappers for Windows
+ *
+ * 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/>.
+ */
+
+#ifndef MPLAYER_OSDEP_IO
+#define MPLAYER_OSDEP_IO
+
+#include "config.h"
+#include <stdbool.h>
+#include <stdint.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <locale.h>
+
+#if HAVE_GLOB_POSIX
+#include <glob.h>
+#endif
+
+#if HAVE_ANDROID
+# include <unistd.h>
+# include <stdio.h>
+
+// replace lseek with the 64bit variant
+#ifdef lseek
+# undef lseek
+#endif
+#define lseek(f,p,w) lseek64((f), (p), (w))
+
+// replace possible fseeko with a
+// lseek64 based solution.
+#ifdef fseeko
+# undef fseeko
+#endif
+static inline int mp_fseeko(FILE* fp, off64_t offset, int whence) {
+ int ret = -1;
+ if ((ret = fflush(fp)) != 0) {
+ return ret;
+ }
+
+ return lseek64(fileno(fp), offset, whence) >= 0 ? 0 : -1;
+}
+#define fseeko(f,p,w) mp_fseeko((f), (p), (w))
+
+#endif // HAVE_ANDROID
+
+#ifndef O_BINARY
+#define O_BINARY 0
+#endif
+
+// This is in POSIX.1-2008, but support outside of Linux is scarce.
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#endif
+#ifndef FD_CLOEXEC
+#define FD_CLOEXEC 0
+#endif
+
+bool mp_set_cloexec(int fd);
+int mp_make_cloexec_pipe(int pipes[2]);
+int mp_make_wakeup_pipe(int pipes[2]);
+void mp_flush_wakeup_pipe(int pipe_end);
+
+#ifdef _WIN32
+#include <wchar.h>
+wchar_t *mp_from_utf8(void *talloc_ctx, const char *s);
+char *mp_to_utf8(void *talloc_ctx, const wchar_t *s);
+#endif
+
+#ifdef __CYGWIN__
+#include <io.h>
+#endif
+
+#ifdef __MINGW32__
+
+#include <stdio.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+int mp_printf(const char *format, ...);
+int mp_fprintf(FILE *stream, const char *format, ...);
+int mp_open(const char *filename, int oflag, ...);
+int mp_creat(const char *filename, int mode);
+int mp_rename(const char *oldpath, const char *newpath);
+FILE *mp_fopen(const char *filename, const char *mode);
+DIR *mp_opendir(const char *path);
+struct dirent *mp_readdir(DIR *dir);
+int mp_closedir(DIR *dir);
+int mp_mkdir(const char *path, int mode);
+char *mp_win32_getcwd(char *buf, size_t size);
+char *mp_getenv(const char *name);
+
+#ifdef environ /* mingw defines it as _environ */
+#undef environ
+#endif
+#define environ (*mp_penviron()) /* ensure initialization and l-value */
+char ***mp_penviron(void);
+
+off_t mp_lseek(int fd, off_t offset, int whence);
+void *mp_dlopen(const char *filename, int flag);
+void *mp_dlsym(void *handle, const char *symbol);
+char *mp_dlerror(void);
+
+// mp_stat types. MSVCRT's dev_t and ino_t are way too short to be unique.
+typedef uint64_t mp_dev_t_;
+#ifdef _WIN64
+typedef unsigned __int128 mp_ino_t_;
+#else
+// 32-bit Windows doesn't have a __int128-type, which means ReFS file IDs will
+// be truncated and might collide. This is probably not a problem because ReFS
+// is not available in consumer versions of Windows.
+typedef uint64_t mp_ino_t_;
+#endif
+#define dev_t mp_dev_t_
+#define ino_t mp_ino_t_
+
+// mp_stat uses a different structure to MSVCRT, with 64-bit inodes
+struct mp_stat {
+ dev_t st_dev;
+ ino_t st_ino;
+ unsigned short st_mode;
+ unsigned int st_nlink;
+ short st_uid;
+ short st_gid;
+ dev_t st_rdev;
+ int64_t st_size;
+ time_t st_atime;
+ time_t st_mtime;
+ time_t st_ctime;
+};
+
+int mp_stat(const char *path, struct mp_stat *buf);
+int mp_fstat(int fd, struct mp_stat *buf);
+
+typedef struct {
+ size_t gl_pathc;
+ char **gl_pathv;
+ size_t gl_offs;
+ void *ctx;
+} mp_glob_t;
+
+// glob-win.c
+int mp_glob(const char *restrict pattern, int flags,
+ int (*errfunc)(const char*, int), mp_glob_t *restrict pglob);
+void mp_globfree(mp_glob_t *pglob);
+
+#define printf(...) mp_printf(__VA_ARGS__)
+#define fprintf(...) mp_fprintf(__VA_ARGS__)
+#define open(...) mp_open(__VA_ARGS__)
+#define creat(...) mp_creat(__VA_ARGS__)
+#define rename(...) mp_rename(__VA_ARGS__)
+#define fopen(...) mp_fopen(__VA_ARGS__)
+#define opendir(...) mp_opendir(__VA_ARGS__)
+#define readdir(...) mp_readdir(__VA_ARGS__)
+#define closedir(...) mp_closedir(__VA_ARGS__)
+#define mkdir(...) mp_mkdir(__VA_ARGS__)
+#define getcwd(...) mp_win32_getcwd(__VA_ARGS__)
+#define getenv(...) mp_getenv(__VA_ARGS__)
+
+#undef lseek
+#define lseek(...) mp_lseek(__VA_ARGS__)
+
+#define RTLD_NOW 0
+#define RTLD_LOCAL 0
+#define dlopen(fn,fg) mp_dlopen((fn), (fg))
+#define dlsym(h,s) mp_dlsym((h), (s))
+#define dlerror mp_dlerror
+
+// Affects both "stat()" and "struct stat".
+#undef stat
+#define stat mp_stat
+
+#undef fstat
+#define fstat(...) mp_fstat(__VA_ARGS__)
+
+#define utime(...) _utime(__VA_ARGS__)
+#define utimbuf _utimbuf
+
+void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
+int munmap(void *addr, size_t length);
+int msync(void *addr, size_t length, int flags);
+#define PROT_READ 1
+#define PROT_WRITE 2
+#define MAP_SHARED 1
+#define MAP_FAILED ((void *)-1)
+#define MS_ASYNC 1
+#define MS_SYNC 2
+#define MS_INVALIDATE 4
+
+#ifndef GLOB_NOMATCH
+#define GLOB_NOMATCH 3
+#endif
+
+#define glob_t mp_glob_t
+#define glob(...) mp_glob(__VA_ARGS__)
+#define globfree(...) mp_globfree(__VA_ARGS__)
+
+// These are stubs since there is not anything that helps with this on Windows.
+#define locale_t int
+#define LC_CTYPE_MASK 0
+locale_t newlocale(int, const char *, locale_t);
+locale_t uselocale(locale_t);
+void freelocale(locale_t);
+
+#else /* __MINGW32__ */
+
+#include <sys/mman.h>
+
+extern char **environ;
+
+#endif /* __MINGW32__ */
+
+int mp_mkostemps(char *template, int suffixlen, int flags);
+
+#endif
diff --git a/osdep/language-apple.c b/osdep/language-apple.c
new file mode 100644
index 0000000..dc83fb5
--- /dev/null
+++ b/osdep/language-apple.c
@@ -0,0 +1,45 @@
+/*
+ * User language lookup for Apple platforms
+ *
+ * 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 "misc/language.h"
+
+#include "apple_utils.h"
+#include "mpv_talloc.h"
+
+char **mp_get_user_langs(void)
+{
+ CFArrayRef arr = CFLocaleCopyPreferredLanguages();
+ if (!arr)
+ return NULL;
+ CFIndex count = CFArrayGetCount(arr);
+ if (!count)
+ return NULL;
+
+ char **ret = talloc_array_ptrtype(NULL, ret, count + 1);
+
+ for (CFIndex i = 0; i < count; i++) {
+ CFStringRef cfstr = CFArrayGetValueAtIndex(arr, i);
+ ret[i] = talloc_steal(ret, cfstr_get_cstr(cfstr));
+ }
+
+ ret[count] = NULL;
+
+ CFRelease(arr);
+ return ret;
+}
diff --git a/osdep/language-posix.c b/osdep/language-posix.c
new file mode 100644
index 0000000..8fd68c6
--- /dev/null
+++ b/osdep/language-posix.c
@@ -0,0 +1,72 @@
+/*
+ * User language lookup for generic POSIX platforms
+ *
+ * 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 "misc/language.h"
+#include "mpv_talloc.h"
+
+#include <stddef.h>
+
+char **mp_get_user_langs(void)
+{
+ static const char *const list[] = {
+ "LC_ALL",
+ "LC_MESSAGES",
+ "LANG",
+ NULL
+ };
+
+ size_t nb = 0;
+ char **ret = NULL;
+ bool has_c = false;
+
+ // Prefer anything we get from LANGUAGE first
+ for (const char *langList = getenv("LANGUAGE"); langList && *langList;) {
+ size_t len = strcspn(langList, ":");
+ MP_TARRAY_GROW(NULL, ret, nb);
+ ret[nb++] = talloc_strndup(ret, langList, len);
+ langList += len;
+ while (*langList == ':')
+ langList++;
+ }
+
+ // Then, the language components of other relevant locale env vars
+ for (int i = 0; list[i]; i++) {
+ const char *envval = getenv(list[i]);
+ if (envval && *envval) {
+ size_t len = strcspn(envval, ".@");
+ if (!strncmp("C", envval, len)) {
+ has_c = true;
+ continue;
+ }
+
+ MP_TARRAY_GROW(NULL, ret, nb);
+ ret[nb++] = talloc_strndup(ret, envval, len);
+ }
+ }
+
+ if (has_c && !nb) {
+ MP_TARRAY_GROW(NULL, ret, nb);
+ ret[nb++] = talloc_strdup(ret, "en");
+ }
+
+ // Null-terminate the list
+ MP_TARRAY_APPEND(NULL, ret, nb, NULL);
+
+ return ret;
+}
diff --git a/osdep/language-win.c b/osdep/language-win.c
new file mode 100644
index 0000000..7d8e7fe
--- /dev/null
+++ b/osdep/language-win.c
@@ -0,0 +1,65 @@
+/*
+ * User language lookup for win32
+ *
+ * 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 "misc/language.h"
+#include "mpv_talloc.h"
+#include "osdep/io.h"
+
+#include <windows.h>
+
+char **mp_get_user_langs(void)
+{
+ size_t nb = 0;
+ char **ret = NULL;
+ ULONG got_count = 0;
+ ULONG got_size = 0;
+ if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &got_count, NULL, &got_size) ||
+ got_size == 0)
+ return NULL;
+
+ wchar_t *buf = talloc_array(NULL, wchar_t, got_size);
+
+ if (!GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &got_count, buf, &got_size) ||
+ got_size == 0)
+ goto cleanup;
+
+ for (ULONG pos = 0; buf[pos]; pos += wcslen(buf + pos) + 1) {
+ ret = talloc_realloc(NULL, ret, char*, (nb + 2));
+ ret[nb++] = mp_to_utf8(ret, buf + pos);
+ }
+ ret[nb] = NULL;
+
+ if (!GetSystemPreferredUILanguages(MUI_LANGUAGE_NAME, &got_count, NULL, &got_size))
+ goto cleanup;
+
+ buf = talloc_realloc(NULL, buf, wchar_t, got_size);
+
+ if (!GetSystemPreferredUILanguages(MUI_LANGUAGE_NAME, &got_count, buf, &got_size))
+ goto cleanup;
+
+ for (ULONG pos = 0; buf[pos]; pos += wcslen(buf + pos) + 1) {
+ ret = talloc_realloc(NULL, ret, char*, (nb + 2));
+ ret[nb++] = mp_to_utf8(ret, buf + pos);
+ }
+ ret[nb] = NULL;
+
+cleanup:
+ talloc_free(buf);
+ return ret;
+}
diff --git a/osdep/macOS_swift_bridge.h b/osdep/macOS_swift_bridge.h
new file mode 100644
index 0000000..9407b6f
--- /dev/null
+++ b/osdep/macOS_swift_bridge.h
@@ -0,0 +1,57 @@
+/*
+ * 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/>.
+ */
+
+// including frameworks here again doesn't make sense, but otherwise the swift
+// compiler doesn't include the needed headers in our generated header file
+#import <IOKit/pwr_mgt/IOPMLib.h>
+#import <QuartzCore/QuartzCore.h>
+
+#include "player/client.h"
+#include "video/out/libmpv.h"
+#include "libmpv/render_gl.h"
+
+#include "options/m_config.h"
+#include "player/core.h"
+#include "input/input.h"
+#include "video/out/win_state.h"
+
+#include "osdep/macosx_application_objc.h"
+#include "osdep/macosx_events_objc.h"
+
+
+// complex macros won't get imported to Swift so we have to reassign them
+static int SWIFT_MBTN_LEFT = MP_MBTN_LEFT;
+static int SWIFT_MBTN_MID = MP_MBTN_MID;
+static int SWIFT_MBTN_RIGHT = MP_MBTN_RIGHT;
+static int SWIFT_WHEEL_UP = MP_WHEEL_UP;
+static int SWIFT_WHEEL_DOWN = MP_WHEEL_DOWN;
+static int SWIFT_WHEEL_LEFT = MP_WHEEL_LEFT;
+static int SWIFT_WHEEL_RIGHT = MP_WHEEL_RIGHT;
+static int SWIFT_MBTN_BACK = MP_MBTN_BACK;
+static int SWIFT_MBTN_FORWARD = MP_MBTN_FORWARD;
+static int SWIFT_MBTN9 = MP_MBTN9;
+
+static int SWIFT_KEY_MOUSE_LEAVE = MP_KEY_MOUSE_LEAVE;
+static int SWIFT_KEY_MOUSE_ENTER = MP_KEY_MOUSE_ENTER;
+
+// only used from Swift files and therefore seen as unused by the c compiler
+static void SWIFT_TARRAY_STRING_APPEND(void *t, char ***a, int *i, char *s) __attribute__ ((unused));
+
+static void SWIFT_TARRAY_STRING_APPEND(void *t, char ***a, int *i, char *s)
+{
+ MP_TARRAY_APPEND(t, *a, *i, s);
+}
diff --git a/osdep/macos/libmpv_helper.swift b/osdep/macos/libmpv_helper.swift
new file mode 100644
index 0000000..8b1c697
--- /dev/null
+++ b/osdep/macos/libmpv_helper.swift
@@ -0,0 +1,250 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+import OpenGL.GL
+import OpenGL.GL3
+
+let glDummy: @convention(c) () -> Void = {}
+
+class LibmpvHelper {
+ var log: LogHelper
+ var mpvHandle: OpaquePointer?
+ var mpvRenderContext: OpaquePointer?
+ var macOptsPtr: UnsafeMutableRawPointer?
+ var macOpts: macos_opts = macos_opts()
+ var fbo: GLint = 1
+ let deinitLock = NSLock()
+
+ init(_ mpv: OpaquePointer, _ mpLog: OpaquePointer?) {
+ mpvHandle = mpv
+ log = LogHelper(mpLog)
+
+ guard let app = NSApp as? Application,
+ let ptr = mp_get_config_group(nil,
+ mp_client_get_global(mpvHandle),
+ app.getMacOSConf()) else
+ {
+ log.sendError("macOS config group couldn't be retrieved'")
+ exit(1)
+ }
+ macOptsPtr = ptr
+ macOpts = UnsafeMutablePointer<macos_opts>(OpaquePointer(ptr)).pointee
+ }
+
+ func initRender() {
+ let advanced: CInt = 1
+ let api = UnsafeMutableRawPointer(mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String)
+ let pAddress = mpv_opengl_init_params(get_proc_address: getProcAddress,
+ get_proc_address_ctx: nil)
+
+ MPVHelper.withUnsafeMutableRawPointers([pAddress, advanced]) { (pointers: [UnsafeMutableRawPointer?]) in
+ var params: [mpv_render_param] = [
+ mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api),
+ mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, data: pointers[0]),
+ mpv_render_param(type: MPV_RENDER_PARAM_ADVANCED_CONTROL, data: pointers[1]),
+ mpv_render_param()
+ ]
+
+ if (mpv_render_context_create(&mpvRenderContext, mpvHandle, &params) < 0) {
+ log.sendError("Render context init has failed.")
+ exit(1)
+ }
+ }
+
+ }
+
+ let getProcAddress: (@convention(c) (UnsafeMutableRawPointer?, UnsafePointer<Int8>?)
+ -> UnsafeMutableRawPointer?) =
+ {
+ (ctx: UnsafeMutableRawPointer?, name: UnsafePointer<Int8>?)
+ -> UnsafeMutableRawPointer? in
+ let symbol: CFString = CFStringCreateWithCString(
+ kCFAllocatorDefault, name, kCFStringEncodingASCII)
+ let identifier = CFBundleGetBundleWithIdentifier("com.apple.opengl" as CFString)
+ let addr = CFBundleGetFunctionPointerForName(identifier, symbol)
+
+ if symbol as String == "glFlush" {
+ return unsafeBitCast(glDummy, to: UnsafeMutableRawPointer.self)
+ }
+
+ return addr
+ }
+
+ func setRenderUpdateCallback(_ callback: @escaping mpv_render_update_fn, context object: AnyObject) {
+ if mpvRenderContext == nil {
+ log.sendWarning("Init mpv render context first.")
+ } else {
+ mpv_render_context_set_update_callback(mpvRenderContext, callback, MPVHelper.bridge(obj: object))
+ }
+ }
+
+ func setRenderControlCallback(_ callback: @escaping mp_render_cb_control_fn, context object: AnyObject) {
+ if mpvRenderContext == nil {
+ log.sendWarning("Init mpv render context first.")
+ } else {
+ mp_render_context_set_control_callback(mpvRenderContext, callback, MPVHelper.bridge(obj: object))
+ }
+ }
+
+ func reportRenderFlip() {
+ if mpvRenderContext == nil { return }
+ mpv_render_context_report_swap(mpvRenderContext)
+ }
+
+ func isRenderUpdateFrame() -> Bool {
+ deinitLock.lock()
+ if mpvRenderContext == nil {
+ deinitLock.unlock()
+ return false
+ }
+ let flags: UInt64 = mpv_render_context_update(mpvRenderContext)
+ deinitLock.unlock()
+ return flags & UInt64(MPV_RENDER_UPDATE_FRAME.rawValue) > 0
+ }
+
+ func drawRender(_ surface: NSSize, _ depth: GLint, _ ctx: CGLContextObj, skip: Bool = false) {
+ deinitLock.lock()
+ if mpvRenderContext != nil {
+ var i: GLint = 0
+ let flip: CInt = 1
+ let skip: CInt = skip ? 1 : 0
+ let ditherDepth = depth
+ glGetIntegerv(GLenum(GL_DRAW_FRAMEBUFFER_BINDING), &i)
+ // CAOpenGLLayer has ownership of FBO zero yet can return it to us,
+ // so only utilize a newly received FBO ID if it is nonzero.
+ fbo = i != 0 ? i : fbo
+
+ let data = mpv_opengl_fbo(fbo: Int32(fbo),
+ w: Int32(surface.width),
+ h: Int32(surface.height),
+ internal_format: 0)
+
+ MPVHelper.withUnsafeMutableRawPointers([data, flip, ditherDepth, skip]) { (pointers: [UnsafeMutableRawPointer?]) in
+ var params: [mpv_render_param] = [
+ mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: pointers[0]),
+ mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: pointers[1]),
+ mpv_render_param(type: MPV_RENDER_PARAM_DEPTH, data: pointers[2]),
+ mpv_render_param(type: MPV_RENDER_PARAM_SKIP_RENDERING, data: pointers[3]),
+ mpv_render_param()
+ ]
+ mpv_render_context_render(mpvRenderContext, &params);
+ }
+ } else {
+ glClearColor(0, 0, 0, 1)
+ glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
+ }
+
+ if !skip { CGLFlushDrawable(ctx) }
+
+ deinitLock.unlock()
+ }
+
+ func setRenderICCProfile(_ profile: NSColorSpace) {
+ if mpvRenderContext == nil { return }
+ guard var iccData = profile.iccProfileData else {
+ log.sendWarning("Invalid ICC profile data.")
+ return
+ }
+ iccData.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in
+ guard let baseAddress = ptr.baseAddress, ptr.count > 0 else { return }
+
+ let u8Ptr = baseAddress.assumingMemoryBound(to: UInt8.self)
+ let iccBstr = bstrdup(nil, bstr(start: u8Ptr, len: ptr.count))
+ var icc = mpv_byte_array(data: iccBstr.start, size: iccBstr.len)
+ withUnsafeMutableBytes(of: &icc) { (ptr: UnsafeMutableRawBufferPointer) in
+ let params = mpv_render_param(type: MPV_RENDER_PARAM_ICC_PROFILE, data: ptr.baseAddress)
+ mpv_render_context_set_parameter(mpvRenderContext, params)
+ }
+ }
+ }
+
+ func setRenderLux(_ lux: Int) {
+ if mpvRenderContext == nil { return }
+ var light = lux
+ withUnsafeMutableBytes(of: &light) { (ptr: UnsafeMutableRawBufferPointer) in
+ let params = mpv_render_param(type: MPV_RENDER_PARAM_AMBIENT_LIGHT, data: ptr.baseAddress)
+ mpv_render_context_set_parameter(mpvRenderContext, params)
+ }
+ }
+
+ func commandAsync(_ cmd: [String?], id: UInt64 = 1) {
+ if mpvHandle == nil { return }
+ var mCmd = cmd
+ mCmd.append(nil)
+ var cargs = mCmd.map { $0.flatMap { UnsafePointer<Int8>(strdup($0)) } }
+ mpv_command_async(mpvHandle, id, &cargs)
+ for ptr in cargs { free(UnsafeMutablePointer(mutating: ptr)) }
+ }
+
+ // Unsafe function when called while using the render API
+ func command(_ cmd: String) {
+ if mpvHandle == nil { return }
+ mpv_command_string(mpvHandle, cmd)
+ }
+
+ func getBoolProperty(_ name: String) -> Bool {
+ if mpvHandle == nil { return false }
+ var value = Int32()
+ mpv_get_property(mpvHandle, name, MPV_FORMAT_FLAG, &value)
+ return value > 0
+ }
+
+ func getIntProperty(_ name: String) -> Int {
+ if mpvHandle == nil { return 0 }
+ var value = Int64()
+ mpv_get_property(mpvHandle, name, MPV_FORMAT_INT64, &value)
+ return Int(value)
+ }
+
+ func getStringProperty(_ name: String) -> String? {
+ guard let mpv = mpvHandle else { return nil }
+ guard let value = mpv_get_property_string(mpv, name) else { return nil }
+ let str = String(cString: value)
+ mpv_free(value)
+ return str
+ }
+
+ func deinitRender() {
+ mpv_render_context_set_update_callback(mpvRenderContext, nil, nil)
+ mp_render_context_set_control_callback(mpvRenderContext, nil, nil)
+ deinitLock.lock()
+ mpv_render_context_free(mpvRenderContext)
+ mpvRenderContext = nil
+ deinitLock.unlock()
+ }
+
+ func deinitMPV(_ destroy: Bool = false) {
+ if destroy {
+ mpv_destroy(mpvHandle)
+ }
+ ta_free(macOptsPtr)
+ macOptsPtr = nil
+ mpvHandle = nil
+ }
+
+ // *(char **) MPV_FORMAT_STRING on mpv_event_property
+ class func mpvStringArrayToString(_ obj: UnsafeMutableRawPointer) -> String? {
+ let cstr = UnsafeMutablePointer<UnsafeMutablePointer<Int8>>(OpaquePointer(obj))
+ return String(cString: cstr[0])
+ }
+
+ // MPV_FORMAT_FLAG
+ class func mpvFlagToBool(_ obj: UnsafeMutableRawPointer) -> Bool? {
+ return UnsafePointer<Bool>(OpaquePointer(obj))?.pointee
+ }
+}
diff --git a/osdep/macos/log_helper.swift b/osdep/macos/log_helper.swift
new file mode 100644
index 0000000..9464075
--- /dev/null
+++ b/osdep/macos/log_helper.swift
@@ -0,0 +1,47 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+class LogHelper: NSObject {
+ var log: OpaquePointer?
+
+ init(_ log: OpaquePointer?) {
+ self.log = log
+ }
+
+ func sendVerbose(_ msg: String) {
+ send(message: msg, type: MSGL_V)
+ }
+
+ func sendInfo(_ msg: String) {
+ send(message: msg, type: MSGL_INFO)
+ }
+
+ func sendWarning(_ msg: String) {
+ send(message: msg, type: MSGL_WARN)
+ }
+
+ func sendError(_ msg: String) {
+ send(message: msg, type: MSGL_ERR)
+ }
+
+ func send(message msg: String, type t: Int) {
+ let args: [CVarArg] = [ (msg as NSString).utf8String ?? "NO MESSAGE"]
+ mp_msg_va(log, Int32(t), "%s\n", getVaList(args))
+ }
+}
diff --git a/osdep/macos/mpv_helper.swift b/osdep/macos/mpv_helper.swift
new file mode 100644
index 0000000..3b2a716
--- /dev/null
+++ b/osdep/macos/mpv_helper.swift
@@ -0,0 +1,156 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+typealias swift_wakeup_cb_fn = (@convention(c) (UnsafeMutableRawPointer?) -> Void)?
+
+class MPVHelper {
+ var log: LogHelper
+ var vo: UnsafeMutablePointer<vo>
+ var optsCachePtr: UnsafeMutablePointer<m_config_cache>
+ var optsPtr: UnsafeMutablePointer<mp_vo_opts>
+ var macOptsCachePtr: UnsafeMutablePointer<m_config_cache>
+ var macOptsPtr: UnsafeMutablePointer<macos_opts>
+
+ // these computed properties return a local copy of the struct accessed:
+ // - don't use if you rely on the pointers
+ // - only for reading
+ var vout: vo { get { return vo.pointee } }
+ var optsCache: m_config_cache { get { return optsCachePtr.pointee } }
+ var opts: mp_vo_opts { get { return optsPtr.pointee } }
+ var macOptsCache: m_config_cache { get { return macOptsCachePtr.pointee } }
+ var macOpts: macos_opts { get { return macOptsPtr.pointee } }
+
+ var input: OpaquePointer { get { return vout.input_ctx } }
+
+ init(_ vo: UnsafeMutablePointer<vo>, _ log: LogHelper) {
+ self.vo = vo
+ self.log = log
+
+ guard let app = NSApp as? Application,
+ let cache = m_config_cache_alloc(vo, vo.pointee.global, app.getVoSubConf()) else
+ {
+ log.sendError("NSApp couldn't be retrieved")
+ exit(1)
+ }
+
+ optsCachePtr = cache
+ optsPtr = UnsafeMutablePointer<mp_vo_opts>(OpaquePointer(cache.pointee.opts))
+
+ guard let macCache = m_config_cache_alloc(vo,
+ vo.pointee.global,
+ app.getMacOSConf()) else
+ {
+ // will never be hit, mp_get_config_group asserts for invalid groups
+ exit(1)
+ }
+ macOptsCachePtr = macCache
+ macOptsPtr = UnsafeMutablePointer<macos_opts>(OpaquePointer(macCache.pointee.opts))
+ }
+
+ func canBeDraggedAt(_ pos: NSPoint) -> Bool {
+ let canDrag = !mp_input_test_dragging(input, Int32(pos.x), Int32(pos.y))
+ return canDrag
+ }
+
+ func mouseEnabled() -> Bool {
+ return mp_input_mouse_enabled(input)
+ }
+
+ func setMousePosition(_ pos: NSPoint) {
+ mp_input_set_mouse_pos(input, Int32(pos.x), Int32(pos.y))
+ }
+
+ func putAxis(_ mpkey: Int32, delta: Double) {
+ mp_input_put_wheel(input, mpkey, delta)
+ }
+
+ func nextChangedOption(property: inout UnsafeMutableRawPointer?) -> Bool {
+ return m_config_cache_get_next_changed(optsCachePtr, &property)
+ }
+
+ func setOption(fullscreen: Bool) {
+ optsPtr.pointee.fullscreen = fullscreen
+ _ = withUnsafeMutableBytes(of: &optsPtr.pointee.fullscreen) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(optsCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setOption(minimized: Bool) {
+ optsPtr.pointee.window_minimized = minimized
+ _ = withUnsafeMutableBytes(of: &optsPtr.pointee.window_minimized) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(optsCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setOption(maximized: Bool) {
+ optsPtr.pointee.window_maximized = maximized
+ _ = withUnsafeMutableBytes(of: &optsPtr.pointee.window_maximized) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(optsCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setMacOptionCallback(_ callback: swift_wakeup_cb_fn, context object: AnyObject) {
+ m_config_cache_set_wakeup_cb(macOptsCachePtr, callback, MPVHelper.bridge(obj: object))
+ }
+
+ func nextChangedMacOption(property: inout UnsafeMutableRawPointer?) -> Bool {
+ return m_config_cache_get_next_changed(macOptsCachePtr, &property)
+ }
+
+ func command(_ cmd: String) {
+ let cCmd = UnsafePointer<Int8>(strdup(cmd))
+ let mpvCmd = mp_input_parse_cmd(input, bstr0(cCmd), "")
+ mp_input_queue_cmd(input, mpvCmd)
+ free(UnsafeMutablePointer(mutating: cCmd))
+ }
+
+ // (__bridge void*)
+ class func bridge<T: AnyObject>(obj: T) -> UnsafeMutableRawPointer {
+ return UnsafeMutableRawPointer(Unmanaged.passUnretained(obj).toOpaque())
+ }
+
+ // (__bridge T*)
+ class func bridge<T: AnyObject>(ptr: UnsafeRawPointer) -> T {
+ return Unmanaged<T>.fromOpaque(ptr).takeUnretainedValue()
+ }
+
+ class func withUnsafeMutableRawPointers(_ arguments: [Any],
+ pointers: [UnsafeMutableRawPointer?] = [],
+ closure: (_ pointers: [UnsafeMutableRawPointer?]) -> Void) {
+ if arguments.count > 0 {
+ let args = Array(arguments.dropFirst(1))
+ var newPtrs = pointers
+ var firstArg = arguments.first
+ withUnsafeMutableBytes(of: &firstArg) { (ptr: UnsafeMutableRawBufferPointer) in
+ newPtrs.append(ptr.baseAddress)
+ withUnsafeMutableRawPointers(args, pointers: newPtrs, closure: closure)
+ }
+
+ return
+ }
+
+ closure(pointers)
+ }
+
+ class func getPointer<T>(_ value: inout T) -> UnsafeMutableRawPointer? {
+ return withUnsafeMutableBytes(of: &value) { (ptr: UnsafeMutableRawBufferPointer) in
+ ptr.baseAddress
+ }
+ }
+}
diff --git a/osdep/macos/precise_timer.swift b/osdep/macos/precise_timer.swift
new file mode 100644
index 0000000..f4ad3bb
--- /dev/null
+++ b/osdep/macos/precise_timer.swift
@@ -0,0 +1,153 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+struct Timing {
+ let time: UInt64
+ let closure: () -> ()
+}
+
+class PreciseTimer {
+ unowned var common: Common
+ var mpv: MPVHelper? { get { return common.mpv } }
+
+ let nanoPerSecond: Double = 1e+9
+ let machToNano: Double = {
+ var timebase: mach_timebase_info = mach_timebase_info()
+ mach_timebase_info(&timebase)
+ return Double(timebase.numer) / Double(timebase.denom)
+ }()
+
+ let condition = NSCondition()
+ var events: [Timing] = []
+ var isRunning: Bool = true
+ var isHighPrecision: Bool = false
+
+ var thread: pthread_t!
+ var threadPort: thread_port_t = thread_port_t()
+ let policyFlavor = thread_policy_flavor_t(THREAD_TIME_CONSTRAINT_POLICY)
+ let policyCount = MemoryLayout<thread_time_constraint_policy>.size /
+ MemoryLayout<integer_t>.size
+ var typeNumber: mach_msg_type_number_t {
+ return mach_msg_type_number_t(policyCount)
+ }
+ var threadAttr: pthread_attr_t = {
+ var attr = pthread_attr_t()
+ var param = sched_param()
+ pthread_attr_init(&attr)
+ param.sched_priority = sched_get_priority_max(SCHED_FIFO)
+ pthread_attr_setschedparam(&attr, &param)
+ pthread_attr_setschedpolicy(&attr, SCHED_FIFO)
+ return attr
+ }()
+
+ init?(common com: Common) {
+ common = com
+
+ pthread_create(&thread, &threadAttr, entryC, MPVHelper.bridge(obj: self))
+ if thread == nil {
+ common.log.sendWarning("Couldn't create pthread for high precision timer")
+ return nil
+ }
+
+ threadPort = pthread_mach_thread_np(thread)
+ }
+
+ func updatePolicy(periodSeconds: Double = 1 / 60.0) {
+ let period = periodSeconds * nanoPerSecond / machToNano
+ var policy = thread_time_constraint_policy(
+ period: UInt32(period),
+ computation: UInt32(0.75 * period),
+ constraint: UInt32(0.85 * period),
+ preemptible: 1
+ )
+
+ let success = withUnsafeMutablePointer(to: &policy) {
+ $0.withMemoryRebound(to: integer_t.self, capacity: policyCount) {
+ thread_policy_set(threadPort, policyFlavor, $0, typeNumber)
+ }
+ }
+
+ isHighPrecision = success == KERN_SUCCESS
+ if !isHighPrecision {
+ common.log.sendWarning("Couldn't create a high precision timer")
+ }
+ }
+
+ func terminate() {
+ condition.lock()
+ isRunning = false
+ condition.signal()
+ condition.unlock()
+ pthread_kill(thread, SIGALRM)
+ pthread_join(thread, nil)
+ }
+
+ func scheduleAt(time: UInt64, closure: @escaping () -> ()) {
+ condition.lock()
+ let firstEventTime = events.first?.time ?? 0
+ let lastEventTime = events.last?.time ?? 0
+ events.append(Timing(time: time, closure: closure))
+
+ if lastEventTime > time {
+ events.sort{ $0.time < $1.time }
+ }
+
+ condition.signal()
+ condition.unlock()
+
+ if firstEventTime > time {
+ pthread_kill(thread, SIGALRM)
+ }
+ }
+
+ let threadSignal: @convention(c) (Int32) -> () = { (sig: Int32) in }
+
+ let entryC: @convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? = { (ptr: UnsafeMutableRawPointer) in
+ let ptimer: PreciseTimer = MPVHelper.bridge(ptr: ptr)
+ ptimer.entry()
+ return nil
+ }
+
+ func entry() {
+ signal(SIGALRM, threadSignal)
+
+ while isRunning {
+ condition.lock()
+ while events.count == 0 && isRunning {
+ condition.wait()
+ }
+
+ if !isRunning { break }
+
+ guard let event = events.first else {
+ continue
+ }
+ condition.unlock()
+
+ mach_wait_until(event.time)
+
+ condition.lock()
+ if events.first?.time == event.time && isRunning {
+ event.closure()
+ events.removeFirst()
+ }
+ condition.unlock()
+ }
+ }
+}
diff --git a/osdep/macos/remote_command_center.swift b/osdep/macos/remote_command_center.swift
new file mode 100644
index 0000000..6fb2229
--- /dev/null
+++ b/osdep/macos/remote_command_center.swift
@@ -0,0 +1,191 @@
+/*
+ * 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/>.
+ */
+
+import MediaPlayer
+
+class RemoteCommandCenter: NSObject {
+ enum KeyType {
+ case normal
+ case repeatable
+ }
+
+ var config: [MPRemoteCommand:[String:Any]] = [
+ MPRemoteCommandCenter.shared().pauseCommand: [
+ "mpKey": MP_KEY_PAUSEONLY,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().playCommand: [
+ "mpKey": MP_KEY_PLAYONLY,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().stopCommand: [
+ "mpKey": MP_KEY_STOP,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().nextTrackCommand: [
+ "mpKey": MP_KEY_NEXT,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().previousTrackCommand: [
+ "mpKey": MP_KEY_PREV,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().togglePlayPauseCommand: [
+ "mpKey": MP_KEY_PLAY,
+ "keyType": KeyType.normal
+ ],
+ MPRemoteCommandCenter.shared().seekForwardCommand: [
+ "mpKey": MP_KEY_FORWARD,
+ "keyType": KeyType.repeatable,
+ "state": MP_KEY_STATE_UP
+ ],
+ MPRemoteCommandCenter.shared().seekBackwardCommand: [
+ "mpKey": MP_KEY_REWIND,
+ "keyType": KeyType.repeatable,
+ "state": MP_KEY_STATE_UP
+ ],
+ ]
+
+ var nowPlayingInfo: [String: Any] = [
+ MPNowPlayingInfoPropertyMediaType: NSNumber(value: MPNowPlayingInfoMediaType.video.rawValue),
+ MPNowPlayingInfoPropertyDefaultPlaybackRate: NSNumber(value: 1),
+ MPNowPlayingInfoPropertyPlaybackProgress: NSNumber(value: 0.0),
+ MPMediaItemPropertyPlaybackDuration: NSNumber(value: 0),
+ MPMediaItemPropertyTitle: "mpv",
+ MPMediaItemPropertyAlbumTitle: "mpv",
+ MPMediaItemPropertyArtist: "mpv",
+ ]
+
+ let disabledCommands: [MPRemoteCommand] = [
+ MPRemoteCommandCenter.shared().changePlaybackRateCommand,
+ MPRemoteCommandCenter.shared().changeRepeatModeCommand,
+ MPRemoteCommandCenter.shared().changeShuffleModeCommand,
+ MPRemoteCommandCenter.shared().skipForwardCommand,
+ MPRemoteCommandCenter.shared().skipBackwardCommand,
+ MPRemoteCommandCenter.shared().changePlaybackPositionCommand,
+ MPRemoteCommandCenter.shared().enableLanguageOptionCommand,
+ MPRemoteCommandCenter.shared().disableLanguageOptionCommand,
+ MPRemoteCommandCenter.shared().ratingCommand,
+ MPRemoteCommandCenter.shared().likeCommand,
+ MPRemoteCommandCenter.shared().dislikeCommand,
+ MPRemoteCommandCenter.shared().bookmarkCommand,
+ ]
+
+ var mpInfoCenter: MPNowPlayingInfoCenter { get { return MPNowPlayingInfoCenter.default() } }
+ var isPaused: Bool = false { didSet { updatePlaybackState() } }
+
+ @objc override init() {
+ super.init()
+
+ for cmd in disabledCommands {
+ cmd.isEnabled = false
+ }
+ }
+
+ @objc func start() {
+ for (cmd, _) in config {
+ cmd.isEnabled = true
+ cmd.addTarget { [unowned self] event in
+ return self.cmdHandler(event)
+ }
+ }
+
+ if let app = NSApp as? Application, let icon = app.getMPVIcon() {
+ let albumArt = MPMediaItemArtwork(boundsSize: icon.size) { _ in
+ return icon
+ }
+ nowPlayingInfo[MPMediaItemPropertyArtwork] = albumArt
+ }
+
+ mpInfoCenter.nowPlayingInfo = nowPlayingInfo
+ mpInfoCenter.playbackState = .playing
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(self.makeCurrent),
+ name: NSApplication.willBecomeActiveNotification,
+ object: nil
+ )
+ }
+
+ @objc func stop() {
+ for (cmd, _) in config {
+ cmd.isEnabled = false
+ cmd.removeTarget(nil)
+ }
+
+ mpInfoCenter.nowPlayingInfo = nil
+ mpInfoCenter.playbackState = .unknown
+ }
+
+ @objc func makeCurrent(notification: NSNotification) {
+ mpInfoCenter.playbackState = .paused
+ mpInfoCenter.playbackState = .playing
+ updatePlaybackState()
+ }
+
+ func updatePlaybackState() {
+ mpInfoCenter.playbackState = isPaused ? .paused : .playing
+ }
+
+ func cmdHandler(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
+ guard let cmdConfig = config[event.command],
+ let mpKey = cmdConfig["mpKey"] as? Int32,
+ let keyType = cmdConfig["keyType"] as? KeyType else
+ {
+ return .commandFailed
+ }
+
+ var state = cmdConfig["state"] as? UInt32 ?? 0
+
+ if let currentState = cmdConfig["state"] as? UInt32, keyType == .repeatable {
+ state = MP_KEY_STATE_DOWN
+ config[event.command]?["state"] = MP_KEY_STATE_DOWN
+ if currentState == MP_KEY_STATE_DOWN {
+ state = MP_KEY_STATE_UP
+ config[event.command]?["state"] = MP_KEY_STATE_UP
+ }
+ }
+
+ EventsResponder.sharedInstance().handleMPKey(mpKey, withMask: Int32(state))
+
+ return .success
+ }
+
+ @objc func processEvent(_ event: UnsafeMutablePointer<mpv_event>) {
+ switch event.pointee.event_id {
+ case MPV_EVENT_PROPERTY_CHANGE:
+ handlePropertyChange(event)
+ default:
+ break
+ }
+ }
+
+ func handlePropertyChange(_ event: UnsafeMutablePointer<mpv_event>) {
+ let pData = OpaquePointer(event.pointee.data)
+ guard let property = UnsafePointer<mpv_event_property>(pData)?.pointee else {
+ return
+ }
+
+ switch String(cString: property.name) {
+ case "pause" where property.format == MPV_FORMAT_FLAG:
+ isPaused = LibmpvHelper.mpvFlagToBool(property.data) ?? false
+ default:
+ break
+ }
+ }
+}
diff --git a/osdep/macos/swift_compat.swift b/osdep/macos/swift_compat.swift
new file mode 100644
index 0000000..83059da
--- /dev/null
+++ b/osdep/macos/swift_compat.swift
@@ -0,0 +1,36 @@
+/*
+ * 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/>.
+ */
+
+
+#if !swift(>=5.0)
+extension Data {
+ mutating func withUnsafeMutableBytes<Type>(_ body: (UnsafeMutableRawBufferPointer) throws -> Type) rethrows -> Type {
+ let dataCount = count
+ return try withUnsafeMutableBytes { (ptr: UnsafeMutablePointer<UInt8>) throws -> Type in
+ try body(UnsafeMutableRawBufferPointer(start: ptr, count: dataCount))
+ }
+ }
+}
+#endif
+
+#if !swift(>=4.2)
+extension NSDraggingInfo {
+ var draggingPasteboard: NSPasteboard {
+ get { return draggingPasteboard() }
+ }
+}
+#endif
diff --git a/osdep/macos/swift_extensions.swift b/osdep/macos/swift_extensions.swift
new file mode 100644
index 0000000..127c568
--- /dev/null
+++ b/osdep/macos/swift_extensions.swift
@@ -0,0 +1,58 @@
+/*
+ * 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/>.
+ */
+
+import Cocoa
+
+extension NSDeviceDescriptionKey {
+ static let screenNumber = NSDeviceDescriptionKey("NSScreenNumber")
+}
+
+extension NSScreen {
+
+ public var displayID: CGDirectDisplayID {
+ get {
+ return deviceDescription[.screenNumber] as? CGDirectDisplayID ?? 0
+ }
+ }
+}
+
+extension NSColor {
+
+ convenience init(hex: String) {
+ let int = Int(hex.dropFirst(), radix: 16) ?? 0
+ let alpha = CGFloat((int >> 24) & 0x000000FF)/255
+ let red = CGFloat((int >> 16) & 0x000000FF)/255
+ let green = CGFloat((int >> 8) & 0x000000FF)/255
+ let blue = CGFloat((int) & 0x000000FF)/255
+
+ self.init(calibratedRed: red, green: green, blue: blue, alpha: alpha)
+ }
+}
+
+extension Bool {
+
+ init(_ int32: Int32) {
+ self.init(int32 != 0)
+ }
+}
+
+extension Int32 {
+
+ init(_ bool: Bool) {
+ self.init(bool ? 1 : 0)
+ }
+}
diff --git a/osdep/macosx_application.h b/osdep/macosx_application.h
new file mode 100644
index 0000000..753b9f0
--- /dev/null
+++ b/osdep/macosx_application.h
@@ -0,0 +1,55 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MPV_MACOSX_APPLICATION
+#define MPV_MACOSX_APPLICATION
+
+#include "osdep/macosx_menubar.h"
+#include "options/m_option.h"
+
+enum {
+ FRAME_VISIBLE = 0,
+ FRAME_WHOLE,
+};
+
+enum {
+ RENDER_TIMER_CALLBACK = 0,
+ RENDER_TIMER_PRECISE,
+ RENDER_TIMER_SYSTEM,
+};
+
+struct macos_opts {
+ int macos_title_bar_style;
+ int macos_title_bar_appearance;
+ int macos_title_bar_material;
+ struct m_color macos_title_bar_color;
+ int macos_fs_animation_duration;
+ bool macos_force_dedicated_gpu;
+ int macos_app_activation_policy;
+ int macos_geometry_calculation;
+ int macos_render_timer;
+ int cocoa_cb_sw_renderer;
+ bool cocoa_cb_10bit_context;
+};
+
+// multithreaded wrapper for mpv_main
+int cocoa_main(int argc, char *argv[]);
+void cocoa_register_menu_item_action(MPMenuKey key, void* action);
+
+extern const struct m_sub_options macos_conf;
+
+#endif /* MPV_MACOSX_APPLICATION */
diff --git a/osdep/macosx_application.m b/osdep/macosx_application.m
new file mode 100644
index 0000000..73503ad
--- /dev/null
+++ b/osdep/macosx_application.m
@@ -0,0 +1,375 @@
+/*
+ * 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 <stdio.h>
+#include "config.h"
+#include "mpv_talloc.h"
+
+#include "common/msg.h"
+#include "input/input.h"
+#include "player/client.h"
+#include "options/m_config.h"
+#include "options/options.h"
+
+#import "osdep/macosx_application_objc.h"
+#import "osdep/macosx_events_objc.h"
+#include "osdep/threads.h"
+#include "osdep/main-fn.h"
+
+#if HAVE_MACOS_TOUCHBAR
+#import "osdep/macosx_touchbar.h"
+#endif
+#if HAVE_MACOS_COCOA_CB
+#include "osdep/macOS_swift.h"
+#endif
+
+#define MPV_PROTOCOL @"mpv://"
+
+#define OPT_BASE_STRUCT struct macos_opts
+const struct m_sub_options macos_conf = {
+ .opts = (const struct m_option[]) {
+ {"macos-title-bar-appearance", OPT_CHOICE(macos_title_bar_appearance,
+ {"auto", 0}, {"aqua", 1}, {"darkAqua", 2},
+ {"vibrantLight", 3}, {"vibrantDark", 4},
+ {"aquaHighContrast", 5}, {"darkAquaHighContrast", 6},
+ {"vibrantLightHighContrast", 7},
+ {"vibrantDarkHighContrast", 8})},
+ {"macos-title-bar-material", OPT_CHOICE(macos_title_bar_material,
+ {"titlebar", 0}, {"selection", 1}, {"menu", 2},
+ {"popover", 3}, {"sidebar", 4}, {"headerView", 5},
+ {"sheet", 6}, {"windowBackground", 7}, {"hudWindow", 8},
+ {"fullScreen", 9}, {"toolTip", 10}, {"contentBackground", 11},
+ {"underWindowBackground", 12}, {"underPageBackground", 13},
+ {"dark", 14}, {"light", 15}, {"mediumLight", 16},
+ {"ultraDark", 17})},
+ {"macos-title-bar-color", OPT_COLOR(macos_title_bar_color)},
+ {"macos-fs-animation-duration",
+ OPT_CHOICE(macos_fs_animation_duration, {"default", -1}),
+ M_RANGE(0, 1000)},
+ {"macos-force-dedicated-gpu", OPT_BOOL(macos_force_dedicated_gpu)},
+ {"macos-app-activation-policy", OPT_CHOICE(macos_app_activation_policy,
+ {"regular", 0}, {"accessory", 1}, {"prohibited", 2})},
+ {"macos-geometry-calculation", OPT_CHOICE(macos_geometry_calculation,
+ {"visible", FRAME_VISIBLE}, {"whole", FRAME_WHOLE})},
+ {"macos-render-timer", OPT_CHOICE(macos_render_timer,
+ {"callback", RENDER_TIMER_CALLBACK}, {"precise", RENDER_TIMER_PRECISE},
+ {"system", RENDER_TIMER_SYSTEM})},
+ {"cocoa-cb-sw-renderer", OPT_CHOICE(cocoa_cb_sw_renderer,
+ {"auto", -1}, {"no", 0}, {"yes", 1})},
+ {"cocoa-cb-10bit-context", OPT_BOOL(cocoa_cb_10bit_context)},
+ {0}
+ },
+ .size = sizeof(struct macos_opts),
+ .defaults = &(const struct macos_opts){
+ .macos_title_bar_color = {0, 0, 0, 0},
+ .macos_fs_animation_duration = -1,
+ .cocoa_cb_sw_renderer = -1,
+ .cocoa_cb_10bit_context = true
+ },
+};
+
+// Whether the NSApplication singleton was created. If this is false, we are
+// running in libmpv mode, and cocoa_main() was never called.
+static bool application_instantiated;
+
+static mp_thread playback_thread_id;
+
+@interface Application ()
+{
+ EventsResponder *_eventsResponder;
+}
+
+@end
+
+static Application *mpv_shared_app(void)
+{
+ return (Application *)[Application sharedApplication];
+}
+
+static void terminate_cocoa_application(void)
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [NSApp hide:NSApp];
+ [NSApp terminate:NSApp];
+ });
+}
+
+@implementation Application
+@synthesize menuBar = _menu_bar;
+@synthesize openCount = _open_count;
+@synthesize cocoaCB = _cocoa_cb;
+
+- (void)sendEvent:(NSEvent *)event
+{
+ if ([self modalWindow] || ![_eventsResponder processKeyEvent:event])
+ [super sendEvent:event];
+ [_eventsResponder wakeup];
+}
+
+- (id)init
+{
+ if (self = [super init]) {
+ _eventsResponder = [EventsResponder sharedInstance];
+
+ NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager];
+ [em setEventHandler:self
+ andSelector:@selector(getUrl:withReplyEvent:)
+ forEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
+ }
+
+ return self;
+}
+
+- (void)dealloc
+{
+ NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager];
+ [em removeEventHandlerForEventClass:kInternetEventClass
+ andEventID:kAEGetURL];
+ [em removeEventHandlerForEventClass:kCoreEventClass
+ andEventID:kAEQuitApplication];
+ [super dealloc];
+}
+
+static const char macosx_icon[] =
+#include "TOOLS/osxbundle/icon.icns.inc"
+;
+
+- (NSImage *)getMPVIcon
+{
+ // The C string contains a trailing null, so we strip it away
+ NSData *icon_data = [NSData dataWithBytesNoCopy:(void *)macosx_icon
+ length:sizeof(macosx_icon) - 1
+ freeWhenDone:NO];
+ return [[NSImage alloc] initWithData:icon_data];
+}
+
+#if HAVE_MACOS_TOUCHBAR
+- (NSTouchBar *)makeTouchBar
+{
+ TouchBar *tBar = [[TouchBar alloc] init];
+ [tBar setApp:self];
+ tBar.delegate = tBar;
+ tBar.customizationIdentifier = customID;
+ tBar.defaultItemIdentifiers = @[play, previousItem, nextItem, seekBar];
+ tBar.customizationAllowedItemIdentifiers = @[play, seekBar, previousItem,
+ nextItem, previousChapter, nextChapter, cycleAudio, cycleSubtitle,
+ currentPosition, timeLeft];
+ return tBar;
+}
+#endif
+
+- (void)processEvent:(struct mpv_event *)event
+{
+#if HAVE_MACOS_TOUCHBAR
+ [(TouchBar *)self.touchBar processEvent:event];
+#endif
+ if (_cocoa_cb) {
+ [_cocoa_cb processEvent:event];
+ }
+}
+
+- (void)setMpvHandle:(struct mpv_handle *)ctx
+{
+#if HAVE_MACOS_COCOA_CB
+ [NSApp setCocoaCB:[[CocoaCB alloc] init:ctx]];
+#endif
+}
+
+- (const struct m_sub_options *)getMacOSConf
+{
+ return &macos_conf;
+}
+
+- (const struct m_sub_options *)getVoSubConf
+{
+ return &vo_sub_opts;
+}
+
+- (void)queueCommand:(char *)cmd
+{
+ [_eventsResponder queueCommand:cmd];
+}
+
+- (void)stopMPV:(char *)cmd
+{
+ if (![_eventsResponder queueCommand:cmd])
+ terminate_cocoa_application();
+}
+
+- (void)applicationWillFinishLaunching:(NSNotification *)notification
+{
+ NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager];
+ [em setEventHandler:self
+ andSelector:@selector(handleQuitEvent:withReplyEvent:)
+ forEventClass:kCoreEventClass
+ andEventID:kAEQuitApplication];
+}
+
+- (void)handleQuitEvent:(NSAppleEventDescriptor *)event
+ withReplyEvent:(NSAppleEventDescriptor *)replyEvent
+{
+ [self stopMPV:"quit"];
+}
+
+- (void)getUrl:(NSAppleEventDescriptor *)event
+ withReplyEvent:(NSAppleEventDescriptor *)replyEvent
+{
+ NSString *url =
+ [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
+
+ url = [url stringByReplacingOccurrencesOfString:MPV_PROTOCOL
+ withString:@""
+ options:NSAnchoredSearch
+ range:NSMakeRange(0, [MPV_PROTOCOL length])];
+
+ url = [url stringByRemovingPercentEncoding];
+ [_eventsResponder handleFilesArray:@[url]];
+}
+
+- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames
+{
+ if (mpv_shared_app().openCount > 0) {
+ mpv_shared_app().openCount--;
+ return;
+ }
+ [self openFiles:filenames];
+}
+
+- (void)openFiles:(NSArray *)filenames
+{
+ SEL cmpsel = @selector(localizedStandardCompare:);
+ NSArray *files = [filenames sortedArrayUsingSelector:cmpsel];
+ [_eventsResponder handleFilesArray:files];
+}
+@end
+
+struct playback_thread_ctx {
+ int *argc;
+ char ***argv;
+};
+
+static void cocoa_run_runloop(void)
+{
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ [NSApp run];
+ [pool drain];
+}
+
+static MP_THREAD_VOID playback_thread(void *ctx_obj)
+{
+ mp_thread_set_name("core/playback");
+ @autoreleasepool {
+ struct playback_thread_ctx *ctx = (struct playback_thread_ctx*) ctx_obj;
+ int r = mpv_main(*ctx->argc, *ctx->argv);
+ terminate_cocoa_application();
+ // normally never reached - unless the cocoa mainloop hasn't started yet
+ exit(r);
+ }
+}
+
+void cocoa_register_menu_item_action(MPMenuKey key, void* action)
+{
+ if (application_instantiated)
+ [[NSApp menuBar] registerSelector:(SEL)action forKey:key];
+}
+
+static void init_cocoa_application(bool regular)
+{
+ NSApp = mpv_shared_app();
+ [NSApp setDelegate:NSApp];
+ [NSApp setMenuBar:[[MenuBar alloc] init]];
+
+ // Will be set to Regular from cocoa_common during UI creation so that we
+ // don't create an icon when playing audio only files.
+ [NSApp setActivationPolicy: regular ?
+ NSApplicationActivationPolicyRegular :
+ NSApplicationActivationPolicyAccessory];
+
+ atexit_b(^{
+ // Because activation policy has just been set to behave like a real
+ // application, that policy must be reset on exit to prevent, among
+ // other things, the menubar created here from remaining on screen.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited];
+ });
+ });
+}
+
+static bool bundle_started_from_finder()
+{
+ NSString* bundle = [[[NSProcessInfo processInfo] environment] objectForKey:@"MPVBUNDLE"];
+ return [bundle isEqual:@"true"];
+}
+
+static bool is_psn_argument(char *arg_to_check)
+{
+ NSString *arg = [NSString stringWithUTF8String:arg_to_check];
+ return [arg hasPrefix:@"-psn_"];
+}
+
+static void setup_bundle(int *argc, char *argv[])
+{
+ if (*argc > 1 && is_psn_argument(argv[1])) {
+ *argc = 1;
+ argv[1] = NULL;
+ }
+
+ NSDictionary *env = [[NSProcessInfo processInfo] environment];
+ NSString *path_bundle = [env objectForKey:@"PATH"];
+ NSString *path_new = [NSString stringWithFormat:@"%@:%@:%@:%@:%@",
+ path_bundle,
+ @"/usr/local/bin",
+ @"/usr/local/sbin",
+ @"/opt/local/bin",
+ @"/opt/local/sbin"];
+ setenv("PATH", [path_new UTF8String], 1);
+}
+
+int cocoa_main(int argc, char *argv[])
+{
+ @autoreleasepool {
+ application_instantiated = true;
+ [[EventsResponder sharedInstance] setIsApplication:YES];
+
+ struct playback_thread_ctx ctx = {0};
+ ctx.argc = &argc;
+ ctx.argv = &argv;
+
+ if (bundle_started_from_finder()) {
+ setup_bundle(&argc, argv);
+ init_cocoa_application(true);
+ } else {
+ for (int i = 1; i < argc; i++)
+ if (argv[i][0] != '-')
+ mpv_shared_app().openCount++;
+ init_cocoa_application(false);
+ }
+
+ mp_thread_create(&playback_thread_id, playback_thread, &ctx);
+ [[EventsResponder sharedInstance] waitForInputContext];
+ cocoa_run_runloop();
+
+ // This should never be reached: cocoa_run_runloop blocks until the
+ // process is quit
+ fprintf(stderr, "There was either a problem "
+ "initializing Cocoa or the Runloop was stopped unexpectedly. "
+ "Please report this issues to a developer.\n");
+ mp_thread_join(playback_thread_id);
+ return 1;
+ }
+}
diff --git a/osdep/macosx_application_objc.h b/osdep/macosx_application_objc.h
new file mode 100644
index 0000000..11959a8
--- /dev/null
+++ b/osdep/macosx_application_objc.h
@@ -0,0 +1,40 @@
+/*
+ * 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/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#include "osdep/macosx_application.h"
+#import "osdep/macosx_menubar_objc.h"
+
+@class CocoaCB;
+struct mpv_event;
+struct mpv_handle;
+
+@interface Application : NSApplication
+
+- (NSImage *)getMPVIcon;
+- (void)processEvent:(struct mpv_event *)event;
+- (void)queueCommand:(char *)cmd;
+- (void)stopMPV:(char *)cmd;
+- (void)openFiles:(NSArray *)filenames;
+- (void)setMpvHandle:(struct mpv_handle *)ctx;
+- (const struct m_sub_options *)getMacOSConf;
+- (const struct m_sub_options *)getVoSubConf;
+
+@property(nonatomic, retain) MenuBar *menuBar;
+@property(nonatomic, assign) size_t openCount;
+@property(nonatomic, retain) CocoaCB *cocoaCB;
+@end
diff --git a/osdep/macosx_events.h b/osdep/macosx_events.h
new file mode 100644
index 0000000..9188c8b
--- /dev/null
+++ b/osdep/macosx_events.h
@@ -0,0 +1,36 @@
+/*
+ * Cocoa Application Event Handling
+ *
+ * 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/>.
+ */
+
+#ifndef MACOSX_EVENTS_H
+#define MACOSX_EVENTS_H
+#include "input/keycodes.h"
+
+struct input_ctx;
+struct mpv_handle;
+
+void cocoa_put_key(int keycode);
+void cocoa_put_key_with_modifiers(int keycode, int modifiers);
+
+void cocoa_init_media_keys(void);
+void cocoa_uninit_media_keys(void);
+
+void cocoa_set_input_context(struct input_ctx *input_context);
+void cocoa_set_mpv_handle(struct mpv_handle *ctx);
+
+#endif
diff --git a/osdep/macosx_events.m b/osdep/macosx_events.m
new file mode 100644
index 0000000..627077a
--- /dev/null
+++ b/osdep/macosx_events.m
@@ -0,0 +1,408 @@
+/*
+ * Cocoa Application Event Handling
+ *
+ * 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/>.
+ */
+
+// Carbon header is included but Carbon is NOT linked to mpv's binary. This
+// file only needs this include to use the keycode definitions in keymap.
+#import <Carbon/Carbon.h>
+
+// Media keys definitions
+#import <IOKit/hidsystem/ev_keymap.h>
+#import <Cocoa/Cocoa.h>
+
+#include "mpv_talloc.h"
+#include "input/event.h"
+#include "input/input.h"
+#include "player/client.h"
+#include "input/keycodes.h"
+// doesn't make much sense, but needed to access keymap functionality
+#include "video/out/vo.h"
+
+#import "osdep/macosx_events_objc.h"
+#import "osdep/macosx_application_objc.h"
+
+#include "config.h"
+
+#if HAVE_MACOS_COCOA_CB
+#include "osdep/macOS_swift.h"
+#endif
+
+@interface EventsResponder ()
+{
+ struct input_ctx *_inputContext;
+ struct mpv_handle *_ctx;
+ BOOL _is_application;
+ NSCondition *_input_lock;
+}
+
+- (NSEvent *)handleKey:(NSEvent *)event;
+- (BOOL)setMpvHandle:(struct mpv_handle *)ctx;
+- (void)readEvents;
+- (void)startMediaKeys;
+- (void)stopMediaKeys;
+- (int)mapKeyModifiers:(int)cocoaModifiers;
+- (int)keyModifierMask:(NSEvent *)event;
+@end
+
+
+#define NSLeftAlternateKeyMask (0x000020 | NSEventModifierFlagOption)
+#define NSRightAlternateKeyMask (0x000040 | NSEventModifierFlagOption)
+
+static bool LeftAltPressed(int mask)
+{
+ return (mask & NSLeftAlternateKeyMask) == NSLeftAlternateKeyMask;
+}
+
+static bool RightAltPressed(int mask)
+{
+ return (mask & NSRightAlternateKeyMask) == NSRightAlternateKeyMask;
+}
+
+static const struct mp_keymap keymap[] = {
+ // special keys
+ {kVK_Return, MP_KEY_ENTER}, {kVK_Escape, MP_KEY_ESC},
+ {kVK_Delete, MP_KEY_BACKSPACE}, {kVK_Option, MP_KEY_BACKSPACE},
+ {kVK_Control, MP_KEY_BACKSPACE}, {kVK_Shift, MP_KEY_BACKSPACE},
+ {kVK_Tab, MP_KEY_TAB},
+
+ // cursor keys
+ {kVK_UpArrow, MP_KEY_UP}, {kVK_DownArrow, MP_KEY_DOWN},
+ {kVK_LeftArrow, MP_KEY_LEFT}, {kVK_RightArrow, MP_KEY_RIGHT},
+
+ // navigation block
+ {kVK_Help, MP_KEY_INSERT}, {kVK_ForwardDelete, MP_KEY_DELETE},
+ {kVK_Home, MP_KEY_HOME}, {kVK_End, MP_KEY_END},
+ {kVK_PageUp, MP_KEY_PAGE_UP}, {kVK_PageDown, MP_KEY_PAGE_DOWN},
+
+ // F-keys
+ {kVK_F1, MP_KEY_F + 1}, {kVK_F2, MP_KEY_F + 2}, {kVK_F3, MP_KEY_F + 3},
+ {kVK_F4, MP_KEY_F + 4}, {kVK_F5, MP_KEY_F + 5}, {kVK_F6, MP_KEY_F + 6},
+ {kVK_F7, MP_KEY_F + 7}, {kVK_F8, MP_KEY_F + 8}, {kVK_F9, MP_KEY_F + 9},
+ {kVK_F10, MP_KEY_F + 10}, {kVK_F11, MP_KEY_F + 11}, {kVK_F12, MP_KEY_F + 12},
+ {kVK_F13, MP_KEY_F + 13}, {kVK_F14, MP_KEY_F + 14}, {kVK_F15, MP_KEY_F + 15},
+ {kVK_F16, MP_KEY_F + 16}, {kVK_F17, MP_KEY_F + 17}, {kVK_F18, MP_KEY_F + 18},
+ {kVK_F19, MP_KEY_F + 19}, {kVK_F20, MP_KEY_F + 20},
+
+ // numpad
+ {kVK_ANSI_KeypadPlus, '+'}, {kVK_ANSI_KeypadMinus, '-'},
+ {kVK_ANSI_KeypadMultiply, '*'}, {kVK_ANSI_KeypadDivide, '/'},
+ {kVK_ANSI_KeypadEnter, MP_KEY_KPENTER},
+ {kVK_ANSI_KeypadDecimal, MP_KEY_KPDEC},
+ {kVK_ANSI_Keypad0, MP_KEY_KP0}, {kVK_ANSI_Keypad1, MP_KEY_KP1},
+ {kVK_ANSI_Keypad2, MP_KEY_KP2}, {kVK_ANSI_Keypad3, MP_KEY_KP3},
+ {kVK_ANSI_Keypad4, MP_KEY_KP4}, {kVK_ANSI_Keypad5, MP_KEY_KP5},
+ {kVK_ANSI_Keypad6, MP_KEY_KP6}, {kVK_ANSI_Keypad7, MP_KEY_KP7},
+ {kVK_ANSI_Keypad8, MP_KEY_KP8}, {kVK_ANSI_Keypad9, MP_KEY_KP9},
+
+ {0, 0}
+};
+
+static int convert_key(unsigned key, unsigned charcode)
+{
+ int mpkey = lookup_keymap_table(keymap, key);
+ if (mpkey)
+ return mpkey;
+ return charcode;
+}
+
+void cocoa_init_media_keys(void)
+{
+ [[EventsResponder sharedInstance] startMediaKeys];
+}
+
+void cocoa_uninit_media_keys(void)
+{
+ [[EventsResponder sharedInstance] stopMediaKeys];
+}
+
+void cocoa_put_key(int keycode)
+{
+ [[EventsResponder sharedInstance] putKey:keycode];
+}
+
+void cocoa_put_key_with_modifiers(int keycode, int modifiers)
+{
+ keycode |= [[EventsResponder sharedInstance] mapKeyModifiers:modifiers];
+ cocoa_put_key(keycode);
+}
+
+void cocoa_set_input_context(struct input_ctx *input_context)
+{
+ [[EventsResponder sharedInstance] setInputContext:input_context];
+}
+
+static void wakeup(void *context)
+{
+ [[EventsResponder sharedInstance] readEvents];
+}
+
+void cocoa_set_mpv_handle(struct mpv_handle *ctx)
+{
+ if ([[EventsResponder sharedInstance] setMpvHandle:ctx]) {
+ mpv_observe_property(ctx, 0, "duration", MPV_FORMAT_DOUBLE);
+ mpv_observe_property(ctx, 0, "time-pos", MPV_FORMAT_DOUBLE);
+ mpv_observe_property(ctx, 0, "pause", MPV_FORMAT_FLAG);
+ mpv_set_wakeup_callback(ctx, wakeup, NULL);
+ }
+}
+
+@implementation EventsResponder
+
+@synthesize remoteCommandCenter = _remoteCommandCenter;
+
++ (EventsResponder *)sharedInstance
+{
+ static EventsResponder *responder = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ responder = [EventsResponder new];
+ });
+ return responder;
+}
+
+- (id)init
+{
+ self = [super init];
+ if (self) {
+ _input_lock = [NSCondition new];
+ }
+ return self;
+}
+
+- (void)waitForInputContext
+{
+ [_input_lock lock];
+ while (!_inputContext)
+ [_input_lock wait];
+ [_input_lock unlock];
+}
+
+- (void)setInputContext:(struct input_ctx *)ctx
+{
+ [_input_lock lock];
+ _inputContext = ctx;
+ [_input_lock signal];
+ [_input_lock unlock];
+}
+
+- (void)wakeup
+{
+ [_input_lock lock];
+ if (_inputContext)
+ mp_input_wakeup(_inputContext);
+ [_input_lock unlock];
+}
+
+- (bool)queueCommand:(char *)cmd
+{
+ bool r = false;
+ [_input_lock lock];
+ if (_inputContext) {
+ mp_cmd_t *cmdt = mp_input_parse_cmd(_inputContext, bstr0(cmd), "");
+ mp_input_queue_cmd(_inputContext, cmdt);
+ r = true;
+ }
+ [_input_lock unlock];
+ return r;
+}
+
+- (void)putKey:(int)keycode
+{
+ [_input_lock lock];
+ if (_inputContext)
+ mp_input_put_key(_inputContext, keycode);
+ [_input_lock unlock];
+}
+
+- (BOOL)useAltGr
+{
+ BOOL r = YES;
+ [_input_lock lock];
+ if (_inputContext)
+ r = mp_input_use_alt_gr(_inputContext);
+ [_input_lock unlock];
+ return r;
+}
+
+- (void)setIsApplication:(BOOL)isApplication
+{
+ _is_application = isApplication;
+}
+
+- (BOOL)setMpvHandle:(struct mpv_handle *)ctx
+{
+ if (_is_application) {
+ dispatch_sync(dispatch_get_main_queue(), ^{
+ _ctx = ctx;
+ [NSApp setMpvHandle:ctx];
+ });
+ return YES;
+ } else {
+ mpv_destroy(ctx);
+ return NO;
+ }
+}
+
+- (void)readEvents
+{
+ dispatch_async(dispatch_get_main_queue(), ^{
+ while (_ctx) {
+ mpv_event *event = mpv_wait_event(_ctx, 0);
+ if (event->event_id == MPV_EVENT_NONE)
+ break;
+ [self processEvent:event];
+ }
+ });
+}
+
+-(void)processEvent:(struct mpv_event *)event
+{
+ if(_is_application) {
+ [NSApp processEvent:event];
+ }
+
+ if (_remoteCommandCenter) {
+ [_remoteCommandCenter processEvent:event];
+ }
+
+ switch (event->event_id) {
+ case MPV_EVENT_SHUTDOWN: {
+#if HAVE_MACOS_COCOA_CB
+ if ([(Application *)NSApp cocoaCB].isShuttingDown) {
+ _ctx = nil;
+ return;
+ }
+#endif
+ mpv_destroy(_ctx);
+ _ctx = nil;
+ break;
+ }
+ }
+}
+
+- (void)startMediaKeys
+{
+#if HAVE_MACOS_MEDIA_PLAYER
+ if (_remoteCommandCenter == nil) {
+ _remoteCommandCenter = [[RemoteCommandCenter alloc] init];
+ }
+#endif
+
+ [_remoteCommandCenter start];
+}
+
+- (void)stopMediaKeys
+{
+ [_remoteCommandCenter stop];
+}
+
+- (int)mapKeyModifiers:(int)cocoaModifiers
+{
+ int mask = 0;
+ if (cocoaModifiers & NSEventModifierFlagShift)
+ mask |= MP_KEY_MODIFIER_SHIFT;
+ if (cocoaModifiers & NSEventModifierFlagControl)
+ mask |= MP_KEY_MODIFIER_CTRL;
+ if (LeftAltPressed(cocoaModifiers) ||
+ (RightAltPressed(cocoaModifiers) && ![self useAltGr]))
+ mask |= MP_KEY_MODIFIER_ALT;
+ if (cocoaModifiers & NSEventModifierFlagCommand)
+ mask |= MP_KEY_MODIFIER_META;
+ return mask;
+}
+
+- (int)mapTypeModifiers:(NSEventType)type
+{
+ NSDictionary *map = @{
+ @(NSEventTypeKeyDown) : @(MP_KEY_STATE_DOWN),
+ @(NSEventTypeKeyUp) : @(MP_KEY_STATE_UP),
+ };
+ return [map[@(type)] intValue];
+}
+
+- (int)keyModifierMask:(NSEvent *)event
+{
+ return [self mapKeyModifiers:[event modifierFlags]] |
+ [self mapTypeModifiers:[event type]];
+}
+
+-(BOOL)handleMPKey:(int)key withMask:(int)mask
+{
+ if (key > 0) {
+ cocoa_put_key(key | mask);
+ if (mask & MP_KEY_STATE_UP)
+ cocoa_put_key(MP_INPUT_RELEASE_ALL);
+ return YES;
+ } else {
+ return NO;
+ }
+}
+
+- (NSEvent*)handleKey:(NSEvent *)event
+{
+ if ([event isARepeat]) return nil;
+
+ NSString *chars;
+
+ if ([self useAltGr] && RightAltPressed([event modifierFlags])) {
+ chars = [event characters];
+ } else {
+ chars = [event charactersIgnoringModifiers];
+ }
+
+ struct bstr t = bstr0([chars UTF8String]);
+ int key = convert_key([event keyCode], bstr_decode_utf8(t, &t));
+
+ if (key > -1)
+ [self handleMPKey:key withMask:[self keyModifierMask:event]];
+
+ return nil;
+}
+
+- (bool)processKeyEvent:(NSEvent *)event
+{
+ if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp){
+ if (![[NSApp mainMenu] performKeyEquivalent:event])
+ [self handleKey:event];
+ return true;
+ }
+ return false;
+}
+
+- (void)handleFilesArray:(NSArray *)files
+{
+ enum mp_dnd_action action = [NSEvent modifierFlags] &
+ NSEventModifierFlagShift ? DND_APPEND : DND_REPLACE;
+
+ size_t num_files = [files count];
+ char **files_utf8 = talloc_array(NULL, char*, num_files);
+ [files enumerateObjectsUsingBlock:^(NSString *p, NSUInteger i, BOOL *_){
+ if ([p hasPrefix:@"file:///.file/id="])
+ p = [[NSURL URLWithString:p] path];
+ char *filename = (char *)[p UTF8String];
+ size_t bytes = [p lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
+ files_utf8[i] = talloc_memdup(files_utf8, filename, bytes + 1);
+ }];
+ [_input_lock lock];
+ if (_inputContext)
+ mp_event_drop_files(_inputContext, num_files, files_utf8, action);
+ [_input_lock unlock];
+ talloc_free(files_utf8);
+}
+
+@end
diff --git a/osdep/macosx_events_objc.h b/osdep/macosx_events_objc.h
new file mode 100644
index 0000000..9394fe7
--- /dev/null
+++ b/osdep/macosx_events_objc.h
@@ -0,0 +1,45 @@
+/*
+ * Cocoa Application Event Handling
+ *
+ * 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/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#include "osdep/macosx_events.h"
+
+@class RemoteCommandCenter;
+struct input_ctx;
+
+@interface EventsResponder : NSObject
+
++ (EventsResponder *)sharedInstance;
+- (void)setInputContext:(struct input_ctx *)ctx;
+- (void)setIsApplication:(BOOL)isApplication;
+
+/// Blocks until inputContext is present.
+- (void)waitForInputContext;
+- (void)wakeup;
+- (void)putKey:(int)keycode;
+- (void)handleFilesArray:(NSArray *)files;
+
+- (bool)queueCommand:(char *)cmd;
+- (bool)processKeyEvent:(NSEvent *)event;
+
+- (BOOL)handleMPKey:(int)key withMask:(int)mask;
+
+@property(nonatomic, retain) RemoteCommandCenter *remoteCommandCenter;
+
+@end
diff --git a/osdep/macosx_menubar.h b/osdep/macosx_menubar.h
new file mode 100644
index 0000000..509083d
--- /dev/null
+++ b/osdep/macosx_menubar.h
@@ -0,0 +1,30 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MPV_MACOSX_MENU
+#define MPV_MACOSX_MENU
+
+// Menu Keys identifying menu items
+typedef enum {
+ MPM_H_SIZE,
+ MPM_N_SIZE,
+ MPM_D_SIZE,
+ MPM_MINIMIZE,
+ MPM_ZOOM,
+} MPMenuKey;
+
+#endif /* MPV_MACOSX_MENU */
diff --git a/osdep/macosx_menubar.m b/osdep/macosx_menubar.m
new file mode 100644
index 0000000..5c6cd47
--- /dev/null
+++ b/osdep/macosx_menubar.m
@@ -0,0 +1,853 @@
+/*
+ * 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 "config.h"
+#include "common/common.h"
+
+#import "macosx_menubar_objc.h"
+#import "osdep/macosx_application_objc.h"
+
+@implementation MenuBar
+{
+ NSArray *menuTree;
+}
+
+- (id)init
+{
+ if (self = [super init]) {
+ NSUserDefaults *userDefaults =[NSUserDefaults standardUserDefaults];
+ [userDefaults setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
+ [userDefaults setBool:YES forKey:@"NSDisabledDictationMenuItem"];
+ [userDefaults setBool:YES forKey:@"NSDisabledCharacterPaletteMenuItem"];
+ [NSWindow setAllowsAutomaticWindowTabbing: NO];
+
+ menuTree = @[
+ @{
+ @"name": @"Apple",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"About mpv",
+ @"action" : @"about",
+ @"key" : @"",
+ @"target" : self
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Preferences…",
+ @"action" : @"preferences:",
+ @"key" : @",",
+ @"target" : self,
+ @"file" : @"mpv.conf",
+ @"alertTitle1": @"No Application found to open your config file.",
+ @"alertText1" : @"Please open the mpv.conf file with "
+ "your preferred text editor in the now "
+ "open folder to edit your config.",
+ @"alertTitle2": @"No config file found.",
+ @"alertText2" : @"Please create a mpv.conf file with your "
+ "preferred text editor in the now open folder.",
+ @"alertTitle3": @"No config path or file found.",
+ @"alertText3" : @"Please create the following path ~/.config/mpv/ "
+ "and a mpv.conf file within with your preferred "
+ "text editor."
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Keyboard Shortcuts Config…",
+ @"action" : @"preferences:",
+ @"key" : @"",
+ @"target" : self,
+ @"file" : @"input.conf",
+ @"alertTitle1": @"No Application found to open your config file.",
+ @"alertText1" : @"Please open the input.conf file with "
+ "your preferred text editor in the now "
+ "open folder to edit your config.",
+ @"alertTitle2": @"No config file found.",
+ @"alertText2" : @"Please create a input.conf file with your "
+ "preferred text editor in the now open folder.",
+ @"alertTitle3": @"No config path or file found.",
+ @"alertText3" : @"Please create the following path ~/.config/mpv/ "
+ "and a input.conf file within with your preferred "
+ "text editor."
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Services",
+ @"key" : @"",
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Hide mpv",
+ @"action" : @"hide:",
+ @"key" : @"h",
+ @"target" : NSApp
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Hide Others",
+ @"action" : @"hideOtherApplications:",
+ @"key" : @"h",
+ @"modifiers" : [NSNumber numberWithUnsignedInteger:
+ NSEventModifierFlagCommand |
+ NSEventModifierFlagOption],
+ @"target" : NSApp
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show All",
+ @"action" : @"unhideAllApplications:",
+ @"key" : @"",
+ @"target" : NSApp
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Quit and Remember Position",
+ @"action" : @"quit:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"quit-watch-later"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Quit mpv",
+ @"action" : @"quit:",
+ @"key" : @"q",
+ @"target" : self,
+ @"cmd" : @"quit"
+ }]
+ ]
+ },
+ @{
+ @"name": @"File",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Open File…",
+ @"action" : @"openFile",
+ @"key" : @"o",
+ @"target" : self
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Open URL…",
+ @"action" : @"openURL",
+ @"key" : @"O",
+ @"target" : self
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Open Playlist…",
+ @"action" : @"openPlaylist",
+ @"key" : @"",
+ @"target" : self
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Close",
+ @"action" : @"performClose:",
+ @"key" : @"w"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Save Screenshot",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"async screenshot"
+ }]
+ ]
+ },
+ @{
+ @"name": @"Edit",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Undo",
+ @"action" : @"undo:",
+ @"key" : @"z"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Redo",
+ @"action" : @"redo:",
+ @"key" : @"Z"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Cut",
+ @"action" : @"cut:",
+ @"key" : @"x"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Copy",
+ @"action" : @"copy:",
+ @"key" : @"c"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Paste",
+ @"action" : @"paste:",
+ @"key" : @"v"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Select All",
+ @"action" : @"selectAll:",
+ @"key" : @"a"
+ }]
+ ]
+ },
+ @{
+ @"name": @"View",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Fullscreen",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle fullscreen"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Float on Top",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle ontop"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Visibility on All Workspaces",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle on-all-workspaces"
+ }],
+#if HAVE_MACOS_TOUCHBAR
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Customize Touch Bar…",
+ @"action" : @"toggleTouchBarCustomizationPalette:",
+ @"key" : @"",
+ @"target" : NSApp
+ }]
+#endif
+ ]
+ },
+ @{
+ @"name": @"Video",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Zoom Out",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add panscan -0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Zoom In",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add panscan 0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Zoom",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set panscan 0"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Aspect Ratio 4:3",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"4:3\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Aspect Ratio 16:9",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"16:9\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Aspect Ratio 1.85:1",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"1.85:1\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Aspect Ratio 2.35:1",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"2.35:1\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Aspect Ratio",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-aspect-override \"-1\""
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Rotate Left",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values video-rotate 0 270 180 90"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Rotate Right",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values video-rotate 90 180 270 0"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Rotation",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set video-rotate 0"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Half Size",
+ @"key" : @"0",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_H_SIZE]
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Normal Size",
+ @"key" : @"1",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_N_SIZE]
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Double Size",
+ @"key" : @"2",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_D_SIZE]
+ }]
+ ]
+ },
+ @{
+ @"name": @"Audio",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Next Audio Track",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle audio"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Previous Audio Track",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle audio down"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Mute",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle mute"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Play Audio Later",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add audio-delay 0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Play Audio Earlier",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add audio-delay -0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Audio Delay",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set audio-delay 0.0 "
+ }]
+ ]
+ },
+ @{
+ @"name": @"Subtitle",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Next Subtitle Track",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle sub"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Previous Subtitle Track",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle sub down"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Force Style",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values sub-ass-override \"force\" \"no\""
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Display Subtitles Later",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add sub-delay 0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Display Subtitles Earlier",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add sub-delay -0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Subtitle Delay",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set sub-delay 0.0"
+ }]
+ ]
+ },
+ @{
+ @"name": @"Playback",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Pause",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle pause"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Increase Speed",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add speed 0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Decrease Speed",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add speed -0.1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Reset Speed",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"set speed 1.0"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show Playlist",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"script-message osc-playlist"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show Chapters",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"script-message osc-chapterlist"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show Tracks",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"script-message osc-tracklist"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Next File",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"playlist-next"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Previous File",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"playlist-prev"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Loop File",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values loop-file \"inf\" \"no\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Toggle Loop Playlist",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"cycle-values loop-playlist \"inf\" \"no\""
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Shuffle",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"playlist-shuffle"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Next Chapter",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add chapter 1"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Previous Chapter",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"add chapter -1"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Step Forward",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"frame-step"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Step Backward",
+ @"action" : @"cmd:",
+ @"key" : @"",
+ @"target" : self,
+ @"cmd" : @"frame-back-step"
+ }]
+ ]
+ },
+ @{
+ @"name": @"Window",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Minimize",
+ @"key" : @"m",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_MINIMIZE]
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Zoom",
+ @"key" : @"z",
+ @"cmdSpecial" : [NSNumber numberWithInt:MPM_ZOOM]
+ }]
+ ]
+ },
+ @{
+ @"name": @"Help",
+ @"menu": @[
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"mpv Website…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://mpv.io"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"mpv on github…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Online Manual…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://mpv.io/manual/master/"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Online Wiki…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv/wiki"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Release Notes…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv/blob/master/RELEASE_NOTES"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Keyboard Shortcuts…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv/blob/master/etc/input.conf"
+ }],
+ @{ @"name": @"separator" },
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Report Issue…",
+ @"action" : @"url:",
+ @"key" : @"",
+ @"target" : self,
+ @"url" : @"https://github.com/mpv-player/mpv/issues/new/choose"
+ }],
+ [NSMutableDictionary dictionaryWithDictionary:@{
+ @"name" : @"Show log File…",
+ @"action" : @"showFile:",
+ @"key" : @"",
+ @"target" : self,
+ @"file" : @"~/Library/Logs/mpv.log",
+ @"alertTitle" : @"No log File found.",
+ @"alertText" : @"You deactivated logging for the Bundle."
+ }]
+ ]
+ }
+ ];
+
+ [NSApp setMainMenu:[self mainMenu]];
+ }
+
+ return self;
+}
+
+- (NSMenu *)mainMenu
+{
+ NSMenu *mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
+ [NSApp setServicesMenu:[[NSMenu alloc] init]];
+ NSString* bundle = [[[NSProcessInfo processInfo] environment] objectForKey:@"MPVBUNDLE"];
+
+ for(id mMenu in menuTree) {
+ NSMenu *menu = [[NSMenu alloc] initWithTitle:mMenu[@"name"]];
+ NSMenuItem *mItem = [mainMenu addItemWithTitle:mMenu[@"name"]
+ action:nil
+ keyEquivalent:@""];
+ [mainMenu setSubmenu:menu forItem:mItem];
+
+ for(id subMenu in mMenu[@"menu"]) {
+ NSString *name = subMenu[@"name"];
+ NSString *action = subMenu[@"action"];
+
+#if HAVE_MACOS_TOUCHBAR
+ if ([action isEqual:@"toggleTouchBarCustomizationPalette:"]) {
+ continue;
+ }
+#endif
+
+ if ([name isEqual:@"Show log File…"] && ![bundle isEqual:@"true"]) {
+ continue;
+ }
+
+ if ([name isEqual:@"separator"]) {
+ [menu addItem:[NSMenuItem separatorItem]];
+ } else {
+ NSMenuItem *iItem = [menu addItemWithTitle:name
+ action:NSSelectorFromString(action)
+ keyEquivalent:subMenu[@"key"]];
+ [iItem setTarget:subMenu[@"target"]];
+ [subMenu setObject:iItem forKey:@"menuItem"];
+
+ NSNumber *m = subMenu[@"modifiers"];
+ if (m) {
+ [iItem setKeyEquivalentModifierMask:m.unsignedIntegerValue];
+ }
+
+ if ([subMenu[@"name"] isEqual:@"Services"]) {
+ iItem.submenu = [NSApp servicesMenu];
+ }
+ }
+ }
+ }
+
+ return mainMenu;
+}
+
+- (void)about
+{
+ NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
+ @"mpv", @"ApplicationName",
+ [(Application *)NSApp getMPVIcon], @"ApplicationIcon",
+ [NSString stringWithUTF8String:mpv_copyright], @"Copyright",
+ [NSString stringWithUTF8String:mpv_version], @"ApplicationVersion",
+ nil];
+ [NSApp orderFrontStandardAboutPanelWithOptions:options];
+}
+
+- (void)preferences:(NSMenuItem *)menuItem
+{
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSMutableDictionary *mItemDict = [self getDictFromMenuItem:menuItem];
+ NSArray *configPaths = @[
+ [NSString stringWithFormat:@"%@/.mpv/", NSHomeDirectory()],
+ [NSString stringWithFormat:@"%@/.config/mpv/", NSHomeDirectory()]];
+
+ for (id path in configPaths) {
+ NSString *fileP = [path stringByAppendingString:mItemDict[@"file"]];
+ if ([fileManager fileExistsAtPath:fileP]){
+ if ([workspace openFile:fileP])
+ return;
+ [workspace openFile:path];
+ [self alertWithTitle:mItemDict[@"alertTitle1"]
+ andText:mItemDict[@"alertText1"]];
+ return;
+ }
+ if ([workspace openFile:path]) {
+ [self alertWithTitle:mItemDict[@"alertTitle2"]
+ andText:mItemDict[@"alertText2"]];
+ return;
+ }
+ }
+
+ [self alertWithTitle:mItemDict[@"alertTitle3"]
+ andText:mItemDict[@"alertText3"]];
+}
+
+- (void)quit:(NSMenuItem *)menuItem
+{
+ NSString *cmd = [self getDictFromMenuItem:menuItem][@"cmd"];
+ [(Application *)NSApp stopMPV:(char *)[cmd UTF8String]];
+}
+
+- (void)openFile
+{
+ NSOpenPanel *panel = [[NSOpenPanel alloc] init];
+ [panel setCanChooseDirectories:YES];
+ [panel setAllowsMultipleSelection:YES];
+
+ if ([panel runModal] == NSModalResponseOK){
+ NSMutableArray *fileArray = [[NSMutableArray alloc] init];
+ for (id url in [panel URLs])
+ [fileArray addObject:[url path]];
+ [(Application *)NSApp openFiles:fileArray];
+ }
+}
+
+- (void)openPlaylist
+{
+ NSOpenPanel *panel = [[NSOpenPanel alloc] init];
+
+ if ([panel runModal] == NSModalResponseOK){
+ NSString *pl = [NSString stringWithFormat:@"loadlist \"%@\"",
+ [panel URLs][0].path];
+ [(Application *)NSApp queueCommand:(char *)[pl UTF8String]];
+ }
+}
+
+- (void)openURL
+{
+ NSAlert *alert = [[NSAlert alloc] init];
+ [alert setMessageText:@"Open URL"];
+ [alert addButtonWithTitle:@"Ok"];
+ [alert addButtonWithTitle:@"Cancel"];
+ [alert setIcon:[(Application *)NSApp getMPVIcon]];
+
+ NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 24)];
+ [input setPlaceholderString:@"URL"];
+ [alert setAccessoryView:input];
+
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ [input becomeFirstResponder];
+ });
+
+ if ([alert runModal] == NSAlertFirstButtonReturn && [input stringValue].length > 0) {
+ NSArray *url = [NSArray arrayWithObjects:[input stringValue], nil];
+ [(Application *)NSApp openFiles:url];
+ }
+}
+
+- (void)cmd:(NSMenuItem *)menuItem
+{
+ NSString *cmd = [self getDictFromMenuItem:menuItem][@"cmd"];
+ [(Application *)NSApp queueCommand:(char *)[cmd UTF8String]];
+}
+
+- (void)url:(NSMenuItem *)menuItem
+{
+ NSString *url = [self getDictFromMenuItem:menuItem][@"url"];
+ [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
+}
+
+- (void)showFile:(NSMenuItem *)menuItem
+{
+ NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
+ NSFileManager *fileManager = [NSFileManager defaultManager];
+ NSMutableDictionary *mItemDict = [self getDictFromMenuItem:menuItem];
+ NSString *file = [mItemDict[@"file"] stringByExpandingTildeInPath];
+
+ if ([fileManager fileExistsAtPath:file]){
+ NSURL *url = [NSURL fileURLWithPath:file];
+ NSArray *urlArray = [NSArray arrayWithObjects:url, nil];
+
+ [workspace activateFileViewerSelectingURLs:urlArray];
+ return;
+ }
+
+ [self alertWithTitle:mItemDict[@"alertTitle"]
+ andText:mItemDict[@"alertText"]];
+}
+
+- (void)alertWithTitle:(NSString *)title andText:(NSString *)text
+{
+ NSAlert *alert = [[NSAlert alloc] init];
+ [alert setMessageText:title];
+ [alert setInformativeText:text];
+ [alert addButtonWithTitle:@"Ok"];
+ [alert setIcon:[(Application *)NSApp getMPVIcon]];
+ [alert runModal];
+}
+
+- (NSMutableDictionary *)getDictFromMenuItem:(NSMenuItem *)menuItem
+{
+ for(id mMenu in menuTree) {
+ for(id subMenu in mMenu[@"menu"]) {
+ if([subMenu[@"menuItem"] isEqual:menuItem])
+ return subMenu;
+ }
+ }
+
+ return nil;
+}
+
+- (void)registerSelector:(SEL)action forKey:(MPMenuKey)key
+{
+ for(id mMenu in menuTree) {
+ for(id subMenu in mMenu[@"menu"]) {
+ if([subMenu[@"cmdSpecial"] isEqual:[NSNumber numberWithInt:key]]) {
+ [subMenu[@"menuItem"] setAction:action];
+ return;
+ }
+ }
+ }
+}
+
+@end
diff --git a/osdep/macosx_menubar_objc.h b/osdep/macosx_menubar_objc.h
new file mode 100644
index 0000000..072fef8
--- /dev/null
+++ b/osdep/macosx_menubar_objc.h
@@ -0,0 +1,25 @@
+/*
+ * 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/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#include "osdep/macosx_menubar.h"
+
+@interface MenuBar : NSObject
+
+- (void)registerSelector:(SEL)action forKey:(MPMenuKey)key;
+
+@end
diff --git a/osdep/macosx_touchbar.h b/osdep/macosx_touchbar.h
new file mode 100644
index 0000000..a03b68c
--- /dev/null
+++ b/osdep/macosx_touchbar.h
@@ -0,0 +1,46 @@
+/*
+ * 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/>.
+ */
+
+#import <Cocoa/Cocoa.h>
+#import "osdep/macosx_application_objc.h"
+
+#define BASE_ID @"io.mpv.touchbar"
+static NSTouchBarCustomizationIdentifier customID = BASE_ID;
+static NSTouchBarItemIdentifier seekBar = BASE_ID ".seekbar";
+static NSTouchBarItemIdentifier play = BASE_ID ".play";
+static NSTouchBarItemIdentifier nextItem = BASE_ID ".nextItem";
+static NSTouchBarItemIdentifier previousItem = BASE_ID ".previousItem";
+static NSTouchBarItemIdentifier nextChapter = BASE_ID ".nextChapter";
+static NSTouchBarItemIdentifier previousChapter = BASE_ID ".previousChapter";
+static NSTouchBarItemIdentifier cycleAudio = BASE_ID ".cycleAudio";
+static NSTouchBarItemIdentifier cycleSubtitle = BASE_ID ".cycleSubtitle";
+static NSTouchBarItemIdentifier currentPosition = BASE_ID ".currentPosition";
+static NSTouchBarItemIdentifier timeLeft = BASE_ID ".timeLeft";
+
+struct mpv_event;
+
+@interface TouchBar : NSTouchBar <NSTouchBarDelegate>
+
+-(void)processEvent:(struct mpv_event *)event;
+
+@property(nonatomic, retain) Application *app;
+@property(nonatomic, retain) NSDictionary *touchbarItems;
+@property(nonatomic, assign) double duration;
+@property(nonatomic, assign) double position;
+@property(nonatomic, assign) int pause;
+
+@end
diff --git a/osdep/macosx_touchbar.m b/osdep/macosx_touchbar.m
new file mode 100644
index 0000000..ccce8f7
--- /dev/null
+++ b/osdep/macosx_touchbar.m
@@ -0,0 +1,334 @@
+/*
+ * 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 "player/client.h"
+#import "macosx_touchbar.h"
+
+@implementation TouchBar
+
+@synthesize app = _app;
+@synthesize touchbarItems = _touchbar_items;
+@synthesize duration = _duration;
+@synthesize position = _position;
+@synthesize pause = _pause;
+
+- (id)init
+{
+ if (self = [super init]) {
+ self.touchbarItems = @{
+ seekBar: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"slider",
+ @"name": @"Seek Bar",
+ @"cmd": @"seek %f absolute-percent"
+ }],
+ play: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Play Button",
+ @"cmd": @"cycle pause",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarPauseTemplate],
+ @"imageAlt": [NSImage imageNamed:NSImageNameTouchBarPlayTemplate]
+ }],
+ previousItem: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Previous Playlist Item",
+ @"cmd": @"playlist-prev",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarGoBackTemplate]
+ }],
+ nextItem: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Next Playlist Item",
+ @"cmd": @"playlist-next",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarGoForwardTemplate]
+ }],
+ previousChapter: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Previous Chapter",
+ @"cmd": @"add chapter -1",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarSkipBackTemplate]
+ }],
+ nextChapter: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Next Chapter",
+ @"cmd": @"add chapter 1",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarSkipAheadTemplate]
+ }],
+ cycleAudio: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Cycle Audio",
+ @"cmd": @"cycle audio",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarAudioInputTemplate]
+ }],
+ cycleSubtitle: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"button",
+ @"name": @"Cycle Subtitle",
+ @"cmd": @"cycle sub",
+ @"image": [NSImage imageNamed:NSImageNameTouchBarComposeTemplate]
+ }],
+ currentPosition: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"text",
+ @"name": @"Current Position"
+ }],
+ timeLeft: [NSMutableDictionary dictionaryWithDictionary:@{
+ @"type": @"text",
+ @"name": @"Time Left"
+ }]
+ };
+
+ [self addObserver:self forKeyPath:@"visible" options:0 context:nil];
+ }
+ return self;
+}
+
+- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar
+ makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier
+{
+ if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"slider"]) {
+ NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
+ NSSlider *slider = [NSSlider sliderWithTarget:self action:@selector(seekbarChanged:)];
+ slider.minValue = 0.0f;
+ slider.maxValue = 100.0f;
+ tbItem.view = slider;
+ tbItem.customizationLabel = self.touchbarItems[identifier][@"name"];
+ [self.touchbarItems[identifier] setObject:slider forKey:@"view"];
+ [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"];
+ [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil];
+ return tbItem;
+ } else if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"button"]) {
+ NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
+ NSImage *tbImage = self.touchbarItems[identifier][@"image"];
+ NSButton *tbButton = [NSButton buttonWithImage:tbImage target:self action:@selector(buttonAction:)];
+ tbItem.view = tbButton;
+ tbItem.customizationLabel = self.touchbarItems[identifier][@"name"];
+ [self.touchbarItems[identifier] setObject:tbButton forKey:@"view"];
+ [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"];
+ [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil];
+ return tbItem;
+ } else if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"text"]) {
+ NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier];
+ NSTextField *tbText = [NSTextField labelWithString:@"0:00"];
+ tbText.alignment = NSTextAlignmentCenter;
+ tbItem.view = tbText;
+ tbItem.customizationLabel = self.touchbarItems[identifier][@"name"];
+ [self.touchbarItems[identifier] setObject:tbText forKey:@"view"];
+ [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"];
+ [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil];
+ return tbItem;
+ }
+
+ return nil;
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath
+ ofObject:(id)object
+ change:(NSDictionary<NSKeyValueChangeKey,id> *)change
+ context:(void *)context {
+ if ([keyPath isEqualToString:@"visible"]) {
+ NSNumber *visible = [object valueForKey:@"visible"];
+ if (visible.boolValue) {
+ [self updateTouchBarTimeItems];
+ [self updatePlayButton];
+ }
+ }
+}
+
+- (void)updateTouchBarTimeItems
+{
+ if (!self.isVisible)
+ return;
+
+ [self updateSlider];
+ [self updateTimeLeft];
+ [self updateCurrentPosition];
+}
+
+- (void)updateSlider
+{
+ NSCustomTouchBarItem *tbItem = self.touchbarItems[seekBar][@"tbItem"];
+ if (!tbItem.visible)
+ return;
+
+ NSSlider *seekSlider = self.touchbarItems[seekBar][@"view"];
+
+ if (self.duration <= 0) {
+ seekSlider.enabled = NO;
+ seekSlider.doubleValue = 0;
+ } else {
+ seekSlider.enabled = YES;
+ if (!seekSlider.highlighted)
+ seekSlider.doubleValue = (self.position/self.duration)*100;
+ }
+}
+
+- (void)updateTimeLeft
+{
+ NSCustomTouchBarItem *tbItem = self.touchbarItems[timeLeft][@"tbItem"];
+ if (!tbItem.visible)
+ return;
+
+ NSTextField *timeLeftItem = self.touchbarItems[timeLeft][@"view"];
+
+ [self removeConstraintForIdentifier:timeLeft];
+ if (self.duration <= 0) {
+ timeLeftItem.stringValue = @"";
+ } else {
+ int left = (int)(floor(self.duration)-floor(self.position));
+ NSString *leftFormat = [self formatTime:left];
+ NSString *durFormat = [self formatTime:self.duration];
+ timeLeftItem.stringValue = [NSString stringWithFormat:@"-%@", leftFormat];
+ [self applyConstraintFromString:[NSString stringWithFormat:@"-%@", durFormat]
+ forIdentifier:timeLeft];
+ }
+}
+
+- (void)updateCurrentPosition
+{
+ NSCustomTouchBarItem *tbItem = self.touchbarItems[currentPosition][@"tbItem"];
+ if (!tbItem.visible)
+ return;
+
+ NSTextField *curPosItem = self.touchbarItems[currentPosition][@"view"];
+ NSString *posFormat = [self formatTime:(int)floor(self.position)];
+ curPosItem.stringValue = posFormat;
+
+ [self removeConstraintForIdentifier:currentPosition];
+ if (self.duration <= 0) {
+ [self applyConstraintFromString:[self formatTime:self.position]
+ forIdentifier:currentPosition];
+ } else {
+ NSString *durFormat = [self formatTime:self.duration];
+ [self applyConstraintFromString:durFormat forIdentifier:currentPosition];
+ }
+}
+
+- (void)updatePlayButton
+{
+ NSCustomTouchBarItem *tbItem = self.touchbarItems[play][@"tbItem"];
+ if (!self.isVisible || !tbItem.visible)
+ return;
+
+ NSButton *playButton = self.touchbarItems[play][@"view"];
+ if (self.pause) {
+ playButton.image = self.touchbarItems[play][@"imageAlt"];
+ } else {
+ playButton.image = self.touchbarItems[play][@"image"];
+ }
+}
+
+- (void)buttonAction:(NSButton *)sender
+{
+ NSString *identifier = [self getIdentifierFromView:sender];
+ [self.app queueCommand:(char *)[self.touchbarItems[identifier][@"cmd"] UTF8String]];
+}
+
+- (void)seekbarChanged:(NSSlider *)slider
+{
+ NSString *identifier = [self getIdentifierFromView:slider];
+ NSString *seek = [NSString stringWithFormat:
+ self.touchbarItems[identifier][@"cmd"], slider.doubleValue];
+ [self.app queueCommand:(char *)[seek UTF8String]];
+}
+
+- (NSString *)formatTime:(int)time
+{
+ int seconds = time % 60;
+ int minutes = (time / 60) % 60;
+ int hours = time / (60 * 60);
+
+ NSString *stime = hours > 0 ? [NSString stringWithFormat:@"%d:", hours] : @"";
+ stime = (stime.length > 0 || minutes > 9) ?
+ [NSString stringWithFormat:@"%@%02d:", stime, minutes] :
+ [NSString stringWithFormat:@"%d:", minutes];
+ stime = [NSString stringWithFormat:@"%@%02d", stime, seconds];
+
+ return stime;
+}
+
+- (void)removeConstraintForIdentifier:(NSTouchBarItemIdentifier)identifier
+{
+ NSTextField *field = self.touchbarItems[identifier][@"view"];
+ [field removeConstraint:self.touchbarItems[identifier][@"constrain"]];
+}
+
+- (void)applyConstraintFromString:(NSString *)string
+ forIdentifier:(NSTouchBarItemIdentifier)identifier
+{
+ NSTextField *field = self.touchbarItems[identifier][@"view"];
+ if (field) {
+ NSString *fString = [[string componentsSeparatedByCharactersInSet:
+ [NSCharacterSet decimalDigitCharacterSet]] componentsJoinedByString:@"0"];
+ NSTextField *textField = [NSTextField labelWithString:fString];
+ NSSize size = [textField frame].size;
+
+ NSLayoutConstraint *con =
+ [NSLayoutConstraint constraintWithItem:field
+ attribute:NSLayoutAttributeWidth
+ relatedBy:NSLayoutRelationEqual
+ toItem:nil
+ attribute:NSLayoutAttributeNotAnAttribute
+ multiplier:1.0
+ constant:(int)ceil(size.width*1.1)];
+ [field addConstraint:con];
+ [self.touchbarItems[identifier] setObject:con forKey:@"constrain"];
+ }
+}
+
+- (NSString *)getIdentifierFromView:(id)view
+{
+ NSString *identifier;
+ for (identifier in self.touchbarItems)
+ if([self.touchbarItems[identifier][@"view"] isEqual:view])
+ break;
+ return identifier;
+}
+
+- (void)processEvent:(struct mpv_event *)event
+{
+ switch (event->event_id) {
+ case MPV_EVENT_END_FILE: {
+ self.position = 0;
+ self.duration = 0;
+ break;
+ }
+ case MPV_EVENT_PROPERTY_CHANGE: {
+ [self handlePropertyChange:(mpv_event_property *)event->data];
+ break;
+ }
+ }
+}
+
+- (void)handlePropertyChange:(struct mpv_event_property *)property
+{
+ NSString *name = [NSString stringWithUTF8String:property->name];
+ mpv_format format = property->format;
+
+ if ([name isEqualToString:@"time-pos"] && format == MPV_FORMAT_DOUBLE) {
+ double newPosition = *(double *)property->data;
+ newPosition = newPosition < 0 ? 0 : newPosition;
+ if ((int)(floor(newPosition) - floor(self.position)) != 0) {
+ self.position = newPosition;
+ [self updateTouchBarTimeItems];
+ }
+ } else if ([name isEqualToString:@"duration"] && format == MPV_FORMAT_DOUBLE) {
+ self.duration = *(double *)property->data;
+ [self updateTouchBarTimeItems];
+ } else if ([name isEqualToString:@"pause"] && format == MPV_FORMAT_FLAG) {
+ self.pause = *(int *)property->data;
+ [self updatePlayButton];
+ }
+}
+
+@end
diff --git a/osdep/main-fn-cocoa.c b/osdep/main-fn-cocoa.c
new file mode 100644
index 0000000..eeed127
--- /dev/null
+++ b/osdep/main-fn-cocoa.c
@@ -0,0 +1,10 @@
+#include "osdep/macosx_application.h"
+
+// This is needed because Cocoa absolutely requires creating the NSApplication
+// singleton and running it in the "main" thread. It is apparently not
+// possible to do this on a separate thread at all. It is not known how
+// Apple managed this colossal fuckup.
+int main(int argc, char *argv[])
+{
+ return cocoa_main(argc, argv);
+}
diff --git a/osdep/main-fn-unix.c b/osdep/main-fn-unix.c
new file mode 100644
index 0000000..c30c4a9
--- /dev/null
+++ b/osdep/main-fn-unix.c
@@ -0,0 +1,6 @@
+#include "main-fn.h"
+
+int main(int argc, char *argv[])
+{
+ return mpv_main(argc, argv);
+}
diff --git a/osdep/main-fn-win.c b/osdep/main-fn-win.c
new file mode 100644
index 0000000..16ea80b
--- /dev/null
+++ b/osdep/main-fn-win.c
@@ -0,0 +1,93 @@
+#include <windows.h>
+
+#ifndef BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE
+#define BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE (0x0001)
+#endif
+
+#include "common/common.h"
+#include "osdep/io.h"
+#include "osdep/terminal.h"
+#include "osdep/main-fn.h"
+
+#ifndef HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION
+
+#define HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION 1
+enum { HeapOptimizeResources = 3 };
+
+struct HEAP_OPTIMIZE_RESOURCES_INFORMATION {
+ DWORD Version;
+ DWORD Flags;
+};
+
+#endif
+
+static bool is_valid_handle(HANDLE h)
+{
+ return h != INVALID_HANDLE_VALUE && h != NULL &&
+ GetFileType(h) != FILE_TYPE_UNKNOWN;
+}
+
+static bool has_redirected_stdio(void)
+{
+ return is_valid_handle(GetStdHandle(STD_INPUT_HANDLE)) ||
+ is_valid_handle(GetStdHandle(STD_OUTPUT_HANDLE)) ||
+ is_valid_handle(GetStdHandle(STD_ERROR_HANDLE));
+}
+
+static void microsoft_nonsense(void)
+{
+ // stop Windows from showing all kinds of annoying error dialogs
+ SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOOPENFILEERRORBOX);
+
+ // Enable heap corruption detection
+ HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);
+
+ // Allow heap cache optimization and memory decommit
+ struct HEAP_OPTIMIZE_RESOURCES_INFORMATION heap_info = {
+ .Version = HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION
+ };
+ HeapSetInformation(NULL, HeapOptimizeResources, &heap_info,
+ sizeof(heap_info));
+
+ // Always use safe search paths for DLLs and other files, ie. never use the
+ // current directory
+ SetDllDirectoryW(L"");
+ SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE |
+ BASE_SEARCH_PATH_PERMANENT);
+}
+
+int main(int argc_, char **argv_)
+{
+ microsoft_nonsense();
+
+ // If started from the console wrapper (see osdep/win32-console-wrapper.c),
+ // attach to the console and set up the standard IO handles
+ bool has_console = terminal_try_attach();
+
+ // If mpv is started from Explorer, the Run dialog or the Start Menu, it
+ // will have no console and no standard IO handles. In this case, the user
+ // is expecting mpv to show some UI, so enable the pseudo-GUI profile.
+ bool gui = !has_console && !has_redirected_stdio();
+
+ int argc = 0;
+ wchar_t **argv = CommandLineToArgvW(GetCommandLineW(), &argc);
+
+ int argv_len = 0;
+ char **argv_u8 = NULL;
+
+ // Build mpv's UTF-8 argv, and add the pseudo-GUI profile if necessary
+ if (argc > 0 && argv[0])
+ MP_TARRAY_APPEND(NULL, argv_u8, argv_len, mp_to_utf8(argv_u8, argv[0]));
+ if (gui) {
+ MP_TARRAY_APPEND(NULL, argv_u8, argv_len,
+ "--player-operation-mode=pseudo-gui");
+ }
+ for (int i = 1; i < argc; i++)
+ MP_TARRAY_APPEND(NULL, argv_u8, argv_len, mp_to_utf8(argv_u8, argv[i]));
+ MP_TARRAY_APPEND(NULL, argv_u8, argv_len, NULL);
+
+ int ret = mpv_main(argv_len - 1, argv_u8);
+
+ talloc_free(argv_u8);
+ return ret;
+}
diff --git a/osdep/main-fn.h b/osdep/main-fn.h
new file mode 100644
index 0000000..8f20308
--- /dev/null
+++ b/osdep/main-fn.h
@@ -0,0 +1 @@
+int mpv_main(int argc, char *argv[]);
diff --git a/osdep/meson.build b/osdep/meson.build
new file mode 100644
index 0000000..21baafc
--- /dev/null
+++ b/osdep/meson.build
@@ -0,0 +1,51 @@
+# custom swift targets
+bridge = join_paths(source_root, 'osdep/macOS_swift_bridge.h')
+header = join_paths(build_root, 'osdep/macOS_swift.h')
+module = join_paths(build_root, 'osdep/macOS_swift.swiftmodule')
+target = join_paths(build_root, 'osdep/macOS_swift.o')
+
+swift_flags = ['-frontend', '-c', '-sdk', macos_sdk_path,
+ '-enable-objc-interop', '-emit-objc-header', '-parse-as-library']
+
+if swift_ver.version_compare('>=6.0')
+ swift_flags += ['-swift-version', '5']
+endif
+
+if get_option('debug')
+ swift_flags += '-g'
+endif
+
+if get_option('optimization') != '0'
+ swift_flags += '-O'
+endif
+
+extra_flags = get_option('swift-flags').split()
+swift_flags += extra_flags
+
+swift_compile = [swift_prog, swift_flags, '-module-name', 'macOS_swift',
+ '-emit-module-path', '@OUTPUT0@', '-import-objc-header', bridge,
+ '-emit-objc-header-path', '@OUTPUT1@', '-o', '@OUTPUT2@',
+ '@INPUT@', '-I.', '-I' + source_root,
+ '-I' + libplacebo.get_variable('includedir',
+ default_value: source_root / 'subprojects' / 'libplacebo' / 'src' / 'include')]
+
+swift_targets = custom_target('swift_targets',
+ input: swift_sources,
+ output: ['macOS_swift.swiftmodule', 'macOS_swift.h', 'macOS_swift.o'],
+ command: swift_compile,
+)
+sources += swift_targets
+
+swift_lib_dir_py = find_program(join_paths(tools_directory, 'macos-swift-lib-directory.py'))
+swift_lib_dir = run_command(swift_lib_dir_py, swift_prog.full_path(), check: true).stdout()
+message('Detected Swift library directory: ' + swift_lib_dir)
+
+# linker flags
+swift_link_flags = ['-L' + swift_lib_dir, '-Xlinker', '-rpath',
+ '-Xlinker', swift_lib_dir, '-rdynamic', '-Xlinker',
+ '-add_ast_path', '-Xlinker', module]
+if swift_ver.version_compare('>=5.0')
+ swift_link_flags += ['-Xlinker', '-rpath', '-Xlinker',
+ '/usr/lib/swift', '-L/usr/lib/swift']
+endif
+add_project_link_arguments(swift_link_flags, language: ['c', 'objc'])
diff --git a/osdep/mpv.exe.manifest b/osdep/mpv.exe.manifest
new file mode 100644
index 0000000..32dd80b
--- /dev/null
+++ b/osdep/mpv.exe.manifest
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+ <assemblyIdentity
+ version="0.0.9.0"
+ processorArchitecture="*"
+ name="mpv"
+ type="win32"
+ />
+ <description>mpv - The Movie Player</description>
+ <application xmlns="urn:schemas-microsoft-com:asm.v3">
+ <windowsSettings>
+ <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">True/PM</dpiAware>
+ <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2,PerMonitor</dpiAwareness>
+ <activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
+ </windowsSettings>
+ </application>
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+ <security>
+ <requestedPrivileges>
+ <requestedExecutionLevel
+ level="asInvoker"
+ uiAccess="false"
+ />
+ </requestedPrivileges>
+ </security>
+ </trustInfo>
+ <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+ <application>
+ <!-- Windows 10 -->
+ <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+ <!-- Windows 8.1 -->
+ <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
+ <!-- Windows 8 -->
+ <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
+ <!-- Windows 7 -->
+ <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
+ <!-- Windows Vista -->
+ <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/osdep/mpv.rc b/osdep/mpv.rc
new file mode 100644
index 0000000..7deb785
--- /dev/null
+++ b/osdep/mpv.rc
@@ -0,0 +1,50 @@
+/*
+ * 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 <winver.h>
+#include "version.h"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 2, 0, 0, 0
+ PRODUCTVERSION 2, 0, 0, 0
+ FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
+ FILEFLAGS 0
+ FILEOS VOS__WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE 0
+ {
+ BLOCK "StringFileInfo" {
+ BLOCK "000004b0" {
+ VALUE "Comments", "mpv is distributed under the terms of the GNU General Public License Version 2 or later."
+ VALUE "CompanyName", "mpv"
+ VALUE "FileDescription", "mpv"
+ VALUE "FileVersion", VERSION
+ VALUE "LegalCopyright", MPVCOPYRIGHT
+ VALUE "OriginalFilename", "mpv.exe"
+ VALUE "ProductName", "mpv"
+ VALUE "ProductVersion", VERSION
+ }
+ }
+ BLOCK "VarFileInfo" {
+ VALUE "Translation", 0, 1200
+ }
+ }
+
+IDI_ICON1 ICON DISCARDABLE "etc/mpv-icon.ico"
+
+// for some reason RT_MANIFEST does not work
+1 24 "mpv.exe.manifest"
diff --git a/osdep/path-darwin.c b/osdep/path-darwin.c
new file mode 100644
index 0000000..9c7fcda
--- /dev/null
+++ b/osdep/path-darwin.c
@@ -0,0 +1,77 @@
+/*
+ * 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 "options/path.h"
+#include "osdep/threads.h"
+#include "path.h"
+
+#include "config.h"
+
+static mp_once path_init_once = MP_STATIC_ONCE_INITIALIZER;
+
+static char mpv_home[512];
+static char old_home[512];
+static char mpv_cache[512];
+static char old_cache[512];
+
+static void path_init(void)
+{
+ char *home = getenv("HOME");
+ char *xdg_config = getenv("XDG_CONFIG_HOME");
+
+ if (xdg_config && xdg_config[0]) {
+ snprintf(mpv_home, sizeof(mpv_home), "%s/mpv", xdg_config);
+ } else if (home && home[0]) {
+ snprintf(mpv_home, sizeof(mpv_home), "%s/.config/mpv", home);
+ }
+
+ // Maintain compatibility with old ~/.mpv
+ if (home && home[0]) {
+ snprintf(old_home, sizeof(old_home), "%s/.mpv", home);
+ snprintf(old_cache, sizeof(old_cache), "%s/.mpv/cache", home);
+ }
+
+ if (home && home[0])
+ snprintf(mpv_cache, sizeof(mpv_cache), "%s/Library/Caches/io.mpv", home);
+
+ // If the old ~/.mpv exists, and the XDG config dir doesn't, use the old
+ // config dir only.
+ if (mp_path_exists(old_home) && !mp_path_exists(mpv_home)) {
+ snprintf(mpv_home, sizeof(mpv_home), "%s", old_home);
+ snprintf(mpv_cache, sizeof(mpv_cache), "%s", old_cache);
+ old_home[0] = '\0';
+ old_cache[0] = '\0';
+ }
+}
+
+const char *mp_get_platform_path_darwin(void *talloc_ctx, const char *type)
+{
+ mp_exec_once(&path_init_once, path_init);
+ if (strcmp(type, "home") == 0)
+ return mpv_home;
+ if (strcmp(type, "old_home") == 0)
+ return old_home;
+ if (strcmp(type, "cache") == 0)
+ return mpv_cache;
+ if (strcmp(type, "global") == 0)
+ return MPV_CONFDIR;
+ if (strcmp(type, "desktop") == 0)
+ return getenv("HOME");
+ return NULL;
+}
diff --git a/osdep/path-macosx.m b/osdep/path-macosx.m
new file mode 100644
index 0000000..8a5a704
--- /dev/null
+++ b/osdep/path-macosx.m
@@ -0,0 +1,34 @@
+/*
+ * 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/>.
+ */
+
+#import <Foundation/Foundation.h>
+#include "options/path.h"
+#include "osdep/path.h"
+
+const char *mp_get_platform_path_osx(void *talloc_ctx, const char *type)
+{
+ if (strcmp(type, "osxbundle") == 0 && getenv("MPVBUNDLE")) {
+ NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
+ NSString *path = [[NSBundle mainBundle] resourcePath];
+ char *res = talloc_strdup(talloc_ctx, [path UTF8String]);
+ [pool release];
+ return res;
+ }
+ if (strcmp(type, "desktop") == 0 && getenv("HOME"))
+ return mp_path_join(talloc_ctx, getenv("HOME"), "Desktop");
+ return NULL;
+}
diff --git a/osdep/path-unix.c b/osdep/path-unix.c
new file mode 100644
index 0000000..eae8b60
--- /dev/null
+++ b/osdep/path-unix.c
@@ -0,0 +1,100 @@
+/*
+ * 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 "options/path.h"
+#include "osdep/threads.h"
+#include "path.h"
+
+#include "config.h"
+
+static mp_once path_init_once = MP_STATIC_ONCE_INITIALIZER;
+
+#define CONF_MAX 512
+static char mpv_home[CONF_MAX];
+static char old_home[CONF_MAX];
+static char mpv_cache[CONF_MAX];
+static char old_cache[CONF_MAX];
+static char mpv_state[CONF_MAX];
+#define MKPATH(BUF, ...) (snprintf((BUF), CONF_MAX, __VA_ARGS__) >= CONF_MAX)
+
+static void path_init(void)
+{
+ char *home = getenv("HOME");
+ char *xdg_cache = getenv("XDG_CACHE_HOME");
+ char *xdg_config = getenv("XDG_CONFIG_HOME");
+ char *xdg_state = getenv("XDG_STATE_HOME");
+
+ bool err = false;
+ if (xdg_config && xdg_config[0]) {
+ err = err || MKPATH(mpv_home, "%s/mpv", xdg_config);
+ } else if (home && home[0]) {
+ err = err || MKPATH(mpv_home, "%s/.config/mpv", home);
+ }
+
+ // Maintain compatibility with old ~/.mpv
+ if (home && home[0]) {
+ err = err || MKPATH(old_home, "%s/.mpv", home);
+ err = err || MKPATH(old_cache, "%s/.mpv/cache", home);
+ }
+
+ if (xdg_cache && xdg_cache[0]) {
+ err = err || MKPATH(mpv_cache, "%s/mpv", xdg_cache);
+ } else if (home && home[0]) {
+ err = err || MKPATH(mpv_cache, "%s/.cache/mpv", home);
+ }
+
+ if (xdg_state && xdg_state[0]) {
+ err = err || MKPATH(mpv_state, "%s/mpv", xdg_state);
+ } else if (home && home[0]) {
+ err = err || MKPATH(mpv_state, "%s/.local/state/mpv", home);
+ }
+
+ // If the old ~/.mpv exists, and the XDG config dir doesn't, use the old
+ // config dir only. Also do not use any other XDG directories.
+ if (mp_path_exists(old_home) && !mp_path_exists(mpv_home)) {
+ err = err || MKPATH(mpv_home, "%s", old_home);
+ err = err || MKPATH(mpv_cache, "%s", old_cache);
+ err = err || MKPATH(mpv_state, "%s", old_home);
+ old_home[0] = '\0';
+ old_cache[0] = '\0';
+ }
+
+ if (err) {
+ fprintf(stderr, "Config dir exceeds %d bytes\n", CONF_MAX);
+ abort();
+ }
+}
+
+const char *mp_get_platform_path_unix(void *talloc_ctx, const char *type)
+{
+ mp_exec_once(&path_init_once, path_init);
+ if (strcmp(type, "home") == 0)
+ return mpv_home;
+ if (strcmp(type, "old_home") == 0)
+ return old_home;
+ if (strcmp(type, "cache") == 0)
+ return mpv_cache;
+ if (strcmp(type, "state") == 0)
+ return mpv_state;
+ if (strcmp(type, "global") == 0)
+ return MPV_CONFDIR;
+ if (strcmp(type, "desktop") == 0)
+ return getenv("HOME");
+ return NULL;
+}
diff --git a/osdep/path-uwp.c b/osdep/path-uwp.c
new file mode 100644
index 0000000..7eafb03
--- /dev/null
+++ b/osdep/path-uwp.c
@@ -0,0 +1,35 @@
+/*
+ * 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 <windows.h>
+
+#include "osdep/path.h"
+#include "osdep/io.h"
+#include "options/path.h"
+
+// Missing from MinGW headers.
+WINBASEAPI DWORD WINAPI GetCurrentDirectoryW(DWORD nBufferLength, LPWSTR lpBuffer);
+
+const char *mp_get_platform_path_uwp(void *talloc_ctx, const char *type)
+{
+ if (strcmp(type, "home") == 0) {
+ wchar_t homeDir[_MAX_PATH];
+ if (GetCurrentDirectoryW(_MAX_PATH, homeDir) != 0)
+ return mp_to_utf8(talloc_ctx, homeDir);
+ }
+ return NULL;
+}
diff --git a/osdep/path-win.c b/osdep/path-win.c
new file mode 100644
index 0000000..bddf5a5
--- /dev/null
+++ b/osdep/path-win.c
@@ -0,0 +1,113 @@
+/*
+ * 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 <windows.h>
+#include <shlobj.h>
+#include <knownfolders.h>
+
+#include "options/path.h"
+#include "osdep/io.h"
+#include "osdep/path.h"
+#include "osdep/threads.h"
+
+// Warning: do not use PATH_MAX. Cygwin messed it up.
+
+static mp_once path_init_once = MP_STATIC_ONCE_INITIALIZER;
+
+static char *portable_path;
+
+static char *mp_get_win_exe_dir(void *talloc_ctx)
+{
+ wchar_t w_exedir[MAX_PATH + 1] = {0};
+
+ int len = (int)GetModuleFileNameW(NULL, w_exedir, MAX_PATH);
+ int imax = 0;
+ for (int i = 0; i < len; i++) {
+ if (w_exedir[i] == '\\') {
+ w_exedir[i] = '/';
+ imax = i;
+ }
+ }
+
+ w_exedir[imax] = '\0';
+
+ return mp_to_utf8(talloc_ctx, w_exedir);
+}
+
+static char *mp_get_win_exe_subdir(void *ta_ctx, const char *name)
+{
+ return talloc_asprintf(ta_ctx, "%s/%s", mp_get_win_exe_dir(ta_ctx), name);
+}
+
+static char *mp_get_win_shell_dir(void *talloc_ctx, REFKNOWNFOLDERID folder)
+{
+ wchar_t *w_appdir = NULL;
+
+ if (FAILED(SHGetKnownFolderPath(folder, KF_FLAG_CREATE, NULL, &w_appdir)))
+ return NULL;
+
+ char *appdir = mp_to_utf8(talloc_ctx, w_appdir);
+ CoTaskMemFree(w_appdir);
+ return appdir;
+}
+
+static char *mp_get_win_app_dir(void *talloc_ctx)
+{
+ char *path = mp_get_win_shell_dir(talloc_ctx, &FOLDERID_RoamingAppData);
+ return path ? mp_path_join(talloc_ctx, path, "mpv") : NULL;
+}
+
+static char *mp_get_win_local_app_dir(void *talloc_ctx)
+{
+ char *path = mp_get_win_shell_dir(talloc_ctx, &FOLDERID_LocalAppData);
+ return path ? mp_path_join(talloc_ctx, path, "mpv") : NULL;
+}
+
+static void path_init(void)
+{
+ void *tmp = talloc_new(NULL);
+ char *path = mp_get_win_exe_subdir(tmp, "portable_config");
+ if (path && mp_path_exists(path))
+ portable_path = talloc_strdup(NULL, path);
+ talloc_free(tmp);
+}
+
+const char *mp_get_platform_path_win(void *talloc_ctx, const char *type)
+{
+ mp_exec_once(&path_init_once, path_init);
+ if (portable_path) {
+ if (strcmp(type, "home") == 0)
+ return portable_path;
+ if (strcmp(type, "cache") == 0)
+ return mp_path_join(talloc_ctx, portable_path, "cache");
+ } else {
+ if (strcmp(type, "home") == 0)
+ return mp_get_win_app_dir(talloc_ctx);
+ if (strcmp(type, "cache") == 0)
+ return mp_path_join(talloc_ctx, mp_get_win_local_app_dir(talloc_ctx), "cache");
+ if (strcmp(type, "state") == 0)
+ return mp_get_win_local_app_dir(talloc_ctx);
+ if (strcmp(type, "exe_dir") == 0)
+ return mp_get_win_exe_dir(talloc_ctx);
+ // Not really true, but serves as a way to return a lowest-priority dir.
+ if (strcmp(type, "global") == 0)
+ return mp_get_win_exe_subdir(talloc_ctx, "mpv");
+ }
+ if (strcmp(type, "desktop") == 0)
+ return mp_get_win_shell_dir(talloc_ctx, &FOLDERID_Desktop);
+ return NULL;
+}
diff --git a/osdep/path.h b/osdep/path.h
new file mode 100644
index 0000000..2c00ea5
--- /dev/null
+++ b/osdep/path.h
@@ -0,0 +1,32 @@
+#ifndef OSDEP_PATH_H
+#define OSDEP_PATH_H
+
+// Return a platform-specific path, identified by the type parameter. If the
+// return value is allocated, talloc_ctx is used as talloc parent context.
+//
+// The following type values are defined:
+// "home" the native mpv-specific user config dir
+// "old_home" same as "home", but lesser priority (compatibility)
+// "osxbundle" OSX bundle resource path
+// "global" the least priority, global config file location
+// "desktop" path to desktop contents
+//
+// These additional types are also defined. However, they are not necessarily
+// implemented on every platform. Unlike some other type values that are
+// platform specific (like "osxbundle"), the value of "home" is returned
+// instead if these types are not explicitly defined.
+// "cache" the native mpv-specific user cache dir
+// "state" the native mpv-specific user state dir
+//
+// It is allowed to return a static string, so the caller must set talloc_ctx
+// to something other than NULL to avoid memory leaks.
+typedef const char *(*mp_get_platform_path_cb)(void *talloc_ctx, const char *type);
+
+// Conforming to mp_get_platform_path_cb.
+const char *mp_get_platform_path_darwin(void *talloc_ctx, const char *type);
+const char *mp_get_platform_path_uwp(void *talloc_ctx, const char *type);
+const char *mp_get_platform_path_win(void *talloc_ctx, const char *type);
+const char *mp_get_platform_path_osx(void *talloc_ctx, const char *type);
+const char *mp_get_platform_path_unix(void *talloc_ctx, const char *type);
+
+#endif
diff --git a/osdep/poll_wrapper.c b/osdep/poll_wrapper.c
new file mode 100644
index 0000000..3fe039b
--- /dev/null
+++ b/osdep/poll_wrapper.c
@@ -0,0 +1,89 @@
+/*
+ * 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 <stdlib.h>
+#include <poll.h>
+#include <sys/select.h>
+#include <stdio.h>
+
+#include "common/common.h"
+#include "config.h"
+#include "poll_wrapper.h"
+#include "timer.h"
+
+
+int mp_poll(struct pollfd *fds, int nfds, int64_t timeout_ns)
+{
+#if HAVE_PPOLL
+ struct timespec ts;
+ ts.tv_sec = timeout_ns / MP_TIME_S_TO_NS(1);
+ ts.tv_nsec = timeout_ns % MP_TIME_S_TO_NS(1);
+ struct timespec *tsp = timeout_ns >= 0 ? &ts : NULL;
+ return ppoll(fds, nfds, tsp, NULL);
+#endif
+ // Round-up to 1ms for short timeouts (100us, 1000us]
+ if (timeout_ns > MP_TIME_US_TO_NS(100))
+ timeout_ns = MPMAX(timeout_ns, MP_TIME_MS_TO_NS(1));
+ if (timeout_ns > 0)
+ timeout_ns /= MP_TIME_MS_TO_NS(1);
+ return poll(fds, nfds, timeout_ns);
+}
+
+// poll shim that supports device files on macOS.
+int polldev(struct pollfd fds[], nfds_t nfds, int timeout)
+{
+#ifdef __APPLE__
+ int maxfd = 0;
+ fd_set readfds, writefds;
+ FD_ZERO(&readfds);
+ FD_ZERO(&writefds);
+ for (size_t i = 0; i < nfds; ++i) {
+ struct pollfd *fd = &fds[i];
+ if (fd->fd > maxfd) {
+ maxfd = fd->fd;
+ }
+ if ((fd->events & POLLIN)) {
+ FD_SET(fd->fd, &readfds);
+ }
+ if ((fd->events & POLLOUT)) {
+ FD_SET(fd->fd, &writefds);
+ }
+ }
+ struct timeval _timeout = {
+ .tv_sec = timeout / 1000,
+ .tv_usec = (timeout % 1000) * 1000
+ };
+ int n = select(maxfd + 1, &readfds, &writefds, NULL,
+ timeout != -1 ? &_timeout : NULL);
+ if (n < 0) {
+ return n;
+ }
+ for (size_t i = 0; i < nfds; ++i) {
+ struct pollfd *fd = &fds[i];
+ fd->revents = 0;
+ if (FD_ISSET(fd->fd, &readfds)) {
+ fd->revents |= POLLIN;
+ }
+ if (FD_ISSET(fd->fd, &writefds)) {
+ fd->revents |= POLLOUT;
+ }
+ }
+ return n;
+#else
+ return poll(fds, nfds, timeout);
+#endif
+}
diff --git a/osdep/poll_wrapper.h b/osdep/poll_wrapper.h
new file mode 100644
index 0000000..b359ed3
--- /dev/null
+++ b/osdep/poll_wrapper.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <poll.h>
+#include <stdint.h>
+
+// Behaves like poll(3) but works for device files on macOS.
+// Only supports POLLIN and POLLOUT.
+int polldev(struct pollfd fds[], nfds_t nfds, int timeout);
+
+// Generic polling wrapper. It will try and use higher resolution
+// polling (ppoll) if available.
+int mp_poll(struct pollfd *fds, int nfds, int64_t timeout_ns);
diff --git a/osdep/semaphore.h b/osdep/semaphore.h
new file mode 100644
index 0000000..40cf383
--- /dev/null
+++ b/osdep/semaphore.h
@@ -0,0 +1,37 @@
+#ifndef MP_SEMAPHORE_H_
+#define MP_SEMAPHORE_H_
+
+#include <sys/types.h>
+#include <semaphore.h>
+
+// OSX provides non-working empty stubs, so we emulate them.
+// This should be AS-safe, but cancellation issues were ignored.
+// sem_getvalue() is not provided.
+// sem_post() won't always correctly return an error on overflow.
+// Process-shared semantics are not provided.
+
+#ifdef __APPLE__
+
+#define MP_SEMAPHORE_EMULATION
+
+#include "osdep/threads.h"
+
+#define MP_SEM_VALUE_MAX 4096
+
+typedef struct {
+ int wakeup_pipe[2];
+ mp_mutex lock;
+ // protected by lock
+ unsigned int count;
+} mp_sem_t;
+
+int mp_sem_init(mp_sem_t *sem, int pshared, unsigned int value);
+int mp_sem_wait(mp_sem_t *sem);
+int mp_sem_trywait(mp_sem_t *sem);
+int mp_sem_timedwait(mp_sem_t *sem, int64_t until);
+int mp_sem_post(mp_sem_t *sem);
+int mp_sem_destroy(mp_sem_t *sem);
+
+#endif
+
+#endif
diff --git a/osdep/semaphore_osx.c b/osdep/semaphore_osx.c
new file mode 100644
index 0000000..bfb4d57
--- /dev/null
+++ b/osdep/semaphore_osx.c
@@ -0,0 +1,117 @@
+/* Copyright (C) 2017 the mpv developers
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "osdep/semaphore.h"
+
+#ifdef MP_SEMAPHORE_EMULATION
+
+#include <unistd.h>
+#include <poll.h>
+#include <limits.h>
+#include <sys/time.h>
+#include <errno.h>
+
+#include <common/common.h>
+#include "io.h"
+#include "timer.h"
+
+int mp_sem_init(mp_sem_t *sem, int pshared, unsigned int value)
+{
+ if (pshared) {
+ errno = ENOSYS;
+ return -1;
+ }
+ if (value > INT_MAX) {
+ errno = EINVAL;
+ return -1;
+ }
+ if (mp_make_wakeup_pipe(sem->wakeup_pipe) < 0)
+ return -1;
+ sem->count = 0;
+ mp_mutex_init(&sem->lock);
+ return 0;
+}
+
+int mp_sem_wait(mp_sem_t *sem)
+{
+ return mp_sem_timedwait(sem, -1);
+}
+
+int mp_sem_trywait(mp_sem_t *sem)
+{
+ int r = -1;
+ mp_mutex_lock(&sem->lock);
+ if (sem->count == 0) {
+ char buf[1024];
+ ssize_t s = read(sem->wakeup_pipe[0], buf, sizeof(buf));
+ if (s > 0 && s <= INT_MAX - sem->count) // can't handle overflows correctly
+ sem->count += s;
+ }
+ if (sem->count > 0) {
+ sem->count -= 1;
+ r = 0;
+ }
+ mp_mutex_unlock(&sem->lock);
+ if (r < 0)
+ errno = EAGAIN;
+ return r;
+}
+
+int mp_sem_timedwait(mp_sem_t *sem, int64_t until)
+{
+ while (1) {
+ if (!mp_sem_trywait(sem))
+ return 0;
+
+ int timeout = 0;
+ if (until == -1) {
+ timeout = -1;
+ } else if (until >= 0) {
+ timeout = (until - mp_time_ns()) / MP_TIME_MS_TO_NS(1);
+ timeout = MPCLAMP(timeout, 0, INT_MAX);
+ } else {
+ assert(false && "Invalid mp_time value!");
+ }
+
+ struct pollfd fd = {.fd = sem->wakeup_pipe[0], .events = POLLIN};
+ int r = poll(&fd, 1, timeout);
+ if (r < 0)
+ return -1;
+ if (r == 0) {
+ errno = ETIMEDOUT;
+ return -1;
+ }
+ }
+}
+
+int mp_sem_post(mp_sem_t *sem)
+{
+ if (write(sem->wakeup_pipe[1], &(char){0}, 1) == 1)
+ return 0;
+ // Actually we can't handle overflow fully correctly, because we can't
+ // check sem->count atomically, while still being AS-safe.
+ errno = EOVERFLOW;
+ return -1;
+}
+
+int mp_sem_destroy(mp_sem_t *sem)
+{
+ close(sem->wakeup_pipe[0]);
+ close(sem->wakeup_pipe[1]);
+ mp_mutex_destroy(&sem->lock);
+ return 0;
+}
+
+#endif
diff --git a/osdep/strnlen.h b/osdep/strnlen.h
new file mode 100644
index 0000000..e66932a
--- /dev/null
+++ b/osdep/strnlen.h
@@ -0,0 +1,31 @@
+/*
+ * strnlen wrapper
+ *
+ * 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/>.
+ */
+
+#ifndef MP_OSDEP_STRNLEN
+#define MP_OSDEP_STRNLEN
+
+#include "config.h"
+
+#if HAVE_ANDROID
+// strnlen is broken on current android ndk, see https://code.google.com/p/android/issues/detail?id=74741
+#include "osdep/android/strnlen.h"
+#define strnlen freebsd_strnlen
+#endif
+
+#endif
diff --git a/osdep/subprocess-dummy.c b/osdep/subprocess-dummy.c
new file mode 100644
index 0000000..df74538
--- /dev/null
+++ b/osdep/subprocess-dummy.c
@@ -0,0 +1,7 @@
+#include "subprocess.h"
+
+void mp_subprocess2(struct mp_subprocess_opts *opts,
+ struct mp_subprocess_result *res)
+{
+ *res = (struct mp_subprocess_result){.error = MP_SUBPROCESS_EUNSUPPORTED};
+}
diff --git a/osdep/subprocess-posix.c b/osdep/subprocess-posix.c
new file mode 100644
index 0000000..0656ec5
--- /dev/null
+++ b/osdep/subprocess-posix.c
@@ -0,0 +1,345 @@
+/*
+ * 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 <poll.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <signal.h>
+
+#include "osdep/subprocess.h"
+
+#include "common/common.h"
+#include "misc/thread_tools.h"
+#include "osdep/io.h"
+#include "stream/stream.h"
+
+extern char **environ;
+
+#ifdef SIGRTMAX
+#define SIGNAL_MAX SIGRTMAX
+#else
+#define SIGNAL_MAX 32
+#endif
+
+#define SAFE_CLOSE(fd) do { if ((fd) >= 0) close((fd)); (fd) = -1; } while (0)
+
+// Async-signal-safe execvpe(). POSIX does not list it as async-signal-safe
+// (POSIX is such a joke), so do it manually. While in theory the searching is
+// apparently implementation dependent and not exposed (because POSIX is a
+// joke?), the expected rules are still relatively simple.
+// Doesn't set errno correctly.
+// Somewhat inspired by musl's src/process/execvp.c.
+static int as_execvpe(const char *path, const char *file, char *const argv[],
+ char *const envp[])
+{
+ if (strchr(file, '/') || !file[0])
+ return execve(file, argv, envp);
+
+ size_t flen = strlen(file);
+ while (path && path[0]) {
+ size_t plen = strcspn(path, ":");
+ // Ignore paths that are too long.
+ char fn[PATH_MAX];
+ if (plen + 1 + flen + 1 < sizeof(fn)) {
+ memcpy(fn, path, plen);
+ fn[plen] = '/';
+ memcpy(fn + plen + 1, file, flen + 1);
+ execve(fn, argv, envp);
+ if (errno != EACCES && errno != ENOENT && errno != ENOTDIR)
+ break;
+ }
+ path += plen + (path[plen] == ':' ? 1 : 0);
+ }
+ return -1;
+}
+
+// In the child process, resets the signal mask to defaults. Also clears any
+// signal handlers first so nothing funny happens.
+static void reset_signals_child(void)
+{
+ struct sigaction sa = { 0 };
+ sigset_t sigmask;
+ sa.sa_handler = SIG_DFL;
+ sigemptyset(&sigmask);
+
+ for (int nr = 1; nr < SIGNAL_MAX; nr++)
+ sigaction(nr, &sa, NULL);
+ sigprocmask(SIG_SETMASK, &sigmask, NULL);
+}
+
+// Returns 0 on any error, valid PID on success.
+// This function must be async-signal-safe, as it may be called from a fork().
+static pid_t spawn_process(const char *path, struct mp_subprocess_opts *opts,
+ int src_fds[])
+{
+ int p[2] = {-1, -1};
+ pid_t fres = 0;
+ sigset_t sigmask, oldmask;
+ sigfillset(&sigmask);
+ pthread_sigmask(SIG_BLOCK, &sigmask, &oldmask);
+
+ // We setup a communication pipe to signal failure. Since the child calls
+ // exec() and becomes the calling process, we don't know if or when the
+ // child process successfully ran exec() just from the PID.
+ // Use a CLOEXEC pipe to detect whether exec() was used. Obviously it will
+ // be closed if exec() succeeds, and an error is written if not.
+ // There are also some things further below in the code that need CLOEXEC.
+ if (mp_make_cloexec_pipe(p) < 0)
+ goto done;
+ // Check whether CLOEXEC is really set. Important for correct operation.
+ int p_flags = fcntl(p[0], F_GETFD);
+ if (p_flags == -1 || !FD_CLOEXEC || !(p_flags & FD_CLOEXEC))
+ goto done; // require CLOEXEC; unknown if fallback would be worth it
+
+ fres = fork();
+ if (fres < 0) {
+ fres = 0;
+ goto done;
+ }
+ if (fres == 0) {
+ // child
+ reset_signals_child();
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (src_fds[n] == opts->fds[n].fd) {
+ int flags = fcntl(opts->fds[n].fd, F_GETFD);
+ if (flags == -1)
+ goto child_failed;
+ flags &= ~(unsigned)FD_CLOEXEC;
+ if (fcntl(opts->fds[n].fd, F_SETFD, flags) == -1)
+ goto child_failed;
+ } else if (dup2(src_fds[n], opts->fds[n].fd) < 0) {
+ goto child_failed;
+ }
+ }
+
+ as_execvpe(path, opts->exe, opts->args, opts->env ? opts->env : environ);
+
+ child_failed:
+ (void)write(p[1], &(char){1}, 1); // shouldn't be able to fail
+ _exit(1);
+ }
+
+ SAFE_CLOSE(p[1]);
+
+ int r;
+ do {
+ r = read(p[0], &(char){0}, 1);
+ } while (r < 0 && errno == EINTR);
+
+ // If exec()ing child failed, collect it immediately.
+ if (r != 0) {
+ while (waitpid(fres, &(int){0}, 0) < 0 && errno == EINTR) {}
+ fres = 0;
+ }
+
+done:
+ pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
+ SAFE_CLOSE(p[0]);
+ SAFE_CLOSE(p[1]);
+
+ return fres;
+}
+
+void mp_subprocess2(struct mp_subprocess_opts *opts,
+ struct mp_subprocess_result *res)
+{
+ int status = -1;
+ int comm_pipe[MP_SUBPROCESS_MAX_FDS][2];
+ int src_fds[MP_SUBPROCESS_MAX_FDS];
+ int devnull = -1;
+ pid_t pid = 0;
+ bool spawned = false;
+ bool killed_by_us = false;
+ int cancel_fd = -1;
+ char *path = getenv("PATH");
+ if (!path)
+ path = ""; // failure, who cares
+
+ *res = (struct mp_subprocess_result){0};
+
+ for (int n = 0; n < opts->num_fds; n++)
+ comm_pipe[n][0] = comm_pipe[n][1] = -1;
+
+ if (opts->cancel) {
+ cancel_fd = mp_cancel_get_fd(opts->cancel);
+ if (cancel_fd < 0)
+ goto done;
+ }
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ assert(!(opts->fds[n].on_read && opts->fds[n].on_write));
+
+ if (opts->fds[n].on_read && mp_make_cloexec_pipe(comm_pipe[n]) < 0)
+ goto done;
+
+ if (opts->fds[n].on_write || opts->fds[n].write_buf) {
+ assert(opts->fds[n].on_write && opts->fds[n].write_buf);
+ if (mp_make_cloexec_pipe(comm_pipe[n]) < 0)
+ goto done;
+ MPSWAP(int, comm_pipe[n][0], comm_pipe[n][1]);
+
+ struct sigaction sa = {.sa_handler = SIG_IGN, .sa_flags = SA_RESTART};
+ sigfillset(&sa.sa_mask);
+ sigaction(SIGPIPE, &sa, NULL);
+ }
+ }
+
+ devnull = open("/dev/null", O_RDONLY | O_CLOEXEC);
+ if (devnull < 0)
+ goto done;
+
+ // redirect FDs
+ for (int n = 0; n < opts->num_fds; n++) {
+ int src_fd = devnull;
+ if (comm_pipe[n][1] >= 0)
+ src_fd = comm_pipe[n][1];
+ if (opts->fds[n].src_fd >= 0)
+ src_fd = opts->fds[n].src_fd;
+ src_fds[n] = src_fd;
+ }
+
+ if (opts->detach) {
+ // If we run it detached, we fork a child to start the process; then
+ // it exits immediately, letting PID 1 inherit it. So we don't need
+ // anything else to collect these child PIDs.
+ sigset_t sigmask, oldmask;
+ sigfillset(&sigmask);
+ pthread_sigmask(SIG_BLOCK, &sigmask, &oldmask);
+ pid_t fres = fork();
+ if (fres < 0)
+ goto done;
+ if (fres == 0) {
+ // child
+ setsid();
+ if (!spawn_process(path, opts, src_fds))
+ _exit(1);
+ _exit(0);
+ }
+ pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
+ int child_status = 0;
+ while (waitpid(fres, &child_status, 0) < 0 && errno == EINTR) {}
+ if (!WIFEXITED(child_status) || WEXITSTATUS(child_status) != 0)
+ goto done;
+ } else {
+ pid = spawn_process(path, opts, src_fds);
+ if (!pid)
+ goto done;
+ }
+
+ spawned = true;
+
+ for (int n = 0; n < opts->num_fds; n++)
+ SAFE_CLOSE(comm_pipe[n][1]);
+ SAFE_CLOSE(devnull);
+
+ while (1) {
+ struct pollfd fds[MP_SUBPROCESS_MAX_FDS + 1];
+ int map_fds[MP_SUBPROCESS_MAX_FDS + 1];
+ int num_fds = 0;
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (comm_pipe[n][0] >= 0) {
+ map_fds[num_fds] = n;
+ fds[num_fds++] = (struct pollfd){
+ .events = opts->fds[n].on_read ? POLLIN : POLLOUT,
+ .fd = comm_pipe[n][0],
+ };
+ }
+ }
+ if (!num_fds)
+ break;
+ if (cancel_fd >= 0) {
+ map_fds[num_fds] = -1;
+ fds[num_fds++] = (struct pollfd){.events = POLLIN, .fd = cancel_fd};
+ }
+
+ if (poll(fds, num_fds, -1) < 0 && errno != EINTR)
+ break;
+
+ for (int idx = 0; idx < num_fds; idx++) {
+ if (fds[idx].revents) {
+ int n = map_fds[idx];
+ if (n < 0) {
+ // cancel_fd
+ if (pid)
+ kill(pid, SIGKILL);
+ killed_by_us = true;
+ break;
+ }
+ struct mp_subprocess_fd *fd = &opts->fds[n];
+ if (fd->on_read) {
+ char buf[4096];
+ ssize_t r = read(comm_pipe[n][0], buf, sizeof(buf));
+ if (r < 0 && errno == EINTR)
+ continue;
+ fd->on_read(fd->on_read_ctx, buf, MPMAX(r, 0));
+ if (r <= 0)
+ SAFE_CLOSE(comm_pipe[n][0]);
+ } else if (fd->on_write) {
+ if (!fd->write_buf->len) {
+ fd->on_write(fd->on_write_ctx);
+ if (!fd->write_buf->len) {
+ SAFE_CLOSE(comm_pipe[n][0]);
+ continue;
+ }
+ }
+ ssize_t r = write(comm_pipe[n][0], fd->write_buf->start,
+ fd->write_buf->len);
+ if (r < 0 && errno == EINTR)
+ continue;
+ if (r < 0) {
+ // Let's not signal an error for now - caller can check
+ // whether all buffer was written.
+ SAFE_CLOSE(comm_pipe[n][0]);
+ continue;
+ }
+ *fd->write_buf = bstr_cut(*fd->write_buf, r);
+ }
+ }
+ }
+ }
+
+ // Note: it can happen that a child process closes the pipe, but does not
+ // terminate yet. In this case, we would have to run waitpid() in
+ // a separate thread and use pthread_cancel(), or use other weird
+ // and laborious tricks in order to react to mp_cancel.
+ // So this isn't handled yet.
+ if (pid)
+ while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {}
+
+done:
+ for (int n = 0; n < opts->num_fds; n++) {
+ SAFE_CLOSE(comm_pipe[n][0]);
+ SAFE_CLOSE(comm_pipe[n][1]);
+ }
+ SAFE_CLOSE(devnull);
+
+ if (!spawned || (pid && WIFEXITED(status) && WEXITSTATUS(status) == 127)) {
+ res->error = MP_SUBPROCESS_EINIT;
+ } else if (pid && WIFEXITED(status)) {
+ res->exit_status = WEXITSTATUS(status);
+ } else if (spawned && opts->detach) {
+ // ok
+ } else if (killed_by_us) {
+ res->error = MP_SUBPROCESS_EKILLED_BY_US;
+ } else {
+ res->error = MP_SUBPROCESS_EGENERIC;
+ }
+}
diff --git a/osdep/subprocess-win.c b/osdep/subprocess-win.c
new file mode 100644
index 0000000..5413b24
--- /dev/null
+++ b/osdep/subprocess-win.c
@@ -0,0 +1,516 @@
+/*
+ * 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 <windows.h>
+#include <string.h>
+
+#include "osdep/subprocess.h"
+
+#include "osdep/io.h"
+#include "osdep/windows_utils.h"
+
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "stream/stream.h"
+#include "misc/bstr.h"
+#include "misc/thread_tools.h"
+
+// Internal CRT FD flags
+#define FOPEN (0x01)
+#define FPIPE (0x08)
+#define FDEV (0x40)
+
+static void write_arg(bstr *cmdline, char *arg)
+{
+ // Empty args must be represented as an empty quoted string
+ if (!arg[0]) {
+ bstr_xappend(NULL, cmdline, bstr0("\"\""));
+ return;
+ }
+
+ // If the string doesn't have characters that need to be escaped, it's best
+ // to leave it alone for the sake of Windows programs that don't process
+ // quoted args correctly.
+ if (!strpbrk(arg, " \t\"")) {
+ bstr_xappend(NULL, cmdline, bstr0(arg));
+ return;
+ }
+
+ // If there are characters that need to be escaped, write a quoted string
+ bstr_xappend(NULL, cmdline, bstr0("\""));
+
+ // Escape the argument. To match the behavior of CommandLineToArgvW,
+ // backslashes are only escaped if they appear before a quote or the end of
+ // the string.
+ int num_slashes = 0;
+ for (int pos = 0; arg[pos]; pos++) {
+ switch (arg[pos]) {
+ case '\\':
+ // Count consecutive backslashes
+ num_slashes++;
+ break;
+ case '"':
+ // Write the argument up to the point before the quote
+ bstr_xappend(NULL, cmdline, (struct bstr){arg, pos});
+ arg += pos;
+ pos = 0;
+
+ // Double backslashes preceding the quote
+ for (int i = 0; i < num_slashes; i++)
+ bstr_xappend(NULL, cmdline, bstr0("\\"));
+ num_slashes = 0;
+
+ // Escape the quote itself
+ bstr_xappend(NULL, cmdline, bstr0("\\"));
+ break;
+ default:
+ num_slashes = 0;
+ }
+ }
+
+ // Write the rest of the argument
+ bstr_xappend(NULL, cmdline, bstr0(arg));
+
+ // Double backslashes at the end of the argument
+ for (int i = 0; i < num_slashes; i++)
+ bstr_xappend(NULL, cmdline, bstr0("\\"));
+
+ bstr_xappend(NULL, cmdline, bstr0("\""));
+}
+
+// Convert an array of arguments to a properly escaped command-line string
+static wchar_t *write_cmdline(void *ctx, char *argv0, char **args)
+{
+ // argv0 should always be quoted. Otherwise, arguments may be interpreted as
+ // part of the program name. Also, it can't contain escape sequences.
+ bstr cmdline = {0};
+ bstr_xappend_asprintf(NULL, &cmdline, "\"%s\"", argv0);
+
+ if (args) {
+ for (int i = 0; args[i]; i++) {
+ bstr_xappend(NULL, &cmdline, bstr0(" "));
+ write_arg(&cmdline, args[i]);
+ }
+ }
+
+ wchar_t *wcmdline = mp_from_utf8(ctx, cmdline.start);
+ talloc_free(cmdline.start);
+ return wcmdline;
+}
+
+static void delete_handle_list(void *p)
+{
+ LPPROC_THREAD_ATTRIBUTE_LIST list = p;
+ DeleteProcThreadAttributeList(list);
+}
+
+// Create a PROC_THREAD_ATTRIBUTE_LIST that specifies exactly which handles are
+// inherited by the subprocess
+static LPPROC_THREAD_ATTRIBUTE_LIST create_handle_list(void *ctx,
+ HANDLE *handles, int num)
+{
+ // Get required attribute list size
+ SIZE_T size = 0;
+ if (!InitializeProcThreadAttributeList(NULL, 1, 0, &size)) {
+ if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
+ return NULL;
+ }
+
+ // Allocate attribute list
+ LPPROC_THREAD_ATTRIBUTE_LIST list = talloc_size(ctx, size);
+ if (!InitializeProcThreadAttributeList(list, 1, 0, &size))
+ goto error;
+ talloc_set_destructor(list, delete_handle_list);
+
+ if (!UpdateProcThreadAttribute(list, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
+ handles, num * sizeof(HANDLE), NULL, NULL))
+ goto error;
+
+ return list;
+error:
+ talloc_free(list);
+ return NULL;
+}
+
+// Helper method similar to sparse_poll, skips NULL handles
+static int sparse_wait(HANDLE *handles, unsigned num_handles)
+{
+ unsigned w_num_handles = 0;
+ HANDLE w_handles[MP_SUBPROCESS_MAX_FDS + 2];
+ int map[MP_SUBPROCESS_MAX_FDS + 2];
+ if (num_handles > MP_ARRAY_SIZE(w_handles))
+ return -1;
+
+ for (unsigned i = 0; i < num_handles; i++) {
+ if (!handles[i])
+ continue;
+
+ w_handles[w_num_handles] = handles[i];
+ map[w_num_handles] = i;
+ w_num_handles++;
+ }
+
+ if (w_num_handles == 0)
+ return -1;
+ DWORD i = WaitForMultipleObjects(w_num_handles, w_handles, FALSE, INFINITE);
+ i -= WAIT_OBJECT_0;
+
+ if (i >= w_num_handles)
+ return -1;
+ return map[i];
+}
+
+// Wrapper for ReadFile that treats ERROR_IO_PENDING as success
+static int async_read(HANDLE file, void *buf, unsigned size, OVERLAPPED* ol)
+{
+ if (!ReadFile(file, buf, size, NULL, ol))
+ return (GetLastError() == ERROR_IO_PENDING) ? 0 : -1;
+ return 0;
+}
+
+static bool is_valid_handle(HANDLE h)
+{
+ // _get_osfhandle can return -2 "when the file descriptor is not associated
+ // with a stream"
+ return h && h != INVALID_HANDLE_VALUE && (intptr_t)h != -2;
+}
+
+static wchar_t *convert_environ(void *ctx, char **env)
+{
+ // Environment size in wchar_ts, including the trailing NUL
+ size_t env_size = 1;
+
+ for (int i = 0; env[i]; i++) {
+ int count = MultiByteToWideChar(CP_UTF8, 0, env[i], -1, NULL, 0);
+ if (count <= 0)
+ abort();
+ env_size += count;
+ }
+
+ wchar_t *ret = talloc_array(ctx, wchar_t, env_size);
+ size_t pos = 0;
+
+ for (int i = 0; env[i]; i++) {
+ int count = MultiByteToWideChar(CP_UTF8, 0, env[i], -1,
+ ret + pos, env_size - pos);
+ if (count <= 0)
+ abort();
+ pos += count;
+ }
+
+ return ret;
+}
+
+void mp_subprocess2(struct mp_subprocess_opts *opts,
+ struct mp_subprocess_result *res)
+{
+ wchar_t *tmp = talloc_new(NULL);
+ DWORD r;
+
+ HANDLE share_hndls[MP_SUBPROCESS_MAX_FDS] = {0};
+ int share_hndl_count = 0;
+ HANDLE wait_hndls[MP_SUBPROCESS_MAX_FDS + 2] = {0};
+ int wait_hndl_count = 0;
+
+ struct {
+ HANDLE handle;
+ bool handle_close;
+ char crt_flags;
+
+ HANDLE read;
+ OVERLAPPED read_ol;
+ char *read_buf;
+ } fd_data[MP_SUBPROCESS_MAX_FDS] = {0};
+
+ // The maximum target FD is limited because FDs have to fit in two sparse
+ // arrays in STARTUPINFO.lpReserved2, which has a maximum size of 65535
+ // bytes. The first four bytes are the handle count, followed by one byte
+ // per handle for flags, and an intptr_t per handle for the HANDLE itself.
+ static const int crt_fd_max = (65535 - sizeof(int)) / (1 + sizeof(intptr_t));
+ int crt_fd_count = 0;
+
+ // If the function exits before CreateProcess, there was an init error
+ *res = (struct mp_subprocess_result){ .error = MP_SUBPROCESS_EINIT };
+
+ STARTUPINFOEXW si = {
+ .StartupInfo = {
+ .cb = sizeof si,
+ .dwFlags = STARTF_USESTDHANDLES | STARTF_FORCEOFFFEEDBACK,
+ },
+ };
+
+ PROCESS_INFORMATION pi = {0};
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (opts->fds[n].fd >= crt_fd_max) {
+ // Target FD is too big to fit in the CRT FD array
+ res->error = MP_SUBPROCESS_EUNSUPPORTED;
+ goto done;
+ }
+
+ if (opts->fds[n].fd >= crt_fd_count)
+ crt_fd_count = opts->fds[n].fd + 1;
+
+ if (opts->fds[n].src_fd >= 0) {
+ HANDLE src_handle = (HANDLE)_get_osfhandle(opts->fds[n].src_fd);
+
+ // Invalid handles are just ignored. This is because sometimes the
+ // standard handles are invalid in Windows, like in GUI processes.
+ // In this case mp_subprocess2 callers should still be able to
+ // blindly forward the standard FDs.
+ if (!is_valid_handle(src_handle))
+ continue;
+
+ DWORD type = GetFileType(src_handle);
+ bool is_console_handle = false;
+ switch (type & 0xff) {
+ case FILE_TYPE_DISK:
+ fd_data[n].crt_flags = FOPEN;
+ break;
+ case FILE_TYPE_CHAR:
+ fd_data[n].crt_flags = FOPEN | FDEV;
+ is_console_handle = GetConsoleMode(src_handle, &(DWORD){0});
+ break;
+ case FILE_TYPE_PIPE:
+ fd_data[n].crt_flags = FOPEN | FPIPE;
+ break;
+ case FILE_TYPE_UNKNOWN:
+ continue;
+ }
+
+ if (is_console_handle) {
+ // Some Windows versions have bugs when duplicating console
+ // handles, or when adding console handles to the CreateProcess
+ // handle list, so just use the handle directly for now. Console
+ // handles treat inheritance weirdly, so this should still work.
+ fd_data[n].handle = src_handle;
+ } else {
+ // Instead of making the source handle inheritable, just
+ // duplicate it to an inheritable handle
+ if (!DuplicateHandle(GetCurrentProcess(), src_handle,
+ GetCurrentProcess(), &fd_data[n].handle, 0,
+ TRUE, DUPLICATE_SAME_ACCESS))
+ goto done;
+ fd_data[n].handle_close = true;
+
+ share_hndls[share_hndl_count++] = fd_data[n].handle;
+ }
+
+ } else if (opts->fds[n].on_read && !opts->detach) {
+ fd_data[n].read_ol.hEvent = CreateEventW(NULL, TRUE, FALSE, NULL);
+ if (!fd_data[n].read_ol.hEvent)
+ goto done;
+
+ struct w32_create_anon_pipe_opts o = {
+ .server_flags = PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED,
+ .client_inheritable = true,
+ };
+ if (!mp_w32_create_anon_pipe(&fd_data[n].read, &fd_data[n].handle, &o))
+ goto done;
+ fd_data[n].handle_close = true;
+
+ wait_hndls[n] = fd_data[n].read_ol.hEvent;
+ wait_hndl_count++;
+
+ fd_data[n].crt_flags = FOPEN | FPIPE;
+ fd_data[n].read_buf = talloc_size(tmp, 4096);
+
+ share_hndls[share_hndl_count++] = fd_data[n].handle;
+
+ } else {
+ DWORD access;
+ if (opts->fds[n].fd == 0) {
+ access = FILE_GENERIC_READ;
+ } else if (opts->fds[n].fd <= 2) {
+ access = FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES;
+ } else {
+ access = FILE_GENERIC_READ | FILE_GENERIC_WRITE;
+ }
+
+ SECURITY_ATTRIBUTES sa = {
+ .nLength = sizeof sa,
+ .bInheritHandle = TRUE,
+ };
+ fd_data[n].crt_flags = FOPEN | FDEV;
+ fd_data[n].handle = CreateFileW(L"NUL", access,
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
+ &sa, OPEN_EXISTING, 0, NULL);
+ fd_data[n].handle_close = true;
+ }
+
+ switch (opts->fds[n].fd) {
+ case 0:
+ si.StartupInfo.hStdInput = fd_data[n].handle;
+ break;
+ case 1:
+ si.StartupInfo.hStdOutput = fd_data[n].handle;
+ break;
+ case 2:
+ si.StartupInfo.hStdError = fd_data[n].handle;
+ break;
+ }
+ }
+
+ // Convert the UTF-8 environment into a UTF-16 Windows environment block
+ wchar_t *env = NULL;
+ if (opts->env)
+ env = convert_environ(tmp, opts->env);
+
+ // Convert the args array to a UTF-16 Windows command-line string
+ char **args = opts->args && opts->args[0] ? &opts->args[1] : 0;
+ wchar_t *cmdline = write_cmdline(tmp, opts->exe, args);
+
+ // Get pointers to the arrays in lpReserved2. This is an undocumented data
+ // structure used by MSVCRT (and other frameworks and runtimes) to emulate
+ // FD inheritance. The format is unofficially documented here:
+ // https://www.catch22.net/tuts/undocumented-createprocess
+ si.StartupInfo.cbReserved2 = sizeof(int) + crt_fd_count * (1 + sizeof(intptr_t));
+ si.StartupInfo.lpReserved2 = talloc_size(tmp, si.StartupInfo.cbReserved2);
+ char *crt_buf_flags = si.StartupInfo.lpReserved2 + sizeof(int);
+ char *crt_buf_hndls = crt_buf_flags + crt_fd_count;
+
+ memcpy(si.StartupInfo.lpReserved2, &crt_fd_count, sizeof(int));
+
+ // Fill the handle array with INVALID_HANDLE_VALUE, for unassigned handles
+ for (int n = 0; n < crt_fd_count; n++) {
+ HANDLE h = INVALID_HANDLE_VALUE;
+ memcpy(crt_buf_hndls + n * sizeof(intptr_t), &h, sizeof(intptr_t));
+ }
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ crt_buf_flags[opts->fds[n].fd] = fd_data[n].crt_flags;
+ memcpy(crt_buf_hndls + opts->fds[n].fd * sizeof(intptr_t),
+ &fd_data[n].handle, sizeof(intptr_t));
+ }
+
+ DWORD flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;
+
+ // Specify which handles are inherited by the subprocess. If this isn't
+ // specified, the subprocess inherits all inheritable handles, which could
+ // include handles created by other threads. See:
+ // http://blogs.msdn.com/b/oldnewthing/archive/2011/12/16/10248328.aspx
+ si.lpAttributeList = create_handle_list(tmp, share_hndls, share_hndl_count);
+
+ // If we have a console, the subprocess will automatically attach to it so
+ // it can receive Ctrl+C events. If we don't have a console, prevent the
+ // subprocess from creating its own console window by specifying
+ // CREATE_NO_WINDOW. GetConsoleCP() can be used to reliably determine if we
+ // have a console or not (Cygwin uses it too.)
+ if (!GetConsoleCP())
+ flags |= CREATE_NO_WINDOW;
+
+ if (!CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, flags, env, NULL,
+ &si.StartupInfo, &pi))
+ goto done;
+ talloc_free(cmdline);
+ talloc_free(env);
+ talloc_free(si.StartupInfo.lpReserved2);
+ talloc_free(si.lpAttributeList);
+ CloseHandle(pi.hThread);
+
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (fd_data[n].handle_close && is_valid_handle(fd_data[n].handle))
+ CloseHandle(fd_data[n].handle);
+ fd_data[n].handle = NULL;
+
+ if (fd_data[n].read) {
+ // Do the first read operation on each pipe
+ if (async_read(fd_data[n].read, fd_data[n].read_buf, 4096,
+ &fd_data[n].read_ol))
+ {
+ CloseHandle(fd_data[n].read);
+ wait_hndls[n] = fd_data[n].read = NULL;
+ wait_hndl_count--;
+ }
+ }
+ }
+
+ if (opts->detach) {
+ res->error = MP_SUBPROCESS_OK;
+ goto done;
+ }
+
+ res->error = MP_SUBPROCESS_EGENERIC;
+
+ wait_hndls[MP_SUBPROCESS_MAX_FDS] = pi.hProcess;
+ wait_hndl_count++;
+
+ if (opts->cancel)
+ wait_hndls[MP_SUBPROCESS_MAX_FDS + 1] = mp_cancel_get_event(opts->cancel);
+
+ DWORD exit_code;
+ while (wait_hndl_count) {
+ int n = sparse_wait(wait_hndls, MP_ARRAY_SIZE(wait_hndls));
+
+ if (n >= 0 && n < MP_SUBPROCESS_MAX_FDS) {
+ // Complete the read operation on the pipe
+ if (!GetOverlappedResult(fd_data[n].read, &fd_data[n].read_ol, &r, TRUE)) {
+ CloseHandle(fd_data[n].read);
+ wait_hndls[n] = fd_data[n].read = NULL;
+ wait_hndl_count--;
+ } else {
+ opts->fds[n].on_read(opts->fds[n].on_read_ctx,
+ fd_data[n].read_buf, r);
+
+ // Begin the next read operation on the pipe
+ if (async_read(fd_data[n].read, fd_data[n].read_buf, 4096,
+ &fd_data[n].read_ol))
+ {
+ CloseHandle(fd_data[n].read);
+ wait_hndls[n] = fd_data[n].read = NULL;
+ wait_hndl_count--;
+ }
+ }
+
+ } else if (n == MP_SUBPROCESS_MAX_FDS) { // pi.hProcess
+ GetExitCodeProcess(pi.hProcess, &exit_code);
+ res->exit_status = exit_code;
+
+ CloseHandle(pi.hProcess);
+ wait_hndls[n] = pi.hProcess = NULL;
+ wait_hndl_count--;
+
+ } else if (n == MP_SUBPROCESS_MAX_FDS + 1) { // opts.cancel
+ if (pi.hProcess) {
+ TerminateProcess(pi.hProcess, 1);
+ res->error = MP_SUBPROCESS_EKILLED_BY_US;
+ goto done;
+ }
+ } else {
+ goto done;
+ }
+ }
+
+ res->error = MP_SUBPROCESS_OK;
+
+done:
+ for (int n = 0; n < opts->num_fds; n++) {
+ if (is_valid_handle(fd_data[n].read)) {
+ // Cancel any pending I/O (if the process was killed)
+ CancelIo(fd_data[n].read);
+ GetOverlappedResult(fd_data[n].read, &fd_data[n].read_ol, &r, TRUE);
+ CloseHandle(fd_data[n].read);
+ }
+ if (fd_data[n].handle_close && is_valid_handle(fd_data[n].handle))
+ CloseHandle(fd_data[n].handle);
+ if (fd_data[n].read_ol.hEvent)
+ CloseHandle(fd_data[n].read_ol.hEvent);
+ }
+ if (pi.hProcess)
+ CloseHandle(pi.hProcess);
+ talloc_free(tmp);
+}
diff --git a/osdep/subprocess.c b/osdep/subprocess.c
new file mode 100644
index 0000000..75cd124
--- /dev/null
+++ b/osdep/subprocess.c
@@ -0,0 +1,39 @@
+/*
+ * 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 "common/common.h"
+#include "common/msg.h"
+#include "common/msg_control.h"
+
+#include "subprocess.h"
+
+void mp_devnull(void *ctx, char *data, size_t size)
+{
+}
+
+const char *mp_subprocess_err_str(int num)
+{
+ // Note: these are visible to the public client API
+ switch (num) {
+ case MP_SUBPROCESS_OK: return "success";
+ case MP_SUBPROCESS_EKILLED_BY_US: return "killed";
+ case MP_SUBPROCESS_EINIT: return "init";
+ case MP_SUBPROCESS_EUNSUPPORTED: return "unsupported";
+ case MP_SUBPROCESS_EGENERIC: // fall through
+ default: return "unknown";
+ }
+}
diff --git a/osdep/subprocess.h b/osdep/subprocess.h
new file mode 100644
index 0000000..4bf2dc3
--- /dev/null
+++ b/osdep/subprocess.h
@@ -0,0 +1,88 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_SUBPROCESS_H_
+#define MP_SUBPROCESS_H_
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "misc/bstr.h"
+
+struct mp_cancel;
+
+// Incrementally called with data that was read. Buffer valid only during call.
+// size==0 means EOF.
+typedef void (*subprocess_read_cb)(void *ctx, char *data, size_t size);
+// Incrementally called to refill *mp_subprocess_fd.write_buf, whenever write_buf
+// has length 0 and the pipe is writable. While writing, *write_buf is adjusted
+// to contain only the not yet written data.
+// Not filling the buffer means EOF.
+typedef void (*subprocess_write_cb)(void *ctx);
+
+void mp_devnull(void *ctx, char *data, size_t size);
+
+#define MP_SUBPROCESS_MAX_FDS 10
+
+struct mp_subprocess_fd {
+ int fd; // target FD
+
+ // Only one of on_read or src_fd can be set. If none are set, use /dev/null.
+ // Note: "neutral" initialization requires setting src_fd=-1.
+ subprocess_read_cb on_read; // if not NULL, serve reads
+ void *on_read_ctx; // for on_read(on_read_ctx, ...)
+ subprocess_write_cb on_write; // if not NULL, serve writes
+ void *on_write_ctx; // for on_write(on_write_ctx, ...)
+ bstr *write_buf; // must be !=NULL if on_write is set
+ int src_fd; // if >=0, dup this FD to target FD
+};
+
+struct mp_subprocess_opts {
+ char *exe; // binary to execute (never non-NULL)
+ char **args; // argument list (NULL for none, otherwise NULL-terminated)
+ char **env; // if !NULL, set this as environment variable block
+ // Complete set of FDs passed down. All others are supposed to be closed.
+ struct mp_subprocess_fd fds[MP_SUBPROCESS_MAX_FDS];
+ int num_fds;
+ struct mp_cancel *cancel; // if !NULL, asynchronous process abort (kills it)
+ bool detach; // if true, do not wait for process to end
+};
+
+struct mp_subprocess_result {
+ int error; // one of MP_SUBPROCESS_* (>0 on error)
+ // NB: if WIFEXITED applies, error==0, and this is WEXITSTATUS
+ // on win32, this can use the full 32 bit
+ // if started with detach==true, this is always 0
+ uint32_t exit_status; // if error==0==MP_SUBPROCESS_OK, 0 otherwise
+};
+
+// Subprocess error values.
+#define MP_SUBPROCESS_OK 0 // no error
+#define MP_SUBPROCESS_EGENERIC -1 // unknown error
+#define MP_SUBPROCESS_EKILLED_BY_US -2 // mp_cancel was triggered
+#define MP_SUBPROCESS_EINIT -3 // error during initialization
+#define MP_SUBPROCESS_EUNSUPPORTED -4 // API not supported
+
+// Turn MP_SUBPROCESS_* values into a static string. Never returns NULL.
+const char *mp_subprocess_err_str(int num);
+
+// Caller must set *opts.
+void mp_subprocess2(struct mp_subprocess_opts *opts,
+ struct mp_subprocess_result *res);
+
+#endif
diff --git a/osdep/terminal-dummy.c b/osdep/terminal-dummy.c
new file mode 100644
index 0000000..4b33d78
--- /dev/null
+++ b/osdep/terminal-dummy.c
@@ -0,0 +1,35 @@
+#include "terminal.h"
+
+void terminal_init(void)
+{
+}
+
+void terminal_setup_getch(struct input_ctx *ictx)
+{
+}
+
+void terminal_uninit(void)
+{
+}
+
+bool terminal_in_background(void)
+{
+ return false;
+}
+
+void terminal_get_size(int *w, int *h)
+{
+}
+
+void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height)
+{
+}
+
+void mp_write_console_ansi(void *wstream, char *buf)
+{
+}
+
+bool terminal_try_attach(void)
+{
+ return false;
+}
diff --git a/osdep/terminal-unix.c b/osdep/terminal-unix.c
new file mode 100644
index 0000000..d5b8fe3
--- /dev/null
+++ b/osdep/terminal-unix.c
@@ -0,0 +1,573 @@
+/*
+ * Based on GyS-TermIO v2.0 (for GySmail v3) (copyright (C) 1999 A'rpi/ESP-team)
+ *
+ * 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 <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <signal.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <assert.h>
+
+#include <termios.h>
+#include <unistd.h>
+
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/poll_wrapper.h"
+
+#include "common/common.h"
+#include "misc/bstr.h"
+#include "input/input.h"
+#include "input/keycodes.h"
+#include "misc/ctype.h"
+#include "terminal.h"
+
+// Timeout in ms after which the (normally ambiguous) ESC key is detected.
+#define ESC_TIMEOUT 100
+
+// Timeout in ms after which the poll for input is aborted. The FG/BG state is
+// tested before every wait, and a positive value allows reactivating input
+// processing when mpv is brought to the foreground while it was running in the
+// background. In such a situation, an infinite timeout (-1) will keep mpv
+// waiting for input without realizing the terminal state changed - and thus
+// buffer all keypresses until ENTER is pressed.
+#define INPUT_TIMEOUT 1000
+
+static struct termios tio_orig;
+
+static int tty_in = -1, tty_out = -1;
+
+struct key_entry {
+ const char *seq;
+ int mpkey;
+ // If this is not NULL, then if seq is matched as unique prefix, the
+ // existing sequence is replaced by the following string. Matching
+ // continues normally, and mpkey is or-ed into the final result.
+ const char *replace;
+};
+
+static const struct key_entry keys[] = {
+ {"\010", MP_KEY_BS},
+ {"\011", MP_KEY_TAB},
+ {"\012", MP_KEY_ENTER},
+ {"\177", MP_KEY_BS},
+
+ {"\033[1~", MP_KEY_HOME},
+ {"\033[2~", MP_KEY_INS},
+ {"\033[3~", MP_KEY_DEL},
+ {"\033[4~", MP_KEY_END},
+ {"\033[5~", MP_KEY_PGUP},
+ {"\033[6~", MP_KEY_PGDWN},
+ {"\033[7~", MP_KEY_HOME},
+ {"\033[8~", MP_KEY_END},
+
+ {"\033[11~", MP_KEY_F+1},
+ {"\033[12~", MP_KEY_F+2},
+ {"\033[13~", MP_KEY_F+3},
+ {"\033[14~", MP_KEY_F+4},
+ {"\033[15~", MP_KEY_F+5},
+ {"\033[17~", MP_KEY_F+6},
+ {"\033[18~", MP_KEY_F+7},
+ {"\033[19~", MP_KEY_F+8},
+ {"\033[20~", MP_KEY_F+9},
+ {"\033[21~", MP_KEY_F+10},
+ {"\033[23~", MP_KEY_F+11},
+ {"\033[24~", MP_KEY_F+12},
+
+ {"\033OA", MP_KEY_UP},
+ {"\033OB", MP_KEY_DOWN},
+ {"\033OC", MP_KEY_RIGHT},
+ {"\033OD", MP_KEY_LEFT},
+ {"\033[A", MP_KEY_UP},
+ {"\033[B", MP_KEY_DOWN},
+ {"\033[C", MP_KEY_RIGHT},
+ {"\033[D", MP_KEY_LEFT},
+ {"\033[E", MP_KEY_KP5},
+ {"\033[F", MP_KEY_END},
+ {"\033[H", MP_KEY_HOME},
+
+ {"\033[[A", MP_KEY_F+1},
+ {"\033[[B", MP_KEY_F+2},
+ {"\033[[C", MP_KEY_F+3},
+ {"\033[[D", MP_KEY_F+4},
+ {"\033[[E", MP_KEY_F+5},
+
+ {"\033OE", MP_KEY_KP5}, // mintty?
+ {"\033OM", MP_KEY_KPENTER},
+ {"\033OP", MP_KEY_F+1},
+ {"\033OQ", MP_KEY_F+2},
+ {"\033OR", MP_KEY_F+3},
+ {"\033OS", MP_KEY_F+4},
+
+ {"\033Oa", MP_KEY_UP | MP_KEY_MODIFIER_CTRL}, // urxvt
+ {"\033Ob", MP_KEY_DOWN | MP_KEY_MODIFIER_CTRL},
+ {"\033Oc", MP_KEY_RIGHT | MP_KEY_MODIFIER_CTRL},
+ {"\033Od", MP_KEY_LEFT | MP_KEY_MODIFIER_CTRL},
+ {"\033Oj", '*'}, // also keypad, but we don't have separate codes for them
+ {"\033Ok", '+'},
+ {"\033Om", '-'},
+ {"\033On", MP_KEY_KPDEC},
+ {"\033Oo", '/'},
+ {"\033Op", MP_KEY_KP0},
+ {"\033Oq", MP_KEY_KP1},
+ {"\033Or", MP_KEY_KP2},
+ {"\033Os", MP_KEY_KP3},
+ {"\033Ot", MP_KEY_KP4},
+ {"\033Ou", MP_KEY_KP5},
+ {"\033Ov", MP_KEY_KP6},
+ {"\033Ow", MP_KEY_KP7},
+ {"\033Ox", MP_KEY_KP8},
+ {"\033Oy", MP_KEY_KP9},
+
+ {"\033[a", MP_KEY_UP | MP_KEY_MODIFIER_SHIFT}, // urxvt
+ {"\033[b", MP_KEY_DOWN | MP_KEY_MODIFIER_SHIFT},
+ {"\033[c", MP_KEY_RIGHT | MP_KEY_MODIFIER_SHIFT},
+ {"\033[d", MP_KEY_LEFT | MP_KEY_MODIFIER_SHIFT},
+ {"\033[2^", MP_KEY_INS | MP_KEY_MODIFIER_CTRL},
+ {"\033[3^", MP_KEY_DEL | MP_KEY_MODIFIER_CTRL},
+ {"\033[5^", MP_KEY_PGUP | MP_KEY_MODIFIER_CTRL},
+ {"\033[6^", MP_KEY_PGDWN | MP_KEY_MODIFIER_CTRL},
+ {"\033[7^", MP_KEY_HOME | MP_KEY_MODIFIER_CTRL},
+ {"\033[8^", MP_KEY_END | MP_KEY_MODIFIER_CTRL},
+
+ {"\033[1;2", MP_KEY_MODIFIER_SHIFT, .replace = "\033["}, // xterm
+ {"\033[1;3", MP_KEY_MODIFIER_ALT, .replace = "\033["},
+ {"\033[1;5", MP_KEY_MODIFIER_CTRL, .replace = "\033["},
+ {"\033[1;4", MP_KEY_MODIFIER_ALT | MP_KEY_MODIFIER_SHIFT, .replace = "\033["},
+ {"\033[1;6", MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_SHIFT, .replace = "\033["},
+ {"\033[1;7", MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_ALT, .replace = "\033["},
+ {"\033[1;8",
+ MP_KEY_MODIFIER_CTRL | MP_KEY_MODIFIER_ALT | MP_KEY_MODIFIER_SHIFT,
+ .replace = "\033["},
+
+ {"\033[29~", MP_KEY_MENU},
+ {"\033[Z", MP_KEY_TAB | MP_KEY_MODIFIER_SHIFT},
+
+ {0}
+};
+
+#define BUF_LEN 256
+
+struct termbuf {
+ unsigned char b[BUF_LEN];
+ int len;
+ int mods;
+};
+
+static void skip_buf(struct termbuf *b, unsigned int count)
+{
+ assert(count <= b->len);
+
+ memmove(&b->b[0], &b->b[count], b->len - count);
+ b->len -= count;
+ b->mods = 0;
+}
+
+static struct termbuf buf;
+
+static void process_input(struct input_ctx *input_ctx, bool timeout)
+{
+ while (buf.len) {
+ // Lone ESC is ambiguous, so accept it only after a timeout.
+ if (timeout &&
+ ((buf.len == 1 && buf.b[0] == '\033') ||
+ (buf.len > 1 && buf.b[0] == '\033' && buf.b[1] == '\033')))
+ {
+ mp_input_put_key(input_ctx, MP_KEY_ESC);
+ skip_buf(&buf, 1);
+ }
+
+ int utf8_len = bstr_parse_utf8_code_length(buf.b[0]);
+ if (utf8_len > 1) {
+ if (buf.len < utf8_len)
+ goto read_more;
+
+ mp_input_put_key_utf8(input_ctx, buf.mods, (bstr){buf.b, utf8_len});
+ skip_buf(&buf, utf8_len);
+ continue;
+ }
+
+ const struct key_entry *match = NULL; // may be a partial match
+ for (int n = 0; keys[n].seq; n++) {
+ const struct key_entry *e = &keys[n];
+ if (memcmp(e->seq, buf.b, MPMIN(buf.len, strlen(e->seq))) == 0) {
+ if (match)
+ goto read_more; /* need more bytes to disambiguate */
+ match = e;
+ }
+ }
+
+ if (!match) { // normal or unknown key
+ int mods = 0;
+ if (buf.b[0] == '\033') {
+ if (buf.len > 1 && buf.b[1] == '[') {
+ // unknown CSI sequence. wait till it completes
+ for (int i = 2; i < buf.len; i++) {
+ if (buf.b[i] >= 0x40 && buf.b[i] <= 0x7E) {
+ skip_buf(&buf, i+1);
+ continue; // complete - throw it away
+ }
+ }
+ goto read_more; // not yet complete
+ }
+ // non-CSI esc sequence
+ skip_buf(&buf, 1);
+ if (buf.len > 0 && buf.b[0] > 0 && buf.b[0] < 127) {
+ // meta+normal key
+ mods |= MP_KEY_MODIFIER_ALT;
+ } else {
+ // Throw it away. Typically, this will be a complete,
+ // unsupported sequence, and dropping this will skip it.
+ skip_buf(&buf, buf.len);
+ continue;
+ }
+ }
+ unsigned char c = buf.b[0];
+ skip_buf(&buf, 1);
+ if (c < 32) {
+ // 1..26 is ^A..^Z, and 27..31 is ^3..^7
+ c = c <= 26 ? (c + 'a' - 1) : (c + '3' - 27);
+ mods |= MP_KEY_MODIFIER_CTRL;
+ }
+ mp_input_put_key(input_ctx, c | mods);
+ continue;
+ }
+
+ int seq_len = strlen(match->seq);
+ if (seq_len > buf.len)
+ goto read_more; /* partial match */
+
+ if (match->replace) {
+ int rep = strlen(match->replace);
+ assert(rep <= seq_len);
+ memcpy(buf.b, match->replace, rep);
+ memmove(buf.b + rep, buf.b + seq_len, buf.len - seq_len);
+ buf.len = rep + buf.len - seq_len;
+ buf.mods |= match->mpkey;
+ continue;
+ }
+
+ mp_input_put_key(input_ctx, buf.mods | match->mpkey);
+ skip_buf(&buf, seq_len);
+ }
+
+read_more: ; /* need more bytes */
+}
+
+static int getch2_active = 0;
+static int getch2_enabled = 0;
+static bool read_terminal;
+
+static void enable_kx(bool enable)
+{
+ // This check is actually always true, as enable_kx calls are all guarded
+ // by read_terminal, which is true only if both stdin and stdout are a
+ // tty. Note that stderr being redirected away has no influence over mpv's
+ // I/O handling except for disabling the terminal OSD, and thus stderr
+ // shouldn't be relied on here either.
+ if (isatty(tty_out)) {
+ char *cmd = enable ? "\033=" : "\033>";
+ (void)write(tty_out, cmd, strlen(cmd));
+ }
+}
+
+static void do_activate_getch2(void)
+{
+ if (getch2_active || !read_terminal)
+ return;
+
+ enable_kx(true);
+
+ struct termios tio_new;
+ tcgetattr(tty_in,&tio_new);
+
+ tio_new.c_lflag &= ~(ICANON|ECHO); /* Clear ICANON and ECHO. */
+ tio_new.c_cc[VMIN] = 1;
+ tio_new.c_cc[VTIME] = 0;
+ tcsetattr(tty_in,TCSANOW,&tio_new);
+
+ getch2_active = 1;
+}
+
+static void do_deactivate_getch2(void)
+{
+ if (!getch2_active)
+ return;
+
+ enable_kx(false);
+ tcsetattr(tty_in, TCSANOW, &tio_orig);
+
+ getch2_active = 0;
+}
+
+// sigaction wrapper
+static int setsigaction(int signo, void (*handler) (int),
+ int flags, bool do_mask)
+{
+ struct sigaction sa;
+ sa.sa_handler = handler;
+
+ if(do_mask)
+ sigfillset(&sa.sa_mask);
+ else
+ sigemptyset(&sa.sa_mask);
+
+ sa.sa_flags = flags | SA_RESTART;
+ return sigaction(signo, &sa, NULL);
+}
+
+static void getch2_poll(void)
+{
+ if (!getch2_enabled)
+ return;
+
+ // check if stdin is in the foreground process group
+ int newstatus = (tcgetpgrp(tty_in) == getpgrp());
+
+ // and activate getch2 if it is, deactivate otherwise
+ if (newstatus)
+ do_activate_getch2();
+ else
+ do_deactivate_getch2();
+}
+
+static mp_thread input_thread;
+static struct input_ctx *input_ctx;
+static int death_pipe[2] = {-1, -1};
+enum { PIPE_STOP, PIPE_CONT };
+static int stop_cont_pipe[2] = {-1, -1};
+
+static void stop_sighandler(int signum)
+{
+ int saved_errno = errno;
+ (void)write(stop_cont_pipe[1], &(char){PIPE_STOP}, 1);
+ errno = saved_errno;
+
+ // note: for this signal, we use SA_RESETHAND but do NOT mask signals
+ // so this will invoke the default handler
+ raise(SIGTSTP);
+}
+
+static void continue_sighandler(int signum)
+{
+ int saved_errno = errno;
+ // SA_RESETHAND has reset SIGTSTP, so we need to restore it here
+ setsigaction(SIGTSTP, stop_sighandler, SA_RESETHAND, false);
+
+ (void)write(stop_cont_pipe[1], &(char){PIPE_CONT}, 1);
+ errno = saved_errno;
+}
+
+static void safe_close(int *p)
+{
+ if (*p >= 0)
+ close(*p);
+ *p = -1;
+}
+
+static void close_sig_pipes(void)
+{
+ for (int n = 0; n < 2; n++) {
+ safe_close(&death_pipe[n]);
+ safe_close(&stop_cont_pipe[n]);
+ }
+}
+
+static void close_tty(void)
+{
+ if (tty_in >= 0 && tty_in != STDIN_FILENO)
+ close(tty_in);
+
+ tty_in = tty_out = -1;
+}
+
+static void quit_request_sighandler(int signum)
+{
+ int saved_errno = errno;
+ (void)write(death_pipe[1], &(char){1}, 1);
+ errno = saved_errno;
+}
+
+static MP_THREAD_VOID terminal_thread(void *ptr)
+{
+ mp_thread_set_name("terminal/input");
+ bool stdin_ok = read_terminal; // if false, we still wait for SIGTERM
+ while (1) {
+ getch2_poll();
+ struct pollfd fds[3] = {
+ { .events = POLLIN, .fd = death_pipe[0] },
+ { .events = POLLIN, .fd = stop_cont_pipe[0] },
+ { .events = POLLIN, .fd = tty_in }
+ };
+ /*
+ * if the process isn't in foreground process group, then on macos
+ * polldev() doesn't rest and gets into 100% cpu usage (see issue #11795)
+ * with read() returning EIO. but we shouldn't quit on EIO either since
+ * the process might be foregrounded later.
+ *
+ * so just avoid poll-ing tty_in when we know the process is not in the
+ * foreground. there's a small race window, but the timeout will take
+ * care of it so it's fine.
+ */
+ bool is_fg = tcgetpgrp(tty_in) == getpgrp();
+ int r = polldev(fds, stdin_ok && is_fg ? 3 : 2, buf.len ? ESC_TIMEOUT : INPUT_TIMEOUT);
+ if (fds[0].revents) {
+ do_deactivate_getch2();
+ break;
+ }
+ if (fds[1].revents & POLLIN) {
+ int8_t c = -1;
+ (void)read(stop_cont_pipe[0], &c, 1);
+ if (c == PIPE_STOP)
+ do_deactivate_getch2();
+ else if (c == PIPE_CONT)
+ getch2_poll();
+ }
+ if (fds[2].revents) {
+ int retval = read(tty_in, &buf.b[buf.len], BUF_LEN - buf.len);
+ if (!retval || (retval == -1 && errno != EINTR && errno != EAGAIN && errno != EIO))
+ break; // EOF/closed
+ if (retval > 0) {
+ buf.len += retval;
+ process_input(input_ctx, false);
+ }
+ }
+ if (r == 0)
+ process_input(input_ctx, true);
+ }
+ char c;
+ bool quit = read(death_pipe[0], &c, 1) == 1 && c == 1;
+ // Important if we received SIGTERM, rather than regular quit.
+ if (quit) {
+ struct mp_cmd *cmd = mp_input_parse_cmd(input_ctx, bstr0("quit 4"), "");
+ if (cmd)
+ mp_input_queue_cmd(input_ctx, cmd);
+ }
+ MP_THREAD_RETURN();
+}
+
+void terminal_setup_getch(struct input_ctx *ictx)
+{
+ if (!getch2_enabled || input_ctx)
+ return;
+
+ if (mp_make_wakeup_pipe(death_pipe) < 0)
+ return;
+ if (mp_make_wakeup_pipe(stop_cont_pipe) < 0) {
+ close_sig_pipes();
+ return;
+ }
+
+ // Disable reading from the terminal even if stdout is not a tty, to make
+ // mpv ... | less
+ // do the right thing.
+ read_terminal = isatty(tty_in) && isatty(STDOUT_FILENO);
+
+ input_ctx = ictx;
+
+ if (mp_thread_create(&input_thread, terminal_thread, NULL)) {
+ input_ctx = NULL;
+ close_sig_pipes();
+ close_tty();
+ return;
+ }
+
+ setsigaction(SIGINT, quit_request_sighandler, SA_RESETHAND, false);
+ setsigaction(SIGQUIT, quit_request_sighandler, SA_RESETHAND, false);
+ setsigaction(SIGTERM, quit_request_sighandler, SA_RESETHAND, false);
+}
+
+void terminal_uninit(void)
+{
+ if (!getch2_enabled)
+ return;
+
+ // restore signals
+ setsigaction(SIGCONT, SIG_DFL, 0, false);
+ setsigaction(SIGTSTP, SIG_DFL, 0, false);
+ setsigaction(SIGINT, SIG_DFL, 0, false);
+ setsigaction(SIGQUIT, SIG_DFL, 0, false);
+ setsigaction(SIGTERM, SIG_DFL, 0, false);
+ setsigaction(SIGTTIN, SIG_DFL, 0, false);
+ setsigaction(SIGTTOU, SIG_DFL, 0, false);
+
+ if (input_ctx) {
+ (void)write(death_pipe[1], &(char){0}, 1);
+ mp_thread_join(input_thread);
+ close_sig_pipes();
+ input_ctx = NULL;
+ }
+
+ do_deactivate_getch2();
+ close_tty();
+
+ getch2_enabled = 0;
+ read_terminal = false;
+}
+
+bool terminal_in_background(void)
+{
+ return read_terminal && tcgetpgrp(STDERR_FILENO) != getpgrp();
+}
+
+void terminal_get_size(int *w, int *h)
+{
+ struct winsize ws;
+ if (ioctl(tty_in, TIOCGWINSZ, &ws) < 0 || !ws.ws_row || !ws.ws_col)
+ return;
+
+ *w = ws.ws_col;
+ *h = ws.ws_row;
+}
+
+void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height)
+{
+ struct winsize ws;
+ if (ioctl(tty_in, TIOCGWINSZ, &ws) < 0 || !ws.ws_row || !ws.ws_col
+ || !ws.ws_xpixel || !ws.ws_ypixel)
+ return;
+
+ *rows = ws.ws_row;
+ *cols = ws.ws_col;
+ *px_width = ws.ws_xpixel;
+ *px_height = ws.ws_ypixel;
+}
+
+void terminal_init(void)
+{
+ assert(!getch2_enabled);
+ getch2_enabled = 1;
+
+ tty_in = tty_out = open("/dev/tty", O_RDWR | O_CLOEXEC);
+ if (tty_in < 0) {
+ tty_in = STDIN_FILENO;
+ tty_out = STDOUT_FILENO;
+ }
+
+ tcgetattr(tty_in, &tio_orig);
+
+ // handlers to fix terminal settings
+ setsigaction(SIGCONT, continue_sighandler, 0, true);
+ setsigaction(SIGTSTP, stop_sighandler, SA_RESETHAND, false);
+ setsigaction(SIGTTIN, SIG_IGN, 0, true);
+ setsigaction(SIGTTOU, SIG_IGN, 0, true);
+
+ getch2_poll();
+}
diff --git a/osdep/terminal-win.c b/osdep/terminal-win.c
new file mode 100644
index 0000000..8f3410c
--- /dev/null
+++ b/osdep/terminal-win.c
@@ -0,0 +1,425 @@
+/* Windows TermIO
+ *
+ * copyright (C) 2003 Sascha Sommer
+ *
+ * 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 <fcntl.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <windows.h>
+#include <io.h>
+#include <assert.h>
+#include "common/common.h"
+#include "input/keycodes.h"
+#include "input/input.h"
+#include "terminal.h"
+#include "osdep/io.h"
+#include "osdep/threads.h"
+#include "osdep/w32_keyboard.h"
+
+// https://docs.microsoft.com/en-us/windows/console/setconsolemode
+// These values are effective on Windows 10 build 16257 (August 2017) or later
+#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
+ #define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
+#endif
+#ifndef DISABLE_NEWLINE_AUTO_RETURN
+ #define DISABLE_NEWLINE_AUTO_RETURN 0x0008
+#endif
+
+// Note: the DISABLE_NEWLINE_AUTO_RETURN docs say it enables delayed-wrap, but
+// it's wrong. It does only what its names suggests - and we want it unset:
+// https://github.com/microsoft/terminal/issues/4126#issuecomment-571418661
+static void attempt_native_out_vt(HANDLE hOut, DWORD basemode)
+{
+ DWORD vtmode = basemode | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
+ vtmode &= ~DISABLE_NEWLINE_AUTO_RETURN;
+ if (!SetConsoleMode(hOut, vtmode))
+ SetConsoleMode(hOut, basemode);
+}
+
+static bool is_native_out_vt(HANDLE hOut)
+{
+ DWORD cmode;
+ return GetConsoleMode(hOut, &cmode) &&
+ (cmode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) &&
+ !(cmode & DISABLE_NEWLINE_AUTO_RETURN);
+}
+
+#define hSTDOUT GetStdHandle(STD_OUTPUT_HANDLE)
+#define hSTDERR GetStdHandle(STD_ERROR_HANDLE)
+
+#define FOREGROUND_ALL (FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE)
+#define BACKGROUND_ALL (BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE)
+
+static short stdoutAttrs = 0; // copied from the screen buffer on init
+static const unsigned char ansi2win32[8] = {
+ 0,
+ FOREGROUND_RED,
+ FOREGROUND_GREEN,
+ FOREGROUND_GREEN | FOREGROUND_RED,
+ FOREGROUND_BLUE,
+ FOREGROUND_BLUE | FOREGROUND_RED,
+ FOREGROUND_BLUE | FOREGROUND_GREEN,
+ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED,
+};
+static const unsigned char ansi2win32bg[8] = {
+ 0,
+ BACKGROUND_RED,
+ BACKGROUND_GREEN,
+ BACKGROUND_GREEN | BACKGROUND_RED,
+ BACKGROUND_BLUE,
+ BACKGROUND_BLUE | BACKGROUND_RED,
+ BACKGROUND_BLUE | BACKGROUND_GREEN,
+ BACKGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_RED,
+};
+
+static bool running;
+static HANDLE death;
+static mp_thread input_thread;
+static struct input_ctx *input_ctx;
+
+void terminal_get_size(int *w, int *h)
+{
+ CONSOLE_SCREEN_BUFFER_INFO cinfo;
+ HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
+ if (GetConsoleScreenBufferInfo(hOut, &cinfo)) {
+ *w = cinfo.dwMaximumWindowSize.X - (is_native_out_vt(hOut) ? 0 : 1);
+ *h = cinfo.dwMaximumWindowSize.Y;
+ }
+}
+
+void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height)
+{
+}
+
+static bool has_input_events(HANDLE h)
+{
+ DWORD num_events;
+ if (!GetNumberOfConsoleInputEvents(h, &num_events))
+ return false;
+ return !!num_events;
+}
+
+static void read_input(HANDLE in)
+{
+ // Process any input events in the buffer
+ while (has_input_events(in)) {
+ INPUT_RECORD event;
+ if (!ReadConsoleInputW(in, &event, 1, &(DWORD){0}))
+ break;
+
+ // Only key-down events are interesting to us
+ if (event.EventType != KEY_EVENT)
+ continue;
+ KEY_EVENT_RECORD *record = &event.Event.KeyEvent;
+ if (!record->bKeyDown)
+ continue;
+
+ UINT vkey = record->wVirtualKeyCode;
+ bool ext = record->dwControlKeyState & ENHANCED_KEY;
+
+ int mods = 0;
+ if (record->dwControlKeyState & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED))
+ mods |= MP_KEY_MODIFIER_ALT;
+ if (record->dwControlKeyState & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED))
+ mods |= MP_KEY_MODIFIER_CTRL;
+ if (record->dwControlKeyState & SHIFT_PRESSED)
+ mods |= MP_KEY_MODIFIER_SHIFT;
+
+ int mpkey = mp_w32_vkey_to_mpkey(vkey, ext);
+ if (mpkey) {
+ mp_input_put_key(input_ctx, mpkey | mods);
+ } else {
+ // Only characters should be remaining
+ int c = record->uChar.UnicodeChar;
+ // The ctrl key always produces control characters in the console.
+ // Shift them back up to regular characters.
+ if (c > 0 && c < 0x20 && (mods & MP_KEY_MODIFIER_CTRL))
+ c += (mods & MP_KEY_MODIFIER_SHIFT) ? 0x40 : 0x60;
+ if (c >= 0x20)
+ mp_input_put_key(input_ctx, c | mods);
+ }
+ }
+}
+
+static MP_THREAD_VOID input_thread_fn(void *ptr)
+{
+ mp_thread_set_name("terminal/input");
+ HANDLE in = ptr;
+ HANDLE stuff[2] = {in, death};
+ while (1) {
+ DWORD r = WaitForMultipleObjects(2, stuff, FALSE, INFINITE);
+ if (r != WAIT_OBJECT_0)
+ break;
+ read_input(in);
+ }
+ MP_THREAD_RETURN();
+}
+
+void terminal_setup_getch(struct input_ctx *ictx)
+{
+ if (running)
+ return;
+
+ HANDLE in = GetStdHandle(STD_INPUT_HANDLE);
+ if (GetNumberOfConsoleInputEvents(in, &(DWORD){0})) {
+ input_ctx = ictx;
+ death = CreateEventW(NULL, TRUE, FALSE, NULL);
+ if (!death)
+ return;
+ if (mp_thread_create(&input_thread, input_thread_fn, in)) {
+ CloseHandle(death);
+ return;
+ }
+ running = true;
+ }
+}
+
+void terminal_uninit(void)
+{
+ if (running) {
+ SetEvent(death);
+ mp_thread_join(input_thread);
+ input_ctx = NULL;
+ running = false;
+ }
+}
+
+bool terminal_in_background(void)
+{
+ return false;
+}
+
+void mp_write_console_ansi(HANDLE wstream, char *buf)
+{
+ wchar_t *wbuf = mp_from_utf8(NULL, buf);
+ wchar_t *pos = wbuf;
+
+ while (*pos) {
+ if (is_native_out_vt(wstream)) {
+ WriteConsoleW(wstream, pos, wcslen(pos), NULL, NULL);
+ break;
+ }
+ wchar_t *next = wcschr(pos, '\033');
+ if (!next) {
+ WriteConsoleW(wstream, pos, wcslen(pos), NULL, NULL);
+ break;
+ }
+ next[0] = '\0';
+ WriteConsoleW(wstream, pos, wcslen(pos), NULL, NULL);
+ if (next[1] == '[') {
+ // CSI - Control Sequence Introducer
+ next += 2;
+
+ // CSI codes generally follow this syntax:
+ // "\033[" [ <i> (';' <i> )* ] <c>
+ // where <i> are integers, and <c> a single char command code.
+ // Also see: http://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes
+ int params[16]; // 'm' might be unlimited; ignore that
+ int num_params = 0;
+ while (num_params < MP_ARRAY_SIZE(params)) {
+ wchar_t *end = next;
+ long p = wcstol(next, &end, 10);
+ if (end == next)
+ break;
+ next = end;
+ params[num_params++] = p;
+ if (next[0] != ';' || !next[0])
+ break;
+ next += 1;
+ }
+ wchar_t code = next[0];
+ if (code)
+ next += 1;
+ CONSOLE_SCREEN_BUFFER_INFO info;
+ GetConsoleScreenBufferInfo(wstream, &info);
+ switch (code) {
+ case 'K': { // erase to end of line
+ COORD at = info.dwCursorPosition;
+ int len = info.dwSize.X - at.X;
+ FillConsoleOutputCharacterW(wstream, ' ', len, at, &(DWORD){0});
+ SetConsoleCursorPosition(wstream, at);
+ break;
+ }
+ case 'A': { // cursor up
+ info.dwCursorPosition.Y -= 1;
+ SetConsoleCursorPosition(wstream, info.dwCursorPosition);
+ break;
+ }
+ case 'm': { // "SGR"
+ short attr = info.wAttributes;
+ if (num_params == 0) // reset
+ params[num_params++] = 0;
+
+ // we don't emulate italic, reverse/underline don't always work
+ for (int n = 0; n < num_params; n++) {
+ int p = params[n];
+ if (p == 0) {
+ attr = stdoutAttrs;
+ } else if (p == 1) {
+ attr |= FOREGROUND_INTENSITY;
+ } else if (p == 22) {
+ attr &= ~FOREGROUND_INTENSITY;
+ } else if (p == 4) {
+ attr |= COMMON_LVB_UNDERSCORE;
+ } else if (p == 24) {
+ attr &= ~COMMON_LVB_UNDERSCORE;
+ } else if (p == 7) {
+ attr |= COMMON_LVB_REVERSE_VIDEO;
+ } else if (p == 27) {
+ attr &= ~COMMON_LVB_REVERSE_VIDEO;
+ } else if (p >= 30 && p <= 37) {
+ attr &= ~FOREGROUND_ALL;
+ attr |= ansi2win32[p - 30];
+ } else if (p == 39) {
+ attr &= ~FOREGROUND_ALL;
+ attr |= stdoutAttrs & FOREGROUND_ALL;
+ } else if (p >= 40 && p <= 47) {
+ attr &= ~BACKGROUND_ALL;
+ attr |= ansi2win32bg[p - 40];
+ } else if (p == 49) {
+ attr &= ~BACKGROUND_ALL;
+ attr |= stdoutAttrs & BACKGROUND_ALL;
+ } else if (p == 38 || p == 48) { // ignore and skip sub-values
+ // 256 colors: <38/48>;5;N true colors: <38/48>;2;R;G;B
+ if (n+1 < num_params) {
+ n += params[n+1] == 5 ? 2
+ : params[n+1] == 2 ? 4
+ : num_params; /* unrecognized -> the rest */
+ }
+ }
+ }
+
+ if (attr != info.wAttributes)
+ SetConsoleTextAttribute(wstream, attr);
+ break;
+ }
+ }
+ } else if (next[1] == ']') {
+ // OSC - Operating System Commands
+ next += 2;
+
+ // OSC sequences generally follow this syntax:
+ // "\033]" <command> ST
+ // Where <command> is a string command
+ wchar_t *cmd = next;
+ while (next[0]) {
+ // BEL can be used instead of ST in xterm
+ if (next[0] == '\007' || next[0] == 0x9c) {
+ next[0] = '\0';
+ next += 1;
+ break;
+ }
+ if (next[0] == '\033' && next[1] == '\\') {
+ next[0] = '\0';
+ next += 2;
+ break;
+ }
+ next += 1;
+ }
+
+ // Handle xterm-style OSC commands
+ if (cmd[0] && cmd[1] == ';') {
+ wchar_t code = cmd[0];
+ wchar_t *param = cmd + 2;
+
+ switch (code) {
+ case '0': // Change Icon Name and Window Title
+ case '2': // Change Window Title
+ SetConsoleTitleW(param);
+ break;
+ }
+ }
+ } else {
+ WriteConsoleW(wstream, L"\033", 1, NULL, NULL);
+ }
+ pos = next;
+ }
+
+ talloc_free(wbuf);
+}
+
+static bool is_a_console(HANDLE h)
+{
+ return GetConsoleMode(h, &(DWORD){0});
+}
+
+static void reopen_console_handle(DWORD std, int fd, FILE *stream)
+{
+ HANDLE handle = GetStdHandle(std);
+ if (is_a_console(handle)) {
+ if (fd == 0) {
+ freopen("CONIN$", "rt", stream);
+ } else {
+ freopen("CONOUT$", "wt", stream);
+ }
+ setvbuf(stream, NULL, _IONBF, 0);
+
+ // Set the low-level FD to the new handle value, since mp_subprocess2
+ // callers might rely on low-level FDs being set. Note, with this
+ // method, fileno(stdin) != STDIN_FILENO, but that shouldn't matter.
+ int unbound_fd = -1;
+ if (fd == 0) {
+ unbound_fd = _open_osfhandle((intptr_t)handle, _O_RDONLY);
+ } else {
+ unbound_fd = _open_osfhandle((intptr_t)handle, _O_WRONLY);
+ }
+ // dup2 will duplicate the underlying handle. Don't close unbound_fd,
+ // since that will close the original handle.
+ dup2(unbound_fd, fd);
+ }
+}
+
+bool terminal_try_attach(void)
+{
+ // mpv.exe is a flagged as a GUI application, but it acts as a console
+ // application when started from the console wrapper (see
+ // osdep/win32-console-wrapper.c). The console wrapper sets
+ // _started_from_console=yes, so check that variable before trying to
+ // attach to the console.
+ wchar_t console_env[4] = { 0 };
+ if (!GetEnvironmentVariableW(L"_started_from_console", console_env, 4))
+ return false;
+ if (wcsncmp(console_env, L"yes", 4))
+ return false;
+ SetEnvironmentVariableW(L"_started_from_console", NULL);
+
+ if (!AttachConsole(ATTACH_PARENT_PROCESS))
+ return false;
+
+ // We have a console window. Redirect input/output streams to that console's
+ // low-level handles, so things that use stdio work later on.
+ reopen_console_handle(STD_INPUT_HANDLE, STDIN_FILENO, stdin);
+ reopen_console_handle(STD_OUTPUT_HANDLE, STDOUT_FILENO, stdout);
+ reopen_console_handle(STD_ERROR_HANDLE, STDERR_FILENO, stderr);
+
+ return true;
+}
+
+void terminal_init(void)
+{
+ CONSOLE_SCREEN_BUFFER_INFO cinfo;
+ DWORD cmode = 0;
+ GetConsoleMode(hSTDOUT, &cmode);
+ cmode |= (ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT);
+ attempt_native_out_vt(hSTDOUT, cmode);
+ attempt_native_out_vt(hSTDERR, cmode);
+ GetConsoleScreenBufferInfo(hSTDOUT, &cinfo);
+ stdoutAttrs = cinfo.wAttributes;
+}
diff --git a/osdep/terminal.h b/osdep/terminal.h
new file mode 100644
index 0000000..5383a17
--- /dev/null
+++ b/osdep/terminal.h
@@ -0,0 +1,60 @@
+/*
+ * Based on GyS-TermIO v2.0 (for GySmail v3) (copyright (C) 1999 A'rpi/ESP-team)
+ *
+ * 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/>.
+ */
+
+#ifndef MPLAYER_GETCH2_H
+#define MPLAYER_GETCH2_H
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#define TERM_ESC_GOTO_YX "\033[%d;%df"
+#define TERM_ESC_HIDE_CURSOR "\033[?25l"
+#define TERM_ESC_RESTORE_CURSOR "\033[?25h"
+
+#define TERM_ESC_CLEAR_SCREEN "\033[2J"
+#define TERM_ESC_ALT_SCREEN "\033[?1049h"
+#define TERM_ESC_NORMAL_SCREEN "\033[?1049l"
+
+struct input_ctx;
+
+/* Global initialization for terminal output. */
+void terminal_init(void);
+
+/* Setup ictx to read keys from the terminal */
+void terminal_setup_getch(struct input_ctx *ictx);
+
+/* Undo terminal_init(), and also terminal_setup_getch() */
+void terminal_uninit(void);
+
+/* Return whether the process has been backgrounded. */
+bool terminal_in_background(void);
+
+/* Get terminal-size in columns/rows. */
+void terminal_get_size(int *w, int *h);
+
+/* Get terminal-size in columns/rows and width/height in pixels. */
+void terminal_get_size2(int *rows, int *cols, int *px_width, int *px_height);
+
+// Windows only.
+void mp_write_console_ansi(void *wstream, char *buf);
+
+/* Windows-only function to attach to the parent process's console */
+bool terminal_try_attach(void);
+
+#endif /* MPLAYER_GETCH2_H */
diff --git a/osdep/threads-posix.c b/osdep/threads-posix.c
new file mode 100644
index 0000000..0b09a7c
--- /dev/null
+++ b/osdep/threads-posix.c
@@ -0,0 +1,64 @@
+/*
+ * 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 <stdio.h>
+#include <errno.h>
+
+#include "common/common.h"
+#include "config.h"
+#include "threads.h"
+#include "timer.h"
+
+#if HAVE_BSD_THREAD_NAME
+#include <pthread_np.h>
+#endif
+
+int mp_ptwrap_check(const char *file, int line, int res)
+{
+ if (res && res != ETIMEDOUT) {
+ fprintf(stderr, "%s:%d: internal error: pthread result %d (%s)\n",
+ file, line, res, mp_strerror(res));
+ abort();
+ }
+ return res;
+}
+
+int mp_ptwrap_mutex_init(const char *file, int line, pthread_mutex_t *m,
+ const pthread_mutexattr_t *attr)
+{
+ pthread_mutexattr_t m_attr;
+ if (!attr) {
+ attr = &m_attr;
+ pthread_mutexattr_init(&m_attr);
+ // Force normal mutexes to error checking.
+ pthread_mutexattr_settype(&m_attr, PTHREAD_MUTEX_ERRORCHECK);
+ }
+ int res = mp_ptwrap_check(file, line, (pthread_mutex_init)(m, attr));
+ if (attr == &m_attr)
+ pthread_mutexattr_destroy(&m_attr);
+ return res;
+}
+
+int mp_ptwrap_mutex_trylock(const char *file, int line, pthread_mutex_t *m)
+{
+ int res = (pthread_mutex_trylock)(m);
+
+ if (res != EBUSY)
+ mp_ptwrap_check(file, line, res);
+
+ return res;
+}
diff --git a/osdep/threads-posix.h b/osdep/threads-posix.h
new file mode 100644
index 0000000..482e4a8
--- /dev/null
+++ b/osdep/threads-posix.h
@@ -0,0 +1,247 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <errno.h>
+#include <pthread.h>
+#include <stdio.h>
+
+#include "common/common.h"
+#include "config.h"
+#include "osdep/compiler.h"
+#include "timer.h"
+
+int mp_ptwrap_check(const char *file, int line, int res);
+int mp_ptwrap_mutex_init(const char *file, int line, pthread_mutex_t *m,
+ const pthread_mutexattr_t *attr);
+int mp_ptwrap_mutex_trylock(const char *file, int line, pthread_mutex_t *m);
+
+#if HAVE_PTHREAD_DEBUG
+
+// pthread debugging wrappers. Technically, this is undefined behavior, because
+// you are not supposed to define any symbols that clash with reserved names.
+// Other than that, they should be fine.
+
+// Note: mpv normally never checks pthread error return values of certain
+// functions that should never fail. It does so because these cases would
+// be undefined behavior anyway (such as double-frees etc.). However,
+// since there are no good pthread debugging tools, these wrappers are
+// provided for the sake of debugging. They crash on unexpected errors.
+//
+// Technically, pthread_cond/mutex_init() can fail with ENOMEM. We don't
+// really respect this for normal/recursive mutex types, as due to the
+// existence of static initializers, no sane implementation could actually
+// require allocating memory.
+
+#define MP_PTWRAP(fn, ...) \
+ mp_ptwrap_check(__FILE__, __LINE__, (fn)(__VA_ARGS__))
+
+// ISO C defines that all standard functions can be macros, except undef'ing
+// them is allowed and must make the "real" definitions available. (Whatever.)
+#undef pthread_cond_init
+#undef pthread_cond_destroy
+#undef pthread_cond_broadcast
+#undef pthread_cond_signal
+#undef pthread_cond_wait
+#undef pthread_cond_timedwait
+#undef pthread_detach
+#undef pthread_join
+#undef pthread_mutex_destroy
+#undef pthread_mutex_lock
+#undef pthread_mutex_trylock
+#undef pthread_mutex_unlock
+
+#define pthread_cond_init(...) MP_PTWRAP(pthread_cond_init, __VA_ARGS__)
+#define pthread_cond_destroy(...) MP_PTWRAP(pthread_cond_destroy, __VA_ARGS__)
+#define pthread_cond_broadcast(...) MP_PTWRAP(pthread_cond_broadcast, __VA_ARGS__)
+#define pthread_cond_signal(...) MP_PTWRAP(pthread_cond_signal, __VA_ARGS__)
+#define pthread_cond_wait(...) MP_PTWRAP(pthread_cond_wait, __VA_ARGS__)
+#define pthread_cond_timedwait(...) MP_PTWRAP(pthread_cond_timedwait, __VA_ARGS__)
+#define pthread_detach(...) MP_PTWRAP(pthread_detach, __VA_ARGS__)
+#define pthread_join(...) MP_PTWRAP(pthread_join, __VA_ARGS__)
+#define pthread_mutex_destroy(...) MP_PTWRAP(pthread_mutex_destroy, __VA_ARGS__)
+#define pthread_mutex_lock(...) MP_PTWRAP(pthread_mutex_lock, __VA_ARGS__)
+#define pthread_mutex_unlock(...) MP_PTWRAP(pthread_mutex_unlock, __VA_ARGS__)
+
+#define pthread_mutex_init(...) \
+ mp_ptwrap_mutex_init(__FILE__, __LINE__, __VA_ARGS__)
+
+#define pthread_mutex_trylock(...) \
+ mp_ptwrap_mutex_trylock(__FILE__, __LINE__, __VA_ARGS__)
+
+#endif
+
+typedef struct {
+ pthread_cond_t cond;
+ clockid_t clk_id;
+} mp_cond;
+
+typedef pthread_mutex_t mp_mutex;
+typedef pthread_mutex_t mp_static_mutex;
+typedef pthread_once_t mp_once;
+typedef pthread_t mp_thread_id;
+typedef pthread_t mp_thread;
+
+#define MP_STATIC_COND_INITIALIZER (mp_cond){ .cond = PTHREAD_COND_INITIALIZER, .clk_id = CLOCK_REALTIME }
+#define MP_STATIC_MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER
+#define MP_STATIC_ONCE_INITIALIZER PTHREAD_ONCE_INIT
+
+static inline int mp_mutex_init_type_internal(mp_mutex *mutex, enum mp_mutex_type mtype)
+{
+ int mutex_type;
+ switch (mtype) {
+ case MP_MUTEX_RECURSIVE:
+ mutex_type = PTHREAD_MUTEX_RECURSIVE;
+ break;
+ case MP_MUTEX_NORMAL:
+ default:
+#ifndef NDEBUG
+ mutex_type = PTHREAD_MUTEX_ERRORCHECK;
+#else
+ mutex_type = PTHREAD_MUTEX_DEFAULT;
+#endif
+ break;
+ }
+
+ int ret = 0;
+ pthread_mutexattr_t attr;
+ ret = pthread_mutexattr_init(&attr);
+ if (ret != 0)
+ return ret;
+
+ pthread_mutexattr_settype(&attr, mutex_type);
+ ret = pthread_mutex_init(mutex, &attr);
+ pthread_mutexattr_destroy(&attr);
+ assert(!ret);
+ return ret;
+}
+
+#define mp_mutex_destroy pthread_mutex_destroy
+#define mp_mutex_lock pthread_mutex_lock
+#define mp_mutex_trylock pthread_mutex_trylock
+#define mp_mutex_unlock pthread_mutex_unlock
+
+static inline int mp_cond_init(mp_cond *cond)
+{
+ assert(cond);
+
+ int ret = 0;
+ pthread_condattr_t attr;
+ ret = pthread_condattr_init(&attr);
+ if (ret)
+ return ret;
+
+ cond->clk_id = CLOCK_REALTIME;
+#if HAVE_PTHREAD_CONDATTR_SETCLOCK
+ if (!pthread_condattr_setclock(&attr, CLOCK_MONOTONIC))
+ cond->clk_id = CLOCK_MONOTONIC;
+#endif
+
+ ret = pthread_cond_init(&cond->cond, &attr);
+ pthread_condattr_destroy(&attr);
+ return ret;
+}
+
+static inline int mp_cond_destroy(mp_cond *cond)
+{
+ assert(cond);
+ return pthread_cond_destroy(&cond->cond);
+}
+
+static inline int mp_cond_broadcast(mp_cond *cond)
+{
+ assert(cond);
+ return pthread_cond_broadcast(&cond->cond);
+}
+
+static inline int mp_cond_signal(mp_cond *cond)
+{
+ assert(cond);
+ return pthread_cond_signal(&cond->cond);
+}
+
+static inline int mp_cond_wait(mp_cond *cond, mp_mutex *mutex)
+{
+ assert(cond);
+ return pthread_cond_wait(&cond->cond, mutex);
+}
+
+static inline int mp_cond_timedwait(mp_cond *cond, mp_mutex *mutex, int64_t timeout)
+{
+ assert(cond);
+
+ timeout = MPMAX(0, timeout);
+ // consider anything above 1000 days as infinity
+ if (timeout > MP_TIME_S_TO_NS(1000 * 24 * 60 * 60))
+ return pthread_cond_wait(&cond->cond, mutex);
+
+ struct timespec ts;
+ clock_gettime(cond->clk_id, &ts);
+ ts.tv_sec += timeout / MP_TIME_S_TO_NS(1);
+ ts.tv_nsec += timeout % MP_TIME_S_TO_NS(1);
+ if (ts.tv_nsec >= MP_TIME_S_TO_NS(1)) {
+ ts.tv_nsec -= MP_TIME_S_TO_NS(1);
+ ts.tv_sec++;
+ }
+
+ return pthread_cond_timedwait(&cond->cond, mutex, &ts);
+}
+
+static inline int mp_cond_timedwait_until(mp_cond *cond, mp_mutex *mutex, int64_t until)
+{
+ return mp_cond_timedwait(cond, mutex, until - mp_time_ns());
+}
+
+#define mp_exec_once pthread_once
+
+#define MP_THREAD_VOID void *
+#define MP_THREAD_RETURN() return NULL
+
+#define mp_thread_create(t, f, a) pthread_create(t, NULL, f, a)
+#define mp_thread_join(t) pthread_join(t, NULL)
+#define mp_thread_join_id(t) pthread_join(t, NULL)
+#define mp_thread_detach pthread_detach
+#define mp_thread_current_id pthread_self
+#define mp_thread_id_equal(a, b) ((a) == (b))
+#define mp_thread_get_id(thread) (thread)
+
+static inline void mp_thread_set_name(const char *name)
+{
+#if HAVE_GLIBC_THREAD_NAME
+ if (pthread_setname_np(pthread_self(), name) == ERANGE) {
+ char tname[16] = {0}; // glibc-checked kernel limit
+ strncpy(tname, name, sizeof(tname) - 1);
+ pthread_setname_np(pthread_self(), tname);
+ }
+#elif HAVE_BSD_THREAD_NAME
+ pthread_set_name_np(pthread_self(), name);
+#elif HAVE_OSX_THREAD_NAME
+ pthread_setname_np(name);
+#endif
+}
+
+static inline int64_t mp_thread_cpu_time_ns(mp_thread_id thread)
+{
+#if defined(_POSIX_TIMERS) && _POSIX_TIMERS > 0 && defined(_POSIX_THREAD_CPUTIME)
+ clockid_t id;
+ struct timespec ts;
+ if (pthread_getcpuclockid(thread, &id) == 0 && clock_gettime(id, &ts) == 0)
+ return MP_TIME_S_TO_NS(ts.tv_sec) + ts.tv_nsec;
+#endif
+ return 0;
+}
diff --git a/osdep/threads-win32.h b/osdep/threads-win32.h
new file mode 100644
index 0000000..dbce353
--- /dev/null
+++ b/osdep/threads-win32.h
@@ -0,0 +1,224 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <errno.h>
+#include <process.h>
+#include <windows.h>
+
+#include "common/common.h"
+#include "timer.h"
+
+typedef struct {
+ char use_cs;
+ union {
+ CRITICAL_SECTION cs;
+ SRWLOCK srw;
+ };
+} mp_mutex;
+
+typedef CONDITION_VARIABLE mp_cond;
+typedef INIT_ONCE mp_once;
+typedef mp_mutex mp_static_mutex;
+typedef HANDLE mp_thread;
+typedef DWORD mp_thread_id;
+
+#define MP_STATIC_COND_INITIALIZER CONDITION_VARIABLE_INIT
+#define MP_STATIC_MUTEX_INITIALIZER (mp_mutex){ .srw = SRWLOCK_INIT }
+#define MP_STATIC_ONCE_INITIALIZER INIT_ONCE_STATIC_INIT
+
+static inline int mp_mutex_init_type_internal(mp_mutex *mutex, enum mp_mutex_type mtype)
+{
+ mutex->use_cs = mtype == MP_MUTEX_RECURSIVE;
+ if (mutex->use_cs)
+ return !InitializeCriticalSectionEx(&mutex->cs, 0, 0);
+ InitializeSRWLock(&mutex->srw);
+ return 0;
+}
+
+static inline int mp_mutex_destroy(mp_mutex *mutex)
+{
+ if (mutex->use_cs)
+ DeleteCriticalSection(&mutex->cs);
+ return 0;
+}
+
+static inline int mp_mutex_lock(mp_mutex *mutex)
+{
+ if (mutex->use_cs) {
+ EnterCriticalSection(&mutex->cs);
+ } else {
+ AcquireSRWLockExclusive(&mutex->srw);
+ }
+ return 0;
+}
+
+static inline int mp_mutex_trylock(mp_mutex *mutex)
+{
+ if (mutex->use_cs)
+ return !TryEnterCriticalSection(&mutex->cs);
+ return !TryAcquireSRWLockExclusive(&mutex->srw);
+}
+
+static inline int mp_mutex_unlock(mp_mutex *mutex)
+{
+ if (mutex->use_cs) {
+ LeaveCriticalSection(&mutex->cs);
+ } else {
+ ReleaseSRWLockExclusive(&mutex->srw);
+ }
+ return 0;
+}
+
+static inline int mp_cond_init(mp_cond *cond)
+{
+ InitializeConditionVariable(cond);
+ return 0;
+}
+
+static inline int mp_cond_destroy(mp_cond *cond)
+{
+ // condition variables are not destroyed
+ (void) cond;
+ return 0;
+}
+
+static inline int mp_cond_broadcast(mp_cond *cond)
+{
+ WakeAllConditionVariable(cond);
+ return 0;
+}
+
+static inline int mp_cond_signal(mp_cond *cond)
+{
+ WakeConditionVariable(cond);
+ return 0;
+}
+
+static inline int mp_cond_timedwait(mp_cond *cond, mp_mutex *mutex, int64_t timeout)
+{
+ timeout = MPCLAMP(timeout, 0, MP_TIME_MS_TO_NS(INFINITE)) / MP_TIME_MS_TO_NS(1);
+
+ int ret = 0;
+ int hrt = mp_start_hires_timers(timeout);
+ BOOL bRet;
+
+ if (mutex->use_cs) {
+ bRet = SleepConditionVariableCS(cond, &mutex->cs, timeout);
+ } else {
+ bRet = SleepConditionVariableSRW(cond, &mutex->srw, timeout, 0);
+ }
+ if (bRet == FALSE)
+ ret = GetLastError() == ERROR_TIMEOUT ? ETIMEDOUT : EINVAL;
+
+ mp_end_hires_timers(hrt);
+ return ret;
+}
+
+static inline int mp_cond_wait(mp_cond *cond, mp_mutex *mutex)
+{
+ return mp_cond_timedwait(cond, mutex, MP_TIME_MS_TO_NS(INFINITE));
+}
+
+static inline int mp_cond_timedwait_until(mp_cond *cond, mp_mutex *mutex, int64_t until)
+{
+ return mp_cond_timedwait(cond, mutex, until - mp_time_ns());
+}
+
+static inline int mp_exec_once(mp_once *once, void (*init_routine)(void))
+{
+ BOOL pending;
+
+ if (!InitOnceBeginInitialize(once, 0, &pending, NULL))
+ abort();
+
+ if (pending) {
+ init_routine();
+ InitOnceComplete(once, 0, NULL);
+ }
+
+ return 0;
+}
+
+#define MP_THREAD_VOID unsigned __stdcall
+#define MP_THREAD_RETURN() return 0
+
+static inline int mp_thread_create(mp_thread *thread,
+ MP_THREAD_VOID (*fun)(void *),
+ void *__restrict arg)
+{
+ *thread = (HANDLE) _beginthreadex(NULL, 0, fun, arg, 0, NULL);
+ return *thread ? 0 : -1;
+}
+
+static inline int mp_thread_join(mp_thread thread)
+{
+ DWORD ret = WaitForSingleObject(thread, INFINITE);
+ if (ret != WAIT_OBJECT_0)
+ return ret == WAIT_ABANDONED ? EINVAL : EDEADLK;
+ CloseHandle(thread);
+ return 0;
+}
+
+static inline int mp_thread_join_id(mp_thread_id id)
+{
+ mp_thread thread = OpenThread(SYNCHRONIZE, FALSE, id);
+ if (!thread)
+ return ESRCH;
+ int ret = mp_thread_join(thread);
+ if (ret)
+ CloseHandle(thread);
+ return ret;
+}
+
+static inline int mp_thread_detach(mp_thread thread)
+{
+ return CloseHandle(thread) ? 0 : EINVAL;
+}
+
+#define mp_thread_current_id GetCurrentThreadId
+#define mp_thread_id_equal(a, b) ((a) == (b))
+#define mp_thread_get_id(thread) GetThreadId(thread)
+
+// declared in io.h, which we don't want to pull in everywhere
+wchar_t *mp_from_utf8(void *talloc_ctx, const char *s);
+static inline void mp_thread_set_name(const char *name)
+{
+ HRESULT (WINAPI *pSetThreadDescription)(HANDLE, PCWSTR);
+#if !HAVE_UWP
+ HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
+ if (!kernel32)
+ return;
+ pSetThreadDescription = (void *) GetProcAddress(kernel32, "SetThreadDescription");
+ if (!pSetThreadDescription)
+ return;
+#else
+ WINBASEAPI HRESULT WINAPI
+ SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription);
+ pSetThreadDescription = &SetThreadDescription;
+#endif
+ wchar_t *wname = mp_from_utf8(NULL, name);
+ pSetThreadDescription(GetCurrentThread(), wname);
+ talloc_free(wname);
+}
+
+static inline int64_t mp_thread_cpu_time_ns(mp_thread_id thread)
+{
+ (void) thread;
+ return 0;
+}
diff --git a/osdep/threads.h b/osdep/threads.h
new file mode 100644
index 0000000..b6d950e
--- /dev/null
+++ b/osdep/threads.h
@@ -0,0 +1,23 @@
+#ifndef MP_OSDEP_THREADS_H_
+#define MP_OSDEP_THREADS_H_
+
+#include "config.h"
+
+enum mp_mutex_type {
+ MP_MUTEX_NORMAL = 0,
+ MP_MUTEX_RECURSIVE,
+};
+
+#define mp_mutex_init(mutex) \
+ mp_mutex_init_type(mutex, MP_MUTEX_NORMAL)
+
+#define mp_mutex_init_type(mutex, mtype) \
+ mp_mutex_init_type_internal(mutex, mtype)
+
+#if HAVE_WIN32_THREADS
+#include "threads-win32.h"
+#else
+#include "threads-posix.h"
+#endif
+
+#endif
diff --git a/osdep/timer-darwin.c b/osdep/timer-darwin.c
new file mode 100644
index 0000000..bb8a9b4
--- /dev/null
+++ b/osdep/timer-darwin.c
@@ -0,0 +1,48 @@
+/*
+ * Precise timer routines using Mach timing
+ *
+ * Copyright (c) 2003-2004, Dan Villiom Podlaski Christiansen
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use, copy,
+ * modify, merge, publish, distribute, sublicense, and/or sell copies
+ * of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ */
+
+#include <unistd.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <time.h>
+#include <math.h>
+#include <sys/time.h>
+#include <mach/mach_time.h>
+
+#include "common/msg.h"
+#include "timer.h"
+
+static double timebase_ratio_ns;
+
+void mp_sleep_ns(int64_t ns)
+{
+ uint64_t deadline = ns / timebase_ratio_ns + mach_absolute_time();
+ mach_wait_until(deadline);
+}
+
+uint64_t mp_raw_time_ns(void)
+{
+ return mach_absolute_time() * timebase_ratio_ns;
+}
+
+void mp_raw_time_init(void)
+{
+ struct mach_timebase_info timebase;
+
+ mach_timebase_info(&timebase);
+ timebase_ratio_ns = (double)timebase.numer / (double)timebase.denom;
+}
diff --git a/osdep/timer-linux.c b/osdep/timer-linux.c
new file mode 100644
index 0000000..559a496
--- /dev/null
+++ b/osdep/timer-linux.c
@@ -0,0 +1,64 @@
+/*
+ * precise timer routines for Linux/UNIX
+ * copyright (C) LGB & A'rpi/ASTRAL
+ *
+ * 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 <errno.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include "common/common.h"
+#include "timer.h"
+
+static clockid_t clk_id;
+
+void mp_sleep_ns(int64_t ns)
+{
+ if (ns < 0)
+ return;
+ struct timespec ts;
+ ts.tv_sec = ns / MP_TIME_S_TO_NS(1);
+ ts.tv_nsec = ns % MP_TIME_S_TO_NS(1);
+ nanosleep(&ts, NULL);
+}
+
+uint64_t mp_raw_time_ns(void)
+{
+ struct timespec tp = {0};
+ clock_gettime(clk_id, &tp);
+ return MP_TIME_S_TO_NS(tp.tv_sec) + tp.tv_nsec;
+}
+
+void mp_raw_time_init(void)
+{
+ static const clockid_t clock_ids[] = {
+#ifdef CLOCK_MONOTONIC_RAW
+ CLOCK_MONOTONIC_RAW,
+#endif
+ CLOCK_MONOTONIC,
+ };
+
+ struct timespec tp;
+ for (int i = 0; i < MP_ARRAY_SIZE(clock_ids); ++i) {
+ clk_id = clock_ids[i];
+ if (!clock_gettime(clk_id, &tp))
+ return;
+ }
+ fputs("No clock source available!\n", stderr);
+ abort();
+}
diff --git a/osdep/timer-win32.c b/osdep/timer-win32.c
new file mode 100644
index 0000000..7867b5a
--- /dev/null
+++ b/osdep/timer-win32.c
@@ -0,0 +1,141 @@
+/*
+ * 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 <windows.h>
+#include <sys/time.h>
+#include <mmsystem.h>
+#include <stdlib.h>
+#include <versionhelpers.h>
+
+#include "timer.h"
+
+#include "config.h"
+
+static LARGE_INTEGER perf_freq;
+
+// ms values
+static int hires_max = 50;
+static int hires_res = 1;
+
+int mp_start_hires_timers(int wait_ms)
+{
+#if !HAVE_UWP
+ // policy: request hires_res ms resolution if wait < hires_max ms
+ if (wait_ms > 0 && wait_ms <= hires_max &&
+ timeBeginPeriod(hires_res) == TIMERR_NOERROR)
+ {
+ return hires_res;
+ }
+#endif
+ return 0;
+}
+
+void mp_end_hires_timers(int res_ms)
+{
+#if !HAVE_UWP
+ if (res_ms > 0)
+ timeEndPeriod(res_ms);
+#endif
+}
+
+void mp_sleep_ns(int64_t ns)
+{
+ if (ns < 0)
+ return;
+
+ int hrt = mp_start_hires_timers(ns < 1e6 ? 1 : ns / 1e6);
+
+#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
+#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x2
+#endif
+
+ HANDLE timer = CreateWaitableTimerEx(NULL, NULL,
+ CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
+ TIMER_ALL_ACCESS);
+
+ // CREATE_WAITABLE_TIMER_HIGH_RESOLUTION is supported in Windows 10 1803+,
+ // retry without it.
+ if (!timer)
+ timer = CreateWaitableTimerEx(NULL, NULL, 0, TIMER_ALL_ACCESS);
+
+ if (!timer)
+ goto end;
+
+ // Time is expected in 100 nanosecond intervals.
+ // Negative values indicate relative time.
+ LARGE_INTEGER time = (LARGE_INTEGER){ .QuadPart = -(ns / 100) };
+ if (!SetWaitableTimer(timer, &time, 0, NULL, NULL, 0))
+ goto end;
+
+ if (WaitForSingleObject(timer, INFINITE) != WAIT_OBJECT_0)
+ goto end;
+
+end:
+ if (timer)
+ CloseHandle(timer);
+ mp_end_hires_timers(hrt);
+}
+
+uint64_t mp_raw_time_ns(void)
+{
+ LARGE_INTEGER perf_count;
+ QueryPerformanceCounter(&perf_count);
+
+ // Convert QPC units (1/perf_freq seconds) to nanoseconds. This will work
+ // without overflow because the QPC value is guaranteed not to roll-over
+ // within 100 years, so perf_freq must be less than 2.9*10^9.
+ return perf_count.QuadPart / perf_freq.QuadPart * UINT64_C(1000000000) +
+ perf_count.QuadPart % perf_freq.QuadPart * UINT64_C(1000000000) / perf_freq.QuadPart;
+}
+
+void mp_raw_time_init(void)
+{
+ QueryPerformanceFrequency(&perf_freq);
+
+#if !HAVE_UWP
+ // allow (undocumented) control of all the High Res Timers parameters,
+ // for easier experimentation and diagnostic of bug reports.
+ const char *v;
+
+ // 1..1000 ms max timetout for hires (used in "perwait" mode)
+ if ((v = getenv("MPV_HRT_MAX"))) {
+ int hmax = atoi(v);
+ if (hmax >= 1 && hmax <= 1000)
+ hires_max = hmax;
+ }
+
+ // 1..15 ms hires resolution (not used in "never" mode)
+ if ((v = getenv("MPV_HRT_RES"))) {
+ int res = atoi(v);
+ if (res >= 1 && res <= 15)
+ hires_res = res;
+ }
+
+ // "always"/"never"/"perwait" (or "auto" - same as unset)
+ if (!(v = getenv("MPV_HRT")) || !strcmp(v, "auto"))
+ v = IsWindows10OrGreater() ? "perwait" : "always";
+
+ if (!strcmp(v, "perwait")) {
+ // no-op, already per-wait
+ } else if (!strcmp(v, "never")) {
+ hires_max = 0;
+ } else { // "always" or unknown value
+ hires_max = 0;
+ timeBeginPeriod(hires_res);
+ }
+#endif
+}
diff --git a/osdep/timer.c b/osdep/timer.c
new file mode 100644
index 0000000..d0a8a92
--- /dev/null
+++ b/osdep/timer.c
@@ -0,0 +1,67 @@
+/*
+ * 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 <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/time.h>
+#include <limits.h>
+#include <assert.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/random.h"
+#include "threads.h"
+#include "timer.h"
+
+static uint64_t raw_time_offset;
+static mp_once timer_init_once = MP_STATIC_ONCE_INITIALIZER;
+
+static void do_timer_init(void)
+{
+ mp_raw_time_init();
+ mp_rand_seed(mp_raw_time_ns());
+ raw_time_offset = mp_raw_time_ns();
+ assert(raw_time_offset > 0);
+}
+
+void mp_time_init(void)
+{
+ mp_exec_once(&timer_init_once, do_timer_init);
+}
+
+int64_t mp_time_ns(void)
+{
+ return mp_raw_time_ns() - raw_time_offset;
+}
+
+double mp_time_sec(void)
+{
+ return mp_time_ns() / 1e9;
+}
+
+int64_t mp_time_ns_add(int64_t time_ns, double timeout_sec)
+{
+ assert(time_ns > 0); // mp_time_ns() returns strictly positive values
+ double t = MPCLAMP(timeout_sec * 1e9, -0x1p63, 0x1p63);
+ int64_t ti = t == 0x1p63 ? INT64_MAX : (int64_t)t;
+ if (ti > INT64_MAX - time_ns)
+ return INT64_MAX;
+ if (ti <= -time_ns)
+ return 1;
+ return time_ns + ti;
+}
diff --git a/osdep/timer.h b/osdep/timer.h
new file mode 100644
index 0000000..3a925ca
--- /dev/null
+++ b/osdep/timer.h
@@ -0,0 +1,63 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MPLAYER_TIMER_H
+#define MPLAYER_TIMER_H
+
+#include <inttypes.h>
+
+// Initialize timer, must be called at least once at start.
+void mp_time_init(void);
+
+// Return time in nanoseconds. Never wraps. Never returns 0 or negative values.
+int64_t mp_time_ns(void);
+
+// Return time in seconds. Can have down to 1 nanosecond resolution, but will
+// be much worse when casted to float.
+double mp_time_sec(void);
+
+// Provided by OS specific functions (timer-linux.c)
+void mp_raw_time_init(void);
+// ensure this doesn't return 0
+uint64_t mp_raw_time_ns(void);
+
+// Sleep in nanoseconds.
+void mp_sleep_ns(int64_t ns);
+
+#ifdef _WIN32
+// returns: timer resolution in ms if needed and started successfully, else 0
+int mp_start_hires_timers(int wait_ms);
+
+// call unconditionally with the return value of mp_start_hires_timers
+void mp_end_hires_timers(int resolution_ms);
+#endif /* _WIN32 */
+
+// Converts time units to nanoseconds (int64_t)
+#define MP_TIME_S_TO_NS(s) ((s) * INT64_C(1000000000))
+#define MP_TIME_MS_TO_NS(ms) ((ms) * INT64_C(1000000))
+#define MP_TIME_US_TO_NS(us) ((us) * INT64_C(1000))
+
+// Converts nanoseconds to specified time unit (double)
+#define MP_TIME_NS_TO_S(ns) ((ns) / (double)1000000000)
+#define MP_TIME_NS_TO_MS(ns) ((ns) / (double)1000000)
+#define MP_TIME_NS_TO_US(ns) ((ns) / (double)1000)
+
+// Add a time in seconds to the given time in nanoseconds, and return it.
+// Takes care of possible overflows. Never returns a negative or 0 time.
+int64_t mp_time_ns_add(int64_t time_ns, double timeout_sec);
+
+#endif /* MPLAYER_TIMER_H */
diff --git a/osdep/w32_keyboard.c b/osdep/w32_keyboard.c
new file mode 100644
index 0000000..52221e6
--- /dev/null
+++ b/osdep/w32_keyboard.c
@@ -0,0 +1,123 @@
+/*
+ * 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 <windows.h>
+#include "osdep/w32_keyboard.h"
+#include "input/keycodes.h"
+
+struct keymap {
+ int from;
+ int to;
+};
+
+static const struct keymap vk_map_ext[] = {
+ // cursor keys
+ {VK_LEFT, MP_KEY_LEFT}, {VK_UP, MP_KEY_UP}, {VK_RIGHT, MP_KEY_RIGHT},
+ {VK_DOWN, MP_KEY_DOWN},
+
+ // navigation block
+ {VK_INSERT, MP_KEY_INSERT}, {VK_DELETE, MP_KEY_DELETE},
+ {VK_HOME, MP_KEY_HOME}, {VK_END, MP_KEY_END}, {VK_PRIOR, MP_KEY_PAGE_UP},
+ {VK_NEXT, MP_KEY_PAGE_DOWN},
+
+ // numpad independent of numlock
+ {VK_RETURN, MP_KEY_KPENTER},
+
+ {0, 0}
+};
+
+static const struct keymap vk_map[] = {
+ // special keys
+ {VK_ESCAPE, MP_KEY_ESC}, {VK_BACK, MP_KEY_BS}, {VK_TAB, MP_KEY_TAB},
+ {VK_RETURN, MP_KEY_ENTER}, {VK_PAUSE, MP_KEY_PAUSE},
+ {VK_SLEEP, MP_KEY_SLEEP}, {VK_SNAPSHOT, MP_KEY_PRINT},
+ {VK_APPS, MP_KEY_MENU},
+
+ // F-keys
+ {VK_F1, MP_KEY_F+1}, {VK_F2, MP_KEY_F+2}, {VK_F3, MP_KEY_F+3},
+ {VK_F4, MP_KEY_F+4}, {VK_F5, MP_KEY_F+5}, {VK_F6, MP_KEY_F+6},
+ {VK_F7, MP_KEY_F+7}, {VK_F8, MP_KEY_F+8}, {VK_F9, MP_KEY_F+9},
+ {VK_F10, MP_KEY_F+10}, {VK_F11, MP_KEY_F+11}, {VK_F12, MP_KEY_F+12},
+ {VK_F13, MP_KEY_F+13}, {VK_F14, MP_KEY_F+14}, {VK_F15, MP_KEY_F+15},
+ {VK_F16, MP_KEY_F+16}, {VK_F17, MP_KEY_F+17}, {VK_F18, MP_KEY_F+18},
+ {VK_F19, MP_KEY_F+19}, {VK_F20, MP_KEY_F+20}, {VK_F21, MP_KEY_F+21},
+ {VK_F22, MP_KEY_F+22}, {VK_F23, MP_KEY_F+23}, {VK_F24, MP_KEY_F+24},
+
+ // numpad with numlock
+ {VK_NUMPAD0, MP_KEY_KP0}, {VK_NUMPAD1, MP_KEY_KP1},
+ {VK_NUMPAD2, MP_KEY_KP2}, {VK_NUMPAD3, MP_KEY_KP3},
+ {VK_NUMPAD4, MP_KEY_KP4}, {VK_NUMPAD5, MP_KEY_KP5},
+ {VK_NUMPAD6, MP_KEY_KP6}, {VK_NUMPAD7, MP_KEY_KP7},
+ {VK_NUMPAD8, MP_KEY_KP8}, {VK_NUMPAD9, MP_KEY_KP9},
+ {VK_DECIMAL, MP_KEY_KPDEC},
+
+ // numpad without numlock
+ {VK_INSERT, MP_KEY_KPINS}, {VK_END, MP_KEY_KPEND}, {VK_DOWN, MP_KEY_KPDOWN},
+ {VK_NEXT, MP_KEY_KPPGDOWN}, {VK_LEFT, MP_KEY_KPLEFT}, {VK_CLEAR, MP_KEY_KP5},
+ {VK_RIGHT, MP_KEY_KPRIGHT}, {VK_HOME, MP_KEY_KPHOME}, {VK_UP, MP_KEY_KPUP},
+ {VK_PRIOR, MP_KEY_KPPGUP}, {VK_DELETE, MP_KEY_KPDEL},
+
+ {0, 0}
+};
+
+static const struct keymap appcmd_map[] = {
+ {APPCOMMAND_MEDIA_NEXTTRACK, MP_KEY_NEXT},
+ {APPCOMMAND_MEDIA_PREVIOUSTRACK, MP_KEY_PREV},
+ {APPCOMMAND_MEDIA_STOP, MP_KEY_STOP},
+ {APPCOMMAND_MEDIA_PLAY_PAUSE, MP_KEY_PLAYPAUSE},
+ {APPCOMMAND_MEDIA_PLAY, MP_KEY_PLAY},
+ {APPCOMMAND_MEDIA_PAUSE, MP_KEY_PAUSE},
+ {APPCOMMAND_MEDIA_RECORD, MP_KEY_RECORD},
+ {APPCOMMAND_MEDIA_FAST_FORWARD, MP_KEY_FORWARD},
+ {APPCOMMAND_MEDIA_REWIND, MP_KEY_REWIND},
+ {APPCOMMAND_MEDIA_CHANNEL_UP, MP_KEY_CHANNEL_UP},
+ {APPCOMMAND_MEDIA_CHANNEL_DOWN, MP_KEY_CHANNEL_DOWN},
+ {APPCOMMAND_VOLUME_MUTE, MP_KEY_MUTE},
+ {APPCOMMAND_VOLUME_DOWN, MP_KEY_VOLUME_DOWN},
+ {APPCOMMAND_VOLUME_UP, MP_KEY_VOLUME_UP},
+ {APPCOMMAND_BROWSER_HOME, MP_KEY_HOMEPAGE},
+ {APPCOMMAND_LAUNCH_MAIL, MP_KEY_MAIL},
+ {APPCOMMAND_BROWSER_FAVORITES, MP_KEY_FAVORITES},
+ {APPCOMMAND_BROWSER_SEARCH, MP_KEY_SEARCH},
+ {0, 0}
+};
+
+static int lookup_keymap(const struct keymap *map, int key)
+{
+ while (map->from && map->from != key) map++;
+ return map->to;
+}
+
+int mp_w32_vkey_to_mpkey(UINT vkey, bool extended)
+{
+ // The extended flag is set for the navigation cluster and the arrow keys,
+ // so it can be used to differentiate between them and the numpad. The
+ // numpad enter key also has this flag set.
+ int mpkey = lookup_keymap(extended ? vk_map_ext : vk_map, vkey);
+
+ // If we got the extended flag for a key we don't recognize, search the
+ // normal keymap before giving up
+ if (extended && !mpkey)
+ mpkey = lookup_keymap(vk_map, vkey);
+
+ return mpkey;
+}
+
+int mp_w32_appcmd_to_mpkey(UINT appcmd)
+{
+ return lookup_keymap(appcmd_map, appcmd);
+}
diff --git a/osdep/w32_keyboard.h b/osdep/w32_keyboard.h
new file mode 100644
index 0000000..b06cdee
--- /dev/null
+++ b/osdep/w32_keyboard.h
@@ -0,0 +1,29 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_W32_KEYBOARD
+#define MP_W32_KEYBOARD
+
+#include <stdbool.h>
+
+/* Convert a Windows virtual key code to an mpv key */
+int mp_w32_vkey_to_mpkey(UINT vkey, bool extended);
+
+/* Convert a WM_APPCOMMAND value to an mpv key */
+int mp_w32_appcmd_to_mpkey(UINT appcmd);
+
+#endif
diff --git a/osdep/win32-console-wrapper.c b/osdep/win32-console-wrapper.c
new file mode 100644
index 0000000..4e74dac
--- /dev/null
+++ b/osdep/win32-console-wrapper.c
@@ -0,0 +1,89 @@
+/*
+ * conredir, a hack to get working console IO with Windows GUI applications
+ *
+ * Copyright (c) 2013, Martin Herkt
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <windows.h>
+
+int wmain(int argc, wchar_t **argv, wchar_t **envp);
+
+static void cr_perror(const wchar_t *prefix)
+{
+ wchar_t *error;
+
+ FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, GetLastError(),
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ (LPWSTR)&error, 0, NULL);
+
+ fwprintf(stderr, L"%s: %s", prefix, error);
+ LocalFree(error);
+}
+
+static int cr_runproc(wchar_t *name, wchar_t *cmdline)
+{
+ STARTUPINFOW si;
+ STARTUPINFOW our_si;
+ PROCESS_INFORMATION pi;
+ DWORD retval = 1;
+
+ ZeroMemory(&si, sizeof(si));
+ si.cb = sizeof(si);
+ si.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
+ si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+ si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
+ si.dwFlags |= STARTF_USESTDHANDLES;
+
+ // Copy the list of inherited CRT file descriptors to the new process
+ our_si.cb = sizeof(our_si);
+ GetStartupInfoW(&our_si);
+ si.lpReserved2 = our_si.lpReserved2;
+ si.cbReserved2 = our_si.cbReserved2;
+
+ ZeroMemory(&pi, sizeof(pi));
+
+ if (!CreateProcessW(name, cmdline, NULL, NULL, TRUE, 0,
+ NULL, NULL, &si, &pi)) {
+
+ cr_perror(L"CreateProcess");
+ } else {
+ WaitForSingleObject(pi.hProcess, INFINITE);
+ GetExitCodeProcess(pi.hProcess, &retval);
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ }
+
+ return (int)retval;
+}
+
+int wmain(int argc, wchar_t **argv, wchar_t **envp)
+{
+ wchar_t *cmd;
+ wchar_t exe[MAX_PATH];
+
+ cmd = GetCommandLineW();
+ GetModuleFileNameW(NULL, exe, MAX_PATH);
+ wcscpy(wcsrchr(exe, '.') + 1, L"exe");
+
+ // Set an environment variable so the child process can tell whether it
+ // was started from this wrapper and attach to the console accordingly
+ SetEnvironmentVariableW(L"_started_from_console", L"yes");
+
+ return cr_runproc(exe, cmd);
+}
diff --git a/osdep/windows_utils.c b/osdep/windows_utils.c
new file mode 100644
index 0000000..8cedf93
--- /dev/null
+++ b/osdep/windows_utils.c
@@ -0,0 +1,229 @@
+/*
+ * 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 <inttypes.h>
+#include <stdatomic.h>
+#include <stdio.h>
+
+#include <windows.h>
+#include <errors.h>
+#include <audioclient.h>
+#include <d3d9.h>
+#include <dxgi1_2.h>
+
+#include "common/common.h"
+#include "windows_utils.h"
+
+char *mp_GUID_to_str_buf(char *buf, size_t buf_size, const GUID *guid)
+{
+ snprintf(buf, buf_size,
+ "{%8.8x-%4.4x-%4.4x-%2.2x%2.2x-%2.2x%2.2x%2.2x%2.2x%2.2x%2.2x}",
+ (unsigned) guid->Data1, guid->Data2, guid->Data3,
+ guid->Data4[0], guid->Data4[1],
+ guid->Data4[2], guid->Data4[3],
+ guid->Data4[4], guid->Data4[5],
+ guid->Data4[6], guid->Data4[7]);
+ return buf;
+}
+
+static char *hresult_to_str(const HRESULT hr)
+{
+#define E(x) case x : return # x ;
+ switch (hr) {
+ E(S_OK)
+ E(S_FALSE)
+ E(E_FAIL)
+ E(E_OUTOFMEMORY)
+ E(E_POINTER)
+ E(E_HANDLE)
+ E(E_NOTIMPL)
+ E(E_INVALIDARG)
+ E(E_PROP_ID_UNSUPPORTED)
+ E(E_NOINTERFACE)
+ E(REGDB_E_IIDNOTREG)
+ E(CO_E_NOTINITIALIZED)
+ E(AUDCLNT_E_NOT_INITIALIZED)
+ E(AUDCLNT_E_ALREADY_INITIALIZED)
+ E(AUDCLNT_E_WRONG_ENDPOINT_TYPE)
+ E(AUDCLNT_E_DEVICE_INVALIDATED)
+ E(AUDCLNT_E_NOT_STOPPED)
+ E(AUDCLNT_E_BUFFER_TOO_LARGE)
+ E(AUDCLNT_E_OUT_OF_ORDER)
+ E(AUDCLNT_E_UNSUPPORTED_FORMAT)
+ E(AUDCLNT_E_INVALID_SIZE)
+ E(AUDCLNT_E_DEVICE_IN_USE)
+ E(AUDCLNT_E_BUFFER_OPERATION_PENDING)
+ E(AUDCLNT_E_THREAD_NOT_REGISTERED)
+ E(AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED)
+ E(AUDCLNT_E_ENDPOINT_CREATE_FAILED)
+ E(AUDCLNT_E_SERVICE_NOT_RUNNING)
+ E(AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED)
+ E(AUDCLNT_E_EXCLUSIVE_MODE_ONLY)
+ E(AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL)
+ E(AUDCLNT_E_EVENTHANDLE_NOT_SET)
+ E(AUDCLNT_E_INCORRECT_BUFFER_SIZE)
+ E(AUDCLNT_E_BUFFER_SIZE_ERROR)
+ E(AUDCLNT_E_CPUUSAGE_EXCEEDED)
+ E(AUDCLNT_E_BUFFER_ERROR)
+ E(AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED)
+ E(AUDCLNT_E_INVALID_DEVICE_PERIOD)
+ E(AUDCLNT_E_INVALID_STREAM_FLAG)
+ E(AUDCLNT_E_ENDPOINT_OFFLOAD_NOT_CAPABLE)
+ E(AUDCLNT_E_RESOURCES_INVALIDATED)
+ E(AUDCLNT_S_BUFFER_EMPTY)
+ E(AUDCLNT_S_THREAD_ALREADY_REGISTERED)
+ E(AUDCLNT_S_POSITION_STALLED)
+ E(D3DERR_WRONGTEXTUREFORMAT)
+ E(D3DERR_UNSUPPORTEDCOLOROPERATION)
+ E(D3DERR_UNSUPPORTEDCOLORARG)
+ E(D3DERR_UNSUPPORTEDALPHAOPERATION)
+ E(D3DERR_UNSUPPORTEDALPHAARG)
+ E(D3DERR_TOOMANYOPERATIONS)
+ E(D3DERR_CONFLICTINGTEXTUREFILTER)
+ E(D3DERR_UNSUPPORTEDFACTORVALUE)
+ E(D3DERR_CONFLICTINGRENDERSTATE)
+ E(D3DERR_UNSUPPORTEDTEXTUREFILTER)
+ E(D3DERR_CONFLICTINGTEXTUREPALETTE)
+ E(D3DERR_DRIVERINTERNALERROR)
+ E(D3DERR_NOTFOUND)
+ E(D3DERR_MOREDATA)
+ E(D3DERR_DEVICELOST)
+ E(D3DERR_DEVICENOTRESET)
+ E(D3DERR_NOTAVAILABLE)
+ E(D3DERR_OUTOFVIDEOMEMORY)
+ E(D3DERR_INVALIDDEVICE)
+ E(D3DERR_INVALIDCALL)
+ E(D3DERR_DRIVERINVALIDCALL)
+ E(D3DERR_WASSTILLDRAWING)
+ E(D3DOK_NOAUTOGEN)
+ E(D3DERR_DEVICEREMOVED)
+ E(D3DERR_DEVICEHUNG)
+ E(S_NOT_RESIDENT)
+ E(S_RESIDENT_IN_SHARED_MEMORY)
+ E(S_PRESENT_MODE_CHANGED)
+ E(S_PRESENT_OCCLUDED)
+ E(D3DERR_UNSUPPORTEDOVERLAY)
+ E(D3DERR_UNSUPPORTEDOVERLAYFORMAT)
+ E(D3DERR_CANNOTPROTECTCONTENT)
+ E(D3DERR_UNSUPPORTEDCRYPTO)
+ E(D3DERR_PRESENT_STATISTICS_DISJOINT)
+ E(DXGI_ERROR_DEVICE_HUNG)
+ E(DXGI_ERROR_DEVICE_REMOVED)
+ E(DXGI_ERROR_DEVICE_RESET)
+ E(DXGI_ERROR_DRIVER_INTERNAL_ERROR)
+ E(DXGI_ERROR_INVALID_CALL)
+ E(DXGI_ERROR_WAS_STILL_DRAWING)
+ E(DXGI_STATUS_OCCLUDED)
+ default:
+ return "<Unknown>";
+ }
+#undef E
+}
+
+static char *fmtmsg_buf(char *buf, size_t buf_size, DWORD errorID)
+{
+ DWORD n = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, errorID, 0, buf, buf_size, NULL);
+ if (!n && GetLastError() == ERROR_MORE_DATA) {
+ snprintf(buf, buf_size,
+ "<Insufficient buffer size (%zd) for error message>",
+ buf_size);
+ } else {
+ if (n > 0 && buf[n-1] == '\n')
+ buf[n-1] = '\0';
+ if (n > 1 && buf[n-2] == '\r')
+ buf[n-2] = '\0';
+ }
+ return buf;
+}
+#define fmtmsg(hr) fmtmsg_buf((char[243]){0}, 243, (hr))
+
+char *mp_HRESULT_to_str_buf(char *buf, size_t buf_size, HRESULT hr)
+{
+ char* msg = fmtmsg(hr);
+ msg = msg[0] ? msg : hresult_to_str(hr);
+ snprintf(buf, buf_size, "%s (0x%"PRIx32")", msg, (uint32_t)hr);
+ return buf;
+}
+
+bool mp_w32_create_anon_pipe(HANDLE *server, HANDLE *client,
+ struct w32_create_anon_pipe_opts *opts)
+{
+ static atomic_ulong counter = 0;
+
+ // Generate pipe name
+ unsigned long id = atomic_fetch_add(&counter, 1);
+ unsigned pid = GetCurrentProcessId();
+ wchar_t buf[36];
+ swprintf(buf, MP_ARRAY_SIZE(buf), L"\\\\.\\pipe\\mpv-anon-%08x-%08lx",
+ pid, id);
+
+ DWORD client_access = 0;
+ DWORD out_buffer = opts->out_buf_size;
+ DWORD in_buffer = opts->in_buf_size;
+
+ if (opts->server_flags & PIPE_ACCESS_INBOUND) {
+ client_access |= FILE_GENERIC_WRITE | FILE_READ_ATTRIBUTES;
+ if (!in_buffer)
+ in_buffer = 4096;
+ }
+ if (opts->server_flags & PIPE_ACCESS_OUTBOUND) {
+ client_access |= FILE_GENERIC_READ | FILE_WRITE_ATTRIBUTES;
+ if (!out_buffer)
+ out_buffer = 4096;
+ }
+
+ SECURITY_ATTRIBUTES inherit_sa = {
+ .nLength = sizeof inherit_sa,
+ .bInheritHandle = TRUE,
+ };
+
+ // The function for creating anonymous pipes (CreatePipe) can't create
+ // overlapped pipes, so instead, use a named pipe with a unique name
+ *server = CreateNamedPipeW(buf,
+ opts->server_flags | FILE_FLAG_FIRST_PIPE_INSTANCE,
+ opts->server_mode | PIPE_REJECT_REMOTE_CLIENTS,
+ 1, out_buffer, in_buffer, 0,
+ opts->server_inheritable ? &inherit_sa : NULL);
+ if (*server == INVALID_HANDLE_VALUE)
+ goto error;
+
+ // Open the write end of the pipe as a synchronous handle
+ *client = CreateFileW(buf, client_access, 0,
+ opts->client_inheritable ? &inherit_sa : NULL,
+ OPEN_EXISTING,
+ opts->client_flags | SECURITY_SQOS_PRESENT |
+ SECURITY_ANONYMOUS, NULL);
+ if (*client == INVALID_HANDLE_VALUE) {
+ CloseHandle(*server);
+ goto error;
+ }
+
+ if (opts->client_mode) {
+ if (!SetNamedPipeHandleState(*client, &opts->client_mode, NULL, NULL)) {
+ CloseHandle(*server);
+ CloseHandle(*client);
+ goto error;
+ }
+ }
+
+ return true;
+error:
+ *server = *client = INVALID_HANDLE_VALUE;
+ return false;
+}
diff --git a/osdep/windows_utils.h b/osdep/windows_utils.h
new file mode 100644
index 0000000..a8a5e94
--- /dev/null
+++ b/osdep/windows_utils.h
@@ -0,0 +1,49 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MP_WINDOWS_UTILS_H_
+#define MP_WINDOWS_UTILS_H_
+
+#include <windows.h>
+#include <stdbool.h>
+
+// Conditionally release a COM interface and set the pointer to NULL
+#define SAFE_RELEASE(u) \
+ do { if ((u) != NULL) (u)->lpVtbl->Release(u); (u) = NULL; } while(0)
+
+char *mp_GUID_to_str_buf(char *buf, size_t buf_size, const GUID *guid);
+#define mp_GUID_to_str(guid) mp_GUID_to_str_buf((char[40]){0}, 40, (guid))
+char *mp_HRESULT_to_str_buf(char *buf, size_t buf_size, HRESULT hr);
+#define mp_HRESULT_to_str(hr) mp_HRESULT_to_str_buf((char[256]){0}, 256, (hr))
+#define mp_LastError_to_str() mp_HRESULT_to_str(HRESULT_FROM_WIN32(GetLastError()))
+
+struct w32_create_anon_pipe_opts {
+ DWORD server_flags;
+ DWORD server_mode;
+ bool server_inheritable;
+ DWORD out_buf_size;
+ DWORD in_buf_size;
+
+ DWORD client_flags;
+ DWORD client_mode;
+ bool client_inheritable;
+};
+
+bool mp_w32_create_anon_pipe(HANDLE *server, HANDLE *client,
+ struct w32_create_anon_pipe_opts *opts);
+
+#endif