首页 > 代码库 > iOS开发 - AVPlayer实现流音频边播边存

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

边播边下有三套左右实现思路,本文使用AVPlayer + AVURLAsset实现。

概述

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. 初始化音频文件持久化文件夹 & 缓存文件

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()    }

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 connection(connection: NSURLConnection, didReceiveData data: NSData) {        //  寻址到文件末尾        self.fileHandle?.seekToEndOfFile()        self.fileHandle?.writeData(data)        self.downLoadingOffset += data.length        self.receiveVideoDataHandler?(task: self)//        print("线程 - \(NSThread.currentThread())")        //  注意,这里用子线程有问题        let queue = dispatch_queue_create("com.azen.taskConnect", DISPATCH_QUEUE_SERIAL)        dispatch_async(queue) {//            //  寻址到文件末尾//            self.fileHandle?.seekToEndOfFile()//            self.fileHandle?.writeData(data)//            self.downLoadingOffset += data.length//            self.receiveVideoDataHandler?(task: self)//            let thread = NSThread.currentThread()//            print("线程 - \(thread)")        }

2.1.5. 服务器文件返回完毕,把缓存文件放入持久化文件夹

    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: <#return value description#>     */    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 funcsextension 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 funcsextension 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<Void>) {        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"    //  缓冲文件路径 - 非持久化文件路径 - 当前逻辑下,有且只有一个缓冲文件}

iOS音频边播边下Demo,戳这里~

 

本人学习收藏

文/Azen(简书作者)
原文链接:http://www.jianshu.com/p/4f586d63a532
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

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