feat(macos): add picture-in-picture support

This commit is contained in:
edde746
2026-03-02 18:19:36 +01:00
parent 7ebbfb9ef7
commit 8d3f448fdf
31 changed files with 559 additions and 37 deletions
+65 -7
View File
@@ -13,7 +13,8 @@ private class MetalLayer: CAMetalLayer {
override var drawableSize: CGSize {
get { return super.drawableSize }
set {
if Int(newValue.width) > 1 && Int(newValue.height) > 1 {
// Allow .zero (auto-derive from bounds) or valid sizes > 1x1
if newValue == .zero || (Int(newValue.width) > 1 && Int(newValue.height) > 1) {
super.drawableSize = newValue
}
}
@@ -49,6 +50,9 @@ class MpvPlayerCore: NSObject {
private(set) var isInitialized = false
// PiP state
var isPipActive = false
// HDR settings
private var hdrEnabled = true // User preference for HDR
private var lastSigPeak: Double = 0.0 // Last known sig-peak for re-evaluation
@@ -119,25 +123,25 @@ class MpvPlayerCore: NSObject {
// MARK: - Fullscreen Transition Handling
@objc private func windowWillEnterFullScreen(_ notification: Notification) {
guard mpv != nil else { return }
guard mpv != nil, !isPipActive else { return }
print("[MpvPlayerCore] willEnterFullScreen — disabling video output")
mpv_set_property_string(mpv, "vid", "no")
}
@objc private func windowDidEnterFullScreen(_ notification: Notification) {
guard mpv != nil else { return }
guard mpv != nil, !isPipActive else { return }
print("[MpvPlayerCore] didEnterFullScreen — re-enabling video output")
mpv_set_property_string(mpv, "vid", "auto")
}
@objc private func windowWillExitFullScreen(_ notification: Notification) {
guard mpv != nil else { return }
guard mpv != nil, !isPipActive else { return }
print("[MpvPlayerCore] willExitFullScreen — disabling video output")
mpv_set_property_string(mpv, "vid", "no")
}
@objc private func windowDidExitFullScreen(_ notification: Notification) {
guard mpv != nil else { return }
guard mpv != nil, !isPipActive else { return }
print("[MpvPlayerCore] didExitFullScreen — re-enabling video output")
mpv_set_property_string(mpv, "vid", "auto")
}
@@ -300,10 +304,64 @@ class MpvPlayerCore: NSObject {
}
}
// MARK: - PiP Support
/// The Metal layer used for video rendering, exposed for PiP.
/// PiP moves this layer to its own window; mpv continues rendering to it.
var videoLayer: CAMetalLayer? { metalLayer }
/// Re-attach the Metal layer to the main window after PiP exits.
func reattachMetalLayer() {
guard let metalLayer = metalLayer, let contentView = window?.contentView else { return }
if metalLayer.superlayer == nil {
contentView.wantsLayer = true
contentView.layer?.insertSublayer(metalLayer, at: 0)
metalLayer.frame = contentView.bounds
if let screen = window?.screen ?? NSScreen.main {
metalLayer.contentsScale = screen.backingScaleFactor
metalLayer.drawableSize = CGSize(
width: contentView.bounds.width * screen.backingScaleFactor,
height: contentView.bounds.height * screen.backingScaleFactor
)
}
}
print("[MpvPlayerCore] Metal layer reattached to window")
}
/// Force a redraw (useful after PiP exit when paused).
/// Uses a seek to the current position to trigger a frame render.
func forceDraw() {
command(["seek", "0", "relative+exact"])
}
/// Whether mpv is currently paused
var isPaused: Bool {
guard let mpv = mpv else { return true }
var flag: Int32 = 0
mpv_get_property(mpv, "pause", MPV_FORMAT_FLAG, &flag)
return flag != 0
}
/// Current playback duration in seconds
var duration: Double {
guard let mpv = mpv else { return 0 }
var value: Double = 0
mpv_get_property(mpv, "duration", MPV_FORMAT_DOUBLE, &value)
return value
}
/// Current playback time in seconds
var timePos: Double {
guard let mpv = mpv else { return 0 }
var value: Double = 0
mpv_get_property(mpv, "time-pos", MPV_FORMAT_DOUBLE, &value)
return value
}
// MARK: - Visibility
func setVisible(_ visible: Bool) {
guard let layer = metalLayer else { return }
guard let layer = metalLayer, !isPipActive else { return }
if visible {
// Re-insert after background layer but before Flutter control views
@@ -321,7 +379,7 @@ class MpvPlayerCore: NSObject {
}
func updateFrame(_ frame: CGRect? = nil) {
guard let metalLayer = metalLayer else { return }
guard let metalLayer = metalLayer, !isPipActive else { return }
if let frame = frame {
metalLayer.frame = frame