编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

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

wxchong 2024-06-24 19:58:40 开源技术 57 ℃ 0 评论

概述

1. AVPlayer简介

2. AVPlayer播放原理

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

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

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

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

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

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

其他

其他方法包括断线重连以及公开一个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  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  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 

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

文章转自 Azen的简书


Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表