©️ OverlookArt
首页 / AppleDevelop / AVPlayer 音频播放器

AVPlayer 音频播放器

  • AVPlayer 是管理媒体资产的播放和时间的控制器对象。使用AVPlayer实例播放基于文件的本地和远程视听媒体资源。

  • AVAsset 是一个视听媒体模型对象,视听媒体模型基于文件的媒体,如QuickTime电影或MP3音频文件,以及使用HTTP实时流(HLS)流式传输的媒体。视听媒体模型是的一个或多个视听媒体轨道(AVAssetTrack)实例的容器对象。

  • AVAssetTrack 视听媒体轨道实例对统一类型的媒体轨道进行建模。最常用的轨道类型是音频视频,但资产也可能包含补充轨道,如隐藏式字幕、字幕和计时元数据。

  • 认识播放器 AVPlayer
  • 认识播放对象 AVPlayerItem
  • 认识试听媒体资产 AVAsset
  • 播放器 AVPlayer 的初始化
  • 监听 AVPlayer 的播放进度
  • 设置切换播放对象
  • 监听 AVPlayerItem 属性
  • 切换播放速率
  • 播放中心概览及控制

初始化 Player

1private var player: AVPlayer = AVPlayer(playerItem: nil)

监听 AVPlayer 属性

 1/// 添加 player 监听
 2private func addPlayerObserver(){
 3    player.addObserver(self, forKeyPath: "timeControlStatus", options: .new, context: nil)
 4    let interval = CMTimeMakeWithSeconds(1.0, preferredTimescale: 600)
 5    timeObserverToken = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
 6        if self.player.currentItem?.status == .readyToPlay {
 7            self.playerTimePlayChange(time: time)
 8        }
 9    }
10}
11    
12/// 移除 player 监听
13private func removePlayerObserver(){
14    player.removeObserver(self, forKeyPath: "timeControlStatus")
15    if let token = timeObserverToken {
16        player.removeTimeObserver(token)
17        timeObserverToken = nil
18    }
19}

设置播放数据

使用 AVPlayer 一次播放单个媒体资产。您可以使用其 replaceCurrentItem(with:)方法重复使用播放器实例来播放其他媒体资产,但它一次只管理单个媒体资产的播放。每次播放新的媒体资产时,都需要重新设置。

 1/// 设置 player 的播放数据
 2///
 3/// 在这里可以重置 audio 相关的控件
 4private func setupPlay(){
 5    // 重置播放器的进度控制数据
 6    audioSlider.updatePlayTime(0, totalTime: 0)
 7    // 重置播放按钮状态
 8    playBtn.isSelected = false
 9    guard let model = self.audioDeatilModel else { return }
10    if let url = URL(string: model.playUrl) {
11        setupPlayerItem(url: url)
12    }else{
13        debugPrint("无效的播放地址")
14    }
15}
16    
17private func setupPlayerItem(url: URL){
18    // 移除之前的 AVPlayerItem 监听
19    removePlayerItemObserver()
20    // 创建 AVPlayerItem
21    let item = AVPlayerItem(asset: AVAsset(url: url))
22    // 设置 AVPlayerItem 监听
23    addPlayerItemObserver(item: item)
24    // 将 AVPlayerItem 设置到播放器
25    player.replaceCurrentItem(with: item)
26}

Note

如果需要设置 url 的请求头请使用 AVURLAsset 来加载媒体资源。
AVURLAsset(url: url, options: [“AVURLAssetHTTPHeaderFieldsKey”:[“Referer”: “*.caigou2003.com”]])
这是苹果未公开的 API

监听 AVPlayerItem 的属性

 1/// 添加 playerItem 监听
 2private func addPlayerItemObserver(item: AVPlayerItem){
 3    /// 添加加载状态
 4    item.addObserver(self, forKeyPath: "status", options: .new, context: nil)
 5    /// 添加缓存状态
 6    item.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
 7    /// 添加播放完成状态
 8    NotificationCenter.default.addObserver(self, selector: #selector(playEnd), name: .AVPlayerItemDidPlayToEndTime, object: item)
 9}
10    
11/// 移除 playerItem 监听
12private func removePlayerItemObserver(){
13    // 移除加载状态监听
14    player.currentItem?.removeObserver(self, forKeyPath: "status")
15    // 移除缓存状态监听
16    player.currentItem?.removeObserver(self, forKeyPath: "loadedTimeRanges")
17    // 移除播放完成状态监听
18    NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: nil)
19}

重写自身的 observeValue 方法

 1override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
 2    if let playerItem = object as? AVPlayerItem {
 3        if keyPath == "status" {
 4            playerLoadStatusChange(PlayerItem: playerItem)
 5        }else if keyPath == "loadedTimeRanges" {
 6            playerLoadTimeRangesChange(PlayerItem: playerItem)
 7        }
 8    } else if let _ = object as? AVPlayer {
 9        if keyPath == "timeControlStatus" {
10            playerTimeControlStatusChange()
11        }
12    }
13}

