AVPlayer播放器

AVPlayer是一个功能强大的iOS视频播放器(也是最坑的),在这仅仅介绍AVPlayer的基本使用,但是不太推荐使用,最好还是自己解码并显示,或者使用其他播放器。

AVPlayer

SystemPlayerView

系统建议的播放器View写法,也是最基础的播放器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
open class SystemPlayerView: UIView {

open var player: AVPlayer? {
get {
return playerLayer.player
}

set {
playerLayer.player = newValue
}
}

open var playerLayer: AVPlayerLayer {
return layer as! AVPlayerLayer
}

open override class var layerClass: AnyClass {
return AVPlayerLayer.self
}
}

PlayerViewController

创建一个ViewController来放置Player,所有的事件都由Controller来处理(进度条拖动,暂停,开始。。)

Observer

定义全局KVOContext:private var MediaPlayerViewControllerKVOContext = 0

比较有意义的就是下面几个属性的监听。

  • 播放器的rate,为0的时候表示暂停,不为0的时候表示 可能在播放

这里的播放并不是我们常说的播放状态,它更像AudioToolBoxAudioQueueStart之后的状态,表示当前队列服务在运行,如果当前在缓冲的状态,视频画面是不动的,但是依旧是播放的状态。所以仅仅依靠这个判断画面是否在动其实并不是很准确

1
player.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: [.new], context: &MediaPlayerViewControllerKVOContext)
  • 缓冲区是否空了,true表示当前缓冲区已经空了,无法继续播放视频;false表示当前缓冲区有数据。

但是并不表示这个值为false的时候视频可以播放,只要缓冲区有数据这个值就会变成false,所以这个值最常用的就是监听到这个值为true的时候显示缓冲Loading。

1
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferEmpty), options: [.new], context: &MediaPlayerViewControllerKVOContext)
  • 缓冲区的数据是否可以支持视频开始播放,true表示当前视频可以播放了,false表示缓冲区的Buffer还不能完全支持播放器进行播放。

当监听到这个值为true的时候消失Loading。

1
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp), options: [.new], context: &MediaPlayerViewControllerKVOContext)
  • 是否缓冲完成

当值为true的时候表示当前的视频已经缓冲完成,之后不再进行缓冲操作,之后的I/O操作将被暂停。

1
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferFull), options: [.new], context: &MediaPlayerViewControllerKVOContext)
  • 播放总时间,获取到总的播放时长。

一般监听到这个值一个有效值的时候,播放、暂停按钮可以点击;进度条变成可以操作;显示总时间等操作。

1
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.duration), options: [.new], context: &MediaPlayerViewControllerKVOContext)
  • 播放状态,获取当前的播放状态。

总共有三种状态,如果监听了initial首先是unknown,在这里直接监听new,所以直接返回failed或者readyToPlay,当监听到readyToPlay表示当前视频已经准备好播放了,当监听到failed表示当前视频无法播放。⚠️ 在这里才可以进行seek操作。

1
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.new], context: &MediaPlayerViewControllerKVOContext)
  • 缓冲进度。

在这里我们可以拿到播放器的缓冲进度(但这个缓冲进度可能并不是连续的,而且在每次seek之后都会从seek点开始缓冲)。

1
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges), options: [.new], context: &MediaPlayerViewControllerKVOContext)
  • 具体的监听处理:

判断当前视频是否可以播放的一些关键点:

1
2
3
4
5
6
    private static let assetKeysRequiredToPlay = [
// "tracks",
"playable",
"duration",
"hasProtectedContent"
]

具体的监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

guard context == &VideoPlayerViewControllerKVOContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}

/// Getting new value.
let newValue: Any? = change?[.newKey]

//MARK: AVPlayer observer

if keyPath == #keyPath(AVPlayer.rate) {

// Update playPauseButton type.
let newRate = (newValue as? NSNumber)?.doubleValue

