summaryrefslogtreecommitdiffstats
path: root/video/out/cocoa_cb_common.swift
diff options
context:
space:
mode:
Diffstat (limited to 'video/out/cocoa_cb_common.swift')
-rw-r--r--video/out/cocoa_cb_common.swift485
1 files changed, 485 insertions, 0 deletions
diff --git a/video/out/cocoa_cb_common.swift b/video/out/cocoa_cb_common.swift
new file mode 100644
index 0000000000..9a6da82ecf
--- /dev/null
+++ b/video/out/cocoa_cb_common.swift
@@ -0,0 +1,485 @@
+/*
+ * 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.pwr_mgt
+
+class CocoaCB: NSObject {
+
+ var mpv: MPVHelper!
+ var window: Window!
+ var view: EventsView!
+ var layer: VideoLayer!
+ var link: CVDisplayLink?
+
+ var cursorHidden: Bool = false
+ var cursorVisibilityWanted: Bool = true
+ var isShuttingDown: Bool = false
+
+ enum State {
+ case uninit
+ case needsInit
+ case `init`
+ }
+ var backendState: State = .uninit
+
+ let eventsLock = NSLock()
+ var events: Int = 0
+
+ var lightSensor: io_connect_t = 0
+ var lastLmu: UInt64 = 0
+ var lightSensorIOPort: IONotificationPortRef?
+ var displaySleepAssertion: IOPMAssertionID = IOPMAssertionID(0)
+
+ let queue: DispatchQueue = DispatchQueue(label: "io.mpv.queue")
+
+ override init() {
+ super.init()
+ window = Window(cocoaCB: self)
+
+ view = EventsView(frame: window.contentView!.bounds, cocoaCB: self)
+ window.contentView!.addSubview(view)
+
+ layer = VideoLayer(cocoaCB: self)
+ view.layer = layer
+ view.wantsLayer = true
+ }
+
+ func setMpvHandle(_ ctx: OpaquePointer) {
+ mpv = MPVHelper(ctx)
+ layer.setUpGLCB()
+ }
+
+ func preinit() {
+ if backendState == .uninit {
+ backendState = .needsInit
+ DispatchQueue.main.async {
+ self.updateICCProfile()
+ }
+ startDisplayLink()
+ } else {
+ layer.setVideo(true)
+ }
+ }
+
+ func uninit() {
+ layer.setVideo(false)
+ window.orderOut(nil)
+ }
+
+ func reconfig() {
+ if backendState == .needsInit {
+ initBackend()
+ } else {
+ updateWindowSize()
+ layer.neededFlips += 1
+ }
+ }
+
+ func initBackend() {
+ NSApp.setActivationPolicy(.regular)
+
+ let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main()
+ let wr = getWindowGeometry(forScreen: targetScreen!, videoOut: mpv.mpctx!.pointee.video_out)
+ let win = Window(contentRect: wr, styleMask: window.styleMask,
+ screen: targetScreen, cocoaCB: self)
+ win.title = window.title
+ win.setOnTop(mpv.getBoolProperty("ontop"))
+ win.setBorder(mpv.getBoolProperty("border"))
+ win.keepAspect = mpv.getBoolProperty("keepaspect-window")
+ window.close()
+ window = win
+ window.contentView!.addSubview(view)
+ view.frame = window.contentView!.frame
+
+ setAppIcon()
+ window.isRestorable = false
+ window.makeMain()
+ window.makeKeyAndOrderFront(nil)
+ NSApp.activate(ignoringOtherApps: true)
+ layer.setVideo(true)
+
+ if self.mpv.getBoolProperty("fullscreen") {
+ window.toggleFullScreen(nil)
+ } else {
+ window.isMovableByWindowBackground = true
+ }
+
+ initLightSensor()
+ addDisplayReconfigureObserver()
+ backendState = .init
+ }
+
+ func updateWindowSize() {
+ if layer.hasVideo {
+ let targetScreen = getTargetScreen(forFullscreen: false) ?? NSScreen.main()
+ let wr = getWindowGeometry(forScreen: targetScreen!, videoOut: mpv.mpctx!.pointee.video_out)
+ if !window.isVisible {
+ window.makeKeyAndOrderFront(nil)
+ }
+ window.updateSize(wr.size)
+ }
+ }
+
+ func setAppIcon() {
+ if let app = NSApp as? Application {
+ NSApp.applicationIconImage = app.getMPVIcon()
+ }
+ }
+
+ func startDisplayLink() {
+ let displayId = UInt32(window.screen!.deviceDescription["NSScreenNumber"] as! Int)
+ CVDisplayLinkCreateWithActiveCGDisplays(&link)
+ CVDisplayLinkSetCurrentCGDisplay(link!, displayId)
+ CVDisplayLinkSetOutputHandler(link!) { link, now, out, inFlags, outFlags -> CVReturn in
+ self.layer.reportFlip()
+ return kCVReturnSuccess
+ }
+ CVDisplayLinkStart(link!)
+ }
+
+ func stopDisplaylink() {
+ if link != nil && CVDisplayLinkIsRunning(link!) {
+ CVDisplayLinkStop(link!)
+ }
+ }
+
+ func updateDisplaylink() {
+ let displayId = UInt32(window.screen!.deviceDescription["NSScreenNumber"] as! Int)
+ CVDisplayLinkSetCurrentCGDisplay(link!, displayId)
+
+ queue.asyncAfter(deadline: DispatchTime.now() + 0.1) {
+ self.flagEvents(VO_EVENT_WIN_STATE)
+ }
+ }
+
+ func currentFps() -> Double {
+ var actualFps = CVDisplayLinkGetActualOutputVideoRefreshPeriod(link!)
+ let nominalData = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link!)
+
+ if (nominalData.flags & Int32(CVTimeFlags.isIndefinite.rawValue)) < 1 {
+ let nominalFps = Double(nominalData.timeScale) / Double(nominalData.timeValue)
+
+ if actualFps > 0 {
+ actualFps = 1/actualFps
+ }
+
+ if fabs(actualFps - nominalFps) > 0.1 {
+ mpv.sendVerbose("Falling back to nominal display refresh rate: \(nominalFps)")
+ return nominalFps
+ } else {
+ return actualFps
+ }
+ }
+ mpv.sendWarning("Falling back to standard display refresh rate: 60Hz")
+ return 60.0
+ }
+
+ func enableDisplaySleep() {
+ IOPMAssertionRelease(displaySleepAssertion)
+ displaySleepAssertion = IOPMAssertionID(0)
+ }
+
+ func disableDisplaySleep() {
+ if displaySleepAssertion != IOPMAssertionID(0) { return }
+ IOPMAssertionCreateWithName(
+ kIOPMAssertionTypePreventUserIdleDisplaySleep as CFString,
+ IOPMAssertionLevel(kIOPMAssertionLevelOn),
+ "io.mpv.video_playing_back" as CFString,
+ &displaySleepAssertion)
+ }
+
+ func updateCusorVisibility() {
+ setCursorVisiblility(cursorVisibilityWanted)
+ }
+
+ func setCursorVisiblility(_ visible: Bool) {
+ let visibility = visible ? true : !view.canHideCursor()
+
+ if visibility && cursorHidden {
+ NSCursor.unhide()
+ cursorHidden = false;
+ } else if !visibility && !cursorHidden {
+ NSCursor.hide()
+ cursorHidden = true
+ }
+ }
+
+ func updateICCProfile() {
+ if mpv.getBoolProperty("icc-profile-auto") {
+ mpv.setGLCBICCProfile(window.screen!.colorSpace!)
+ }
+ layer.colorspace = window.screen!.colorSpace!.cgColorSpace!
+ }
+
+ func lmuToLux(_ v: UInt64) -> Int {
+ // the polinomial approximation for apple lmu value -> lux was empirically
+ // derived by firefox developers (Apple provides no documentation).
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=793728
+ let power_c4 = 1 / pow(10, 27)
+ let power_c3 = 1 / pow(10, 19)
+ let power_c2 = 1 / pow(10, 12)
+ let power_c1 = 1 / pow(10, 5)
+
+ let term4 = -3.0 * power_c4 * pow(Decimal(v), 4)
+ let term3 = 2.6 * power_c3 * pow(Decimal(v), 3)
+ let term2 = -3.4 * power_c2 * pow(Decimal(v), 2)
+ let term1 = 3.9 * power_c1 * Decimal(v)
+
+ let lux = Int(ceil( Double((term4 + term3 + term2 + term1 - 0.19) as NSNumber)))
+ return Int(lux > 0 ? lux : 0)
+ }
+
+ var lightSensorCallback: IOServiceInterestCallback = { (ctx, service, messageType, messageArgument) -> Void in
+ let ccb: CocoaCB = MPVHelper.bridge(ptr: ctx!)
+
+ var outputs: UInt32 = 2
+ var values: [UInt64] = [0, 0]
+
+ var kr = IOConnectCallMethod(ccb.lightSensor, 0, nil, 0, nil, 0, &values, &outputs, nil, nil)
+ if kr == KERN_SUCCESS {
+ var mean = (values[0] + values[1]) / 2
+ if ccb.lastLmu != mean {
+ ccb.lastLmu = mean
+ ccb.mpv.setGLCBLux(ccb.lmuToLux(ccb.lastLmu))
+ }
+ }
+ }
+
+ func initLightSensor() {
+ let srv = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleLMUController"))
+ if srv == IO_OBJECT_NULL {
+ mpv.sendVerbose("Can't find an ambient light sensor")
+ return
+ }
+
+ lightSensorIOPort = IONotificationPortCreate(kIOMasterPortDefault)
+ IONotificationPortSetDispatchQueue(lightSensorIOPort, queue)
+ var n = io_object_t()
+ IOServiceAddInterestNotification(lightSensorIOPort, srv, kIOGeneralInterest, lightSensorCallback, MPVHelper.bridge(obj: self), &n)
+ let kr = IOServiceOpen(srv, mach_task_self_, 0, &lightSensor)
+ IOObjectRelease(srv)
+
+ if kr != KERN_SUCCESS {
+ mpv.sendVerbose("Can't start ambient light sensor connection")
+ return
+ }
+ lightSensorCallback(MPVHelper.bridge(obj: self), 0, 0, nil)
+ }
+
+ func uninitLightSensor() {
+ if lightSensorIOPort != nil {
+ IONotificationPortDestroy(lightSensorIOPort)
+ IOObjectRelease(lightSensor)
+ }
+ }
+
+ var reconfigureCallback: CGDisplayReconfigurationCallBack = { (display, flags, userInfo) in
+ if flags.contains(.setModeFlag) {
+ let ccb: CocoaCB = MPVHelper.bridge(ptr: userInfo!)
+ let displayID = (ccb.window.screen!.deviceDescription["NSScreenNumber"] as! NSNumber).intValue
+ if UInt32(displayID) == display {
+ ccb.mpv.sendVerbose("Detected display mode change, updating screen refresh rate\n");
+ ccb.flagEvents(VO_EVENT_WIN_STATE)
+ }
+ }
+ }
+
+ func addDisplayReconfigureObserver() {
+ CGDisplayRegisterReconfigurationCallback(reconfigureCallback, MPVHelper.bridge(obj: self))
+ }
+
+ func removeDisplayReconfigureObserver() {
+ CGDisplayRemoveReconfigurationCallback(reconfigureCallback, MPVHelper.bridge(obj: self))
+ }
+
+ func getTargetScreen(forFullscreen fs: Bool) -> NSScreen? {
+ let screenID = fs ? mpv.getStringProperty("fs-screen") ?? "current":
+ mpv.getStringProperty("screen") ?? "current"
+
+ switch screenID {
+ case "current", "default", "all":
+ return getScreenBy(id: -1)
+ default:
+ return getScreenBy(id: Int(screenID)!)
+ }
+ }
+
+ func getScreenBy(id screenID: Int) -> NSScreen? {
+ let screens = NSScreen.screens()
+ if screenID >= screens!.count {
+ mpv.sendInfo("Screen ID \(screenID) does not exist, falling back to current device")
+ return nil
+ } else if screenID < 0 {
+ return nil
+ }
+ return screens![screenID]
+ }
+
+ func getWindowGeometry(forScreen targetScreen: NSScreen,
+ videoOut vo: UnsafeMutablePointer<vo>) -> NSRect {
+ let r = targetScreen.convertRectToBacking(targetScreen.frame)
+ var screenRC: mp_rect = mp_rect(x0: Int32(0),
+ y0: Int32(0),
+ x1: Int32(r.size.width),
+ y1: Int32(r.size.height))
+
+ var geo: vo_win_geometry = vo_win_geometry()
+ vo_calc_window_geometry2(vo, &screenRC, Double(targetScreen.backingScaleFactor), &geo)
+
+ // flip y coordinates
+ geo.win.y1 = Int32(r.size.height) - geo.win.y1
+ geo.win.y0 = Int32(r.size.height) - geo.win.y0
+
+ let wr = NSMakeRect(CGFloat(geo.win.x0), CGFloat(geo.win.y1),
+ CGFloat(geo.win.x1 - geo.win.x0),
+ CGFloat(geo.win.y0 - geo.win.y1))
+ return targetScreen.convertRectFromBacking(wr)
+ }
+
+ func flagEvents(_ ev: Int) {
+ eventsLock.lock()
+ events |= ev
+ eventsLock.unlock()
+ }
+
+ func checkEvents() -> Int {
+ eventsLock.lock()
+ let ev = events
+ events = 0
+ eventsLock.unlock()
+ return ev
+ }
+
+ var controlCallback: mpv_opengl_cb_control_fn = { ( ctx, events, request, data ) -> Int32 in
+ let ccb: CocoaCB = MPVHelper.bridge(ptr: ctx!)
+
+ switch mp_voctrl(request) {
+ case VOCTRL_CHECK_EVENTS:
+ events!.pointee = Int32(ccb.checkEvents())
+ return VO_TRUE
+ case VOCTRL_FULLSCREEN:
+ DispatchQueue.main.async {
+ ccb.window.toggleFullScreen(nil)
+ }
+ return VO_TRUE
+ case VOCTRL_GET_FULLSCREEN:
+ let fsData = data!.assumingMemoryBound(to: Int32.self)
+ fsData.pointee = ccb.window.isInFullscreen ? 1 : 0
+ return VO_TRUE
+ case VOCTRL_GET_DISPLAY_FPS:
+ let fps = data!.assumingMemoryBound(to: CDouble.self)
+ fps.pointee = ccb.currentFps()
+ return VO_TRUE
+ case VOCTRL_RESTORE_SCREENSAVER:
+ ccb.enableDisplaySleep()
+ return VO_TRUE
+ case VOCTRL_KILL_SCREENSAVER:
+ ccb.disableDisplaySleep()
+ return VO_TRUE
+ case VOCTRL_SET_CURSOR_VISIBILITY:
+ ccb.cursorVisibilityWanted = data!.assumingMemoryBound(to: CBool.self).pointee
+ DispatchQueue.main.async {
+ ccb.setCursorVisiblility(ccb.cursorVisibilityWanted)
+ }
+ return VO_TRUE
+ case VOCTRL_SET_UNFS_WINDOW_SIZE:
+ let sizeData = data!.assumingMemoryBound(to: Int32.self)
+ let size = UnsafeBufferPointer(start: sizeData, count: 2)
+ var rect = NSMakeRect(0, 0, CGFloat(size[0]), CGFloat(size[1]))
+ DispatchQueue.main.async {
+ if !ccb.mpv.getBoolProperty("hidpi-window-scale") {
+ rect = ccb.window.currentScreen!.convertRectFromBacking(rect)
+ }
+ ccb.window.updateSize(rect.size)
+ }
+ return VO_TRUE
+ case VOCTRL_GET_WIN_STATE:
+ let minimized = data!.assumingMemoryBound(to: Int32.self)
+ minimized.pointee = ccb.window.isMiniaturized ? VO_WIN_STATE_MINIMIZED : Int32(0)
+ return VO_TRUE
+ case VOCTRL_UPDATE_WINDOW_TITLE:
+ let titleData = data!.assumingMemoryBound(to: Int8.self)
+ let title = String(cString: titleData)
+ DispatchQueue.main.async {
+ ccb.window.title = String(cString: titleData)
+ }
+ return VO_TRUE
+ case VOCTRL_PREINIT:
+ ccb.preinit()
+ return VO_TRUE
+ case VOCTRL_UNINIT:
+ DispatchQueue.main.async {
+ ccb.uninit()
+ }
+ return VO_TRUE
+ case VOCTRL_RECONFIG:
+ DispatchQueue.main.async {
+ ccb.reconfig()
+ }
+ return VO_TRUE
+ default:
+ return VO_NOTIMPL
+ }
+ }
+
+ func processEvent(_ event: UnsafePointer<mpv_event>) {
+ switch event.pointee.event_id {
+ case MPV_EVENT_SHUTDOWN:
+ if window.isAnimating {
+ isShuttingDown = true
+ return
+ }
+ setCursorVisiblility(true)
+ stopDisplaylink()
+ uninitLightSensor()
+ removeDisplayReconfigureObserver()
+ mpv.deinitGLCB()
+ mpv.deinitMPV()
+ case MPV_EVENT_PROPERTY_CHANGE:
+ if backendState == .init {
+ handlePropertyChange(event)
+ }
+ default:
+ break
+ }
+ }
+
+ func handlePropertyChange(_ event: UnsafePointer<mpv_event>) {
+ let pData = OpaquePointer(event.pointee.data)
+ guard let property = UnsafePointer<mpv_event_property>(pData)?.pointee else {
+ return
+ }
+
+ switch String(cString: property.name) {
+ case "border":
+ if let data = MPVHelper.mpvFlagToBool(property.data) {
+ window.setBorder(data)
+ }
+ case "ontop":
+ if let data = MPVHelper.mpvFlagToBool(property.data) {
+ window.setOnTop(data)
+ }
+ case "keepaspect-window":
+ if let data = MPVHelper.mpvFlagToBool(property.data) {
+ window.keepAspect = data
+ }
+ default:
+ break
+ }
+ }
+}