fix(apple): use platform-specific mpv rendering
This commit is contained in:
+2
-2
@@ -11,7 +11,7 @@ PODS:
|
||||
- file_picker (0.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- os_media_controls (0.0.1):
|
||||
- os_media_controls (0.2.1):
|
||||
- Flutter
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
@@ -113,7 +113,7 @@ SPEC CHECKSUMS:
|
||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
||||
file_picker: 8fc6fe5e42585a217d44d22f79ec046cb8d81140
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
os_media_controls: 94cc278f5802b82b2d6373003aeb511f96718b27
|
||||
os_media_controls: 3b06fe2dc0977dfa94f9c1dd73df7a55022d202f
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
|
||||
sentry_flutter: 31101687061fb85211ebab09ce6eb8db4e9ba74f
|
||||
|
||||
@@ -781,7 +781,7 @@
|
||||
repositoryURL = "https://github.com/edde746/MPVKit";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = e9dc70092d8763c5cbee5ea72906cebc42f8dac8;
|
||||
revision = 86dcf97f04e572abb5641e74780251a1d5634ff3;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/edde746/MPVKit",
|
||||
"state" : {
|
||||
"revision" : "e9dc70092d8763c5cbee5ea72906cebc42f8dac8"
|
||||
"revision" : "86dcf97f04e572abb5641e74780251a1d5634ff3"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/edde746/MPVKit",
|
||||
"state" : {
|
||||
"revision" : "e9dc70092d8763c5cbee5ea72906cebc42f8dac8"
|
||||
"revision" : "86dcf97f04e572abb5641e74780251a1d5634ff3"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AVFoundation
|
||||
import QuartzCore
|
||||
import UIKit
|
||||
|
||||
/// Core MPV player using AVFoundation sample-buffer rendering for iOS/tvOS.
|
||||
@@ -20,12 +21,13 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
self.window = window
|
||||
|
||||
let container = UIView(frame: window.bounds)
|
||||
container.backgroundColor = .clear
|
||||
container.backgroundColor = .black
|
||||
container.isUserInteractionEnabled = false
|
||||
|
||||
let layer = MpvVideoLayer()
|
||||
layer.frame = container.bounds
|
||||
layer.contentsScale = window.screen.nativeScale
|
||||
layer.isOpaque = true
|
||||
layer.backgroundColor = UIColor.black.cgColor
|
||||
layer.videoGravity = .resizeAspect
|
||||
|
||||
@@ -57,33 +59,35 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
var sampleBufferDisplayLayer: MpvVideoLayer? { videoLayer }
|
||||
|
||||
func setVisible(_ visible: Bool) {
|
||||
guard let containerView else { return }
|
||||
guard containerView != nil else { return }
|
||||
|
||||
isVisible = visible
|
||||
if visible { refreshExternalDisplayAttachment() }
|
||||
containerView.isHidden = !visible
|
||||
setContainerHidden(!visible)
|
||||
if !visible { mainBlankView?.isHidden = true }
|
||||
}
|
||||
|
||||
func updateFrame(_ frame: CGRect? = nil) {
|
||||
guard let videoLayer, let containerView else { return }
|
||||
|
||||
if let frame {
|
||||
containerView.frame = frame
|
||||
videoLayer.frame = containerView.bounds
|
||||
} else if let superview = containerView.superview {
|
||||
containerView.frame = superview.bounds
|
||||
videoLayer.frame = containerView.bounds
|
||||
} else if let window {
|
||||
containerView.frame = window.bounds
|
||||
videoLayer.frame = containerView.bounds
|
||||
withoutLayerAnimations {
|
||||
if let frame {
|
||||
containerView.frame = frame
|
||||
videoLayer.frame = containerView.bounds
|
||||
} else if let superview = containerView.superview {
|
||||
containerView.frame = superview.bounds
|
||||
videoLayer.frame = containerView.bounds
|
||||
} else if let window {
|
||||
containerView.frame = window.bounds
|
||||
videoLayer.frame = containerView.bounds
|
||||
}
|
||||
|
||||
mainBlankView?.frame = window?.bounds ?? .zero
|
||||
|
||||
let screen = containerView.window?.screen ?? window?.screen ?? UIScreen.main
|
||||
let scale = screen.nativeScale > 0 ? screen.nativeScale : screen.scale
|
||||
videoLayer.contentsScale = scale
|
||||
}
|
||||
|
||||
mainBlankView?.frame = window?.bounds ?? .zero
|
||||
|
||||
let screen = containerView.window?.screen ?? window?.screen ?? UIScreen.main
|
||||
let scale = screen.nativeScale > 0 ? screen.nativeScale : screen.scale
|
||||
videoLayer.contentsScale = scale
|
||||
}
|
||||
|
||||
func externalDisplayDidChange() {
|
||||
@@ -105,7 +109,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
setMainBlankViewVisible(false)
|
||||
}
|
||||
|
||||
containerView.isHidden = !isVisible
|
||||
setContainerHidden(!isVisible)
|
||||
updateFrame()
|
||||
}
|
||||
|
||||
@@ -122,12 +126,17 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
private func moveContainerView(to superview: UIView) {
|
||||
guard let containerView else { return }
|
||||
|
||||
if containerView.superview !== superview {
|
||||
containerView.removeFromSuperview()
|
||||
withoutLayerAnimations {
|
||||
if containerView.superview !== superview {
|
||||
containerView.removeFromSuperview()
|
||||
superview.insertSubview(containerView, at: 0)
|
||||
} else if superview.subviews.first !== containerView {
|
||||
superview.insertSubview(containerView, at: 0)
|
||||
}
|
||||
|
||||
containerView.frame = superview.bounds
|
||||
containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
containerView.frame = superview.bounds
|
||||
containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
superview.insertSubview(containerView, at: 0)
|
||||
}
|
||||
|
||||
private func setMainBlankViewVisible(_ visible: Bool) {
|
||||
@@ -138,22 +147,37 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
}
|
||||
|
||||
let blankView = mainBlankView ?? UIView(frame: window.bounds)
|
||||
blankView.backgroundColor = .black
|
||||
blankView.isUserInteractionEnabled = false
|
||||
blankView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
blankView.frame = window.bounds
|
||||
withoutLayerAnimations {
|
||||
blankView.backgroundColor = .black
|
||||
blankView.isUserInteractionEnabled = false
|
||||
blankView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
blankView.frame = window.bounds
|
||||
|
||||
if blankView.superview !== window {
|
||||
blankView.removeFromSuperview()
|
||||
window.insertSubview(blankView, at: 0)
|
||||
} else {
|
||||
window.insertSubview(blankView, at: 0)
|
||||
if blankView.superview !== window {
|
||||
blankView.removeFromSuperview()
|
||||
window.insertSubview(blankView, at: 0)
|
||||
} else if window.subviews.first !== blankView {
|
||||
window.insertSubview(blankView, at: 0)
|
||||
}
|
||||
|
||||
blankView.isHidden = false
|
||||
}
|
||||
|
||||
blankView.isHidden = false
|
||||
mainBlankView = blankView
|
||||
}
|
||||
|
||||
private func setContainerHidden(_ hidden: Bool) {
|
||||
withoutLayerAnimations {
|
||||
containerView?.isHidden = hidden
|
||||
}
|
||||
}
|
||||
|
||||
private func withoutLayerAnimations(_ updates: () -> Void) {
|
||||
CATransaction.begin()
|
||||
CATransaction.setDisableActions(true)
|
||||
updates()
|
||||
CATransaction.commit()
|
||||
}
|
||||
|
||||
/// Nudge mpv to present the current paused frame after leaving PiP.
|
||||
func forceDraw() {
|
||||
command(["seek", "0", "relative+exact"])
|
||||
@@ -167,7 +191,10 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
#if os(iOS)
|
||||
if #available(iOS 17.0, *) {
|
||||
edrHeadroom = containerView?.window?.screen.potentialEDRHeadroom ?? 1.0
|
||||
videoLayer.wantsExtendedDynamicRangeContent = hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
|
||||
withoutLayerAnimations {
|
||||
videoLayer.wantsExtendedDynamicRangeContent =
|
||||
hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class AmbientLightingService {
|
||||
AmbientLightingService(this._player);
|
||||
|
||||
bool get isEnabled => _enabled;
|
||||
bool get isSupported => _player.playerType == 'mpv' && !Platform.isIOS && !Platform.isMacOS;
|
||||
bool get isSupported => _player.playerType == 'mpv' && !Platform.isIOS;
|
||||
|
||||
/// Enable ambient lighting effect.
|
||||
///
|
||||
|
||||
@@ -22,7 +22,7 @@ class ShaderService {
|
||||
|
||||
ShaderPreset get currentPreset => _currentPreset;
|
||||
|
||||
static bool get isPlatformSupported => !Platform.isIOS && !Platform.isMacOS;
|
||||
static bool get isPlatformSupported => !Platform.isIOS;
|
||||
|
||||
/// Check if the player is MPV (shaders are MPV-only)
|
||||
bool get isSupported => _player.playerType == 'mpv' && isPlatformSupported;
|
||||
|
||||
@@ -5,8 +5,11 @@ class MainFlutterWindow: NSWindow {
|
||||
override func awakeFromNib() {
|
||||
let flutterViewController = FlutterViewController()
|
||||
|
||||
// Enable transparency for Metal layer behind Flutter
|
||||
self.backgroundColor = NSColor.clear
|
||||
// Keep the window itself opaque so WindowServer does not have to blend the
|
||||
// whole video window with the desktop every frame. Flutter stays clear so
|
||||
// the native video layer behind it remains visible.
|
||||
self.isOpaque = true
|
||||
self.backgroundColor = NSColor.black
|
||||
flutterViewController.backgroundColor = NSColor.clear
|
||||
|
||||
let windowFrame = self.frame
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Cocoa
|
||||
import QuartzCore
|
||||
|
||||
/// Delegate to notify the plugin of PiP lifecycle events
|
||||
protocol MpvPipDelegate: AnyObject {
|
||||
@@ -13,8 +14,8 @@ protocol MpvPipDelegate: AnyObject {
|
||||
}
|
||||
|
||||
/// Encapsulates macOS Picture-in-Picture using the private PIP.framework (PIPViewController).
|
||||
/// This approach wraps the existing video layer in PiP — no VO switching needed.
|
||||
/// mpv continues rendering to its AVFoundation display layer throughout PiP.
|
||||
/// This approach wraps the existing Metal rendering layer in PiP — no VO switching needed.
|
||||
/// mpv continues rendering to its CAMetalLayer throughout PiP.
|
||||
class MpvPipController: NSObject, PIPViewControllerDelegate {
|
||||
|
||||
// MARK: - Properties
|
||||
@@ -39,18 +40,22 @@ class MpvPipController: NSObject, PIPViewControllerDelegate {
|
||||
|
||||
static var isSupported: Bool { true }
|
||||
|
||||
/// Enter PiP by wrapping the given video layer in a view and presenting it.
|
||||
/// Enter PiP by wrapping the given Metal layer in a view and presenting it.
|
||||
/// The layer continues receiving mpv frames — no VO switch needed.
|
||||
func startPip(videoLayer: CALayer, window: NSWindow, aspectRatio: NSSize) {
|
||||
func startPip(metalLayer: CAMetalLayer, window: NSWindow, aspectRatio: NSSize) {
|
||||
guard !isActive else { return }
|
||||
|
||||
sourceWindow = window
|
||||
|
||||
// Create a layer-hosting wrapper view for the video layer.
|
||||
// Create a layer-hosting wrapper view for the Metal layer.
|
||||
// PIPViewController resizes the view (and its root layer) as the PiP window resizes.
|
||||
let videoView = NSView(frame: NSRect(origin: .zero, size: aspectRatio))
|
||||
videoView.wantsLayer = true
|
||||
videoView.layer = videoLayer
|
||||
videoView.layer = metalLayer
|
||||
|
||||
// Reset drawableSize to zero so it auto-derives from the layer's bounds.
|
||||
// Without this, the explicit main-window drawableSize persists in PiP.
|
||||
metalLayer.drawableSize = .zero
|
||||
|
||||
// Create a view controller for PIPViewController
|
||||
let vc = NSViewController()
|
||||
@@ -92,16 +97,16 @@ class MpvPipController: NSObject, PIPViewControllerDelegate {
|
||||
autoPipEnabled = enabled
|
||||
}
|
||||
|
||||
/// Clean up after PiP closes — detaches the video layer from the wrapper view
|
||||
/// Clean up after PiP closes — detaches the Metal layer from the wrapper view
|
||||
/// so MpvPlayerCore can re-add it to the main window.
|
||||
/// Returns the layer that was hosted in PiP.
|
||||
/// Returns the Metal layer that was hosted in PiP.
|
||||
@discardableResult
|
||||
func detachLayer() -> CALayer? {
|
||||
let videoLayer = pipVideoView?.layer
|
||||
func detachLayer() -> CAMetalLayer? {
|
||||
let metalLayer = pipVideoView?.layer as? CAMetalLayer
|
||||
pipVideoView?.layer = CALayer() // detach before removing
|
||||
pipVideoView = nil
|
||||
pipVideoVC = nil
|
||||
return videoLayer
|
||||
return metalLayer
|
||||
}
|
||||
|
||||
// MARK: - PIPViewControllerDelegate
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import AVFoundation
|
||||
import Cocoa
|
||||
import Libmpv
|
||||
import QuartzCore
|
||||
|
||||
/// Core MPV player using AVFoundation sample-buffer rendering.
|
||||
/// Core MPV player using Metal rendering on macOS.
|
||||
class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
|
||||
private weak var window: NSWindow?
|
||||
@@ -23,28 +23,33 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
|
||||
self.window = window
|
||||
|
||||
let layer = MpvVideoLayer()
|
||||
let layer = MpvMetalLayer()
|
||||
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]
|
||||
layer.videoGravity = .resizeAspect
|
||||
|
||||
videoLayer = layer
|
||||
updateEDRMode(sigPeak: lastSigPeak)
|
||||
metalLayer = layer
|
||||
|
||||
contentView.wantsLayer = true
|
||||
contentView.layer?.addSublayer(layer)
|
||||
guard let contentLayer = contentView.layer else {
|
||||
print("[MpvPlayerCore] No content layer")
|
||||
metalLayer = nil
|
||||
return false
|
||||
}
|
||||
attachMetalLayer(to: contentLayer, frame: contentView.bounds)
|
||||
updateEDRMode(sigPeak: lastSigPeak)
|
||||
|
||||
print("[MpvPlayerCore] Video layer added, frame: \(layer.frame)")
|
||||
print("[MpvPlayerCore] Metal layer added, frame: \(layer.frame)")
|
||||
|
||||
guard setupMpv() else {
|
||||
print("[MpvPlayerCore] Failed to setup MPV")
|
||||
layer.removeFromSuperlayer()
|
||||
videoLayer = nil
|
||||
metalLayer = nil
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -75,23 +80,18 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
|
||||
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 }
|
||||
func reattachMetalLayer() {
|
||||
guard 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
|
||||
}
|
||||
contentView.wantsLayer = true
|
||||
if let contentLayer = contentView.layer {
|
||||
attachMetalLayer(to: contentLayer, frame: contentView.bounds)
|
||||
}
|
||||
|
||||
print("[MpvPlayerCore] Video layer reattached to window")
|
||||
print("[MpvPlayerCore] Metal layer reattached to window")
|
||||
}
|
||||
|
||||
func forceDraw() {
|
||||
@@ -102,22 +102,24 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
private var pausedState = true
|
||||
|
||||
func setVisible(_ visible: Bool) {
|
||||
guard let videoLayer, !isPipActive else { return }
|
||||
guard metalLayer != nil, !isPipActive else { return }
|
||||
|
||||
isVisible = visible
|
||||
isBackgrounded = !visible
|
||||
|
||||
if visible {
|
||||
videoLayer.removeFromSuperlayer()
|
||||
if let superlayer = window?.contentView?.layer {
|
||||
superlayer.insertSublayer(videoLayer, at: 0)
|
||||
if let contentView = window?.contentView {
|
||||
contentView.wantsLayer = true
|
||||
if let superlayer = contentView.layer {
|
||||
attachMetalLayer(to: superlayer, frame: contentView.bounds)
|
||||
}
|
||||
}
|
||||
beginPlaybackActivity()
|
||||
} else {
|
||||
endPlaybackActivity()
|
||||
}
|
||||
|
||||
videoLayer.isHidden = !visible
|
||||
setMetalLayerHidden(!visible)
|
||||
print("[MpvPlayerCore] setVisible(\(visible))")
|
||||
}
|
||||
|
||||
@@ -131,46 +133,41 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
}
|
||||
|
||||
func updateFrame(_ frame: CGRect? = nil) {
|
||||
guard let videoLayer, !isPipActive else { return }
|
||||
guard let metalLayer, !isPipActive else { return }
|
||||
|
||||
let targetFrame: CGRect
|
||||
if let frame {
|
||||
videoLayer.frame = frame
|
||||
targetFrame = frame
|
||||
} else if let contentView = window?.contentView {
|
||||
videoLayer.frame = contentView.bounds
|
||||
targetFrame = contentView.bounds
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if let screen = window?.screen ?? NSScreen.main {
|
||||
videoLayer.contentsScale = screen.backingScaleFactor
|
||||
withoutLayerAnimations {
|
||||
metalLayer.frame = targetFrame
|
||||
updateDrawableSize(for: metalLayer)
|
||||
}
|
||||
updateEDRMode(sigPeak: lastSigPeak)
|
||||
|
||||
}
|
||||
|
||||
override func updateEDRMode(sigPeak: Double) {
|
||||
guard let videoLayer else { return }
|
||||
guard let metalLayer 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
|
||||
withoutLayerAnimations {
|
||||
metalLayer.wantsExtendedDynamicRangeContent = shouldEnableEDR
|
||||
}
|
||||
|
||||
print(
|
||||
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), potentialHeadroom: \(potentialHeadroom))"
|
||||
)
|
||||
}
|
||||
|
||||
func dispose() {
|
||||
@@ -181,8 +178,8 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
disposeSharedState(destroySynchronously: false)
|
||||
|
||||
videoLayer?.removeFromSuperlayer()
|
||||
videoLayer = nil
|
||||
metalLayer?.removeFromSuperlayer()
|
||||
metalLayer = nil
|
||||
isInitialized = false
|
||||
print("[MpvPlayerCore] Disposed")
|
||||
}
|
||||
@@ -202,19 +199,19 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
}
|
||||
|
||||
@objc private func windowOcclusionDidChange(_ notification: Notification) {
|
||||
guard let videoLayer, mpv != nil, !isPipActive else { return }
|
||||
guard metalLayer != nil, 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
|
||||
print("[MpvPlayerCore] Window occluded - hiding Metal layer")
|
||||
setMetalLayerHidden(true)
|
||||
layerHiddenForOcclusion = true
|
||||
isBackgrounded = true
|
||||
endPlaybackActivity()
|
||||
} else if windowVisible && layerHiddenForOcclusion {
|
||||
print("[MpvPlayerCore] Window visible - showing video layer")
|
||||
print("[MpvPlayerCore] Window visible - showing Metal layer")
|
||||
layerHiddenForOcclusion = false
|
||||
videoLayer.isHidden = false
|
||||
setMetalLayerHidden(!isVisible)
|
||||
isBackgrounded = false
|
||||
if !pausedState {
|
||||
beginPlaybackActivity()
|
||||
@@ -237,4 +234,46 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
||||
self.playbackActivity = nil
|
||||
print("[MpvPlayerCore] Ended playback activity assertion")
|
||||
}
|
||||
|
||||
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)
|
||||
updates()
|
||||
CATransaction.commit()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter PiP by moving the AVFoundation video layer to a PiP window.
|
||||
/// Enter PiP by moving the Metal rendering layer to a PiP window.
|
||||
/// No VO switching — mpv keeps rendering to the same layer.
|
||||
private func enterPip(manual: Bool, result: FlutterResult? = nil) {
|
||||
guard let playerCore = playerCore else {
|
||||
@@ -167,8 +167,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
])
|
||||
return
|
||||
}
|
||||
guard let videoLayer = playerCore.videoLayer else {
|
||||
result?(["success": false, "errorCode": "failed", "errorMessage": "No video layer"])
|
||||
guard let metalLayer = playerCore.metalLayer else {
|
||||
result?(["success": false, "errorCode": "failed", "errorMessage": "No Metal layer"])
|
||||
return
|
||||
}
|
||||
guard let window = findFlutterWindow()?.0 else {
|
||||
@@ -191,7 +191,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
||||
enteredPipViaAuto = !manual
|
||||
playerCore.isPipActive = true
|
||||
|
||||
pip.startPip(videoLayer: videoLayer, window: window, aspectRatio: aspectRatio)
|
||||
pip.startPip(metalLayer: metalLayer, window: window, aspectRatio: aspectRatio)
|
||||
pipChannel?.invokeMethod("onPipChanged", arguments: true)
|
||||
result?(["success": true])
|
||||
}
|
||||
@@ -353,11 +353,11 @@ extension MpvPlayerPlugin: MpvPipDelegate {
|
||||
playerCore?.isPipActive = false
|
||||
enteredPipViaAuto = false
|
||||
|
||||
// Detach the video layer from the PiP wrapper view
|
||||
// Detach the Metal layer from the PiP wrapper view
|
||||
pipController?.detachLayer()
|
||||
|
||||
// Re-attach the video layer to the main window
|
||||
playerCore?.reattachVideoLayer()
|
||||
// Re-attach the Metal layer to the main window
|
||||
playerCore?.reattachMetalLayer()
|
||||
|
||||
// Force a redraw if paused (prevents black frame after PiP exit)
|
||||
if playerCore?.isPaused == true {
|
||||
|
||||
@@ -14,7 +14,35 @@ protocol MpvPlayerDelegate: AnyObject {
|
||||
func onEvent(name: String, data: [String: Any]?)
|
||||
}
|
||||
|
||||
class MpvVideoLayer: AVSampleBufferDisplayLayer {}
|
||||
#if os(macOS)
|
||||
// Workaround for MoltenVK problems that cause flicker.
|
||||
// https://github.com/mpv-player/mpv/pull/13651
|
||||
class MpvMetalLayer: CAMetalLayer {
|
||||
override var drawableSize: CGSize {
|
||||
get { super.drawableSize }
|
||||
set {
|
||||
if newValue == .zero || (Int(newValue.width) > 1 && Int(newValue.height) > 1) {
|
||||
super.drawableSize = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var wantsExtendedDynamicRangeContent: Bool {
|
||||
get { super.wantsExtendedDynamicRangeContent }
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
super.wantsExtendedDynamicRangeContent = newValue
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
super.wantsExtendedDynamicRangeContent = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
class MpvVideoLayer: AVSampleBufferDisplayLayer {}
|
||||
#endif
|
||||
|
||||
/// Safely convert a C string to Swift String with UTF-8 validation.
|
||||
/// Falls back to Latin-1 decoding if the bytes are not valid UTF-8.
|
||||
@@ -36,7 +64,11 @@ func safeString(_ cstr: UnsafePointer<CChar>) -> String {
|
||||
class MpvPlayerCoreBase: NSObject {
|
||||
weak var delegate: MpvPlayerDelegate?
|
||||
|
||||
var videoLayer: MpvVideoLayer?
|
||||
#if os(macOS)
|
||||
var metalLayer: MpvMetalLayer?
|
||||
#else
|
||||
var videoLayer: MpvVideoLayer?
|
||||
#endif
|
||||
var mpv: OpaquePointer?
|
||||
var isInitialized = false
|
||||
var isDisposing = false
|
||||
@@ -100,7 +132,11 @@ class MpvPlayerCoreBase: NSObject {
|
||||
func updateEDRMode(sigPeak: Double) {}
|
||||
|
||||
func setupMpv() -> Bool {
|
||||
guard let videoLayer else { return false }
|
||||
#if os(macOS)
|
||||
guard let renderLayer = metalLayer else { return false }
|
||||
#else
|
||||
guard let renderLayer = videoLayer else { return false }
|
||||
#endif
|
||||
|
||||
mpv = mpv_create()
|
||||
guard let mpv else {
|
||||
@@ -114,7 +150,7 @@ class MpvPlayerCoreBase: NSObject {
|
||||
checkError(mpv_request_log_messages(mpv, "warn"))
|
||||
#endif
|
||||
|
||||
var layer = Int64(Int(bitPattern: Unmanaged.passUnretained(videoLayer).toOpaque()))
|
||||
var layer = Int64(Int(bitPattern: Unmanaged.passUnretained(renderLayer).toOpaque()))
|
||||
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer))
|
||||
applySharedMpvOptions()
|
||||
configurePlatformMpvOptions()
|
||||
@@ -167,8 +203,8 @@ class MpvPlayerCoreBase: NSObject {
|
||||
}
|
||||
#endif
|
||||
|
||||
if isAvFoundationManagedProperty(name) {
|
||||
print("[MpvPlayerCore] Ignoring managed AVFoundation property: \(name)=\(value)")
|
||||
if isManagedRendererProperty(name) {
|
||||
print("[MpvPlayerCore] Ignoring managed renderer property: \(name)=\(value)")
|
||||
completion(.success(()))
|
||||
return
|
||||
}
|
||||
@@ -356,48 +392,60 @@ class MpvPlayerCoreBase: NSObject {
|
||||
|
||||
private func applySharedMpvOptions() {
|
||||
guard let mpv else { return }
|
||||
checkError(mpv_set_option_string(mpv, "vo", "avfoundation"))
|
||||
#if targetEnvironment(simulator)
|
||||
checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "no"))
|
||||
checkError(mpv_set_option_string(mpv, "hwdec", "no"))
|
||||
#else
|
||||
checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "yes"))
|
||||
#if os(macOS)
|
||||
checkError(mpv_set_option_string(mpv, "vo", "gpu-next"))
|
||||
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"))
|
||||
#else
|
||||
checkError(mpv_set_option_string(mpv, "vo", "avfoundation"))
|
||||
#if targetEnvironment(simulator)
|
||||
checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "no"))
|
||||
checkError(mpv_set_option_string(mpv, "hwdec", "no"))
|
||||
#else
|
||||
checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "yes"))
|
||||
checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox"))
|
||||
#endif
|
||||
#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", "yes"))
|
||||
}
|
||||
|
||||
private func isAvFoundationManagedProperty(_ name: String) -> Bool {
|
||||
private func isManagedRendererProperty(_ name: String) -> Bool {
|
||||
name == "vo" || name == "wid" || name == "gpu-api" || name == "gpu-context"
|
||||
|| name == "avfoundation-composite-osd"
|
||||
}
|
||||
|
||||
private func updateVideoGravityIfNeeded(name: String, value: String) {
|
||||
let gravity: AVLayerVideoGravity
|
||||
cacheLock.lock()
|
||||
switch name {
|
||||
case "panscan":
|
||||
currentPanscan = Double(value) ?? 0
|
||||
case "video-aspect-override":
|
||||
aspectOverrideActive = value != "no" && value != "-1" && value != "0"
|
||||
default:
|
||||
cacheLock.unlock()
|
||||
#if os(macOS)
|
||||
return
|
||||
}
|
||||
#else
|
||||
let gravity: AVLayerVideoGravity
|
||||
cacheLock.lock()
|
||||
switch name {
|
||||
case "panscan":
|
||||
currentPanscan = Double(value) ?? 0
|
||||
case "video-aspect-override":
|
||||
aspectOverrideActive = value != "no" && value != "-1" && value != "0"
|
||||
default:
|
||||
cacheLock.unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if aspectOverrideActive {
|
||||
gravity = .resize
|
||||
} else if currentPanscan > 0 {
|
||||
gravity = .resizeAspectFill
|
||||
} else {
|
||||
gravity = .resizeAspect
|
||||
}
|
||||
cacheLock.unlock()
|
||||
if aspectOverrideActive {
|
||||
gravity = .resize
|
||||
} else if currentPanscan > 0 {
|
||||
gravity = .resizeAspectFill
|
||||
} else {
|
||||
gravity = .resizeAspect
|
||||
}
|
||||
cacheLock.unlock()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.videoLayer?.videoGravity = gravity
|
||||
}
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.videoLayer?.videoGravity = gravity
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private func cancelPendingRequests() {
|
||||
|
||||
Reference in New Issue
Block a user