diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:13:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-04 01:13:14 +0000 |
commit | 60e8a3d404f0640fa5a3f834eae54b4f1fb9127d (patch) | |
tree | 1da89a218d0ecf010c67a87cb2f625c4cb18e7d7 /osdep/mac | |
parent | Adding upstream version 0.37.0. (diff) | |
download | mpv-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.h | 59 | ||||
-rw-r--r-- | osdep/mac/app_bridge.m | 123 | ||||
-rw-r--r-- | osdep/mac/app_bridge_objc.h | 56 | ||||
-rw-r--r-- | osdep/mac/app_hub.swift | 130 | ||||
-rw-r--r-- | osdep/mac/application.swift | 123 | ||||
-rw-r--r-- | osdep/mac/event_helper.swift | 147 | ||||
-rw-r--r-- | osdep/mac/input_helper.swift | 275 | ||||
-rw-r--r-- | osdep/mac/libmpv_helper.swift | 181 | ||||
-rw-r--r-- | osdep/mac/log_helper.swift | 67 | ||||
-rw-r--r-- | osdep/mac/menu_bar.swift | 397 | ||||
-rw-r--r-- | osdep/mac/meson.build | 63 | ||||
-rw-r--r-- | osdep/mac/option_helper.swift | 74 | ||||
-rw-r--r-- | osdep/mac/precise_timer.swift | 152 | ||||
-rw-r--r-- | osdep/mac/presentation.swift | 56 | ||||
-rw-r--r-- | osdep/mac/remote_command_center.swift | 202 | ||||
-rw-r--r-- | osdep/mac/swift_compat.swift | 36 | ||||
-rw-r--r-- | osdep/mac/swift_extensions.swift | 93 | ||||
-rw-r--r-- | osdep/mac/touch_bar.swift | 297 | ||||
-rw-r--r-- | osdep/mac/type_helper.swift | 69 |
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, ¶ms) < 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, ¶ms); + } + } 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, ¶m) + 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 + } +} |