debugPrint("🎬: newRate: \(newRate)")
}

//MARK: AVPlayerItem observer

guard let playerItem = playerItem else { return }

if keyPath == #keyPath(AVPlayerItem.isPlaybackBufferEmpty) {

/// Start loading.
if playerItem.isPlaybackBufferEmpty {
bufferingState = .delayed
}
debugPrint("🎬: isPlaybackBufferEmpty: \(playerItem.isPlaybackBufferEmpty)")
} else if keyPath == #keyPath(AVPlayerItem.isPlaybackBufferFull) {

debugPrint("🎬: isPlaybackBufferFull: \(playerItem.isPlaybackBufferFull)")
} else if keyPath == #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp) {

/// Stop loading.
if playerItem.isPlaybackLikelyToKeepUp {
bufferingState = .ready
}

debugPrint("🎬: isPlaybackLikelyToKeepUp: \(playerItem.isPlaybackLikelyToKeepUp)")
} else if keyPath == #keyPath(AVPlayerItem.duration) {

let newDuration: CMTime = (newValue as? NSValue)?.timeValue ?? kCMTimeZero

let hasValidDuration = newDuration.isNumeric && newDuration.value != 0
let newDurationSeconds = hasValidDuration ? newDuration.seconds : 0.0

/// UI
setControlBar(isEnable: hasValidDuration, duration: newDurationSeconds)

debugPrint("🎬: hasValidDuration: \(hasValidDuration), newDurationSeconds: \(newDurationSeconds)")
} else if keyPath == #keyPath(AVPlayerItem.status) {
// Display an error if status becomes Failed

/*
Handle `NSNull` value for `NSKeyValueChangeNewKey`, i.e. when
`player.currentItem` is nil.
*/

let newStatus = AVPlayerItemStatus(rawValue: (newValue as? NSNumber)?.intValue ?? 0) ?? .unknown

switch newStatus {
case .failed:
delegate?.videoPlayerViewController(self, didCompletedPlayToEndWithError: playerItem.error)
case .readyToPlay:

var errors: [NSError?] = []

for key in VideoPlayerViewController.assetKeysRequiredToPlay {
var error: NSError?
if playerItem.asset.statusOfValue(forKey: key, error: &error) == .failed {
errors.append(error)
}
}

if !playerItem.asset.isPlayable || playerItem.asset.hasProtectedContent {
delegate?.videoPlayerViewController(self, didCompletedPlayToEndWithError: errors.first as? Error)
return
}

if isAutoPlay { play() }
if let seekTime = requestSeek {
seek(toTime: seekTime)
requestSeek = nil
}
delegate?.videoPlayerViewControllerReadyToPlay(self)
case .unknown:
break
}
debugPrint("🎬: newStatus: \(newStatus.rawValue)")
} else if keyPath == #keyPath(AVPlayerItem.loadedTimeRanges) {

guard let timeRange = playerItem.loadedTimeRanges.first?.timeRangeValue, duration.value != 0 else { return }

let progress = Float((timeRange.start.seconds + timeRange.duration.seconds) / duration.seconds)
videoPlayerView.controlBar.timeSlider.setBufferProgress(progress)
debugPrint("🎬: progress: \(progress)")
}
}

全屏实现:

设备可旋转:

当设备可以旋转是最好处理的,直接调用下面代码就可以实现全屏模式。

判断是否是全屏:

1
var isFullScreen: Bool { return UIApplication.shared.statusBarOrientation.isLandscape }

监听旋转事件:

