本文字数:11934字
预计阅读时间:40分钟
BackgroundModes
,然后勾选BackgroundModes
中的Audio, Airplay, and Picture in Picture
。AVAudioSession
,在AppDelegate.Swift
中application(_:didFinishLaunchingWithOptions:)
方法设置如下代码:AVFoundation
AVAudioSession
支持后台播放
// 导入AVFoundation
import AVFoundation
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 添加设置代码
do {
// 设置AVAudioSession.Category.playback后,在静音模式下,或者APP进入后台,或者锁定屏幕后还可以继续播放。
try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: AVAudioSession.Mode.moviePlayback)
} catch {
print(error)
}
return true
}
AVPlayerViewController
来实现播放器画中画,首先导入AVKit
,获取要播放的资源,然后使用AVPlayerViewController
来进行播放,代码如下:import AVKit
/// 获取播放的资源
fileprivate func playerResource() -> AVQueuePlayer? {
guard let videoURL = Bundle.main.url(forResource: "suancaidegang", withExtension: "mp4") else {
return nil
}
let item = AVPlayerItem(url: videoURL)
let player = AVQueuePlayer(playerItem: item)
player.actionAtItemEnd = .pause
return player
}
@IBAction func systemPlayerAction(_ sender: Any) {
guard let player = playerResource() else {
return
}
let avPlayerVC = AVPlayerViewController()
avPlayerVC.player = player
present(avPlayerVC, animated: true) {
player.play()
}
}
AVPlayerViewController
直接支持了画中画的播放;点击进入画中画后,之前全屏的播放界面自动关掉;点击画中画返回播放界面后,画中画关闭,但是之前的播放界面也没有重新打开,效果如下:而这里很明显,画中画返回不了之前的播放界面是有问题的,所以要修改一下,加入可以设置再进入画中画时全屏的播放界面不关闭,点击画中画的返回是否可以正常呢?这里AVPlayerViewControllerDelegate
的方法 playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool
可以控制进入画中画时是否关闭当前界面。// 在此方法中添加avPlayerVC.delegate = self
@IBAction func systemPlayerAction(_ sender: Any) {
guard let player = playerResource() else {
return
}
let avPlayerVC = AVPlayerViewController()
avPlayerVC.delegate = self
avPlayerVC.player = player
present(avPlayerVC, animated: true) {
player.play()
}
}
// 设置AVPlayerViewControllerDelegate
extension ViewController: AVPlayerViewControllerDelegate {
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool {
// 返回false时,进入画中画后播放界面不关闭
// 返回true时,进入画中画后播放界面自动关闭,默认为true
return false
}
}
This video is playing in picture in picture
,且没有关闭按钮;画中画返回时,播放界面可以继续接着播放。效果如下:AVPlayerViewControllerDelegate
中有另外一个方法playerViewController(_ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void)
,画中画点击返回时会触发这个方法,所以要做的内容是,在这个方法被触发时,重新唤起播放视频界面,代码如下:
extension ViewController: AVPlayerViewControllerDelegate {
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool {
// 这里修改为返回true,即进入画中画时关闭播放界面
return true
}
func playerViewController(_ playerViewController: AVPlayerViewController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
restore(playerVC: playerViewController, completionHandler: completionHandler)
}
}
fileprivate func restore(playerVC: UIViewController, completionHandler: @escaping (Bool) -> Void) {
if let presentedVC = presentedViewController {
// 说明当前正在播放的界面还存在
// 先关闭界面,再弹出播放界面
presentedVC.dismiss(animated: false) { [weak self] in
self?.present(playerVC, animated: false) {
completionHandler(true)
}
}
} else {
// 直接弹出播放界面
present(playerVC, animated: false) {
completionHandler(true)
}
}
}
AVPlayerViewController
,需要在自定义播放器界面实现点击唤起画中画播放,并且实现画中画的代理方法AVPictureInPictureControllerDelegate
,在画中画的代理方法中,处理画中画返回时的逻辑。需要着重注意的是,如果设置进入画中画后播放界面消失,则当前的播放界面会被释放掉,会导致播放界面上的画中画播放也会消失,所以需要特殊处理下,声明一个全局的Picture in Picture Across All Platforms
protocol CustomPlayerVCDelegate: AnyObject {
func playerViewController(
_ playerViewController: MWCustomPlayerVC,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler
completionHandler: @escaping (Bool) -> Void
)
}
private var activeCustomPlayerVCs = Set<MWCustomPlayerVC>()
class MWCustomPlayerVC: UIViewController {
// MARK: - properties
private var pictureInPictureVC: AVPictureInPictureController?
weak var delegate: CustomPlayerVCDelegate?
var autoDismissAtPip: Bool = false // 进入画中画时,是否自动关闭当前播放页面
var enterPipBtn: CustomPlayerCircularButtonView?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = .black
setupPictureInPictureVC()
setupEnterPipBtn()
}
fileprivate func setupPictureInPictureVC() {
guard let playerLayer = playerLayer else {
return
}
pictureInPictureVC = AVPictureInPictureController(playerLayer: playerLayer)
pictureInPictureVC?.delegate = self
}
fileprivate func setupEnterPipBtn() {
enterPipBtn = CustomPlayerCircularButtonView(symbolName: "pip.enter", height: 50.0)
enterPipBtn?.addTarget(self, action: #selector(handleEnterPipAction), for: [.primaryActionTriggered, .touchUpInside])
view.addSubview(enterPipBtn!)
enterPipBtn?.snp.makeConstraints { make in
make.right.equalToSuperview().inset(10.0)
make.centerY.equalTo(self.view.snp.centerY)
make.width.height.equalTo(50.0)
}
}
// 点击唤起画中画界面
@objc
fileprivate func handleEnterPipAction() {
pictureInPictureVC?.startPictureInPicture()
}
}
extension MWCustomPlayerVC: AVPictureInPictureControllerDelegate {
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
// 进入画中画播放的代理方法
activeCustomPlayerVCs.insert(self)
enterPipBtn?.isHidden = true
}
// 画中画开始播放后,当前播放界面是否消失
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
if autoDismissAtPip {
dismiss(animated: true)
}
}
// 画中画进入失败
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
activeCustomPlayerVCs.remove(self)
enterPipBtn?.isHidden = false
}
// 画中画返回的代理方法
func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void) {
delegate?.playerViewController(self, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
}
}
@IBAction func customPlayerAction(_ sender: Any) {
guard let player = playerResource() else {
return
}
let playerVC = MWCustomPlayerVC()
playerVC.modalPresentationStyle = .fullScreen
playerVC.delegate = self
playerVC.player = player
playerVC.autoDismissAtPip = true
present(playerVC, animated: true) {
player.play()
}
}
extension ViewController: CustomPlayerVCDelegate {
func playerViewController(
_ playerViewController: MWCustomPlayerVC,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler
completionHandler: @escaping (Bool) -> Void) {
restore(playerVC: playerViewController, completionHandler: completionHandler)
}
}
view
view
tricky
window
window
window
view
view
view
view
UIPiPView
view
CMSampleBuffer
initWithSampleBufferDisplayLayer
AVSampleBufferDisplayLayer
AVPictureInPictureController.ContentSource
AVPictureInPictureController.ContentSource
AVPictureInPictureController
AVSampleBufferDisplayLayer
CMSampleBuffer
view
AVPictureInPictureController
view
CMSampleBuffer
CMSampleBuffer
AVPictureInPictureController
view
t
imer
timer
UIPiPView
UIPiPView
view
UIPipView
import UIKit
import UIPiPView
import SnapKit
class MWFullTimerVC: MWBaseVC {
// MARK: - properties
private let pipView = UIPiPView()
private let timeLabel = UILabel()
private let dateFormatStr = "yyyy-MM-dd HH:mm:ss"
private var timer: Timer?
// MARK: - view life cycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
view.backgroundColor = UIColor.black
setupPipView()
setupTimeLabel()
createDisplayLink()
}
fileprivate func setupPipView() {
let width = UIScreen.main.bounds.width
pipView.frame = CGRect(x: 10.0, y: 0, width: width - 20.0, height: 50.0)
view.addSubview(pipView)
pipView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview().inset(10.0)
make.top.equalToSuperview().inset(100.0)
make.height.equalTo(50.0)
}
}
fileprivate func setupTimeLabel() {
timeLabel.font = UIFont.boldSystemFont(ofSize: 16.0)
timeLabel.textColor = UIColor.white
timeLabel.backgroundColor = UIColor.orange
timeLabel.textAlignment = .center
pipView.addSubview(timeLabel)
timeLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.top.equalToSuperview()
make.height.equalToSuperview()
}
}
func createDisplayLink() {
timer = Timer(timeInterval: 0.1/60, repeats: true, block: { [weak self] _ in
self?.refresh()
})
RunLoop.current.add(timer!, forMode: RunLoop.Mode.common)
timer?.fire()
}
// MARK: - init
// MARK: - utils
func reloadTime() {
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = dateFormatStr
self.timeLabel.text = formatter.string(from: date)
}
// MARK: - action
func refresh() {
reloadTime()
}
override func handleEnterPipAction() {
super.handleEnterPipAction()
if pipView.isPictureInPictureActive() {
pipView.stopPictureInPicture()
} else {
pipView.startPictureInPicture(withRefreshInterval: 0.1/60.0)
}
}
// MARK: - other
}
window
window
window
pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController)
window
view
view
class MWPipWindowVC: UIViewController {
func setupTextPlayerView(on targetView: UIView) {
targetView.addSubview(textPlayView)
textPlayView.text = text1
textPlayView.snp.makeConstraints { make in
make.centerY.equalTo(targetView.snp.centerY)
make.leading.trailing.equalToSuperview()
make.height.equalTo(250.0)
}
}
fileprivate func setupTimer() {
timer = Timer(timeInterval: 2.0, repeats: true, block: { [weak self] _ in
self?.handleTimerAction()
})
RunLoop.current.add(timer!, forMode: RunLoop.Mode.common)
timer?.fire()
}
// MARK: - utils
// MARK: - action
func handleTimerAction() {
let dataList = [text1, text2, text3, text4, text5]
count += 1
let index = count % 5
let str = dataList[index]
textPlayView.text = str
}
}
extension MWPipWindowVC: AVPictureInPictureControllerDelegate {
func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
activeCustomPlayerVCs.insert(self)
enterPipBtn?.isHidden = true
if let window = UIApplication.shared.windows.first {
setupTextPlayerView(on: window)
}
}
xxx
}
view
window
view
view
window
view
view
window
view
window
view