AVAutoWait/PlaybackDetailsViewController.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
View controller class that manages display of properties related to automatic waiting |
*/ |
import UIKit |
import AVFoundation |
/// View controller to display the current property values of a given AVPlayer and its current AVPlayerItem |
class PlaybackDetailsViewController: UIViewController { |
// MARK: Properties |
@IBOutlet weak var rateLabel : UILabel! |
@IBOutlet weak var timeControlStatusLabel : UILabel! |
@IBOutlet weak var reasonForWaitingLabel : UILabel! |
@IBOutlet weak var likelyToKeepUpLabel : UILabel! |
@IBOutlet weak var loadedTimeRangesLabel : UILabel! |
@IBOutlet weak var currentTimeLabel: UILabel! |
@IBOutlet weak var playbackBufferFullLabel: UILabel! |
@IBOutlet weak var playbackBufferEmptyLabel: UILabel! |
@IBOutlet weak var timebaseRateLabel: UILabel! |
var player : AVPlayer? |
// AVPlayerItem.currentTime() and the AVPlayerItem.timebase's rate are not KVO observable. We check their values regularly using this timer. |
private let nonObservablePropertiesUpdateTimer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main) |
// An array of key paths for the properties we want to observe. |
private let observedKeyPaths = [ |
#keyPath(PlaybackDetailsViewController.player.rate), |
#keyPath(PlaybackDetailsViewController.player.timeControlStatus), |
#keyPath(PlaybackDetailsViewController.player.reasonForWaitingToPlay), |
#keyPath(PlaybackDetailsViewController.player.currentItem.playbackLikelyToKeepUp), |
#keyPath(PlaybackDetailsViewController.player.currentItem.loadedTimeRanges), |
#keyPath(PlaybackDetailsViewController.player.currentItem.playbackBufferFull), |
#keyPath(PlaybackDetailsViewController.player.currentItem.playbackBufferEmpty) |
] |
private var observerContext = 0 |
// MARK: View Life Cycle |
override func viewDidLoad() { |
super.viewDidLoad() |
nonObservablePropertiesUpdateTimer.setEventHandler { [weak self] in |
self?.updateNonObservableProperties() |
} |
nonObservablePropertiesUpdateTimer.scheduleRepeating(deadline: DispatchTime.now(), interval: DispatchTimeInterval.milliseconds(100)) |
nonObservablePropertiesUpdateTimer.resume() |
// Register observers for the properties we want to display. |
for keyPath in observedKeyPaths { |
addObserver(self, forKeyPath: keyPath, options: [.new, .initial], context: &observerContext) |
} |
} |
deinit { |
// Un-register observers |
for keyPath in observedKeyPaths { |
removeObserver(self, forKeyPath: keyPath, context: &observerContext) |
} |
} |
// MARK: Helpers |
/// Helper function to get a background color for the timeControlStatus label. |
private func labelBackgroundColor(forTimeControlStatus status: AVPlayerTimeControlStatus) -> UIColor { |
switch status { |
case .paused: |
return #colorLiteral(red: 0.8196078538894653, green: 0.2627451121807098, blue: 0.2823528945446014, alpha: 1) |
case .playing: |
return #colorLiteral(red: 0.2881325483322144, green: 0.6088829636573792, blue: 0.261575847864151, alpha: 1) |
case .waitingToPlayAtSpecifiedRate: |
return #colorLiteral(red: 0.8679746985435486, green: 0.4876297116279602, blue: 0.2578189671039581, alpha: 1) |
} |
} |
/// Helper function to get an abbreviated description for the waiting reason. |
private func abbreviatedDescription(forReasonForWaitingToPlay reason: String) -> String { |
switch reason { |
case AVPlayerWaitingToMinimizeStallsReason: |
return "Minimizing Stalls" |
case AVPlayerWaitingWhileEvaluatingBufferingRateReason: |
return "Evaluating Buffering Rate" |
case AVPlayerWaitingWithNoItemToPlayReason: |
return "No Item" |
default: |
return "UNKOWN" |
} |
} |
// MARK: Property Change Handlers |
//Update the UI as AVPlayer properties change. |
override func observeValue(forKeyPath keyPath: String?, of object: AnyObject?, change: [NSKeyValueChangeKey : AnyObject]?, context: UnsafeMutablePointer<Void>?) { |
guard context == &observerContext else { |
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) |
return |
} |
if keyPath == #keyPath(PlaybackDetailsViewController.player.rate) { |
rateLabel.text = player?.rate.description ?? "-" |
} |
else if keyPath == #keyPath(PlaybackDetailsViewController.player.timeControlStatus) { |
timeControlStatusLabel.text = player?.timeControlStatus.description ?? "-" |
timeControlStatusLabel.backgroundColor = (player?.timeControlStatus).map(labelBackgroundColor(forTimeControlStatus:)) ?? #colorLiteral(red: 1, green: 0.9999743700027466, blue: 0.9999912977218628, alpha: 1) |
} |
else if keyPath == #keyPath(PlaybackDetailsViewController.player.reasonForWaitingToPlay) { |
reasonForWaitingLabel.text = player?.reasonForWaitingToPlay.map(abbreviatedDescription(forReasonForWaitingToPlay:)) ?? "-" |
} |
else if keyPath == #keyPath(PlaybackDetailsViewController.player.currentItem.playbackLikelyToKeepUp) { |
likelyToKeepUpLabel.text = player?.currentItem?.isPlaybackLikelyToKeepUp.description ?? "-" |
} |
else if keyPath == #keyPath(PlaybackDetailsViewController.player.currentItem.loadedTimeRanges) { |
loadedTimeRangesLabel.text = player?.currentItem?.loadedTimeRanges.asTimeRanges.description ?? "-" |
} |
else if keyPath == #keyPath(PlaybackDetailsViewController.player.currentItem.playbackBufferFull) { |
playbackBufferFullLabel.text = player?.currentItem?.isPlaybackBufferFull.description ?? "-" |
} |
else if keyPath == #keyPath(PlaybackDetailsViewController.player.currentItem.playbackBufferEmpty) { |
playbackBufferEmptyLabel.text = player?.currentItem?.isPlaybackBufferEmpty.description ?? "-" |
} |
} |
private func updateNonObservableProperties() { |
currentTimeLabel.text = player?.currentItem?.currentTime().description ?? "-" |
timebaseRateLabel.text = player?.currentItem?.timebase != nil ? CMTimebaseGetRate(player!.currentItem!.timebase!).description : "-" |
} |
} |
// MARK: - Extensions to improve readability of printed properties |
// Add description for AVPlayerTimeControlStatus. |
extension AVPlayerTimeControlStatus : CustomStringConvertible{ |
public var description: String { |
switch self { |
case .paused: |
return " Paused " |
case .playing: |
return " Playing " |
case .waitingToPlayAtSpecifiedRate: |
return " Waiting " |
} |
} |
} |
// Simple description of CMTime, e.g., 2.4s. |
extension CMTime : CustomStringConvertible { |
public var description : String { |
return String(format: "%.1fs", self.seconds) |
} |
} |
// Simple description of CMTimeRange, e.g., [2.4s, 2.8s]. |
extension CMTimeRange : CustomStringConvertible { |
public var description: String { |
return "[\(self.start), \(self.end)]" |
} |
} |
// Convert a collection of NSValues into an array of CMTimeRanges. |
private extension Collection where Iterator.Element == NSValue { |
var asTimeRanges : [CMTimeRange] { |
return self.map({ value -> CMTimeRange in |
return value.timeRangeValue |
}) |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13