1
NotificationCenter.default.addObserver(self, selector: #selector(), name: .UIApplicationDidChangeStatusBarOrientation, object: nil)

直接旋转:

1
2
3
4
5
6
7
8
9
if isFullScreen {
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UIApplication.shared.setStatusBarHidden(false, with: .fade)
UIApplication.shared.statusBarOrientation = .portrait
} else {
UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation")
UIApplication.shared.setStatusBarHidden(false, with: .fade)
UIApplication.shared.statusBarOrientation = .landscapeRight
}

设备无法旋转:

如果在Xcode中配置仅Portrait模式,那么调用上面的方法是没有用的。

所以针对这个就需要使用仿射变换了(有点坑)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/// Full screen view
class RotationPlayerView: UIView {

/// Player view
var playerView = SystemPlayerView()

/// Full screen view
weak var fullScreenView: UIView? = UIApplication.shared.keyWindow
/// Origin view, using when rotated from full screen to portrait.
weak var originView: UIView? = nil

/// 16 : 9 (9 / 16)
var playerViewScale: CGFloat = 9 / 16
/// Is current player view full screen.
var isFullScreen: Bool { return origintation != .portrait }
/// Current origintation.
private(set) var origintation: UIDeviceOrientation = .portrait

/// Normal size.
private var originSize: CGSize = .zero
/// Is rotation animation completed.
fileprivate var isRotateAnimationCompleted: Bool = true

override init(frame: CGRect) {
super.init(frame: frame)

backgroundColor = .black
playerView.backgroundColor = .black

addSubview(playerView)
}

override func layoutSubviews() {
super.layoutSubviews()

let playerViewSize = CGSize(width: bounds.size.width, height: bounds.size.width * playerViewScale)
playerView.bounds.size = playerViewSize
playerView.center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

/// Rotate player view.
///
/// - Parameters:
/// - origintation: To origintation
/// - completed: Call back when it completed.
func rotate(toOrigintation origintation: UIDeviceOrientation, completed: ((UIDeviceOrientation) -> ())? = nil) {

guard isRotateAnimationCompleted else { return }
isRotateAnimationCompleted = false

let completedHandle = {
self.origintation = origintation
completed?(origintation)
}

/// Need to record portrait origin view and size.
if self.origintation == .portrait {
originView = superview
originSize = frame.size
}


switch origintation {
case .portrait where self.origintation != .portrait:

moveTo(view: originView)

UIView.animate(withDuration: 0.4, animations: {
self.transform = CGAffineTransform.identity
self.frame = CGRect(origin: .zero, size: self.originSize)
}, completion: { _ in
self.isRotateAnimationCompleted = true
completedHandle()
})
case .landscapeLeft where self.origintation != .landscapeLeft:

guard let fullScreenView = fullScreenView else { return }

moveTo(view: fullScreenView)

UIView.animate(withDuration: 0.4, animations: {
self.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi / 2))
self.center = CGPoint(x: fullScreenView.bounds.size.width / 2, y: fullScreenView.bounds.size.height / 2)
self.bounds.size = CGSize(width: fullScreenView.bounds.size.height, height: fullScreenView.bounds.size.width)
}, completion: { _ in
self.isRotateAnimationCompleted = true
completedHandle()
})
case .landscapeRight where self.origintation != .landscapeRight:

guard let fullScreenView = fullScreenView else { return }

moveTo(view: fullScreenView)

UIView.animate(withDuration: 0.4, animations: {
self.transform = CGAffineTransform(rotationAngle: -CGFloat(Double.pi / 2))
self.center = CGPoint(x: fullScreenView.bounds.size.width / 2, y: fullScreenView.bounds.size.height / 2)
self.bounds.size = CGSize(width: fullScreenView.bounds.size.height, height: fullScreenView.bounds.size.width)
}, completion: { _ in
self.isRotateAnimationCompleted = true
completedHandle()
})
default:
isRotateAnimationCompleted = true
return
}
}

private func moveTo(view: UIView?) {
removeFromSuperview()
frame = convert(bounds, to: view)
view?.addSubview(self)
}
}

Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
import UIKit
import AVFoundation

public protocol PlayerTimeConversion {
var cmTime: CMTime { get }
}

extension CMTime: PlayerTimeConversion {
public var cmTime: CMTime { return self }
}

