diff --git a/macos/Runner/MpvPlayer/MpvPlayerCore.swift b/macos/Runner/MpvPlayer/MpvPlayerCore.swift index 7c479f8d9..927522602 100644 --- a/macos/Runner/MpvPlayer/MpvPlayerCore.swift +++ b/macos/Runner/MpvPlayer/MpvPlayerCore.swift @@ -1,5 +1,6 @@ import Cocoa import Libmpv +import Metal import QuartzCore /// Core MPV player using Metal rendering on macOS. @@ -7,7 +8,6 @@ class MpvPlayerCore: MpvPlayerCoreBase { private weak var window: NSWindow? private var playbackActivity: NSObjectProtocol? - private var layerHiddenForOcclusion = false private var isDisposed = false func initialize(in window: NSWindow) -> Bool { @@ -23,14 +23,15 @@ class MpvPlayerCore: MpvPlayerCoreBase { self.window = window + // Create and configure Metal layer for mpv rendering let layer = MpvMetalLayer() + layer.device = MTLCreateSystemDefaultDevice() layer.frame = contentView.bounds if let screen = window.screen ?? NSScreen.main { layer.contentsScale = screen.backingScaleFactor } layer.framebufferOnly = true layer.isOpaque = true - layer.backgroundColor = NSColor.black.cgColor layer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable] metalLayer = layer @@ -41,10 +42,10 @@ class MpvPlayerCore: MpvPlayerCoreBase { metalLayer = nil return false } - attachMetalLayer(to: contentLayer, frame: contentView.bounds) - updateEDRMode(sigPeak: lastSigPeak) - - print("[MpvPlayerCore] Metal layer added, frame: \(layer.frame)") + withoutLayerAnimations { + contentLayer.insertSublayer(layer, at: 0) + layer.frame = contentView.bounds + } guard setupMpv() else { print("[MpvPlayerCore] Failed to setup MPV") @@ -84,11 +85,24 @@ class MpvPlayerCore: MpvPlayerCoreBase { } func reattachMetalLayer() { - guard let contentView = window?.contentView else { return } + guard let metalLayer, let contentView = window?.contentView else { return } contentView.wantsLayer = true - if let contentLayer = contentView.layer { - attachMetalLayer(to: contentLayer, frame: contentView.bounds) + if let superlayer = contentView.layer, metalLayer.superlayer !== superlayer { + withoutLayerAnimations { + superlayer.backgroundColor = NSColor.black.cgColor + superlayer.isOpaque = true + superlayer.insertSublayer(metalLayer, at: 0) + metalLayer.frame = contentView.bounds + if let screen = window?.screen ?? NSScreen.main { + let scale = screen.backingScaleFactor + metalLayer.contentsScale = scale + metalLayer.drawableSize = CGSize( + width: contentView.bounds.width * scale, + height: contentView.bounds.height * scale + ) + } + } } print("[MpvPlayerCore] Metal layer reattached to window") @@ -107,9 +121,7 @@ class MpvPlayerCore: MpvPlayerCoreBase { if visible && isVisible && !shouldRestoreOnWindowVisible { isBackgrounded = false - if metalLayer?.isHidden == true { - setMetalLayerHidden(false) - } + metalLayer?.isHidden = false beginPlaybackActivity() print("[MpvPlayerCore] setVisible(true) skipped - already visible") return @@ -121,18 +133,13 @@ class MpvPlayerCore: MpvPlayerCoreBase { if visible { shouldRestoreOnWindowVisible = false - if let contentView = window?.contentView { - contentView.wantsLayer = true - if let superlayer = contentView.layer { - attachMetalLayer(to: superlayer, frame: contentView.bounds) - } - } + reattachMetalLayer() beginPlaybackActivity() } else { endPlaybackActivity() } - setMetalLayerHidden(!visible) + metalLayer?.isHidden = !visible print("[MpvPlayerCore] setVisible(\(visible), restoreOnWindowVisible: \(restoreOnWindowVisible))") } @@ -159,9 +166,15 @@ class MpvPlayerCore: MpvPlayerCoreBase { withoutLayerAnimations { metalLayer.frame = targetFrame - updateDrawableSize(for: metalLayer) + if let screen = window?.screen ?? NSScreen.main { + let scale = screen.backingScaleFactor + metalLayer.contentsScale = scale + metalLayer.drawableSize = CGSize( + width: targetFrame.width * scale, + height: targetFrame.height * scale + ) + } } - updateEDRMode(sigPeak: lastSigPeak) } override func updateEDRMode(sigPeak: Double) { @@ -178,6 +191,13 @@ class MpvPlayerCore: MpvPlayerCoreBase { metalLayer.wantsExtendedDynamicRangeContent = shouldEnableEDR } + if shouldEnableEDR { + let peakNits = Int(potentialHeadroom * 80) + setPropertyAsync("target-peak", value: "\(peakNits)") { _ in } + } else { + setPropertyAsync("target-peak", value: "auto") { _ in } + } + print( "[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), potentialHeadroom: \(potentialHeadroom))" ) @@ -212,23 +232,15 @@ class MpvPlayerCore: MpvPlayerCoreBase { } @objc private func windowOcclusionDidChange(_ notification: Notification) { - guard metalLayer != nil, mpv != nil, !isPipActive else { return } + guard mpv != nil, !isPipActive else { return } let windowVisible = window?.occlusionState.contains(.visible) ?? true - if !windowVisible && !layerHiddenForOcclusion { - print("[MpvPlayerCore] Window occluded - hiding Metal layer") - setMetalLayerHidden(true) - layerHiddenForOcclusion = true + if !windowVisible { + print("[MpvPlayerCore] Window occluded") isBackgrounded = true endPlaybackActivity() - } else if windowVisible && layerHiddenForOcclusion { - print("[MpvPlayerCore] Window visible - showing Metal layer") - layerHiddenForOcclusion = false - if shouldRestoreOnWindowVisible { - restoreMetalLayerAfterOcclusion() - } else { - setMetalLayerHidden(!isVisible) - } + } else { + print("[MpvPlayerCore] Window visible") isBackgrounded = false if !pausedState { beginPlaybackActivity() @@ -252,59 +264,6 @@ class MpvPlayerCore: MpvPlayerCoreBase { print("[MpvPlayerCore] Ended playback activity assertion") } - private func restoreMetalLayerAfterOcclusion() { - if let metalLayer, let contentView = window?.contentView { - contentView.wantsLayer = true - if let superlayer = contentView.layer { - let targetFrame = contentView.bounds - let needsAttach = metalLayer.superlayer !== superlayer || superlayer.sublayers?.first !== metalLayer - if needsAttach { - attachMetalLayer(to: superlayer, frame: targetFrame) - } else if !metalLayer.frame.equalTo(targetFrame) { - updateFrame(targetFrame) - } - } - } - isVisible = true - shouldRestoreOnWindowVisible = false - setMetalLayerHidden(false) - } - - private func attachMetalLayer(to superlayer: CALayer, frame: CGRect) { - guard let metalLayer else { return } - - withoutLayerAnimations { - superlayer.backgroundColor = NSColor.black.cgColor - superlayer.isOpaque = true - - let needsReorder = superlayer.sublayers?.first !== metalLayer - if metalLayer.superlayer !== superlayer || needsReorder { - metalLayer.removeFromSuperlayer() - superlayer.insertSublayer(metalLayer, at: 0) - } - - metalLayer.frame = frame - updateDrawableSize(for: metalLayer) - } - } - - private func updateDrawableSize(for metalLayer: CAMetalLayer) { - if let screen = window?.screen ?? NSScreen.main { - let scale = screen.backingScaleFactor - metalLayer.contentsScale = scale - metalLayer.drawableSize = CGSize( - width: metalLayer.frame.width * scale, - height: metalLayer.frame.height * scale - ) - } - } - - private func setMetalLayerHidden(_ hidden: Bool) { - withoutLayerAnimations { - metalLayer?.isHidden = hidden - } - } - private func withoutLayerAnimations(_ updates: () -> Void) { CATransaction.begin() CATransaction.setDisableActions(true) diff --git a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift index 5fbfb4e91..e44681e99 100644 --- a/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift +++ b/shared/apple/MpvPlayer/MpvPlayerCoreBase.swift @@ -15,10 +15,8 @@ protocol MpvPlayerDelegate: AnyObject { func onEvent(name: String, data: [String: Any]?) } -#if os(macOS) - // Workaround for MoltenVK problems that cause flicker. - // https://github.com/mpv-player/mpv/pull/13651 - class MpvMetalLayer: CAMetalLayer { + #if os(macOS) + class MpvMetalLayer: CAMetalLayer { override var drawableSize: CGSize { get { super.drawableSize } set { @@ -84,7 +82,7 @@ class MpvPlayerCoreBase: NSObject { weak var delegate: MpvPlayerDelegate? #if os(macOS) - var metalLayer: MpvMetalLayer? + var metalLayer: CAMetalLayer? #else var videoLayer: MpvVideoLayer? #endif @@ -336,6 +334,16 @@ class MpvPlayerCoreBase: NSObject { } } + #if os(macOS) + private func configureMoltenVKPlacementHeaps() { + guard let device = MTLCreateSystemDefaultDevice() else { return } + let supportsPlacementHeaps = device.supportsFamily(.apple2) || device.supportsFamily(.mac2) + if !supportsPlacementHeaps { + setenv("MVK_CONFIG_USE_MTLHEAP", "0", 1) + } + } + #endif + func setupMpv() -> Bool { #if os(macOS) guard let renderLayer = metalLayer else { return false } @@ -559,10 +567,17 @@ class MpvPlayerCoreBase: NSObject { setRawStringPropertyAsync( "target-colorspace-hint", - value: enabled ? "auto" : "no", + value: enabled ? "yes" : "no", completion: completion ?? { _ in } ) + #if os(macOS) + setRawStringPropertyAsync("target-prim", value: enabled ? "auto" : "bt.709") { _ in } + setRawStringPropertyAsync("target-trc", value: enabled ? "auto" : "bt.1886") { _ in } + setRawStringPropertyAsync("tone-mapping", value: "auto") { _ in } + setRawStringPropertyAsync("gamut-mapping-mode", value: "auto") { _ in } + #endif + DispatchQueue.main.async { self.updateEDRMode(sigPeak: sigPeak) } @@ -755,6 +770,11 @@ class MpvPlayerCoreBase: NSObject { checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan")) checkError(mpv_set_option_string(mpv, "gpu-context", "moltenvk")) checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox")) + checkError(mpv_set_option_string(mpv, "tone-mapping", "auto")) + checkError(mpv_set_option_string(mpv, "hdr-compute-peak", "auto")) + checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "auto")) + checkError(mpv_set_option_string(mpv, "target-prim", "auto")) + checkError(mpv_set_option_string(mpv, "target-trc", "auto")) #else checkError(mpv_set_option_string(mpv, "vo", "avfoundation")) #if targetEnvironment(simulator) @@ -770,25 +790,15 @@ class MpvPlayerCoreBase: NSObject { checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "no")) checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox")) #endif + checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "auto")) #endif checkError(mpv_set_option_string(mpv, "hwdec-codecs", "all")) checkError(mpv_set_option_string(mpv, "hwdec-software-fallback", "yes")) - checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "auto")) // Pause on the last frame at EOF instead of unloading the file, so seeking // back after the video ends still works (matches Linux/Windows). checkError(mpv_set_option_string(mpv, "keep-open", "yes")) } - #if os(macOS) - private func configureMoltenVKPlacementHeaps() { - guard let device = MTLCreateSystemDefaultDevice() else { return } - let supportsPlacementHeaps = device.supportsFamily(.apple2) || device.supportsFamily(.mac2) - if !supportsPlacementHeaps { - setenv("MVK_CONFIG_USE_MTLHEAP", "0", 1) - } - } - #endif - private func isManagedRendererProperty(_ name: String) -> Bool { name == "vo" || name == "wid" || name == "gpu-api" || name == "gpu-context" || name == "avfoundation-composite-osd" || name == "avfoundation-pip-composite-osd" @@ -981,7 +991,9 @@ class MpvPlayerCoreBase: NSObject { print("[MpvPlayerCore] MPV shutdown event") case MPV_EVENT_PLAYBACK_RESTART: + let sigPeak = lastSigPeak DispatchQueue.main.async { + self.updateEDRMode(sigPeak: sigPeak) self.delegate?.onEvent(name: "playback-restart", data: nil) }