chore: add native formatting checks
This commit is contained in:
@@ -2,11 +2,11 @@ import AVKit
|
||||
import UIKit
|
||||
|
||||
#if os(tvOS)
|
||||
// tvOS stub: AVPictureInPictureController has different constraints on tvOS
|
||||
// and is not supported by the Plezy flow. Provide a no-op shell so callers
|
||||
// in MpvPlayerPlugin compile unchanged; isSupported reports false so PiP is
|
||||
// never attempted at runtime.
|
||||
protocol MpvPipDelegate: AnyObject {
|
||||
// tvOS stub: AVPictureInPictureController has different constraints on tvOS
|
||||
// and is not supported by the Plezy flow. Provide a no-op shell so callers
|
||||
// in MpvPlayerPlugin compile unchanged; isSupported reports false so PiP is
|
||||
// never attempted at runtime.
|
||||
protocol MpvPipDelegate: AnyObject {
|
||||
func pipWillStart()
|
||||
func pipDidStart()
|
||||
func pipDidStop(restored: Bool)
|
||||
@@ -15,35 +15,35 @@ protocol MpvPipDelegate: AnyObject {
|
||||
func pipSkip(byInterval seconds: Double)
|
||||
var isPipPlaying: Bool { get }
|
||||
var pipDuration: Double { get }
|
||||
}
|
||||
}
|
||||
|
||||
class MpvPipController: NSObject {
|
||||
class MpvPipController: NSObject {
|
||||
static var isSupported: Bool { false }
|
||||
weak var delegate: MpvPipDelegate?
|
||||
var isPipActive: Bool { false }
|
||||
var autoStartEnabled: Bool { false }
|
||||
var layerPointer: UnsafeMutableRawPointer {
|
||||
// Return a dummy non-null pointer — layerPointer is handed to mpv for
|
||||
// rendering into PiP, which never activates on tvOS.
|
||||
UnsafeMutableRawPointer(bitPattern: 0x1)!
|
||||
// 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 setAutoStart(_ enabled: Bool) {}
|
||||
func warmLayer(currentTime: Double, isPlaying: Bool) {}
|
||||
func pushBlankFrame(width: Int32 = 1920, height: Int32 = 1080) {}
|
||||
func startPip(waitForFrame: Bool = true, completion: @escaping (Bool) -> Void) {
|
||||
completion(false)
|
||||
completion(false)
|
||||
}
|
||||
func stopPip() {}
|
||||
func invalidatePlaybackState() {}
|
||||
func flushLayer() {}
|
||||
func syncTimebase(currentTime: Double, isPlaying: Bool) {}
|
||||
func teardown() {}
|
||||
}
|
||||
}
|
||||
#else
|
||||
|
||||
/// Delegate to notify the plugin of PiP lifecycle events
|
||||
protocol MpvPipDelegate: AnyObject {
|
||||
/// Delegate to notify the plugin of PiP lifecycle events
|
||||
protocol MpvPipDelegate: AnyObject {
|
||||
/// Called when PiP is about to start (system or app-initiated)
|
||||
func pipWillStart()
|
||||
func pipDidStart()
|
||||
@@ -58,11 +58,11 @@ protocol MpvPipDelegate: AnyObject {
|
||||
var isPipPlaying: Bool { get }
|
||||
/// Get total duration in seconds
|
||||
var pipDuration: Double { get }
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates all iOS Picture-in-Picture logic using AVSampleBufferDisplayLayer.
|
||||
/// Requires iOS 15+ for the ContentSource API; on older versions, `setup()` is a no-op.
|
||||
class MpvPipController: NSObject {
|
||||
/// Encapsulates all iOS Picture-in-Picture logic using AVSampleBufferDisplayLayer.
|
||||
/// Requires iOS 15+ for the ContentSource API; on older versions, `setup()` is a no-op.
|
||||
class MpvPipController: NSObject {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@@ -73,340 +73,346 @@ class MpvPipController: NSObject {
|
||||
|
||||
/// Pointer to the sample buffer layer for passing to mpv as `wid`
|
||||
var layerPointer: UnsafeMutableRawPointer {
|
||||
Unmanaged.passUnretained(sampleBufferLayer).toOpaque()
|
||||
Unmanaged.passUnretained(sampleBufferLayer).toOpaque()
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
setup()
|
||||
super.init()
|
||||
setup()
|
||||
}
|
||||
|
||||
private func setup() {
|
||||
guard #available(iOS 15.0, *) else { return }
|
||||
guard #available(iOS 15.0, *) else { return }
|
||||
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
} catch {
|
||||
print("[MpvPipController] Failed to configure audio session: \(error)")
|
||||
}
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||
try AVAudioSession.sharedInstance().setActive(true)
|
||||
} catch {
|
||||
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 windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first(where: { $0.isKeyWindow }) {
|
||||
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
|
||||
}
|
||||
// The sample buffer layer must be in a visible view hierarchy for
|
||||
// isPictureInPicturePossible to become true.
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first(where: { $0.isKeyWindow })
|
||||
{
|
||||
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()
|
||||
}
|
||||
|
||||
/// Helper that conforms to the iOS 15+ delegate protocols
|
||||
private var delegateHelper: AnyObject?
|
||||
|
||||
private func createPipController() {
|
||||
guard #available(iOS 15.0, *) else { return }
|
||||
let helper = PipDelegateHelper(controller: self)
|
||||
let contentSource = AVPictureInPictureController.ContentSource(
|
||||
sampleBufferDisplayLayer: sampleBufferLayer,
|
||||
playbackDelegate: helper
|
||||
)
|
||||
self.delegateHelper = helper
|
||||
pipController = AVPictureInPictureController(contentSource: contentSource)
|
||||
pipController?.delegate = helper
|
||||
if #available(iOS 14.2, *) {
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
}
|
||||
guard #available(iOS 15.0, *) else { return }
|
||||
let helper = PipDelegateHelper(controller: self)
|
||||
let contentSource = AVPictureInPictureController.ContentSource(
|
||||
sampleBufferDisplayLayer: sampleBufferLayer,
|
||||
playbackDelegate: helper
|
||||
)
|
||||
self.delegateHelper = helper
|
||||
pipController = AVPictureInPictureController(contentSource: contentSource)
|
||||
pipController?.delegate = helper
|
||||
if #available(iOS 14.2, *) {
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable/disable system auto-PiP (starts PiP automatically on background transition)
|
||||
func setAutoStart(_ enabled: Bool) {
|
||||
guard #available(iOS 14.2, *) else { return }
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled
|
||||
guard #available(iOS 14.2, *) else { return }
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = enabled
|
||||
}
|
||||
|
||||
/// Push a black frame to the sample buffer layer so PiP has content
|
||||
/// to display immediately (before vo_pip decodes the first real frame).
|
||||
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 }
|
||||
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, [])
|
||||
// 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)
|
||||
}
|
||||
// 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
|
||||
)
|
||||
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 }
|
||||
// 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 }
|
||||
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
|
||||
}
|
||||
// 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)
|
||||
sampleBufferLayer.enqueue(sb)
|
||||
}
|
||||
|
||||
/// Sync the layer's controlTimebase with the actual playback position.
|
||||
/// This makes the PiP progress bar show the correct time.
|
||||
func syncTimebase(currentTime: Double, isPlaying: Bool) {
|
||||
guard let timebase = sampleBufferLayer.controlTimebase else { return }
|
||||
let cmTime = CMTime(seconds: currentTime, preferredTimescale: 1000)
|
||||
CMTimebaseSetTime(timebase, time: cmTime)
|
||||
CMTimebaseSetRate(timebase, rate: isPlaying ? 1.0 : 0.0)
|
||||
guard let timebase = sampleBufferLayer.controlTimebase else { return }
|
||||
let cmTime = CMTime(seconds: currentTime, preferredTimescale: 1000)
|
||||
CMTimebaseSetTime(timebase, time: cmTime)
|
||||
CMTimebaseSetRate(timebase, rate: isPlaying ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
/// Ensure the layer has a timebase and blank frame so the system considers
|
||||
/// PiP possible (required for canStartPictureInPictureAutomaticallyFromInline).
|
||||
func warmLayer(currentTime: Double, isPlaying: Bool) {
|
||||
if sampleBufferLayer.controlTimebase == nil {
|
||||
var timebase: CMTimebase?
|
||||
CMTimebaseCreateWithSourceClock(
|
||||
allocator: kCFAllocatorDefault,
|
||||
sourceClock: CMClockGetHostTimeClock(),
|
||||
timebaseOut: &timebase
|
||||
)
|
||||
if let tb = timebase {
|
||||
sampleBufferLayer.controlTimebase = tb
|
||||
}
|
||||
if sampleBufferLayer.controlTimebase == nil {
|
||||
var timebase: CMTimebase?
|
||||
CMTimebaseCreateWithSourceClock(
|
||||
allocator: kCFAllocatorDefault,
|
||||
sourceClock: CMClockGetHostTimeClock(),
|
||||
timebaseOut: &timebase
|
||||
)
|
||||
if let tb = timebase {
|
||||
sampleBufferLayer.controlTimebase = tb
|
||||
}
|
||||
syncTimebase(currentTime: currentTime, isPlaying: isPlaying)
|
||||
pushBlankFrame()
|
||||
}
|
||||
syncTimebase(currentTime: currentTime, isPlaying: isPlaying)
|
||||
pushBlankFrame()
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
static var isSupported: Bool {
|
||||
guard #available(iOS 15.0, *) else { return false }
|
||||
return AVPictureInPictureController.isPictureInPictureSupported()
|
||||
guard #available(iOS 15.0, *) else { return false }
|
||||
return AVPictureInPictureController.isPictureInPictureSupported()
|
||||
}
|
||||
|
||||
/// Start PiP. When `waitForFrame` is false (auto-PiP), skips the frame
|
||||
/// readiness check since the scene is about to deactivate.
|
||||
func startPip(waitForFrame: Bool = true, completion: @escaping (Bool) -> Void) {
|
||||
guard let pipController = pipController else {
|
||||
completion(false)
|
||||
return
|
||||
guard let pipController = pipController else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
var attempts = 0
|
||||
func tryStart() {
|
||||
let possible = pipController.isPictureInPicturePossible
|
||||
let hasTimebase = sampleBufferLayer.controlTimebase != nil
|
||||
|
||||
let hasFrame: Bool
|
||||
if !waitForFrame {
|
||||
hasFrame = true // Skip frame check for auto-PiP
|
||||
} else if #available(iOS 17.4, *) {
|
||||
hasFrame = sampleBufferLayer.isReadyForDisplay
|
||||
} else {
|
||||
hasFrame = true
|
||||
}
|
||||
|
||||
var attempts = 0
|
||||
func tryStart() {
|
||||
let possible = pipController.isPictureInPicturePossible
|
||||
let hasTimebase = sampleBufferLayer.controlTimebase != nil
|
||||
|
||||
let hasFrame: Bool
|
||||
if !waitForFrame {
|
||||
hasFrame = true // Skip frame check for auto-PiP
|
||||
} else if #available(iOS 17.4, *) {
|
||||
hasFrame = sampleBufferLayer.isReadyForDisplay
|
||||
} else {
|
||||
hasFrame = true
|
||||
}
|
||||
|
||||
if possible && hasTimebase && hasFrame {
|
||||
print("[MpvPipController] vo_pip ready after \(attempts) retries, starting PiP")
|
||||
pipController.startPictureInPicture()
|
||||
completion(true)
|
||||
} else if attempts < 40 {
|
||||
attempts += 1
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { tryStart() }
|
||||
} else {
|
||||
print("[MpvPipController] PiP not ready after \(attempts) retries (possible=\(possible), timebase=\(hasTimebase))")
|
||||
completion(false)
|
||||
}
|
||||
if possible && hasTimebase && hasFrame {
|
||||
print("[MpvPipController] vo_pip ready after \(attempts) retries, starting PiP")
|
||||
pipController.startPictureInPicture()
|
||||
completion(true)
|
||||
} else if attempts < 40 {
|
||||
attempts += 1
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { tryStart() }
|
||||
} else {
|
||||
print(
|
||||
"[MpvPipController] PiP not ready after \(attempts) retries (possible=\(possible), timebase=\(hasTimebase))"
|
||||
)
|
||||
completion(false)
|
||||
}
|
||||
tryStart()
|
||||
}
|
||||
tryStart()
|
||||
}
|
||||
|
||||
func stopPip() {
|
||||
pipController?.stopPictureInPicture()
|
||||
pipController?.stopPictureInPicture()
|
||||
}
|
||||
|
||||
/// Invalidate the playback state so PiP updates its UI (play/pause button)
|
||||
func invalidatePlaybackState() {
|
||||
pipController?.invalidatePlaybackState()
|
||||
pipController?.invalidatePlaybackState()
|
||||
}
|
||||
|
||||
/// Fully tear down PiP — removes the container view from the window and
|
||||
/// destroys the AVPictureInPictureController so the system can no longer
|
||||
/// trigger auto-PiP after the player is disposed.
|
||||
func teardown() {
|
||||
pipController?.stopPictureInPicture()
|
||||
if #available(iOS 14.2, *) {
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
}
|
||||
pipController = nil
|
||||
delegateHelper = nil
|
||||
sampleBufferLayer.flushAndRemoveImage()
|
||||
sampleBufferLayer.controlTimebase = nil
|
||||
sampleBufferLayer.removeFromSuperlayer()
|
||||
containerView?.removeFromSuperview()
|
||||
containerView = nil
|
||||
pipController?.stopPictureInPicture()
|
||||
if #available(iOS 14.2, *) {
|
||||
pipController?.canStartPictureInPictureAutomaticallyFromInline = false
|
||||
}
|
||||
pipController = 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
|
||||
func flushLayer() {
|
||||
sampleBufferLayer.flushAndRemoveImage()
|
||||
sampleBufferLayer.flushAndRemoveImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PiP Delegate Helper (iOS 15+)
|
||||
// MARK: - PiP Delegate Helper (iOS 15+)
|
||||
|
||||
/// Separate class conforming to AVPictureInPictureControllerDelegate and
|
||||
/// AVPictureInPictureSampleBufferPlaybackDelegate since these require iOS 15+
|
||||
/// availability for the ContentSource-based delegate methods.
|
||||
@available(iOS 15.0, *)
|
||||
private class PipDelegateHelper: NSObject, AVPictureInPictureControllerDelegate,
|
||||
/// Separate class conforming to AVPictureInPictureControllerDelegate and
|
||||
/// AVPictureInPictureSampleBufferPlaybackDelegate since these require iOS 15+
|
||||
/// availability for the ContentSource-based delegate methods.
|
||||
@available(iOS 15.0, *)
|
||||
private class PipDelegateHelper: NSObject, AVPictureInPictureControllerDelegate,
|
||||
AVPictureInPictureSampleBufferPlaybackDelegate
|
||||
{
|
||||
{
|
||||
weak var controller: MpvPipController?
|
||||
private var isRestoring = false
|
||||
|
||||
init(controller: MpvPipController) {
|
||||
self.controller = controller
|
||||
super.init()
|
||||
self.controller = controller
|
||||
super.init()
|
||||
}
|
||||
|
||||
// MARK: - AVPictureInPictureControllerDelegate
|
||||
|
||||
func pictureInPictureControllerWillStartPictureInPicture(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) {
|
||||
print("[MpvPipController] PiP will start")
|
||||
controller?.delegate?.pipWillStart()
|
||||
print("[MpvPipController] PiP will start")
|
||||
controller?.delegate?.pipWillStart()
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStartPictureInPicture(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) {
|
||||
print("[MpvPipController] PiP did start")
|
||||
controller?.delegate?.pipDidStart()
|
||||
print("[MpvPipController] PiP did start")
|
||||
controller?.delegate?.pipDidStart()
|
||||
}
|
||||
|
||||
func pictureInPictureControllerDidStopPictureInPicture(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) {
|
||||
let restored = isRestoring
|
||||
isRestoring = false
|
||||
print("[MpvPipController] PiP did stop (restored: \(restored))")
|
||||
controller?.delegate?.pipDidStop(restored: restored)
|
||||
let restored = isRestoring
|
||||
isRestoring = false
|
||||
print("[MpvPipController] PiP did stop (restored: \(restored))")
|
||||
controller?.delegate?.pipDidStop(restored: restored)
|
||||
}
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
failedToStartPictureInPictureWithError error: Error
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
failedToStartPictureInPictureWithError error: Error
|
||||
) {
|
||||
print("[MpvPipController] PiP failed to start: \(error)")
|
||||
controller?.delegate?.pipDidFailToStart(error: error)
|
||||
print("[MpvPipController] PiP failed to start: \(error)")
|
||||
controller?.delegate?.pipDidFailToStart(error: error)
|
||||
}
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler:
|
||||
@escaping (Bool) -> Void
|
||||
) {
|
||||
print("[MpvPipController] PiP restore user interface")
|
||||
isRestoring = true
|
||||
completionHandler(true)
|
||||
print("[MpvPipController] PiP restore user interface")
|
||||
isRestoring = true
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
func pictureInPictureControllerWillStopPictureInPicture(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) {
|
||||
print("[MpvPipController] PiP will stop")
|
||||
print("[MpvPipController] PiP will stop")
|
||||
}
|
||||
|
||||
// MARK: - AVPictureInPictureSampleBufferPlaybackDelegate
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
setPlaying playing: Bool
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
setPlaying playing: Bool
|
||||
) {
|
||||
print("[MpvPipController] PiP setPlaying: \(playing)")
|
||||
controller?.delegate?.pipSetPlaying(playing)
|
||||
print("[MpvPipController] PiP setPlaying: \(playing)")
|
||||
controller?.delegate?.pipSetPlaying(playing)
|
||||
}
|
||||
|
||||
func pictureInPictureControllerTimeRangeForPlayback(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) -> CMTimeRange {
|
||||
let duration = controller?.delegate?.pipDuration ?? 0
|
||||
if duration > 0 {
|
||||
return CMTimeRange(
|
||||
start: .zero,
|
||||
duration: CMTime(seconds: duration, preferredTimescale: 1000)
|
||||
)
|
||||
}
|
||||
return CMTimeRange(start: .zero, duration: CMTime(seconds: 1, preferredTimescale: 1))
|
||||
let duration = controller?.delegate?.pipDuration ?? 0
|
||||
if duration > 0 {
|
||||
return CMTimeRange(
|
||||
start: .zero,
|
||||
duration: CMTime(seconds: duration, preferredTimescale: 1000)
|
||||
)
|
||||
}
|
||||
return CMTimeRange(start: .zero, duration: CMTime(seconds: 1, preferredTimescale: 1))
|
||||
}
|
||||
|
||||
func pictureInPictureControllerIsPlaybackPaused(
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
_ pictureInPictureController: AVPictureInPictureController
|
||||
) -> Bool {
|
||||
return !(controller?.delegate?.isPipPlaying ?? false)
|
||||
return !(controller?.delegate?.isPipPlaying ?? false)
|
||||
}
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
didTransitionToRenderSize newRenderSize: CMVideoDimensions
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
didTransitionToRenderSize newRenderSize: CMVideoDimensions
|
||||
) {}
|
||||
|
||||
func pictureInPictureController(
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
skipByInterval skipInterval: CMTime,
|
||||
completion completionHandler: @escaping () -> Void
|
||||
_ pictureInPictureController: AVPictureInPictureController,
|
||||
skipByInterval skipInterval: CMTime,
|
||||
completion completionHandler: @escaping () -> Void
|
||||
) {
|
||||
let seconds = CMTimeGetSeconds(skipInterval)
|
||||
print("[MpvPipController] PiP skip by \(seconds)s")
|
||||
controller?.delegate?.pipSkip(byInterval: seconds)
|
||||
completionHandler()
|
||||
let seconds = CMTimeGetSeconds(skipInterval)
|
||||
print("[MpvPipController] PiP skip by \(seconds)s")
|
||||
controller?.delegate?.pipSkip(byInterval: seconds)
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // !os(tvOS)
|
||||
|
||||
Reference in New Issue
Block a user