extension CMTimeValue: PlayerTimeConversion {
public var cmTime: CMTime { return CMTime(value: self, timescale: 1) }
}

extension TimeInterval: PlayerTimeConversion {
public var cmTime: CMTime { return CMTime(seconds: self, preferredTimescale: 1) }
}

protocol VideoPlayerViewControllerDelegate: class {

func videoPlayerViewController(_ controller: VideoPlayerViewController, didCompletedPlayToEndWithError error: Error?)
func videoPlayerViewController(_ controller: VideoPlayerViewController, didChangeBufferingState state: BufferingState)
func videoPlayerViewController(_ controller: VideoPlayerViewController, didCompletedRotatedPlayer isFullScreen: Bool)


func videoPlayerViewController(_ controller: VideoPlayerViewController, listButtonClicked listButton: UIButton)
func videoPlayerViewController(_ controller: VideoPlayerViewController, closeButtonClicked closeButton: UIButton)
func videoPlayerViewController(_ controller: VideoPlayerViewController, didSeekToTime time: CMTime)
func videoPlayerViewController(_ controller: VideoPlayerViewController, didChangeStatus status: PlaybackState)
func videoPlayerViewControllerReadyToPlay(_ controller: VideoPlayerViewController)
}

/// Asset playback states.
public enum PlaybackState: Int, CustomStringConvertible {
case stopped = 0
case playing
case paused
case failed

public var description: String {
get {
switch self {
case .stopped:
return "Stopped"
case .playing:
return "Playing"
case .failed:
return "Failed"
case .paused:
return "Paused"
}
}
}
}

/// Asset buffering states.
public enum BufferingState: Int, CustomStringConvertible {
case unknown = 0
case ready
case delayed

public var description: String {
get {
switch self {
case .unknown:
return "Unknown"
case .ready:
return "Ready"
case .delayed:
return "Delayed"
}
}
}
}

private var VideoPlayerViewControllerKVOContext = 0

