6

iOS AVFoundation - 音视频播放器 AVPlayer

 3 years ago
source link: http://blog.danthought.com/programming/2020/06/24/ios-avplayer/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

iOS AVFoundation - 音视频播放器 AVPlayer

iOS 中的音视频处理库 AVFoundation 内容很丰富,功能也很强大,本文主要讲解 AVPlayer 音视频播放器,可以播放本地视频文件,也可以播放网络视频文件。

Camera Sea

完整代码:

AVAsset 对媒体资源的抽象,如视频资源和音频资源。

// 本地文件
if let fileURL = Bundle.main.url(forResource: "boat", withExtension: "mov") {
    let asset = AVURLAsset(url: url)
}

// 网络文件
if let fileURL = URL(string: "http://danthought.com/morning.mov") {
    let asset = AVURLAsset(url: url)
}

AVPlayerItem 对媒体资源的时间状态和表现状态的抽象。

let item = AVPlayerItem(asset: asset)

AVPlayer 是控制媒体资源播放的统一接口,AVPlayer 和 AVPlayerItem 是一对多的关系。

let player = AVPlayer()
player.actionAtItemEnd = .none
player.replaceCurrentItem(with: item) // decoding audio and video

AVPlayerLayer 管理 AVPlayer 输出的视频图像,在界面上显示出来。

class PlayerView: UIView {
    
    override class var layerClass: AnyClass {
        return AVPlayerLayer.self
    }
    
    private var playerLayer: AVPlayerLayer? {
        return layer as? AVPlayerLayer
    }
    
    private var player: AVPlayer? {
        get {
            return playerLayer?.player
        }
        set {
            playerLayer?.player = newValue
        }
    }
    
}

获取播放器的状态都是异步的,相关状态变更时来通知你。

监控 AVPlayerItem 的 status,得到视频是否可以播放了:

statusObservation = item.observe(\.status) { [weak self] item, _ in
    self?.itemStatusChanged(item)
}

private func itemStatusChanged(_ item: AVPlayerItem) {
    statusObservation?.invalidate()
    if item.status == .readyToPlay {
        if !item.duration.isIndefinite {
            durationChanged(item)
        } else {
            durationObservation = item.observe(\.duration) { [weak self] item, _ in
                self?.durationChanged(item)
            }
        }
    } else {
        indicatorView.stopAnimating()
        DTMessageBar.error(message: "视频无法播放")
    }
}

得到可以播放的状态,还需要知道视频的时长,也是异步的通知:

durationObservation = item.observe(\.duration) { [weak self] item, _ in
    self?.durationChanged(item)
}

private func durationChanged(_ item: AVPlayerItem) {
    if !item.duration.isIndefinite {
        durationObservation?.invalidate()
        indicatorView.stopAnimating()
        duration = CMTimeGetSeconds(item.duration)
        let current = CMTimeGetSeconds(item.currentTime())

        let timeScale = CMTimeScale(NSEC_PER_SEC)
        var time = CMTime(seconds: 1, preferredTimescale: timeScale)
        currentTimeObservation =
            player?.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in
                self?.itemCurrentTimeChanged(time)
        }
        
        NotificationCenter.default.addObserver(self, selector: #selector(playerDidFinishPlaying(_ :)),
                                               name: .AVPlayerItemDidPlayToEndTime, object: player?.currentItem)
    }
}

根据视频播放进度来更新界面进度条:

let timeScale = CMTimeScale(NSEC_PER_SEC)
var time = CMTime(seconds: 1, preferredTimescale: timeScale)
currentTimeObservation =
    player?.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in
        self?.itemCurrentTimeChanged(time)
}


private func itemCurrentTimeChanged(_ time: CMTime) {
    let current = CMTimeGetSeconds(time)
    if style != .simple {
        updateTime(for: currentTimeLabel, time: current)
    }
    if let slider = slider {
        if !isSliding {
            slider.setValue(Float(current), animated: true)
        }
    }
    if let progressView = progressView {
        progressView.setProgress(Float(current/duration), animated: true)
    }
}

iOS 中使用 UISlider 来做 seek 控制是比较自然的选择:

slider = UISlider()
slider.isContinuous = false
// slider开始滑动事件
slider.addTarget(self, action: #selector(sliderTouchBegin(_:)), for: .touchDown)
// slider滑动中事件
slider.addTarget(self, action: #selector(sliderValueChanged(_:)), for: .valueChanged)

@objc private func sliderTouchBegin(_ slider: UISlider) {
    isSliding = true
}

@objc private func sliderValueChanged(_ slider: UISlider) {
    let seconds = Double(slider.value)
    let timeScale = CMTimeScale(NSEC_PER_SEC)
    let time = CMTime(seconds: seconds, preferredTimescale: timeScale)
    player?.seek(to: time, completionHandler: { _ in
        self.isSliding = false
    })
}

上面代码中有一个 isSliding 的状态用于控制是否在滑动中,是为了避免滑动过程中,进度更新代码将其进度更新,就会产生冲突:

private func itemCurrentTimeChanged(_ time: CMTime) {
    let current = CMTimeGetSeconds(time)
    if let slider = slider {
        if !isSliding {
            slider.setValue(Float(current), animated: true)
        }
    }    
}

上面使用 AVPlayer 的方式也是可以边下边播的,但是有一个场景是在一个界面的头部嵌入界面播放时,点击了全屏播放,跳到一个新界面进行全屏播放,希望两个界面的视频播放进度是同步的,之前的解决方案是传递 AVPlayer:

class PlayerViewController: UIViewController {
    func setPlayer(_ player: AVPlayer) {
        playerView.setPlayer(player)
    }
}

这种方式非常的笨拙,AVPlayer 是黑盒,也没有办法把其下载的网络视频数据拿出来,将视频数据下载和视频播放分离开就是解决方案,有如下参考:

更可控和更完整的视频播放器

总的来说,为了易用性,AVPlayer 是黑盒,很多部分开发者并不能自己调控,下面看一下一个完整播放器的整体架构:

iOS Audio Video Consumer

要实现一个这样的视频播放器需要花费较多的时间,跨平台的视频播放器实现体系主要是 ffplay 和 vlc:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK