summaryrefslogtreecommitdiffstats
path: root/osdep/mac
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 01:13:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 01:13:14 +0000
commit60e8a3d404f0640fa5a3f834eae54b4f1fb9127d (patch)
tree1da89a218d0ecf010c67a87cb2f625c4cb18e7d7 /osdep/mac
parentAdding upstream version 0.37.0. (diff)
downloadmpv-upstream.tar.xz
mpv-upstream.zip
Adding upstream version 0.38.0.upstream/0.38.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'osdep/mac')
-rw-r--r--osdep/mac/app_bridge.h59
-rw-r--r--osdep/mac/app_bridge.m123
-rw-r--r--osdep/mac/app_bridge_objc.h56
-rw-r--r--osdep/mac/app_hub.swift130
-rw-r--r--osdep/mac/application.swift123
-rw-r--r--osdep/mac/event_helper.swift147
-rw-r--r--osdep/mac/input_helper.swift275
-rw-r--r--osdep/mac/libmpv_helper.swift181
-rw-r--r--osdep/mac/log_helper.swift67
-rw-r--r--osdep/mac/menu_bar.swift397
-rw-r--r--osdep/mac/meson.build63
-rw-r--r--osdep/mac/option_helper.swift74
-rw-r--r--osdep/mac/precise_timer.swift152
-rw-r--r--osdep/mac/presentation.swift56
-rw-r--r--osdep/mac/remote_command_center.swift202
-rw-r--r--osdep/mac/swift_compat.swift36
-rw-r--r--osdep/mac/swift_extensions.swift93
-rw-r--r--osdep/mac/touch_bar.swift297
-rw-r--r--osdep/mac/type_helper.swift69
19 files changed, 2600 insertions, 0 deletions
diff --git a/osdep/mac/app_bridge.h b/osdep/mac/app_bridge.h
new file mode 100644
index 0000000..db03c8e
--- /dev/null
+++ b/osdep/mac/app_bridge.h
@@ -0,0 +1,59 @@
+/*
+ * 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 <stdbool.h>
+#include "options/m_option.h"
+
+struct input_ctx;
+struct mpv_handle;
+
+enum {
+ FRAME_VISIBLE = 0,
+ FRAME_WHOLE,
+};
+
+enum {
+ RENDER_TIMER_PRESENTATION_FEEDBACK = -1,
+ RENDER_TIMER_SYSTEM,
+ RENDER_TIMER_CALLBACK,
+ RENDER_TIMER_PRECISE,
+};
+
+struct macos_opts {
+ 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;
+};
+
+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);
+void cocoa_init_cocoa_cb(void);
+// multithreaded wrapper for mpv_main
+int cocoa_main(int argc, char *argv[]);
+
+extern const struct m_sub_options macos_conf;
diff --git a/osdep/mac/app_bridge.m b/osdep/mac/app_bridge.m
new file mode 100644
index 0000000..bf39efe
--- /dev/null
+++ b/osdep/mac/app_bridge.m
@@ -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 "config.h"
+
+#import "osdep/mac/app_bridge_objc.h"
+
+#if HAVE_SWIFT
+#include "osdep/mac/swift.h"
+#endif
+
+#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}, {"feedback", RENDER_TIMER_PRESENTATION_FEEDBACK})},
+ {"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,
+ .macos_render_timer = RENDER_TIMER_CALLBACK,
+ .cocoa_cb_sw_renderer = -1,
+ .cocoa_cb_10bit_context = true
+ },
+};
+
+static const char app_icon[] =
+#include "TOOLS/osxbundle/icon.icns.inc"
+;
+
+NSData *app_bridge_icon(void)
+{
+ return [NSData dataWithBytesNoCopy:(void *)app_icon length:sizeof(app_icon) - 1 freeWhenDone:NO];
+}
+
+void app_bridge_tarray_append(void *t, char ***a, int *i, char *s)
+{
+ MP_TARRAY_APPEND(t, *a, *i, s);
+}
+
+const struct m_sub_options *app_bridge_mac_conf(void)
+{
+ return &macos_conf;
+}
+
+const struct m_sub_options *app_bridge_vo_conf(void)
+{
+ return &vo_sub_opts;
+}
+
+void cocoa_init_media_keys(void)
+{
+ [[AppHub shared] startRemote];
+}
+
+void cocoa_uninit_media_keys(void)
+{
+ [[AppHub shared] stopRemote];
+}
+
+void cocoa_set_input_context(struct input_ctx *input_context)
+{
+ [[AppHub shared] initInput:input_context];
+}
+
+void cocoa_set_mpv_handle(struct mpv_handle *ctx)
+{
+ [[AppHub shared] initMpv:ctx];
+}
+
+void cocoa_init_cocoa_cb(void)
+{
+ [[AppHub shared] initCocoaCb];
+}
+
+int cocoa_main(int argc, char *argv[])
+{
+ return [(Application *)[Application sharedApplication] main:argc :argv];
+}
+
diff --git a/osdep/mac/app_bridge_objc.h b/osdep/mac/app_bridge_objc.h
new file mode 100644
index 0000000..6020198
--- /dev/null
+++ b/osdep/mac/app_bridge_objc.h
@@ -0,0 +1,56 @@
+/*
+ * 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 <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 "input/event.h"
+#include "input/keycodes.h"
+#include "video/out/win_state.h"
+
+#include "osdep/main-fn.h"
+#include "osdep/mac/app_bridge.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;
+
+static const char *swift_mpv_version = mpv_version;
+static const char *swift_mpv_copyright = mpv_copyright;
+
+NSData *app_bridge_icon(void);
+void app_bridge_tarray_append(void *t, char ***a, int *i, char *s);
+const struct m_sub_options *app_bridge_mac_conf(void);
+const struct m_sub_options *app_bridge_vo_conf(void);
diff --git a/osdep/mac/app_hub.swift b/osdep/mac/app_hub.swift
new file mode 100644
index 0000000..997cc33
--- /dev/null
+++ b/osdep/mac/app_hub.swift
@@ -0,0 +1,130 @@
+/*
+ * 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 AppHub: NSObject {
+ @objc static let shared = AppHub()
+
+ var mpv: OpaquePointer?
+ var input: InputHelper
+ var log: LogHelper
+ var option: OptionHelper?
+ var event: EventHelper?
+ var menu: MenuBar?
+#if HAVE_MACOS_MEDIA_PLAYER
+ var remote: RemoteCommandCenter?
+#endif
+#if HAVE_MACOS_TOUCHBAR
+ var touchBar: TouchBar?
+#endif
+#if HAVE_MACOS_COCOA_CB
+ var cocoaCb: CocoaCB?
+#endif
+
+ let MPV_PROTOCOL: String = "mpv://"
+ var isApplication: Bool { get { NSApp is Application } }
+ var openEvents: Int = 0
+
+ private override init() {
+ input = InputHelper()
+ log = LogHelper()
+ super.init()
+ if isApplication { menu = MenuBar(self) }
+#if HAVE_MACOS_MEDIA_PLAYER
+ remote = RemoteCommandCenter(self)
+#endif
+ log.verbose("AppHub initialised")
+ }
+
+ @objc func initMpv(_ mpv: OpaquePointer) {
+ event = EventHelper(self, mpv)
+ if let mpv = event?.mpv {
+ self.mpv = mpv
+ log.log = mp_log_new(nil, mp_client_get_log(mpv), "app")
+ option = OptionHelper(UnsafeMutablePointer(mpv), mp_client_get_global(mpv))
+ input.option = option
+ }
+
+#if HAVE_MACOS_MEDIA_PLAYER
+ remote?.registerEvents()
+#endif
+#if HAVE_MACOS_TOUCHBAR
+ touchBar = TouchBar(self)
+#endif
+ log.verbose("AppHub functionality initialised")
+ }
+
+ @objc func initInput(_ input: OpaquePointer?) {
+ log.verbose("Initialising Input")
+ self.input.signal(input: input)
+ }
+
+ @objc func initCocoaCb() {
+#if HAVE_MACOS_COCOA_CB
+ if !isApplication { return }
+ log.verbose("Initialising CocoaCB")
+ DispatchQueue.main.sync {
+ self.cocoaCb = self.cocoaCb ?? CocoaCB(mpv_create_client(mpv, "cocoacb"))
+ }
+#endif
+ }
+
+ @objc func startRemote() {
+#if HAVE_MACOS_MEDIA_PLAYER
+ log.verbose("Starting RemoteCommandCenter")
+ remote?.start()
+#endif
+ }
+
+ @objc func stopRemote() {
+#if HAVE_MACOS_MEDIA_PLAYER
+ log.verbose("Stoping RemoteCommandCenter")
+ remote?.stop()
+#endif
+ }
+
+ func open(urls: [URL]) {
+ let files = urls.map {
+ if $0.isFileURL { return $0.path }
+ var path = $0.absoluteString
+ if path.hasPrefix(MPV_PROTOCOL) { path.removeFirst(MPV_PROTOCOL.count) }
+ return path.removingPercentEncoding ?? path
+ }.sorted { (strL: String, strR: String) -> Bool in
+ return strL.localizedStandardCompare(strR) == .orderedAscending
+ }
+ log.verbose("\(openEvents > 0 ? "Appending" : "Opening") dropped files: \(files)")
+ input.open(files: files, append: openEvents > 0)
+ openEvents += 1
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.openEvents -= 1 }
+ }
+
+ func getIcon() -> NSImage {
+ guard let iconData = app_bridge_icon(), let icon = NSImage(data: iconData) else {
+ return NSImage(size: NSSize(width: 1, height: 1))
+ }
+ return icon
+ }
+
+ func getMacConf() -> UnsafePointer<m_sub_options>? {
+ return app_bridge_mac_conf()
+ }
+
+ func getVoConf() -> UnsafePointer<m_sub_options>? {
+ return app_bridge_vo_conf()
+ }
+}
diff --git a/osdep/mac/application.swift b/osdep/mac/application.swift
new file mode 100644
index 0000000..30f37a6
--- /dev/null
+++ b/osdep/mac/application.swift
@@ -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/>.
+ */
+
+import Cocoa
+
+class Application: NSApplication, NSApplicationDelegate {
+ var appHub: AppHub { get { return AppHub.shared } }
+ var eventManager: NSAppleEventManager { get { return NSAppleEventManager.shared() } }
+ var isBundle: Bool { get { return ProcessInfo.processInfo.environment["MPVBUNDLE"] == "true" } }
+ var playbackThreadId: mp_thread!
+ var argc: Int32?
+ var argv: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?
+
+ override init() {
+ super.init()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func sendEvent(_ event: NSEvent) {
+ if modalWindow != nil || !appHub.input.processKey(event: event) {
+ super.sendEvent(event)
+ }
+ appHub.input.wakeup()
+ }
+
+#if HAVE_MACOS_TOUCHBAR
+ override func makeTouchBar() -> NSTouchBar? {
+ return appHub.touchBar
+ }
+#endif
+
+ func application(_ application: NSApplication, open urls: [URL]) {
+ appHub.open(urls: urls)
+ }
+
+ func applicationWillFinishLaunching(_ notification: Notification) {
+ // register quit and exit events
+ eventManager.setEventHandler(
+ self,
+ andSelector: #selector(handleQuit(event:replyEvent:)),
+ forEventClass: AEEventClass(kCoreEventClass),
+ andEventID: kAEQuitApplication
+ )
+ atexit_b({
+ // clean up after exit() was called
+ DispatchQueue.main.async {
+ NSApp.hide(NSApp)
+ NSApp.setActivationPolicy(.prohibited)
+ self.eventManager.removeEventHandler(forEventClass: AEEventClass(kCoreEventClass), andEventID: kAEQuitApplication)
+ }
+ })
+ }
+
+ // quit from App icon, external quit from NSWorkspace
+ @objc func handleQuit(event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) {
+ // send quit to core, terminates mpv_main called in playbackThread,
+ if !appHub.input.command("quit") {
+ appHub.log.warning("Could not properly shut down mpv")
+ exit(1)
+ }
+ }
+
+ func setupBundle() {
+ if !isBundle { return }
+
+ // started from finder the first argument after the binary may start with -psn_
+ if CommandLine.argc > 1 && CommandLine.arguments[1].hasPrefix("-psn_") {
+ argc? = 1
+ argv?[1] = nil
+ }
+
+ let path = (ProcessInfo.processInfo.environment["PATH"] ?? "") +
+ ":/usr/local/bin:/usr/local/sbin:/opt/local/bin:/opt/local/sbin"
+ appHub.log.verbose("Setting Bundle $PATH to: \(path)")
+ _ = path.withCString { setenv("PATH", $0, 1) }
+ }
+
+ let playbackThread: @convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? = { (ptr: UnsafeMutableRawPointer) in
+ let application: Application = TypeHelper.bridge(ptr: ptr)
+ mp_thread_set_name("core/playback")
+ let exitCode: Int32 = mpv_main(application.argc ?? 1, application.argv)
+ // exit of any proper shut down
+ exit(exitCode)
+ }
+
+ @objc func main(_ argc: Int32, _ argv: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>) -> Int {
+ self.argc = argc
+ self.argv = argv
+
+ NSApp = self
+ NSApp.delegate = self
+ NSApp.setActivationPolicy(isBundle ? .regular : .accessory)
+ setupBundle()
+ pthread_create(&playbackThreadId, nil, playbackThread, TypeHelper.bridge(obj: self))
+ appHub.input.wait()
+ NSApp.run()
+
+ // should never be reached
+ print("""
+ There was either a problem initializing Cocoa or the Runloop was stopped unexpectedly. \
+ Please report this issues to a developer.\n
+ """)
+ pthread_join(playbackThreadId, nil)
+ return 1
+ }
+}
diff --git a/osdep/mac/event_helper.swift b/osdep/mac/event_helper.swift
new file mode 100644
index 0000000..277a9aa
--- /dev/null
+++ b/osdep/mac/event_helper.swift
@@ -0,0 +1,147 @@
+/*
+ * 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
+
+protocol EventSubscriber: AnyObject {
+ var uid: Int { get }
+ func handle(event: EventHelper.Event)
+}
+
+extension EventSubscriber {
+ var uid: Int { get { return Int(bitPattern: ObjectIdentifier(self)) }}
+}
+
+extension EventHelper {
+ typealias wakeup_cb = (@convention(c) (UnsafeMutableRawPointer?) -> Void)?
+
+ struct Event {
+ var id: String {
+ get { name + (name.starts(with: "MPV_EVENT_") ? "" : String(format.rawValue)) }
+ }
+ var idReset: String {
+ get { name + (name.starts(with: "MPV_EVENT_") ? "" : String(MPV_FORMAT_NONE.rawValue)) }
+ }
+ let name: String
+ let format: mpv_format
+ let string: String?
+ let bool: Bool?
+ let double: Double?
+
+ init(
+ name: String = "",
+ format: mpv_format = MPV_FORMAT_NONE,
+ string: String? = nil,
+ bool: Bool? = nil,
+ double: Double? = nil
+
+ ) {
+ self.name = name
+ self.format = format
+ self.string = string
+ self.bool = bool
+ self.double = double
+ }
+ }
+}
+
+class EventHelper {
+ unowned let appHub: AppHub
+ var mpv: OpaquePointer?
+ var events: [String:[Int:EventSubscriber]] = [:]
+
+ init?(_ appHub: AppHub, _ mpv: OpaquePointer) {
+ if !appHub.isApplication {
+ mpv_destroy(mpv)
+ return nil
+ }
+
+ self.appHub = appHub
+ self.mpv = mpv
+ mpv_set_wakeup_callback(mpv, wakeup, TypeHelper.bridge(obj: self))
+ }
+
+ func subscribe(_ subscriber: any EventSubscriber, event: Event) {
+ guard let mpv = mpv else { return }
+
+ if !event.name.isEmpty {
+ if !events.keys.contains(event.idReset) {
+ events[event.idReset] = [:]
+ }
+ if !events.keys.contains(event.id) {
+ mpv_observe_property(mpv, 0, event.name, event.format)
+ events[event.id] = [:]
+ }
+ events[event.idReset]?[subscriber.uid] = subscriber
+ events[event.id]?[subscriber.uid] = subscriber
+ }
+ }
+
+ let wakeup: wakeup_cb = { ( ctx ) in
+ let event = unsafeBitCast(ctx, to: EventHelper.self)
+ DispatchQueue.main.async { event.eventLoop() }
+ }
+
+ func eventLoop() {
+ while let mpv = mpv, let event = mpv_wait_event(mpv, 0) {
+ if event.pointee.event_id == MPV_EVENT_NONE { break }
+ handle(event: event)
+ }
+ }
+
+ func handle(event: UnsafeMutablePointer<mpv_event>) {
+ switch event.pointee.event_id {
+ case MPV_EVENT_PROPERTY_CHANGE:
+ handle(property: event)
+ default:
+ for (_, subscriber) in events[String(describing: event.pointee.event_id)] ?? [:] {
+ subscriber.handle(event: .init(name: String(describing: event.pointee.event_id)))
+ }
+ }
+
+ if event.pointee.event_id == MPV_EVENT_SHUTDOWN {
+ mpv_destroy(mpv)
+ mpv = nil
+ }
+ }
+
+ func handle(property mpvEvent: UnsafeMutablePointer<mpv_event>) {
+ let pData = OpaquePointer(mpvEvent.pointee.data)
+ guard let property = UnsafePointer<mpv_event_property>(pData)?.pointee else {
+ return
+ }
+
+ let name = String(cString: property.name)
+ let format = property.format
+ for (_, subscriber) in events[name + String(format.rawValue)] ?? [:] {
+ var event: Event? = nil
+ switch format {
+ case MPV_FORMAT_STRING:
+ event = .init(name: name, format: format, string: TypeHelper.toString(property.data))
+ case MPV_FORMAT_FLAG:
+ event = .init(name: name, format: format, bool: TypeHelper.toBool(property.data))
+ case MPV_FORMAT_DOUBLE:
+ event = .init(name: name, format: format, double: TypeHelper.toDouble(property.data))
+ case MPV_FORMAT_NONE:
+ event = .init(name: name, format: format)
+ default: break
+ }
+
+ if let e = event { subscriber.handle(event: e) }
+ }
+ }
+}
diff --git a/osdep/mac/input_helper.swift b/osdep/mac/input_helper.swift
new file mode 100644
index 0000000..0e4ec1f
--- /dev/null
+++ b/osdep/mac/input_helper.swift
@@ -0,0 +1,275 @@
+/*
+ * 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 Carbon.HIToolbox
+
+class InputHelper: NSObject {
+ var option: OptionHelper?
+ var lock = NSCondition()
+ private var input: OpaquePointer?
+
+ let keymap: [mp_keymap] = [
+ // special keys
+ .init(kVK_Return, MP_KEY_ENTER), .init(kVK_Escape, MP_KEY_ESC),
+ .init(kVK_Delete, MP_KEY_BACKSPACE), .init(kVK_Tab, MP_KEY_TAB),
+ .init(kVK_VolumeUp, MP_KEY_VOLUME_UP), .init(kVK_VolumeDown, MP_KEY_VOLUME_DOWN),
+ .init(kVK_Mute, MP_KEY_MUTE),
+
+ // cursor keys
+ .init(kVK_UpArrow, MP_KEY_UP), .init(kVK_DownArrow, MP_KEY_DOWN),
+ .init(kVK_LeftArrow, MP_KEY_LEFT), .init(kVK_RightArrow, MP_KEY_RIGHT),
+
+ // navigation block
+ .init(kVK_Help, MP_KEY_INSERT), .init(kVK_ForwardDelete, MP_KEY_DELETE),
+ .init(kVK_Home, MP_KEY_HOME), .init(kVK_End, MP_KEY_END),
+ .init(kVK_PageUp, MP_KEY_PAGE_UP), .init(kVK_PageDown, MP_KEY_PAGE_DOWN),
+
+ // F-keys
+ .init(kVK_F1, MP_KEY_F + 1), .init(kVK_F2, MP_KEY_F + 2), .init(kVK_F3, MP_KEY_F + 3),
+ .init(kVK_F4, MP_KEY_F + 4), .init(kVK_F5, MP_KEY_F + 5), .init(kVK_F6, MP_KEY_F + 6),
+ .init(kVK_F7, MP_KEY_F + 7), .init(kVK_F8, MP_KEY_F + 8), .init(kVK_F9, MP_KEY_F + 9),
+ .init(kVK_F10, MP_KEY_F + 10), .init(kVK_F11, MP_KEY_F + 11), .init(kVK_F12, MP_KEY_F + 12),
+ .init(kVK_F13, MP_KEY_F + 13), .init(kVK_F14, MP_KEY_F + 14), .init(kVK_F15, MP_KEY_F + 15),
+ .init(kVK_F16, MP_KEY_F + 16), .init(kVK_F17, MP_KEY_F + 17), .init(kVK_F18, MP_KEY_F + 18),
+ .init(kVK_F19, MP_KEY_F + 19), .init(kVK_F20, MP_KEY_F + 20),
+
+ // numpad
+ .init(kVK_ANSI_KeypadPlus, Int32(Character("+").asciiValue ?? 0)),
+ .init(kVK_ANSI_KeypadMinus, Int32(Character("-").asciiValue ?? 0)),
+ .init(kVK_ANSI_KeypadMultiply, Int32(Character("*").asciiValue ?? 0)),
+ .init(kVK_ANSI_KeypadDivide, Int32(Character("/").asciiValue ?? 0)),
+ .init(kVK_ANSI_KeypadEnter, MP_KEY_KPENTER), .init(kVK_ANSI_KeypadDecimal, MP_KEY_KPDEC),
+ .init(kVK_ANSI_Keypad0, MP_KEY_KP0), .init(kVK_ANSI_Keypad1, MP_KEY_KP1),
+ .init(kVK_ANSI_Keypad2, MP_KEY_KP2), .init(kVK_ANSI_Keypad3, MP_KEY_KP3),
+ .init(kVK_ANSI_Keypad4, MP_KEY_KP4), .init(kVK_ANSI_Keypad5, MP_KEY_KP5),
+ .init(kVK_ANSI_Keypad6, MP_KEY_KP6), .init(kVK_ANSI_Keypad7, MP_KEY_KP7),
+ .init(kVK_ANSI_Keypad8, MP_KEY_KP8), .init(kVK_ANSI_Keypad9, MP_KEY_KP9),
+
+ .init(0, 0)
+ ]
+
+ init(_ input: OpaquePointer? = nil, _ option: OptionHelper? = nil) {
+ super.init()
+ self.input = input
+ self.option = option
+ }
+
+ func put(
+ key: Int32,
+ modifiers: NSEvent.ModifierFlags = .init(rawValue: 0),
+ type: NSEvent.EventType = .applicationDefined
+ ) {
+ lock.withLock {
+ putKey(key, modifiers: modifiers, type: type)
+ }
+ }
+
+ private func putKey(
+ _ key: Int32,
+ modifiers: NSEvent.ModifierFlags = .init(rawValue: 0),
+ type: NSEvent.EventType = .applicationDefined
+ ) {
+ if key < 1 { return }
+
+ guard let input = input else { return }
+ let code = key | mapModifier(modifiers) | mapType(type)
+ mp_input_put_key(input, code)
+
+ if type == .keyUp {
+ mp_input_put_key(input, MP_INPUT_RELEASE_ALL)
+ }
+ }
+
+ @objc func processKey(event: NSEvent) -> Bool {
+ if event.type != .keyDown && event.type != .keyUp { return false }
+ if NSApp.mainMenu?.performKeyEquivalent(with: event) ?? false || event.isARepeat { return true }
+
+ return lock.withLock {
+ let mpkey = lookup_keymap_table(keymap, Int32(event.keyCode))
+ if mpkey > 0 {
+ putKey(mpkey, modifiers: event.modifierFlags, type: event.type)
+ return true
+ }
+
+ guard let chars = event.characters, let charsNoMod = event.charactersIgnoringModifiers else { return false }
+ let key = (useAltGr() && event.modifierFlags.contains(.optionRight)) ? chars : charsNoMod
+ key.withCString {
+ var bstr = bstr0($0)
+ putKey(bstr_decode_utf8(bstr, &bstr), modifiers: event.modifierFlags, type: event.type)
+ }
+ return true
+ }
+ }
+
+ func processMouse(event: NSEvent) {
+ if !mouseEnabled() { return }
+ lock.withLock {
+ putKey(map(button: event.buttonNumber), modifiers: event.modifierFlags, type: event.type)
+ if event.clickCount > 1 {
+ putKey(map(button: event.buttonNumber), modifiers: event.modifierFlags, type: .keyUp)
+ }
+ }
+ }
+
+ func processWheel(event: NSEvent) {
+ if !mouseEnabled() { return }
+ lock.withLock {
+ guard let input = input else { return }
+ let modifiers = event.modifierFlags
+ let precise = event.hasPreciseScrollingDeltas
+ var deltaX = event.deltaX * 0.1
+ var deltaY = event.deltaY * 0.1
+
+ if !precise {
+ deltaX = modifiers.contains(.shift) ? event.scrollingDeltaY : event.scrollingDeltaX
+ deltaY = modifiers.contains(.shift) ? event.scrollingDeltaX : event.scrollingDeltaY
+ }
+
+ var key = deltaY > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN
+ var delta = Double(deltaY)
+ if abs(deltaX) > abs(deltaY) {
+ key = deltaX > 0 ? SWIFT_WHEEL_LEFT : SWIFT_WHEEL_RIGHT
+ delta = Double(deltaX)
+ }
+
+ mp_input_put_wheel(input, key | mapModifier(modifiers), precise ? abs(delta) : 1)
+ }
+ }
+
+ func draggable(at pos: NSPoint) -> Bool {
+ lock.withLock {
+ guard let input = input else { return false }
+ return !mp_input_test_dragging(input, Int32(pos.x), Int32(pos.y))
+ }
+ }
+
+ func mouseEnabled() -> Bool {
+ lock.withLock {
+ guard let input = input else { return true }
+ return mp_input_mouse_enabled(input)
+ }
+ }
+
+ func setMouse(position: NSPoint) {
+ if !mouseEnabled() { return }
+ lock.withLock {
+ guard let input = input else { return }
+ mp_input_set_mouse_pos(input, Int32(position.x), Int32(position.y))
+ }
+ }
+
+ @discardableResult @objc func command(_ cmd: String) -> Bool {
+ lock.withLock {
+ guard let input = input else { return false }
+ 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))
+ return true
+ }
+ }
+
+ private func mapType(_ type: NSEvent.EventType) -> Int32 {
+ let typeMapping: [NSEvent.EventType:UInt32] = [
+ .keyDown: MP_KEY_STATE_DOWN,
+ .keyUp: MP_KEY_STATE_UP,
+ .leftMouseDown: MP_KEY_STATE_DOWN,
+ .leftMouseUp: MP_KEY_STATE_UP,
+ .rightMouseDown: MP_KEY_STATE_DOWN,
+ .rightMouseUp: MP_KEY_STATE_UP,
+ .otherMouseDown: MP_KEY_STATE_DOWN,
+ .otherMouseUp: MP_KEY_STATE_UP,
+ ]
+
+ return Int32(typeMapping[type] ?? 0);
+ }
+
+ private func mapModifier(_ modifiers: NSEvent.ModifierFlags) -> Int32 {
+ var mask: UInt32 = 0;
+
+ if modifiers.contains(.shift) {
+ mask |= MP_KEY_MODIFIER_SHIFT
+ }
+ if modifiers.contains(.control) {
+ mask |= MP_KEY_MODIFIER_CTRL
+ }
+ if modifiers.contains(.command) {
+ mask |= MP_KEY_MODIFIER_META
+ }
+ if modifiers.contains(.optionLeft) || modifiers.contains(.optionRight) && !useAltGr() {
+ mask |= MP_KEY_MODIFIER_ALT
+ }
+
+ return Int32(mask)
+ }
+
+ private func map(button: Int) -> Int32 {
+ let buttonMapping: [Int:Int32] = [
+ 0: SWIFT_MBTN_LEFT,
+ 1: SWIFT_MBTN_RIGHT,
+ 2: SWIFT_MBTN_MID,
+ 3: SWIFT_MBTN_FORWARD,
+ 4: SWIFT_MBTN_BACK,
+ ]
+
+ return Int32(buttonMapping[button] ?? SWIFT_MBTN9 + Int32(button - 5));
+ }
+
+ @objc func open(files: [String], append: Bool = false) {
+ lock.withLock {
+ guard let input = input else { return }
+ if (option?.vo.drag_and_drop ?? -1) == -2 { return }
+
+ var action = DND_APPEND
+ if !append {
+ action = NSEvent.modifierFlags.contains(.shift) ? DND_APPEND : DND_REPLACE
+ if (option?.vo.drag_and_drop ?? -1) >= 0 {
+ action = mp_dnd_action(UInt32(option?.vo.drag_and_drop ?? Int32(DND_REPLACE.rawValue)))
+ }
+ }
+
+ let filesClean = files.map{ $0.hasPrefix("file:///.file/id=") ? (URL(string: $0)?.path ?? $0) : $0 }
+ var filesPtr = filesClean.map { UnsafeMutablePointer<CChar>(strdup($0)) }
+ mp_event_drop_files(input, Int32(files.count), &filesPtr, action)
+ for charPtr in filesPtr { free(UnsafeMutablePointer(mutating: charPtr)) }
+ }
+ }
+
+ private func useAltGr() -> Bool {
+ guard let input = input else { return false }
+ return mp_input_use_alt_gr(input)
+ }
+
+ @objc func wakeup() {
+ lock.withLock {
+ guard let input = input else { return }
+ mp_input_wakeup(input)
+ }
+ }
+
+ func signal(input: OpaquePointer? = nil) {
+ lock.withLock {
+ self.input = input
+ if input != nil { lock.signal() }
+ }
+ }
+
+ @objc func wait() {
+ lock.withLock { while input == nil { lock.wait() } }
+ }
+}
diff --git a/osdep/mac/libmpv_helper.swift b/osdep/mac/libmpv_helper.swift
new file mode 100644
index 0000000..23953eb
--- /dev/null
+++ b/osdep/mac/libmpv_helper.swift
@@ -0,0 +1,181 @@
+/*
+ * 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 mpv: OpaquePointer?
+ var mpvRenderContext: OpaquePointer?
+ var fbo: GLint = 1
+ let uninitLock = NSLock()
+
+ init(_ mpv: OpaquePointer, _ log: LogHelper) {
+ self.mpv = mpv
+ self.log = log
+ }
+
+ 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)
+
+ TypeHelper.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, mpv, &params) < 0) {
+ log.error("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.warning("Init mpv render context first.")
+ } else {
+ mpv_render_context_set_update_callback(mpvRenderContext, callback, TypeHelper.bridge(obj: object))
+ }
+ }
+
+ func setRenderControlCallback(_ callback: @escaping mp_render_cb_control_fn, context object: AnyObject) {
+ if mpvRenderContext == nil {
+ log.warning("Init mpv render context first.")
+ } else {
+ mp_render_context_set_control_callback(mpvRenderContext, callback, TypeHelper.bridge(obj: object))
+ }
+ }
+
+ func reportRenderFlip() {
+ if mpvRenderContext == nil { return }
+ mpv_render_context_report_swap(mpvRenderContext)
+ }
+
+ func isRenderUpdateFrame() -> Bool {
+ uninitLock.lock()
+ if mpvRenderContext == nil {
+ uninitLock.unlock()
+ return false
+ }
+ let flags: UInt64 = mpv_render_context_update(mpvRenderContext)
+ uninitLock.unlock()
+ return flags & UInt64(MPV_RENDER_UPDATE_FRAME.rawValue) > 0
+ }
+
+ func drawRender(_ surface: NSSize, _ depth: GLint, _ ctx: CGLContextObj, skip: Bool = false) {
+ uninitLock.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)
+
+ TypeHelper.withUnsafeMutableRawPointers([data, flip, ditherDepth, skip]) { (pointers: [UnsafeMutableRawPointer?]) in
+ var params: [mpv_render_param] = [
+ mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: pointers[0]),
+ mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: pointers[1]),
+ mpv_render_param(type: MPV_RENDER_PARAM_DEPTH, data: pointers[2]),
+ mpv_render_param(type: MPV_RENDER_PARAM_SKIP_RENDERING, data: pointers[3]),
+ mpv_render_param()
+ ]
+ mpv_render_context_render(mpvRenderContext, &params);
+ }
+ } else {
+ glClearColor(0, 0, 0, 1)
+ glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
+ }
+
+ if !skip { CGLFlushDrawable(ctx) }
+
+ uninitLock.unlock()
+ }
+
+ func setRenderICCProfile(_ profile: NSColorSpace) {
+ if mpvRenderContext == nil { return }
+ guard var iccData = profile.iccProfileData else {
+ log.warning("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 uninit() {
+ mpv_render_context_set_update_callback(mpvRenderContext, nil, nil)
+ mp_render_context_set_control_callback(mpvRenderContext, nil, nil)
+ uninitLock.lock()
+ mpv_render_context_free(mpvRenderContext)
+ mpvRenderContext = nil
+ mpv_destroy(mpv)
+ mpv = nil
+ uninitLock.unlock()
+ }
+}
diff --git a/osdep/mac/log_helper.swift b/osdep/mac/log_helper.swift
new file mode 100644
index 0000000..3d349a4
--- /dev/null
+++ b/osdep/mac/log_helper.swift
@@ -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/>.
+ */
+
+import Cocoa
+import os
+
+class LogHelper {
+ var log: OpaquePointer?
+ let logger = Logger(subsystem: "io.mpv", category: "mpv")
+
+ let loggerMapping: [Int:OSLogType] = [
+ MSGL_V: .debug,
+ MSGL_INFO: .info,
+ MSGL_WARN: .error,
+ MSGL_ERR: .fault,
+ ]
+
+ init(_ log: OpaquePointer? = nil) {
+ self.log = log
+ }
+
+ func verbose(_ message: String) {
+ send(message: message, type: MSGL_V)
+ }
+
+ func info(_ message: String) {
+ send(message: message, type: MSGL_INFO)
+ }
+
+ func warning(_ message: String) {
+ send(message: message, type: MSGL_WARN)
+ }
+
+ func error(_ message: String) {
+ send(message: message, type: MSGL_ERR)
+ }
+
+ func send(message: String, type: Int) {
+ guard let log = log else {
+ logger.log(level: loggerMapping[type] ?? .default, "\(message, privacy: .public)")
+ return
+ }
+
+ let args: [CVarArg] = [(message as NSString).utf8String ?? "NO MESSAGE"]
+ mp_msg_va(log, Int32(type), "%s\n", getVaList(args))
+ }
+
+ deinit {
+ // only a manual dereferencing will trigger this, cleanup properly in that case
+ ta_free(UnsafeMutablePointer(log))
+ log = nil
+ }
+}
diff --git a/osdep/mac/menu_bar.swift b/osdep/mac/menu_bar.swift
new file mode 100644
index 0000000..b58367a
--- /dev/null
+++ b/osdep/mac/menu_bar.swift
@@ -0,0 +1,397 @@
+/*
+ * 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 MenuBar {
+ class MenuItem: NSMenuItem {
+ var config: Config?
+ }
+
+ enum `Type`: Comparable {
+ case menu
+ case menuServices
+ case separator
+ case item
+ case itemNormalSize
+ case itemHalfSize
+ case itemDoubleSize
+ case itemMinimize
+ case itemZoom
+ }
+
+ struct Config {
+ let name: String
+ let key: String
+ let modifiers: NSEvent.ModifierFlags
+ let type: Type
+ let action: Selector?
+ let target: AnyObject?
+ let command: String
+ let url: String
+ var configs: [Config]
+
+ init(
+ name: String = "",
+ key: String = "",
+ modifiers: NSEvent.ModifierFlags = .command,
+ type: Type = .item,
+ action: Selector? = nil,
+ target: AnyObject? = nil,
+ command: String = "",
+ url: String = "",
+ configs: [Config] = []
+ ) {
+ self.name = name
+ self.key = key
+ self.modifiers = modifiers
+ self.type = configs.isEmpty ? type : .menu
+ self.action = action
+ self.target = target
+ self.command = command
+ self.url = url
+ self.configs = configs
+ }
+ }
+}
+
+class MenuBar: NSObject {
+ unowned let appHub: AppHub
+ let mainMenu = NSMenu(title: "Main")
+ let servicesMenu = NSMenu(title: "Services")
+ var menuConfigs: [Config] = []
+ var dynamicMenuItems: [Type:[MenuItem]] = [:]
+ let appIcon: NSImage
+
+ @objc init(_ appHub: AppHub) {
+ self.appHub = appHub
+ UserDefaults.standard.set(false, forKey: "NSFullScreenMenuItemEverywhere")
+ UserDefaults.standard.set(true, forKey: "NSDisabledDictationMenuItem")
+ UserDefaults.standard.set(true, forKey: "NSDisabledCharacterPaletteMenuItem")
+ NSWindow.allowsAutomaticWindowTabbing = false
+ appIcon = appHub.getIcon()
+
+ super.init()
+
+ let appMenuConfigs = [
+ Config(name: "About mpv", action: #selector(about), target: self),
+ Config(type: .separator),
+ Config(
+ name: "Settings…",
+ key: ",",
+ action: #selector(settings(_:)),
+ target: self,
+ url: "mpv.conf"
+ ),
+ Config(
+ name: "Keyboard Shortcuts Config…",
+ action: #selector(settings(_:)),
+ target: self,
+ url: "input.conf"
+ ),
+ Config(type: .separator),
+ Config(name: "Services", type: .menuServices),
+ Config(type: .separator),
+ Config(name: "Hide mpv", key: "h", action: #selector(NSApp.hide(_:))),
+ Config(name: "Hide Others", key: "h", modifiers: [.command, .option], action: #selector(NSApp.hideOtherApplications(_:))),
+ Config(name: "Show All", action: #selector(NSApp.unhideAllApplications(_:))),
+ Config(type: .separator),
+ Config(name: "Quit and Remember Position", action: #selector(command(_:)), target: self, command: "quit-watch-later"),
+ Config(name: "Quit mpv", key: "q", action: #selector(command(_:)), target: self, command: "quit"),
+ ]
+
+ let fileMenuConfigs = [
+ Config(name: "Open File…", key: "o", action: #selector(openFiles), target: self),
+ Config(name: "Open URL…", key: "O", action: #selector(openUrl), target: self),
+ Config(name: "Open Playlist…", action: #selector(openPlaylist), target: self),
+ Config(type: .separator),
+ Config(name: "Close", key: "w", action: #selector(NSWindow.performClose(_:))),
+ Config(name: "Save Screenshot", action: #selector(command(_:)), target: self, command: "async screenshot"),
+ ]
+
+ let editMenuConfigs = [
+ Config(name: "Undo", key: "z", action: Selector(("undo:"))),
+ Config(name: "Redo", key: "Z", action: Selector(("redo:"))),
+ Config(type: .separator),
+ Config(name: "Cut", key: "x", action: #selector(NSText.cut(_:))),
+ Config(name: "Copy", key: "c", action: #selector(NSText.copy(_:))),
+ Config(name: "Paste", key: "v", action: #selector(NSText.paste(_:))),
+ Config(name: "Select All", key: "a", action: #selector(NSResponder.selectAll(_:))),
+ ]
+
+ var viewMenuConfigs = [
+ Config(name: "Toggle Fullscreen", action: #selector(command(_:)), target: self, command: "cycle fullscreen"),
+ Config(name: "Toggle Float on Top", action: #selector(command(_:)), target: self, command: "cycle ontop"),
+ Config(
+ name: "Toggle Visibility on All Workspaces",
+ action: #selector(command(_:)),
+ target: self,
+ command: "cycle on-all-workspaces"
+ ),
+ ]
+#if HAVE_MACOS_TOUCHBAR
+ viewMenuConfigs += [
+ Config(type: .separator),
+ Config(name: "Customize Touch Bar…", action: #selector(NSApp.toggleTouchBarCustomizationPalette(_:))),
+ ]
+#endif
+
+ let videoMenuConfigs = [
+ Config(name: "Zoom Out", action: #selector(command(_:)), target: self, command: "add panscan -0.1"),
+ Config(name: "Zoom In", action: #selector(command(_:)), target: self, command: "add panscan 0.1"),
+ Config(name: "Reset Zoom", action: #selector(command(_:)), target: self, command: "set panscan 0"),
+ Config(type: .separator),
+ Config(name: "Aspect Ratio 4:3", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"4:3\""),
+ Config(name: "Aspect Ratio 16:9", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"16:9\""),
+ Config(name: "Aspect Ratio 1.85:1", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"1.85:1\""),
+ Config(name: "Aspect Ratio 2.35:1", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"2.35:1\""),
+ Config(name: "Reset Aspect Ratio", action: #selector(command(_:)), target: self, command: "set video-aspect-override \"-1\""),
+ Config(type: .separator),
+ Config(name: "Rotate Left", action: #selector(command(_:)), target: self, command: "cycle-values video-rotate 0 270 180 90"),
+ Config(name: "Rotate Right", action: #selector(command(_:)), target: self, command: "cycle-values video-rotate 90 180 270 0"),
+ Config(name: "Reset Rotation", action: #selector(command(_:)), target: self, command: "set video-rotate 0"),
+ Config(type: .separator),
+ Config(name: "Half Size", key: "0", type: .itemHalfSize),
+ Config(name: "Normal Size", key: "1", type: .itemNormalSize),
+ Config(name: "Double Size", key: "2", type: .itemDoubleSize),
+ ]
+
+ let audioMenuConfigs = [
+ Config(name: "Next Audio Track", action: #selector(command(_:)), target: self, command: "cycle audio"),
+ Config(name: "Previous Audio Track", action: #selector(command(_:)), target: self, command: "cycle audio down"),
+ Config(type: .separator),
+ Config(name: "Toggle Mute", action: #selector(command(_:)), target: self, command: "cycle mute"),
+ Config(type: .separator),
+ Config(name: "Play Audio Later", action: #selector(command(_:)), target: self, command: "add audio-delay 0.1"),
+ Config(name: "Play Audio Earlier", action: #selector(command(_:)), target: self, command: "add audio-delay -0.1"),
+ Config(name: "Reset Audio Delay", action: #selector(command(_:)), target: self, command: "set audio-delay 0.0"),
+ ]
+
+ let subtitleMenuConfigs = [
+ Config(name: "Next Subtitle Track", action: #selector(command(_:)), target: self, command: "cycle sub"),
+ Config(name: "Previous Subtitle Track", action: #selector(command(_:)), target: self, command: "cycle sub down"),
+ Config(type: .separator),
+ Config(name: "Toggle Force Style", action: #selector(command(_:)), target: self, command: "cycle-values sub-ass-override \"force\" \"no\""),
+ Config(type: .separator),
+ Config(name: "Display Subtitles Later", action: #selector(command(_:)), target: self, command: "add sub-delay 0.1"),
+ Config(name: "Display Subtitles Earlier", action: #selector(command(_:)), target: self, command: "add sub-delay -0.1"),
+ Config(name: "Reset Subtitle Delay", action: #selector(command(_:)), target: self, command: "set sub-delay 0.0"),
+ ]
+
+ let playbackMenuConfigs = [
+ Config(name: "Toggle Pause", action: #selector(command(_:)), target: self, command: "cycle pause"),
+ Config(name: "Increase Speed", action: #selector(command(_:)), target: self, command: "add speed 0.1"),
+ Config(name: "Decrease Speed", action: #selector(command(_:)), target: self, command: "add speed -0.1"),
+ Config(name: "Reset Speed", action: #selector(command(_:)), target: self, command: "set speed 1.0"),
+ Config(type: .separator),
+ Config(name: "Show Playlist", action: #selector(command(_:)), target: self, command: "script-message osc-playlist"),
+ Config(name: "Show Chapters", action: #selector(command(_:)), target: self, command: "script-message osc-chapterlist"),
+ Config(name: "Show Tracks", action: #selector(command(_:)), target: self, command: "script-message osc-tracklist"),
+ Config(type: .separator),
+ Config(name: "Next File", action: #selector(command(_:)), target: self, command: "playlist-next"),
+ Config(name: "Previous File", action: #selector(command(_:)), target: self, command: "playlist-prev"),
+ Config(name: "Toggle Loop File", action: #selector(command(_:)), target: self, command: "cycle-values loop-file \"inf\" \"no\""),
+ Config(name: "Toggle Loop Playlist", action: #selector(command(_:)), target: self, command: "cycle-values loop-playlist \"inf\" \"no\""),
+ Config(name: "Shuffle", action: #selector(command(_:)), target: self, command: "playlist-shuffle"),
+ Config(type: .separator),
+ Config(name: "Next Chapter", action: #selector(command(_:)), target: self, command: "add chapter 1"),
+ Config(name: "Previous Chapter", action: #selector(command(_:)), target: self, command: "add chapter -1"),
+ Config(type: .separator),
+ Config(name: "Step Forward", action: #selector(command(_:)), target: self, command: "frame-step"),
+ Config(name: "Step Backward", action: #selector(command(_:)), target: self, command: "frame-back-step"),
+ ]
+
+ let windowMenuConfigs = [
+ Config(name: "Minimize", key: "m", type: .itemMinimize),
+ Config(name: "Zoom", type: .itemZoom),
+ ]
+
+ var helpMenuConfigs = [
+ Config(name: "mpv Website…", action: #selector(url(_:)), target: self, url: "https://mpv.io"),
+ Config(name: "mpv on GitHub…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv"),
+ Config(type: .separator),
+ Config(name: "Online Manual…", action: #selector(url(_:)), target: self, url: "https://mpv.io/manual/master/"),
+ Config(name: "Online Wiki…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv/wiki"),
+ Config(name: "Release Notes…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv/blob/master/RELEASE_NOTES"),
+ Config(name: "Keyboard Shortcuts…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv/blob/master/etc/input.conf"),
+ Config(type: .separator),
+ Config(name: "Report Issue…", action: #selector(url(_:)), target: self, url: "https://github.com/mpv-player/mpv/issues/new/choose"),
+ ]
+ if ProcessInfo.processInfo.environment["MPVBUNDLE"] == "true" {
+ helpMenuConfigs += [
+ Config(name: "Show log File…", action: #selector(showFile(_:)), target: self, url: NSHomeDirectory() + "/Library/Logs/mpv.log")
+ ]
+ }
+
+ menuConfigs = [
+ Config(name: "Apple", configs: appMenuConfigs),
+ Config(name: "File", configs: fileMenuConfigs),
+ Config(name: "Edit", configs: editMenuConfigs),
+ Config(name: "View", configs: viewMenuConfigs),
+ Config(name: "Video", configs: videoMenuConfigs),
+ Config(name: "Audio", configs: audioMenuConfigs),
+ Config(name: "Subtitle", configs: subtitleMenuConfigs),
+ Config(name: "Playback", configs: playbackMenuConfigs),
+ Config(name: "Window", configs: windowMenuConfigs),
+ Config(name: "Help", configs: helpMenuConfigs),
+ ]
+
+ createMenu(parentMenu: mainMenu, configs: menuConfigs)
+ NSApp.mainMenu = mainMenu
+ NSApp.servicesMenu = servicesMenu
+ }
+
+ func createMenu(parentMenu: NSMenu, configs: [Config]) {
+ for config in configs {
+ let item = createMenuItem(parentMenu: parentMenu, config: config)
+
+ if config.type <= .menuServices {
+ let menu = config.type == .menuServices ? servicesMenu : NSMenu(title: config.name)
+ item.submenu = menu
+ createMenu(parentMenu: menu, configs: config.configs)
+ }
+
+ if config.type > Type.item {
+ dynamicMenuItems[config.type] = (dynamicMenuItems[config.type] ?? []) + [item]
+ }
+ }
+ }
+
+ func createMenuItem(parentMenu: NSMenu, config: Config) -> MenuItem {
+ var item = MenuItem(title: config.name, action: config.action, keyEquivalent: config.key)
+ item.config = config
+ item.target = config.target
+ item.keyEquivalentModifierMask = config.modifiers
+
+ if config.type == .separator {
+ item = MenuItem.separator() as? MenuItem ?? item
+ }
+ parentMenu.addItem(item)
+
+ return item
+ }
+
+ @objc func about() {
+ NSApp.orderFrontStandardAboutPanel(options: [
+ .applicationName: "mpv",
+ .applicationIcon: appIcon,
+ .applicationVersion: String(cString: swift_mpv_version),
+ .init(rawValue: "Copyright"): String(cString: swift_mpv_copyright),
+ ])
+ }
+
+ @objc func settings(_ menuItem: MenuItem) {
+ guard let menuConfig = menuItem.config else { return }
+ let configPaths: [URL] = [
+ URL(fileURLWithPath: NSHomeDirectory() + "/.config/mpv/", isDirectory: true),
+ URL(fileURLWithPath: NSHomeDirectory() + "/.mpv/", isDirectory: true),
+ ]
+
+ for path in configPaths {
+ let configFile = path.appendingPathComponent(menuConfig.url, isDirectory: false)
+
+ if FileManager.default.fileExists(atPath: configFile.path) {
+ if NSWorkspace.shared.open(configFile) {
+ return
+ }
+ NSWorkspace.shared.open(path)
+ alert(title: "No Application found to open your config file.", text: "Please open the \(menuConfig.url) file with your preferred text editor in the now open folder to edit your config.")
+ return
+ }
+
+ if NSWorkspace.shared.open(path) {
+ alert(title: "No config file found.", text: "Please create a \(menuConfig.url) file with your preferred text editor in the now open folder.")
+ return
+ }
+ }
+ }
+
+ @objc func openFiles() {
+ let panel = NSOpenPanel()
+ panel.allowsMultipleSelection = true
+ panel.canChooseDirectories = true
+
+ if panel.runModal() == .OK {
+ appHub.input.open(files: panel.urls.map { $0.path })
+ }
+ }
+
+ @objc func openPlaylist() {
+ let panel = NSOpenPanel()
+
+ if panel.runModal() == .OK, let url = panel.urls.first {
+ appHub.input.command("loadlist \"\(url.path)\"")
+ }
+ }
+
+ @objc func openUrl() {
+ let alert = NSAlert()
+ alert.messageText = "Open URL"
+ alert.icon = appIcon
+ alert.addButton(withTitle: "Ok")
+ alert.addButton(withTitle: "Cancel")
+
+ let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24))
+ input.placeholderString = "URL"
+ alert.accessoryView = input
+
+ DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
+ input.becomeFirstResponder()
+ }
+
+ if alert.runModal() == .alertFirstButtonReturn && input.stringValue.count > 0 {
+ appHub.input.open(files: [input.stringValue])
+ }
+ }
+
+ @objc func command(_ menuItem: MenuItem) {
+ guard let menuConfig = menuItem.config else { return }
+ appHub.input.command(menuConfig.command)
+ }
+
+ @objc func url(_ menuItem: MenuItem) {
+ guard let menuConfig = menuItem.config,
+ let url = URL(string: menuConfig.url) else { return }
+ NSWorkspace.shared.open(url)
+ }
+
+ @objc func showFile(_ menuItem: MenuItem) {
+ guard let menuConfig = menuItem.config else { return }
+ let url = URL(fileURLWithPath: menuConfig.url)
+ if FileManager.default.fileExists(atPath: url.path) {
+ NSWorkspace.shared.activateFileViewerSelecting([url])
+ return
+ }
+
+ alert(title: "No log File found.", text: "You deactivated logging for the Bundle.")
+ }
+
+ func alert(title: String, text: String) {
+ let alert = NSAlert()
+ alert.messageText = title
+ alert.informativeText = text
+ alert.icon = appIcon
+ alert.addButton(withTitle: "Ok")
+ alert.runModal()
+ }
+
+ func register(_ selector: Selector, key: Type) {
+ for menuItem in dynamicMenuItems[key] ?? [] {
+ menuItem.action = selector
+ }
+ }
+}
diff --git a/osdep/mac/meson.build b/osdep/mac/meson.build
new file mode 100644
index 0000000..8ddbdba
--- /dev/null
+++ b/osdep/mac/meson.build
@@ -0,0 +1,63 @@
+# custom swift targets
+bridge = join_paths(source_root, 'osdep/mac/app_bridge_objc.h')
+header = join_paths(build_root, 'osdep/mac/swift.h')
+module = join_paths(build_root, 'osdep/mac/swift.swiftmodule')
+target = join_paths(build_root, 'osdep/mac/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
+
+if macos_cocoa_cb.allowed()
+ swift_flags += ['-D', 'HAVE_MACOS_COCOA_CB']
+endif
+
+if macos_touchbar.allowed()
+ swift_flags += ['-D', 'HAVE_MACOS_TOUCHBAR']
+endif
+
+if macos_media_player.allowed()
+ swift_flags += ['-D', 'HAVE_MACOS_MEDIA_PLAYER']
+endif
+
+extra_flags = get_option('swift-flags').split()
+swift_flags += extra_flags
+
+swift_compile = [swift_prog, swift_flags, '-module-name', '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: ['swift.swiftmodule', 'swift.h', '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/mac/option_helper.swift b/osdep/mac/option_helper.swift
new file mode 100644
index 0000000..c44f090
--- /dev/null
+++ b/osdep/mac/option_helper.swift
@@ -0,0 +1,74 @@
+/*
+ * 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 OptionHelper {
+ var voCachePtr: UnsafeMutablePointer<m_config_cache>
+ var macCachePtr: UnsafeMutablePointer<m_config_cache>
+
+ var voPtr: UnsafeMutablePointer<mp_vo_opts>
+ { get { return UnsafeMutablePointer<mp_vo_opts>(OpaquePointer(voCachePtr.pointee.opts)) } }
+ var macPtr: UnsafeMutablePointer<macos_opts>
+ { get { return UnsafeMutablePointer<macos_opts>(OpaquePointer(macCachePtr.pointee.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 vo: mp_vo_opts { get { return voPtr.pointee } }
+ var mac: macos_opts { get { return macPtr.pointee } }
+
+ init(_ taParent: UnsafeMutableRawPointer, _ global: OpaquePointer?) {
+ voCachePtr = m_config_cache_alloc(taParent, global, AppHub.shared.getVoConf())
+ macCachePtr = m_config_cache_alloc(taParent, global, AppHub.shared.getMacConf())
+ }
+
+ func nextChangedOption(property: inout UnsafeMutableRawPointer?) -> Bool {
+ return m_config_cache_get_next_changed(voCachePtr, &property)
+ }
+
+ func setOption(fullscreen: Bool) {
+ voPtr.pointee.fullscreen = fullscreen
+ _ = withUnsafeMutableBytes(of: &voPtr.pointee.fullscreen) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(voCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setOption(minimized: Bool) {
+ voPtr.pointee.window_minimized = minimized
+ _ = withUnsafeMutableBytes(of: &voPtr.pointee.window_minimized) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(voCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setOption(maximized: Bool) {
+ voPtr.pointee.window_maximized = maximized
+ _ = withUnsafeMutableBytes(of: &voPtr.pointee.window_maximized) { (ptr: UnsafeMutableRawBufferPointer) in
+ m_config_cache_write_opt(voCachePtr, ptr.baseAddress)
+ }
+ }
+
+ func setMacOptionCallback(_ callback: swift_wakeup_cb_fn, context object: AnyObject) {
+ m_config_cache_set_wakeup_cb(macCachePtr, callback, TypeHelper.bridge(obj: object))
+ }
+
+ func nextChangedMacOption(property: inout UnsafeMutableRawPointer?) -> Bool {
+ return m_config_cache_get_next_changed(macCachePtr, &property)
+ }
+}
diff --git a/osdep/mac/precise_timer.swift b/osdep/mac/precise_timer.swift
new file mode 100644
index 0000000..d4837f9
--- /dev/null
+++ b/osdep/mac/precise_timer.swift
@@ -0,0 +1,152 @@
+/*
+ * 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
+
+ let nanoPerSecond: Double = 1e+9
+ let machToNano: Double = {
+ var timebase: mach_timebase_info = mach_timebase_info()
+ mach_timebase_info(&timebase)
+ return Double(timebase.numer) / Double(timebase.denom)
+ }()
+
+ let condition = NSCondition()
+ var events: [Timing] = []
+ var isRunning: Bool = true
+ var isHighPrecision: Bool = false
+
+ var thread: pthread_t!
+ var threadPort: thread_port_t = thread_port_t()
+ let policyFlavor = thread_policy_flavor_t(THREAD_TIME_CONSTRAINT_POLICY)
+ let policyCount = MemoryLayout<thread_time_constraint_policy>.size /
+ MemoryLayout<integer_t>.size
+ var typeNumber: mach_msg_type_number_t {
+ return mach_msg_type_number_t(policyCount)
+ }
+ var threadAttr: pthread_attr_t = {
+ var attr = pthread_attr_t()
+ var param = sched_param()
+ pthread_attr_init(&attr)
+ param.sched_priority = sched_get_priority_max(SCHED_FIFO)
+ pthread_attr_setschedparam(&attr, &param)
+ pthread_attr_setschedpolicy(&attr, SCHED_FIFO)
+ return attr
+ }()
+
+ init?(common com: Common) {
+ common = com
+
+ pthread_create(&thread, &threadAttr, entryC, TypeHelper.bridge(obj: self))
+ if thread == nil {
+ common.log.warning("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.warning("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 = TypeHelper.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/mac/presentation.swift b/osdep/mac/presentation.swift
new file mode 100644
index 0000000..c1d521a
--- /dev/null
+++ b/osdep/mac/presentation.swift
@@ -0,0 +1,56 @@
+/*
+ * 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/>.
+ */
+
+extension Presentation {
+ struct Time {
+ let cvTime: CVTimeStamp
+ var skipped: Int64 = 0
+ var time: Int64 { return mp_time_ns_from_raw_time(mp_raw_time_ns_from_mach(cvTime.hostTime)) }
+ var duration: Int64 {
+ let durationSeconds = Double(cvTime.videoRefreshPeriod) / Double(cvTime.videoTimeScale)
+ return Int64(durationSeconds * Presentation.nanoPerSecond * cvTime.rateScalar)
+ }
+
+ init(_ time: CVTimeStamp) {
+ cvTime = time
+ }
+ }
+}
+
+class Presentation {
+ unowned var common: Common
+ var times: [Time] = []
+ static let nanoPerSecond: Double = 1e+9
+
+ init(common com: Common) {
+ common = com
+ }
+
+ func add(time: CVTimeStamp) {
+ times.append(Time(time))
+ }
+
+ func next() -> Time? {
+ let now = mp_time_ns()
+ let count = times.count
+ times.removeAll(where: { $0.time <= now })
+ var time = times.first
+ time?.skipped = Int64(max(count - times.count - 1, 0))
+
+ return time
+ }
+}
diff --git a/osdep/mac/remote_command_center.swift b/osdep/mac/remote_command_center.swift
new file mode 100644
index 0000000..0e0eaeb
--- /dev/null
+++ b/osdep/mac/remote_command_center.swift
@@ -0,0 +1,202 @@
+/*
+ * 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 MediaPlayer
+
+extension RemoteCommandCenter {
+ typealias ConfigHandler = (MPRemoteCommandEvent) -> (MPRemoteCommandHandlerStatus)
+
+ enum KeyType {
+ case normal
+ case repeatable
+ }
+
+ struct Config {
+ let key: Int32
+ let type: KeyType
+ var state: NSEvent.EventType = .applicationDefined
+ let handler: ConfigHandler
+
+ init(key: Int32 = 0, type: KeyType = .normal, handler: @escaping ConfigHandler = { event in return .commandFailed }) {
+ self.key = key
+ self.type = type
+ self.handler = handler
+ }
+ }
+}
+
+class RemoteCommandCenter: EventSubscriber {
+ unowned let appHub: AppHub
+ var event: EventHelper? { get { return appHub.event } }
+ var input: InputHelper { get { return appHub.input } }
+ var configs: [MPRemoteCommand:Config] = [:]
+ var disabledCommands: [MPRemoteCommand] = []
+ var isPaused: Bool = false { didSet { updateInfoCenter() } }
+ var duration: Double = 0 { didSet { updateInfoCenter() } }
+ var position: Double = 0 { didSet { updateInfoCenter() } }
+ var rate: Double = 1 { didSet { updateInfoCenter() } }
+ var title: String = "" { didSet { updateInfoCenter() } }
+ var chapter: String? { didSet { updateInfoCenter() } }
+ var album: String? { didSet { updateInfoCenter() } }
+ var artist: String? { didSet { updateInfoCenter() } }
+ var cover: NSImage
+
+ var infoCenter: MPNowPlayingInfoCenter { get { return MPNowPlayingInfoCenter.default() } }
+ var commandCenter: MPRemoteCommandCenter { get { return MPRemoteCommandCenter.shared() } }
+
+ init(_ appHub: AppHub) {
+ self.appHub = appHub
+ cover = appHub.getIcon()
+
+ configs = [
+ commandCenter.pauseCommand: Config(key: MP_KEY_PAUSEONLY, handler: keyHandler),
+ commandCenter.playCommand: Config(key: MP_KEY_PLAYONLY, handler: keyHandler),
+ commandCenter.stopCommand: Config(key: MP_KEY_STOP, handler: keyHandler),
+ commandCenter.nextTrackCommand: Config(key: MP_KEY_NEXT, handler: keyHandler),
+ commandCenter.previousTrackCommand: Config(key: MP_KEY_PREV, handler: keyHandler),
+ commandCenter.togglePlayPauseCommand: Config(key: MP_KEY_PLAY, handler: keyHandler),
+ commandCenter.seekForwardCommand: Config(key: MP_KEY_FORWARD, type: .repeatable, handler: keyHandler),
+ commandCenter.seekBackwardCommand: Config(key: MP_KEY_REWIND, type: .repeatable, handler: keyHandler),
+ commandCenter.changePlaybackPositionCommand: Config(handler: seekHandler),
+ ]
+
+ disabledCommands = [
+ commandCenter.changePlaybackRateCommand,
+ commandCenter.changeRepeatModeCommand,
+ commandCenter.changeShuffleModeCommand,
+ commandCenter.skipForwardCommand,
+ commandCenter.skipBackwardCommand,
+ commandCenter.enableLanguageOptionCommand,
+ commandCenter.disableLanguageOptionCommand,
+ commandCenter.ratingCommand,
+ commandCenter.likeCommand,
+ commandCenter.dislikeCommand,
+ commandCenter.bookmarkCommand,
+ ]
+
+ for cmd in disabledCommands {
+ cmd.isEnabled = false
+ }
+ }
+
+ func registerEvents() {
+ event?.subscribe(self, event: .init(name: "duration", format: MPV_FORMAT_DOUBLE))
+ event?.subscribe(self, event: .init(name: "time-pos", format: MPV_FORMAT_DOUBLE))
+ event?.subscribe(self, event: .init(name: "speed", format: MPV_FORMAT_DOUBLE))
+ event?.subscribe(self, event: .init(name: "pause", format: MPV_FORMAT_FLAG))
+ event?.subscribe(self, event: .init(name: "media-title", format: MPV_FORMAT_STRING))
+ event?.subscribe(self, event: .init(name: "chapter-metadata/title", format: MPV_FORMAT_STRING))
+ event?.subscribe(self, event: .init(name: "metadata/by-key/album", format: MPV_FORMAT_STRING))
+ event?.subscribe(self, event: .init(name: "metadata/by-key/artist", format: MPV_FORMAT_STRING))
+ }
+
+ func start() {
+ for (cmd, config) in configs {
+ cmd.isEnabled = true
+ cmd.addTarget(handler: config.handler)
+ }
+
+ updateInfoCenter()
+
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(self.makeCurrent),
+ name: NSApplication.willBecomeActiveNotification,
+ object: nil
+ )
+ }
+
+ func stop() {
+ for (cmd, _) in configs {
+ cmd.isEnabled = false
+ cmd.removeTarget(nil)
+ }
+
+ infoCenter.nowPlayingInfo = nil
+ infoCenter.playbackState = .unknown
+
+ NotificationCenter.default.removeObserver(
+ self,
+ name: NSApplication.willBecomeActiveNotification,
+ object: nil
+ )
+ }
+
+ @objc func makeCurrent(notification: NSNotification) {
+ infoCenter.playbackState = .paused
+ infoCenter.playbackState = .playing
+ updateInfoCenter()
+ }
+
+ func updateInfoCenter() {
+ infoCenter.playbackState = isPaused ? .paused : .playing
+ infoCenter.nowPlayingInfo = (infoCenter.nowPlayingInfo ?? [:]).merging([
+ MPNowPlayingInfoPropertyMediaType: NSNumber(value: MPNowPlayingInfoMediaType.video.rawValue),
+ MPNowPlayingInfoPropertyPlaybackProgress: NSNumber(value: 0.0),
+ MPNowPlayingInfoPropertyPlaybackRate: NSNumber(value: isPaused ? 0 : rate),
+ MPNowPlayingInfoPropertyElapsedPlaybackTime: NSNumber(value: position),
+ MPMediaItemPropertyPlaybackDuration: NSNumber(value: duration),
+ MPMediaItemPropertyTitle: title,
+ MPMediaItemPropertyArtist: artist ?? chapter ?? "",
+ MPMediaItemPropertyAlbumTitle: album ?? "",
+ MPMediaItemPropertyArtwork: MPMediaItemArtwork(boundsSize: cover.size) { _ in return self.cover }
+ ]) { (_, new) in new }
+ }
+
+ lazy var keyHandler: ConfigHandler = { event in
+ guard let config = self.configs[event.command] else {
+ return .commandFailed
+ }
+
+ var state = config.state
+ if config.type == .repeatable {
+ state = config.state == .keyDown ? .keyUp : .keyDown
+ self.configs[event.command]?.state = state
+ }
+ self.input.put(key: config.key, type: state)
+
+ return .success
+ }
+
+ lazy var seekHandler: ConfigHandler = { event in
+ guard let posEvent = event as? MPChangePlaybackPositionCommandEvent else {
+ return .commandFailed
+ }
+
+ let cmd = String(format: "seek %.02f absolute", posEvent.positionTime)
+ return self.input.command(cmd) ? .success : .commandFailed
+ }
+
+ func handle(event: EventHelper.Event) {
+ switch event.name {
+ case "time-pos":
+ let newPosition = max(event.double ?? 0, 0)
+ if Int((floor(newPosition) - floor(position)) / rate) != 0 {
+ position = newPosition
+ }
+ case "pause": isPaused = event.bool ?? false
+ case "duration": duration = event.double ?? 0
+ case "speed": rate = event.double ?? 1
+ case "media-title": title = event.string ?? ""
+ case "chapter-metadata/title": chapter = event.string
+ case "metadata/by-key/album": album = event.string
+ case "metadata/by-key/artist": artist = event.string
+ default: break
+ }
+ }
+}
diff --git a/osdep/mac/swift_compat.swift b/osdep/mac/swift_compat.swift
new file mode 100644
index 0000000..83059da
--- /dev/null
+++ b/osdep/mac/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/mac/swift_extensions.swift b/osdep/mac/swift_extensions.swift
new file mode 100644
index 0000000..ed6c86c
--- /dev/null
+++ b/osdep/mac/swift_extensions.swift
@@ -0,0 +1,93 @@
+/*
+ * 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 IOKit.hidsystem
+
+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 NSEvent.ModifierFlags {
+ public static var optionLeft: NSEvent.ModifierFlags = .init(rawValue: UInt(NX_DEVICELALTKEYMASK))
+ public static var optionRight: NSEvent.ModifierFlags = .init(rawValue: UInt(NX_DEVICERALTKEYMASK))
+}
+
+extension mp_keymap {
+ init(_ f: Int, _ t: Int32) {
+ self.init(from: Int32(f), to: t)
+ }
+}
+
+extension mpv_event_id: CustomStringConvertible {
+ public var description: String {
+ switch self {
+ case MPV_EVENT_NONE: return "MPV_EVENT_NONE2"
+ case MPV_EVENT_SHUTDOWN: return "MPV_EVENT_SHUTDOWN"
+ case MPV_EVENT_LOG_MESSAGE: return "MPV_EVENT_LOG_MESSAGE"
+ case MPV_EVENT_GET_PROPERTY_REPLY: return "MPV_EVENT_GET_PROPERTY_REPLY"
+ case MPV_EVENT_SET_PROPERTY_REPLY: return "MPV_EVENT_SET_PROPERTY_REPLY"
+ case MPV_EVENT_COMMAND_REPLY: return "MPV_EVENT_COMMAND_REPLY"
+ case MPV_EVENT_START_FILE: return "MPV_EVENT_START_FILE"
+ case MPV_EVENT_END_FILE: return "MPV_EVENT_END_FILE"
+ case MPV_EVENT_FILE_LOADED: return "MPV_EVENT_FILE_LOADED"
+ case MPV_EVENT_IDLE: return "MPV_EVENT_IDLE"
+ case MPV_EVENT_TICK: return "MPV_EVENT_TICK"
+ case MPV_EVENT_CLIENT_MESSAGE: return "MPV_EVENT_CLIENT_MESSAGE"
+ case MPV_EVENT_VIDEO_RECONFIG: return "MPV_EVENT_VIDEO_RECONFIG"
+ case MPV_EVENT_AUDIO_RECONFIG: return "MPV_EVENT_AUDIO_RECONFIG"
+ case MPV_EVENT_SEEK: return "MPV_EVENT_SEEK"
+ case MPV_EVENT_PLAYBACK_RESTART: return "MPV_EVENT_PLAYBACK_RESTART"
+ case MPV_EVENT_PROPERTY_CHANGE: return "MPV_EVENT_PROPERTY_CHANGE"
+ case MPV_EVENT_QUEUE_OVERFLOW: return "MPV_EVENT_QUEUE_OVERFLOW"
+ case MPV_EVENT_HOOK: return "MPV_EVENT_HOOK"
+ default: return "MPV_EVENT_" + String(self.rawValue)
+ }
+ }
+}
+
+extension Bool {
+ init(_ int32: Int32) {
+ self.init(int32 != 0)
+ }
+}
+
+extension Int32 {
+ init(_ bool: Bool) {
+ self.init(bool ? 1 : 0)
+ }
+}
diff --git a/osdep/mac/touch_bar.swift b/osdep/mac/touch_bar.swift
new file mode 100644
index 0000000..8e64c51
--- /dev/null
+++ b/osdep/mac/touch_bar.swift
@@ -0,0 +1,297 @@
+/*
+ * 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 NSTouchBar.CustomizationIdentifier {
+ public static let customId: NSTouchBar.CustomizationIdentifier = "io.mpv.touchbar"
+}
+
+extension NSTouchBarItem.Identifier {
+ public static let seekBar = NSTouchBarItem.Identifier(custom: ".seekbar")
+ public static let play = NSTouchBarItem.Identifier(custom: ".play")
+ public static let nextItem = NSTouchBarItem.Identifier(custom: ".nextItem")
+ public static let previousItem = NSTouchBarItem.Identifier(custom: ".previousItem")
+ public static let nextChapter = NSTouchBarItem.Identifier(custom: ".nextChapter")
+ public static let previousChapter = NSTouchBarItem.Identifier(custom: ".previousChapter")
+ public static let cycleAudio = NSTouchBarItem.Identifier(custom: ".cycleAudio")
+ public static let cycleSubtitle = NSTouchBarItem.Identifier(custom: ".cycleSubtitle")
+ public static let currentPosition = NSTouchBarItem.Identifier(custom: ".currentPosition")
+ public static let timeLeft = NSTouchBarItem.Identifier(custom: ".timeLeft")
+
+ init(custom: String) {
+ self.init(NSTouchBar.CustomizationIdentifier.customId + custom)
+ }
+}
+
+extension TouchBar {
+ typealias ViewHandler = (Config) -> (NSView)
+
+ struct Config {
+ let name: String
+ let command: String
+ var item: NSCustomTouchBarItem?
+ var constraint: NSLayoutConstraint?
+ let image: NSImage
+ let imageAlt: NSImage
+ let handler: ViewHandler
+
+ init(
+ name: String = "",
+ command: String = "",
+ item: NSCustomTouchBarItem? = nil,
+ constraint: NSLayoutConstraint? = nil,
+ image: NSImage? = nil,
+ imageAlt: NSImage? = nil,
+ handler: @escaping ViewHandler = { _ in return NSButton(title: "", target: nil, action: nil) }
+ ) {
+ self.name = name
+ self.command = command
+ self.item = item
+ self.constraint = constraint
+ self.image = image ?? NSImage(size: NSSize(width: 1, height: 1))
+ self.imageAlt = imageAlt ?? NSImage(size: NSSize(width: 1, height: 1))
+ self.handler = handler
+ }
+ }
+}
+
+class TouchBar: NSTouchBar, NSTouchBarDelegate, EventSubscriber {
+ unowned let appHub: AppHub
+ var event: EventHelper? { get { return appHub.event } }
+ var input: InputHelper { get { return appHub.input } }
+ var configs: [NSTouchBarItem.Identifier:Config] = [:]
+ var isPaused: Bool = false { didSet { updatePlayButton() } }
+ var position: Double = 0 { didSet { updateTouchBarTimeItems() } }
+ var duration: Double = 0 { didSet { updateTouchBarTimeItems() } }
+ var rate: Double = 1
+
+ init(_ appHub: AppHub) {
+ self.appHub = appHub
+ super.init()
+
+ configs = [
+ .seekBar: Config(name: "Seek Bar", command: "seek %f absolute-percent", handler: createSlider),
+ .currentPosition: Config(name: "Current Position", handler: createText),
+ .timeLeft: Config(name: "Time Left", handler: createText),
+ .play: Config(
+ name: "Play Button",
+ command: "cycle pause",
+ image: .init(named: NSImage.touchBarPauseTemplateName),
+ imageAlt: .init(named: NSImage.touchBarPlayTemplateName),
+ handler: createButton
+ ),
+ .previousItem: Config(
+ name: "Previous Playlist Item",
+ command: "playlist-prev",
+ image: .init(named: NSImage.touchBarGoBackTemplateName),
+ handler: createButton
+ ),
+ .nextItem: Config(
+ name: "Next Playlist Item",
+ command: "playlist-next",
+ image: .init(named: NSImage.touchBarGoForwardTemplateName),
+ handler: createButton
+ ),
+ .previousChapter: Config(
+ name: "Previous Chapter",
+ command: "add chapter -1",
+ image: .init(named: NSImage.touchBarSkipBackTemplateName),
+ handler: createButton
+ ),
+ .nextChapter: Config(
+ name: "Next Chapter",
+ command: "add chapter 1",
+ image: .init(named: NSImage.touchBarSkipAheadTemplateName),
+ handler: createButton
+ ),
+ .cycleAudio: Config(
+ name: "Cycle Audio",
+ command: "cycle audio",
+ image: .init(named: NSImage.touchBarAudioInputTemplateName),
+ handler: createButton
+ ),
+ .cycleSubtitle: Config(
+ name: "Cycle Subtitle",
+ command: "cycle sub",
+ image: .init(named: NSImage.touchBarComposeTemplateName),
+ handler: createButton
+ )
+ ]
+
+ delegate = self
+ customizationIdentifier = .customId;
+ defaultItemIdentifiers = [.play, .previousItem, .nextItem, .seekBar]
+ customizationAllowedItemIdentifiers = [.play, .seekBar, .previousItem, .nextItem,
+ .previousChapter, .nextChapter, .cycleAudio, .cycleSubtitle, .currentPosition, .timeLeft]
+ addObserver(self, forKeyPath: "visible", options: [.new], context: nil)
+
+ event?.subscribe(self, event: .init(name: "duration", format: MPV_FORMAT_DOUBLE))
+ event?.subscribe(self, event: .init(name: "time-pos", format: MPV_FORMAT_DOUBLE))
+ event?.subscribe(self, event: .init(name: "speed", format: MPV_FORMAT_DOUBLE))
+ event?.subscribe(self, event: .init(name: "pause", format: MPV_FORMAT_FLAG))
+ event?.subscribe(self, event: .init(name: "MPV_EVENT_END_FILE"))
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
+ guard let config = configs[identifier] else { return nil }
+
+ let item = NSCustomTouchBarItem(identifier: identifier)
+ item.view = config.handler(config)
+ item.customizationLabel = config.name
+ configs[identifier]?.item = item
+ item.addObserver(self, forKeyPath: "visible", options: [.new], context: nil)
+ return item
+ }
+
+ lazy var createButton: ViewHandler = { config in
+ return NSButton(image: config.image, target: self, action: #selector(Self.buttonAction(_:)))
+ }
+
+ lazy var createText: ViewHandler = { config in
+ let text = NSTextField(labelWithString: "0:00")
+ text.alignment = .center
+ return text
+ }
+
+ lazy var createSlider: ViewHandler = { config in
+ let slider = NSSlider(target: self, action: #selector(Self.seekbarChanged(_:)))
+ slider.minValue = 0
+ slider.maxValue = 100
+ return slider
+ }
+
+ override func observeValue(
+ forKeyPath keyPath: String?,
+ of object: Any?,
+ change: [NSKeyValueChangeKey:Any]?,
+ context: UnsafeMutableRawPointer?
+ ) {
+ guard let visible = change?[.newKey] as? Bool else { return }
+ if keyPath == "isVisible" && visible {
+ updateTouchBarTimeItems()
+ updatePlayButton()
+ }
+ }
+
+ func updateTouchBarTimeItems() {
+ if !isVisible { return }
+ updateSlider()
+ updateTimeLeft()
+ updateCurrentPosition()
+ }
+
+ func updateSlider() {
+ guard let config = configs[.seekBar], let slider = config.item?.view as? NSSlider else { return }
+ if !(config.item?.isVisible ?? false) { return }
+
+ slider.isEnabled = duration > 0
+ if !slider.isHighlighted {
+ slider.doubleValue = slider.isEnabled ? (position / duration) * 100 : 0
+ }
+ }
+
+ func updateTimeLeft() {
+ guard let config = configs[.timeLeft], let text = config.item?.view as? NSTextField else { return }
+ if !(config.item?.isVisible ?? false) { return }
+
+ removeConstraintFor(identifier: .timeLeft)
+ text.stringValue = duration > 0 ? "-" + format(time: Int(floor(duration) - floor(position))) : ""
+ if !text.stringValue.isEmpty {
+ applyConstraintFrom(string: "-" + format(time: Int(duration)), identifier: .timeLeft)
+ }
+ }
+
+ func updateCurrentPosition() {
+ guard let config = configs[.currentPosition], let text = config.item?.view as? NSTextField else { return }
+ if !(config.item?.isVisible ?? false) { return }
+
+ text.stringValue = format(time: Int(floor(position)))
+ removeConstraintFor(identifier: .currentPosition)
+ applyConstraintFrom(string: format(time: Int(duration > 0 ? duration : position)), identifier: .currentPosition)
+ }
+
+ func updatePlayButton() {
+ guard let config = configs[.play], let button = config.item?.view as? NSButton else { return }
+ if !isVisible || !(config.item?.isVisible ?? false) { return }
+ button.image = isPaused ? configs[.play]?.imageAlt : configs[.play]?.image
+ }
+
+ @objc func buttonAction(_ button: NSButton) {
+ guard let identifier = getIdentifierFrom(view: button), let command = configs[identifier]?.command else { return }
+ input.command(command)
+ }
+
+ @objc func seekbarChanged(_ slider: NSSlider) {
+ guard let identifier = getIdentifierFrom(view: slider), let command = configs[identifier]?.command else { return }
+ input.command(String(format: command, slider.doubleValue))
+ }
+
+ func format(time: Int) -> String {
+ let formatter = DateComponentsFormatter()
+ formatter.unitsStyle = .positional
+ formatter.zeroFormattingBehavior = time >= (60 * 60) ? [.dropLeading] : []
+ formatter.allowedUnits = time >= (60 * 60) ? [.hour, .minute, .second] : [.minute, .second]
+ return formatter.string(from: TimeInterval(time)) ?? "0:00"
+ }
+
+ func removeConstraintFor(identifier: NSTouchBarItem.Identifier) {
+ guard let text = configs[identifier]?.item?.view as? NSTextField,
+ let constraint = configs[identifier]?.constraint as? NSLayoutConstraint else { return }
+ text.removeConstraint(constraint)
+ }
+
+ func applyConstraintFrom(string: String, identifier: NSTouchBarItem.Identifier) {
+ guard let text = configs[identifier]?.item?.view as? NSTextField else { return }
+ let fullString = string.components(separatedBy: .decimalDigits).joined(separator: "0")
+ let textField = NSTextField(labelWithString: fullString)
+ let con = NSLayoutConstraint(item: text, attribute: .width, relatedBy: .equal, toItem: nil,
+ attribute: .notAnAttribute, multiplier: 1.1, constant: ceil(textField.frame.size.width))
+ text.addConstraint(con)
+ configs[identifier]?.constraint = con
+ }
+
+ func getIdentifierFrom(view: NSView) -> NSTouchBarItem.Identifier? {
+ for (identifier, config) in configs {
+ if config.item?.view == view {
+ return identifier
+ }
+ }
+ return nil
+ }
+
+ func handle(event: EventHelper.Event) {
+ switch event.name {
+ case "MPV_EVENT_END_FILE":
+ position = 0
+ duration = 0
+ case "time-pos":
+ let newPosition = max(event.double ?? 0, 0)
+ if Int((floor(newPosition) - floor(position)) / rate) != 0 {
+ position = newPosition
+ }
+ case "pause": isPaused = event.bool ?? false
+ case "duration": duration = event.double ?? 0
+ case "speed": rate = event.double ?? 1
+ default: break
+ }
+ }
+}
diff --git a/osdep/mac/type_helper.swift b/osdep/mac/type_helper.swift
new file mode 100644
index 0000000..bd90d0a
--- /dev/null
+++ b/osdep/mac/type_helper.swift
@@ -0,0 +1,69 @@
+/*
+ * 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/>.
+ */
+
+class TypeHelper {
+ // (__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 toPointer<T>(_ value: inout T) -> UnsafeMutableRawPointer? {
+ return withUnsafeMutableBytes(of: &value) { (ptr: UnsafeMutableRawBufferPointer) in
+ ptr.baseAddress
+ }
+ }
+
+ // *(char **) MPV_FORMAT_STRING
+ class func toString(_ obj: UnsafeMutableRawPointer?) -> String? {
+ guard let str = obj else { return nil }
+ let cstr = UnsafeMutablePointer<UnsafeMutablePointer<Int8>>(OpaquePointer(str))
+ return String(cString: cstr[0])
+ }
+
+ // MPV_FORMAT_FLAG
+ class func toBool(_ obj: UnsafeMutableRawPointer) -> Bool? {
+ return UnsafePointer<Bool>(OpaquePointer(obj))?.pointee
+ }
+
+ // MPV_FORMAT_DOUBLE
+ class func toDouble(_ obj: UnsafeMutableRawPointer) -> Double? {
+ return UnsafePointer<Double>(OpaquePointer(obj))?.pointee
+ }
+}