My most recent job is to develop a universal video player for use by different departments of the company. The main outputs are:

  • CachedPlayer
    • encapsulationAVPlayer, provide a more friendly API
    • Video is played and cached
    • preload
  • CachedPlayerView
    • cover
    • Load state loadingView
  • FullScreenVideoBoxView
    • Video playback UI integration
    • Easily embedded intoUITableViewCell.UICollectionViewCell
    • Automatically handles entering and exiting full screen
    • Gesture drag to exit full screen

Contents of series of articles:

  1. IOS Video Player Development
  2. IOS Video Caching and Preloading
  3. IOS Full Screen Animation and Gestures

CachedPlayer

AVPlayer is very powerful, but its API is not very friendly. We need to monitor several attributes of AVPlayer and AVPlayerItem through KVO to obtain their exact status and playing progress. CachedPlayer provides a simpler and more straightforward API that encapsulates the complexity of AVPlayer internally.

state

enum Status {
    case unknown        // Initial state
    case buffering      / / load
    case playing        / / play
    case paused         / / pause
    case end            // Play to the end
    case error          // Playback error
}
private(set) var status = Status.unknown // The initial default is unknown

Copy the code

AVPlayer does not have an exact status for us to obtain the current player status. In the process of using AVPlayer, we often need to judge the current status through multiple attributes. The primary task of CachedPlayer is to encapsulate state. CachedPlayer changes the current state by listening for multiple attributes of AVPlayer and AVPlayerItem and then calling updateStatus().

private func updateStatus(a) {
    DispatchQueue.main.async {  // Change the state on the main thread, because the outside world usually listens for status changes for UI operations
        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 { // The expected rate is not zero
                if currentItem.isPlaybackLikelyToKeepUp {
                    self.status = .playing
                } else {
                    self.status = .buffering
                }
            } else {
                self.status = .paused
            }
        }
    }
}
Copy the code

After iOS 10, timeControlStatus was introduced to let us know whether the current player is playing, buffered or paused. And when player. AutomaticallyWaitsToMinimizeStalling = false, AVPlayer load data immediately, there will be no waitingToPlayAtSpecifiedRate state, It just switches between playing and Paused.

Attribute to monitor

KVO

AVPlayer:

  • Rate: indicates the expected playback rate
  • Status: player status [whether playback fails]
  • TimeControlStatus: current play status [pause, buffer, play]

AVPlayerItem:

  • Status: indicates the playback status
  • PlaybackLikelyToKeepUp: Whether it is playing
  • IsPlaybackBufferEmpty: Indicates whether the buffer is empty
  • IsPlaybackBufferFull: Indicates whether the buffer is full

TimeObserver

AVPlayer can add timed listeners to get its current playing time.


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.secondselse {
        return
    }
    if total.isNaN || total.isZero {
        return
    }
    self.duration = total
    self.playedDuration = time.seconds
})
Copy the code

TimeObserver needs to be removed from the player during deinit.

Notification

Currently only to listen AVPlayerItemDidPlayToEndTime, when playing to the end, then set the status to the end.

API

Provide basic broadcast methods externally:


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

The callback

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 {
        guardstatus ! = oldValueelse {
            return} statusDidChangeHandler? (status) } }Copy the code

The external uses these two closures to listen for changes in playback state and playback progress, respectively.

CachedPlayerView

CachedPlayerView is an API that provides UIKit, that integrates CachedPlayer into it. In development, we directly create a CachedPlayerView instance to add to the View.

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

Add it in CachedPlayer

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

So it’s easy to use, just a few lines of code inside the ViewController, and you can play the video

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()

Copy the code

More and more

The basic package of this player has been completed, providing a simple external interface, and has unified status monitoring. How the next article will speak AVAssetResourceLoaderDelegate to achieve seeding and download function. CachedPlayer will then be extended to encapsulate the cache logic calls.

The source address