欢迎小伙伴的到来~~~
本次的代码已经上传GitHub
(一)AVFoundation简介
AVFoundation框架将四大技术领域结合在一起,涵盖了在苹果平台上捕捉,处理,合成,控制,导入和导出视听媒体的广泛任务。
这篇文章将为大家简单总结一下AVFoundation框架中的AVPlayer,并使用AVPlayer创建一个播放器。
(二) AVPlayer
1、介绍
AVPlayer是用于管理媒体资产的回放和定时的控制器对象。您可以使用一个AVPlayer播放本地和远程基于文件的媒体,如QuickTime电影和MP3音频文件,以及使用HTTP Live Streaming提供的视听媒体。
AVPlayer一次播放单个媒体资源。
创建和管理媒体资产的队列使用AVPlayerAVQueuePlayer(本章不对AVPlayerAVQueuePlayer进行介绍)
2、使用AVPlayer
2.1初始化
init()、init(url: URL)
init(playerItem: AVPlayerItem?):使用一个AVPlayerItem进行初始化播放器
AVPlayerItem用于模拟播放器播放资产的时间和呈现状态
AVPlayerItem使用init(url: URL)、init(asset: AVAsset)初始化
AVAsset定义组成资产轨道的集合性质(可以访问表示集合轨迹的实例,如果需要可以独立检查这些实例)以后的文章有机会详细介绍AVAsset
AVPlayer一次只能播放单个媒体资源,更换新的播放使用replaceCurrentItem(with:)方法
//初始化
self.player = AVPlayer()
self.player = AVPlayer(url: URL(string: "url")!)
//let item = AVPlayerItem(asset: AVAsset(url: URL(string: "url")!))
let item = AVPlayerItem(url: URL(string: "url")!)
self.player = AVPlayer(playerItem: item)
//AVPlayer一次播放单个媒体资源,使用replaceCurrentItem设置新的播放
self.player.replaceCurrentItem(with: playerItem)
2.2对播放进行控制
//AVPlayer没有停止方法,只要播放和暂停
self.player.play()
self.player.pause()
//更换播放项目,AVPlayer每次只能播放一个媒体
self.player.replaceCurrentItem(with: item)
//获取当前正在播放项目
self.player.currentTime
//播放速率,速率为0时表示已经播放已经暂停或停止
self.player.rate = 1.0
//跳转到指定播放位置
self.player.seek(to: CMTime(value: 1, timescale: 1))
//添加视频Layer层(不是必须添加)用于显示视频图像
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.frame = self.view.bounds
self.view.layer.addSublayer(playerLayer)
self.player.seek(to: CMTime(value: 1, timescale: 1))//跳转到指定播放位置
CMTime的出现是为了解决浮点数表示的时间计算过程中出现误差的问题,在AVFoundation中很多类都使用CMTime来表示时间,CMTime的结构如下
//typedef int64_t CMTimeValue
//typedef int32_t CMTimeScale
typedef struct
CMTimeValue value;
CMTimeScale timescale;
CMTimeFlags flags;
CMTimeEpoch epoch;
} CMTime;
这里只对value和timescale两个参数进行简单介绍,更为详细的介绍可以参考这篇文章《理解CMTime》
timescale表示一秒被分成的片段数量,value表示片段总量。
例如CMTime(value: 1, timescale: 2) timescale将一秒分成两份,value有一个片段,这样这个CMTime表示的就是0.5秒的位置。以此推算CMTime(value: 10, timescale: 10)这个就表示1秒的位置
3、获取播放时间、播放状态
3.1监听播放状态
通过监听AVPlayerItem中的一些属性来获得来得到当前播放媒体的状态。
//状态监听
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.status),
options: [.new],
context: nil)
//缓冲时间监听
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges),
options: [.new],
context: nil)
//缓冲不足暂停
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferEmpty),
options: [.new],
context: nil)
//缓冲可以跟上播放
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp),
options: [.new],
context: nil)
//缓冲完成
playerItem.addObserver(self,
forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferFull),
options: [.new],
context: nil)
// MARK: - AVPlayerItem Observer
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(AVPlayerItem.status) {
//状态监听
let status: AVPlayerItemStatus
if let statusNumber = change?[.newKey] as? NSNumber {
status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
switch status {
case .readyToPlay:
//可以开始播放了
self.player.play()
break
case .failed:
//播放失败
print("error:\(String(describing: self.player.error))")
break
case .unknown:
// 未知错误
break
}
} else if keyPath == #keyPath(AVPlayerItem.loadedTimeRanges) {
//缓冲时间监听
} else if keyPath == #keyPath(AVPlayerItem.isPlaybackBufferEmpty) {
//缓冲不足暂停
} else if keyPath == #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp) {
//缓冲可以跟上播放
} else if keyPath == #keyPath(AVPlayerItem.isPlaybackBufferFull) {
//缓冲完成
}
}
3.2播放时间监听
//添加周期时间观测器
let interval = CMTime(value: 1, timescale: 1)
self.player?.addPeriodicTimeObserver(forInterval: interval, queue: nil)
{ (time) in
let currentItemTime = CMTimeGetSeconds(time)
}
3.3 AVPlayerItem相关通知
//AVPlayerItem播放到结束通知
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEndTime(_:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
//AVPlayerItem媒体没有及时到达制定播放位置
NotificationCenter.default.addObserver(self, selector: #selector(playerItemPlaybackStalled(_:)), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: nil)
//AVPlayerItem添加了一个新的错误日志条目
NotificationCenter.default.addObserver(self, selector: #selector(playerItemNewErrorLogEntry(_:)), name: NSNotification.Name.AVPlayerItemNewErrorLogEntry, object: nil)
/*
//其他通知
AVF_EXPORT NSString *const AVPlayerItemTimeJumpedNotification NS_AVAILABLE(10_7, 5_0); //该项目的当前时间改变了不连续
AVF_EXPORT NSString *const AVPlayerItemDidPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_0); //项目已发挥到其结束时间
AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_3); //项目未能发挥其结束时间。
AVF_EXPORT NSString *const AVPlayerItemPlaybackStalledNotification NS_AVAILABLE(10_9, 6_0); //媒体没有及时到达继续播放。
AVF_EXPORT NSString *const AVPlayerItemNewAccessLogEntryNotification NS_AVAILABLE(10_9, 6_0); //添加了一个新的访问日志条目。
AVF_EXPORT NSString *const AVPlayerItemNewErrorLogEntryNotification NS_AVAILABLE(10_9, 6_0); //添加了一个新的错误日志条目。
// notification userInfo key type
AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeErrorKey NS_AVAILABLE(10_7, 4_3); // NSError
*/
4、设置后台播放
//设置后台播放
do {
let session = AVAudioSession.sharedInstance()
//设置后台播放
try session.setCategory(AVAudioSessionCategoryPlayback)
//设置活跃(true静音状态下有声音)
try session.setActive(true)
} catch let error as NSError {
print(error)
}
当播放为视频时进入后台会自动暂停,我暂时想到的解决办法是进入后台后手动进行一下play(),代码如下
// MARK: - 应用进入后台通知
@objc func applicationEnterBackground(_ notification: NSNotification) {
//处理视频播放中进入后台自动暂停的问题
weak var weakSelf = self
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()+0.2) {
if weakSelf.isPlayStatus == false{
weakSelf?.play()
}
}
}
5、中断和线路改变通知
//添加中断通知(被电话或者其他事件打断播放)
NotificationCenter.default.addObserver(self, selector: #selector(handleInterruption(_:)), name: NSNotification.Name.AVAudioSessionInterruption, object:nil)
//添加线路改变通知
NotificationCenter.default.addObserver(self, selector: #selector(handleRouteChange(_:)), name: NSNotification.Name.AVAudioSessionRouteChange, object: nil)
//MARK: - 中断通知
@objc func handleInterruption(_ notification: NSNotification) {
let info = notification.userInfo;
let type = AVAudioSessionInterruptionType(rawValue: info?[AVAudioSessionInterruptionTypeKey] as! UInt)
//中断开始,被打断
if type == AVAudioSessionInterruptionType.began {
self.pause()
} else {//中断结束
let optionType = AVAudioSessionInterruptionOptions(rawValue: info?[AVAudioSessionInterruptionOptionKey] as! UInt)
if optionType == AVAudioSessionInterruptionOptions.shouldResume {
//应用获得可以继续播放通知,应该恢复播放
self.play()
}
}
}
//MARK: - 线路切换通知
@objc func handleRouteChange(_ notification: NSNotification) {
let info = notification.userInfo;
let reason = AVAudioSessionRouteChangeReason(rawValue: info?[AVAudioSessionRouteChangeReasonKey] as! UInt)
if reason == AVAudioSessionRouteChangeReason.oldDeviceUnavailable {
//旧设备不可用时
//获取上一个输出设备
let previousRoute = (info?[AVAudioSessionRouteChangePreviousRouteKey] as! AVAudioSessionRouteDescription)
let previousOutput = previousRoute.outputs[0]
let portType = previousOutput.portType
//如果是断开耳机连接
if (portType.elementsEqual(AVAudioSessionPortHeadphones)) {
self.pause()
}
}
}
本次的代码已经上传GitHub, 代码中我还加入了接收RemoteControl遥控(耳机线控和锁屏控制)和锁屏歌曲信息的显示等功能,实现了简单的一个播放器功能,欢迎小伙伴点击GitHub下载