当前位置:   article > 正文

iOS开发:AVPlayer实现流音频边播边存

ios avplayer多个视频顺序播放

iOS开发:AVPlayer实现流音频边播边存 

概述

1. AVPlayer简介

  • AVPlayer存在于AVFoundation中,可以播放视频和音频,可以理解为一个随身听

  • AVPlayer的关联类:

    • AVAsset:一个抽象类,不能直接使用,代表一个要播放的资源。可以理解为一个磁带子类AVURLAsset是根据URL生成的包含媒体信息的资源对象。我们就是要通过这个类的代理实现音频的边播边下的

    • AVPlayerItem:可以理解为一个装在磁带盒子里的磁带

2. AVPlayer播放原理

  • 给播放器设置好想要它播放的URL

  • 播放器向URL所在的服务器发送请求,请求两个东西

    • 所需音频片段的起始offset

    • 所需的音频长度

  • 服务器根据请求的内容,返回数据

  • 播放器拿到数据拼装成文件

  • 播放器从拼装好的文件中,找出现在需要播放的片段,进行播放

3. 边播边下的原理

实现边下边播,其实就是手动实现AVPlayer的上列播放过程。

  • 当播放器需要预先缓存一些数据的时候,不让播放器直接向服务器发起请求,而是向我们自己写的某个类(暂且称之为播放器的秘书)发起缓存请求

  • 秘书根据播放器的缓存请求的请求内容,向服务器发起请求。

  • 服务器返回秘书所需的数据

  • 秘书把服务器返回的数据写进本地的缓存文件

  • 当需要播放某段声音的时候,向秘书发出播放请求索要这段音频文件

  • 秘书从本地的缓存文件中找到播放器播放请求所需片段,返回给播放器

  • 播放器拿到数据开心滴播放

  • 当整首歌都缓存完成以后,秘书需要把缓存文件拷贝一份,改个名字,这个文件就是我们所需要的本地持久化文件

  • 下次播放器再播放歌曲的时候,先判断下本地有木有这个名字的文件,有则播放本地文件,木有则向秘书要数据

技术实现

OK,边播边下的原理知道了,我们可以正式写代码了~建议先从文末链接处把Demo下载下来,对着Demo咱们慢慢道来~

1. 类

共需要三个类:

  • MusicPlayerManagerCEO。单例,负责整个工程所有的播放、暂停、下一曲、结束、判断应该播放本地文件还是从服务器拉数据之类的事情

  • RequestLoader:就是上文所说的秘书,负责给播放器提供播放所需的音频片段,以及找人向服务器索要数据

  • RequestTask秘书的小弟。负责和服务器连接、向服务器请求数据、把请求回来的数据写到本地缓存文件、把写完的缓存文件移到持久化目录去。所有脏活累活都是他做。

2. 方法

先从小弟说起

2.1.  RequestTask

2.1.0. 概说

如上文所说,小弟是负责做脏活累活的。 负责和服务器连接、向服务器请求数据、把请求回来的数据写到本地缓存文件、把写完的缓存文件移到持久化目录去

