import AVKit import AVFoundation protocol PiPControllerDelegate: AnyObject { func pipController(_ controller: PiPController, willStartPictureInPicture: Bool) func pipController(_ controller: PiPController, didStartPictureInPicture: Bool) func pipController(_ controller: PiPController, willStopPictureInPicture: Bool) func pipController(_ controller: PiPController, didStopPictureInPicture: Bool) func pipController(_ controller: PiPController, restoreUserInterfaceForPictureInPictureStop completionHandler: @escaping (Bool) -> Void) func pipControllerPlay(_ controller: PiPController) func pipControllerPause(_ controller: PiPController) func pipController(_ controller: PiPController, skipByInterval interval: CMTime) func pipControllerIsPlaying(_ controller: PiPController) -> Bool func pipControllerDuration(_ controller: PiPController) -> Double } final class PiPController: NSObject { private var pipController: AVPictureInPictureController? private weak var sampleBufferDisplayLayer: AVSampleBufferDisplayLayer? weak var delegate: PiPControllerDelegate? var isPictureInPictureSupported: Bool { return AVPictureInPictureController.isPictureInPictureSupported() } var isPictureInPictureActive: Bool { return pipController?.isPictureInPictureActive ?? false } var isPictureInPicturePossible: Bool { return pipController?.isPictureInPicturePossible ?? false } init(sampleBufferDisplayLayer: AVSampleBufferDisplayLayer) { self.sampleBufferDisplayLayer = sampleBufferDisplayLayer super.init() setupPictureInPicture() } private func setupPictureInPicture() { guard isPictureInPictureSupported, let displayLayer = sampleBufferDisplayLayer else { return } let contentSource = AVPictureInPictureController.ContentSource( sampleBufferDisplayLayer: displayLayer, playbackDelegate: self ) pipController = AVPictureInPictureController(contentSource: contentSource) pipController?.delegate = self pipController?.requiresLinearPlayback = false #if !os(tvOS) pipController?.canStartPictureInPictureAutomaticallyFromInline = true #endif } func startPictureInPicture() { guard let pipController = pipController, pipController.isPictureInPicturePossible else { return } pipController.startPictureInPicture() } func stopPictureInPicture() { pipController?.stopPictureInPicture() } func invalidate() { if Thread.isMainThread { pipController?.invalidatePlaybackState() } else { DispatchQueue.main.async { [weak self] in self?.pipController?.invalidatePlaybackState() } } } func updatePlaybackState() { if Thread.isMainThread { pipController?.invalidatePlaybackState() } else { DispatchQueue.main.async { [weak self] in self?.pipController?.invalidatePlaybackState() } } } } // MARK: - AVPictureInPictureControllerDelegate extension PiPController: AVPictureInPictureControllerDelegate { func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { delegate?.pipController(self, willStartPictureInPicture: true) } func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { delegate?.pipController(self, didStartPictureInPicture: true) } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) { Logger.shared.log("Failed to start PiP: \(error.localizedDescription)", type: "Error") delegate?.pipController(self, didStartPictureInPicture: false) } func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { delegate?.pipController(self, willStopPictureInPicture: true) } func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { delegate?.pipController(self, didStopPictureInPicture: true) } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { delegate?.pipController(self, restoreUserInterfaceForPictureInPictureStop: completionHandler) } } // MARK: - AVPictureInPictureSampleBufferPlaybackDelegate extension PiPController: AVPictureInPictureSampleBufferPlaybackDelegate { func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool) { if playing { delegate?.pipControllerPlay(self) } else { delegate?.pipControllerPause(self) } } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, didTransitionToRenderSize newRenderSize: CMVideoDimensions) { } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, skipByInterval skipInterval: CMTime, completion completionHandler: @escaping () -> Void) { delegate?.pipController(self, skipByInterval: skipInterval) completionHandler() } var isPlaying: Bool { return delegate?.pipControllerIsPlaying(self) ?? false } var timeRangeForPlayback: CMTimeRange { let duration = delegate?.pipControllerDuration(self) ?? 0 if duration > 0 { let cmDuration = CMTime(seconds: duration, preferredTimescale: 1000) return CMTimeRange(start: .zero, duration: cmDuration) } return CMTimeRange(start: .zero, duration: .positiveInfinity) } func pictureInPictureControllerTimeRangeForPlayback(_ pictureInPictureController: AVPictureInPictureController) -> CMTimeRange { return timeRangeForPlayback } func pictureInPictureControllerIsPlaybackPaused(_ pictureInPictureController: AVPictureInPictureController) -> Bool { return !isPlaying } func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, setPlaying playing: Bool, completion: @escaping () -> Void) { if playing { delegate?.pipControllerPlay(self) } else { delegate?.pipControllerPause(self) } completion() } }