diff options
Diffstat (limited to 'osdep')
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, ¶ms) < 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, ¶ms); + } + } 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, ¶m) + 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 |