播放器的状态与进度

 1/// AVPlayer 加载 AVPlayerItem 的状态
 2/// - Parameter item: 加载中的 AVPlayerItem
 3///
 4/// 控制前进、回退、及进度控件的启用状态
 5private func playerLoadStatusChange(PlayerItem item: AVPlayerItem){
 6    self.setupSeekControl(isEnable: player.currentItem?.status == .readyToPlay)
 7    switch player.currentItem?.status {
 8        case .readyToPlay:
 9            debugPrint("准备播放")
10            if let duration = player.currentItem?.duration {
11                audioSlider.updatePlayTime(0, totalTime: CMTimeGetSeconds(duration))
12            }
13            // 自动播放(列表循环播放时,音频数据准备好后不需要点播放按钮自动播放)
14            if playAuto {
15                playAudio()
16            }
17        case .failed:
18            debugPrint("加载失败")
19        case .unknown:
20            debugPrint("未知状态")
21        case .none:
22            debugPrint("空状态")
23        @unknown default:
24            fatalError()
25    }
26}
27
28/// AVPlayer 缓存 AVPlayerItem 的进度
29/// - Parameter item: 缓存中的 AVPlayerItem
30private func playerLoadTimeRangesChange(PlayerItem item: AVPlayerItem){
31    if let timeRange = player.currentItem?.loadedTimeRanges.first?.timeRangeValue {
32        let startSeconds = CMTimeGetSeconds(timeRange.start)
33        let durationSeconds = CMTimeGetSeconds(timeRange.duration)
34        let bufferedDuration = startSeconds + durationSeconds
35        let totalDuration = CMTimeGetSeconds(item.duration)
36        let bufferProgress = bufferedDuration / totalDuration
37        let str = String(format: "缓存进度:%.0f", bufferProgress * 100) + "%"
38        debugPrint(str)
39    }
40}
41
42/// AVPlayer 的播放状态
43///
44/// 在这里更新播放按钮的 播放暂停状态
45private func playerTimeControlStatusChange(){
46    switch player.timeControlStatus {
47        case .paused:
48            debugPrint("AVPlayer: ->paused")
49            playBtn.isSelected = false
50        case .waitingToPlayAtSpecifiedRate:
51            debugPrint("AVPlayer: ->waiting")
52            playBtn.isSelected = false
53        case .playing:
54            debugPrint("AVPlayer: ->playing")
55            playBtn.isSelected = true
56        @unknown default:
57            fatalError()
58    }
59}
60
61/// AVPlayer 的播放时间
62///
63/// 在这里更新进度控件的信息, 如播放进度、播放时长、总时长
64/// - Parameter time: 播放时间
65private func playerTimePlayChange(time: CMTime){
66    let currentTime = CMTimeGetSeconds(time)
67    let totalTime = CMTimeGetSeconds(self.player.currentItem?.duration ?? CMTimeMake(value: 1, timescale: 1))
68    debugPrint("当前播放时间: \(currentTime) 秒, 总时长: \(totalTime) 秒")
69    if !audioSlider.isTracking && !isSeeking {
70        self.audioSlider.updatePlayTime(currentTime, totalTime: totalTime)
71    }
72
73    // 定时关闭    
74    if let date = playEndDate, date.timeIntervalSince1970 - Date().timeIntervalSince1970 <= 0 {
75        pauseAudio()
76        clearClokcInfo()
77    }
78}

播放器的控制

 1/// 播放
 2public func playAudio(){
 3    if player.currentItem?.status == .readyToPlay {
 4        player.play()
 5        /// 设置播放速率
 6        player.rate = playRate
 7    }
 8}
 9
10/// 暂停
11public func pauseAudio(){
12    player.pause()
13}
14
15/// 快进到指定位置
16/// - Parameter tipositionTime: 目标位置 秒
17private func seekJump(PositionTime positionTime: TimeInterval) {
18    let time = CMTime(seconds: positionTime, preferredTimescale: 600)
19    setupSeekControl(isEnable: false)
20    self.isSeeking = true
21    player.seek(to: time) { isFinish in
22        self.isSeeking = false
23        self.setupSeekControl(isEnable: true)
24    }
25}
26
27/// 跳到播放位置
28/// - Parameter value: 目标位置 0~1
29///
30/// 这是以播放进度为基准的跳转播放
31private func seekJump(Progress value: Float){
32    if let playItem = player.currentItem {
33        let totalTime = playItem.duration.seconds
34        if totalTime.isNaN { return }
35        let targetTime = totalTime * value.double
36        audioSlider.updatePlayTime(targetTime, totalTime: totalTime)
37        self.seekJump(PositionTime: targetTime)
38    }
39}
40
41/// 跳到指定位置
42/// - Parameter time: 目标位置 秒
43///
44/// 这是以步进时间为基准的跳转播放
45/// 例如:快进10秒,则调用 seekJump(StepTime: 10),快退5秒,则调用 seekJump(StepTime: -5)
46private func seekJump(StepTime stepTime: Double) {
47    if let playItem = player.currentItem {
48        let totalTime = playItem.duration.seconds
49        if totalTime.isNaN { return }
50        let currentTime = player.currentTime().seconds
51        var targetTime = currentTime + stepTime
52        if targetTime < 0 { targetTime = 0 }
53        if targetTime > totalTime { targetTime = totalTime }
54        self.seekJump(PositionTime: targetTime)
55    }
56}