Learn-iOS-Swift-by-Examples/AVFoundationSimplePlayer/Objective-C/AVFoundationSimplePlayer-iOS/AAPLPlayerViewController.m

317 lines
12 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2016 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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