赞
踩
我把msu8开发好的Swift项目放在这里,后期写一个m3u8播放器文章在移动过去:Swift-(iOS 15 16)m3u8视频播放器
Swift 技术 音频,音乐(AVAudioSession设置,音乐中断)
Swift 技术 监听电话中断,音乐(用于恢复播放音乐)(源码)
Swift 第三方 播放器AliyunPlayer(阿里云播放器)(源码)
Swift 需求 音乐播放暂停淡出淡放(声音逐渐消失)(视频)(源码)
OC 技术 DOUAudioStreamer音乐播放器的使用(源码)
http://music.163.com/song/media/outer/url?id=468176711.mp3
开发的时候如何从网上拿到mp3,mp4的链接.咪咕音乐
然后点进去,选择点击一首音乐,右键点击网站(检查),看下面图片操作就能拿到播放链接.
https://freetyst.nf.migu.cn/public/product9th/product41/2020/09/1514/2020%E5%B9%B409%E6%9C%8804%E6%97%A511%E7%82%B909%E5%88%86%E7%B4%A7%E6%80%A5%E5%86%85%E5%AE%B9%E5%87%86%E5%85%A5%E6%AD%A3%E4%B8%9C25%E9%A6%96258276/%E6%AD%8C%E6%9B%B2%E4%B8%8B%E8%BD%BD/MP3_40_16_Stero/6005662RPV2142203.mp3
https://vd3.bdstatic.com/mda-kksevzw0s5ap0x6k/hd/cae_h264_nowatermark/1606445103/mda-kksevzw0s5ap0x6k.mp4
现在很多播放器都会显示一个播放器进度条加载的显示过程,但是iOS的控件里面没有这种控件,系统就提供了UISlider这个控件.所以我自己封装了一个可以看到播放器的封装.并写了一遍文件记录整个封装的过程和解说
AVPlayer是一个用来播放的对象,支持播放本地,分步下载,或者通过HLS协议得到的流媒体.
它是一个不可见的组件,如果播放mp3,mp4类的音频文件,可以.但是如果要想播放视频文件,我们就需要了解另外一个类AVPlayerLayer
AVPlayerLayer
它是对于CALayer类的扩展,通过框架在屏幕上显示内容,作为视频的渲染面,然后在用户面前进行展示.在创建AVPlayerLayer时,同时需要一个指向 AVPlayer的指针,把两者联系在一起.
说白了,AVPlayerItem是一个载体,承载AVAsset,然后通过AVPlayer进行播放.我们要想对一个资源进行播放,那么就要通过AVPlayerItem和AVPlayerItemTrack来构建对应的动态内容
注意视频一加载就会播放
let filePath = Bundle.main.path(forResource: "SuchAs", ofType: "mp4")
let videoURL = URL(fileURLWithPath: filePath!)
playerItem = AVPlayerItem(url: videoURL)
player = AVPlayer(playerItem: playerItem)
player.rate = 1.0//播放速度 播放前设置
//创建显示视频的图层
let playerLayer = AVPlayerLayer.init(player: player)
playerLayer.videoGravity = .resizeAspect
playerLayer.frame = CGRect(x: 0, y: 0, width: 150, height: 300)
playView.layer.addSublayer(playerLayer)
player.pause() //停止播放
let videoURL = URL(string: "http://n1cdn.miaopai.com/stream/1UKfVpOmazRYEb4fVejwhgpX~3uIxmHBV~8VCQ___0_1506471211.mp4?ssig=e9a0601e2c3261e5c6b6c91a1111ced3&time_stamp=1652542020536")
playerItem = AVPlayerItem(url: videoURL!)
player = AVPlayer(playerItem: playerItem)
player.rate = 1.0//播放速度 播放前设置
//创建显示视频的图层
let playerLayer = AVPlayerLayer.init(player: player)
playerLayer.videoGravity = .resizeAspect
playerLayer.frame = CGRect(x: 0, y: 0, width: 150, height: 300)
playView.layer.addSublayer(playerLayer)
player.pause()
let filePath = Bundle.main.path(forResource: "Faded", ofType: "mp3")
let videoURL = URL(fileURLWithPath: filePath!)
let playerItem = AVPlayerItem(url: videoURL)
player = AVPlayer(playerItem: playerItem)
player.rate = 1.0//播放速度 播放前设置
let videoURL = URL(string: self.model.PronounceUrl)
let playerItem = AVPlayerItem(url: videoURL!)
player = AVPlayer(playerItem: playerItem)
player.rate = 1.0//播放速度 播放前设置
player.play()
NotificationCenter.default.addObserver(self, selector: #selector(playToEndTime), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil)
//监听播放结束
@objc func playToEndTime(){
print("播放完成")
}
deinit {
//监听播放结束
NotificationCenter.default.removeObserver(self)
}
@objc func playToEndTime(){
player?.seek(to: CMTime(value: 0, timescale: 1))
player?.play()
}
//观察属性
self.playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)
//缓存区间,可用来获取缓存了多少
self.playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
//缓存不够了 自动暂停播放
self.playerItem.addObserver(self, forKeyPath: "playbackBufferEmpty", options: .new, context: nil)
//缓存好了 手动播放
self.playerItem.addObserver(self, forKeyPath: "playbackLikelyToKeepUp", options: .new, context: nil)
//KVO观察 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == "status" { switch self.playerItem.status{ case .readyToPlay: //player.play() print("play") case .failed: print("failed") case.unknown: print("unkonwn") default: break } }else if keyPath == "loadedTimeRanges"{ let loadTimeArray = self.playerItem.loadedTimeRanges //获取最新缓存的区间 let newTimeRange : CMTimeRange = loadTimeArray.first as! CMTimeRange let startSeconds = CMTimeGetSeconds(newTimeRange.start); let durationSeconds = CMTimeGetSeconds(newTimeRange.duration); let totalBuffer = startSeconds + durationSeconds;//缓冲总长度 print("当前缓冲时间:%f",totalBuffer) }else if keyPath == "playbackBufferEmpty"{ print("正在缓存视频请稍等") } else if keyPath == "playbackLikelyToKeepUp"{ print("缓存好了继续播放") //self.player.play() } }
//用于实时监听滑块的进度
self.player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 1), queue: DispatchQueue.main) { [weak self](time) in
//当前正在播放的时间
let loadTime = CMTimeGetSeconds(time)
//视频总时间
let totalTime = CMTimeGetSeconds((self?.player.currentItem?.duration)!)
//滑块进度
self?.slider.value = Float(loadTime/totalTime)
self?.loadTimeLabel.text = self?.changeTimeFormat(timeInterval: loadTime)
self?.totalTimeLabel.text = self?.changeTimeFormat(timeInterval: CMTimeGetSeconds((self?.player.currentItem?.duration)!))
}
//转时间格式
func changeTimeFormat(timeInterval:TimeInterval) -> String{
return String(format: "%02d:%02d:%02d",(Int(timeInterval) % 3600) / 60, Int(timeInterval) / 3600,Int(timeInterval) % 60)
}
#pragma mark - 设置 播放了的时间 滚动条的播放进度 播放结束时间 -(void)getProgress { //用于在指定的时间间隔内定期触发回调来更新播放进度或执行其他操作。 /** * @param Interval : 指定回调触发的时间间隔(最少要1秒) * @param queue : 指定回调的执行队列,一般使用主队列(DispatchQueue.main) * @param block : 回调闭包,接收一个CMTime参数,表示当前播放的时间 * * @return nil * * */ [[AVPlayerManager new].player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { //获取当前的播放了的时间 NSString *startTime = [self formatTimeWithTimeInterVal:CMTimeGetSeconds(time)]; //歌曲的总时间 NSString *endTime = [self formatTimeWithTimeInterVal:CMTimeGetSeconds([AVPlayerManager new].playerItem.duration)]; //获取当前的播放了的时间 float percent = CMTimeGetSeconds(time) / CMTimeGetSeconds([AVPlayerManager new].playerItem.duration); float value = percent * 100; self.progressBlock(startTime, endTime, value); }]; }
import UIKit import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white let thumb = UIImageView.init(frame: CGRect.init(x: 50, y: 200, width: 300, height: 200)) self.view.addSubview(thumb) let filePath = Bundle.main.path(forResource: "SuchAs", ofType: "mp4") let videoURL = URL(fileURLWithPath: filePath!) let asset = AVURLAsset.init(url: videoURL, options: nil) let gen = AVAssetImageGenerator.init(asset: asset) gen.appliesPreferredTrackTransform = true let time = CMTimeMakeWithSeconds(0.0, preferredTimescale: 1) var actualTime : CMTime = CMTimeMakeWithSeconds(0, preferredTimescale: 0) do { let image = try gen.copyCGImage(at: time, actualTime: &actualTime) thumb.image = UIImage.init(cgImage: image) } catch { print("错误") } } }
import UIKit import AVFoundation class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white let thumb = UIImageView.init(frame: CGRect.init(x: 50, y: 200, width: 300, height: 200)) self.view.addSubview(thumb) let asset = AVURLAsset.init(url: URL.init(string: "http://gslb.miaopai.com/stream/1UKfVpOmazRYEb4fVejwhgpX~3uIxmHBV~8VCQ__.mp4")!, options: nil) let gen = AVAssetImageGenerator.init(asset: asset) gen.appliesPreferredTrackTransform = true let time = CMTimeMakeWithSeconds(0.0, preferredTimescale: 1) var actualTime : CMTime = CMTimeMakeWithSeconds(0, preferredTimescale: 0) do { let image = try gen.copyCGImage(at: time, actualTime: &actualTime) thumb.image = UIImage.init(cgImage: image) } catch { print("错误") } } }
一般视频有声音没有画面,多数都是AVPlayerLayer有关系的。
那么这次的原因是因为,使用系统监听的网络封装,从飞行模式转换到wifi模式之后,当时在异步线程刷新UI导致的。
修改办法:
1.如何做到启动顺便停止汽水音乐的App的音乐播放?
2.如果我想在点击播放按键的时候才停止播放,音乐的App的音乐播放.如何修改?
造成该原因是因为下面的代码造成的.
一旦启动就会停止,汽水音乐正在播放的音乐.因为App启动的时候,category默认的设置是:AVAudioSessionCategorySoloAmbient
只要把上面的代码不要写在Appdelegate里面启动,就不会停止汽水音乐播放的音乐.
那么上面的第二个问题,如果我想在点击播放按键的时候才停止播放,音乐的App的音乐播放.如何修改?
把这句代码写在点击播放的哪里,也就是播放才设置.
该bug应该写在视频播放器里面,目前先总结到这里
滑动闪烁的问题
修好-滑动没有问题
造成这个问题的原因是:当修改进度的时候(player.seek(to: seekTime)
),并不是马上就能获取当前的进度就是修改好的进度,监听获取当前进度(player.addPeriodicTimeObserver(forInterval:
),在没有修改进度成功之间返回的进度还是没有拖拽的进度.所以就会出现拖拽进度后,出现闪烁的问题(闪烁回去没有修改的进度点哪里). 修改方法
1.新建一个属性,当拖拽之后,(player.addPeriodicTimeObserver(forInterval:
)监听进度方法禁止更新.
2.当拖拽进度之后,等到进度修改成功了,更新当前进度条,之后才去允许,(player.addPeriodicTimeObserver(forInterval:
)监听进度方法更新.
步骤1.info.plist直接设置
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
或者也可以向下面的设置也是同样的效果
我发现这样设置之后好像已经支持后台播放,不过网上很多还需要下面的设置.
写在appDelegate
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setActive:YES error:nil];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
不过要是想 后台播放下一曲还需要再添加一行代码
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
写在appDelegate
//设置音乐的播放模式
private func setAudioSupport(){
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(AVAudioSession.Category.playback)
try session.setActive(true)
} catch {
print(error)
}
UIApplication.shared.beginReceivingRemoteControlEvents()
}
2023.5.3日修改一个非常头疼的问题,当时在2022.11月份的时候修改NML,JAZZ,WORLD,的时候也修改过的同样出现这个问题,后来隔了很久,NSWL也发现这个问题,测试了很久才会想起来,之前是修过的,但是也花了很长时间才测试出来.
主要原因有两个;
原因一:
启动的时候重新设置播放规则,看下面代码.
设置这个的目的是为了能够支持后台播放,但是我发现,注释了一样能支持后台播放,我还以SDK本身启动的时候有设置,这个我就不管它了
原因二:
app启动把音乐加载到iPhone的播放器里面,占用的网易云在iPhone播放器里面的音乐,因为不管app有多少个,硬件的播放器就只有一个,如果想播放音乐,就需要把软件的音乐放到硬件的播放器里面对音乐进行播放.
NML,JAZZ,WORLD,NSWL 这些app有一个本身有一个需求,如果app整个播放的情况下,杀死了app,重新启动的时候需要把迷你播放器或者播放器详情重新弹出来,然后点击播放按键就开始播放音乐.
问题就在于,app启动的时候把保存在本地里面的歌曲拿出来,然后谈起播放器详情的信息进行显示,然后把播放管理器的歌曲放到硬件的播放器里面,网易云正在播放的音乐就停止了.,原因就在前面红色的字体里.拿到本地的歌曲赋值UI同时加载音乐.
修改它有两个办法:
1.app启动的时候,歌曲赋值UI就是赋值UI,歌曲放到硬件播放器,这样把这个两个步骤单独分开.
2.app启动的时候,点击播放,切换歌曲的时候,才把本地的歌曲赋值到播放管理器的模型里面
目前修改为下面的代码,思想主要是上面核心的问题,下面自己用于回想当时的情景,
最好的修改方法应该是:app启动的时候,歌曲赋值UI就是赋值UI,歌曲放到硬件播放器,这样把这个两个步骤单独分开.
2023.03.23 NSWL遇到一个问题,当NSWL正在播放音乐的时候,然后打开抖音播放视频,或者打开汽水音乐app播放音乐,这个时候NSWL的音乐被中断了,我们应该App收到通知去暂停播放音乐.
但是NSWL音乐是中断了,但是AliyunPlayer的SDK并没有提供(抢占音乐)代理,所以无法进行设置暂停播放的操作.
但是NML使用 DOUAudioStreamer SDK播放音乐当抢占音乐的时候代理是有回调的,所以设置很方便
那么AliyunPlayer既然没有这个功能,我们应该如何处理这个bug,看下面的逻辑图就明白了
下面的主要的代码
// 检测中断音乐通知
NotificationCenter.default.addObserver(self, selector: #selector(interruptionNotification(notifi:)), name: AVAudioSession.interruptionNotification, object: nil)
// 检测中断音乐通知
@objc func interruptionNotification(notifi:Notification) {
//false 中断结束 true 中断开始
print("Key: \(notifi.userInfo?["AVAudioSessionInterruptionTypeKey"] as? Bool)")
let suspend = notifi.userInfo?["AVAudioSessionInterruptionTypeKey"] as? Bool ?? false
if suspend {
NotificationCenter.default.post(name: NSNotification.Name.suspendNotification, object: nil)
}
}
上面的解决方法,我写在播放管理起里面的,有时候我怕出发不了,毕竟那时候倍抢占了音乐,自己的app肯定在后台,所以我在Appdelegate里面也写多了一个代码,用于出发暂停按键的设置,看自己的需求,有空的话可以写一下.
// 检测中断音乐通知
NotificationCenter.default.addObserver(self, selector: #selector(interruptionNotification(notifi:)), name: AVAudioSession.interruptionNotification, object: nil)
// 检测中断音乐通知
@objc func interruptionNotification(notifi:Notification) {
//false 中断结束 true 中断开始
//print("Key: \(notifi.userInfo?["AVAudioSessionInterruptionTypeKey"] as? Bool)")
let suspend = notifi.userInfo?["AVAudioSessionInterruptionTypeKey"] as? Bool ?? false
if suspend {
NotificationCenter.default.post(name: NSNotification.Name.suspendNotification, object: nil)
}
}
写好之后NSWL没有上架过不知道能够上架,听说CallKit在中国禁用了.下面的方法就是用CallKit解决的
不行的话就使用CoreTelephony
下面的代码写在AppDelegate里面的.
UIApplicationDelegate
///属性记录电话 打来中断
private let callObserver = CXCallObserver()
callObserver.setDelegate(self, queue: nil)
extension AppDelegate: CXCallObserverDelegate { func callObserver(_ callObserver: CXCallObserver, callChanged call: CXCall) { if !call.isOutgoing && !call.isOnHold && !call.hasConnected && !call.hasEnded {// 暂停播放 if AliPlayerManger.default.playerState == 1 { AliPlayerManger.default.playerState = 0 callPhoneOutGoing = true AliPlayerManger.default.pause() } } else if !call.isOutgoing && !call.isOnHold && !call.hasConnected && call.hasEnded {// 开始播放 //print("来电-挂掉(未接通)") if AliPlayerManger.default.playerState == 0 && callPhoneOutGoing == true { AliPlayerManger.default.playerState = 1 callPhoneOutGoing = false AliPlayerManger.default.play() } } else if !call.isOutgoing && !call.isOnHold && call.hasConnected && !call.hasEnded {// 暂停播放 //print("来电-接通") if AliPlayerManger.default.playerState == 1 { AliPlayerManger.default.playerState = 0 callPhoneOutGoing = true AliPlayerManger.default.pause() } } else if !call.isOutgoing && !call.isOnHold && call.hasConnected && call.hasEnded {// 开始播放 //print("来电-接通-挂掉") if AliPlayerManger.default.playerState == 0 && callPhoneOutGoing == true { AliPlayerManger.default.playerState = 1 callPhoneOutGoing = false AliPlayerManger.default.play() } } else if call.isOutgoing && !call.isOnHold && !call.hasConnected && !call.hasEnded {// 暂停播放 //print("拨打") if AliPlayerManger.default.playerState == 1 { AliPlayerManger.default.playerState = 0 callPhoneOutGoing = true AliPlayerManger.default.pause() } } else if call.isOutgoing && !call.isOnHold && !call.hasConnected && call.hasEnded {// 开始播放 //print("拨打-挂掉(未接通)") if AliPlayerManger.default.playerState == 0 && callPhoneOutGoing == true { AliPlayerManger.default.playerState = 1 callPhoneOutGoing = false AliPlayerManger.default.play() } } else if call.isOutgoing && !call.isOnHold && call.hasConnected && !call.hasEnded {// 暂停播放 //print("拨打-接通") if AliPlayerManger.default.playerState == 1 { AliPlayerManger.default.playerState = 0 callPhoneOutGoing = true AliPlayerManger.default.pause() } } else if call.isOutgoing && !call.isOnHold && call.hasConnected && call.hasEnded {// 开始播放 //print("拨打-接通-挂掉") if AliPlayerManger.default.playerState == 0 && callPhoneOutGoing == true { AliPlayerManger.default.playerState = 1 callPhoneOutGoing = false AliPlayerManger.default.play() } } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。