public final class VideoPlayerViewController: UIViewController {

/// Auto hidden control bar and top bar with time.
public var hiddenTimeInterval: TimeInterval = 3
/// Is auto rotation player view when device rotation, default is true(Only useful if system auto rotation is open).
public var isAutoRotation: Bool = true
/// Is auto play when url is set.
public var isAutoPlay: Bool = true
/// URL for playback.
public var url: URL? = nil {
didSet {
guard isViewLoaded else { return }
set(url: url)
}
}

/// Duration for palyer item
public var duration: CMTime { return playerItem?.duration ?? kCMTimeZero }
/// Current time.
public var currentTime: CMTime { return player.currentTime() }
/// Current playback state of the VideoPlayerViewController.
public private(set) var playbackState: PlaybackState = .stopped
/// Current buffering state of the Player.
public private(set) var bufferingState: BufferingState = .unknown {
didSet {
delegate?.videoPlayerViewController(self, didChangeBufferingState: bufferingState)
}
}
/// Is playing
public var isPlaying: Bool { return player.rate != 0 }

var videoPlayerView: VideoPlayerView { return view as! VideoPlayerView }
weak var delegate: VideoPlayerViewControllerDelegate? = nil


/// AVPlayer item
private var playerItem: AVPlayerItem? { return player.currentItem }
/// AVPlayer
private var player: AVPlayer { return videoPlayerView.playerView.player! }
/// Check if control view is hidden.
private var isControlViewHidden: Bool { return videoPlayerView.isControlViewHidden }
/// Check if control view is hidden or display animation completed.
private var isControlViewAnimationCompleted: Bool = true
/// Timer for hidden control view.
private var controlViewTimer: Timer? = nil
/// Check if user is sliding slider, control bar will not auto hidden when user is sliding.
private var isUserSliding: Bool = false
/// Request seek
private var requestSeek: CMTime? = nil

/// AVPlayer time observer token.
private var timeObserverToken: AnyObject? = nil
// Attempt to load and test these asset keys before playing
private static let assetKeysRequiredToPlay = [
// "tracks",
"playable",
"duration",
"hasProtectedContent"
]

public override func loadView() {
view = VideoPlayerView(player: AVPlayer())
}

override public func viewDidLoad() {
super.viewDidLoad()

videoPlayerView.playbackButton.addTarget(self, action: #selector(playbackButtonClicked(_:)), for: .touchUpInside)

videoPlayerView.topBar.listButton.addTarget(self, action: #selector(listButtonClicked(_:)), for: .touchUpInside)
videoPlayerView.topBar.closeButton.addTarget(self, action: #selector(closeButtonClicked(_:)), for: .touchUpInside)

videoPlayerView.controlBar.playbackButton.addTarget(self, action: #selector(playbackButtonClicked(_:)), for: .touchUpInside)
videoPlayerView.controlBar.volumButton.addTarget(self, action: #selector(volumButtonClicked(_:)), for: .touchUpInside)
videoPlayerView.controlBar.zoomButton.addTarget(self, action: #selector(zoomButtonClicked(_:)), for: .touchUpInside)
videoPlayerView.controlBar.timeSlider.addTarget(self, action: #selector(timeSliderDidSlide(_:)), for: .touchUpInside)
videoPlayerView.controlBar.timeSlider.addTarget(self, action: #selector(timeSliderDidSlide(_:)), for: .touchUpOutside)
videoPlayerView.controlBar.timeSlider.addTarget(self, action: #selector(timeSliderSliding(_:)), for: .valueChanged)

videoPlayerView.playerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapGesture(_:))))

if let url = url {
set(url: url)
}
}

public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

NotificationCenter.default.addObserver(self, selector: #selector(deviceRotated(_:)), name: .UIDeviceOrientationDidChange, object: nil)
player.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: [.new], context: &VideoPlayerViewControllerKVOContext)
}

public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)

setupPlayerPeriodicTimeObserver(false)
NotificationCenter.default.removeObserver(self, name: .UIDeviceOrientationDidChange, object: nil)
player.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate), context: &VideoPlayerViewControllerKVOContext)
}

//MARK: - Setting URL AVURLAssest AVPlayerItem

private func set(url: URL?) {

/// Reset
stop()
resetUI()

set(asset: url == nil ? nil : AVURLAsset(url: url!, options: nil))
}

private func set(asset: AVAsset?) {
set(playerItem: asset == nil ? nil : AVPlayerItem(asset: asset!, automaticallyLoadedAssetKeys: VideoPlayerViewController.assetKeysRequiredToPlay))
}

private func set(playerItem: AVPlayerItem?) {

/// Remove Observer

self.playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferEmpty), context: &VideoPlayerViewControllerKVOContext)
self.playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferFull), context: &VideoPlayerViewControllerKVOContext)
self.playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp), context: &VideoPlayerViewControllerKVOContext)
self.playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.duration), context: &VideoPlayerViewControllerKVOContext)
self.playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), context: &VideoPlayerViewControllerKVOContext)
self.playerItem?.removeObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges), context: &VideoPlayerViewControllerKVOContext)

/// Remove Notification

if let oldPlayerItem = self.playerItem {
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: oldPlayerItem)
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemFailedToPlayToEndTime, object: oldPlayerItem)
}

/// Add Observer

playerItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferEmpty), options: [.new], context: &VideoPlayerViewControllerKVOContext)
playerItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackBufferFull), options: [.new], context: &VideoPlayerViewControllerKVOContext)
playerItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp), options: [.new], context: &VideoPlayerViewControllerKVOContext)
playerItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.duration), options: [.new], context: &VideoPlayerViewControllerKVOContext)
playerItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: [.new], context: &VideoPlayerViewControllerKVOContext)
playerItem?.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.loadedTimeRanges), options: [.new], context: &VideoPlayerViewControllerKVOContext)

