feat(apple): use AVFoundation mpv output
This commit is contained in:
@@ -781,7 +781,7 @@
|
|||||||
repositoryURL = "https://github.com/edde746/MPVKit";
|
repositoryURL = "https://github.com/edde746/MPVKit";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = revision;
|
kind = revision;
|
||||||
revision = 29803ccbfa631997d496f34c2c1ed191cc8fd2c3;
|
revision = 8aa5e1abb68fffe1d29511da9e02feca575c9311;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3"
|
"revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3"
|
"revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AVFoundation
|
||||||
import AVKit
|
import AVKit
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -22,11 +23,7 @@ import UIKit
|
|||||||
weak var delegate: MpvPipDelegate?
|
weak var delegate: MpvPipDelegate?
|
||||||
var isPipActive: Bool { false }
|
var isPipActive: Bool { false }
|
||||||
var autoStartEnabled: Bool { false }
|
var autoStartEnabled: Bool { false }
|
||||||
var layerPointer: UnsafeMutableRawPointer {
|
init(sampleBufferDisplayLayer: AVSampleBufferDisplayLayer) { super.init() }
|
||||||
// Return a dummy non-null pointer — layerPointer is handed to mpv for
|
|
||||||
// rendering into PiP, which never activates on tvOS.
|
|
||||||
UnsafeMutableRawPointer(bitPattern: 0x1)!
|
|
||||||
}
|
|
||||||
func setup(with layer: CALayer, containerView: UIView) {}
|
func setup(with layer: CALayer, containerView: UIView) {}
|
||||||
func setAutoStart(_ enabled: Bool) {}
|
func setAutoStart(_ enabled: Bool) {}
|
||||||
func warmLayer(currentTime: Double, isPlaying: Bool) {}
|
func warmLayer(currentTime: Double, isPlaying: Bool) {}
|
||||||
@@ -67,18 +64,13 @@ import UIKit
|
|||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
private var pipController: AVPictureInPictureController?
|
private var pipController: AVPictureInPictureController?
|
||||||
private let sampleBufferLayer = AVSampleBufferDisplayLayer()
|
private weak var sampleBufferLayer: AVSampleBufferDisplayLayer?
|
||||||
private var containerView: UIView?
|
|
||||||
weak var delegate: MpvPipDelegate?
|
weak var delegate: MpvPipDelegate?
|
||||||
|
|
||||||
/// Pointer to the sample buffer layer for passing to mpv as `wid`
|
|
||||||
var layerPointer: UnsafeMutableRawPointer {
|
|
||||||
Unmanaged.passUnretained(sampleBufferLayer).toOpaque()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
override init() {
|
init(sampleBufferDisplayLayer: AVSampleBufferDisplayLayer) {
|
||||||
|
self.sampleBufferLayer = sampleBufferDisplayLayer
|
||||||
super.init()
|
super.init()
|
||||||
setup()
|
setup()
|
||||||
}
|
}
|
||||||
@@ -93,18 +85,6 @@ import UIKit
|
|||||||
print("[MpvPipController] Failed to configure audio session: \(error)")
|
print("[MpvPipController] Failed to configure audio session: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The sample buffer layer must be in a visible view hierarchy for
|
|
||||||
// isPictureInPicturePossible to become true.
|
|
||||||
if let window = ExternalDisplayManager.mainApplicationWindow() {
|
|
||||||
let view = UIView(frame: window.bounds)
|
|
||||||
view.clipsToBounds = true
|
|
||||||
view.isUserInteractionEnabled = false
|
|
||||||
sampleBufferLayer.frame = view.bounds
|
|
||||||
view.layer.addSublayer(sampleBufferLayer)
|
|
||||||
window.addSubview(view)
|
|
||||||
containerView = view
|
|
||||||
}
|
|
||||||
|
|
||||||
createPipController()
|
createPipController()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +92,7 @@ import UIKit
|
|||||||
private var delegateHelper: AnyObject?
|
private var delegateHelper: AnyObject?
|
||||||
|
|
||||||
private func createPipController() {
|
private func createPipController() {
|
||||||
guard #available(iOS 15.0, *) else { return }
|
guard #available(iOS 15.0, *), let sampleBufferLayer else { return }
|
||||||
let helper = PipDelegateHelper(controller: self)
|
let helper = PipDelegateHelper(controller: self)
|
||||||
let contentSource = AVPictureInPictureController.ContentSource(
|
let contentSource = AVPictureInPictureController.ContentSource(
|
||||||
sampleBufferDisplayLayer: sampleBufferLayer,
|
sampleBufferDisplayLayer: sampleBufferLayer,
|
||||||
@@ -132,82 +112,23 @@ import UIKit
|
|||||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled
|
pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Push a black frame to the sample buffer layer so PiP has content
|
/// vo_avfoundation renders into the inline display layer, so PiP reuses
|
||||||
/// to display immediately (before vo_pip decodes the first real frame).
|
/// the current video frame instead of pushing synthetic buffers here.
|
||||||
func pushBlankFrame(width: Int32 = 1920, height: Int32 = 1080) {
|
func pushBlankFrame(width: Int32 = 1920, height: Int32 = 1080) {
|
||||||
var pixelBuffer: CVPixelBuffer?
|
|
||||||
let attrs: [String: Any] = [
|
|
||||||
kCVPixelBufferIOSurfacePropertiesKey as String: [:]
|
|
||||||
]
|
|
||||||
let status = CVPixelBufferCreate(
|
|
||||||
kCFAllocatorDefault, Int(width), Int(height),
|
|
||||||
kCVPixelFormatType_32BGRA, attrs as CFDictionary, &pixelBuffer
|
|
||||||
)
|
|
||||||
guard status == kCVReturnSuccess, let pb = pixelBuffer else { return }
|
|
||||||
|
|
||||||
// Fill with black
|
|
||||||
CVPixelBufferLockBaseAddress(pb, [])
|
|
||||||
if let base = CVPixelBufferGetBaseAddress(pb) {
|
|
||||||
memset(base, 0, CVPixelBufferGetDataSize(pb))
|
|
||||||
}
|
|
||||||
CVPixelBufferUnlockBaseAddress(pb, [])
|
|
||||||
|
|
||||||
// Use current timebase time for PTS (if available) so the frame isn't stale
|
|
||||||
let pts: CMTime
|
|
||||||
if let tb = sampleBufferLayer.controlTimebase {
|
|
||||||
pts = CMTimebaseGetTime(tb)
|
|
||||||
} else {
|
|
||||||
pts = CMTime(value: 0, timescale: 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
var timing = CMSampleTimingInfo(
|
|
||||||
duration: CMTime(value: 1, timescale: 30),
|
|
||||||
presentationTimeStamp: pts,
|
|
||||||
decodeTimeStamp: .invalid
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create format description and sample buffer
|
|
||||||
var formatDesc: CMVideoFormatDescription?
|
|
||||||
CMVideoFormatDescriptionCreateForImageBuffer(
|
|
||||||
allocator: kCFAllocatorDefault,
|
|
||||||
imageBuffer: pb,
|
|
||||||
formatDescriptionOut: &formatDesc
|
|
||||||
)
|
|
||||||
guard let fmt = formatDesc else { return }
|
|
||||||
|
|
||||||
var sampleBuffer: CMSampleBuffer?
|
|
||||||
CMSampleBufferCreateReadyWithImageBuffer(
|
|
||||||
allocator: kCFAllocatorDefault,
|
|
||||||
imageBuffer: pb,
|
|
||||||
formatDescription: fmt,
|
|
||||||
sampleTiming: &timing,
|
|
||||||
sampleBufferOut: &sampleBuffer
|
|
||||||
)
|
|
||||||
guard let sb = sampleBuffer else { return }
|
|
||||||
|
|
||||||
// Set DisplayImmediately so it shows regardless of timebase timing
|
|
||||||
if let attachments = CMSampleBufferGetSampleAttachmentsArray(
|
|
||||||
sb, createIfNecessary: true) as? [NSMutableDictionary],
|
|
||||||
let dict = attachments.first
|
|
||||||
{
|
|
||||||
dict[kCMSampleAttachmentKey_DisplayImmediately] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
sampleBufferLayer.enqueue(sb)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync the layer's controlTimebase with the actual playback position.
|
/// Sync the layer's controlTimebase with the actual playback position.
|
||||||
/// This makes the PiP progress bar show the correct time.
|
/// This makes the PiP progress bar show the correct time.
|
||||||
func syncTimebase(currentTime: Double, isPlaying: Bool) {
|
func syncTimebase(currentTime: Double, isPlaying: Bool) {
|
||||||
guard let timebase = sampleBufferLayer.controlTimebase else { return }
|
guard let timebase = sampleBufferLayer?.controlTimebase else { return }
|
||||||
let cmTime = CMTime(seconds: currentTime, preferredTimescale: 1000)
|
let cmTime = CMTime(seconds: currentTime, preferredTimescale: 1000)
|
||||||
CMTimebaseSetTime(timebase, time: cmTime)
|
CMTimebaseSetTime(timebase, time: cmTime)
|
||||||
CMTimebaseSetRate(timebase, rate: isPlaying ? 1.0 : 0.0)
|
CMTimebaseSetRate(timebase, rate: isPlaying ? 1.0 : 0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure the layer has a timebase and blank frame so the system considers
|
/// Ensure the layer has a timebase so the PiP progress UI can follow mpv.
|
||||||
/// PiP possible (required for canStartPictureInPictureAutomaticallyFromInline).
|
|
||||||
func warmLayer(currentTime: Double, isPlaying: Bool) {
|
func warmLayer(currentTime: Double, isPlaying: Bool) {
|
||||||
|
guard let sampleBufferLayer else { return }
|
||||||
if sampleBufferLayer.controlTimebase == nil {
|
if sampleBufferLayer.controlTimebase == nil {
|
||||||
var timebase: CMTimebase?
|
var timebase: CMTimebase?
|
||||||
CMTimebaseCreateWithSourceClock(
|
CMTimebaseCreateWithSourceClock(
|
||||||
@@ -220,7 +141,6 @@ import UIKit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncTimebase(currentTime: currentTime, isPlaying: isPlaying)
|
syncTimebase(currentTime: currentTime, isPlaying: isPlaying)
|
||||||
pushBlankFrame()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public API
|
// MARK: - Public API
|
||||||
@@ -241,19 +161,19 @@ import UIKit
|
|||||||
var attempts = 0
|
var attempts = 0
|
||||||
func tryStart() {
|
func tryStart() {
|
||||||
let possible = pipController.isPictureInPicturePossible
|
let possible = pipController.isPictureInPicturePossible
|
||||||
let hasTimebase = sampleBufferLayer.controlTimebase != nil
|
let hasTimebase = self.sampleBufferLayer?.controlTimebase != nil
|
||||||
|
|
||||||
let hasFrame: Bool
|
let hasFrame: Bool
|
||||||
if !waitForFrame {
|
if !waitForFrame {
|
||||||
hasFrame = true // Skip frame check for auto-PiP
|
hasFrame = true // Skip frame check for auto-PiP
|
||||||
} else if #available(iOS 17.4, *) {
|
} else if #available(iOS 17.4, *) {
|
||||||
hasFrame = sampleBufferLayer.isReadyForDisplay
|
hasFrame = self.sampleBufferLayer?.isReadyForDisplay ?? false
|
||||||
} else {
|
} else {
|
||||||
hasFrame = true
|
hasFrame = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if possible && hasTimebase && hasFrame {
|
if possible && hasTimebase && hasFrame {
|
||||||
print("[MpvPipController] vo_pip ready after \(attempts) retries, starting PiP")
|
print("[MpvPipController] vo_avfoundation ready after \(attempts) retries, starting PiP")
|
||||||
pipController.startPictureInPicture()
|
pipController.startPictureInPicture()
|
||||||
completion(true)
|
completion(true)
|
||||||
} else if attempts < 40 {
|
} else if attempts < 40 {
|
||||||
@@ -278,9 +198,7 @@ import UIKit
|
|||||||
pipController?.invalidatePlaybackState()
|
pipController?.invalidatePlaybackState()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fully tear down PiP — removes the container view from the window and
|
/// Fully tear down PiP without touching the shared inline display layer.
|
||||||
/// destroys the AVPictureInPictureController so the system can no longer
|
|
||||||
/// trigger auto-PiP after the player is disposed.
|
|
||||||
func teardown() {
|
func teardown() {
|
||||||
pipController?.stopPictureInPicture()
|
pipController?.stopPictureInPicture()
|
||||||
if #available(iOS 14.2, *) {
|
if #available(iOS 14.2, *) {
|
||||||
@@ -288,16 +206,10 @@ import UIKit
|
|||||||
}
|
}
|
||||||
pipController = nil
|
pipController = nil
|
||||||
delegateHelper = nil
|
delegateHelper = nil
|
||||||
sampleBufferLayer.flushAndRemoveImage()
|
|
||||||
sampleBufferLayer.controlTimebase = nil
|
|
||||||
sampleBufferLayer.removeFromSuperlayer()
|
|
||||||
containerView?.removeFromSuperview()
|
|
||||||
containerView = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Flush enqueued sample buffers from the layer to free video frame memory
|
/// PiP shares the inline display layer, so cleanup must not flush it.
|
||||||
func flushLayer() {
|
func flushLayer() {
|
||||||
sampleBufferLayer.flushAndRemoveImage()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Libmpv
|
import AVFoundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
/// Core MPV player using Metal rendering for iOS.
|
/// Core MPV player using AVFoundation sample-buffer rendering for iOS/tvOS.
|
||||||
class MpvPlayerCore: MpvPlayerCoreBase {
|
class MpvPlayerCore: MpvPlayerCoreBase {
|
||||||
|
|
||||||
private var containerView: UIView?
|
private var containerView: UIView?
|
||||||
@@ -23,15 +23,15 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
container.backgroundColor = .clear
|
container.backgroundColor = .clear
|
||||||
container.isUserInteractionEnabled = false
|
container.isUserInteractionEnabled = false
|
||||||
|
|
||||||
let layer = MpvMetalLayer()
|
let layer = MpvVideoLayer()
|
||||||
layer.frame = container.bounds
|
layer.frame = container.bounds
|
||||||
layer.contentsScale = window.screen.nativeScale
|
layer.contentsScale = window.screen.nativeScale
|
||||||
layer.framebufferOnly = true
|
|
||||||
layer.backgroundColor = UIColor.black.cgColor
|
layer.backgroundColor = UIColor.black.cgColor
|
||||||
|
layer.videoGravity = .resizeAspect
|
||||||
|
|
||||||
container.layer.addSublayer(layer)
|
container.layer.addSublayer(layer)
|
||||||
containerView = container
|
containerView = container
|
||||||
metalLayer = layer
|
videoLayer = layer
|
||||||
|
|
||||||
window.insertSubview(container, at: 0)
|
window.insertSubview(container, at: 0)
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
print("[MpvPlayerCore] Failed to setup MPV")
|
print("[MpvPlayerCore] Failed to setup MPV")
|
||||||
layer.removeFromSuperlayer()
|
layer.removeFromSuperlayer()
|
||||||
container.removeFromSuperview()
|
container.removeFromSuperview()
|
||||||
metalLayer = nil
|
videoLayer = nil
|
||||||
containerView = nil
|
containerView = nil
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -54,40 +54,7 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchToPipVO(layerPtr: UnsafeMutableRawPointer) -> Bool {
|
var sampleBufferDisplayLayer: MpvVideoLayer? { videoLayer }
|
||||||
guard mpv != nil else { return false }
|
|
||||||
|
|
||||||
print("[MpvPlayerCore] Switching to pip VO for PiP")
|
|
||||||
|
|
||||||
metalLayer?.removeFromSuperlayer()
|
|
||||||
|
|
||||||
setProperty("vid", value: "no")
|
|
||||||
setInt64PropertyAsync("wid", value: Int64(Int(bitPattern: layerPtr))) { _ in }
|
|
||||||
setProperty("vo", value: "pip")
|
|
||||||
setProperty("vid", value: "auto")
|
|
||||||
|
|
||||||
print("[MpvPlayerCore] Switched to pip VO successfully")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func switchToGpuNextVO() -> Bool {
|
|
||||||
guard mpv != nil, let metalLayer else { return false }
|
|
||||||
|
|
||||||
print("[MpvPlayerCore] Switching back to gpu-next VO")
|
|
||||||
|
|
||||||
setProperty("vid", value: "no")
|
|
||||||
setInt64PropertyAsync("wid", value: Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque()))) { _ in
|
|
||||||
}
|
|
||||||
applyGpuNextOptions()
|
|
||||||
setProperty("vid", value: "auto")
|
|
||||||
|
|
||||||
if metalLayer.superlayer == nil, let containerView {
|
|
||||||
containerView.layer.addSublayer(metalLayer)
|
|
||||||
}
|
|
||||||
|
|
||||||
print("[MpvPlayerCore] Switched back to gpu-next VO successfully")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func setVisible(_ visible: Bool) {
|
func setVisible(_ visible: Bool) {
|
||||||
guard let containerView else { return }
|
guard let containerView else { return }
|
||||||
@@ -99,28 +66,24 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateFrame(_ frame: CGRect? = nil) {
|
func updateFrame(_ frame: CGRect? = nil) {
|
||||||
guard let metalLayer, let containerView else { return }
|
guard let videoLayer, let containerView else { return }
|
||||||
|
|
||||||
if let frame {
|
if let frame {
|
||||||
containerView.frame = frame
|
containerView.frame = frame
|
||||||
metalLayer.frame = containerView.bounds
|
videoLayer.frame = containerView.bounds
|
||||||
} else if let superview = containerView.superview {
|
} else if let superview = containerView.superview {
|
||||||
containerView.frame = superview.bounds
|
containerView.frame = superview.bounds
|
||||||
metalLayer.frame = containerView.bounds
|
videoLayer.frame = containerView.bounds
|
||||||
} else if let window {
|
} else if let window {
|
||||||
containerView.frame = window.bounds
|
containerView.frame = window.bounds
|
||||||
metalLayer.frame = containerView.bounds
|
videoLayer.frame = containerView.bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
mainBlankView?.frame = window?.bounds ?? .zero
|
mainBlankView?.frame = window?.bounds ?? .zero
|
||||||
|
|
||||||
let screen = containerView.window?.screen ?? window?.screen ?? UIScreen.main
|
let screen = containerView.window?.screen ?? window?.screen ?? UIScreen.main
|
||||||
let scale = screen.nativeScale > 0 ? screen.nativeScale : screen.scale
|
let scale = screen.nativeScale > 0 ? screen.nativeScale : screen.scale
|
||||||
metalLayer.contentsScale = scale
|
videoLayer.contentsScale = scale
|
||||||
metalLayer.drawableSize = CGSize(
|
|
||||||
width: metalLayer.frame.width * scale,
|
|
||||||
height: metalLayer.frame.height * scale
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func externalDisplayDidChange() {
|
func externalDisplayDidChange() {
|
||||||
@@ -191,20 +154,19 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
mainBlankView = blankView
|
mainBlankView = blankView
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nudge mpv to present the current paused frame after switching back from PiP.
|
/// Nudge mpv to present the current paused frame after leaving PiP.
|
||||||
func forceDraw() {
|
func forceDraw() {
|
||||||
command(["seek", "0", "relative+exact"])
|
command(["seek", "0", "relative+exact"])
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateEDRMode(sigPeak: Double) {
|
override func updateEDRMode(sigPeak: Double) {
|
||||||
guard let metalLayer else { return }
|
guard let videoLayer else { return }
|
||||||
|
|
||||||
var edrHeadroom: CGFloat = 1.0
|
var edrHeadroom: CGFloat = 1.0
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 17.0, *) {
|
||||||
edrHeadroom = containerView?.window?.screen.potentialEDRHeadroom ?? 1.0
|
edrHeadroom = containerView?.window?.screen.potentialEDRHeadroom ?? 1.0
|
||||||
metalLayer.wantsExtendedDynamicRangeContent =
|
videoLayer.wantsExtendedDynamicRangeContent = hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
|
||||||
hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -221,8 +183,8 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
#endif
|
#endif
|
||||||
disposeSharedState(destroySynchronously: false)
|
disposeSharedState(destroySynchronously: false)
|
||||||
|
|
||||||
metalLayer?.removeFromSuperlayer()
|
videoLayer?.removeFromSuperlayer()
|
||||||
metalLayer = nil
|
videoLayer = nil
|
||||||
containerView?.removeFromSuperview()
|
containerView?.removeFromSuperview()
|
||||||
containerView = nil
|
containerView = nil
|
||||||
mainBlankView?.removeFromSuperview()
|
mainBlankView?.removeFromSuperview()
|
||||||
|
|||||||
@@ -96,9 +96,10 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
|
|
||||||
// MARK: - PiP
|
// MARK: - PiP
|
||||||
|
|
||||||
private func ensurePipController() -> MpvPipController {
|
private func ensurePipController() -> MpvPipController? {
|
||||||
if let existing = pipController { return existing }
|
if let existing = pipController { return existing }
|
||||||
let controller = MpvPipController()
|
guard let layer = playerCore?.sampleBufferDisplayLayer else { return nil }
|
||||||
|
let controller = MpvPipController(sampleBufferDisplayLayer: layer)
|
||||||
controller.delegate = self
|
controller.delegate = self
|
||||||
pipController = controller
|
pipController = controller
|
||||||
return controller
|
return controller
|
||||||
@@ -158,7 +159,10 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
if let args = call.arguments as? [String: Any], let ready = args["ready"] as? Bool {
|
if let args = call.arguments as? [String: Any], let ready = args["ready"] as? Bool {
|
||||||
self.autoPipEnabled = ready
|
self.autoPipEnabled = ready
|
||||||
if ready {
|
if ready {
|
||||||
let pip = self.ensurePipController()
|
guard let pip = self.ensurePipController() else {
|
||||||
|
result(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
pip.setAutoStart(true)
|
pip.setAutoStart(true)
|
||||||
// Warm the layer so the system considers PiP possible
|
// Warm the layer so the system considers PiP possible
|
||||||
if let pc = self.playerCore {
|
if let pc = self.playerCore {
|
||||||
@@ -175,16 +179,14 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Switch to PiP VO and prepare the sample buffer layer for PiP display.
|
/// Prepare the shared AVFoundation display layer for PiP display.
|
||||||
/// Returns the MpvPipController on success, nil on failure.
|
/// Returns the MpvPipController on success, nil on failure.
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func switchToPipAndPrepare() -> MpvPipController? {
|
private func preparePip() -> MpvPipController? {
|
||||||
guard let playerCore = playerCore else { return nil }
|
guard let playerCore = playerCore else { return nil }
|
||||||
let pip = ensurePipController()
|
guard let pip = ensurePipController() else { return nil }
|
||||||
guard playerCore.switchToPipVO(layerPtr: pip.layerPointer) else { return nil }
|
|
||||||
pendingInlineRestoreAfterPip = false
|
pendingInlineRestoreAfterPip = false
|
||||||
playerCore.isPipStarting = true
|
playerCore.isPipStarting = true
|
||||||
pip.pushBlankFrame()
|
|
||||||
pip.syncTimebase(currentTime: playerCore.timePos, isPlaying: !playerCore.isPaused)
|
pip.syncTimebase(currentTime: playerCore.timePos, isPlaying: !playerCore.isPaused)
|
||||||
pip.invalidatePlaybackState()
|
pip.invalidatePlaybackState()
|
||||||
return pip
|
return pip
|
||||||
@@ -205,10 +207,10 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
])
|
])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let pip = switchToPipAndPrepare() else {
|
guard let pip = preparePip() else {
|
||||||
result?([
|
result?([
|
||||||
"success": false, "errorCode": "vo_switch_failed",
|
"success": false, "errorCode": "pip_prepare_failed",
|
||||||
"errorMessage": "Failed to switch VO",
|
"errorMessage": "Failed to prepare PiP",
|
||||||
])
|
])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -232,15 +234,13 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
playerCore?.isPipActive = false
|
playerCore?.isPipActive = false
|
||||||
isManualPipRequest = false
|
isManualPipRequest = false
|
||||||
stopPipTimebaseSync()
|
stopPipTimebaseSync()
|
||||||
pipController?.flushLayer()
|
|
||||||
let restoredInlineVO = playerCore?.switchToGpuNextVO() ?? false
|
|
||||||
if pause {
|
if pause {
|
||||||
playerCore?.setPropertyAsync("pause", value: "yes") { [weak self] _ in
|
playerCore?.setPropertyAsync("pause", value: "yes") { [weak self] _ in
|
||||||
self?.pipController?.invalidatePlaybackState()
|
self?.pipController?.invalidatePlaybackState()
|
||||||
self?.syncPipTimebase()
|
self?.syncPipTimebase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pendingInlineRestoreAfterPip = restoredInlineVO
|
pendingInlineRestoreAfterPip = true
|
||||||
if pendingInlineRestoreAfterPip {
|
if pendingInlineRestoreAfterPip {
|
||||||
if isSceneActive {
|
if isSceneActive {
|
||||||
restoreInlinePlayerAfterPip()
|
restoreInlinePlayerAfterPip()
|
||||||
@@ -393,11 +393,11 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
extension MpvPlayerPlugin: MpvPipDelegate {
|
extension MpvPlayerPlugin: MpvPipDelegate {
|
||||||
|
|
||||||
func pipWillStart() {
|
func pipWillStart() {
|
||||||
// If PiP was system-initiated (not via our enterPip), switch VO now
|
// If PiP was system-initiated (not via our enterPip), prepare the shared layer now.
|
||||||
guard let playerCore = playerCore, !playerCore.isPipStarting else { return }
|
guard let playerCore = playerCore, !playerCore.isPipStarting else { return }
|
||||||
print("[MpvPlayerPlugin] System-initiated PiP detected, switching VO")
|
print("[MpvPlayerPlugin] System-initiated PiP detected, preparing shared layer")
|
||||||
if switchToPipAndPrepare() == nil {
|
if preparePip() == nil {
|
||||||
print("[MpvPlayerPlugin] VO switch failed for system-initiated PiP")
|
print("[MpvPlayerPlugin] PiP preparation failed for system-initiated PiP")
|
||||||
pipController?.stopPip()
|
pipController?.stopPip()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import '../../i18n/strings.g.dart';
|
import '../../i18n/strings.g.dart';
|
||||||
import '../../models/hotkey_model.dart';
|
import '../../models/hotkey_model.dart';
|
||||||
import '../../services/keyboard_shortcuts_service.dart';
|
import '../../services/keyboard_shortcuts_service.dart';
|
||||||
|
import '../../services/shader_service.dart';
|
||||||
import '../../utils/snackbar_helper.dart';
|
import '../../utils/snackbar_helper.dart';
|
||||||
import '../../focus/focusable_button.dart';
|
import '../../focus/focusable_button.dart';
|
||||||
import '../../widgets/focused_scroll_scaffold.dart';
|
import '../../widgets/focused_scroll_scaffold.dart';
|
||||||
@@ -19,7 +20,9 @@ class KeyboardShortcutsScreen extends StatelessWidget {
|
|||||||
listenable: keyboardService,
|
listenable: keyboardService,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final hotkeys = keyboardService.hotkeys;
|
final hotkeys = keyboardService.hotkeys;
|
||||||
final actions = hotkeys.keys.toList();
|
final actions = hotkeys.keys
|
||||||
|
.where((action) => action != 'shader_toggle' || ShaderService.isPlatformSupported)
|
||||||
|
.toList();
|
||||||
return FocusedScrollScaffold(
|
return FocusedScrollScaffold(
|
||||||
title: Text(t.settings.keyboardShortcuts),
|
title: Text(t.settings.keyboardShortcuts),
|
||||||
slivers: [
|
slivers: [
|
||||||
|
|||||||
@@ -212,7 +212,9 @@ extension _VideoPlayerBuildMethods on VideoPlayerScreenState {
|
|||||||
onLiveSeek: _captureBuffer != null ? _seekLivePosition : null,
|
onLiveSeek: _captureBuffer != null ? _seekLivePosition : null,
|
||||||
onJumpToLive: _captureBuffer != null && !_isAtLiveEdge ? _jumpToLiveEdge : null,
|
onJumpToLive: _captureBuffer != null && !_isAtLiveEdge ? _jumpToLiveEdge : null,
|
||||||
isAmbientLightingEnabled: _ambientLightingService?.isEnabled ?? false,
|
isAmbientLightingEnabled: _ambientLightingService?.isEnabled ?? false,
|
||||||
onToggleAmbientLighting: _toggleAmbientLighting,
|
onToggleAmbientLighting: _ambientLightingService?.isSupported == true
|
||||||
|
? _toggleAmbientLighting
|
||||||
|
: null,
|
||||||
toastController: _toastController,
|
toastController: _toastController,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,8 +37,9 @@ extension _VideoPlayerPipShaderMethods on VideoPlayerScreenState {
|
|||||||
|
|
||||||
if (_videoPIPManager == null || _videoFilterManager == null) return;
|
if (_videoPIPManager == null || _videoFilterManager == null) return;
|
||||||
|
|
||||||
// Only handle exit - entry is handled by onBeforeEnterPip callback
|
if (isInPip) {
|
||||||
if (!isInPip) {
|
_videoFilterManager!.enterPipMode();
|
||||||
|
} else {
|
||||||
final restoreAmbient = _videoFilterManager!.hadAmbientLightingBeforePip;
|
final restoreAmbient = _videoFilterManager!.hadAmbientLightingBeforePip;
|
||||||
_videoFilterManager!.exitPipMode();
|
_videoFilterManager!.exitPipMode();
|
||||||
// Restore ambient lighting if it was active before PiP
|
// Restore ambient lighting if it was active before PiP
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class AmbientLightingService {
|
|||||||
AmbientLightingService(this._player);
|
AmbientLightingService(this._player);
|
||||||
|
|
||||||
bool get isEnabled => _enabled;
|
bool get isEnabled => _enabled;
|
||||||
bool get isSupported => _player.playerType == 'mpv';
|
bool get isSupported => _player.playerType == 'mpv' && !Platform.isIOS && !Platform.isMacOS;
|
||||||
|
|
||||||
/// Enable ambient lighting effect.
|
/// Enable ambient lighting effect.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import '../models/shader_preset.dart';
|
import '../models/shader_preset.dart';
|
||||||
import '../mpv/player/player.dart';
|
import '../mpv/player/player.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
@@ -20,8 +22,10 @@ class ShaderService {
|
|||||||
|
|
||||||
ShaderPreset get currentPreset => _currentPreset;
|
ShaderPreset get currentPreset => _currentPreset;
|
||||||
|
|
||||||
|
static bool get isPlatformSupported => !Platform.isIOS && !Platform.isMacOS;
|
||||||
|
|
||||||
/// Check if the player is MPV (shaders are MPV-only)
|
/// Check if the player is MPV (shaders are MPV-only)
|
||||||
bool get isSupported => _player.playerType == 'mpv';
|
bool get isSupported => _player.playerType == 'mpv' && isPlatformSupported;
|
||||||
|
|
||||||
/// Apply a shader preset to the video player.
|
/// Apply a shader preset to the video player.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ class VideoPIPManager {
|
|||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset video filter to contain mode BEFORE entering PiP (Android only —
|
// Reset video filter to contain mode before entering PiP. Android, iOS,
|
||||||
// iOS switches VO entirely so the filter is irrelevant)
|
// and macOS all reuse the inline video surface/layer for PiP.
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid || Platform.isIOS || Platform.isMacOS) {
|
||||||
onBeforeEnterPip?.call();
|
onBeforeEnterPip?.call();
|
||||||
// Wait a frame for the filter change to take effect
|
// Wait a frame for the filter change to take effect
|
||||||
await Future.delayed(const Duration(milliseconds: 50));
|
await Future.delayed(const Duration(milliseconds: 50));
|
||||||
|
|||||||
@@ -875,7 +875,7 @@
|
|||||||
repositoryURL = "https://github.com/edde746/MPVKit";
|
repositoryURL = "https://github.com/edde746/MPVKit";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = revision;
|
kind = revision;
|
||||||
revision = 29803ccbfa631997d496f34c2c1ed191cc8fd2c3;
|
revision = 8aa5e1abb68fffe1d29511da9e02feca575c9311;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3"
|
"revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3"
|
"revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ protocol MpvPipDelegate: AnyObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Encapsulates macOS Picture-in-Picture using the private PIP.framework (PIPViewController).
|
/// Encapsulates macOS Picture-in-Picture using the private PIP.framework (PIPViewController).
|
||||||
/// This approach wraps the existing Metal rendering view in PiP — no VO switching needed.
|
/// This approach wraps the existing video layer in PiP — no VO switching needed.
|
||||||
/// mpv continues rendering to its CAMetalLayer throughout PiP.
|
/// mpv continues rendering to its AVFoundation display layer throughout PiP.
|
||||||
class MpvPipController: NSObject, PIPViewControllerDelegate {
|
class MpvPipController: NSObject, PIPViewControllerDelegate {
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
@@ -39,23 +39,18 @@ class MpvPipController: NSObject, PIPViewControllerDelegate {
|
|||||||
|
|
||||||
static var isSupported: Bool { true }
|
static var isSupported: Bool { true }
|
||||||
|
|
||||||
/// Enter PiP by wrapping the given Metal layer in a view and presenting it.
|
/// Enter PiP by wrapping the given video layer in a view and presenting it.
|
||||||
/// The layer continues receiving mpv frames — no VO switch needed.
|
/// The layer continues receiving mpv frames — no VO switch needed.
|
||||||
func startPip(metalLayer: CAMetalLayer, window: NSWindow, aspectRatio: NSSize) {
|
func startPip(videoLayer: CALayer, window: NSWindow, aspectRatio: NSSize) {
|
||||||
guard !isActive else { return }
|
guard !isActive else { return }
|
||||||
|
|
||||||
sourceWindow = window
|
sourceWindow = window
|
||||||
|
|
||||||
// Create a layer-hosting wrapper view for the Metal layer.
|
// Create a layer-hosting wrapper view for the video layer.
|
||||||
// PIPViewController resizes the view (and its root layer) as the PiP window resizes.
|
// PIPViewController resizes the view (and its root layer) as the PiP window resizes.
|
||||||
let videoView = NSView(frame: NSRect(origin: .zero, size: aspectRatio))
|
let videoView = NSView(frame: NSRect(origin: .zero, size: aspectRatio))
|
||||||
videoView.wantsLayer = true
|
videoView.wantsLayer = true
|
||||||
videoView.layer = metalLayer
|
videoView.layer = videoLayer
|
||||||
|
|
||||||
// Reset drawableSize to zero so it auto-derives from the layer's bounds.
|
|
||||||
// Without this, the explicit drawableSize set by updateFrame() (main window size)
|
|
||||||
// persists and causes mpv/MoltenVK to render at the wrong resolution in PiP.
|
|
||||||
metalLayer.drawableSize = .zero
|
|
||||||
|
|
||||||
// Create a view controller for PIPViewController
|
// Create a view controller for PIPViewController
|
||||||
let vc = NSViewController()
|
let vc = NSViewController()
|
||||||
@@ -97,16 +92,16 @@ class MpvPipController: NSObject, PIPViewControllerDelegate {
|
|||||||
autoPipEnabled = enabled
|
autoPipEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clean up after PiP closes — detaches the Metal layer from the wrapper view
|
/// Clean up after PiP closes — detaches the video layer from the wrapper view
|
||||||
/// so MpvPlayerCore can re-add it to the main window.
|
/// so MpvPlayerCore can re-add it to the main window.
|
||||||
/// Returns the Metal layer that was hosted in PiP.
|
/// Returns the layer that was hosted in PiP.
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func detachLayer() -> CAMetalLayer? {
|
func detachLayer() -> CALayer? {
|
||||||
let metalLayer = pipVideoView?.layer as? CAMetalLayer
|
let videoLayer = pipVideoView?.layer
|
||||||
pipVideoView?.layer = CALayer() // detach before removing
|
pipVideoView?.layer = CALayer() // detach before removing
|
||||||
pipVideoView = nil
|
pipVideoView = nil
|
||||||
pipVideoVC = nil
|
pipVideoVC = nil
|
||||||
return metalLayer
|
return videoLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - PIPViewControllerDelegate
|
// MARK: - PIPViewControllerDelegate
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import AVFoundation
|
||||||
import Cocoa
|
import Cocoa
|
||||||
import Libmpv
|
import Libmpv
|
||||||
|
|
||||||
/// Core MPV player using Metal rendering.
|
/// Core MPV player using AVFoundation sample-buffer rendering.
|
||||||
class MpvPlayerCore: MpvPlayerCoreBase {
|
class MpvPlayerCore: MpvPlayerCoreBase {
|
||||||
|
|
||||||
private weak var window: NSWindow?
|
private weak var window: NSWindow?
|
||||||
@@ -22,27 +23,28 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
|
|
||||||
self.window = window
|
self.window = window
|
||||||
|
|
||||||
let layer = MpvMetalLayer()
|
let layer = MpvVideoLayer()
|
||||||
layer.frame = contentView.bounds
|
layer.frame = contentView.bounds
|
||||||
if let screen = window.screen ?? NSScreen.main {
|
if let screen = window.screen ?? NSScreen.main {
|
||||||
layer.contentsScale = screen.backingScaleFactor
|
layer.contentsScale = screen.backingScaleFactor
|
||||||
}
|
}
|
||||||
layer.framebufferOnly = true
|
|
||||||
layer.isOpaque = true
|
layer.isOpaque = true
|
||||||
layer.backgroundColor = NSColor.black.cgColor
|
layer.backgroundColor = NSColor.black.cgColor
|
||||||
layer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
|
layer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
|
||||||
|
layer.videoGravity = .resizeAspect
|
||||||
|
|
||||||
metalLayer = layer
|
videoLayer = layer
|
||||||
|
updateEDRMode(sigPeak: lastSigPeak)
|
||||||
|
|
||||||
contentView.wantsLayer = true
|
contentView.wantsLayer = true
|
||||||
contentView.layer?.addSublayer(layer)
|
contentView.layer?.addSublayer(layer)
|
||||||
|
|
||||||
print("[MpvPlayerCore] Metal layer added, frame: \(layer.frame)")
|
print("[MpvPlayerCore] Video layer added, frame: \(layer.frame)")
|
||||||
|
|
||||||
guard setupMpv() else {
|
guard setupMpv() else {
|
||||||
print("[MpvPlayerCore] Failed to setup MPV")
|
print("[MpvPlayerCore] Failed to setup MPV")
|
||||||
layer.removeFromSuperlayer()
|
layer.removeFromSuperlayer()
|
||||||
metalLayer = nil
|
videoLayer = nil
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,29 +75,23 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
|
|
||||||
override func configurePlatformMpvOptions() {
|
override func configurePlatformMpvOptions() {
|
||||||
guard let mpv else { return }
|
guard let mpv else { return }
|
||||||
|
checkError(mpv_set_option_string(mpv, "avfoundation-composite-osd", "no"))
|
||||||
checkError(mpv_set_option_string(mpv, "ao", "avfoundation,coreaudio"))
|
checkError(mpv_set_option_string(mpv, "ao", "avfoundation,coreaudio"))
|
||||||
// Default fifo (vsync) mode — mailbox was causing continuous GPU rendering even when paused
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var videoLayer: CAMetalLayer? { metalLayer }
|
func reattachVideoLayer() {
|
||||||
|
guard let videoLayer, let contentView = window?.contentView else { return }
|
||||||
|
|
||||||
func reattachMetalLayer() {
|
if videoLayer.superlayer == nil {
|
||||||
guard let metalLayer, let contentView = window?.contentView else { return }
|
|
||||||
|
|
||||||
if metalLayer.superlayer == nil {
|
|
||||||
contentView.wantsLayer = true
|
contentView.wantsLayer = true
|
||||||
contentView.layer?.insertSublayer(metalLayer, at: 0)
|
contentView.layer?.insertSublayer(videoLayer, at: 0)
|
||||||
metalLayer.frame = contentView.bounds
|
videoLayer.frame = contentView.bounds
|
||||||
if let screen = window?.screen ?? NSScreen.main {
|
if let screen = window?.screen ?? NSScreen.main {
|
||||||
metalLayer.contentsScale = screen.backingScaleFactor
|
videoLayer.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")
|
print("[MpvPlayerCore] Video layer reattached to window")
|
||||||
}
|
}
|
||||||
|
|
||||||
func forceDraw() {
|
func forceDraw() {
|
||||||
@@ -106,22 +102,22 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
private var pausedState = true
|
private var pausedState = true
|
||||||
|
|
||||||
func setVisible(_ visible: Bool) {
|
func setVisible(_ visible: Bool) {
|
||||||
guard let metalLayer, !isPipActive else { return }
|
guard let videoLayer, !isPipActive else { return }
|
||||||
|
|
||||||
isVisible = visible
|
isVisible = visible
|
||||||
isBackgrounded = !visible
|
isBackgrounded = !visible
|
||||||
|
|
||||||
if visible {
|
if visible {
|
||||||
metalLayer.removeFromSuperlayer()
|
videoLayer.removeFromSuperlayer()
|
||||||
if let superlayer = window?.contentView?.layer {
|
if let superlayer = window?.contentView?.layer {
|
||||||
superlayer.insertSublayer(metalLayer, at: 0)
|
superlayer.insertSublayer(videoLayer, at: 0)
|
||||||
}
|
}
|
||||||
beginPlaybackActivity()
|
beginPlaybackActivity()
|
||||||
} else {
|
} else {
|
||||||
endPlaybackActivity()
|
endPlaybackActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
metalLayer.isHidden = !visible
|
videoLayer.isHidden = !visible
|
||||||
print("[MpvPlayerCore] setVisible(\(visible))")
|
print("[MpvPlayerCore] setVisible(\(visible))")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,38 +131,48 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateFrame(_ frame: CGRect? = nil) {
|
func updateFrame(_ frame: CGRect? = nil) {
|
||||||
guard let metalLayer, !isPipActive else { return }
|
guard let videoLayer, !isPipActive else { return }
|
||||||
|
|
||||||
if let frame {
|
if let frame {
|
||||||
metalLayer.frame = frame
|
videoLayer.frame = frame
|
||||||
} else if let contentView = window?.contentView {
|
} else if let contentView = window?.contentView {
|
||||||
metalLayer.frame = contentView.bounds
|
videoLayer.frame = contentView.bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
if let screen = window?.screen ?? NSScreen.main {
|
if let screen = window?.screen ?? NSScreen.main {
|
||||||
let scale = screen.backingScaleFactor
|
videoLayer.contentsScale = screen.backingScaleFactor
|
||||||
metalLayer.drawableSize = CGSize(
|
|
||||||
width: metalLayer.frame.width * scale,
|
|
||||||
height: metalLayer.frame.height * scale
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
updateEDRMode(sigPeak: lastSigPeak)
|
||||||
|
|
||||||
print("[MpvPlayerCore] updateFrame: \(metalLayer.frame)")
|
print("[MpvPlayerCore] updateFrame: \(videoLayer.frame)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateEDRMode(sigPeak: Double) {
|
override func updateEDRMode(sigPeak: Double) {
|
||||||
guard let metalLayer else { return }
|
guard let videoLayer else { return }
|
||||||
|
|
||||||
var edrHeadroom: CGFloat = 1.0
|
var currentHeadroom: CGFloat = 1.0
|
||||||
|
var potentialHeadroom: CGFloat = 1.0
|
||||||
if let screen = window?.screen ?? NSScreen.main {
|
if let screen = window?.screen ?? NSScreen.main {
|
||||||
edrHeadroom = screen.maximumExtendedDynamicRangeColorComponentValue
|
currentHeadroom = screen.maximumExtendedDynamicRangeColorComponentValue
|
||||||
|
potentialHeadroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue
|
||||||
}
|
}
|
||||||
|
|
||||||
let shouldEnableEDR = hdrEnabled && sigPeak > 1.0 && edrHeadroom > 1.0
|
let signalHeadroom = CGFloat(max(sigPeak, 1.0))
|
||||||
metalLayer.wantsExtendedDynamicRangeContent = shouldEnableEDR
|
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(
|
print(
|
||||||
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), headroom: \(edrHeadroom))"
|
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), currentHeadroom: \(currentHeadroom), potentialHeadroom: \(potentialHeadroom), contentHeadroom: \(shouldEnableEDR ? contentHeadroom : 0))"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +184,8 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
disposeSharedState(destroySynchronously: false)
|
disposeSharedState(destroySynchronously: false)
|
||||||
|
|
||||||
metalLayer?.removeFromSuperlayer()
|
videoLayer?.removeFromSuperlayer()
|
||||||
metalLayer = nil
|
videoLayer = nil
|
||||||
isInitialized = false
|
isInitialized = false
|
||||||
print("[MpvPlayerCore] Disposed")
|
print("[MpvPlayerCore] Disposed")
|
||||||
}
|
}
|
||||||
@@ -199,19 +205,19 @@ class MpvPlayerCore: MpvPlayerCoreBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func windowOcclusionDidChange(_ notification: Notification) {
|
@objc private func windowOcclusionDidChange(_ notification: Notification) {
|
||||||
guard let metalLayer, mpv != nil, !isPipActive else { return }
|
guard let videoLayer, mpv != nil, !isPipActive else { return }
|
||||||
|
|
||||||
let windowVisible = window?.occlusionState.contains(.visible) ?? true
|
let windowVisible = window?.occlusionState.contains(.visible) ?? true
|
||||||
if !windowVisible && !layerHiddenForOcclusion {
|
if !windowVisible && !layerHiddenForOcclusion {
|
||||||
print("[MpvPlayerCore] Window occluded - hiding Metal layer")
|
print("[MpvPlayerCore] Window occluded - hiding video layer")
|
||||||
metalLayer.isHidden = true
|
videoLayer.isHidden = true
|
||||||
layerHiddenForOcclusion = true
|
layerHiddenForOcclusion = true
|
||||||
isBackgrounded = true
|
isBackgrounded = true
|
||||||
endPlaybackActivity()
|
endPlaybackActivity()
|
||||||
} else if windowVisible && layerHiddenForOcclusion {
|
} else if windowVisible && layerHiddenForOcclusion {
|
||||||
print("[MpvPlayerCore] Window visible - showing Metal layer")
|
print("[MpvPlayerCore] Window visible - showing video layer")
|
||||||
layerHiddenForOcclusion = false
|
layerHiddenForOcclusion = false
|
||||||
metalLayer.isHidden = false
|
videoLayer.isHidden = false
|
||||||
isBackgrounded = false
|
isBackgrounded = false
|
||||||
if !pausedState {
|
if !pausedState {
|
||||||
beginPlaybackActivity()
|
beginPlaybackActivity()
|
||||||
|
|||||||
@@ -158,8 +158,8 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter PiP by moving the Metal rendering layer to a PiP window.
|
/// Enter PiP by moving the AVFoundation video layer to a PiP window.
|
||||||
/// No VO switching — mpv keeps rendering to the same Metal layer.
|
/// No VO switching — mpv keeps rendering to the same layer.
|
||||||
private func enterPip(manual: Bool, result: FlutterResult? = nil) {
|
private func enterPip(manual: Bool, result: FlutterResult? = nil) {
|
||||||
guard let playerCore = playerCore else {
|
guard let playerCore = playerCore else {
|
||||||
result?([
|
result?([
|
||||||
@@ -167,7 +167,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
])
|
])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard let metalLayer = playerCore.videoLayer else {
|
guard let videoLayer = playerCore.videoLayer else {
|
||||||
result?(["success": false, "errorCode": "failed", "errorMessage": "No video layer"])
|
result?(["success": false, "errorCode": "failed", "errorMessage": "No video layer"])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ class MpvPlayerPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, MpvPluginS
|
|||||||
enteredPipViaAuto = !manual
|
enteredPipViaAuto = !manual
|
||||||
playerCore.isPipActive = true
|
playerCore.isPipActive = true
|
||||||
|
|
||||||
pip.startPip(metalLayer: metalLayer, window: window, aspectRatio: aspectRatio)
|
pip.startPip(videoLayer: videoLayer, window: window, aspectRatio: aspectRatio)
|
||||||
pipChannel?.invokeMethod("onPipChanged", arguments: true)
|
pipChannel?.invokeMethod("onPipChanged", arguments: true)
|
||||||
result?(["success": true])
|
result?(["success": true])
|
||||||
}
|
}
|
||||||
@@ -353,11 +353,11 @@ extension MpvPlayerPlugin: MpvPipDelegate {
|
|||||||
playerCore?.isPipActive = false
|
playerCore?.isPipActive = false
|
||||||
enteredPipViaAuto = false
|
enteredPipViaAuto = false
|
||||||
|
|
||||||
// Detach the Metal layer from the PiP wrapper view
|
// Detach the video layer from the PiP wrapper view
|
||||||
pipController?.detachLayer()
|
pipController?.detachLayer()
|
||||||
|
|
||||||
// Re-attach the Metal layer to the main window
|
// Re-attach the video layer to the main window
|
||||||
playerCore?.reattachMetalLayer()
|
playerCore?.reattachVideoLayer()
|
||||||
|
|
||||||
// Force a redraw if paused (prevents black frame after PiP exit)
|
// Force a redraw if paused (prevents black frame after PiP exit)
|
||||||
if playerCore?.isPaused == true {
|
if playerCore?.isPaused == true {
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: plezy
|
name: plezy
|
||||||
description: "A beautiful Plex client for Flutter"
|
description: "A beautiful Plex client for Flutter"
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 2.0.0+82
|
version: 2.0.0+84
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.10.7 <4.0.0"
|
sdk: ">=3.10.7 <4.0.0"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AVFoundation
|
||||||
import Foundation
|
import Foundation
|
||||||
import Libmpv
|
import Libmpv
|
||||||
import QuartzCore
|
import QuartzCore
|
||||||
@@ -13,49 +14,7 @@ protocol MpvPlayerDelegate: AnyObject {
|
|||||||
func onEvent(name: String, data: [String: Any]?)
|
func onEvent(name: String, data: [String: Any]?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for MoltenVK problems that cause flicker.
|
class MpvVideoLayer: AVSampleBufferDisplayLayer {}
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if os(iOS)
|
|
||||||
// wantsExtendedDynamicRangeContent is unavailable on tvOS as of SDK 26.4,
|
|
||||||
// so this override only applies to iOS / macOS.
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
override var wantsExtendedDynamicRangeContent: Bool {
|
|
||||||
get { super.wantsExtendedDynamicRangeContent }
|
|
||||||
set {
|
|
||||||
if Thread.isMainThread {
|
|
||||||
super.wantsExtendedDynamicRangeContent = newValue
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.sync {
|
|
||||||
super.wantsExtendedDynamicRangeContent = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#elseif os(macOS)
|
|
||||||
override var wantsExtendedDynamicRangeContent: Bool {
|
|
||||||
get { super.wantsExtendedDynamicRangeContent }
|
|
||||||
set {
|
|
||||||
if Thread.isMainThread {
|
|
||||||
super.wantsExtendedDynamicRangeContent = newValue
|
|
||||||
} else {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
super.wantsExtendedDynamicRangeContent = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Safely convert a C string to Swift String with UTF-8 validation.
|
/// 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.
|
/// Falls back to Latin-1 decoding if the bytes are not valid UTF-8.
|
||||||
@@ -77,7 +36,7 @@ func safeString(_ cstr: UnsafePointer<CChar>) -> String {
|
|||||||
class MpvPlayerCoreBase: NSObject {
|
class MpvPlayerCoreBase: NSObject {
|
||||||
weak var delegate: MpvPlayerDelegate?
|
weak var delegate: MpvPlayerDelegate?
|
||||||
|
|
||||||
var metalLayer: MpvMetalLayer?
|
var videoLayer: MpvVideoLayer?
|
||||||
var mpv: OpaquePointer?
|
var mpv: OpaquePointer?
|
||||||
var isInitialized = false
|
var isInitialized = false
|
||||||
var isDisposing = false
|
var isDisposing = false
|
||||||
@@ -118,6 +77,8 @@ class MpvPlayerCoreBase: NSObject {
|
|||||||
private var cachedTimePos = 0.0
|
private var cachedTimePos = 0.0
|
||||||
private var cachedWidth = 0.0
|
private var cachedWidth = 0.0
|
||||||
private var cachedHeight = 0.0
|
private var cachedHeight = 0.0
|
||||||
|
private var currentPanscan = 0.0
|
||||||
|
private var aspectOverrideActive = false
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
super.init()
|
super.init()
|
||||||
@@ -129,7 +90,7 @@ class MpvPlayerCoreBase: NSObject {
|
|||||||
func updateEDRMode(sigPeak: Double) {}
|
func updateEDRMode(sigPeak: Double) {}
|
||||||
|
|
||||||
func setupMpv() -> Bool {
|
func setupMpv() -> Bool {
|
||||||
guard let metalLayer else { return false }
|
guard let videoLayer else { return false }
|
||||||
|
|
||||||
mpv = mpv_create()
|
mpv = mpv_create()
|
||||||
guard let mpv else {
|
guard let mpv else {
|
||||||
@@ -143,7 +104,7 @@ class MpvPlayerCoreBase: NSObject {
|
|||||||
checkError(mpv_request_log_messages(mpv, "warn"))
|
checkError(mpv_request_log_messages(mpv, "warn"))
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var layer = Int64(Int(bitPattern: Unmanaged.passUnretained(metalLayer).toOpaque()))
|
var layer = Int64(Int(bitPattern: Unmanaged.passUnretained(videoLayer).toOpaque()))
|
||||||
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer))
|
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &layer))
|
||||||
applySharedMpvOptions()
|
applySharedMpvOptions()
|
||||||
configurePlatformMpvOptions()
|
configurePlatformMpvOptions()
|
||||||
@@ -186,6 +147,21 @@ class MpvPlayerCoreBase: NSObject {
|
|||||||
value: String,
|
value: String,
|
||||||
completion: @escaping (Result<Void, Error>) -> Void
|
completion: @escaping (Result<Void, Error>) -> Void
|
||||||
) {
|
) {
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
if name == "hwdec" {
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if isAvFoundationManagedProperty(name) {
|
||||||
|
print("[MpvPlayerCore] Ignoring managed AVFoundation property: \(name)=\(value)")
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVideoGravityIfNeeded(name: name, value: value)
|
||||||
|
|
||||||
if name == "pause" {
|
if name == "pause" {
|
||||||
setCachedPaused(value == "yes" || value == "true" || value == "1")
|
setCachedPaused(value == "yes" || value == "true" || value == "1")
|
||||||
}
|
}
|
||||||
@@ -365,22 +341,49 @@ class MpvPlayerCoreBase: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyGpuNextOptions() {
|
|
||||||
guard mpv != nil else { return }
|
|
||||||
setProperty("gpu-api", value: "vulkan")
|
|
||||||
setProperty("gpu-context", value: "moltenvk")
|
|
||||||
setProperty("vo", value: "gpu-next")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func applySharedMpvOptions() {
|
private func applySharedMpvOptions() {
|
||||||
guard let mpv else { return }
|
guard let mpv else { return }
|
||||||
checkError(mpv_set_option_string(mpv, "vo", "gpu-next"))
|
checkError(mpv_set_option_string(mpv, "vo", "avfoundation"))
|
||||||
checkError(mpv_set_option_string(mpv, "gpu-api", "vulkan"))
|
#if targetEnvironment(simulator)
|
||||||
checkError(mpv_set_option_string(mpv, "gpu-context", "moltenvk"))
|
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"))
|
checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox"))
|
||||||
|
#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"))
|
checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func isAvFoundationManagedProperty(_ name: String) -> Bool {
|
||||||
|
name == "vo" || name == "wid" || name == "gpu-api" || name == "gpu-context"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateVideoGravityIfNeeded(name: String, value: String) {
|
||||||
|
switch name {
|
||||||
|
case "panscan":
|
||||||
|
currentPanscan = Double(value) ?? 0
|
||||||
|
case "video-aspect-override":
|
||||||
|
aspectOverrideActive = value != "no" && value != "-1" && value != "0"
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let gravity: AVLayerVideoGravity
|
||||||
|
if aspectOverrideActive {
|
||||||
|
gravity = .resize
|
||||||
|
} else if currentPanscan > 0 {
|
||||||
|
gravity = .resizeAspectFill
|
||||||
|
} else {
|
||||||
|
gravity = .resizeAspect
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.videoLayer?.videoGravity = gravity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func cancelPendingRequests() {
|
private func cancelPendingRequests() {
|
||||||
pendingRequestsLock.lock()
|
pendingRequestsLock.lock()
|
||||||
let pending = pendingRequests
|
let pending = pendingRequests
|
||||||
|
|||||||
@@ -711,8 +711,8 @@
|
|||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/edde746/MPVKit";
|
repositoryURL = "https://github.com/edde746/MPVKit";
|
||||||
requirement = {
|
requirement = {
|
||||||
branch = main;
|
kind = revision;
|
||||||
kind = branch;
|
revision = 8aa5e1abb68fffe1d29511da9e02feca575c9311;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311"
|
||||||
"revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/edde746/MPVKit",
|
"location" : "https://github.com/edde746/MPVKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"revision" : "8aa5e1abb68fffe1d29511da9e02feca575c9311"
|
||||||
"revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -42,16 +42,18 @@ end
|
|||||||
|
|
||||||
# Swift Package: MPVKit.
|
# Swift Package: MPVKit.
|
||||||
pkg_url = 'https://github.com/edde746/MPVKit'
|
pkg_url = 'https://github.com/edde746/MPVKit'
|
||||||
|
pkg_revision = '8aa5e1abb68fffe1d29511da9e02feca575c9311'
|
||||||
existing_pkg = project.root_object.package_references.find do |p|
|
existing_pkg = project.root_object.package_references.find do |p|
|
||||||
p.repositoryURL == pkg_url rescue false
|
p.repositoryURL == pkg_url rescue false
|
||||||
end
|
end
|
||||||
|
|
||||||
if existing_pkg
|
if existing_pkg
|
||||||
puts "[skip] MPVKit SPM package already present"
|
existing_pkg.requirement = { 'kind' => 'revision', 'revision' => pkg_revision }
|
||||||
|
puts "[set ] MPVKit SPM package revision"
|
||||||
else
|
else
|
||||||
pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
|
pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
|
||||||
pkg.repositoryURL = pkg_url
|
pkg.repositoryURL = pkg_url
|
||||||
pkg.requirement = { 'kind' => 'branch', 'branch' => 'main' }
|
pkg.requirement = { 'kind' => 'revision', 'revision' => pkg_revision }
|
||||||
project.root_object.package_references << pkg
|
project.root_object.package_references << pkg
|
||||||
|
|
||||||
product = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
|
product = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
|
||||||
|
|||||||
Reference in New Issue
Block a user