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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发:AVPlayer实现流音频边播边存相关的知识,希望对你有一定的参考价值。

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. 初始化音频文件持久化文件夹 & 缓存文件

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实现流音频边播边存的主要内容,如果未能解决你的问题,请参考以下文章

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

使用 AVPlayer 在 iOS 中音频流期间的播放速度

iOS SDK AVPlayer - 如何检测音频流是不是可搜索?

带有 AVPlayer 的音频流

如何在 iOS 中使用 AVPlayer 缓冲音频?

在 iOS 13 上,AVPlayer 会为此 HLS 视频选择仅音频流。我可以控制这种行为,而是让它加载视频+音频吗?