/// Add Notification

if let newPlayerItem = playerItem {
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEndTime(_:)), name: .AVPlayerItemDidPlayToEndTime, object: newPlayerItem)
NotificationCenter.default.addObserver(self, selector: #selector(playerItemFailedToPlayToEndTime(_:)), name: .AVPlayerItemFailedToPlayToEndTime, object: newPlayerItem)
}

setupPlayerPeriodicTimeObserver(playerItem != nil)

player.replaceCurrentItem(with: playerItem)
}

//MARK: - Player Periodic Time Observer

/// Set up player periodic time observer
///
/// - Parameter isValid: Is valid.
private func setupPlayerPeriodicTimeObserver(_ isValid: Bool) {
// Only add the time observer if one hasn't been created yet.
if isValid && timeObserverToken == nil {
// Use a weak self variable to avoid a retain cycle in the block.
timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 1), queue: .main) { [weak self] time in

/// Do not change slider value when user is sliding.
guard self?.isUserSliding == false else { return }
self?.videoPlayerView.controlBar.currentTimeLabel.text = Int(time.seconds).timeString
self?.videoPlayerView.controlBar.timeSlider.value = Float(time.seconds)

} as AnyObject?
} else if !isValid ,let timeObserverToken = timeObserverToken {
player.removeTimeObserver(timeObserverToken)
self.timeObserverToken = nil
}
}

//MARK: - Playback funcs

/// Begins playback of the media from the current time.
public func play() {
player.play()
playbackState = .playing
switchPlaybackButtonStatus(true)
}

/// Pauses playback of the media.
public func pause() {
player.pause()
playbackState = .paused
switchPlaybackButtonStatus(false)
}

/// Stops playback of the media.
public func stop() {
player.pause()
playerItem?.seek(to: kCMTimeZero)
playbackState = .stopped
switchPlaybackButtonStatus(false)
}

/// Updates the playback time to the specified time bound.
///
/// - Parameters:
/// - time: The time to switch to move the playback.
/// - toleranceBefore: The tolerance allowed before time.
/// - toleranceAfter: The tolerance allowed after time.
public func seek(toTime time: PlayerTimeConversion,
toleranceBefore: CMTime = kCMTimeZero,
toleranceAfter: CMTime = kCMTimeZero,
completionHandler: ((Bool) -> ())? = nil) {
//TODO: Crash when sometimes.
guard playerItem?.status == .readyToPlay else { requestSeek = time.cmTime; return }

playerItem?.seek(to: time.cmTime, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter, completionHandler: {
completionHandler?($0)
})
}

//MARK: - Selector - Control Bar

@objc private func timeSliderDidSlide(_ sender: UISlider) {
createTimer()
let time = CMTime(seconds: Double(sender.value), preferredTimescale: 1)
seek(toTime: time, completionHandler: { _ in
self.isUserSliding = false
self.delegate?.videoPlayerViewController(self, didSeekToTime: time.cmTime)
})
}

@objc private func timeSliderSliding(_ sender: UISlider) {
isUserSliding = true
videoPlayerView.controlBar.currentTimeLabel.text = Int(sender.value).timeString
}

@objc private func playbackButtonClicked(_ sender: UIButton) {
sender.isSelected ? pause() : play()
delegate?.videoPlayerViewController(self, didChangeStatus: playbackState)
}

@objc private func zoomButtonClicked(_ sender: UIButton) {
videoPlayerView.rotate(toOrigintation: sender.isSelected ? .portrait : .landscapeLeft, completed: {
sender.isSelected = !sender.isSelected
self.delegate?.videoPlayerViewController(self, didCompletedRotatedPlayer: $0 != .portrait)
})
}

@objc private func volumButtonClicked(_ sender: UIButton) {
sender.isSelected = !sender.isSelected
player.isMuted = sender.isSelected
}

//MARK: Selector - Top Bar

