From c5e4538bc4b63f02bf89f62aa25a37b9eb0c0316 Mon Sep 17 00:00:00 2001 From: Akemi Date: Mon, 12 Feb 2018 12:28:19 +0100 Subject: cocoa-cb: initial implementation via opengl-cb API this is meant to replace the old and not properly working vo_gpu/opengl cocoa backend in the future. the problems are various shortcomings of Apple's opengl implementation and buggy behaviour in certain circumstances that couldn't be properly worked around. there are also certain regressions on newer macOS versions from 10.11 onwards. - awful opengl performance with a none layer backed context - huge amount of dropped frames with an early context flush - flickering of system elements like the dock or volume indicator - double buffering not properly working with a none layer backed context - bad performance in fullscreen because of system optimisations all the problems were caused by using a normal opengl context, that seems somewhat abandoned by apple, and are fixed by using a layer backed opengl context instead. problems that couldn't be fixed could be properly worked around. this has all features our old backend has sans the wid embedding, the possibility to disable the automatic GPU switching and taking screenshots of the window content. the first was deemed unnecessary by me for now, since i just use the libmpv API that others can use anyway. second is technically not possible atm because we have to pre-allocate our opengl context at a time the config isn't read yet, so we can't get the needed property. third one is a bit tricky because of deadlocking and it needed to be in sync, hopefully i can work around that in the future. this also has at least one additional feature or eye-candy. a properly working fullscreen animation with the native fs. also since this is a direct port of the old backend of the parts that could be used, though with adaptions and improvements, this looks a lot cleaner and easier to understand. some credit goes to @pigoz for the initial swift build support which i could improve upon. Fixes: #5478, #5393, #5152, #5151, #4615, #4476, #3978, #3746, #3739, #2392, #2217 --- video/out/cocoa-cb/events_view.swift | 266 +++++++++++++++++++ video/out/cocoa-cb/video_layer.swift | 210 +++++++++++++++ video/out/cocoa-cb/window.swift | 456 ++++++++++++++++++++++++++++++++ video/out/cocoa_cb_common.swift | 485 +++++++++++++++++++++++++++++++++++ video/out/opengl/context_cocoa.c | 2 + video/out/vo.h | 5 + video/out/vo_opengl_cb.c | 88 +++++-- 7 files changed, 1492 insertions(+), 20 deletions(-) create mode 100644 video/out/cocoa-cb/events_view.swift create mode 100644 video/out/cocoa-cb/video_layer.swift create mode 100644 video/out/cocoa-cb/window.swift create mode 100644 video/out/cocoa_cb_common.swift (limited to 'video/out') diff --git a/video/out/cocoa-cb/events_view.swift b/video/out/cocoa-cb/events_view.swift new file mode 100644 index 0000000000..4cb154c64a --- /dev/null +++ b/video/out/cocoa-cb/events_view.swift @@ -0,0 +1,266 @@ +/* + * 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 . + */ + +import Cocoa + +class EventsView: NSView { + + weak var cocoaCB: CocoaCB! + var mpv: MPVHelper! { + get { return cocoaCB == nil ? nil : cocoaCB.mpv } + } + + var tracker: NSTrackingArea? + var hasMouseDown: Bool = false + + override var isFlipped: Bool { return true } + override var acceptsFirstResponder: Bool { return true } + + + init(frame frameRect: NSRect, cocoaCB ccb: CocoaCB) { + cocoaCB = ccb + super.init(frame: frameRect) + autoresizingMask = [.viewWidthSizable, .viewHeightSizable] + wantsBestResolutionOpenGLSurface = true + register(forDraggedTypes: [NSFilenamesPboardType, NSURLPboardType]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func updateTrackingAreas() { + if tracker != nil { + removeTrackingArea(tracker!) + } + + if mpv != nil && !mpv.getBoolProperty("input-cursor") { + return + } + + tracker = NSTrackingArea(rect: self.bounds, + options: [.activeAlways, .mouseEnteredAndExited, .mouseMoved, .enabledDuringMouseDrag], + owner: self, userInfo: nil) + addTrackingArea(tracker!) + + if containsMouseLocation() { + cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0) + } + } + + override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { + guard let types = sender.draggingPasteboard().types else { return [] } + if types.contains(NSFilenamesPboardType) || types.contains(NSURLPboardType) { + return .copy + } + return [] + } + + override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { + let pb = sender.draggingPasteboard() + guard let types = sender.draggingPasteboard().types else { return false } + if types.contains(NSFilenamesPboardType) { + if let files = pb.propertyList(forType: NSFilenamesPboardType) as? [Any] { + EventsResponder.sharedInstance().handleFilesArray(files) + return true + } + } else if types.contains(NSURLPboardType) { + if let url = pb.propertyList(forType: NSURLPboardType) as? [Any] { + EventsResponder.sharedInstance().handleFilesArray(url) + return true + } + } + return false + } + + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + return mpv.getBoolProperty("input-cursor") + } + + override func becomeFirstResponder() -> Bool { + return mpv.getBoolProperty("input-cursor") || + mpv.getBoolProperty("input-vo-keyboard") + } + + override func resignFirstResponder() -> Bool { + return true + } + + override func mouseEntered(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_ENTER, 0) + } + } + + override func mouseExited(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0) + } + } + + override func mouseMoved(with event: NSEvent) { + if mpv != nil && mpv.getBoolProperty("input-cursor") { + signalMouseMovement(event) + } + } + + override func mouseDragged(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseMovement(event) + } + } + + override func mouseDown(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseDown(event) + } + } + + override func mouseUp(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseUp(event) + } + cocoaCB.window.isMoving = false + } + + override func rightMouseDown(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseDown(event) + } + } + + override func rightMouseUp(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseUp(event) + } + } + + override func otherMouseDown(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseDown(event) + } + } + + override func otherMouseUp(with event: NSEvent) { + if mpv.getBoolProperty("input-cursor") { + signalMouseUp(event) + } + } + + func signalMouseDown(_ event: NSEvent) { + signalMouseEvent(event, SWIFT_KEY_STATE_DOWN) + if event.clickCount > 1 { + signalMouseEvent(event, SWIFT_KEY_STATE_UP) + } + } + + func signalMouseUp(_ event: NSEvent) { + signalMouseEvent(event, SWIFT_KEY_STATE_UP) + } + + func signalMouseEvent(_ event: NSEvent, _ state: Int32) { + hasMouseDown = state == SWIFT_KEY_STATE_DOWN + let mpkey = getMpvButton(event) + cocoa_put_key_with_modifiers((mpkey | state), Int32(event.modifierFlags.rawValue)); + } + + func signalMouseMovement(_ event: NSEvent) { + var point = convert(event.locationInWindow, from: nil) + point = convertToBacking(point) + point.y = -point.y + + cocoaCB.window.updateMovableBackground(point) + if !cocoaCB.window.isMoving { + mpv.setMousePosition(point) + } + } + + func preciseScroll(_ event: NSEvent) { + var delta: Double + var cmd: Int32 + + if fabs(event.deltaY) >= fabs(event.deltaX) { + delta = Double(event.deltaY) * 0.1; + cmd = delta > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN; + } else { + delta = Double(event.deltaX) * 0.1; + cmd = delta > 0 ? SWIFT_WHEEL_RIGHT : SWIFT_WHEEL_LEFT; + } + + mpv.putAxis(cmd, delta: fabs(delta)) + } + + override func scrollWheel(with event: NSEvent) { + if !mpv.getBoolProperty("input-cursor") { + return + } + + if event.hasPreciseScrollingDeltas { + preciseScroll(event) + } else { + let modifiers = event.modifierFlags + let deltaX = modifiers.contains(.shift) ? event.scrollingDeltaY : event.scrollingDeltaX + let deltaY = modifiers.contains(.shift) ? event.scrollingDeltaX : event.scrollingDeltaY + var mpkey: Int32 + + if fabs(deltaY) >= fabs(deltaX) { + mpkey = deltaY > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN; + } else { + mpkey = deltaX > 0 ? SWIFT_WHEEL_RIGHT : SWIFT_WHEEL_LEFT; + } + + cocoa_put_key_with_modifiers(mpkey, Int32(modifiers.rawValue)) + } + } + + func containsMouseLocation() -> Bool { + if cocoaCB == nil { return false } + var topMargin: CGFloat = 0.0 + let menuBarHeight = NSApp.mainMenu!.menuBarHeight + + if cocoaCB.window.isInFullscreen && (menuBarHeight > 0) { + let titleBar = NSWindow.frameRect(forContentRect: CGRect.zero, styleMask: .titled) + topMargin = titleBar.size.height + 1 + menuBarHeight + } + + var vF = window!.screen!.frame + vF.size.height -= topMargin + + let vFW = window!.convertFromScreen(vF) + let vFV = convert(vFW, from: nil) + let pt = convert(window!.mouseLocationOutsideOfEventStream, from: nil) + + let clippedBounds = bounds.intersection(vFV) + return clippedBounds.contains(pt) + } + + func canHideCursor() -> Bool { + return !hasMouseDown && containsMouseLocation() && window!.isKeyWindow + } + + func getMpvButton(_ event: NSEvent) -> Int32 { + let buttonNumber = event.buttonNumber + switch (buttonNumber) { + case 0: return SWIFT_MBTN_LEFT; + case 1: return SWIFT_MBTN_RIGHT; + case 2: return SWIFT_MBTN_MID; + case 3: return SWIFT_MBTN_BACK; + case 4: return SWIFT_MBTN_FORWARD; + default: return SWIFT_MBTN9 + Int32(buttonNumber - 5); + } + } +} diff --git a/video/out/cocoa-cb/video_layer.swift b/video/out/cocoa-cb/video_layer.swift new file mode 100644 index 0000000000..05f0894159 --- /dev/null +++ b/video/out/cocoa-cb/video_layer.swift @@ -0,0 +1,210 @@ +/* + * 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 . + */ + +import Cocoa +import OpenGL.GL +import OpenGL.GL3 + +class VideoLayer: CAOpenGLLayer { + + weak var cocoaCB: CocoaCB! + var mpv: MPVHelper! { + get { return cocoaCB == nil ? nil : cocoaCB.mpv } + } + + let videoLock = NSLock() + var hasVideo: Bool = false + var neededFlips: Int = 0 + var cglContext: CGLContextObj? = nil + + var canDrawOffScreen: Bool = false + var lastThread: Thread? = nil + + var needsICCUpdate: Bool = false { + didSet { + if needsICCUpdate == true { + neededFlips += 1 + } + } + } + + let surfaceLock = NSLock() + var surfaceSize: NSSize? + + var inLiveResize: Bool = false { + didSet { + if inLiveResize == false { + isAsynchronous = false + display() + } else { + surfaceLock.lock() + updateSurfaceSize() + surfaceLock.unlock() + isAsynchronous = true + } + } + } + + init(cocoaCB ccb: CocoaCB) { + cocoaCB = ccb + super.init() + autoresizingMask = [.layerWidthSizable, .layerHeightSizable] + backgroundColor = NSColor.black.cgColor + contentsScale = cocoaCB.window.backingScaleFactor + } + + override init(layer: Any) { + let oldLayer = layer as! VideoLayer + cocoaCB = oldLayer.cocoaCB + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setUpGLCB() { + self.mpv.initGLCB() + self.mpv.setGLCBUpdateCallback(self.updateCallback, context: self) + self.mpv.setGLCBControlCallback(self.cocoaCB.controlCallback, context: self.cocoaCB) + } + + override func canDraw(inCGLContext ctx: CGLContextObj, + pixelFormat pf: CGLPixelFormatObj, + forLayerTime t: CFTimeInterval, + displayTime ts: UnsafePointer?) -> Bool { + return mpv != nil && cocoaCB.backendState == .init + } + + override func draw(inCGLContext ctx: CGLContextObj, + pixelFormat pf: CGLPixelFormatObj, + forLayerTime t: CFTimeInterval, + displayTime ts: UnsafePointer?) { + neededFlips = 0 + canDrawOffScreen = Thread.current == lastThread + lastThread = Thread.current + draw(ctx) + } + + func draw(_ ctx: CGLContextObj) { + surfaceLock.lock() + if inLiveResize == false { + updateSurfaceSize() + } + + mpv.drawGLCB(surfaceSize!) + surfaceLock.unlock() + CGLFlushDrawable(ctx) + + if needsICCUpdate { + needsICCUpdate = false + cocoaCB.updateICCProfile() + } + } + + func updateSurfaceSize() { + surfaceSize = bounds.size + surfaceSize!.width *= contentsScale + surfaceSize!.height *= contentsScale + } + + override func copyCGLPixelFormat(forDisplayMask mask: UInt32) -> CGLPixelFormatObj { + let glVersions: [CGLOpenGLProfile] = [ + kCGLOGLPVersion_3_2_Core, + kCGLOGLPVersion_Legacy + ] + + var pix: CGLPixelFormatObj? + var err: CGLError = CGLError(rawValue: 0) + var npix: GLint = 0 + + verLoop : for ver in glVersions { + var glAttributes: [CGLPixelFormatAttribute] = [ + kCGLPFAOpenGLProfile, CGLPixelFormatAttribute(ver.rawValue), + kCGLPFAAccelerated, + kCGLPFADoubleBuffer, + kCGLPFABackingStore, + kCGLPFAAllowOfflineRenderers, + kCGLPFASupportsAutomaticGraphicsSwitching, + _CGLPixelFormatAttribute(rawValue: 0) + ] + + for index in stride(from: glAttributes.count-2, through: 4, by: -1) { + err = CGLChoosePixelFormat(glAttributes, &pix, &npix) + if err == kCGLBadAttribute { + glAttributes.remove(at: index) + } else { + break verLoop + } + } + } + + if err != kCGLNoError { + fatalError("Couldn't create CGL pixel format: \(CGLErrorString(err)) (\(err))") + } + return pix! + } + + override func copyCGLContext(forPixelFormat pf: CGLPixelFormatObj) -> CGLContextObj { + let ctx = super.copyCGLContext(forPixelFormat: pf) + var i: GLint = 1 + CGLSetParameter(ctx, kCGLCPSwapInterval, &i) + CGLSetCurrentContext(ctx) + cglContext = ctx + + if let app = NSApp as? Application { + app.initMPVCore() + } + return ctx + } + + let updateCallback: mpv_opengl_cb_update_fn = { (ctx) in + let layer: VideoLayer = MPVHelper.bridge(ptr: ctx!) + layer.neededFlips += 1 + } + + override func display() { + super.display() + if !isAsynchronous { + CATransaction.flush() + } + } + + func setVideo(_ state: Bool) { + videoLock.lock() + hasVideo = state + neededFlips = 0 + videoLock.unlock() + } + + func reportFlip() { + mpv.reportGLCBFlip() + videoLock.lock() + if !isAsynchronous && neededFlips > 0 && hasVideo { + if !cocoaCB.window.occlusionState.contains(.visible) && + neededFlips > 1 && canDrawOffScreen + { + draw(cglContext!) + display() + } else { + display() + } + } + videoLock.unlock() + } + +} diff --git a/video/out/cocoa-cb/window.swift b/video/out/cocoa-cb/window.swift new file mode 100644 index 0000000000..c93537c03a --- /dev/null +++ b/video/out/cocoa-cb/window.swift @@ -0,0 +1,456 @@ +/* + * 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 . + */ + +import Cocoa + +class Window: NSWindow, NSWindowDelegate { + + weak var cocoaCB: CocoaCB! = nil + var mpv: MPVHelper! { + get { return cocoaCB == nil ? nil : cocoaCB.mpv } + } + + var targetScreen: NSScreen? + var previousScreen: NSScreen? + var currentScreen: NSScreen? + var unfScreen: NSScreen? + + var unfsContentFrame: NSRect? + var isInFullscreen: Bool = false + var isAnimating: Bool = false + var isMoving: Bool = false + var forceTargetScreen: Bool = false + + var keepAspect: Bool = true { + didSet { + if !isInFullscreen { + unfsContentFrame = convertToScreen(contentView!.frame) + } + + if keepAspect { + contentAspectRatio = unfsContentFrame!.size + } else { + resizeIncrements = NSSize(width: 1.0, height: 1.0) + } + } + } + + override var canBecomeKey: Bool { return true } + override var canBecomeMain: Bool { return true } + + override var styleMask: NSWindowStyleMask { + get { return super.styleMask } + set { + let responder = firstResponder + let windowTitle = title + super.styleMask = newValue + makeFirstResponder(responder) + title = windowTitle + } + } + + convenience init(cocoaCB ccb: CocoaCB) { + self.init(contentRect: NSMakeRect(0, 0, 960, 480), + styleMask: [.titled, .closable, .miniaturizable, .resizable], + backing: .buffered, defer: false, screen: NSScreen.main()) + cocoaCB = ccb + title = "mpv" + } + + convenience init(contentRect: NSRect, styleMask style: NSWindowStyleMask, + screen: NSScreen?, cocoaCB ccb: CocoaCB) + { + self.init(contentRect: contentRect, styleMask: style, + backing: .buffered, defer: false, screen: screen) + cocoaCB = ccb + minSize = NSMakeSize(160, 90) + collectionBehavior = .fullScreenPrimary + delegate = self + + unfsContentFrame = convertToScreen(contentView!.frame) + targetScreen = screen! + currentScreen = screen! + unfScreen = screen! + + if let app = NSApp as? Application { + app.menuBar.register(#selector(setHalfWindowSize), for: MPM_H_SIZE) + app.menuBar.register(#selector(setNormalWindowSize), for: MPM_N_SIZE) + app.menuBar.register(#selector(setDoubleWindowSize), for: MPM_D_SIZE) + app.menuBar.register(#selector(performMiniaturize(_:)), for: MPM_MINIMIZE) + app.menuBar.register(#selector(performZoom(_:)), for: MPM_ZOOM) + } + } + + override func toggleFullScreen(_ sender: Any?) { + if isAnimating { + return + } + + isAnimating = true + + targetScreen = cocoaCB.getTargetScreen(forFullscreen: !isInFullscreen) + if targetScreen == nil && previousScreen == nil { + targetScreen = screen + } else if targetScreen == nil { + targetScreen = previousScreen + previousScreen = nil + } else { + previousScreen = screen + } + + if !isInFullscreen { + unfsContentFrame = convertToScreen(contentView!.frame) + unfScreen = screen + } + // move window to target screen when going to fullscreen + if !isInFullscreen && (targetScreen != screen) { + let frame = calculateWindowPosition(for: targetScreen!, withoutBounds: false) + setFrame(frame, display: true) + } + + if mpv.getBoolProperty("native-fs") { + super.toggleFullScreen(sender) + } else { + if !isInFullscreen { + setToFullScreen() + } + else { + setToWindow() + } + } + } + + func customWindowsToEnterFullScreen(for window: NSWindow) -> [NSWindow]? { + return [window] + } + + func customWindowsToExitFullScreen(for window: NSWindow) -> [NSWindow]? { + return [window] + } + + func window(_ window: NSWindow, startCustomAnimationToEnterFullScreenWithDuration duration: TimeInterval) { + var newFrame = targetScreen!.frame + let cRect = contentRect(forFrameRect: frame) + newFrame.size.height = newFrame.size.height - (frame.size.height - cRect.size.height) + let intermediateFrame = aspectFit(rect: cRect, in: newFrame) + + NSAnimationContext.runAnimationGroup({ (context) -> Void in + context.duration = duration-0.1 + window.animator().setFrame(intermediateFrame, display: true) + }, completionHandler: { }) + } + + func window(_ window: NSWindow, startCustomAnimationToExitFullScreenWithDuration duration: TimeInterval) { + let newFrame = calculateWindowPosition(for: targetScreen!, withoutBounds: targetScreen == screen) + let intermediateFrame = aspectFit(rect: newFrame, in: screen!.frame) + setFrame(intermediateFrame, display: true) + cocoaCB.layer.display() + + NSAnimationContext.runAnimationGroup({ (context) -> Void in + context.duration = duration-0.1 + window.animator().setFrame(newFrame, display: true) + }, completionHandler: { }) + } + + func windowDidEnterFullScreen(_ notification: Notification) { + isInFullscreen = true + cocoaCB.flagEvents(VO_EVENT_FULLSCREEN_STATE) + cocoaCB.updateCusorVisibility() + endAnimation() + } + + func windowDidExitFullScreen(_ notification: Notification) { + isInFullscreen = false + cocoaCB.flagEvents(VO_EVENT_FULLSCREEN_STATE) + endAnimation() + } + + func windowDidFailToEnterFullScreen(_ window: NSWindow) { + let newFrame = calculateWindowPosition(for: targetScreen!, withoutBounds: targetScreen == screen) + setFrame(newFrame, display: true) + endAnimation() + } + + func windowDidFailToExitFullScreen(_ window: NSWindow) { + let newFrame = targetScreen!.frame + setFrame(newFrame, display: true) + endAnimation() + } + + func endAnimation() { + isAnimating = false + cocoaCB.isShuttingDown = false + } + + func setToFullScreen() { + styleMask.insert(.fullScreen) + NSApp.presentationOptions = [.autoHideMenuBar, .autoHideDock] + setFrame(targetScreen!.frame, display: true) + endAnimation() + isInFullscreen = true + cocoaCB.flagEvents(VO_EVENT_FULLSCREEN_STATE) + cocoaCB.layer.neededFlips += 1 + } + + func setToWindow() { + let newFrame = calculateWindowPosition(for: targetScreen!, withoutBounds: targetScreen == screen) + NSApp.presentationOptions = [] + setFrame(newFrame, display: true) + styleMask.remove(.fullScreen) + endAnimation() + isInFullscreen = false + cocoaCB.flagEvents(VO_EVENT_FULLSCREEN_STATE) + cocoaCB.layer.neededFlips += 1 + } + + func setBorder(_ state: Bool) { + if styleMask.contains(.titled) != state { + if state { + styleMask.remove(.borderless) + styleMask.insert(.titled) + } else { + styleMask.remove(.titled) + styleMask.insert(.borderless) + } + } + } + + func setOnTop(_ state: Bool) { + if state { + let ontopLevel = mpv.getStringProperty("ontop-level") ?? "window" + switch ontopLevel { + case "window": + level = Int(CGWindowLevelForKey(.floatingWindow)) + case "system": + level = Int(CGWindowLevelForKey(.statusWindow))+1 + default: + level = Int(ontopLevel)! + } + collectionBehavior.remove(.transient) + collectionBehavior.insert(.managed) + } else { + level = Int(CGWindowLevelForKey(.normalWindow)) + } + } + + func updateMovableBackground(_ pos: NSPoint) { + if !isInFullscreen { + isMovableByWindowBackground = mpv.canBeDraggedAt(pos) + } else { + isMovableByWindowBackground = false + } + } + + func updateFrame(_ rect: NSRect) { + if rect != frame { + let cRect = frameRect(forContentRect: rect) + setFrame(cRect, display: true) + unfsContentFrame = rect + } + } + + func updateSize(_ size: NSSize) { + if size != contentView!.frame.size { + let newContentFrame = centeredContentSize(for: frame, size: size) + if !isInFullscreen { + updateFrame(newContentFrame) + } else { + unfsContentFrame = newContentFrame + } + } + } + + override func setFrame(_ frameRect: NSRect, display flag: Bool) { + super.setFrame(frameRect, display: flag) + if keepAspect { + contentAspectRatio = unfsContentFrame!.size + } + } + + func centeredContentSize(for rect: NSRect, size sz: NSSize) -> NSRect { + let cRect = contentRect(forFrameRect: rect) + let dx = (cRect.size.width - sz.width) / 2 + let dy = (cRect.size.height - sz.height) / 2 + return NSInsetRect(cRect, dx, dy) + } + + func aspectFit(rect r: NSRect, in rTarget: NSRect) -> NSRect { + var s = rTarget.width / r.width; + if r.height*s > rTarget.height { + s = rTarget.height / r.height + } + let w = r.width * s + let h = r.height * s + return NSRect(x: rTarget.midX - w/2, y: rTarget.midY - h/2, width: w, height: h) + } + + func calculateWindowPosition(for tScreen: NSScreen, withoutBounds: Bool) -> NSRect { + var newFrame = frameRect(forContentRect: unfsContentFrame!) + let targetFrame = tScreen.frame + let targetVisibleFrame = tScreen.visibleFrame + let unfsScreenFrame = unfScreen!.frame + let visibleWindow = NSIntersectionRect(unfsScreenFrame, newFrame) + + // calculate visible area of every side + let left = newFrame.origin.x - unfsScreenFrame.origin.x + let right = unfsScreenFrame.size.width - + (newFrame.origin.x - unfsScreenFrame.origin.x + newFrame.size.width) + let bottom = newFrame.origin.y - unfsScreenFrame.origin.y + let top = unfsScreenFrame.size.height - + (newFrame.origin.y - unfsScreenFrame.origin.y + newFrame.size.height) + + // normalize visible areas, decide which one to take horizontal/vertical + var xPer = (unfsScreenFrame.size.width - visibleWindow.size.width) + var yPer = (unfsScreenFrame.size.height - visibleWindow.size.height) + if xPer != 0 { xPer = (left >= 0 || right < 0 ? left : right) / xPer } + if yPer != 0 { yPer = (bottom >= 0 || top < 0 ? bottom : top) / yPer } + + // calculate visible area for every side for target screen + let xNewLeft = targetFrame.origin.x + + (targetFrame.size.width - visibleWindow.size.width) * xPer + let xNewRight = targetFrame.origin.x + targetFrame.size.width - + (targetFrame.size.width - visibleWindow.size.width) * xPer - newFrame.size.width + let yNewBottom = targetFrame.origin.y + + (targetFrame.size.height - visibleWindow.size.height) * yPer + let yNewTop = targetFrame.origin.y + targetFrame.size.height - + (targetFrame.size.height - visibleWindow.size.height) * yPer - newFrame.size.height + + // calculate new coordinates, decide which one to take horizontal/vertical + newFrame.origin.x = left >= 0 || right < 0 ? xNewLeft : xNewRight + newFrame.origin.y = bottom >= 0 || top < 0 ? yNewBottom : yNewTop + + // don't place new window on top of a visible menubar + let topMar = targetFrame.size.height - + (newFrame.origin.y - targetFrame.origin.y + newFrame.size.height) + let menuBarHeight = targetFrame.size.height - + (targetVisibleFrame.size.height + targetVisibleFrame.origin.y) + if topMar < menuBarHeight { + newFrame.origin.y -= top - menuBarHeight + } + + if withoutBounds { + return newFrame + } + + // screen bounds right and left + if newFrame.origin.x + newFrame.size.width > targetFrame.origin.x + targetFrame.size.width { + newFrame.origin.x = targetFrame.origin.x + targetFrame.size.width - newFrame.size.width + } + if newFrame.origin.x < targetFrame.origin.x { + newFrame.origin.x = targetFrame.origin.x + } + + // screen bounds top and bottom + if newFrame.origin.y + newFrame.size.height > targetFrame.origin.y + targetFrame.size.height { + newFrame.origin.y = targetFrame.origin.y + targetFrame.size.height - newFrame.size.height + } + if newFrame.origin.y < targetFrame.origin.y { + newFrame.origin.y = targetFrame.origin.y + } + return newFrame + } + + override func constrainFrameRect(_ frameRect: NSRect, to tScreen: NSScreen?) -> NSRect { + if (isAnimating && !isInFullscreen) || (!isAnimating && isInFullscreen) { + return frameRect + } + + var nf: NSRect = frameRect + let ts: NSScreen = tScreen ?? screen ?? NSScreen.main()! + let of: NSRect = frame + let vf: NSRect = (isAnimating ? targetScreen! : ts).visibleFrame + let ncf: NSRect = contentRect(forFrameRect: nf) + + // screen bounds top and bottom + if NSMaxY(nf) > NSMaxY(vf) { + nf.origin.y = NSMaxY(vf) - NSHeight(nf) + } + if NSMaxY(ncf) < NSMinY(vf) { + nf.origin.y = NSMinY(vf) + NSMinY(ncf) - NSMaxY(ncf) + } + + // screen bounds right and left + if NSMinX(nf) > NSMaxX(vf) { + nf.origin.x = NSMaxX(vf) - NSWidth(nf) + } + if NSMaxX(nf) < NSMinX(vf) { + nf.origin.x = NSMinX(vf) + } + + if NSHeight(nf) < NSHeight(vf) && NSHeight(of) > NSHeight(vf) && !isInFullscreen { + // If the window height is smaller than the visible frame, but it was + // bigger previously recenter the smaller window vertically. This is + // needed to counter the 'snap to top' behaviour. + nf.origin.y = (NSHeight(vf) - NSHeight(nf)) / 2 + } + return nf + } + + func setNormalWindowSize() { setWindowScale(1.0) } + func setHalfWindowSize() { setWindowScale(0.5) } + func setDoubleWindowSize() { setWindowScale(2.0) } + + func setWindowScale(_ scale: Double) { + mpv.commandAsync(["osd-auto", "set", "window-scale", "\(scale)"]) + } + + func windowDidChangeScreen(_ notification: Notification) { + if screen == nil { + return + } + if !isAnimating && (currentScreen != screen) { + previousScreen = screen + } + if currentScreen != screen { + cocoaCB.updateDisplaylink() + } + currentScreen = screen + } + + func windowDidChangeScreenProfile(_ notification: Notification) { + cocoaCB.layer.needsICCUpdate = true + } + + func windowDidChangeBackingProperties(_ notification: Notification) { + cocoaCB.layer.contentsScale = backingScaleFactor + } + + func windowWillStartLiveResize(_ notification: Notification) { + cocoaCB.layer.inLiveResize = true + } + + func windowDidEndLiveResize(_ notification: Notification) { + cocoaCB.layer.inLiveResize = false + } + + func windowShouldClose(_ sender: Any) -> Bool { + cocoa_put_key(SWIFT_KEY_CLOSE_WIN) + return false + } + + func windowDidResignKey(_ notification: Notification) { + cocoaCB.setCursorVisiblility(true) + } + + func windowDidBecomeKey(_ notification: Notification) { + cocoaCB.updateCusorVisibility() + } + + func windowWillMove(_ notification: Notification) { + isMoving = true + } +} 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 . + */ + +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) -> 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) { + 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) { + let pData = OpaquePointer(event.pointee.data) + guard let property = UnsafePointer(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 + } + } +} diff --git a/video/out/opengl/context_cocoa.c b/video/out/opengl/context_cocoa.c index da20f400b2..367c6cd291 100644 --- a/video/out/opengl/context_cocoa.c +++ b/video/out/opengl/context_cocoa.c @@ -171,6 +171,8 @@ static bool cocoa_init(struct ra_ctx *ctx) p->opts = mp_get_config_group(ctx, ctx->global, &cocoa_conf); vo_cocoa_init(ctx->vo); + MP_WARN(ctx->vo, "opengl cocoa backend is deprecated, use opengl-cb instead\n"); + if (!create_gl_context(ctx)) goto fail; diff --git a/video/out/vo.h b/video/out/vo.h index 4ce2cf1371..533eca6a2e 100644 --- a/video/out/vo.h +++ b/video/out/vo.h @@ -73,6 +73,11 @@ enum mp_voctrl { // be updated and redrawn. Optional; emulated if not available. VOCTRL_REDRAW_FRAME, + // Only used internally in vo_opengl_cb + VOCTRL_PREINIT, + VOCTRL_UNINIT, + VOCTRL_RECONFIG, + VOCTRL_FULLSCREEN, VOCTRL_ONTOP, VOCTRL_BORDER, diff --git a/video/out/vo_opengl_cb.c b/video/out/vo_opengl_cb.c index f839f0a197..4be36118a0 100644 --- a/video/out/vo_opengl_cb.c +++ b/video/out/vo_opengl_cb.c @@ -57,6 +57,10 @@ struct mpv_opengl_cb_context { struct mpv_global *global; struct mp_client_api *client_api; + pthread_mutex_t control_lock; + mpv_opengl_cb_control_fn control_cb; + void *control_cb_ctx; + pthread_mutex_t lock; pthread_cond_t wakeup; @@ -78,6 +82,7 @@ struct mpv_opengl_cb_context { bool imgfmt_supported[IMGFMT_END - IMGFMT_START]; bool update_new_opts; struct vo *active; + bool icc_was_set; // --- This is only mutable while initialized=false, during which nothing // except the OpenGL context manager is allowed to access it. @@ -93,7 +98,7 @@ struct mpv_opengl_cb_context { struct mp_vo_opts *vo_opts; }; -static void update(struct vo_priv *p); +static void update(struct mpv_opengl_cb_context *ctx); static void forget_frames(struct mpv_opengl_cb_context *ctx, bool all) { @@ -114,6 +119,7 @@ static void free_ctx(void *ptr) pthread_cond_destroy(&ctx->wakeup); pthread_mutex_destroy(&ctx->lock); + pthread_mutex_destroy(&ctx->control_lock); } struct mpv_opengl_cb_context *mp_opengl_create(struct mpv_global *g, @@ -121,6 +127,7 @@ struct mpv_opengl_cb_context *mp_opengl_create(struct mpv_global *g, { mpv_opengl_cb_context *ctx = talloc_zero(NULL, mpv_opengl_cb_context); talloc_set_destructor(ctx, free_ctx); + pthread_mutex_init(&ctx->control_lock, NULL); pthread_mutex_init(&ctx->lock, NULL); pthread_cond_init(&ctx->wakeup, NULL); @@ -144,6 +151,17 @@ void mpv_opengl_cb_set_update_callback(struct mpv_opengl_cb_context *ctx, pthread_mutex_unlock(&ctx->lock); } + +void mp_client_set_control_callback(struct mpv_opengl_cb_context *ctx, + mpv_opengl_cb_control_fn callback, + void *callback_ctx) +{ + pthread_mutex_lock(&ctx->control_lock); + ctx->control_cb = callback; + ctx->control_cb_ctx = callback_ctx; + pthread_mutex_unlock(&ctx->control_lock); +} + // Reset some GL attributes the user might clobber. For mid-term compatibility // only - we expect both user code and our code to do this correctly. static void reset_gl_state(GL *gl) @@ -223,9 +241,11 @@ int mpv_opengl_cb_uninit_gl(struct mpv_opengl_cb_context *ctx) pthread_mutex_lock(&ctx->lock); forget_frames(ctx, true); ctx->initialized = false; + bool was_active = ctx->active ? true : false; pthread_mutex_unlock(&ctx->lock); - kill_video(ctx->client_api); + if (was_active) + kill_video(ctx->client_api); pthread_mutex_lock(&ctx->lock); assert(!ctx->active); @@ -291,7 +311,7 @@ int mpv_opengl_cb_draw(mpv_opengl_cb_context *ctx, int fbo, int vp_w, int vp_h) &debug); ctx->gl->debug_context = debug; ra_gl_set_debug(ctx->ra_ctx->ra, debug); - if (gl_video_icc_auto_enabled(ctx->renderer)) + if (!ctx->icc_was_set && gl_video_icc_auto_enabled(ctx->renderer)) MP_ERR(ctx, "icc-profile-auto is not available with opengl-cb\n"); } ctx->reconfigured = false; @@ -360,10 +380,10 @@ int mpv_opengl_cb_report_flip(mpv_opengl_cb_context *ctx, int64_t time) } // Called locked. -static void update(struct vo_priv *p) +static void update(struct mpv_opengl_cb_context *ctx) { - if (p->ctx->update_cb) - p->ctx->update_cb(p->ctx->update_cb_ctx); + if (ctx->update_cb) + ctx->update_cb(ctx->update_cb_ctx); } static void draw_frame(struct vo *vo, struct vo_frame *frame) @@ -375,7 +395,7 @@ static void draw_frame(struct vo *vo, struct vo_frame *frame) p->ctx->next_frame = vo_frame_ref(frame); p->ctx->expected_flip_count = p->ctx->flip_count + 1; p->ctx->redrawing = frame->redraw || !frame->current; - update(p); + update(p->ctx); pthread_mutex_unlock(&p->ctx->lock); } @@ -442,17 +462,20 @@ static int query_format(struct vo *vo, int format) return ok; } -static int reconfig(struct vo *vo, struct mp_image_params *params) +// Can only be called from the GL thread +void mp_client_set_icc_profile(struct mpv_opengl_cb_context *ctx, bstr icc_data) { - struct vo_priv *p = vo->priv; - - pthread_mutex_lock(&p->ctx->lock); - forget_frames(p->ctx, true); - p->ctx->img_params = *params; - p->ctx->reconfigured = true; - pthread_mutex_unlock(&p->ctx->lock); + gl_video_set_icc_profile(ctx->renderer, icc_data); + pthread_mutex_lock(&ctx->lock); + ctx->icc_was_set = true; + if (ctx->active) + update(ctx); + pthread_mutex_unlock(&ctx->lock); +} - return 0; +void mp_client_set_ambient_lux(struct mpv_opengl_cb_context *ctx, int lux) +{ + gl_video_set_ambient_lux(ctx->renderer, lux); } static int control(struct vo *vo, uint32_t request, void *data) @@ -475,30 +498,54 @@ static int control(struct vo *vo, uint32_t request, void *data) case VOCTRL_SET_PANSCAN: pthread_mutex_lock(&p->ctx->lock); p->ctx->force_update = true; - update(p); + update(p->ctx); pthread_mutex_unlock(&p->ctx->lock); return VO_TRUE; case VOCTRL_UPDATE_RENDER_OPTS: pthread_mutex_lock(&p->ctx->lock); p->ctx->update_new_opts = true; - update(p); + update(p->ctx); pthread_mutex_unlock(&p->ctx->lock); return VO_TRUE; } - return VO_NOTIMPL; + int r = VO_NOTIMPL; + pthread_mutex_lock(&p->ctx->control_lock); + if (p->ctx->control_cb) { + int events = 0; + r = p->ctx->control_cb(p->ctx->control_cb_ctx, &events, request, data); + vo_event(vo, events); + } + pthread_mutex_unlock(&p->ctx->control_lock); + + return r; +} + +static int reconfig(struct vo *vo, struct mp_image_params *params) +{ + struct vo_priv *p = vo->priv; + + pthread_mutex_lock(&p->ctx->lock); + forget_frames(p->ctx, true); + p->ctx->img_params = *params; + p->ctx->reconfigured = true; + pthread_mutex_unlock(&p->ctx->lock); + control(vo, VOCTRL_RECONFIG, NULL); + + return 0; } static void uninit(struct vo *vo) { struct vo_priv *p = vo->priv; + control(vo, VOCTRL_UNINIT, NULL); pthread_mutex_lock(&p->ctx->lock); forget_frames(p->ctx, true); p->ctx->img_params = (struct mp_image_params){0}; p->ctx->reconfigured = true; p->ctx->active = NULL; - update(p); + update(p->ctx); pthread_mutex_unlock(&p->ctx->lock); } @@ -523,6 +570,7 @@ static int preinit(struct vo *vo) pthread_mutex_unlock(&p->ctx->lock); vo->hwdec_devs = p->ctx->hwdec_devs; + control(vo, VOCTRL_PREINIT, NULL); return 0; } -- cgit v1.2.3