边播边下有三套左右实现思路,本文使用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 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<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实现流音频边播边存的更多相关文章

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

    1. AVPlayer简介 AVPlayer存在于AVFoundation中,可以播放视频和音频,可以理解为一个随身听 AVPlayer的关联类: AVAsset:一个抽象类,不能直接使用,代表一个要 ...

  2. iOS开发拓展篇—封装音频文件播放工具类

    iOS开发拓展篇—封装音频文件播放工具类 一.简单说明 1.关于音乐播放的简单说明 (1)音乐播放用到一个叫做AVAudioPlayer的类 (2)AVAudioPlayer常用方法 加载音乐文件 - ...

  3. ios开发——实用技术篇&网络音频播放

    网络音频播放 在日常的iOS开发中,我们通常会遇到媒体播放的问题,XCode中已经为我们提供了功能非常强大的AVFoundation框架和 MediaPlayer框架.其中AVFoundation框架 ...

  4. iOS开发——高级篇——远程音频、视频播放

    一.远程音频播放(<AVFoundation/AVFoundation.h>) #import <AVFoundation/AVFoundation.h> /** 播放器 */ ...

  5. 李洪强iOS开发之使用CycleScrollView实现轮播图

    01 导入头文件,并且定义CycleScrollView属性 02 初始化,设置frame并且添加到collectionView上 03 调用方法并且设置轮播的图片

  6. IOS开发之瀑布流照片墙实现

    想必大家已经对互联网传统的照片布局方式司空见惯了,这种行列分明的布局虽然对用户来说简洁明了,但是长久的使用难免会产生审美疲劳.现在网上流行一种叫做“瀑布流”的照片布局样式,这种行与列参差不齐的状态着实 ...

  7. 文顶顶iOS开发博客链接整理及部分项目源代码下载

    文顶顶iOS开发博客链接整理及部分项目源代码下载   网上的iOS开发的教程很多,但是像cnblogs博主文顶顶的博客这样内容图文并茂,代码齐全,示例经典,原理也有阐述,覆盖面宽广,自成系统的系列教程 ...

  8. IOS开发中AVFoundation中AVAudioPlayer的使用

    IOS开发中如何调用音频播放组件 1.与音频相关的头文件等都在AVFoundation.h中,所以第一步是添加音频库文件: #import <AVFoundation/AVFoundation. ...

  9. iOS开发系列--音频播放、录音、视频播放、拍照、视频录制

    --iOS多媒体 概览 随着移动互联网的发展,如今的手机早已不是打电话.发短信那么简单了,播放音乐.视频.录音.拍照等都是很常用的功能.在iOS中对于多媒体的支持是非常强大的,无论是音视频播放.录制, ...

随机推荐

  1. C++混合编程之idlcpp教程Lua篇(4)

    上一篇在这  C++混合编程之idlcpp教程Lua篇(3) 与前面的工程相似,工程LuaTutorial2中,同样加入了三个文件 LuaTutorial2.cpp, Tutorial2.i, tut ...

  2. 关于.net 中Clipboard.GetDataObject() 之后读出数据读出的数据都是相同的解决方法

    模拟键盘sendkey("^c") 多次复制之后 当使用Clipboard.GetDataObject() 读出数据都是一个值 经过多次尝试 提供一个解决方案 IDataObjec ...

  3. MySQL中表名大小写问题

    在设计数据表时,有自己特有的规则:英文单词的首字母大写,比如表名User, Article, UserRole, 等等,这种办法使用得很顺手习惯,在以往使用的MS SQL Server.MS Acce ...

  4. 跟我一起学WCF(10)——WCF中事务处理

    一.引言 好久没更新,总感觉自己欠了什么一样的,所以今天迫不及待地来更新了,因为后面还有好几个系列准备些,还有很多东西需要学习总结的.今天就来介绍下WCF对事务的支持. 二.WCF事务详解 2.1 事 ...

  5. 跟我一起学WCF(6)——深入解析服务契约[下篇]

    一.引言 在上一篇博文中,我们分析了如何在WCF中实现操作重载,其主要实现要点是服务端通过ServiceContract的Name属性来为操作定义一个别名来使操作名不一样,而在客户端是通过重写客户端代 ...

  6. [stm32] 中断

    #include "stm32f10x.h" #include "stm32f10x_tim.h" #include "misc.h" #i ...

  7. C# 两行代码实现 延迟加载的单例模式(线程安全)

    关键代码第4,5行. 很简单的原理不解释:readonly + Lazy(.Net 4.0 + 的新特性) public class LazySingleton { //Lazy singleton ...

  8. paip.gui控件tabs控件加载内容的原理以及easyui最佳实现

    paip.gui控件tabs控件加载内容的原理以及easyui最佳实现 //////////////tabs控件的加载 同form窗体一样,俩个方式 两个方式:一个是url,简单的文本可以使用这个,不 ...

  9. Leetcode 65 Valid Number 字符串处理

    由于老是更新简单题,我已经醉了,所以今天直接上一道通过率最低的题. 题意:判断字符串是否是一个合法的数字 定义有符号的数字是(n),无符号的数字是(un),有符号的兼容无符号的 合法的数字只有下列几种 ...

  10. javascript设计模式与开发实践阅读笔记(9)——命令模式

    命令模式:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么,此时希望用一种松耦合的方式来设计软件,使得请求发送者和请求接收者能够消除彼此之间的耦合关系. 说法很复 ...