Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerViewController.m
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
View controller containing a player view and basic playback controls. |
*/ |
@import Foundation; |
@import AVFoundation; |
@import CoreMedia.CMTime; |
#import "AAPLPlayerViewController.h" |
#import "AAPLPlayerView.h" |
// Private properties |
@interface AAPLPlayerViewController () |
{ |
AVPlayer *_player; |
AVURLAsset *_asset; |
id<NSObject> _timeObserverToken; |
AVPlayerItem *_playerItem; |
} |
@property AVPlayerItem *playerItem; |
@property (readonly) AVPlayerLayer *playerLayer; |
@end |
@implementation AAPLPlayerViewController |
// MARK: - View Handling |
/* |
KVO context used to differentiate KVO callbacks for this class versus other |
classes in its class hierarchy. |
*/ |
static int AAPLPlayerViewControllerKVOContext = 0; |
- (void)viewWillAppear:(BOOL)animated { |
[super viewWillAppear:animated]; |
/* |
Update the UI when these player properties change. |
Use the context parameter to distinguish KVO for our particular observers and not |
those destined for a subclass that also happens to be observing these properties. |
*/ |
[self addObserver:self forKeyPath:@"asset" options:NSKeyValueObservingOptionNew context:&AAPLPlayerViewControllerKVOContext]; |
[self addObserver:self forKeyPath:@"player.currentItem.duration" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; |
[self addObserver:self forKeyPath:@"player.rate" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; |
[self addObserver:self forKeyPath:@"player.currentItem.status" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial context:&AAPLPlayerViewControllerKVOContext]; |
self.playerView.playerLayer.player = self.player; |
NSURL *movieURL = [[NSBundle mainBundle] URLForResource:@"ElephantSeals" withExtension:@"mov"]; |
self.asset = [AVURLAsset assetWithURL:movieURL]; |
// Use a weak self variable to avoid a retain cycle in the block. |
AAPLPlayerViewController __weak *weakSelf = self; |
_timeObserverToken = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock: |
^(CMTime time) { |
weakSelf.timeSlider.value = CMTimeGetSeconds(time); |
}]; |
} |
- (void)viewDidDisappear:(BOOL)animated { |
[super viewDidDisappear:animated]; |
if (_timeObserverToken) { |
[self.player removeTimeObserver:_timeObserverToken]; |
_timeObserverToken = nil; |
} |
[self.player pause]; |
[self removeObserver:self forKeyPath:@"asset" context:&AAPLPlayerViewControllerKVOContext]; |
[self removeObserver:self forKeyPath:@"player.currentItem.duration" context:&AAPLPlayerViewControllerKVOContext]; |
[self removeObserver:self forKeyPath:@"player.rate" context:&AAPLPlayerViewControllerKVOContext]; |
[self removeObserver:self forKeyPath:@"player.currentItem.status" context:&AAPLPlayerViewControllerKVOContext]; |
} |
// MARK: - Properties |
// Will attempt load and test these asset keys before playing |
+ (NSArray *)assetKeysRequiredToPlay { |
return @[ @"playable", @"hasProtectedContent" ]; |
} |
- (AVPlayer *)player { |
if (!_player) |
_player = [[AVPlayer alloc] init]; |
return _player; |
} |
- (CMTime)currentTime { |
return self.player.currentTime; |
} |
- (void)setCurrentTime:(CMTime)newCurrentTime { |
[self.player seekToTime:newCurrentTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero]; |
} |
- (CMTime)duration { |
return self.player.currentItem ? self.player.currentItem.duration : kCMTimeZero; |
} |
- (float)rate { |
return self.player.rate; |
} |
- (void)setRate:(float)newRate { |
self.player.rate = newRate; |
} |
- (AVPlayerLayer *)playerLayer { |
return self.playerView.playerLayer; |
} |
- (AVPlayerItem *)playerItem { |
return _playerItem; |
} |
- (void)setPlayerItem:(AVPlayerItem *)newPlayerItem { |
if (_playerItem != newPlayerItem) { |
_playerItem = newPlayerItem; |
// If needed, configure player item here before associating it with a player |
// (example: adding outputs, setting text style rules, selecting media options) |
[self.player replaceCurrentItemWithPlayerItem:_playerItem]; |
} |
} |
// MARK: - Asset Loading |
- (void)asynchronouslyLoadURLAsset:(AVURLAsset *)newAsset { |
/* |
Using AVAsset now runs the risk of blocking the current thread |
(the main UI thread) whilst I/O happens to populate the |
properties. It's prudent to defer our work until the properties |
we need have been loaded. |
*/ |
[newAsset loadValuesAsynchronouslyForKeys:AAPLPlayerViewController.assetKeysRequiredToPlay completionHandler:^{ |
/* |
The asset invokes its completion handler on an arbitrary queue. |
To avoid multiple threads using our internal state at the same time |
we'll elect to use the main thread at all times, let's dispatch |
our handler to the main queue. |
*/ |
dispatch_async(dispatch_get_main_queue(), ^{ |
if (newAsset != self.asset) { |
/* |
self.asset has already changed! No point continuing because |
another newAsset will come along in a moment. |
*/ |
return; |
} |
/* |
Test whether the values of each of the keys we need have been |
successfully loaded. |
*/ |
for (NSString *key in self.class.assetKeysRequiredToPlay) { |
NSError *error = nil; |
if ([newAsset statusOfValueForKey:key error:&error] == AVKeyValueStatusFailed) { |
NSString *message = [NSString localizedStringWithFormat:NSLocalizedString(@"error.asset_key_%@_failed.description", @"Can't use this AVAsset because one of it's keys failed to load"), key]; |
[self handleErrorWithMessage:message error:error]; |
return; |
} |
} |
// We can't play this asset. |
if (!newAsset.playable || newAsset.hasProtectedContent) { |
NSString *message = NSLocalizedString(@"error.asset_not_playable.description", @"Can't use this AVAsset because it isn't playable or has protected content"); |
[self handleErrorWithMessage:message error:nil]; |
return; |
} |
/* |
We can play this asset. Create a new AVPlayerItem and make it |
our player's current item. |
*/ |
self.playerItem = [AVPlayerItem playerItemWithAsset:newAsset]; |
}); |
}]; |
} |
// MARK: - IBActions |
- (IBAction)playPauseButtonWasPressed:(UIButton *)sender { |
if (self.player.rate != 1.0) { |
// not playing foward so play |
if (CMTIME_COMPARE_INLINE(self.currentTime, ==, self.duration)) { |
// at end so got back to begining |
self.currentTime = kCMTimeZero; |
} |
[self.player play]; |
} else { |
// playing so pause |
[self.player pause]; |
} |
} |
- (IBAction)rewindButtonWasPressed:(UIButton *)sender { |
self.rate = MAX(self.player.rate - 2.0, -2.0); // rewind no faster than -2.0 |
} |
- (IBAction)fastForwardButtonWasPressed:(UIButton *)sender { |
self.rate = MIN(self.player.rate + 2.0, 2.0); // fast forward no faster than 2.0 |
} |
- (IBAction)timeSliderDidChange:(UISlider *)sender { |
self.currentTime = CMTimeMakeWithSeconds(sender.value, 1000); |
} |
// MARK: - KV Observation |
// Update our UI when player or player.currentItem changes |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { |
if (context != &AAPLPlayerViewControllerKVOContext) { |
// KVO isn't for us. |
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
return; |
} |
if ([keyPath isEqualToString:@"asset"]) { |
if (self.asset) { |
[self asynchronouslyLoadURLAsset:self.asset]; |
} |
} |
else if ([keyPath isEqualToString:@"player.currentItem.duration"]) { |
// Update timeSlider and enable/disable controls when duration > 0.0 |
// Handle NSNull value for NSKeyValueChangeNewKey, i.e. when player.currentItem is nil |
NSValue *newDurationAsValue = change[NSKeyValueChangeNewKey]; |
CMTime newDuration = [newDurationAsValue isKindOfClass:[NSValue class]] ? newDurationAsValue.CMTimeValue : kCMTimeZero; |
BOOL hasValidDuration = CMTIME_IS_NUMERIC(newDuration) && newDuration.value != 0; |
double newDurationSeconds = hasValidDuration ? CMTimeGetSeconds(newDuration) : 0.0; |
self.timeSlider.maximumValue = newDurationSeconds; |
self.timeSlider.value = hasValidDuration ? CMTimeGetSeconds(self.currentTime) : 0.0; |
self.rewindButton.enabled = hasValidDuration; |
self.playPauseButton.enabled = hasValidDuration; |
self.fastForwardButton.enabled = hasValidDuration; |
self.timeSlider.enabled = hasValidDuration; |
self.startTimeLabel.enabled = hasValidDuration; |
self.durationLabel.enabled = hasValidDuration; |
int wholeMinutes = (int)trunc(newDurationSeconds / 60); |
self.durationLabel.text = [NSString stringWithFormat:@"%d:%02d", wholeMinutes, (int)trunc(newDurationSeconds) - wholeMinutes * 60]; |
} |
else if ([keyPath isEqualToString:@"player.rate"]) { |
// Update playPauseButton image |
double newRate = [change[NSKeyValueChangeNewKey] doubleValue]; |
UIImage *buttonImage = (newRate == 1.0) ? [UIImage imageNamed:@"PauseButton"] : [UIImage imageNamed:@"PlayButton"]; |
[self.playPauseButton setImage:buttonImage forState:UIControlStateNormal]; |
} |
else if ([keyPath isEqualToString:@"player.currentItem.status"]) { |
// Display an error if status becomes Failed |
// Handle NSNull value for NSKeyValueChangeNewKey, i.e. when player.currentItem is nil |
NSNumber *newStatusAsNumber = change[NSKeyValueChangeNewKey]; |
AVPlayerItemStatus newStatus = [newStatusAsNumber isKindOfClass:[NSNumber class]] ? newStatusAsNumber.integerValue : AVPlayerItemStatusUnknown; |
if (newStatus == AVPlayerItemStatusFailed) { |
[self handleErrorWithMessage:self.player.currentItem.error.localizedDescription error:self.player.currentItem.error]; |
} |
} else { |
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
} |
} |
// Trigger KVO for anyone observing our properties affected by player and player.currentItem |
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key { |
if ([key isEqualToString:@"duration"]) { |
return [NSSet setWithArray:@[ @"player.currentItem.duration" ]]; |
} else if ([key isEqualToString:@"currentTime"]) { |
return [NSSet setWithArray:@[ @"player.currentItem.currentTime" ]]; |
} else if ([key isEqualToString:@"rate"]) { |
return [NSSet setWithArray:@[ @"player.rate" ]]; |
} else { |
return [super keyPathsForValuesAffectingValueForKey:key]; |
} |
} |
// MARK: - Error Handling |
- (void)handleErrorWithMessage:(NSString *)message error:(NSError *)error { |
NSLog(@"Error occured with message: %@, error: %@.", message, error); |
NSString *alertTitle = NSLocalizedString(@"alert.error.title", @"Alert title for errors"); |
NSString *defaultAlertMesssage = NSLocalizedString(@"error.default.description", @"Default error message when no NSError provided"); |
UIAlertController *controller = [UIAlertController alertControllerWithTitle:alertTitle message:message ?: defaultAlertMesssage preferredStyle:UIAlertControllerStyleAlert]; |
NSString *alertActionTitle = NSLocalizedString(@"alert.error.actions.OK", @"OK on error alert"); |
UIAlertAction *action = [UIAlertAction actionWithTitle:alertActionTitle style:UIAlertActionStyleDefault handler:nil]; |
[controller addAction:action]; |
[self presentViewController:controller animated:YES completion:nil]; |
} |
@end |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13