1 |
efrain |
1 |
// Player.swift
|
|
|
2 |
//
|
|
|
3 |
// Created by patrick piemonte on 11/26/14.
|
|
|
4 |
//
|
|
|
5 |
// The MIT License (MIT)
|
|
|
6 |
//
|
|
|
7 |
// Copyright (c) 2014-present patrick piemonte (http://patrickpiemonte.com/)
|
|
|
8 |
//
|
|
|
9 |
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
10 |
// of this software and associated documentation files (the "Software"), to deal
|
|
|
11 |
// in the Software without restriction, including without limitation the rights
|
|
|
12 |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
13 |
// copies of the Software, and to permit persons to whom the Software is
|
|
|
14 |
// furnished to do so, subject to the following conditions:
|
|
|
15 |
//
|
|
|
16 |
// The above copyright notice and this permission notice shall be included in all
|
|
|
17 |
// copies or substantial portions of the Software.
|
|
|
18 |
//
|
|
|
19 |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
20 |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
21 |
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
22 |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
23 |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
24 |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
25 |
// SOFTWARE.
|
|
|
26 |
|
|
|
27 |
import UIKit
|
|
|
28 |
import Foundation
|
|
|
29 |
import AVFoundation
|
|
|
30 |
import CoreGraphics
|
|
|
31 |
|
|
|
32 |
// MARK: - error types
|
|
|
33 |
|
|
|
34 |
/// Error domain for all Player errors.
|
|
|
35 |
public let PlayerErrorDomain = "PlayerErrorDomain"
|
|
|
36 |
|
|
|
37 |
/// Error types.
|
|
|
38 |
public enum PlayerError: Error, CustomStringConvertible {
|
|
|
39 |
case failed
|
|
|
40 |
|
|
|
41 |
public var description: String {
|
|
|
42 |
get {
|
|
|
43 |
switch self {
|
|
|
44 |
case .failed:
|
|
|
45 |
return "failed"
|
|
|
46 |
}
|
|
|
47 |
}
|
|
|
48 |
}
|
|
|
49 |
}
|
|
|
50 |
|
|
|
51 |
// MARK: - PlayerDelegate
|
|
|
52 |
|
|
|
53 |
/// Player delegate protocol
|
|
|
54 |
public protocol PlayerDelegate: AnyObject {
|
|
|
55 |
func playerReady(_ player: Player)
|
|
|
56 |
func playerPlaybackStateDidChange(_ player: Player)
|
|
|
57 |
func playerBufferingStateDidChange(_ player: Player)
|
|
|
58 |
|
|
|
59 |
// This is the time in seconds that the video has been buffered.
|
|
|
60 |
// If implementing a UIProgressView, user this value / player.maximumDuration to set progress.
|
|
|
61 |
func playerBufferTimeDidChange(_ bufferTime: Double)
|
|
|
62 |
|
|
|
63 |
func player(_ player: Player, didFailWithError error: Error?)
|
|
|
64 |
}
|
|
|
65 |
|
|
|
66 |
|
|
|
67 |
/// Player playback protocol
|
|
|
68 |
public protocol PlayerPlaybackDelegate: AnyObject {
|
|
|
69 |
func playerCurrentTimeDidChange(_ player: Player)
|
|
|
70 |
func playerPlaybackWillStartFromBeginning(_ player: Player)
|
|
|
71 |
func playerPlaybackDidEnd(_ player: Player)
|
|
|
72 |
func playerPlaybackWillLoop(_ player: Player)
|
|
|
73 |
func playerPlaybackDidLoop(_ player: Player)
|
|
|
74 |
}
|
|
|
75 |
|
|
|
76 |
// MARK: - Player
|
|
|
77 |
|
|
|
78 |
/// â–¶ï¸ Player, simple way to play and stream media
|
|
|
79 |
open class Player: UIViewController {
|
|
|
80 |
|
|
|
81 |
// types
|
|
|
82 |
|
|
|
83 |
/// Video fill mode options for `Player.fillMode`.
|
|
|
84 |
///
|
|
|
85 |
/// - resize: Stretch to fill.
|
|
|
86 |
/// - resizeAspectFill: Preserve aspect ratio, filling bounds.
|
|
|
87 |
/// - resizeAspectFit: Preserve aspect ratio, fill within bounds.
|
|
|
88 |
public typealias FillMode = AVLayerVideoGravity
|
|
|
89 |
|
|
|
90 |
/// Asset playback states.
|
|
|
91 |
public enum PlaybackState: Int, CustomStringConvertible {
|
|
|
92 |
case stopped = 0
|
|
|
93 |
case playing
|
|
|
94 |
case paused
|
|
|
95 |
case failed
|
|
|
96 |
|
|
|
97 |
public var description: String {
|
|
|
98 |
get {
|
|
|
99 |
switch self {
|
|
|
100 |
case .stopped:
|
|
|
101 |
return "Stopped"
|
|
|
102 |
case .playing:
|
|
|
103 |
return "Playing"
|
|
|
104 |
case .failed:
|
|
|
105 |
return "Failed"
|
|
|
106 |
case .paused:
|
|
|
107 |
return "Paused"
|
|
|
108 |
}
|
|
|
109 |
}
|
|
|
110 |
}
|
|
|
111 |
}
|
|
|
112 |
|
|
|
113 |
/// Asset buffering states.
|
|
|
114 |
public enum BufferingState: Int, CustomStringConvertible {
|
|
|
115 |
case unknown = 0
|
|
|
116 |
case ready
|
|
|
117 |
case delayed
|
|
|
118 |
|
|
|
119 |
public var description: String {
|
|
|
120 |
get {
|
|
|
121 |
switch self {
|
|
|
122 |
case .unknown:
|
|
|
123 |
return "Unknown"
|
|
|
124 |
case .ready:
|
|
|
125 |
return "Ready"
|
|
|
126 |
case .delayed:
|
|
|
127 |
return "Delayed"
|
|
|
128 |
}
|
|
|
129 |
}
|
|
|
130 |
}
|
|
|
131 |
}
|
|
|
132 |
|
|
|
133 |
// properties
|
|
|
134 |
|
|
|
135 |
/// Player delegate.
|
|
|
136 |
open weak var playerDelegate: PlayerDelegate?
|
|
|
137 |
|
|
|
138 |
/// Playback delegate.
|
|
|
139 |
open weak var playbackDelegate: PlayerPlaybackDelegate?
|
|
|
140 |
|
|
|
141 |
// configuration
|
|
|
142 |
|
|
|
143 |
/// Local or remote URL for the file asset to be played.
|
|
|
144 |
///
|
|
|
145 |
/// - Parameter url: URL of the asset.
|
|
|
146 |
open var url: URL? {
|
|
|
147 |
didSet {
|
|
|
148 |
if let url = self.url {
|
|
|
149 |
setup(url: url)
|
|
|
150 |
}
|
|
|
151 |
}
|
|
|
152 |
}
|
|
|
153 |
|
|
|
154 |
/// For setting up with AVAsset instead of URL
|
|
|
155 |
/// Note: This will reset the `url` property. (cannot set both)
|
|
|
156 |
open var asset: AVAsset? {
|
|
|
157 |
get { return _asset }
|
|
|
158 |
set { _ = newValue.map { setupAsset($0) } }
|
|
|
159 |
}
|
|
|
160 |
|
|
|
161 |
/// Specifies how the video is displayed within a player layer’s bounds.
|
|
|
162 |
/// The default value is `AVLayerVideoGravityResizeAspect`. See `PlayerFillMode`.
|
|
|
163 |
open var fillMode: Player.FillMode {
|
|
|
164 |
get {
|
|
|
165 |
return self._playerView.playerFillMode
|
|
|
166 |
}
|
|
|
167 |
set {
|
|
|
168 |
self._playerView.playerFillMode = newValue
|
|
|
169 |
}
|
|
|
170 |
}
|
|
|
171 |
|
|
|
172 |
/// Determines if the video should autoplay when streaming a URL.
|
|
|
173 |
open var autoplay: Bool = true
|
|
|
174 |
|
|
|
175 |
/// Mutes audio playback when true.
|
|
|
176 |
open var muted: Bool {
|
|
|
177 |
get {
|
|
|
178 |
return self._avplayer.isMuted
|
|
|
179 |
}
|
|
|
180 |
set {
|
|
|
181 |
self._avplayer.isMuted = newValue
|
|
|
182 |
}
|
|
|
183 |
}
|
|
|
184 |
|
|
|
185 |
/// Volume for the player, ranging from 0.0 to 1.0 on a linear scale.
|
|
|
186 |
open var volume: Float {
|
|
|
187 |
get {
|
|
|
188 |
return self._avplayer.volume
|
|
|
189 |
}
|
|
|
190 |
set {
|
|
|
191 |
self._avplayer.volume = newValue
|
|
|
192 |
}
|
|
|
193 |
}
|
|
|
194 |
|
|
|
195 |
/// Pauses playback automatically when resigning active.
|
|
|
196 |
open var playbackPausesWhenResigningActive: Bool = true
|
|
|
197 |
|
|
|
198 |
/// Pauses playback automatically when backgrounded.
|
|
|
199 |
open var playbackPausesWhenBackgrounded: Bool = true
|
|
|
200 |
|
|
|
201 |
/// Resumes playback when became active.
|
|
|
202 |
open var playbackResumesWhenBecameActive: Bool = true
|
|
|
203 |
|
|
|
204 |
/// Resumes playback when entering foreground.
|
|
|
205 |
open var playbackResumesWhenEnteringForeground: Bool = true
|
|
|
206 |
|
|
|
207 |
// state
|
|
|
208 |
|
|
|
209 |
open var isPlayingVideo: Bool {
|
|
|
210 |
get {
|
|
|
211 |
guard let asset = self._asset else {
|
|
|
212 |
return false
|
|
|
213 |
}
|
|
|
214 |
return asset.tracks(withMediaType: .video).count != 0
|
|
|
215 |
}
|
|
|
216 |
}
|
|
|
217 |
|
|
|
218 |
/// Playback automatically loops continuously when true.
|
|
|
219 |
open var playbackLoops: Bool {
|
|
|
220 |
get {
|
|
|
221 |
return self._avplayer.actionAtItemEnd == .none
|
|
|
222 |
}
|
|
|
223 |
set {
|
|
|
224 |
if newValue {
|
|
|
225 |
self._avplayer.actionAtItemEnd = .none
|
|
|
226 |
} else {
|
|
|
227 |
self._avplayer.actionAtItemEnd = .pause
|
|
|
228 |
}
|
|
|
229 |
}
|
|
|
230 |
}
|
|
|
231 |
|
|
|
232 |
/// Playback freezes on last frame frame when true and does not reset seek position timestamp..
|
|
|
233 |
open var playbackFreezesAtEnd: Bool = false
|
|
|
234 |
|
|
|
235 |
/// Current playback state of the Player.
|
|
|
236 |
open var playbackState: PlaybackState = .stopped {
|
|
|
237 |
didSet {
|
|
|
238 |
if playbackState != oldValue || !playbackEdgeTriggered {
|
|
|
239 |
self.executeClosureOnMainQueueIfNecessary {
|
|
|
240 |
self.playerDelegate?.playerPlaybackStateDidChange(self)
|
|
|
241 |
}
|
|
|
242 |
}
|
|
|
243 |
}
|
|
|
244 |
}
|
|
|
245 |
|
|
|
246 |
/// Current buffering state of the Player.
|
|
|
247 |
open var bufferingState: BufferingState = .unknown {
|
|
|
248 |
didSet {
|
|
|
249 |
if bufferingState != oldValue || !playbackEdgeTriggered {
|
|
|
250 |
self.executeClosureOnMainQueueIfNecessary {
|
|
|
251 |
self.playerDelegate?.playerBufferingStateDidChange(self)
|
|
|
252 |
}
|
|
|
253 |
}
|
|
|
254 |
}
|
|
|
255 |
}
|
|
|
256 |
|
|
|
257 |
/// Playback buffering size in seconds.
|
|
|
258 |
open var bufferSizeInSeconds: Double = 10
|
|
|
259 |
|
|
|
260 |
/// Playback is not automatically triggered from state changes when true.
|
|
|
261 |
open var playbackEdgeTriggered: Bool = true
|
|
|
262 |
|
|
|
263 |
/// Maximum duration of playback.
|
|
|
264 |
open var maximumDuration: TimeInterval {
|
|
|
265 |
get {
|
|
|
266 |
if let playerItem = self._playerItem {
|
|
|
267 |
return CMTimeGetSeconds(playerItem.duration)
|
|
|
268 |
} else {
|
|
|
269 |
return CMTimeGetSeconds(CMTime.indefinite)
|
|
|
270 |
}
|
|
|
271 |
}
|
|
|
272 |
}
|
|
|
273 |
|
|
|
274 |
/// Media playback's current time interval in seconds.
|
|
|
275 |
open var currentTimeInterval: TimeInterval {
|
|
|
276 |
get {
|
|
|
277 |
if let playerItem = self._playerItem {
|
|
|
278 |
return CMTimeGetSeconds(playerItem.currentTime())
|
|
|
279 |
} else {
|
|
|
280 |
return CMTimeGetSeconds(CMTime.indefinite)
|
|
|
281 |
}
|
|
|
282 |
}
|
|
|
283 |
}
|
|
|
284 |
|
|
|
285 |
/// Media playback's current time.
|
|
|
286 |
open var currentTime: CMTime {
|
|
|
287 |
get {
|
|
|
288 |
if let playerItem = self._playerItem {
|
|
|
289 |
return playerItem.currentTime()
|
|
|
290 |
} else {
|
|
|
291 |
return CMTime.indefinite
|
|
|
292 |
}
|
|
|
293 |
}
|
|
|
294 |
}
|
|
|
295 |
|
|
|
296 |
/// The natural dimensions of the media.
|
|
|
297 |
open var naturalSize: CGSize {
|
|
|
298 |
get {
|
|
|
299 |
if let playerItem = self._playerItem,
|
|
|
300 |
let track = playerItem.asset.tracks(withMediaType: .video).first {
|
|
|
301 |
|
|
|
302 |
let size = track.naturalSize.applying(track.preferredTransform)
|
|
|
303 |
return CGSize(width: abs(size.width), height: abs(size.height))
|
|
|
304 |
} else {
|
|
|
305 |
return CGSize.zero
|
|
|
306 |
}
|
|
|
307 |
}
|
|
|
308 |
}
|
|
|
309 |
|
|
|
310 |
/// self.view as PlayerView type
|
|
|
311 |
public var playerView: PlayerView {
|
|
|
312 |
get {
|
|
|
313 |
return self._playerView
|
|
|
314 |
}
|
|
|
315 |
}
|
|
|
316 |
|
|
|
317 |
/// Return the av player layer for consumption by things such as Picture in Picture
|
|
|
318 |
open func playerLayer() -> AVPlayerLayer? {
|
|
|
319 |
return self._playerView.playerLayer
|
|
|
320 |
}
|
|
|
321 |
|
|
|
322 |
/// Indicates the desired limit of network bandwidth consumption for this item.
|
|
|
323 |
open var preferredPeakBitRate: Double = 0 {
|
|
|
324 |
didSet {
|
|
|
325 |
self._playerItem?.preferredPeakBitRate = self.preferredPeakBitRate
|
|
|
326 |
}
|
|
|
327 |
}
|
|
|
328 |
|
|
|
329 |
/// Indicates a preferred upper limit on the resolution of the video to be downloaded.
|
|
|
330 |
@available(iOS 11.0, tvOS 11.0, *)
|
|
|
331 |
open var preferredMaximumResolution: CGSize {
|
|
|
332 |
get {
|
|
|
333 |
return self._playerItem?.preferredMaximumResolution ?? CGSize.zero
|
|
|
334 |
}
|
|
|
335 |
set {
|
|
|
336 |
self._playerItem?.preferredMaximumResolution = newValue
|
|
|
337 |
self._preferredMaximumResolution = newValue
|
|
|
338 |
}
|
|
|
339 |
}
|
|
|
340 |
|
|
|
341 |
// MARK: - private instance vars
|
|
|
342 |
|
|
|
343 |
internal var _asset: AVAsset? {
|
|
|
344 |
didSet {
|
|
|
345 |
if let _ = self._asset {
|
|
|
346 |
self.setupPlayerItem(nil)
|
|
|
347 |
}
|
|
|
348 |
}
|
|
|
349 |
}
|
|
|
350 |
internal lazy var _avplayer: AVPlayer = {
|
|
|
351 |
let avplayer = AVPlayer()
|
|
|
352 |
avplayer.actionAtItemEnd = .pause
|
|
|
353 |
return avplayer
|
|
|
354 |
}()
|
|
|
355 |
internal var _playerItem: AVPlayerItem?
|
|
|
356 |
|
|
|
357 |
internal var _playerObservers = [NSKeyValueObservation]()
|
|
|
358 |
internal var _playerItemObservers = [NSKeyValueObservation]()
|
|
|
359 |
internal var _playerLayerObserver: NSKeyValueObservation?
|
|
|
360 |
internal var _playerTimeObserver: Any?
|
|
|
361 |
|
|
|
362 |
internal var _playerView: PlayerView = PlayerView(frame: .zero)
|
|
|
363 |
internal var _seekTimeRequested: CMTime?
|
|
|
364 |
internal var _lastBufferTime: Double = 0
|
|
|
365 |
internal var _preferredMaximumResolution: CGSize = .zero
|
|
|
366 |
|
|
|
367 |
// Boolean that determines if the user or calling coded has trigged autoplay manually.
|
|
|
368 |
internal var _hasAutoplayActivated: Bool = true
|
|
|
369 |
|
|
|
370 |
// MARK: - object lifecycle
|
|
|
371 |
|
|
|
372 |
public convenience init() {
|
|
|
373 |
self.init(nibName: nil, bundle: nil)
|
|
|
374 |
}
|
|
|
375 |
|
|
|
376 |
public required init?(coder aDecoder: NSCoder) {
|
|
|
377 |
super.init(coder: aDecoder)
|
|
|
378 |
}
|
|
|
379 |
|
|
|
380 |
public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
|
|
|
381 |
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
|
|
|
382 |
}
|
|
|
383 |
|
|
|
384 |
deinit {
|
|
|
385 |
self._avplayer.pause()
|
|
|
386 |
self.setupPlayerItem(nil)
|
|
|
387 |
|
|
|
388 |
self.removePlayerObservers()
|
|
|
389 |
|
|
|
390 |
self.playerDelegate = nil
|
|
|
391 |
self.removeApplicationObservers()
|
|
|
392 |
|
|
|
393 |
self.playbackDelegate = nil
|
|
|
394 |
self.removePlayerLayerObservers()
|
|
|
395 |
|
|
|
396 |
self._playerView.player = nil
|
|
|
397 |
}
|
|
|
398 |
|
|
|
399 |
// MARK: - view lifecycle
|
|
|
400 |
|
|
|
401 |
open override func loadView() {
|
|
|
402 |
super.loadView()
|
|
|
403 |
self._playerView.frame = self.view.bounds
|
|
|
404 |
self.view = self._playerView
|
|
|
405 |
}
|
|
|
406 |
|
|
|
407 |
open override func viewDidLoad() {
|
|
|
408 |
super.viewDidLoad()
|
|
|
409 |
self._playerView.player = self._avplayer
|
|
|
410 |
|
|
|
411 |
if let url = self.url {
|
|
|
412 |
setup(url: url)
|
|
|
413 |
} else if let asset = self.asset {
|
|
|
414 |
setupAsset(asset)
|
|
|
415 |
}
|
|
|
416 |
|
|
|
417 |
self.addPlayerLayerObservers()
|
|
|
418 |
self.addPlayerObservers()
|
|
|
419 |
self.addApplicationObservers()
|
|
|
420 |
}
|
|
|
421 |
|
|
|
422 |
open override func viewDidDisappear(_ animated: Bool) {
|
|
|
423 |
super.viewDidDisappear(animated)
|
|
|
424 |
if self.playbackState == .playing {
|
|
|
425 |
self.pause()
|
|
|
426 |
}
|
|
|
427 |
}
|
|
|
428 |
|
|
|
429 |
}
|
|
|
430 |
|
|
|
431 |
// MARK: - performance
|
|
|
432 |
|
|
|
433 |
extension Player {
|
|
|
434 |
|
|
|
435 |
/// Total time spent playing.
|
|
|
436 |
public var totalDurationWatched: TimeInterval {
|
|
|
437 |
get {
|
|
|
438 |
var totalDurationWatched = 0.0
|
|
|
439 |
if let accessLog = self._playerItem?.accessLog(), accessLog.events.isEmpty == false {
|
|
|
440 |
for event in accessLog.events where event.durationWatched > 0 {
|
|
|
441 |
totalDurationWatched += event.durationWatched
|
|
|
442 |
}
|
|
|
443 |
}
|
|
|
444 |
return totalDurationWatched
|
|
|
445 |
}
|
|
|
446 |
}
|
|
|
447 |
|
|
|
448 |
/// Time weighted value of the variant indicated bitrate. Measure of overall stream quality.
|
|
|
449 |
var timeWeightedIBR: Double {
|
|
|
450 |
var timeWeightedIBR = 0.0
|
|
|
451 |
let totalDurationWatched = self.totalDurationWatched
|
|
|
452 |
|
|
|
453 |
if let accessLog = self._playerItem?.accessLog(), totalDurationWatched > 0 {
|
|
|
454 |
for event in accessLog.events {
|
|
|
455 |
if event.durationWatched > 0 && event.indicatedBitrate > 0 {
|
|
|
456 |
let eventTimeWeight = event.durationWatched / totalDurationWatched
|
|
|
457 |
timeWeightedIBR += event.indicatedBitrate * eventTimeWeight
|
|
|
458 |
}
|
|
|
459 |
}
|
|
|
460 |
}
|
|
|
461 |
return timeWeightedIBR
|
|
|
462 |
}
|
|
|
463 |
|
|
|
464 |
/// Stall rate measured in stalls per hour. Normalized measure of stream interruptions caused by stream buffer depleation.
|
|
|
465 |
var stallRate: Double {
|
|
|
466 |
var totalNumberOfStalls = 0
|
|
|
467 |
let totalHoursWatched = self.totalDurationWatched / 3600
|
|
|
468 |
|
|
|
469 |
if let accessLog = self._playerItem?.accessLog(), totalDurationWatched > 0 {
|
|
|
470 |
for event in accessLog.events {
|
|
|
471 |
totalNumberOfStalls += event.numberOfStalls
|
|
|
472 |
}
|
|
|
473 |
}
|
|
|
474 |
return Double(totalNumberOfStalls) / totalHoursWatched
|
|
|
475 |
}
|
|
|
476 |
|
|
|
477 |
}
|
|
|
478 |
|
|
|
479 |
// MARK: - actions
|
|
|
480 |
|
|
|
481 |
extension Player {
|
|
|
482 |
|
|
|
483 |
/// Begins playback of the media from the beginning.
|
|
|
484 |
open func playFromBeginning() {
|
|
|
485 |
self.playbackDelegate?.playerPlaybackWillStartFromBeginning(self)
|
|
|
486 |
self._avplayer.seek(to: CMTime.zero)
|
|
|
487 |
self.playFromCurrentTime()
|
|
|
488 |
}
|
|
|
489 |
|
|
|
490 |
/// Begins playback of the media from the current time.
|
|
|
491 |
open func playFromCurrentTime() {
|
|
|
492 |
if !self.autoplay {
|
|
|
493 |
// External call to this method with autoplay disabled. Re-activate it before calling play.
|
|
|
494 |
self._hasAutoplayActivated = true
|
|
|
495 |
}
|
|
|
496 |
self.play()
|
|
|
497 |
}
|
|
|
498 |
|
|
|
499 |
fileprivate func play() {
|
|
|
500 |
if self.autoplay || self._hasAutoplayActivated {
|
|
|
501 |
self.playbackState = .playing
|
|
|
502 |
self._avplayer.play()
|
|
|
503 |
}
|
|
|
504 |
}
|
|
|
505 |
|
|
|
506 |
/// Pauses playback of the media.
|
|
|
507 |
open func pause() {
|
|
|
508 |
if self.playbackState != .playing {
|
|
|
509 |
return
|
|
|
510 |
}
|
|
|
511 |
|
|
|
512 |
self._avplayer.pause()
|
|
|
513 |
self.playbackState = .paused
|
|
|
514 |
}
|
|
|
515 |
|
|
|
516 |
/// Stops playback of the media.
|
|
|
517 |
open func stop() {
|
|
|
518 |
if self.playbackState == .stopped {
|
|
|
519 |
return
|
|
|
520 |
}
|
|
|
521 |
|
|
|
522 |
self._avplayer.pause()
|
|
|
523 |
self.playbackState = .stopped
|
|
|
524 |
self.playbackDelegate?.playerPlaybackDidEnd(self)
|
|
|
525 |
}
|
|
|
526 |
|
|
|
527 |
/// Updates playback to the specified time.
|
|
|
528 |
///
|
|
|
529 |
/// - Parameters:
|
|
|
530 |
/// - time: The time to switch to move the playback.
|
|
|
531 |
/// - completionHandler: Call block handler after seeking/
|
|
|
532 |
open func seek(to time: CMTime, completionHandler: ((Bool) -> Swift.Void)? = nil) {
|
|
|
533 |
if let playerItem = self._playerItem {
|
|
|
534 |
return playerItem.seek(to: time, completionHandler: completionHandler)
|
|
|
535 |
} else {
|
|
|
536 |
self._seekTimeRequested = time
|
|
|
537 |
}
|
|
|
538 |
}
|
|
|
539 |
|
|
|
540 |
/// Updates the playback time to the specified time bound.
|
|
|
541 |
///
|
|
|
542 |
/// - Parameters:
|
|
|
543 |
/// - time: The time to switch to move the playback.
|
|
|
544 |
/// - toleranceBefore: The tolerance allowed before time.
|
|
|
545 |
/// - toleranceAfter: The tolerance allowed after time.
|
|
|
546 |
/// - completionHandler: call block handler after seeking
|
|
|
547 |
open func seekToTime(to time: CMTime, toleranceBefore: CMTime, toleranceAfter: CMTime, completionHandler: ((Bool) -> Swift.Void)? = nil) {
|
|
|
548 |
if let playerItem = self._playerItem {
|
|
|
549 |
return playerItem.seek(to: time, toleranceBefore: toleranceBefore, toleranceAfter: toleranceAfter, completionHandler: completionHandler)
|
|
|
550 |
}
|
|
|
551 |
}
|
|
|
552 |
|
|
|
553 |
/// Captures a snapshot of the current Player asset.
|
|
|
554 |
///
|
|
|
555 |
/// - Parameter completionHandler: Returns a UIImage of the requested video frame. (Great for thumbnails!)
|
|
|
556 |
open func takeSnapshot(completionHandler: ((_ image: UIImage?, _ error: Error?) -> Void)? ) {
|
|
|
557 |
guard let asset = self._playerItem?.asset else {
|
|
|
558 |
DispatchQueue.main.async {
|
|
|
559 |
completionHandler?(nil, nil)
|
|
|
560 |
}
|
|
|
561 |
return
|
|
|
562 |
}
|
|
|
563 |
|
|
|
564 |
let imageGenerator = AVAssetImageGenerator(asset: asset)
|
|
|
565 |
imageGenerator.appliesPreferredTrackTransform = true
|
|
|
566 |
|
|
|
567 |
let currentTime = self._playerItem?.currentTime() ?? CMTime.zero
|
|
|
568 |
|
|
|
569 |
imageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: currentTime)]) { (requestedTime, image, actualTime, result, error) in
|
|
|
570 |
guard let image = image else {
|
|
|
571 |
DispatchQueue.main.async {
|
|
|
572 |
completionHandler?(nil, error)
|
|
|
573 |
}
|
|
|
574 |
return
|
|
|
575 |
}
|
|
|
576 |
|
|
|
577 |
switch result {
|
|
|
578 |
case .succeeded:
|
|
|
579 |
let uiimage = UIImage(cgImage: image)
|
|
|
580 |
DispatchQueue.main.async {
|
|
|
581 |
completionHandler?(uiimage, nil)
|
|
|
582 |
}
|
|
|
583 |
break
|
|
|
584 |
case .failed, .cancelled:
|
|
|
585 |
fallthrough
|
|
|
586 |
@unknown default:
|
|
|
587 |
DispatchQueue.main.async {
|
|
|
588 |
completionHandler?(nil, nil)
|
|
|
589 |
}
|
|
|
590 |
break
|
|
|
591 |
}
|
|
|
592 |
}
|
|
|
593 |
}
|
|
|
594 |
|
|
|
595 |
}
|
|
|
596 |
|
|
|
597 |
// MARK: - loading funcs
|
|
|
598 |
|
|
|
599 |
extension Player {
|
|
|
600 |
|
|
|
601 |
fileprivate func setup(url: URL) {
|
|
|
602 |
guard isViewLoaded else { return }
|
|
|
603 |
|
|
|
604 |
// ensure everything is reset beforehand
|
|
|
605 |
if self.playbackState == .playing {
|
|
|
606 |
self.pause()
|
|
|
607 |
}
|
|
|
608 |
|
|
|
609 |
// Reset autoplay flag since a new url is set.
|
|
|
610 |
self._hasAutoplayActivated = false
|
|
|
611 |
if self.autoplay {
|
|
|
612 |
self.playbackState = .playing
|
|
|
613 |
} else {
|
|
|
614 |
self.playbackState = .stopped
|
|
|
615 |
}
|
|
|
616 |
|
|
|
617 |
self.setupPlayerItem(nil)
|
|
|
618 |
|
|
|
619 |
let asset = AVURLAsset(url: url, options: .none)
|
|
|
620 |
self.setupAsset(asset)
|
|
|
621 |
}
|
|
|
622 |
|
|
|
623 |
fileprivate func setupAsset(_ asset: AVAsset, loadableKeys: [String] = ["tracks", "playable", "duration"]) {
|
|
|
624 |
guard isViewLoaded else { return }
|
|
|
625 |
|
|
|
626 |
if self.playbackState == .playing {
|
|
|
627 |
self.pause()
|
|
|
628 |
}
|
|
|
629 |
|
|
|
630 |
self.bufferingState = .unknown
|
|
|
631 |
|
|
|
632 |
self._asset = asset
|
|
|
633 |
|
|
|
634 |
self._asset?.loadValuesAsynchronously(forKeys: loadableKeys, completionHandler: { () -> Void in
|
|
|
635 |
guard let asset = self._asset else {
|
|
|
636 |
return
|
|
|
637 |
}
|
|
|
638 |
|
|
|
639 |
for key in loadableKeys {
|
|
|
640 |
var error: NSError? = nil
|
|
|
641 |
let status = asset.statusOfValue(forKey: key, error: &error)
|
|
|
642 |
if status == .failed {
|
|
|
643 |
self.playbackState = .failed
|
|
|
644 |
self.executeClosureOnMainQueueIfNecessary {
|
|
|
645 |
self.playerDelegate?.player(self, didFailWithError: error)
|
|
|
646 |
}
|
|
|
647 |
return
|
|
|
648 |
}
|
|
|
649 |
}
|
|
|
650 |
|
|
|
651 |
if !asset.isPlayable {
|
|
|
652 |
self.playbackState = .failed
|
|
|
653 |
self.executeClosureOnMainQueueIfNecessary {
|
|
|
654 |
self.playerDelegate?.player(self, didFailWithError: PlayerError.failed)
|
|
|
655 |
}
|
|
|
656 |
return
|
|
|
657 |
}
|
|
|
658 |
|
|
|
659 |
let playerItem = AVPlayerItem(asset:asset)
|
|
|
660 |
self.setupPlayerItem(playerItem)
|
|
|
661 |
})
|
|
|
662 |
}
|
|
|
663 |
|
|
|
664 |
fileprivate func setupPlayerItem(_ playerItem: AVPlayerItem?) {
|
|
|
665 |
|
|
|
666 |
self.removePlayerItemObservers()
|
|
|
667 |
|
|
|
668 |
if let currentPlayerItem = self._playerItem {
|
|
|
669 |
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: currentPlayerItem)
|
|
|
670 |
NotificationCenter.default.removeObserver(self, name: .AVPlayerItemFailedToPlayToEndTime, object: currentPlayerItem)
|
|
|
671 |
}
|
|
|
672 |
|
|
|
673 |
self._playerItem = playerItem
|
|
|
674 |
|
|
|
675 |
self._playerItem?.preferredPeakBitRate = self.preferredPeakBitRate
|
|
|
676 |
if #available(iOS 11.0, tvOS 11.0, *) {
|
|
|
677 |
self._playerItem?.preferredMaximumResolution = self._preferredMaximumResolution
|
|
|
678 |
}
|
|
|
679 |
|
|
|
680 |
if let seek = self._seekTimeRequested, self._playerItem != nil {
|
|
|
681 |
self._seekTimeRequested = nil
|
|
|
682 |
self.seek(to: seek)
|
|
|
683 |
}
|
|
|
684 |
|
|
|
685 |
if let updatedPlayerItem = self._playerItem {
|
|
|
686 |
self.addPlayerItemObservers()
|
|
|
687 |
NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidPlayToEndTime(_:)), name: .AVPlayerItemDidPlayToEndTime, object: updatedPlayerItem)
|
|
|
688 |
NotificationCenter.default.addObserver(self, selector: #selector(playerItemFailedToPlayToEndTime(_:)), name: .AVPlayerItemFailedToPlayToEndTime, object: updatedPlayerItem)
|
|
|
689 |
}
|
|
|
690 |
|
|
|
691 |
self._avplayer.replaceCurrentItem(with: self._playerItem)
|
|
|
692 |
|
|
|
693 |
// update new playerItem settings
|
|
|
694 |
if self.playbackLoops {
|
|
|
695 |
self._avplayer.actionAtItemEnd = .none
|
|
|
696 |
} else {
|
|
|
697 |
self._avplayer.actionAtItemEnd = .pause
|
|
|
698 |
}
|
|
|
699 |
}
|
|
|
700 |
|
|
|
701 |
}
|
|
|
702 |
|
|
|
703 |
// MARK: - NSNotifications
|
|
|
704 |
|
|
|
705 |
extension Player {
|
|
|
706 |
|
|
|
707 |
// MARK: - UIApplication
|
|
|
708 |
|
|
|
709 |
internal func addApplicationObservers() {
|
|
|
710 |
NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationWillResignActive(_:)), name: UIApplication.willResignActiveNotification, object: nil)
|
|
|
711 |
NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
|
712 |
NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
|
713 |
NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
|
714 |
}
|
|
|
715 |
|
|
|
716 |
internal func removeApplicationObservers() {
|
|
|
717 |
NotificationCenter.default.removeObserver(self)
|
|
|
718 |
}
|
|
|
719 |
|
|
|
720 |
// MARK: - AVPlayerItem handlers
|
|
|
721 |
|
|
|
722 |
@objc internal func playerItemDidPlayToEndTime(_ aNotification: Notification) {
|
|
|
723 |
self.executeClosureOnMainQueueIfNecessary {
|
|
|
724 |
if self.playbackLoops {
|
|
|
725 |
self.playbackDelegate?.playerPlaybackWillLoop(self)
|
|
|
726 |
self._avplayer.seek(to: CMTime.zero)
|
|
|
727 |
self._avplayer.play()
|
|
|
728 |
self.playbackDelegate?.playerPlaybackDidLoop(self)
|
|
|
729 |
} else if self.playbackFreezesAtEnd {
|
|
|
730 |
self.stop()
|
|
|
731 |
} else {
|
|
|
732 |
self._avplayer.seek(to: CMTime.zero, completionHandler: { _ in
|
|
|
733 |
self.stop()
|
|
|
734 |
})
|
|
|
735 |
}
|
|
|
736 |
}
|
|
|
737 |
}
|
|
|
738 |
|
|
|
739 |
@objc internal func playerItemFailedToPlayToEndTime(_ aNotification: Notification) {
|
|
|
740 |
self.playbackState = .failed
|
|
|
741 |
}
|
|
|
742 |
|
|
|
743 |
// MARK: - UIApplication handlers
|
|
|
744 |
|
|
|
745 |
@objc internal func handleApplicationWillResignActive(_ aNotification: Notification) {
|
|
|
746 |
if self.playbackState == .playing && self.playbackPausesWhenResigningActive {
|
|
|
747 |
self.pause()
|
|
|
748 |
}
|
|
|
749 |
}
|
|
|
750 |
|
|
|
751 |
@objc internal func handleApplicationDidBecomeActive(_ aNotification: Notification) {
|
|
|
752 |
if self.playbackState == .paused && self.playbackResumesWhenBecameActive {
|
|
|
753 |
self.play()
|
|
|
754 |
}
|
|
|
755 |
}
|
|
|
756 |
|
|
|
757 |
@objc internal func handleApplicationDidEnterBackground(_ aNotification: Notification) {
|
|
|
758 |
if self.playbackState == .playing && self.playbackPausesWhenBackgrounded {
|
|
|
759 |
self.pause()
|
|
|
760 |
}
|
|
|
761 |
}
|
|
|
762 |
|
|
|
763 |
@objc internal func handleApplicationWillEnterForeground(_ aNoticiation: Notification) {
|
|
|
764 |
if self.playbackState != .playing && self.playbackResumesWhenEnteringForeground {
|
|
|
765 |
self.play()
|
|
|
766 |
}
|
|
|
767 |
}
|
|
|
768 |
|
|
|
769 |
}
|
|
|
770 |
|
|
|
771 |
// MARK: - KVO
|
|
|
772 |
|
|
|
773 |
extension Player {
|
|
|
774 |
|
|
|
775 |
// MARK: - AVPlayerItemObservers
|
|
|
776 |
|
|
|
777 |
internal func addPlayerItemObservers() {
|
|
|
778 |
guard let playerItem = self._playerItem else {
|
|
|
779 |
return
|
|
|
780 |
}
|
|
|
781 |
|
|
|
782 |
self._playerItemObservers.append(playerItem.observe(\.isPlaybackBufferEmpty, options: [.new, .old]) { [weak self] (object, change) in
|
|
|
783 |
if object.isPlaybackBufferEmpty {
|
|
|
784 |
self?.bufferingState = .delayed
|
|
|
785 |
}
|
|
|
786 |
|
|
|
787 |
switch object.status {
|
|
|
788 |
case .failed:
|
|
|
789 |
self?.playbackState = PlaybackState.failed
|
|
|
790 |
default:
|
|
|
791 |
break
|
|
|
792 |
}
|
|
|
793 |
})
|
|
|
794 |
|
|
|
795 |
self._playerItemObservers.append(playerItem.observe(\.isPlaybackLikelyToKeepUp, options: [.new, .old]) { [weak self] (object, change) in
|
|
|
796 |
guard let strongSelf = self else {
|
|
|
797 |
return
|
|
|
798 |
}
|
|
|
799 |
|
|
|
800 |
if object.isPlaybackLikelyToKeepUp {
|
|
|
801 |
strongSelf.bufferingState = .ready
|
|
|
802 |
if strongSelf.playbackState == .playing {
|
|
|
803 |
strongSelf.playFromCurrentTime()
|
|
|
804 |
}
|
|
|
805 |
}
|
|
|
806 |
|
|
|
807 |
switch object.status {
|
|
|
808 |
case .failed:
|
|
|
809 |
strongSelf.playbackState = PlaybackState.failed
|
|
|
810 |
break
|
|
|
811 |
default:
|
|
|
812 |
break
|
|
|
813 |
}
|
|
|
814 |
})
|
|
|
815 |
|
|
|
816 |
self._playerItemObservers.append(playerItem.observe(\.loadedTimeRanges, options: [.new, .old]) { [weak self] (object, change) in
|
|
|
817 |
guard let strongSelf = self else {
|
|
|
818 |
return
|
|
|
819 |
}
|
|
|
820 |
|
|
|
821 |
let timeRanges = object.loadedTimeRanges
|
|
|
822 |
if let timeRange = timeRanges.first?.timeRangeValue {
|
|
|
823 |
let bufferedTime = CMTimeGetSeconds(CMTimeAdd(timeRange.start, timeRange.duration))
|
|
|
824 |
if strongSelf._lastBufferTime != bufferedTime {
|
|
|
825 |
strongSelf._lastBufferTime = bufferedTime
|
|
|
826 |
strongSelf.executeClosureOnMainQueueIfNecessary {
|
|
|
827 |
strongSelf.playerDelegate?.playerBufferTimeDidChange(bufferedTime)
|
|
|
828 |
}
|
|
|
829 |
}
|
|
|
830 |
}
|
|
|
831 |
|
|
|
832 |
let currentTime = CMTimeGetSeconds(object.currentTime())
|
|
|
833 |
let passedTime = strongSelf._lastBufferTime <= 0 ? currentTime : (strongSelf._lastBufferTime - currentTime)
|
|
|
834 |
|
|
|
835 |
if (passedTime >= strongSelf.bufferSizeInSeconds ||
|
|
|
836 |
strongSelf._lastBufferTime == strongSelf.maximumDuration ||
|
|
|
837 |
timeRanges.first == nil) &&
|
|
|
838 |
strongSelf.playbackState == .playing {
|
|
|
839 |
strongSelf.play()
|
|
|
840 |
}
|
|
|
841 |
})
|
|
|
842 |
}
|
|
|
843 |
|
|
|
844 |
internal func removePlayerItemObservers() {
|
|
|
845 |
for observer in self._playerItemObservers {
|
|
|
846 |
observer.invalidate()
|
|
|
847 |
}
|
|
|
848 |
self._playerItemObservers.removeAll()
|
|
|
849 |
}
|
|
|
850 |
|
|
|
851 |
// MARK: - AVPlayerLayerObservers
|
|
|
852 |
|
|
|
853 |
internal func addPlayerLayerObservers() {
|
|
|
854 |
self._playerLayerObserver = self._playerView.playerLayer.observe(\.isReadyForDisplay, options: [.new, .old]) { [weak self] (object, change) in
|
|
|
855 |
self?.executeClosureOnMainQueueIfNecessary {
|
|
|
856 |
if let strongSelf = self {
|
|
|
857 |
strongSelf.playerDelegate?.playerReady(strongSelf)
|
|
|
858 |
}
|
|
|
859 |
}
|
|
|
860 |
}
|
|
|
861 |
}
|
|
|
862 |
|
|
|
863 |
internal func removePlayerLayerObservers() {
|
|
|
864 |
self._playerLayerObserver?.invalidate()
|
|
|
865 |
self._playerLayerObserver = nil
|
|
|
866 |
}
|
|
|
867 |
|
|
|
868 |
// MARK: - AVPlayerObservers
|
|
|
869 |
|
|
|
870 |
internal func addPlayerObservers() {
|
|
|
871 |
self._playerTimeObserver = self._avplayer.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 100), queue: DispatchQueue.main, using: { [weak self] timeInterval in
|
|
|
872 |
guard let strongSelf = self else {
|
|
|
873 |
return
|
|
|
874 |
}
|
|
|
875 |
strongSelf.playbackDelegate?.playerCurrentTimeDidChange(strongSelf)
|
|
|
876 |
})
|
|
|
877 |
|
|
|
878 |
if #available(iOS 10.0, tvOS 10.0, *) {
|
|
|
879 |
self._playerObservers.append(self._avplayer.observe(\.timeControlStatus, options: [.new, .old]) { [weak self] (object, change) in
|
|
|
880 |
switch object.timeControlStatus {
|
|
|
881 |
case .paused:
|
|
|
882 |
self?.playbackState = .paused
|
|
|
883 |
case .playing:
|
|
|
884 |
self?.playbackState = .playing
|
|
|
885 |
case .waitingToPlayAtSpecifiedRate:
|
|
|
886 |
fallthrough
|
|
|
887 |
@unknown default:
|
|
|
888 |
break
|
|
|
889 |
}
|
|
|
890 |
})
|
|
|
891 |
}
|
|
|
892 |
|
|
|
893 |
}
|
|
|
894 |
|
|
|
895 |
internal func removePlayerObservers() {
|
|
|
896 |
if let observer = self._playerTimeObserver {
|
|
|
897 |
self._avplayer.removeTimeObserver(observer)
|
|
|
898 |
}
|
|
|
899 |
for observer in self._playerObservers {
|
|
|
900 |
observer.invalidate()
|
|
|
901 |
}
|
|
|
902 |
self._playerObservers.removeAll()
|
|
|
903 |
}
|
|
|
904 |
|
|
|
905 |
}
|
|
|
906 |
|
|
|
907 |
// MARK: - queues
|
|
|
908 |
|
|
|
909 |
extension Player {
|
|
|
910 |
|
|
|
911 |
internal func executeClosureOnMainQueueIfNecessary(withClosure closure: @escaping () -> Void) {
|
|
|
912 |
if Thread.isMainThread {
|
|
|
913 |
closure()
|
|
|
914 |
} else {
|
|
|
915 |
DispatchQueue.main.async(execute: closure)
|
|
|
916 |
}
|
|
|
917 |
}
|
|
|
918 |
|
|
|
919 |
}
|
|
|
920 |
|
|
|
921 |
// MARK: - PlayerView
|
|
|
922 |
|
|
|
923 |
public class PlayerView: UIView {
|
|
|
924 |
|
|
|
925 |
// MARK: - overrides
|
|
|
926 |
|
|
|
927 |
public override class var layerClass: AnyClass {
|
|
|
928 |
get {
|
|
|
929 |
return AVPlayerLayer.self
|
|
|
930 |
}
|
|
|
931 |
}
|
|
|
932 |
|
|
|
933 |
// MARK: - internal properties
|
|
|
934 |
|
|
|
935 |
internal var playerLayer: AVPlayerLayer {
|
|
|
936 |
get {
|
|
|
937 |
return self.layer as! AVPlayerLayer
|
|
|
938 |
}
|
|
|
939 |
}
|
|
|
940 |
|
|
|
941 |
internal var player: AVPlayer? {
|
|
|
942 |
get {
|
|
|
943 |
return self.playerLayer.player
|
|
|
944 |
}
|
|
|
945 |
set {
|
|
|
946 |
self.playerLayer.player = newValue
|
|
|
947 |
self.playerLayer.isHidden = (self.playerLayer.player == nil)
|
|
|
948 |
}
|
|
|
949 |
}
|
|
|
950 |
|
|
|
951 |
// MARK: - public properties
|
|
|
952 |
|
|
|
953 |
public var playerBackgroundColor: UIColor? {
|
|
|
954 |
get {
|
|
|
955 |
if let cgColor = self.playerLayer.backgroundColor {
|
|
|
956 |
return UIColor(cgColor: cgColor)
|
|
|
957 |
}
|
|
|
958 |
return nil
|
|
|
959 |
}
|
|
|
960 |
set {
|
|
|
961 |
self.playerLayer.backgroundColor = newValue?.cgColor
|
|
|
962 |
}
|
|
|
963 |
}
|
|
|
964 |
|
|
|
965 |
public var playerFillMode: Player.FillMode {
|
|
|
966 |
get {
|
|
|
967 |
return self.playerLayer.videoGravity
|
|
|
968 |
}
|
|
|
969 |
set {
|
|
|
970 |
self.playerLayer.videoGravity = newValue
|
|
|
971 |
}
|
|
|
972 |
}
|
|
|
973 |
|
|
|
974 |
public var isReadyForDisplay: Bool {
|
|
|
975 |
get {
|
|
|
976 |
return self.playerLayer.isReadyForDisplay
|
|
|
977 |
}
|
|
|
978 |
}
|
|
|
979 |
|
|
|
980 |
// MARK: - object lifecycle
|
|
|
981 |
|
|
|
982 |
public override init(frame: CGRect) {
|
|
|
983 |
super.init(frame: frame)
|
|
|
984 |
self.playerLayer.isHidden = true
|
|
|
985 |
self.playerFillMode = .resizeAspect
|
|
|
986 |
}
|
|
|
987 |
|
|
|
988 |
required public init?(coder aDecoder: NSCoder) {
|
|
|
989 |
super.init(coder: aDecoder)
|
|
|
990 |
self.playerLayer.isHidden = true
|
|
|
991 |
self.playerFillMode = .resizeAspect
|
|
|
992 |
}
|
|
|
993 |
|
|
|
994 |
deinit {
|
|
|
995 |
self.player?.pause()
|
|
|
996 |
self.player = nil
|
|
|
997 |
}
|
|
|
998 |
|
|
|
999 |
}
|