Files
plezy/macos/Runner/MpvPlayer/MpvPlayerCore.swift
T

245 lines
7.2 KiB
Swift

import AVFoundation
import Cocoa
import Libmpv
/// Core MPV player using AVFoundation sample-buffer rendering.
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 {
guard !isInitialized else {
print("[MpvPlayerCore] Already initialized")
return true
}
guard let contentView = window.contentView else {
print("[MpvPlayerCore] No content view")
return false
}
self.window = window
let layer = MpvVideoLayer()
layer.frame = contentView.bounds
if let screen = window.screen ?? NSScreen.main {
layer.contentsScale = screen.backingScaleFactor
}
layer.isOpaque = true
layer.backgroundColor = NSColor.black.cgColor
layer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
layer.videoGravity = .resizeAspect
videoLayer = layer
updateEDRMode(sigPeak: lastSigPeak)
contentView.wantsLayer = true
contentView.layer?.addSublayer(layer)
print("[MpvPlayerCore] Video layer added, frame: \(layer.frame)")
guard setupMpv() else {
print("[MpvPlayerCore] Failed to setup MPV")
layer.removeFromSuperlayer()
videoLayer = nil
return false
}
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(windowDidEnterFullScreen),
name: NSWindow.didEnterFullScreenNotification,
object: window
)
center.addObserver(
self,
selector: #selector(windowDidExitFullScreen),
name: NSWindow.didExitFullScreenNotification,
object: window
)
center.addObserver(
self,
selector: #selector(windowOcclusionDidChange),
name: NSWindow.didChangeOcclusionStateNotification,
object: window
)
isInitialized = true
print("[MpvPlayerCore] Initialized successfully with MPV")
return true
}
override func configurePlatformMpvOptions() {
guard let mpv else { return }
checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "no"))
checkError(mpv_set_option_string(mpv, "ao", "avfoundation,coreaudio"))
}
func reattachVideoLayer() {
guard let videoLayer, let contentView = window?.contentView else { return }
if videoLayer.superlayer == nil {
contentView.wantsLayer = true
contentView.layer?.insertSublayer(videoLayer, at: 0)
videoLayer.frame = contentView.bounds
if let screen = window?.screen ?? NSScreen.main {
videoLayer.contentsScale = screen.backingScaleFactor
}
}
print("[MpvPlayerCore] Video layer reattached to window")
}
func forceDraw() {
command(["seek", "0", "relative+exact"])
}
private var isVisible = false
private var pausedState = true
func setVisible(_ visible: Bool) {
guard let videoLayer, !isPipActive else { return }
isVisible = visible
isBackgrounded = !visible
if visible {
videoLayer.removeFromSuperlayer()
if let superlayer = window?.contentView?.layer {
superlayer.insertSublayer(videoLayer, at: 0)
}
beginPlaybackActivity()
} else {
endPlaybackActivity()
}
videoLayer.isHidden = !visible
print("[MpvPlayerCore] setVisible(\(visible))")
}
func setPaused(_ paused: Bool) {
pausedState = paused
if paused {
endPlaybackActivity()
} else if isVisible {
beginPlaybackActivity()
}
}
func updateFrame(_ frame: CGRect? = nil) {
guard let videoLayer, !isPipActive else { return }
if let frame {
videoLayer.frame = frame
} else if let contentView = window?.contentView {
videoLayer.frame = contentView.bounds
}
if let screen = window?.screen ?? NSScreen.main {
videoLayer.contentsScale = screen.backingScaleFactor
}
updateEDRMode(sigPeak: lastSigPeak)
print("[MpvPlayerCore] updateFrame: \(videoLayer.frame)")
}
override func updateEDRMode(sigPeak: Double) {
guard let videoLayer else { return }
let hdrEnabled = self.hdrEnabled
var currentHeadroom: CGFloat = 1.0
var potentialHeadroom: CGFloat = 1.0
if let screen = window?.screen ?? NSScreen.main {
currentHeadroom = screen.maximumExtendedDynamicRangeColorComponentValue
potentialHeadroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue
}
let signalHeadroom = CGFloat(max(sigPeak, 1.0))
let contentHeadroom = min(signalHeadroom, potentialHeadroom)
let shouldEnableEDR = hdrEnabled && sigPeak > 1.0 && potentialHeadroom > 1.0
if #available(macOS 26.0, *) {
videoLayer.preferredDynamicRange = shouldEnableEDR ? .high : .standard
videoLayer.contentsHeadroom = shouldEnableEDR ? contentHeadroom : 0
}
if #available(macOS 15.0, *) {
videoLayer.toneMapMode = shouldEnableEDR ? .ifSupported : .automatic
}
if #available(macOS 14.0, *) {
videoLayer.wantsExtendedDynamicRangeContent = shouldEnableEDR
}
print(
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), currentHeadroom: \(currentHeadroom), potentialHeadroom: \(potentialHeadroom), contentHeadroom: \(shouldEnableEDR ? contentHeadroom : 0))"
)
}
func dispose() {
if isDisposed { return }
isDisposed = true
endPlaybackActivity()
NotificationCenter.default.removeObserver(self)
disposeSharedState(destroySynchronously: false)
videoLayer?.removeFromSuperlayer()
videoLayer = nil
isInitialized = false
print("[MpvPlayerCore] Disposed")
}
deinit {
dispose()
}
@objc private func windowDidEnterFullScreen(_ notification: Notification) {
guard !isPipActive else { return }
updateFrame()
}
@objc private func windowDidExitFullScreen(_ notification: Notification) {
guard !isPipActive else { return }
updateFrame()
}
@objc private func windowOcclusionDidChange(_ notification: Notification) {
guard let videoLayer, mpv != nil, !isPipActive else { return }
let windowVisible = window?.occlusionState.contains(.visible) ?? true
if !windowVisible && !layerHiddenForOcclusion {
print("[MpvPlayerCore] Window occluded - hiding video layer")
videoLayer.isHidden = true
layerHiddenForOcclusion = true
isBackgrounded = true
endPlaybackActivity()
} else if windowVisible && layerHiddenForOcclusion {
print("[MpvPlayerCore] Window visible - showing video layer")
layerHiddenForOcclusion = false
videoLayer.isHidden = false
isBackgrounded = false
if !pausedState {
beginPlaybackActivity()
}
}
}
private func beginPlaybackActivity() {
guard playbackActivity == nil else { return }
playbackActivity = ProcessInfo.processInfo.beginActivity(
options: [.userInitiated, .latencyCritical],
reason: "Video playback"
)
print("[MpvPlayerCore] Began playback activity assertion")
}
private func endPlaybackActivity() {
guard let playbackActivity else { return }
ProcessInfo.processInfo.endActivity(playbackActivity)
self.playbackActivity = nil
print("[MpvPlayerCore] Ended playback activity assertion")
}
}