ios 边录音边放_iOS开发 - AVPlayer实现流音频边播边存

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

概述

1. AVPlayer简介

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

AVPlayer的关联类:

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

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

2. AVPlayer播放原理

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

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

所需音频片段的起始offset

所需的音频长度

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

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

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

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

}

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:

*/

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值