@objc private func listButtonClicked(_ sender: UIButton) {
delegate?.videoPlayerViewController(self, listButtonClicked: sender)
}

@objc private func closeButtonClicked(_ sender: UIButton) {
delegate?.videoPlayerViewController(self, closeButtonClicked: sender)
}

//MARK: Selector - Gesture

@objc private func tapGesture(_ sender: UITapGestureRecognizer) {
destoryTimer()
operateControlView(!isControlViewHidden)
}

//MARK: Selector - Notification

@objc private func playerItemDidPlayToEndTime(_ sender: Notification) {
stop()
delegate?.videoPlayerViewController(self, didCompletedPlayToEndWithError: nil)
}

@objc private func playerItemFailedToPlayToEndTime(_ sender: Notification) {
playbackState = .failed
delegate?.videoPlayerViewController(self, didCompletedPlayToEndWithError: sender.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error)
}

@objc private func deviceRotated(_ sender: Notification) {
guard isAutoRotation else { return }

let newOrientation = UIDevice.current.orientation
guard newOrientation != videoPlayerView.origintation else { return }

videoPlayerView.rotate(toOrigintation: newOrientation, completed: {
self.delegate?.videoPlayerViewController(self, didCompletedRotatedPlayer: $0 != .portrait)
})
}

//MARK: - KVO

override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

guard context == &VideoPlayerViewControllerKVOContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}

/// Getting new value.
let newValue: Any? = change?[.newKey]

//MARK: AVPlayer observer

if keyPath == #keyPath(AVPlayer.rate) {

// Update playPauseButton type.
let newRate = (newValue as? NSNumber)?.doubleValue

debugPrint("🎬: newRate: \(newRate)")
}

//MARK: AVPlayerItem observer

guard let playerItem = playerItem else { return }

if keyPath == #keyPath(AVPlayerItem.isPlaybackBufferEmpty) {

/// Start loading.
if playerItem.isPlaybackBufferEmpty {
bufferingState = .delayed
}
debugPrint("🎬: isPlaybackBufferEmpty: \(playerItem.isPlaybackBufferEmpty)")
} else if keyPath == #keyPath(AVPlayerItem.isPlaybackBufferFull) {

debugPrint("🎬: isPlaybackBufferFull: \(playerItem.isPlaybackBufferFull)")
} else if keyPath == #keyPath(AVPlayerItem.isPlaybackLikelyToKeepUp) {

/// Stop loading.
if playerItem.isPlaybackLikelyToKeepUp {
bufferingState = .ready
}

debugPrint("🎬: isPlaybackLikelyToKeepUp: \(playerItem.isPlaybackLikelyToKeepUp)")
} else if keyPath == #keyPath(AVPlayerItem.duration) {

let newDuration: CMTime = (newValue as? NSValue)?.timeValue ?? kCMTimeZero

let hasValidDuration = newDuration.isNumeric && newDuration.value != 0
let newDurationSeconds = hasValidDuration ? newDuration.seconds : 0.0

/// UI
setControlBar(isEnable: hasValidDuration, duration: newDurationSeconds)

debugPrint("🎬: hasValidDuration: \(hasValidDuration), newDurationSeconds: \(newDurationSeconds)")
} else if keyPath == #keyPath(AVPlayerItem.status) {
// Display an error if status becomes Failed

/*
Handle `NSNull` value for `NSKeyValueChangeNewKey`, i.e. when
`player.currentItem` is nil.
*/

let newStatus = AVPlayerItemStatus(rawValue: (newValue as? NSNumber)?.intValue ?? 0) ?? .unknown

