引言
在开发视频播放功能时,除了实现基本的播放、暂停、进度控制等功能外,如何处理视频播放完成事件也是至关重要的一环。毕竟,当用户观看完一个视频时,我们可能希望执行一些特定的操作,比如自动播放下一个视频、显示相关推荐内容,或者简单地提示用户视频已结束。
本篇博客将深入探讨如何利用AVFoundation框架实现视频播放功能,并重点关注如何处理播放完成事件,我们将会介绍三种处理视频播放完成的方式:留在播放页面、返回播放列表、自动播放下一个视频。
无论您是新手开发者还是有经验的老手,我相信本文都能为您提供有价值的内容,让您在开发视频播放功能时游刃有余。
实现
想要实现以上介绍的三种视频播放结束后的处理,首先我们需要知道视频什么时候播放结束。AVPlayer或者AVPlayerItem都没有提供播放结束的回调,不过当视频播放完成的时候AVPlayerItem会发送一个AVPlayerItem.didPlayToEndTimeNotification通知,我们可以通过监听该通知的方式来获取播放结束的回调。
创建播放列表
目前我们的项目打开后会直接进入播放页面,在处理播放结束之前,我们先来改造一下工程,创建一个播放列表,点击列表的item进入播放页面。
将viewController改为播放列表:
import UIKit
import AVFoundation
class PHItemCell: UITableViewCell {
// 标题
let itemLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(itemLabel)
itemLabel.textColor = .white
itemLabel.backgroundColor = UIColor.black
itemLabel.textAlignment = .center
itemLabel.font = UIFont.boldSystemFont(ofSize: 20)
itemLabel.layer.cornerRadius = 10.0
itemLabel.layer.masksToBounds = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let width = self.bounds.size.width - 60.0
let height = width * 2.0 / 3.0
itemLabel.frame = CGRect(x: 30.0, y: 0.0, width: self.bounds.size.width - 60.0, height: height)
}
}
class ViewController: UIViewController,UITableViewDelegate,UITableViewDataSource {
/// 列表
let tableView = UITableView(frame: CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height), style: .plain)
/// 数据
let dataArray = ["hubblecast","hubblecast","hubblecast"]
override func viewDidLoad() {
super.viewDidLoad()
let width = self.view.bounds.size.width - 60.0
let height = width * 2.0 / 3.0
self.view.addSubview(tableView)
tableView.delegate = self
tableView.dataSource = self
tableView.rowHeight = height + 10
tableView.register(PHItemCell.self, forCellReuseIdentifier: "cell")
tableView.showsVerticalScrollIndicator = false
}
//MARK: tableviewDelegate
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! PHItemCell
cell.selectionStyle = .none
cell.itemLabel.text = String(format: "视频-%ld", indexPath.row + 1)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let playerViewController = PHPlayerViewController()
playerViewController.modalPresentationStyle = .fullScreen
playerViewController.resource = dataArray[indexPath.row]
self.present(playerViewController, animated: true)
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override var shouldAutorotate: Bool {
return true
}
}
创建PHPlayerViewController播放页面:
import UIKit
private var playerContext = 0
class PHPlayerViewController: UIViewController {
/// 播放控制器
var playerController:PHPlayerController?
/// 资源名称
var resource:String = ""
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .black
guard let url = Bundle.main.url(forResource: resource, withExtension: "m4v") else { return }
playerController = PHPlayerController(url: url)
guard let playerView = playerController?.view else { return }
playerView.backgroundColor = .black
playerView.frame = view.bounds
view.addSubview(playerView)
playerController?.gobackBlock = {[weak self] in
guard let self = self else { return }
self.goBack()
}
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscape
}
override var shouldAutorotate: Bool {
return true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard let playerView = playerController?.view else { return }
playerView.backgroundColor = .black
playerView.frame = view.bounds
}
func goBack() {
self.dismiss(animated: true, completion: nil)
}
deinit {
print("PHPlayerViewContoller-deinit")
}
}
注意这两个控制都重写了supportedInterfaceOrientations,和shouldAutorotate方法来实现屏幕自动横屏竖屏。
监听播放结束
UI部分处理完了接下来我们就来实现播放结束的建提高。将PHPlayerController的对象注册为AVPlayerItem.didPlayToEndTimeNotification通知的监听者。
/// 监听播放结束
func addItemEndObserverForPlayerItem() {
guard let playerItem = playerItem else { return }
let name = AVPlayerItem.didPlayToEndTimeNotification
let queue = OperationQueue.main
itemEndObserver = NotificationCenter.default.addObserver(forName: name, object: playerItem, queue: queue, using: {[weak self] notification in
guard let self = self else { return }
})
}
播放控制器销毁时移除通知。
deinit {
//移除播放结束 监听
if let itemEndObserver = itemEndObserver {
let name = AVPlayerItem.didPlayToEndTimeNotification
NotificationCenter.default.removeObserver(itemEndObserver, name: name, object: self.player?.currentItem)
}
}
在视频准备好播放后添加监听。
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &playerItemContext {
guard let playerItem = playerItem else { return }
guard let player = player else { return }
if playerItem.status == .readyToPlay{
playerItem.removeObserver(self, forKeyPath: status_keypath)
player.play()
let duration = playerItem.duration
// 同步页面开始播放
self.delegate?.playstart()
// 同步时间
self.delegate?.setCuttentTime(time: 0.0, duration: CMTimeGetSeconds(duration))
// 设置标题
let assetTitle = assertTitle()
self.delegate?.setTitle(title: assetTitle)
// 设置字幕
let subtitles = loadMediaOptions()
self.delegate?.setSubtitle(titles: subtitles)
// 监听播放进度
addPlayerItemTimeObserver()
// 监听播放完成
addItemEndObserverForPlayerItem()
}
} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
方式一:返回播放列表
第一种方式也是最简单的实现方式,视频播放完成之后直接返回播放列表。
在PHPlayerController中声明一个返回功能的回调,返回按钮的点击也来触发这个回调。
class PHPlayerController: NSObject, PHPlayerDelegate {
/// 资源
private var asset:AVAsset?
....
/// 返回点击回调
var gobackBlock:(()->Void)?
....
}
监听到播放完成之后调用调用该闭包。
/// 监听播放结束
func addItemEndObserverForPlayerItem() {
guard let playerItem = playerItem else { return }
let name = AVPlayerItem.didPlayToEndTimeNotification
let queue = OperationQueue.main
itemEndObserver = NotificationCenter.default.addObserver(forName: name, object: playerItem, queue: queue, using: {[weak self] notification in
guard let self = self else { return }
guard let gobackBlock = self.gobackBlock else { return }
gobackBlock()
})
}
在播放页面实现该闭包,直接返回列表页面。在控制台可以注意到PHPlayerController以及PHPlayerViewController的deinit方法都触发了。
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .black
guard let url = Bundle.main.url(forResource: resource, withExtension: "m4v") else { return }
playerController = PHPlayerController(url: url)
guard let playerView = playerController?.view else { return }
playerView.backgroundColor = .black
playerView.frame = view.bounds
view.addSubview(playerView)
playerController?.gobackBlock = {[weak self] in
guard let self = self else { return }
self.goBack()
}
}
func goBack() {
self.dismiss(animated: true, completion: nil)
}
deinit {
print("PHPlayerViewContoller-deinit")
}
方式二:保留在播放页面,重置播放状态
这种实现方式和第一种大同小异,不同地方在于我们需要处理播放器的状态,而第一种方式什么都不需要处理,直接返回列表页面,播放页和播放器会自动销毁。
处理播放器状态一共有两个方面,第1方面设置播放进度从0.0开始,第2方面设置控制页面的播放按钮状态为正在暂停状态,进度条进度设置为0.0。
/// 监听播放结束
func addItemEndObserverForPlayerItem() {
guard let playerItem = playerItem else { return }
let name = AVPlayerItem.didPlayToEndTimeNotification
let queue = OperationQueue.main
itemEndObserver = NotificationCenter.default.addObserver(forName: name, object: playerItem, queue: queue, using: {[weak self] notification in
guard let self = self else { return }
self.endPlay2()
})
}
/// 方法2:保留播放页,重置播放状态
func endPlay2() {
guard let delegate = self.delegate else { return }
guard let player = player else { return }
player.seek(to: CMTimeMakeWithSeconds(0.0, preferredTimescale: Int32(NSEC_PER_SEC)))
delegate.playbackComplete()
}
PHControlView中的playbackComplete实现。
/// 播放完成
func playbackComplete() {
self.sliderView.value = 0.0
self.playButton.isHidden = true
}
方式三:自动播放下一个媒体资源
这种方式相较上面两种处理方式要复杂一些,这一方式的主要的几个操作是播放完成后,移除时间监听、移除播放结束监听、修改播放器状态、自动加载新的媒体资源。下面来看一下具体实现。
修改播放器初始化方法,传入资源数组。
import UIKit
import AVFoundation
let status_keypath = "status"
var playerItemContext = 0
class PHPlayerController: NSObject, PHPlayerDelegate {
....
/// url资源列表
var urls:[URL]?
/// index
var index = 0
/// 重写init方法
///
/// - Parameters:
/// - urls: 资源URL列表
init(urls:[URL]) {
super.init()
self.urls = urls
prepareToPlay()
}
准备播放的方法稍微做一点调整,直接调用加载AVPlayerItem的方法。
/// 准备播放
private func prepareToPlay() {
guard let urls = urls else { return }
let url = urls[index]
loadPlayItem(url: url)
guard let playerItem = playerItem else { return }
player = AVPlayer(playerItem: playerItem)
guard let player = player else { return }
playerView = PHPlayerView(player: player)
self.delegate = playerView?.controlView
self.delegate?.delegate = self
}
将加载AVPlayerItem的方法提取出来,因为在播放下一个视频资源的时候也需要进行AVPlayerItem的加载。
func loadPlayItem(url:URL) {
asset = AVAsset(url: url)
let keys = ["tracks","duration","commonMetadata","availableMediaCharacteristicsWithMediaSelectionOptions"]
guard let asset = asset else { return }
playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: keys)
guard let playerItem = playerItem else { return }
playerItem.addObserver(self, forKeyPath: status_keypath, context: &playerItemContext)
}
加载下一个媒体资源。在加载新的媒体资源之前需要将原来的进度监听和播放结束监听进行移除,因为我们接下来需要监听的是新的AVPlayerItem。调用AVPlayer的replaceCurrentItem方法。
/// 播放下一个资源
private func playNextItem() {
//移除播放进度监听
removePlayerItemTimeObserver()
//移除播放结束监听
removeItemEndObserverForPlayerItem()
index = index + 1
guard let count = urls?.count,count > index else {
print("没有更多视频资源")
return
}
guard let urls = urls else { return }
let url = urls[index]
loadPlayItem(url: url)
player?.replaceCurrentItem(with: playerItem)
}
自动播放下一个媒体资源的方法实现。
/// 方法3: 自动播放下一个资源
func endPlay3() {
guard let delegate = self.delegate else { return }
guard let player = player else { return }
player.seek(to: CMTimeMakeWithSeconds(0.0, preferredTimescale: Int32(NSEC_PER_SEC)))
delegate.playbackComplete()
playNextItem()
}
视频播放完成之后调用自动播放下一个媒体资源的方法。
/// 监听播放结束
func addItemEndObserverForPlayerItem() {
guard let playerItem = playerItem else { return }
let name = AVPlayerItem.didPlayToEndTimeNotification
let queue = OperationQueue.main
itemEndObserver = NotificationCenter.default.addObserver(forName: name, object: playerItem, queue: queue, using: {[weak self] notification in
guard let self = self else { return }
self.endPlay3()
})
}
结语
当涉及到处理视频资源播放结束时,通常需要根据具体业务需求进行相应的处理。不同的播放器可能采取不同的策略,如返回播放列表、自动播放下一个视频,或者简单地显示播放结束状态。无论采取何种处理方式,我们都必须注意维护良好的页面显示状态和播放控制器的销毁,以避免可能导致内存泄漏的问题。
确保页面的显示状态正确反映了视频播放的结束状态,同时及时释放播放控制器等资源是至关重要的。这可以通过有效的事件处理和资源管理来实现。当视频播放结束时,我们应该清理不再需要的资源,例如关闭播放器或者更新页面UI以显示适当的结束状态。这样可以确保页面的性能和用户体验始终保持在最佳状态。
总之,对于视频播放结束的处理,我们需要根据具体业务需求采取适当的措施,并且务必注意页面显示状态的维护以及播放控制器等资源的正确释放,以避免可能出现的内存泄漏问题。