标签:
边播边下有三套左右实现思路,本文使用AVPlayer + AVURLAsset实现。
实现边下边播,其实就是手动实现AVPlayer的上列播放过程。
OK,边播边下的原理知道了,我们可以正式写代码了~建议先从文末链接处把Demo下载下来,对着Demo咱们慢慢道来~
共需要三个类:
先从小弟说起
如上文所说,小弟是负责做脏活累活的。 负责和服务器连接、向服务器请求数据、把请求回来的数据写到本地缓存文件、把写完的缓存文件移到持久化目录去
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)
}
/**
连接服务器,请求数据(或拼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()
}
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)
}
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)")
}
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掉和服务器的连接
秘书要干的最主要的事情就是响应播放器老大的号令,所有方法都是围绕着播放器老大来的。秘书需要遵循AVAssetResourceLoaderDelegate协议才能被录用。
这个方法其实是播放器在说:小秘呀,我想要这段音频文件。你能现在给我还是等等给我啊?
一定要返回: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
}
/**
播放器关闭了下载请求
播放器关闭一个旧请求,都会发起一到多个新请求,除非已经播放完毕了
- parameter resourceLoader: 资源管理器
- parameter loadingRequest: 待关请求
*/
public func resourceLoader(resourceLoader: AVAssetResourceLoader, didCancelLoadingRequest loadingRequest: AVAssetResourceLoadingRequest) {
guard let index = pendingRequset.indexOf(loadingRequest) else {return}
pendingRequset.removeAtIndex(index)
}
负责调度所有播放器的,负责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? {