首页 > 代码库 > iOS开发:AVPlayer实现流音频边播边存
iOS开发:AVPlayer实现流音频边播边存
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. 初始化音频文件持久化文件夹 & 缓存文件
1 2 3 4 5 6 7 8 9 10 11 | 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. 与服务器建立连接请求数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /** 连接服务器,请求数据(或拼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头
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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. 处理服务器返回的数据 - 写入缓存文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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记得赏颗星哟~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** 播放器问:是否应该等这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. 代理方法,播放器关闭了下载请求
1 2 3 4 5 6 7 8 9 10 11 | /** 播放器关闭了下载请求 播放器关闭一个旧请求,都会发起一到多个新请求,除非已经播放完毕了 - 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路公交啦~~谢谢大家体谅哦~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | 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) { 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,戳这里~
iOS开发:AVPlayer实现流音频边播边存