switch newStatus {
case .failed:
delegate?.videoPlayerViewController(self, didCompletedPlayToEndWithError: playerItem.error)
case .readyToPlay:

var errors: [NSError?] = []

for key in VideoPlayerViewController.assetKeysRequiredToPlay {
var error: NSError?
if playerItem.asset.statusOfValue(forKey: key, error: &error) == .failed {
errors.append(error)
}
}

if !playerItem.asset.isPlayable || playerItem.asset.hasProtectedContent {
delegate?.videoPlayerViewController(self, didCompletedPlayToEndWithError: errors.first as? Error)
return
}

if isAutoPlay { play() }
if let seekTime = requestSeek {
seek(toTime: seekTime)
requestSeek = nil
}
delegate?.videoPlayerViewControllerReadyToPlay(self)
case .unknown:
break
}
debugPrint("🎬: newStatus: \(newStatus.rawValue)")
} else if keyPath == #keyPath(AVPlayerItem.loadedTimeRanges) {

guard let timeRange = playerItem.loadedTimeRanges.first?.timeRangeValue, duration.value != 0 else { return }

let progress = Float((timeRange.start.seconds + timeRange.duration.seconds) / duration.seconds)
videoPlayerView.controlBar.timeSlider.setBufferProgress(progress)
debugPrint("🎬: progress: \(progress)")
}
}

//MARK: - Operation control bar and top bar dispaly and hidden with auto.


/// Operation control bar and top bar.
///
/// - Parameter isHidden: Is hidden?
private func operateControlView(_ isHidden: Bool) {

guard isControlViewAnimationCompleted else { return }
isControlViewAnimationCompleted = false

UIView.animate(withDuration: 0.2, animations: {
isHidden ? {
self.videoPlayerView.topBar.frame.origin.y -= self.videoPlayerView.topBar.frame.size.height + 6
self.videoPlayerView.controlBar.frame.origin.y += self.videoPlayerView.controlBar.frame.size.height + 6
}() : {
self.videoPlayerView.topBar.frame.origin.y += self.videoPlayerView.topBar.frame.size.height + 6
self.videoPlayerView.controlBar.frame.origin.y -= self.videoPlayerView.controlBar.frame.size.height + 6
}()
}, completion: { _ in
self.isControlViewAnimationCompleted = true
self.createTimer()
})
}

/// Create timer for auto hidden
private func createTimer() {
guard !isControlViewHidden, controlViewTimer == nil else { return }

controlViewTimer = Timer(timeInterval: hiddenTimeInterval, target: self, selector: #selector(autoHiddenControlView), userInfo: nil, repeats: false)
RunLoop.main.add(controlViewTimer!, forMode: .commonModes)
}

/// Destory timer
private func destoryTimer() {
controlViewTimer?.invalidate()
controlViewTimer = nil
}

/// Auto hidden control bar if need.
@objc private func autoHiddenControlView() {
destoryTimer()
guard !isControlViewHidden && !isUserSliding else { return }
operateControlView(true)
destoryTimer()
}

//MARK: - UI

private func resetUI() {
/// Default is prepare to play.
if isAutoPlay { switchPlaybackButtonStatus(true) }
setControlBar(isEnable: false, duration: 0)
}

private func switchPlaybackButtonStatus(_ isPlaying: Bool) {
// play: normal, pause: selected
videoPlayerView.playbackButton.isSelected = isPlaying
videoPlayerView.controlBar.playbackButton.isSelected = isPlaying
videoPlayerView.playbackButton.isHidden = isPlaying
}

private func setControlBar(isEnable: Bool, duration: TimeInterval) {

videoPlayerView.controlBar.timeSlider.maximumValue = Float(duration)
videoPlayerView.controlBar.durationTimeLabel.text = Int(duration).timeString

videoPlayerView.controlBar.playbackButton.isEnabled = isEnable
videoPlayerView.playbackButton.isEnabled = isEnable

videoPlayerView.controlBar.timeSlider.isEnabled = isEnable
if !isEnable { videoPlayerView.controlBar.timeSlider.resetBufferProgress() }
}
}

代码中嵌入了很多UI的元素,由于自己后续不太会用系统的播放器,所以并不多详细讲解,具体可以看下面的视频。