2.1.1. 初始化音频文件持久化文件夹 & 缓存文件
  1. private func _initialTmpFile() {
  2.     do { 
  3.      try NSFileManager.defaultManager().createDirectoryAtPath(StreamAudioConfig.audioDicPath, withIntermediateDirectories: true, attributes: nil) 
  4. catch { 
  5. print("creat dic false -- error:\(error)"
  6. }
  7.     if NSFileManager.defaultManager().fileExistsAtPath(StreamAudioConfig.tempPath) {
  8.         try! NSFileManager.defaultManager().removeItemAtPath(StreamAudioConfig.tempPath)
  9.     }
  10.     NSFileManager.defaultManager().createFileAtPath(StreamAudioConfig.tempPath, contents: nil, attributes: nil)
  11. }
2.1.2. 与服务器建立连接请求数据
  1. /**
  2.      连接服务器,请求数据(或拼range请求部分数据)(此方法中会将协议头修改为http)
  3.      - parameter offset: 请求位置
  4.      */
  5.     public func set(URL url: NSURL, offset: Int) {
  6.         func initialTmpFile() {
  7.             try! NSFileManager.defaultManager().removeItemAtPath(StreamAudioConfig.tempPath)
  8.             NSFileManager.defaultManager().createFileAtPath(StreamAudioConfig.tempPath, contents: nil, attributes: nil)
  9.         }
  10.         _updateFilePath(url)
  11.         self.url = url
  12.         self.offset = offset
  13.         //  如果建立第二次请求,则需初始化缓冲文件
  14.         if taskArr.count >= 1 {
  15.             initialTmpFile()
  16.         }
  17.         //  初始化已下载文件长度
  18.         downLoadingOffset = 0
  19.         //  把stream://xxx的头换成http://的头
  20.         let actualURLComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
  21.         actualURLComponents?.scheme = "http"
  22.         guard let URL = actualURLComponents?.URL else {return}
  23.         let request = NSMutableURLRequest(URL: URL, cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData, timeoutInterval: 20.0)
  24.         //  若非从头下载,且视频长度已知且大于零,则下载offset到videoLength的范围(拼request参数)
  25.         if offset > 0 && videoLength > 0 {
  26.             request.addValue("bytes=\(offset)-\(videoLength - 1)", forHTTPHeaderField: "Range")
  27.         }
  28.         connection?.cancel()
  29.         connection = NSURLConnection(request: request, delegate: self, startImmediately: false)
  30.         connection?.setDelegateQueue(NSOperationQueue.mainQueue())
  31.         connection?.start()
  32.     }
2.1.3. 响应服务器的Response头
  1. public func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
  2.         isFinishLoad = false
  3.         guard response is NSHTTPURLResponse else {return}
  4.         //  解析头部数据
  5.         let httpResponse = response as! NSHTTPURLResponse
  6.         let dic = httpResponse.allHeaderFields
  7.         let content = dic["Content-Range"] as? String
  8.         let array = content?.componentsSeparatedByString("/")
  9.         let length = array?.last
  10.         //  拿到真实长度
  11.         var videoLength = 0
  12.         if Int(length ?? "0") == 0 {
  13.             videoLength = Int(httpResponse.expectedContentLength)
  14.         } else {
  15.             videoLength = Int(length!)!
  16.         }
  17.         self.videoLength = videoLength
  18.         //TODO: 此处需要修改为真实数据格式 - 从字典中取
  19.         self.mimeType = "video/mp4"
  20.         //  回调
  21.         recieveVideoInfoHandler?(task: self, videoLength: videoLength, mimeType: mimeType!)
  22.         //  连接加入到任务数组中
  23.         taskArr.append(connection)
  24.         //  初始化文件传输句柄
  25.         fileHandle = NSFileHandle.init(forWritingAtPath: StreamAudioConfig.tempPath)
  26.     }
2.1.4. 处理服务器返回的数据 - 写入缓存文件中
  1.  public func connectionDidFinishLoading(connection: NSURLConnection) {
  2.         func tmpPersistence() {
  3.             isFinishLoad = true
  4.             let fileName = url?.lastPathComponent
  5. //            let movePath = audioDicPath.stringByAppendingPathComponent(fileName ?? "undefine.mp4")
  6.             let movePath = StreamAudioConfig.audioDicPath + "/\(fileName ?? "undefine.mp4")"
  7.             _ = try? NSFileManager.defaultManager().removeItemAtPath(movePath)
  8.             var isSuccessful = true
  9.             do { try NSFileManager.defaultManager().copyItemAtPath(StreamAudioConfig.tempPath, toPath: movePath) } catch {
  10.                 isSuccessful = false
  11.                 print("tmp文件持久化失败")
  12.             }
  13.             if isSuccessful {
  14.                 print("持久化文件成功!路径 - \(movePath)")
  15.             }
  16.         }
  17.         if taskArr.count < 2 {
  18.             tmpPersistence()
  19.         }
  20.         receiveVideoFinishHanlder?(task: self)
  21.     }
其他

其他方法包括断线重连以及公开一个cancel方法cancel掉和服务器的连接

2.2.  RequestTask

2.2.0. 概说

秘书要干的最主要的事情就是响应播放器老大的号令,所有方法都是围绕着播放器老大来的。秘书需要遵循AVAssetResourceLoaderDelegate协议才能被录用。

2.2.1. 代理方法,播放器需要缓存数据的时候,会调这个方法

这个方法其实是播放器在说:小秘呀,我想要这段音频文件。你能现在给我还是等等给我啊?
一定要返回:true,告诉播放器,我等等给你。
然后,立马找本地缓存文件里有木有这段数据,有把数据拿给播放器,如果木有,则派秘书的小弟向服务器要。
具体实现代码有点多,这里就不全部贴出来了。可以去看看文末的Demo记得赏颗星哟~

  1. /**
  2.      播放器问:是否应该等这requestResource加载完再说?
  3.      这里会出现很多个loadingRequest请求, 需要为每一次请求作出处理
  4.      - parameter resourceLoader: 资源管理器
  5.      - parameter loadingRequest: 每一小块数据的请求
  6.      - returns: 
  7.      */
  8.     public func resourceLoader(resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
  9.         //  添加请求到队列
  10.         pendingRequset.append(loadingRequest)
  11.         //  处理请求
  12.         _dealWithLoadingRequest(loadingRequest)
  13.         print("----\(loadingRequest)")
  14.         return true
  15.     }
2.2.2. 代理方法,播放器关闭了下载请求
  1.  /**
  2.      播放器关闭了下载请求
  3.      播放器关闭一个旧请求,都会发起一到多个新请求,除非已经播放完毕了
  4.      - parameter resourceLoader: 资源管理器
  5.      - parameter loadingRequest: 待关请求
  6.      */
  7.     public func resourceLoader(resourceLoader: AVAssetResourceLoader, didCancelLoadingRequest loadingRequest: AVAssetResourceLoadingRequest) {
  8.         guard let index = pendingRequset.indexOf(loadingRequest) else {return}
  9.         pendingRequset.removeAtIndex(index)
  10.     }

2.3.  MusicPlayerManager

2.3.0. 概说

负责调度所有播放器的,负责App中的一切涉及音频播放的事件
唔。。犯个小懒。。代码直接贴上来咯~要赶不上楼下的538路公交啦~~谢谢大家体谅哦~

  1. public class MusicPlayerManager: NSObject {
  2.     //  public var status
  3.     public var currentURL: NSURL? {
  4.         get {
  5.             guard let currentIndex = currentIndex, musicURLList = musicURLList where currentIndex < musicURLList.count else {return nil}
  6.             return musicURLList[currentIndex]
  7.         }
  8.     }
  9.     /**播放状态,用于需要获取播放器状态的地方KVO*/
  10.     public var status: ManagerStatus = .Non
  11.     /**播放进度*/
  12.     public var progress: CGFloat {
  13.         get {
  14.             if playDuration > 0 {
  15.                 let progress = playTime / playDuration
  16.                 return progress
  17.             } else {
  18.                 return 0
  19.             }
  20.         }
  21.     }
  22.     /**已播放时长*/
  23.     public var playTime: CGFloat = 0
  24.     /**总时长*/
  25.     public var playDuration: CGFloat = CGFloat.max
  26.     /**缓冲时长*/
  27.     public var tmpTime: CGFloat = 0
  28.     public var playEndConsul: (()->())?
  29.     /**强引用控制器,防止被销毁*/
  30.     public var currentController: UIViewController?
  31.     //  private status
  32.     private var currentIndex: Int?
  33.     private var currentItem: AVPlayerItem? {
  34.         get {
  35.             if let currentURL = currentURL {
  36.                 let item = getPlayerItem(withURL: currentURL)
  37.                 return item
  38.             } else {
  39.                 return nil
  40.             }
  41.         }
  42.     }
  43.     private var musicURLList: [NSURL]?
  44.     //  basic element
  45.     public var player: AVPlayer?
  46.     private var playerStatusObserver: NSObject?
  47.     private var resourceLoader: RequestLoader = RequestLoader()
  48.     private var currentAsset: AVURLAsset?
  49.     private var progressCallBack: ((tmpProgress: Float?, playProgress: Float?)->())?
  50.     public class var sharedInstance: MusicPlayerManager {
  51.         struct Singleton {
  52.             static let instance = MusicPlayerManager()
  53.         }
  54.         //  后台播放
  55.         let session = AVAudioSession.sharedInstance()
  56.         do { try session.setActive(true) } catch { print(error) }
  57.         do { try session.setCategory(AVAudioSessionCategoryPlayback) } catch { print(error) }
  58.         return Singleton.instance
  59.     }
  60.     public enum ManagerStatus {
  61.         case Non, LoadSongInfo, ReadyToPlay, Play, Pause, Stop
  62.     }
  63. }
  64. // MARK: - basic public funcs
  65. extension MusicPlayerManager {
  66.     /**
  67.      开始播放
  68.      */
  69.     public func play(musicURL: NSURL?) {
  70.         guard let musicURL = musicURL else {return}
  71.         if let index = getIndexOfMusic(music: musicURL) {   //   歌曲在队列中,则按顺序播放
  72.             currentIndex = index
  73.         } else {
  74.             putMusicToArray(music: musicURL)
  75.             currentIndex = 0
  76.         }
  77.         playMusicWithCurrentIndex()
  78.     }
  79.     public func play(musicURL: NSURL?, callBack: ((tmpProgress: Float?, playProgress: Float?)->())?) {
  80.         play(musicURL)
  81.         progressCallBack = callBack
  82.     }
  83.     public func next() {
  84.         currentIndex = getNextIndex()
  85.         playMusicWithCurrentIndex()
  86.     }
  87.     public func previous() {
  88.         currentIndex = getPreviousIndex()
  89.         playMusicWithCurrentIndex()
  90.     }
  91.     /**
  92.      继续
  93.      */
  94.     public func goOn() {
  95.         player?.rate = 1
  96.     }
  97.     /**
  98.      暂停 - 可继续
  99.      */
  100.     public func pause() {
  101.         player?.rate = 0
  102.     }
  103.     /**
  104.      停止 - 无法继续
  105.      */
  106.     public func stop() {
  107.         endPlay()
  108.     }
  109. }
  110. // MARK: - private funcs
  111. extension MusicPlayerManager {
  112.     private func putMusicToArray(music URL: NSURL) {
  113.         if musicURLList == nil {
  114.             musicURLList = [URL]
  115.         } else {
  116.             musicURLList!.insert(URL, atIndex: 0)
  117.         }
  118.     }
  119.     private func getIndexOfMusic(music URL: NSURL) -> Int? {
  120.         let index = musicURLList?.indexOf(URL)
  121.         return index
  122.     }
  123.     private func getNextIndex() -> Int? {
  124.         if let musicURLList = musicURLList where musicURLList.count > 0 {
  125.             if let currentIndex = currentIndex where currentIndex + 1 < musicURLList.count {
  126.                 return currentIndex + 1
  127.             } else {
  128.                 return 0
  129.             }
  130.         } else {
  131.             return nil
  132.         }
  133.     }
  134.     private func getPreviousIndex() -> Int? {
  135.         if let currentIndex = currentIndex {
  136.             if currentIndex - 1 >= 0 {
  137.                 return currentIndex - 1
  138.             } else {
  139.                 return musicURLList?.count ?? 1 - 1
  140.             }
  141.         } else {
  142.             return nil
  143.         }
  144.     }
  145.     /**
  146.      从头播放音乐列表
  147.      */
  148.     private func replayMusicList() {
  149.         guard let musicURLList = musicURLList where musicURLList.count > 0 else {return}
  150.         currentIndex = 0
  151.         playMusicWithCurrentIndex()
  152.     }
  153.     /**
  154.      播放当前音乐
  155.      */
  156.     private func playMusicWithCurrentIndex() {
  157.         guard let currentURL = currentURL else {return}
  158.         //  结束上一首
  159.         endPlay()
  160.         player = AVPlayer(playerItem: getPlayerItem(withURL: currentURL))
  161.         observePlayingItem()
  162.     }
  163.     /**
  164.      本地不存在,返回nil,否则返回本地URL
  165.      */
  166.     private func getLocationFilePath(url: NSURL) -> NSURL? {
  167.         let fileName = url.lastPathComponent
  168.         let path = StreamAudioConfig.audioDicPath + "/\(fileName ?? "tmp.mp4")"
  169.         if NSFileManager.defaultManager().fileExistsAtPath(path) {
  170.             let url = NSURL.init(fileURLWithPath: path)
  171.             return url
  172.         } else {
  173.             return nil
  174.         }
  175.     }
  176.     private func getPlayerItem(withURL musicURL: NSURL) -> AVPlayerItem {
  177.         if let locationFile = getLocationFilePath(musicURL) {
  178.             let item = AVPlayerItem(URL: locationFile)
  179.             return item
  180.         } else {
  181.             let playURL = resourceLoader.getURL(url: musicURL)!  //  转换协议头
  182.             let asset = AVURLAsset(URL: playURL)
  183.             currentAsset = asset
  184.             asset.resourceLoader.setDelegate(resourceLoader, queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0))
  185.             let item = AVPlayerItem(asset: asset)
  186.             return item
  187.         }
  188.     }
  189.     private func setupPlayer(withURL musicURL: NSURL) {
  190.         let songItem = getPlayerItem(withURL: musicURL)
  191.         player = AVPlayer(playerItem: songItem)
  192.     }
  193.     private func playerPlay() {
  194.         player?.play()
  195.     }
  196.     private func endPlay() {
  197.         status = ManagerStatus.Stop
  198.         player?.rate = 0
  199.         removeObserForPlayingItem()
  200.         player?.replaceCurrentItemWithPlayerItem(nil)
  201.         resourceLoader.cancel()
  202.         currentAsset?.resourceLoader.setDelegate(nil, queue: nil)
  203.         progressCallBack = nil
  204.         resourceLoader = RequestLoader()
  205.         playDuration = 0
  206.         playTime = 0
  207.         playEndConsul?()
  208.         player = nil
  209.     }
  210. }
  211. extension MusicPlayerManager {
  212.     public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) {
  213.         guard object is AVPlayerItem else {return}
  214.         let item = object as! AVPlayerItem
  215.         if keyPath == "status" {
  216.             if item.status == AVPlayerItemStatus.ReadyToPlay {
  217.                 status = .ReadyToPlay
  218.                 print("ReadyToPlay")
  219.                 let duration = item.duration
  220.                 playerPlay()
  221.                 print(duration)
  222.             } else if item.status == AVPlayerItemStatus.Failed {
  223.                 status = .Stop
  224.                 print("Failed")
  225.                 stop()
  226.             }
  227.         } else if keyPath == "loadedTimeRanges" {
  228.             let array = item.loadedTimeRanges
  229.             guard let timeRange = array.first?.CMTimeRangeValue else {return}  //  缓冲时间范围
  230.             let totalBuffer = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration)    //  当前缓冲长度
  231.             tmpTime = CGFloat(tmpTime)
  232.             print("共缓冲 - \(totalBuffer)")
  233.             let tmpProgress = tmpTime / playDuration
  234.             progressCallBack?(tmpProgress: Float(tmpProgress), playProgress: nil)
  235.         }
  236.     }
  237.     private func observePlayingItem() {
  238.         guard let currentItem = self.player?.currentItem else {return}
  239.         //  KVO监听正在播放的对象状态变化
  240.         currentItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: nil)
  241.         //  监听player播放情况
  242.         playerStatusObserver = player?.addPeriodicTimeObserverForInterval(CMTimeMake(11), queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), usingBlock: { [weak self] (time) in
  243.             guard let `self` = self else {return}
  244.             //  获取当前播放时间
  245.             self.status = .Play
  246.             let currentTime = CMTimeGetSeconds(time)
  247.             let totalTime = CMTimeGetSeconds(currentItem.duration)
  248.             self.playDuration = CGFloat(totalTime)
  249.             self.playTime = CGFloat(currentTime)
  250.             print("current time ---- \(currentTime) ---- tutalTime ---- \(totalTime)")
  251.             self.progressCallBack?(tmpProgress: nil, playProgress: Float(self.progress))
  252.             if totalTime - currentTime < 0.1 {
  253.                 self.endPlay()
  254.             }
  255.             }) as? NSObject
  256.         //  监听缓存情况
  257.         currentItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.New, context: nil)
  258.     }
  259.     private func removeObserForPlayingItem() {
  260.         guard let currentItem = self.player?.currentItem else {return}
  261.         currentItem.removeObserver(self, forKeyPath: "status")
  262.         if playerStatusObserver != nil {
  263.             player?.removeTimeObserver(playerStatusObserver!)
  264.             playerStatusObserver = nil
  265.         }
  266.         currentItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
  267.     }
  268. }
  269. public struct StreamAudioConfig {
  270.     static let audioDicPath: String = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last! + "/streamAudio"  //  缓冲文件夹
  271.     static let tempPath: String = audioDicPath + "/temp.mp4"    //  缓冲文件路径 - 非持久化文件路径 - 当前逻辑下,有且只有一个缓冲文件
  272. }

 

转载于:https://www.cnblogs.com/ruixin-jia/p/5851257.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/盐析白兔/article/detail/308887
推荐阅读
相关标签
  

闽ICP备14008679号