iOS開發:AVPlayer實現流音頻邊播邊存

bright1001 8年前發布 | 7K 次閱讀 iOS開發 移動開發

概述

1. AVPlayer簡介

  • AVPlayer存在于AVFoundation中,可以播放視頻和音頻,可以理解為一個 隨身聽

  • AVPlayer的關聯類:

    • AVAsset:一個抽象類,不能直接使用,代表一個要播放的資源。可以理解為一個 磁帶 子類AVURLAsset是根據URL生成的包含媒體信息的資源對象。 我們就是要通過這個類的代理實現音頻的邊播邊下的

    • AVPlayerItem:可以理解為一個 裝在磁帶盒子里的磁帶

    </li> </ul>

    2. AVPlayer播放原理

    • 給播放器設置好想要它播放的URL

    • 播放器向URL所在的服務器發送請求,請求兩個東西

      • 所需音頻片段的起始offset

      • 所需的音頻長度

      </li>
    • 服務器根據請求的內容,返回數據

    • 播放器拿到數據拼裝成文件

    • 播放器從拼裝好的文件中,找出現在需要播放的片段,進行播放

    • </ul>

      3. 邊播邊下的原理

      實現邊下邊播,其實就是手動實現AVPlayer的上列播放過程。

      • 當播放器需要預先緩存一些數據的時候,不讓播放器直接向服務器發起請求,而是向我們自己寫的某個類(暫且稱之為 播放器的秘書 )發起 緩存請求

      • 秘書根據播放器的 緩存請求 的請求內容,向服務器發起請求。

      • 服務器返回 秘書 所需的數據

      • 秘書把服務器返回的數據寫進本地的 緩存文件

      • 當需要播放某段聲音的時候,向 秘書 發出 播放請求 索要這段音頻文件

      • 秘書從本地的緩存文件中找到播放器 播放請求 所需片段,返回給播放器

      • 播放器拿到數據開心滴播放

      • 當整首歌都緩存完成以后,秘書需要把緩存文件拷貝一份,改個名字,這個文件就是我們所需要的本地持久化文件

      • 下次播放器再播放歌曲的時候,先判斷下本地有木有這個名字的文件,有則播放本地文件,木有則向秘書要數據

      技術實現

      OK,邊播邊下的原理知道了,我們可以正式寫代碼了~建議先從文末鏈接處把Demo下載下來,對著Demo咱們慢慢道來~

      1. 類

      共需要三個類:

      • MusicPlayerManager: CEO 。單例,負責整個工程所有的播放、暫停、下一曲、結束、判斷應該播放本地文件還是從服務器拉數據之類的事情

      • RequestLoader:就是上文所說的 秘書 ,負責給播放器提供播放所需的音頻片段,以及找人向服務器索要數據

      • RequestTask: 秘書的小弟 。負責和服務器連接、向服務器請求數據、把請求回來的數據寫到本地緩存文件、把寫完的緩存文件移到持久化目錄去。所有臟活累活都是他做。

      2. 方法

      先從小弟說起

      2.1.  RequestTask

      2.1.0. 概說

      如上文所說,小弟是負責做臟活累活的。 負責和服務器連接、向服務器請求數據、把請求回來的數據寫到本地緩存文件、把寫完的緩存文件移到持久化目錄去

      2.1.1. 初始化音頻文件持久化文件夾 & 緩存文件

      private func _initialTmpFile() {
          do { 
              try NSFileManager.defaultManager().createDirectoryAtPath(StreamAudioConfig.audioDicPath, withIntermediateDirectories: true, attributes: nil) 
          } catch { 
          print("creat dic false -- error:\(error)") 
          }
          if NSFileManager.defaultManager().fileExistsAtPath(StreamAudioConfig.tempPath) {
              try! NSFileManager.defaultManager().removeItemAtPath(StreamAudioConfig.tempPath)
          }
          NSFileManager.defaultManager().createFileAtPath(StreamAudioConfig.tempPath, contents: nil, attributes: nil)
      }

      2.1.2. 與服務器建立連接請求數據

      /**
           連接服務器,請求數據(或拼range請求部分數據)(此方法中會將協議頭修改為http)

       - parameter offset: 請求位置
       */
      public func set(URL url: NSURL, offset: Int) {
      
          func initialTmpFile() {
              try! NSFileManager.defaultManager().removeItemAtPath(StreamAudioConfig.tempPath)
              NSFileManager.defaultManager().createFileAtPath(StreamAudioConfig.tempPath, contents: nil, attributes: nil)
          }
          _updateFilePath(url)
          self.url = url
          self.offset = offset
      
          //  如果建立第二次請求,則需初始化緩沖文件
          if taskArr.count >= 1 {
              initialTmpFile()
          }
      
          //  初始化已下載文件長度
          downLoadingOffset = 0
      
          //  把stream://xxx的頭換成http://的頭
          let actualURLComponents = NSURLComponents(URL: url, resolvingAgainstBaseURL: false)
          actualURLComponents?.scheme = "http"
          guard let URL = actualURLComponents?.URL else {return}
          let request = NSMutableURLRequest(URL: URL, cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData, timeoutInterval: 20.0)
      
          //  若非從頭下載,且視頻長度已知且大于零,則下載offset到videoLength的范圍(拼request參數)
          if offset > 0 && videoLength > 0 {
              request.addValue("bytes=\(offset)-\(videoLength - 1)", forHTTPHeaderField: "Range")
          }
      
          connection?.cancel()
          connection = NSURLConnection(request: request, delegate: self, startImmediately: false)
          connection?.setDelegateQueue(NSOperationQueue.mainQueue())
          connection?.start()
      }</code></pre> 
      

      2.1.3. 響應服務器的Response頭

      public func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
              isFinishLoad = false
              guard response is NSHTTPURLResponse else {return}
              //  解析頭部數據
              let httpResponse = response as! NSHTTPURLResponse
              let dic = httpResponse.allHeaderFields
              let content = dic["Content-Range"] as? String
              let array = content?.componentsSeparatedByString("/")
              let length = array?.last
              //  拿到真實長度
              var videoLength = 0
              if Int(length ?? "0") == 0 {
                  videoLength = Int(httpResponse.expectedContentLength)
              } else {
                  videoLength = Int(length!)!
              }
      
              self.videoLength = videoLength
              //TODO: 此處需要修改為真實數據格式 - 從字典中取
              self.mimeType = "video/mp4"
              //  回調
              recieveVideoInfoHandler?(task: self, videoLength: videoLength, mimeType: mimeType!)
              //  連接加入到任務數組中
              taskArr.append(connection)
              //  初始化文件傳輸句柄
              fileHandle = NSFileHandle.init(forWritingAtPath: StreamAudioConfig.tempPath)
          }

      2.1.4. 處理服務器返回的數據 - 寫入緩存文件中

       public func connectionDidFinishLoading(connection: NSURLConnection) {
              func tmpPersistence() {
                  isFinishLoad = true
                  let fileName = url?.lastPathComponent
      //            let movePath = audioDicPath.stringByAppendingPathComponent(fileName ?? "undefine.mp4")
                  let movePath = StreamAudioConfig.audioDicPath + "/\(fileName ?? "undefine.mp4")"
                  _ = try? NSFileManager.defaultManager().removeItemAtPath(movePath)
      
                  var isSuccessful = true
                  do { try NSFileManager.defaultManager().copyItemAtPath(StreamAudioConfig.tempPath, toPath: movePath) } catch {
                      isSuccessful = false
                      print("tmp文件持久化失敗")
                  }
                  if isSuccessful {
                      print("持久化文件成功!路徑 - \(movePath)")
                  }
              }
      
              if taskArr.count < 2 {
                  tmpPersistence()
              }
      
              receiveVideoFinishHanlder?(task: self)
          }

      其他

      其他方法包括斷線重連以及公開一個cancel方法cancel掉和服務器的連接

      2.2.  RequestTask

      2.2.0. 概說

      秘書要干的最主要的事情就是響應播放器老大的號令,所有方法都是圍繞著播放器老大來的。秘書需要遵循AVAssetResourceLoaderDelegate協議才能被錄用。

      2.2.1. 代理方法,播放器需要緩存數據的時候,會調這個方法

      這個方法其實是播放器在說:小秘呀,我想要這段音頻文件。你能現在給我還是等等給我啊?

      一定要返回:true,告訴播放器,我等等給你。

      然后,立馬找本地緩存文件里有木有這段數據,有把數據拿給播放器,如果木有,則派秘書的小弟向服務器要。

      具體實現代碼有點多,這里就不全部貼出來了。可以去看看 文末的Demo 記得賞顆星喲~

      /**
           播放器問:是否應該等這requestResource加載完再說?
           這里會出現很多個loadingRequest請求, 需要為每一次請求作出處理
      
           - parameter resourceLoader: 資源管理器
           - parameter loadingRequest: 每一小塊數據的請求
      
           - returns: 
           */
          public func resourceLoader(resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
              //  添加請求到隊列
              pendingRequset.append(loadingRequest)
              //  處理請求
              _dealWithLoadingRequest(loadingRequest)
              print("----\(loadingRequest)")
              return true
          }

      2.2.2. 代理方法,播放器關閉了下載請求

       /**
           播放器關閉了下載請求
           播放器關閉一個舊請求,都會發起一到多個新請求,除非已經播放完畢了
      
           - parameter resourceLoader: 資源管理器
           - parameter loadingRequest: 待關請求
           */
          public func resourceLoader(resourceLoader: AVAssetResourceLoader, didCancelLoadingRequest loadingRequest: AVAssetResourceLoadingRequest) {
              guard let index = pendingRequset.indexOf(loadingRequest) else {return}
              pendingRequset.removeAtIndex(index)
          }

      2.3.  MusicPlayerManager

      2.3.0. 概說

      負責調度所有播放器的,負責App中的一切涉及音頻播放的事件

      唔。。犯個小懶。。代碼直接貼上來咯~要趕不上樓下的538路公交啦~~謝謝大家體諒哦~

      public class MusicPlayerManager: NSObject {
      
      
          //  public var status
      
          public var currentURL: NSURL? {
              get {
                  guard let currentIndex = currentIndex, musicURLList = musicURLList where currentIndex < musicURLList.count else {return nil}
                  return musicURLList[currentIndex]
              }
          }
      
          /**播放狀態,用于需要獲取播放器狀態的地方KVO*/
          public var status: ManagerStatus = .Non
          /**播放進度*/
          public var progress: CGFloat {
              get {
                  if playDuration > 0 {
                      let progress = playTime / playDuration
                      return progress
                  } else {
                      return 0
                  }
              }
          }
          /**已播放時長*/
          public var playTime: CGFloat = 0
          /**總時長*/
          public var playDuration: CGFloat = CGFloat.max
          /**緩沖時長*/
          public var tmpTime: CGFloat = 0
      
          public var playEndConsul: (()->())?
          /**強引用控制器,防止被銷毀*/
          public var currentController: UIViewController?
      
          //  private status
          private var currentIndex: Int?
          private var currentItem: AVPlayerItem? {
              get {
                  if let currentURL = currentURL {
                      let item = getPlayerItem(withURL: currentURL)
                      return item
                  } else {
                      return nil
                  }
              }
          }
      
          private var musicURLList: [NSURL]?
      
          //  basic element
          public var player: AVPlayer?
      
          private var playerStatusObserver: NSObject?
          private var resourceLoader: RequestLoader = RequestLoader()
          private var currentAsset: AVURLAsset?
          private var progressCallBack: ((tmpProgress: Float?, playProgress: Float?)->())?
      
          public class var sharedInstance: MusicPlayerManager {
              struct Singleton {
                  static let instance = MusicPlayerManager()
              }
              //  后臺播放
              let session = AVAudioSession.sharedInstance()
              do { try session.setActive(true) } catch { print(error) }
              do { try session.setCategory(AVAudioSessionCategoryPlayback) } catch { print(error) }
              return Singleton.instance
          }
      
          public enum ManagerStatus {
              case Non, LoadSongInfo, ReadyToPlay, Play, Pause, Stop
          }
      }
      
      // MARK: - basic public funcs
      extension MusicPlayerManager {
          /**
           開始播放
           */
          public func play(musicURL: NSURL?) {
              guard let musicURL = musicURL else {return}
              if let index = getIndexOfMusic(music: musicURL) {   //   歌曲在隊列中,則按順序播放
                  currentIndex = index
              } else {
                  putMusicToArray(music: musicURL)
                  currentIndex = 0
              }
              playMusicWithCurrentIndex()
          }
      
          public func play(musicURL: NSURL?, callBack: ((tmpProgress: Float?, playProgress: Float?)->())?) {
              play(musicURL)
              progressCallBack = callBack
          }
      
          public func next() {
              currentIndex = getNextIndex()
              playMusicWithCurrentIndex()
          }
      
          public func previous() {
              currentIndex = getPreviousIndex()
              playMusicWithCurrentIndex()
          }
          /**
           繼續
           */
          public func goOn() {
              player?.rate = 1
          }
          /**
           暫停 - 可繼續
           */
          public func pause() {
              player?.rate = 0
          }
          /**
           停止 - 無法繼續
           */
          public func stop() {
              endPlay()
          }
      }
      
      // MARK: - private funcs
      extension MusicPlayerManager {
      
          private func putMusicToArray(music URL: NSURL) {
              if musicURLList == nil {
                  musicURLList = [URL]
              } else {
                  musicURLList!.insert(URL, atIndex: 0)
              }
          }
      
          private func getIndexOfMusic(music URL: NSURL) -> Int? {
              let index = musicURLList?.indexOf(URL)
              return index
          }
      
          private func getNextIndex() -> Int? {
              if let musicURLList = musicURLList where musicURLList.count > 0 {
                  if let currentIndex = currentIndex where currentIndex + 1 < musicURLList.count {
                      return currentIndex + 1
                  } else {
                      return 0
                  }
              } else {
                  return nil
              }
          }
      
          private func getPreviousIndex() -> Int? {
              if let currentIndex = currentIndex {
                  if currentIndex - 1 >= 0 {
                      return currentIndex - 1
                  } else {
                      return musicURLList?.count ?? 1 - 1
                  }
              } else {
                  return nil
              }
          }
      
          /**
           從頭播放音樂列表
           */
          private func replayMusicList() {
              guard let musicURLList = musicURLList where musicURLList.count > 0 else {return}
              currentIndex = 0
              playMusicWithCurrentIndex()
          }
          /**
           播放當前音樂
           */
          private func playMusicWithCurrentIndex() {
              guard let currentURL = currentURL else {return}
              //  結束上一首
              endPlay()
              player = AVPlayer(playerItem: getPlayerItem(withURL: currentURL))
              observePlayingItem()
          }
          /**
           本地不存在,返回nil,否則返回本地URL
           */
          private func getLocationFilePath(url: NSURL) -> NSURL? {
              let fileName = url.lastPathComponent
              let path = StreamAudioConfig.audioDicPath + "/\(fileName ?? "tmp.mp4")"
              if NSFileManager.defaultManager().fileExistsAtPath(path) {
                  let url = NSURL.init(fileURLWithPath: path)
                  return url
              } else {
                  return nil
              }
          }
      
          private func getPlayerItem(withURL musicURL: NSURL) -> AVPlayerItem {
      
              if let locationFile = getLocationFilePath(musicURL) {
                  let item = AVPlayerItem(URL: locationFile)
                  return item
              } else {
                  let playURL = resourceLoader.getURL(url: musicURL)!  //  轉換協議頭
                  let asset = AVURLAsset(URL: playURL)
                  currentAsset = asset
                  asset.resourceLoader.setDelegate(resourceLoader, queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0))
                  let item = AVPlayerItem(asset: asset)
                  return item
              }
          }
      
          private func setupPlayer(withURL musicURL: NSURL) {
              let songItem = getPlayerItem(withURL: musicURL)
              player = AVPlayer(playerItem: songItem)
          }
      
          private func playerPlay() {
              player?.play()
          }
      
          private func endPlay() {
              status = ManagerStatus.Stop
              player?.rate = 0
              removeObserForPlayingItem()
              player?.replaceCurrentItemWithPlayerItem(nil)
              resourceLoader.cancel()
              currentAsset?.resourceLoader.setDelegate(nil, queue: nil)
      
              progressCallBack = nil
              resourceLoader = RequestLoader()
              playDuration = 0
              playTime = 0
              playEndConsul?()
              player = nil
          }
      }
      
      extension MusicPlayerManager {
          public override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) {
              guard object is AVPlayerItem else {return}
              let item = object as! AVPlayerItem
              if keyPath == "status" {
                  if item.status == AVPlayerItemStatus.ReadyToPlay {
                      status = .ReadyToPlay
                      print("ReadyToPlay")
                      let duration = item.duration
                      playerPlay()
                      print(duration)
                  } else if item.status == AVPlayerItemStatus.Failed {
                      status = .Stop
                      print("Failed")
                      stop()
                  }
              } else if keyPath == "loadedTimeRanges" {
                  let array = item.loadedTimeRanges
                  guard let timeRange = array.first?.CMTimeRangeValue else {return}  //  緩沖時間范圍
                  let totalBuffer = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration)    //  當前緩沖長度
                  tmpTime = CGFloat(tmpTime)
                  print("共緩沖 - \(totalBuffer)")
                  let tmpProgress = tmpTime / playDuration
                  progressCallBack?(tmpProgress: Float(tmpProgress), playProgress: nil)
              }
          }
      
          private func observePlayingItem() {
              guard let currentItem = self.player?.currentItem else {return}
              //  KVO監聽正在播放的對象狀態變化
              currentItem.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.New, context: nil)
              //  監聽player播放情況
              playerStatusObserver = player?.addPeriodicTimeObserverForInterval(CMTimeMake(1, 1), queue: dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), usingBlock: { [weak self] (time) in
                  guard let `self` = self else {return}
                  //  獲取當前播放時間
                  self.status = .Play
                  let currentTime = CMTimeGetSeconds(time)
                  let totalTime = CMTimeGetSeconds(currentItem.duration)
                  self.playDuration = CGFloat(totalTime)
                  self.playTime = CGFloat(currentTime)
                  print("current time ---- \(currentTime) ---- tutalTime ---- \(totalTime)")
                  self.progressCallBack?(tmpProgress: nil, playProgress: Float(self.progress))
                  if totalTime - currentTime < 0.1 {
                      self.endPlay()
                  }
                  }) as? NSObject
              //  監聽緩存情況
              currentItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: NSKeyValueObservingOptions.New, context: nil)
          }
      
          private func removeObserForPlayingItem() {
              guard let currentItem = self.player?.currentItem else {return}
              currentItem.removeObserver(self, forKeyPath: "status")
              if playerStatusObserver != nil {
                  player?.removeTimeObserver(playerStatusObserver!)
                  playerStatusObserver = nil
              }
              currentItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
          }
      }
      
      public struct StreamAudioConfig {
          static let audioDicPath: String = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last! + "/streamAudio"  //  緩沖文件夾
          static let tempPath: String = audioDicPath + "/temp.mp4"    //  緩沖文件路徑 - 非持久化文件路徑 - 當前邏輯下,有且只有一個緩沖文件
      
      }

       

      來自:http://www.cocoachina.com/ios/20160901/17456.html

       

 本文由用戶 bright1001 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!