AVPlayer实现流音频边播边存

正儿八经学iOS系列 - AVPlayer实现流音频边播边存

字数771 阅读538评论6 喜欢5

Collection/Bookmark/Share for width under 768px

边播边下有三套左右实现思路,本文使用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 { tryNSFileManager.defaultManager().createDirectoryAtPath(StreamAudioConfig.audioDicPath, withIntermediateDirectories: true, attributes: nil) } catch {print("creat dic false -- error:\(error)") }

        ifNSFileManager.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 funcset(URL url: NSURL, offset: Int) {


        funcinitialTmpFile() {

            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 letURL = actualURLComponents?.URLelse {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 funcconnection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {

        isFinishLoad = false

        guard response isNSHTTPURLResponse else {return}

        //  解析头部数据

        let httpResponse = responseas! NSHTTPURLResponse

        let dic = httpResponse.allHeaderFields

        let content = dic["Content-Range"]as? String

        let array = content?.componentsSeparatedByString("/")

        let length = array?.last

        //  拿到真实长度

        var videoLength =0

        ifInt(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 funcconnection(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 funcconnectionDidFinishLoading(connection: NSURLConnection) {

        functmpPersistence() {

            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 { tryNSFileManager.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 funcresourceLoader(resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) ->Bool {

        //  添加请求到队列

        pendingRequset.append(loadingRequest)

        //  处理请求

        _dealWithLoadingRequest(loadingRequest)

        print("----\(loadingRequest)")

        return true

    }

2.2.2. 代理方法,播放器关闭了下载请求

    /**

     播放器关闭了下载请求

     播放器关闭一个旧请求,都会发起一到多个新请求,除非已经播放完毕了


     - parameter resourceLoader: 资源管理器

     - parameter loadingRequest: 待关请求

     */

    public funcresourceLoader(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 classMusicPlayerManager: NSObject {



    //  public var status


    public var currentURL:NSURL? {

        get {

            guard let currentIndex = currentIndex, musicURLList = musicURLListwhere currentIndex < musicURLList.countelse {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 {

                return0

            }

        }

    }

    /**已播放时长*/

    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 {

            iflet 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 classvar sharedInstance:MusicPlayerManager {

        structSingleton {

            staticlet instance = MusicPlayerManager()

        }

        //  后台播放

        let session =AVAudioSession.sharedInstance()

        do { try session.setActive(true) } catch {print(error) }

        do { try session.setCategory(AVAudioSessionCategoryPlayback) } catch {print(error) }

        returnSingleton.instance

    }


    public enumManagerStatus {

        caseNon, LoadSongInfo,ReadyToPlay, Play,Pause, Stop

    }

}


// MARK: - basic public funcs

extension MusicPlayerManager {

    /**

     开始播放

     */

    public funcplay(musicURL: NSURL?) {

        guard let musicURL = musicURLelse {return}

        iflet index = getIndexOfMusic(music: musicURL) {  //   歌曲在队列中,则按顺序播放

            currentIndex = index

        } else {

            putMusicToArray(music: musicURL)

            currentIndex = 0

        }

        playMusicWithCurrentIndex()

    }


    public funcplay(musicURL: NSURL?, callBack: ((tmpProgress: Float?, playProgress: Float?)->())?) {

        play(musicURL)

        progressCallBack = callBack

    }


    public funcnext() {

        currentIndex = getNextIndex()

        playMusicWithCurrentIndex()

    }


    public funcprevious() {

        currentIndex = getPreviousIndex()

        playMusicWithCurrentIndex()

    }

    /**

     继续

     */

    public funcgoOn() {

        player?.rate = 1

    }

    /**

     暂停 - 可继续

     */

    public funcpause() {

        player?.rate = 0

    }

    /**

     停止 - 无法继续

     */

    public funcstop() {

        endPlay()

    }

}


// MARK: - private funcs

extension MusicPlayerManager {


    private funcputMusicToArray(music URL: NSURL) {

        if musicURLList == nil {

            musicURLList = [URL]

        } else {

            musicURLList!.insert(URL, atIndex:0)

        }

    }


    private funcgetIndexOfMusic(music URL: NSURL) ->Int? {

        let index = musicURLList?.indexOf(URL)

        return index

    }


    private funcgetNextIndex() -> Int? {

        iflet musicURLList = musicURLList where musicURLList.count >0 {

            iflet currentIndex = currentIndex where currentIndex + 1 < musicURLList.count {

                return currentIndex +1

            } else {

                return0

            }

        } else {

            return nil

        }

    }


    private funcgetPreviousIndex() -> Int? {

        iflet currentIndex = currentIndex {

            if currentIndex -1 >= 0 {

                return currentIndex -1

            } else {

                return musicURLList?.count ??1 - 1

            }

        } else {

            return nil

        }

    }


    /**

     从头播放音乐列表

     */

    private funcreplayMusicList() {

        guard let musicURLList = musicURLListwhere musicURLList.count >0 else {return}

        currentIndex = 0

        playMusicWithCurrentIndex()

    }

    /**

     播放当前音乐

     */

    private funcplayMusicWithCurrentIndex() {

        guard let currentURL = currentURLelse {return}

        //  结束上一首

        endPlay()

        player = AVPlayer(playerItem: getPlayerItem(withURL: currentURL))

        observePlayingItem()

    }

    /**

     本地不存在,返回nil,否则返回本地URL

     */

    private funcgetLocationFilePath(url: NSURL) ->NSURL? {

        let fileName = url.lastPathComponent

        let path =StreamAudioConfig.audioDicPath + "/\(fileName ?? "tmp.mp4")"

        ifNSFileManager.defaultManager().fileExistsAtPath(path) {

            let url =NSURL.init(fileURLWithPath: path)

            return url

        } else {

            return nil

        }

    }


    private funcgetPlayerItem(withURL musicURL: NSURL) ->AVPlayerItem {


        iflet 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 funcsetupPlayer(withURL musicURL: NSURL) {

        let songItem = getPlayerItem(withURL: musicURL)

        player = AVPlayer(playerItem: songItem)

    }


    private funcplayerPlay() {

        player?.play()

    }


    private funcendPlay() {

        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 overridefunc observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {

        guard object isAVPlayerItem else {return}

        let item = objectas! AVPlayerItem

        if keyPath =="status" {

            if item.status ==AVPlayerItemStatus.ReadyToPlay {

                status = .ReadyToPlay

                print("ReadyToPlay")

                let duration = item.duration

                playerPlay()

                print(duration)

            } elseif item.status == AVPlayerItemStatus.Failed {

                status = .Stop

                print("Failed")

                stop()

            }

        } elseif keyPath == "loadedTimeRanges" {

            let array = item.loadedTimeRanges

            guard let timeRange = array.first?.CMTimeRangeValueelse {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 funcobservePlayingItem() {

        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: { [weakself] (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 funcremoveObserForPlayingItem() {

        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 structStreamAudioConfig {

    staticlet audioDicPath: String =NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.UserDomainMask, true).last! +"/streamAudio"  //  缓冲文件夹

    staticlet tempPath: String = audioDicPath +"/temp.mp4"    //  缓冲文件路径 - 非持久化文件路径 - 当前逻辑下,有且只有一个缓冲文件


}



https://github.com/AzenXu/AZXMusicPlayerManager

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值