Classes/MyStreamingMovieViewController.m
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
A UIViewController controller subclass that loads the SecondView nib file that contains its view. |
Contains an action method that is called when the Play Movie button is pressed to play the movie. |
Provides a text edit control for the user to enter a movie URL. |
Manages a collection of transport control UI that allows the user to play/pause and seek. |
*/ |
#import "MyStreamingMovieViewController.h" |
#import "MyPlayerLayerView.h" |
@import AVFoundation; |
static void *MyStreamingMovieViewControllerTimedMetadataObserverContext = &MyStreamingMovieViewControllerTimedMetadataObserverContext; |
static void *MyStreamingMovieViewControllerRateObservationContext = &MyStreamingMovieViewControllerRateObservationContext; |
static void *MyStreamingMovieViewControllerCurrentItemObservationContext = &MyStreamingMovieViewControllerCurrentItemObservationContext; |
static void *MyStreamingMovieViewControllerPlayerItemStatusObserverContext = &MyStreamingMovieViewControllerPlayerItemStatusObserverContext; |
NSString *kTracksKey = @"tracks"; |
NSString *kStatusKey = @"status"; |
NSString *kRateKey = @"rate"; |
NSString *kPlayableKey = @"playable"; |
NSString *kCurrentItemKey = @"currentItem"; |
NSString *kTimedMetadataKey = @"currentItem.timedMetadata"; |
#pragma mark - |
@interface MyStreamingMovieViewController (Player) |
- (CMTime)playerItemDuration; |
- (BOOL)isPlaying; |
- (void)handleTimedMetadata:(AVMetadataItem*)timedMetadata; |
- (void)updateAdList:(NSArray *)newAdList; |
- (void)assetFailedToPrepareForPlayback:(NSError *)error; |
- (void)prepareToPlayAsset:(AVURLAsset *)asset withKeys:(NSArray *)requestedKeys; |
@end |
@implementation MyStreamingMovieViewController |
@synthesize movieURLTextField; |
@synthesize movieTimeControl; |
@synthesize playerLayerView; |
@synthesize player, playerItem; |
@synthesize isPlayingAdText; |
@synthesize toolBar, playButton, stopButton; |
#pragma mark - |
#pragma mark Movie controller methods |
#pragma mark - |
/* --------------------------------------------------------- |
** Methods to handle manipulation of the movie scrubber control |
** ------------------------------------------------------- */ |
#pragma mark Play, Stop Buttons |
/* Show the stop button in the movie player controller. */ |
-(void)showStopButton |
{ |
NSMutableArray *toolbarItems = [NSMutableArray arrayWithArray:[toolBar items]]; |
[toolbarItems replaceObjectAtIndex:0 withObject:stopButton]; |
toolBar.items = toolbarItems; |
} |
/* Show the play button in the movie player controller. */ |
-(void)showPlayButton |
{ |
NSMutableArray *toolbarItems = [NSMutableArray arrayWithArray:[toolBar items]]; |
[toolbarItems replaceObjectAtIndex:0 withObject:playButton]; |
toolBar.items = toolbarItems; |
} |
/* If the media is playing, show the stop button; otherwise, show the play button. */ |
- (void)syncPlayPauseButtons |
{ |
if ([self isPlaying]) |
{ |
[self showStopButton]; |
} |
else |
{ |
[self showPlayButton]; |
} |
} |
-(void)enablePlayerButtons |
{ |
self.playButton.enabled = YES; |
self.stopButton.enabled = YES; |
} |
-(void)disablePlayerButtons |
{ |
self.playButton.enabled = NO; |
self.stopButton.enabled = NO; |
} |
#pragma mark Scrubber control |
/* Set the scrubber based on the player current time. */ |
- (void)syncScrubber |
{ |
CMTime playerDuration = [self playerItemDuration]; |
if (CMTIME_IS_INVALID(playerDuration)) |
{ |
movieTimeControl.minimumValue = 0.0; |
return; |
} |
double duration = CMTimeGetSeconds(playerDuration); |
if (isfinite(duration) && (duration > 0)) |
{ |
float minValue = [movieTimeControl minimumValue]; |
float maxValue = [movieTimeControl maximumValue]; |
double time = CMTimeGetSeconds([player currentTime]); |
[movieTimeControl setValue:(maxValue - minValue) * time / duration + minValue]; |
} |
} |
/* Requests invocation of a given block during media playback to update the |
movie scrubber control. */ |
-(void)initScrubberTimer |
{ |
double interval = .1f; |
CMTime playerDuration = [self playerItemDuration]; |
if (CMTIME_IS_INVALID(playerDuration)) |
{ |
return; |
} |
double duration = CMTimeGetSeconds(playerDuration); |
if (isfinite(duration)) |
{ |
CGFloat width = CGRectGetWidth([movieTimeControl bounds]); |
interval = 0.5f * duration / width; |
} |
/* Update the scrubber during normal playback. */ |
timeObserver = [[player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(interval, NSEC_PER_SEC) |
queue:NULL |
usingBlock: |
^(CMTime time) |
{ |
[self syncScrubber]; |
}] retain]; |
} |
/* Cancels the previously registered time observer. */ |
-(void)removePlayerTimeObserver |
{ |
if (timeObserver) |
{ |
[player removeTimeObserver:timeObserver]; |
[timeObserver release]; |
timeObserver = nil; |
} |
} |
/* The user is dragging the movie controller thumb to scrub through the movie. */ |
- (IBAction)beginScrubbing:(id)sender |
{ |
restoreAfterScrubbingRate = [player rate]; |
[player setRate:0.f]; |
/* Remove previous timer. */ |
[self removePlayerTimeObserver]; |
} |
/* The user has released the movie thumb control to stop scrubbing through the movie. */ |
- (IBAction)endScrubbing:(id)sender |
{ |
if (!timeObserver) |
{ |
CMTime playerDuration = [self playerItemDuration]; |
if (CMTIME_IS_INVALID(playerDuration)) |
{ |
return; |
} |
double duration = CMTimeGetSeconds(playerDuration); |
if (isfinite(duration)) |
{ |
CGFloat width = CGRectGetWidth([movieTimeControl bounds]); |
double tolerance = 0.5f * duration / width; |
timeObserver = [[player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(tolerance, NSEC_PER_SEC) queue:dispatch_get_main_queue() usingBlock: |
^(CMTime time) |
{ |
[self syncScrubber]; |
}] retain]; |
} |
} |
if (restoreAfterScrubbingRate) |
{ |
[player setRate:restoreAfterScrubbingRate]; |
restoreAfterScrubbingRate = 0.f; |
} |
} |
/* Set the player current time to match the scrubber position. */ |
- (IBAction)scrub:(id)sender |
{ |
if ([sender isKindOfClass:[UISlider class]]) |
{ |
UISlider* slider = sender; |
CMTime playerDuration = [self playerItemDuration]; |
if (CMTIME_IS_INVALID(playerDuration)) { |
return; |
} |
double duration = CMTimeGetSeconds(playerDuration); |
if (isfinite(duration)) |
{ |
float minValue = [slider minimumValue]; |
float maxValue = [slider maximumValue]; |
float value = [slider value]; |
double time = duration * (value - minValue) / (maxValue - minValue); |
[player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC)]; |
} |
} |
} |
- (BOOL)isScrubbing |
{ |
return restoreAfterScrubbingRate != 0.f; |
} |
-(void)enableScrubber |
{ |
self.movieTimeControl.enabled = YES; |
} |
-(void)disableScrubber |
{ |
self.movieTimeControl.enabled = NO; |
} |
/* Prevent the slider from seeking during Ad playback. */ |
- (void)sliderSyncToPlayerSeekableTimeRanges |
{ |
NSArray *seekableTimeRanges = [[player currentItem] seekableTimeRanges]; |
if ([seekableTimeRanges count] > 0) |
{ |
NSValue *range = [seekableTimeRanges objectAtIndex:0]; |
CMTimeRange timeRange = [range CMTimeRangeValue]; |
float startSeconds = CMTimeGetSeconds(timeRange.start); |
float durationSeconds = CMTimeGetSeconds(timeRange.duration); |
/* Set the minimum and maximum values of the time slider to match the seekable time range. */ |
movieTimeControl.minimumValue = startSeconds; |
movieTimeControl.maximumValue = startSeconds + durationSeconds; |
} |
} |
#pragma mark Button Action Methods |
- (IBAction)play:(id)sender |
{ |
/* If we are at the end of the movie, we must seek to the beginning first |
before starting playback. */ |
if (YES == seekToZeroBeforePlay) |
{ |
seekToZeroBeforePlay = NO; |
[player seekToTime:kCMTimeZero]; |
} |
[player play]; |
[self showStopButton]; |
} |
- (IBAction)pause:(id)sender |
{ |
[player pause]; |
[self showPlayButton]; |
} |
- (IBAction)loadMovieButtonPressed:(id)sender |
{ |
/* Has the user entered a movie URL? */ |
if (self.movieURLTextField.text.length > 0) |
{ |
NSURL *newMovieURL = [NSURL URLWithString:self.movieURLTextField.text]; |
if ([newMovieURL scheme]) /* Sanity check on the URL. */ |
{ |
/* |
Create an asset for inspection of a resource referenced by a given URL. |
Load the values for the asset keys "tracks", "playable". |
*/ |
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:newMovieURL options:nil]; |
NSArray *requestedKeys = [NSArray arrayWithObjects:kTracksKey, kPlayableKey, nil]; |
/* Tells the asset to load the values of any of the specified keys that are not already loaded. */ |
[asset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler: |
^{ |
dispatch_async( dispatch_get_main_queue(), |
^{ |
/* IMPORTANT: Must dispatch to main queue in order to operate on the AVPlayer and AVPlayerItem. */ |
[self prepareToPlayAsset:asset withKeys:requestedKeys]; |
}); |
}]; |
} |
} |
} |
- (BOOL)textFieldShouldReturn:(UITextField *)theTextField |
{ |
/* When the user presses return, take focus away from the text |
field so that the keyboard is dismissed. */ |
if (theTextField == self.movieURLTextField) |
{ |
[self.movieURLTextField resignFirstResponder]; |
} |
return YES; |
} |
#pragma mark - |
#pragma mark View Controller |
#pragma mark - |
- (void)viewDidUnload |
{ |
self.playerLayerView = nil; |
self.toolBar = nil; |
self.playButton = nil; |
self.stopButton = nil; |
self.movieTimeControl = nil; |
self.movieURLTextField = nil; |
self.isPlayingAdText = nil; |
[timeObserver release]; |
[movieURL release]; |
[super viewDidUnload]; |
} |
- (void)viewDidLoad |
{ |
UIView* view = [self view]; |
UISwipeGestureRecognizer* swipeUpRecognizer = [[UISwipeGestureRecognizer allocWithZone:[self zone]] initWithTarget:self action:@selector(handleSwipe:)]; |
[swipeUpRecognizer setDirection:UISwipeGestureRecognizerDirectionUp]; |
[view addGestureRecognizer:swipeUpRecognizer]; |
[swipeUpRecognizer release]; |
UISwipeGestureRecognizer* swipeDownRecognizer = [[UISwipeGestureRecognizer allocWithZone:[self zone]] initWithTarget:self action:@selector(handleSwipe:)]; |
[swipeDownRecognizer setDirection:UISwipeGestureRecognizerDirectionDown]; |
[view addGestureRecognizer:swipeDownRecognizer]; |
[swipeDownRecognizer release]; |
UIBarButtonItem *scrubberItem = [[UIBarButtonItem alloc] initWithCustomView:movieTimeControl]; |
UIBarButtonItem *flexItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; |
toolBar.items = [NSArray arrayWithObjects:playButton, flexItem, scrubberItem, nil]; |
[scrubberItem release]; |
[flexItem release]; |
[super viewDidLoad]; |
} |
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation |
{ |
/* Supports all orientations. */ |
return YES; |
} |
- (void)handleSwipe:(UISwipeGestureRecognizer *)gestureRecognizer |
{ |
UIView* view = [self view]; |
UISwipeGestureRecognizerDirection direction = [gestureRecognizer direction]; |
CGPoint location = [gestureRecognizer locationInView:view]; |
if (location.y < CGRectGetMidY([view bounds])) |
{ |
if (direction == UISwipeGestureRecognizerDirectionUp) |
{ |
[UIView animateWithDuration:0.2f animations: |
^{ |
[[self navigationController] setNavigationBarHidden:YES animated:YES]; |
} completion: |
^(BOOL finished) |
{ |
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationSlide]; |
}]; |
} |
if (direction == UISwipeGestureRecognizerDirectionDown) |
{ |
[UIView animateWithDuration:0.2f animations: |
^{ |
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide]; |
} completion: |
^(BOOL finished) |
{ |
[[self navigationController] setNavigationBarHidden:NO animated:YES]; |
}]; |
} |
} |
else |
{ |
if (direction == UISwipeGestureRecognizerDirectionDown) |
{ |
if (![toolBar isHidden]) |
{ |
[UIView animateWithDuration:0.2f animations: |
^{ |
[toolBar setTransform:CGAffineTransformMakeTranslation(0.f, CGRectGetHeight([toolBar bounds]))]; |
} completion: |
^(BOOL finished) |
{ |
[toolBar setHidden:YES]; |
}]; |
} |
} |
else if (direction == UISwipeGestureRecognizerDirectionUp) |
{ |
if ([toolBar isHidden]) |
{ |
[toolBar setHidden:NO]; |
[UIView animateWithDuration:0.2f animations: |
^{ |
[toolBar setTransform:CGAffineTransformIdentity]; |
} completion:^(BOOL finished){}]; |
} |
} |
} |
} |
- (void)dealloc |
{ |
[timeObserver release]; |
[movieURL release]; |
[[NSNotificationCenter defaultCenter] removeObserver:self |
name:AVPlayerItemDidPlayToEndTimeNotification |
object:nil]; |
[self.player removeObserver:self forKeyPath:kCurrentItemKey]; |
[self.player removeObserver:self forKeyPath:kTimedMetadataKey]; |
[self.player removeObserver:self forKeyPath:kRateKey]; |
[player release]; |
[adList release]; |
[movieURLTextField release]; |
[movieTimeControl release]; |
[playerLayerView release]; |
[toolBar release]; |
[playButton release]; |
[stopButton release]; |
[isPlayingAdText release]; |
[super dealloc]; |
} |
@end |
@implementation MyStreamingMovieViewController (Player) |
#pragma mark - |
#pragma mark Player |
/* --------------------------------------------------------- |
** Get the duration for a AVPlayerItem. |
** ------------------------------------------------------- */ |
- (CMTime)playerItemDuration |
{ |
AVPlayerItem *thePlayerItem = [player currentItem]; |
if (thePlayerItem.status == AVPlayerItemStatusReadyToPlay) |
{ |
/* |
NOTE: |
Because of the dynamic nature of HTTP Live Streaming Media, the best practice |
for obtaining the duration of an AVPlayerItem object has changed in iOS 4.3. |
Prior to iOS 4.3, you would obtain the duration of a player item by fetching |
the value of the duration property of its associated AVAsset object. However, |
note that for HTTP Live Streaming Media the duration of a player item during |
any particular playback session may differ from the duration of its asset. For |
this reason a new key-value observable duration property has been defined on |
AVPlayerItem. |
See the AV Foundation Release Notes for iOS 4.3 for more information. |
*/ |
return([playerItem duration]); |
} |
return(kCMTimeInvalid); |
} |
- (BOOL)isPlaying |
{ |
return restoreAfterScrubbingRate != 0.f || [player rate] != 0.f; |
} |
#pragma mark Player Notifications |
/* Called when the player item has played to its end time. */ |
- (void) playerItemDidReachEnd:(NSNotification*) aNotification |
{ |
/* Hide the 'Pause' button, show the 'Play' button in the slider control */ |
[self showPlayButton]; |
/* After the movie has played to its end time, seek back to time zero |
to play it again */ |
seekToZeroBeforePlay = YES; |
} |
#pragma mark - |
#pragma mark Timed metadata |
#pragma mark - |
- (void)handleTimedMetadata:(AVMetadataItem*)timedMetadata |
{ |
/* We expect the content to contain plists encoded as timed metadata. AVPlayer turns these into NSDictionaries. */ |
if ([(NSString *)[timedMetadata key] isEqualToString:AVMetadataID3MetadataKeyGeneralEncapsulatedObject]) |
{ |
if ([[timedMetadata value] isKindOfClass:[NSDictionary class]]) |
{ |
NSDictionary *propertyList = (NSDictionary *)[timedMetadata value]; |
/* Metadata payload could be the list of ads. */ |
NSArray *newAdList = [propertyList objectForKey:@"ad-list"]; |
if (newAdList != nil) |
{ |
[self updateAdList:newAdList]; |
NSLog(@"ad-list is %@", newAdList); |
} |
/* Or it might be an ad record. */ |
NSString *adURL = [propertyList objectForKey:@"url"]; |
if (adURL != nil) |
{ |
if ([adURL isEqualToString:@""]) |
{ |
/* Ad is not playing, so clear text. */ |
self.isPlayingAdText.text = @""; |
[self enablePlayerButtons]; |
[self enableScrubber]; /* Enable seeking for main content. */ |
NSLog(@"enabling seek at %g", CMTimeGetSeconds([player currentTime])); |
} |
else |
{ |
/* Display text indicating that an Ad is now playing. */ |
self.isPlayingAdText.text = @"< Ad now playing, seeking is disabled on the movie controller... >"; |
[self disablePlayerButtons]; |
[self disableScrubber]; /* Disable seeking for ad content. */ |
NSLog(@"disabling seek at %g", CMTimeGetSeconds([player currentTime])); |
} |
} |
} |
} |
} |
#pragma mark Ad list |
/* Update current ad list, set slider to match current player item seekable time ranges */ |
- (void)updateAdList:(NSArray *)newAdList |
{ |
if (!adList || ![adList isEqualToArray:newAdList]) |
{ |
newAdList = [newAdList copy]; |
[adList release]; |
adList = newAdList; |
[self sliderSyncToPlayerSeekableTimeRanges]; |
} |
} |
#pragma mark - |
#pragma mark Loading the Asset Keys Asynchronously |
#pragma mark - |
#pragma mark Error Handling - Preparing Assets for Playback Failed |
/* -------------------------------------------------------------- |
** Called when an asset fails to prepare for playback for any of |
** the following reasons: |
** |
** 1) values of asset keys did not load successfully, |
** 2) the asset keys did load successfully, but the asset is not |
** playable |
** 3) the item did not become ready to play. |
** ----------------------------------------------------------- */ |
-(void)assetFailedToPrepareForPlayback:(NSError *)error |
{ |
[self removePlayerTimeObserver]; |
[self syncScrubber]; |
[self disableScrubber]; |
[self disablePlayerButtons]; |
/* Display the error. */ |
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[error localizedDescription] |
message:[error localizedFailureReason] |
delegate:nil |
cancelButtonTitle:@"OK" |
otherButtonTitles:nil]; |
[alertView show]; |
[alertView release]; |
} |
#pragma mark Prepare to play asset |
/* |
Invoked at the completion of the loading of the values for all keys on the asset that we require. |
Checks whether loading was successfull and whether the asset is playable. |
If so, sets up an AVPlayerItem and an AVPlayer to play the asset. |
*/ |
- (void)prepareToPlayAsset:(AVURLAsset *)asset withKeys:(NSArray *)requestedKeys |
{ |
/* Make sure that the value of each key has loaded successfully. */ |
for (NSString *thisKey in requestedKeys) |
{ |
NSError *error = nil; |
AVKeyValueStatus keyStatus = [asset statusOfValueForKey:thisKey error:&error]; |
if (keyStatus == AVKeyValueStatusFailed) |
{ |
[self assetFailedToPrepareForPlayback:error]; |
return; |
} |
/* If you are also implementing the use of -[AVAsset cancelLoading], add your code here to bail |
out properly in the case of cancellation. */ |
} |
/* Use the AVAsset playable property to detect whether the asset can be played. */ |
if (!asset.playable) |
{ |
/* Generate an error describing the failure. */ |
NSString *localizedDescription = NSLocalizedString(@"Item cannot be played", @"Item cannot be played description"); |
NSString *localizedFailureReason = NSLocalizedString(@"The assets tracks were loaded, but could not be made playable.", @"Item cannot be played failure reason"); |
NSDictionary *errorDict = [NSDictionary dictionaryWithObjectsAndKeys: |
localizedDescription, NSLocalizedDescriptionKey, |
localizedFailureReason, NSLocalizedFailureReasonErrorKey, |
nil]; |
NSError *assetCannotBePlayedError = [NSError errorWithDomain:@"StitchedStreamPlayer" code:0 userInfo:errorDict]; |
/* Display the error to the user. */ |
[self assetFailedToPrepareForPlayback:assetCannotBePlayedError]; |
return; |
} |
/* At this point we're ready to set up for playback of the asset. */ |
[self initScrubberTimer]; |
[self enableScrubber]; |
[self enablePlayerButtons]; |
/* Stop observing our prior AVPlayerItem, if we have one. */ |
if (self.playerItem) |
{ |
/* Remove existing player item key value observers and notifications. */ |
[self.playerItem removeObserver:self forKeyPath:kStatusKey]; |
[[NSNotificationCenter defaultCenter] removeObserver:self |
name:AVPlayerItemDidPlayToEndTimeNotification |
object:self.playerItem]; |
} |
/* Create a new instance of AVPlayerItem from the now successfully loaded AVAsset. */ |
self.playerItem = [AVPlayerItem playerItemWithAsset:asset]; |
/* Observe the player item "status" key to determine when it is ready to play. */ |
[self.playerItem addObserver:self |
forKeyPath:kStatusKey |
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew |
context:MyStreamingMovieViewControllerPlayerItemStatusObserverContext]; |
/* When the player item has played to its end time we'll toggle |
the movie controller Pause button to be the Play button */ |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(playerItemDidReachEnd:) |
name:AVPlayerItemDidPlayToEndTimeNotification |
object:self.playerItem]; |
seekToZeroBeforePlay = NO; |
/* Create new player, if we don't already have one. */ |
if (![self player]) |
{ |
/* Get a new AVPlayer initialized to play the specified player item. */ |
[self setPlayer:[AVPlayer playerWithPlayerItem:self.playerItem]]; |
/* Observe the AVPlayer "currentItem" property to find out when any |
AVPlayer replaceCurrentItemWithPlayerItem: replacement will/did |
occur.*/ |
[self.player addObserver:self |
forKeyPath:kCurrentItemKey |
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew |
context:MyStreamingMovieViewControllerCurrentItemObservationContext]; |
/* A 'currentItem.timedMetadata' property observer to parse the media stream timed metadata. */ |
[self.player addObserver:self |
forKeyPath:kTimedMetadataKey |
options:0 |
context:MyStreamingMovieViewControllerTimedMetadataObserverContext]; |
/* Observe the AVPlayer "rate" property to update the scrubber control. */ |
[self.player addObserver:self |
forKeyPath:kRateKey |
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew |
context:MyStreamingMovieViewControllerRateObservationContext]; |
} |
/* Make our new AVPlayerItem the AVPlayer's current item. */ |
if (self.player.currentItem != self.playerItem) |
{ |
/* Replace the player item with a new player item. The item replacement occurs |
asynchronously; observe the currentItem property to find out when the |
replacement will/did occur*/ |
[[self player] replaceCurrentItemWithPlayerItem:self.playerItem]; |
[self syncPlayPauseButtons]; |
} |
[movieTimeControl setValue:0.0]; |
} |
#pragma mark - |
#pragma mark Asset Key Value Observing |
#pragma mark |
#pragma mark Key Value Observer for player rate, currentItem, player item status |
/* --------------------------------------------------------- |
** Called when the value at the specified key path relative |
** to the given object has changed. |
** Adjust the movie play and pause button controls when the |
** player item "status" value changes. Update the movie |
** scrubber control when the player item is ready to play. |
** Adjust the movie scrubber control when the player item |
** "rate" value changes. For updates of the player |
** "currentItem" property, set the AVPlayer for which the |
** player layer displays visual output. |
** NOTE: this method is invoked on the main queue. |
** ------------------------------------------------------- */ |
- (void)observeValueForKeyPath:(NSString*) path |
ofObject:(id)object |
change:(NSDictionary*)change |
context:(void*)context |
{ |
/* AVPlayerItem "status" property value observer. */ |
if (context == MyStreamingMovieViewControllerPlayerItemStatusObserverContext) |
{ |
[self syncPlayPauseButtons]; |
AVPlayerStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue]; |
switch (status) |
{ |
/* Indicates that the status of the player is not yet known because |
it has not tried to load new media resources for playback */ |
case AVPlayerStatusUnknown: |
{ |
[self removePlayerTimeObserver]; |
[self syncScrubber]; |
[self disableScrubber]; |
[self disablePlayerButtons]; |
} |
break; |
case AVPlayerStatusReadyToPlay: |
{ |
/* Once the AVPlayerItem becomes ready to play, i.e. |
[playerItem status] == AVPlayerItemStatusReadyToPlay, |
its duration can be fetched from the item. */ |
playerLayerView.playerLayer.hidden = NO; |
[toolBar setHidden:NO]; |
/* Show the movie slider control since the movie is now ready to play. */ |
movieTimeControl.hidden = NO; |
[self enableScrubber]; |
[self enablePlayerButtons]; |
playerLayerView.playerLayer.backgroundColor = [[UIColor blackColor] CGColor]; |
/* Set the AVPlayerLayer on the view to allow the AVPlayer object to display |
its content. */ |
[playerLayerView.playerLayer setPlayer:player]; |
[self initScrubberTimer]; |
} |
break; |
case AVPlayerStatusFailed: |
{ |
AVPlayerItem *thePlayerItem = (AVPlayerItem *)object; |
[self assetFailedToPrepareForPlayback:thePlayerItem.error]; |
} |
break; |
} |
} |
/* AVPlayer "rate" property value observer. */ |
else if (context == MyStreamingMovieViewControllerRateObservationContext) |
{ |
[self syncPlayPauseButtons]; |
} |
/* AVPlayer "currentItem" property observer. |
Called when the AVPlayer replaceCurrentItemWithPlayerItem: |
replacement will/did occur. */ |
else if (context == MyStreamingMovieViewControllerCurrentItemObservationContext) |
{ |
AVPlayerItem *newPlayerItem = [change objectForKey:NSKeyValueChangeNewKey]; |
/* New player item null? */ |
if (newPlayerItem == (id)[NSNull null]) |
{ |
[self disablePlayerButtons]; |
[self disableScrubber]; |
self.isPlayingAdText.text = @""; |
} |
else /* Replacement of player currentItem has occurred */ |
{ |
/* Set the AVPlayer for which the player layer displays visual output. */ |
[playerLayerView.playerLayer setPlayer:self.player]; |
/* Specifies that the player should preserve the video’s aspect ratio and |
fit the video within the layer’s bounds. */ |
[playerLayerView setVideoFillMode:AVLayerVideoGravityResizeAspect]; |
[self syncPlayPauseButtons]; |
} |
} |
/* Observe the AVPlayer "currentItem.timedMetadata" property to parse the media stream |
timed metadata. */ |
else if (context == MyStreamingMovieViewControllerTimedMetadataObserverContext) |
{ |
NSArray* array = [[player currentItem] timedMetadata]; |
for (AVMetadataItem *metadataItem in array) |
{ |
[self handleTimedMetadata:metadataItem]; |
} |
} |
else |
{ |
[super observeValueForKeyPath:path ofObject:object change:change context:context]; |
} |
return; |
} |
@end |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-07-14