记得刚写音乐播放器那会儿,和大多数人同样。都会想本身写的也许不够完善,还可能会出各类问题,并且如今有不少开源的比较完善的播放器,找个好用的就行了呀,以前也确实是这样作的。可是随着本身的App 在音频播放上的业务以及要求变动,你会发现第三方的彻底不够用,那么就会想着去修改第三方的东西,这时候你会发现,第三方的东西虽好是不少大牛写的(基于底层)比较好用,但改起来却苦不堪言。因此最终仍是回归本源,本身定义音频播放。最近在整理之前的东西,顺便在此分享一下,但愿能够给刚写播放器的兄弟一些帮助.ios
优势:AVPlayer属于AVFoundation框架既能够播放本地音频也能够网络音频,更接近底层也会更加灵活,定制性比较高git
用AVPlayer 播放音视频你会发现,它在设计上各个部分相对独立,这样更有利拆分使用,更加灵活。(好比 用AVPlayer 播放视频你会发现 和生活中看视频套路差很少 它也须要 播放器 显示屏 磁盘)github
AVAsset:AVAsset类专门用于获取多媒体的相关信息,包括获取多媒体的画面、声音等信息,属于一个抽象类,不能直接使用。 AVURLAsset:AVAsset的子类,能够根据一个URL路径建立一个包含媒体信息的AVURLAsset对象 CMTime:是一个结构体,里面存储着当前的播放进度,总的播放时长swift
你会发现 AVPlayer 虽然是一个总体的音频播放器可是,它内部把各个功能分红了单独的对象,定义时就相对独立,又能够组合完成功能。这样作耦合性会下降,这也能给咱们启发,咱们在写一些比较大的功能的时候,要把它们细化成小的功能(查错方便,其余地方也能够用)。接下来的自定义播放器也会用到这种思想。数组
接下来,就详细梳理一下自定义的AVPlayer 以swift 代码为例(为了紧跟时代步伐 - _ -)缓存
oc 和 swift 版本 带缓冲进度的自定义进度条 友好的图片高斯模糊处理 所有在这里了。 WPYPlayerbash
不知道为何图片模糊处理后录出来成这样了,项目比这个好看多了。 微信
1 单例初始化网络
static let playManager = WPY_AVPlayer()
var player : AVPlayer = {
let _player = AVPlayer()
_player.volume = 2.0 //默认最大音量
return _player
}()
复制代码
播放器初始化session
func initPlayer() {
//APP进入后台通知
NotificationCenter.default.addObserver(self, selector: #selector(configLockScreenPlay) , name:UIApplication.didEnterBackgroundNotification, object: nil)
let session = AVAudioSession.sharedInstance()
try? session.setActive(true)
//后台播放
Util_OC.setAVAudioSessionCategory(.playback)
}
复制代码
播放前须要配置一些监听事件 例如: 1 监听播放状态 (对于音频的不一样状态,给与不懂操做) 2 缓冲加载状况(便于有加载播放进度条需求) 3 播放进度(就不用本身用定时器来表示播放时间,那样也不许确,直接用系统的就好) 4 播放结束通知(便于音频播放结束作相关操做) 5 监听打断处理(播放期间被 电话 短信 微信 等打断后的处理)
// 播放前增长配置 监测
func currentItemAddObserver(){
//监听是否靠近耳朵
NotificationCenter.default.addObserver(self, selector: #selector(sensorStateChange), name:UIDevice.proximityStateDidChangeNotification, object: nil)
//播放期间被 电话 短信 微信 等打断后的处理
NotificationCenter.default.addObserver(self, selector: #selector(handleInterreption(sender:)), name:AVAudioSession.interruptionNotification, object:AVAudioSession.sharedInstance())
// 监控播放结束通知
NotificationCenter.default.addObserver(self, selector: #selector(playMusicFinished), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: self.player.currentItem)
//监听状态属性 ,注意AVPlayer也有一个status属性 经过监控它的status也能够得到播放状态
self.player.currentItem?.addObserver(self, forKeyPath: "status", options:[.new,.old], context: nil)
//监控缓冲加载状况属性
self.player.currentItem?.addObserver(self, forKeyPath:"loadedTimeRanges", options: [.new,.old], context: nil)
self.timeObserVer = self.player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1, preferredTimescale: 1), queue: DispatchQueue.main) { [weak self] (time) in
guard let `self` = self else { return }
let currentTime = CMTimeGetSeconds(time)
self.progress = Float(currentTime)
if self.isSeekingToTime {
return
}
let total = self.durantion
if total > 0 {
self.delegate?.updateProgressWith(progress:Float(currentTime) / Float(total))
}
}
}
复制代码
相应的当该 playItem 播放结束时 移除相关监测,观察
// 播放后 删除配置 监测
func currentItemRemoveObserver(){
self.player.currentItem?.removeObserver(self, forKeyPath:"status")
self.player.currentItem?.removeObserver(self, forKeyPath:"loadedTimeRanges")
NotificationCenter.default.removeObserver(self, name:UIDevice.proximityStateDidChangeNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
NotificationCenter.default.removeObserver(self, name:AVAudioSession.interruptionNotification, object: nil)
if(self.timeObserVer != nil){
self.player.removeTimeObserver(self.timeObserVer!)
}
}
复制代码
一些监测的相关处理
1 app进入后台的 进行后台播放
注意:记得在工程中打开发后台播放功能 不然不会后台播放
//锁屏 或 退入后台 保持音频继续播放
@objc func configLockScreenPlay() {
//设置并激活音频会话类别
let session = AVAudioSession.sharedInstance()
Util_OC.setAVAudioSessionCategory(.playback)
try? session.setActive(true)
//容许应用接收远程控制
//设置后台任务ID
var newTaskID = UIBackgroundTaskIdentifier.invalid
newTaskID = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
if (newTaskID != UIBackgroundTaskIdentifier.invalid) && (self.bgTaskId != UIBackgroundTaskIdentifier.invalid) {
UIApplication.shared.endBackgroundTask(self.bgTaskId)
}
self.bgTaskId = newTaskID
}
复制代码
监测和耳朵的距离 来判断是听筒 仍是 外音 播放
@objc func sensorStateChange() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
if UIDevice.current.proximityState == true {
//靠近耳朵
/* AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:category withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil]; */
//swift 4.2 后ios 10 如下不兼容 因此用了oc 的方式写的**
Util_OC.setAVAudioSessionCategory(.playAndRecord)
}else {
//远离耳朵
Util_OC.setAVAudioSessionCategory(.playback)
}
}
}
复制代码
处理播放音频是被来电 或者 其余 打断音频的处理
@objc func handleInterreption(sender:NSNotification) {
let info = sender.userInfo
guard let type : AVAudioSession.InterruptionType = info?[AVAudioSessionInterruptionTypeKey] as? AVAudioSession.InterruptionType else { return }
if type == AVAudioSession.InterruptionType.began {
self.pause()
}else {
guard let options = info![AVAudioSessionInterruptionOptionKey] as? AVAudioSession.InterruptionOptions else {return}
if(options == AVAudioSession.InterruptionOptions.shouldResume){
self.pause()
}
}
}
复制代码
单个音频播放结束后的逻辑处理
@objc func playMusicFinished(){
UIDevice.current.isProximityMonitoringEnabled = true
self.seekToZeroBeforePlay = true
self.isPlay = false
self.updateCurrentPlayState(state: AVPlayerPlayState.AVPlayerPlayStateEnd)
//在这里进逻辑处理
if (self.playType == WPY_AVPlayerType.PlayTypeSpecial) {
self.next()
}
}
复制代码
播放单个音频的方法 如播放 音效, 试听, 问题回答, 即无关联性只有url的
func playMusic(url : String,type:WPY_AVPlayerType){
self.playType = type // 记录播放类型 以便作出不一样处理
self.setPlaySpeed(playSpeed: 1.0) //播放前初始化倍速 1.0
self.currentItemRemoveObserver() //移除上一首的通知 观察
let playUrl = self.loadAudioWithPlaypath(playpath: url)
let playerItem = AVPlayerItem(url: playUrl)
self.playerItem = playerItem
self.currentUrl = url
self.isImmediately = true
self.player.replaceCurrentItem(with: playerItem)
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
self .currentItemAddObserver()
}
复制代码
播放多个连续音频的方法 例如 音乐播放器,或者多个连续的音频
/// 用于播放多个音频的列表 播放方法
///
/// - Parameters:
/// - index: 播放列表中的第几个音频
/// - isImmediately: 是否当即播放
func playTheLine(index :Int,isImmediately :Bool){
self.currentItemRemoveObserver()
self.playType = .PlayTypeLine // 记录播放类型 以便作出不一样处理
let record = self.musicArray[index]
guard let url = record.playpath else { return }
let playUrl = self.loadAudioWithPlaypath(playpath:url )
let playerItem = AVPlayerItem(url: playUrl)
self.playerItem = playerItem
self.currentUrl = url
self.isImmediately = isImmediately
self.currentScenicPoint = record
self.currentIndex = index
if !isImmediately {
self.pause()
}
self.player.replaceCurrentItem(with: playerItem)
self.currentItemAddObserver()
}
复制代码
实现观察者方法 根据 playitem 的播放状态作相应操做 以及 及时更新缓冲进度
/// 观察者 播放状态 和 缓冲进度
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
let item = object as! AVPlayerItem
if keyPath == "status" {
switch item.status {
case AVPlayerItem.Status.readyToPlay:
if isImmediately {
self.play()
}else{
self.setNowPlayingInfo()
}
case AVPlayerItem.Status.failed,AVPlayerItem.Status.unknown:
self.updateCurrentPlayState(state: AVPlayerPlayState.AVPlayerPlayStateNotPlay)
}
}else if keyPath == "loadedTimeRanges" {
let array = item.loadedTimeRanges
let timeRange = array.first?.timeRangeValue
guard let start = timeRange?.start , let end = timeRange?.end else {
return
}
let startSeconds = CMTimeGetSeconds(start)
let durationSeconds = CMTimeGetSeconds(end)
let totalBuffer = startSeconds + durationSeconds
let total = self.durantion
if totalBuffer != 0 && total != 0{
delegate?.updateBufferProgress(progress: Float(totalBuffer) / Float(total))
print("\(Float(totalBuffer) / Float(total))")
}
}
}
}
复制代码
这样 一个url 或者 数组 + 播放序列 就能够实现基本的播放音频了 接下能够写一下 播放器的四个基本操做
暂停
func pause(){....... player.pause() ..........}
复制代码
播放
func play(){ ....... self.player.play() ......}
复制代码
上一首
func next(){ ...... changeTheMusicByIndex .......}
复制代码
下一首
func previous(){ .......... changeTheMusicByIndex .........}
复制代码
由于通常音频的切换会有不少相应的操做须要 好比界面的图片,文字的替换等等 因此咱们统一下载了一个方法里
func changeTheMusicByIndex(index : Int){
self.playTheLine(index: index, isImmediately: true)
delegate?.changeMusicToIndex(index: index)
//
}
复制代码
1 音频混缓冲进度
2 音频播放进度
3 音频切换的相应操做
这三个回调咱们采用代理方式 由于这三个操做通常设计到了 播放界面的单独操做通常为 一对一的
protocol WPY_AVPlayerDelegate : class {
func updateProgressWith(progress : Float)
func changeMusicToIndex(index : Int)
func updateBufferProgress(progress : Float)
}
复制代码
4 音频播放状态 相对于音频播放状态而言,就不必定是一对一了, 例如: 有可能tableView 上的每一个cell中都有试听 操做
并且个能有不一样类型的各类播放形式,而后最基本的播放状态都是要的。因此 咱们对播放状态的回调采用全局通知的形式
里面最好带参数
1 播放类型
2 播放连接 这样能够在一个界面有多个播放时来准确改变补个view 的状态 3 播放相应的状态类型 (统一管理播放状态)
如: 暂停, 播放, 结束, 缓冲准备, 播放出错 case AVPlayerPlayStatePreparing // 准备播放 case AVPlayerPlayStateBeigin // 开始播放 case AVPlayerPlayStatePlaying // 正在播放 case AVPlayerPlayStatePause // 播放暂停 case AVPlayerPlayStateEnd // 播放结束 case AVPlayerPlayStateBufferEmpty // 没有缓存的数据供播放了 case AVPlayerPlayStateBufferToKeepUp //有缓存的数据能够供播放 case AVPlayerPlayStateseekToZeroBeforePlay case AVPlayerPlayStateNotPlay // 不能播放 case AVPlayerPlayStateNotKnow // 未知状况
/// 实时更新播放状态 全局通知(便于多个地方都用到音频播放,改变播放状态)
///
/// - Parameter state: 播放状态
func updateCurrentPlayState(state : AVPlayerPlayState){
if self.currentUrl != nil {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: WPY_PlayerState), object: nil, userInfo: [WPY_PlayerState : state,CurrentPlayUrl : self.currentUrl!,PlayType : self.playType])
}else {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: WPY_PlayerState), object: nil, userInfo: [WPY_PlayerState : state,CurrentPlayUrl : "",PlayType : self.playType])
}
}
复制代码
至此,一个基本的自定义播放器就宣布完成了
监听
//监听是否靠近耳朵
NotificationCenter.default.addObserver(self, selector: #selector(sensorStateChange), name:UIDevice.proximityStateDidChangeNotification, object: nil)
复制代码
相应操做
//监测是否靠近耳朵 转换声音播放模式
@objc func sensorStateChange() {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
if UIDevice.current.proximityState == true {
//靠近耳朵
Util_OC.setAVAudioSessionCategory(.playAndRecord)
}else {
//远离耳朵
Util_OC.setAVAudioSessionCategory(.playback)
}
}
}
复制代码
由于 swift 4.2 对于ios 10.0 如下 不兼容,因此用了调oc的方法解决
有更好处理方式的欢迎交流
+ (void)setAVAudioSessionCategory:(AVAudioSessionCategory) category {
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:category withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker error:nil];
}
复制代码
注意:其实在音频中止播放后 就不该该有这种操做 (听筒转换 屏幕息屏)
因此 咱们应该在 本身的暂停 函数中关掉红外感应
UIDevice.current.isProximityMonitoringEnabled = false
在播放函数中子打开
UIDevice.current.isProximityMonitoringEnabled = true
锁屏显示播放信息 包括到了状态 因此 咱们先进行状态相关操做的时候 ,都应该调用信息设置操做
如 : 暂停 播放 改变进度等
/// 设置锁屏时 播放中心的播放信息、
func setNowPlayingInfo(){
if (self.playType == .PlayTypeLine || self.playType == .PlayTypeSpecial) && self.currentScenicPoint != nil {
// 1 名字
var info = Dictionary<String,Any>()
info[MPMediaItemPropertyTitle] = self.currentScenicPoint?.name ?? ""
// 2 图片
if let image = UIImage(named: "AppIcon"){
info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image:image)//显示的图片
}
// if let url = self.currentScenicPoint?.pictureArray?.first ,let image = UIImage(named: "AppIcon"){
// imageView.kf.setImage(with: URL(string:url), placeholder: image, options: nil, progressBlock: nil) { (img, _, _, _) in
//
// if
// info[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image:img)//显示的图片
// }
// }else{
//
// }
//3 总时长
info[MPMediaItemPropertyPlaybackDuration] = self.durantion
if let duration = self.player.currentItem?.currentTime() {
info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = CMTimeGetSeconds(duration)
}
//4 播放速率
info[MPNowPlayingInfoPropertyPlaybackRate] = 1.0
//最后 设置
MPNowPlayingInfoCenter.default().nowPlayingInfo = info
}
}
复制代码
记得进入后台后开启接收远程事件
UIApplication.shared.beginReceivingRemoteControlEvents()
在某些不须要远程事件是要关掉
UIApplication.shared.endReceivingRemoteControlEvents()
// //后台操做 在delegate 或者 某个VC 中初始化
// override func remoteControlReceived(with event: UIEvent?) {
// guard let event = event else {
// print("no event\n")
// return
// }
//
// if event.type == UIEventType.remoteControl {
// switch event.subtype {
// case .remoteControlTogglePlayPause:
// print("暂停/播放")
//
// case .remoteControlPreviousTrack:
// print("上一首")
// self.previous()
// case .remoteControlNextTrack:
// print("下一首")
// self.next()
// case .remoteControlPlay:
// print("播放")
// self.play()
// case .remoteControlPause:
// print("暂停")
// self.pause()
// default:
// break
// }
// }
// }
//
复制代码
设置 playSpeed 属性用于记录 改变的播放速率 由于有多是暂停状态下改的播放速率,不能及时生效。因此要记录一下
也真由于如此,因此播放时要及时更新下播放速率
self.enableAudioTracks(enable: true, playerItem: self.playerItem!)
注意: 暂停是调用此方法会直接播放,因此要放在播放时再调用
//设置播放速率
func setPlaySpeed(playSpeed:Float) {
if self.isPlay{
self.enableAudioTracks(enable: true, playerItem: self.playerItem!)
self.player.rate = playSpeed;
}
self.playSpeed = playSpeed
}
复制代码
/// 改变播放速率 必实现的方法
///
/// - Parameters:
/// - enable:
/// - playerItem: 当前播放
func enableAudioTracks(enable:Bool,playerItem : AVPlayerItem){
for track : AVPlayerItemTrack in playerItem.tracks {
if track.assetTrack?.mediaType == AVMediaType.audio {
track.isEnabled = enable
}
}
}
复制代码
这个通常的网络库中都有网络状态的判断,那么应该在哪里进行此操做呢
最合理的地方应该是 播放 方法里面。由于在此能够最大限度的控制流量,即便播到一半暂停 网络变化后也能够及时终止
进度条有两个改变
1 随着音频播放,逐渐改变。 2 手动调整位置,调整播放进度
可是这个连个问题会存在交叉问题,即在手动调整进度是若是音频播放不停,并且进度回调也一直在走,那么你会发现进度条在拉的时候是在跳动。
解决方案: 因此 在手动拉进度时,应该停掉音频播放的进度回调,在手动进度结束时,根据进度播放器把音频跳到指定位置播放,同时恢复音频进度回调
- (NSString *)timeFormatted:(int)totalSeconds {
int seconds = totalSeconds % 60;
int minutes = (totalSeconds / 60);
return [NSString stringWithFormat:@"%02d:%02d", minutes, seconds];
}
复制代码
时间通常为两个 一个是当前时间 另外一个是剩余时间或者总时间
这里就须要将avplayer 的时间 CTime 转换为字符串
object-C 的相对来讲比较好处理
//视频的总长度 NSTimeInterval total = CMTimeGetSeconds(self.player.currentItem.duration); 直接取值转化字符串就好
问题是 swift
由于swift对类型要求比较严格,因此要进行类型转换。这时候你会发如今进行时间赋值是会崩溃
缘由:由于 swift 是不会有默认值的。有时音频数据没取回,有可能就已经有赋值操做。
因此 咱们在进行赋值操做前 要进行判断
if self.isNaN || self.isInfinite {
return "00:00"
}
复制代码