iOS 视频播放器开发(一)

最近工作是开发通用的视频播放器,给公司不同部门使用。主要产出是:

  • CachedPlayer
    • 封装AVPlayer,提供更友好API
    • 视频边播边缓存
    • 预加载
  • CachedPlayerView
    • 封面图
    • 加载状态 loadingView
  • FullScreenVideoBoxView
    • 视频播放UI集成
    • 轻松嵌入到UITableViewCellUICollectionViewCell
    • 自动处理进入和退出全屏
    • 手势拖拽退出全屏

系列文章目录:

  1. 《iOS 视频播放器开发》
  2. 《iOS 视频缓存与预加载》
  3. 《iOS 全屏播放动画与手势》

CachedPlayer

AVPlayer的功能十分强大,但是API并不友好。我们需要通过KVO监听AVPlayerAVPlayerItem的多个属性才能获得其确切状态以及播放进度。CachedPlayer提供更加简单直接的API,将AVPlayer的复杂性封装于内部。

状态

enum Status {
    case unknown        // 初始状态
    case buffering      // 加载中
    case playing        // 播放中
    case paused         // 暂停
    case end            // 播放到末尾
    case error          // 播放出错
}
private(set) var status = Status.unknown // 初始默认为unknown

AVPlayer并没有一个确切的status来让我们获取当前播放器状态,在使用过程中往往需要通过多个属性联合判断当前状态。CachedPlayer的首要任务就是对状态进行封装。CachedPlayer通过对AVPlayerAVPlayerItem的多个属性进行监听,然后调用updateStatus()统一改变当前状态。

private func updateStatus() {
    DispatchQueue.main.async {  // 在主线程改变状态,因为外部通常会监听status改变进行UI操作
        guard let currentItem = self.currentItem else {
            return
        }
        if self.player.error != nil || currentItem.error != nil {
            self.status = .error
            
            return
        }
        if #available(iOS 10, *) {
            switch self.player.timeControlStatus {
            case .playing:
                self.status = .playing
            case .paused:
                self.status = .paused
            case .waitingToPlayAtSpecifiedRate:
                self.status = .buffering
            }
        } else {
            if self.player.rate != 0 { // 期望速率不为0
                if currentItem.isPlaybackLikelyToKeepUp {
                    self.status = .playing
                } else {
                    self.status = .buffering
                }
            } else {
                self.status = .paused
            }
        }
    }
}

iOS 10 之后推出了timeControlStatus让我们可以得到当前播放器处于播放、缓冲还是暂停状态。并且当player.automaticallyWaitsToMinimizeStalling = false时,AVPlayer加载数据即立即播放,不会有waitingToPlayAtSpecifiedRate状态,只会在playingpaused之间切换。

属性监听

KVO

AVPlayer:

  • rate: 期望播放速率
  • status: 播放器状态【播放是否失败】
  • timeControlStatus: 当前播放状态【暂停、缓冲、播放】

AVPlayerItem:

  • status: 播放状态
  • playbackLikelyToKeepUp: 是否在播放
  • isPlaybackBufferEmpty: 缓冲区是否为空
  • isPlaybackBufferFull: 缓冲区是否已满

TimeObserver

AVPlayer可以添加定时的监听器获取其当前播放时间。


timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [unowned self] (time) in
    self.updateStatus()
    
    guard let total = self.currentItem?.duration.seconds else {
        return
    }
    if total.isNaN || total.isZero {
        return
    }
    self.duration = total
    self.playedDuration = time.seconds
})

timeObserver需要在deinit时从播放器移除。

Notification

目前仅对AVPlayerItemDidPlayToEndTime进行监听,当播放到结尾,则将status设置为end

API

对外提供基本播放方法:


func replace(with url: URL) {
    currentItem = AVPlayerItem(url: url)
    player.replaceCurrentItem(with: currentItem)
    addItemObservers()
}
func stop() {
    removeItemObservers()
    currentItem = nil
    player.replaceCurrentItem(with: nil)
    
    status = .unknown
}
func play() {
    player.play()
}
func pause() {
    player.pause()
}
func seek(to time: TimeInterval) {
    player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(NSEC_PER_SEC)))
}

回调

var statusDidChangeHandler: ((Status) -> Void)?
var playedDurationDidChangeHandler: ((TimeInterval, TimeInterval) -> Void)?

private(set) var playedDuration: TimeInterval = 0 {
    didSet {
        playedDurationDidChangeHandler?(playedDuration, duration)
    }
}
private(set) var status = Status.unknown {
    didSet {
        guard status != oldValue else {
            return
        }
        statusDidChangeHandler?(status)
    }
}

外部通过这两个闭包来分别监听播放状态的改变以及播放进度的改变。

CachedPlayerView

CachedPlayerView是提供UIKit的API,将CachedPlayer集成在里面。在开发中,我们直接创建CachedPlayerView实例添加到View上。

class CachedPlayerView: UIView {
    private(set) var player = CachedPlayer()
    
    override class var layerClass: AnyClass {
        get {
            return AVPlayerLayer.self
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        player.bind(to: layer as! AVPlayerLayer)
    }
}

CachedPlayer里面添加

func bind(to playerLayer: AVPlayerLayer) {
    playerLayer.player = player
}

那么使用起来就简单了,在ViewController里面只用简单几行代码,就能播放视频了

let playerView = CachedPlayerView()
playerView.player.statusDidChangeHandler = { status in
    print(status)
}
playerView.player.playedDurationDidChangeHandler = { (played, total) in
    print("\(played)/\(total)")
}
playerView.frame = view.bounds
playerView.player.replace(with: url)
playerView.player.play()

更多

到此播放器的基本封装已经完成,提供了简单的对外接口,已经统一了状态监听。
下一篇文章会讲如何通过AVAssetResourceLoaderDelegate实现边播边下载功能。之后会对CachedPlayer进行扩充,封装缓存逻辑调用。

源码地址

你可能感兴趣的:(iOS 视频播放器开发(一))