My most recent job is to develop a universal video player for use by different departments of the company. The main outputs are:
- CachedPlayer
- encapsulation
AVPlayer
, provide a more friendly API - Video is played and cached
- preload
- encapsulation
- CachedPlayerView
- cover
- Load state loadingView
- FullScreenVideoBoxView
- Video playback UI integration
- Easily embedded into
UITableViewCell
.UICollectionViewCell
- Automatically handles entering and exiting full screen
- Gesture drag to exit full screen
Contents of series of articles:
- IOS Video Player Development
- IOS Video Caching and Preloading
- 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