diff options
Diffstat (limited to 'video')
-rw-r--r-- | video/out/cocoa-cb/events_view.swift | 266 | ||||
-rw-r--r-- | video/out/cocoa-cb/video_layer.swift | 210 | ||||
-rw-r--r-- | video/out/cocoa-cb/window.swift | 456 | ||||
-rw-r--r-- | video/out/cocoa_cb_common.swift | 485 | ||||
-rw-r--r-- | video/out/opengl/context_cocoa.c | 2 | ||||
-rw-r--r-- | video/out/vo.h | 5 | ||||
-rw-r--r-- | video/out/vo_opengl_cb.c | 88 |
7 files changed, 1492 insertions, 20 deletions
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 <http://www.gnu.org/licenses/>. + */ + +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 <http://www.gnu.org/licenses/>. + */ + +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<CVTimeStamp>?) -> Bool { + return mpv != nil && cocoaCB.backendState == .init + } + + override func draw(inCGLContext ctx: CGLContextObj, + pixelFormat pf: CGLPixelFormatObj, + forLayerTime t: CFTimeInterval, + displayTime ts: UnsafePointer<CVTimeStamp>?) { + 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 <http://www.gnu.org/licenses/>. + */ + +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 <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 + } + } +} 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; } |