Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 46 additions & 87 deletions macos/Runner/MpvPlayer/MpvPlayerCore.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Cocoa
import Libmpv
import Metal
import QuartzCore

/// Core MPV player using Metal rendering on macOS.
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 {
Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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))")
}

Expand All @@ -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) {
Expand All @@ -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))"
)
Expand Down Expand Up @@ -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()
Expand All @@ -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)
Expand Down
46 changes: 29 additions & 17 deletions shared/apple/MpvPlayer/MpvPlayerCoreBase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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"
Expand Down Expand Up @@ -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)
}

Expand Down