AVARLDelegateDemo/APLViewController.m

/*
 
 
     File: APLViewController.m
 Abstract: ViewController class implementation, defines categories: PlayControl and PlayAsset.
  Version: 1.0
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 
 
 */
#import <AVFoundation/AVFoundation.h>
#import "APLViewController.h"
#import "APLPlayerView.h"
#import "APLCustomAVARLDelegate.h"
 
/* Asset keys */
NSString * const kPlayableKey       = @"playable";
 
/* PlayerItem keys */
NSString * const kStatusKey         = @"status";
 
/* AVPlayer keys */
NSString * const kRateKey           = @"rate";
NSString * const kCurrentItemKey    = @"currentItem";
 
static void *AVARLDelegateDemoViewControllerRateObservationContext = &AVARLDelegateDemoViewControllerRateObservationContext;
static void *AVARLDelegateDemoViewControllerStatusObservationContext = &AVARLDelegateDemoViewControllerStatusObservationContext;
static void *AVARLDelegateDemoViewControllerCurrentItemObservationContext = &AVARLDelegateDemoViewControllerCurrentItemObservationContext;
 
 
@interface APLViewController ()
{
    BOOL seekToZeroBeforePlay;
    APLCustomAVARLDelegate *delegate;
}
 
@property (retain, nonatomic) IBOutlet APLPlayerView *playView;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *pauseButton;
@property (strong, nonatomic) IBOutlet UIBarButtonItem *playButton;
@property (strong, nonatomic) IBOutlet UIToolbar *toolbar;
 
@property (nonatomic, copy) NSURL* URL;
@property (readwrite, retain, setter=setPlayer:, getter=player) AVPlayer* player;
@property (retain) AVPlayerItem* playerItem;
 
- (IBAction) issuePause:(id)sender;
- (IBAction) issuePlay:(id)sender;
- (void) setupToolbar;
- (void) initializeView;
- (void) viewDidLoad;
- (void) setURL:(NSURL *)URL;
- (void) configDelegates:(AVURLAsset *) asset;
@end
 
/*!
 *  Interface for the play control buttons.
 *  Play
 *  Pause
 */
@interface APLViewController (PlayControl)
- (void) showButton:(id) button;
- (void) showPauseButton;
- (void) showPlayButton;
- (void) syncPlayPauseButtons;
- (void) enablePlayerButtons;
- (void) disablePlayerButtons;
@end
 
/*!
 *  Interface for the AVPlayer
 *  - observe the properties
 *  - initialize the play
 *  - play status
 *  - play failed
 *  - play ended
 */
@interface APLViewController (PlayAsset)
- (void) observeValueForKeyPath:(NSString*) path ofObject:(id)object change:(NSDictionary*)change context:(void*)context;
- (void) prepareToPlayAsset:(AVURLAsset *)asset withKeys:(NSArray *)requestedKeys;
- (BOOL) isPlaying;
- (void) assetFailedToPrepareForPlayback:(NSError *)error;
- (void) playerItemDidReachEnd:(NSNotification *)notification;
@end
 
#pragma mark - APLViewController
 
@implementation APLViewController
 
@synthesize player, playerItem, playView, toolbar, playButton, pauseButton;
 
- (void) setupToolbar
{
    self.toolbar.items = [NSArray arrayWithObjects:self.playButton,  nil];
    [self syncPlayPauseButtons];
}
 
- (void) initializeView
{
    // Restore saved media from the defaults system.
    NSURL *URL = [NSURL URLWithString:@"cplp://devimages.apple.com/samplecode/AVARLDelegateDemo/BipBop_gear3_segmented/redirect_prog_index.m3u8"];
    
    if (URL)
    {
        [self setURL:URL];
    }
}
 
- (void) viewDidLoad
{
    [self setupToolbar];
    [self initializeView];
    [super viewDidLoad];    
}
 
/*!
 *  Create the asset to play (using the given URL).
 *  Configure the asset properties and callbacks when the asset is ready.
 */
- (void) setURL:(NSURL*)URL
{
    if ([self URL] != URL)
    {
        self->_URL = [URL copy];
        
        /*
         Create an asset for inspection of a resource referenced by a given URL.
         Load the values for the asset keys  "playable".
         */
        AVURLAsset *asset = [AVURLAsset URLAssetWithURL:self.URL options:nil];
        [self configDelegates:asset];
        
        NSArray *requestedKeys = [NSArray arrayWithObjects: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];
                            });
         }];
    }
    
}
 
/*!
 *  Create and setup the custom delegae instance.
 */
- (void) configDelegates:(AVURLAsset*) asset
{
    //Setup the delegate for custom URL.
    self->delegate = [[APLCustomAVARLDelegate alloc] init];
    AVAssetResourceLoader *resourceLoader = asset.resourceLoader;
    [resourceLoader setDelegate:delegate queue:dispatch_queue_create("AVARLDelegateDemo loader", nil)];
    
}
 
/*!
 *  Gets called when the play button is pressed.
 *  Start the playback of the asset and show the pause button.
 */
- (IBAction) issuePlay:(id)sender {
    if (YES == seekToZeroBeforePlay)
    {
        seekToZeroBeforePlay = NO;
        [self.player seekToTime:kCMTimeZero];
    }
    
    [self.player play];
    [self showPauseButton ];
}
 
/*!
 *  Gets called when the pause button is pressed.
 *  Stop the play and show the play button.
 */
- (IBAction) issuePause:(id)sender {
    [self.player pause];
    [self showPlayButton];
}
@end
 
#pragma mark - APLViewController PlayControl
 
@implementation APLViewController (PlayControl)
 
- (void) showButton:(id)button
{
    NSMutableArray *toolbarItems = [NSMutableArray arrayWithArray:[self.toolbar items]];
    [toolbarItems replaceObjectAtIndex:0 withObject:button];
    self.toolbar.items = toolbarItems;
}
 
- (void) showPlayButton
{
    [self showButton:self.playButton];
}
 
- (void) showPauseButton
{
    [self showButton:self.pauseButton];
}
 
- (void) syncPlayPauseButtons
{
    //If we are playing, show the pause button otherwise show the play button
    if ([self isPlaying])
    {
        [self showPauseButton];
    } else
    {
        [self showPlayButton];
    }
}
 
-(void) enablePlayerButtons
{
    self.playButton.enabled = YES;
    self.pauseButton.enabled = YES;
}
 
-(void) disablePlayerButtons
{
    self.playButton.enabled = NO;
    self.pauseButton.enabled = NO;
}
 
@end
 
#pragma mark - APLViewController PlayAsset
 
@implementation APLViewController (PlayAsset)
/*!
 *  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 == AVARLDelegateDemoViewControllerStatusObservationContext)
    {
        [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 disablePlayerButtons];
            }
            break;
                
            case AVPlayerStatusReadyToPlay:
            {
                /* Once the AVPlayerItem becomes ready to play, i.e.
                 [playerItem status] == AVPlayerItemStatusReadyToPlay,
                 its duration can be fetched from the item. */
                
                [self enablePlayerButtons];
            }
            break;
                
            case AVPlayerStatusFailed:
            {
                AVPlayerItem *pItem = (AVPlayerItem *)object;
                [self assetFailedToPrepareForPlayback:pItem.error];
            }
            break;
        }
    }
    /* AVPlayer "rate" property value observer. */
    else if (context == AVARLDelegateDemoViewControllerRateObservationContext)
    {
        [self syncPlayPauseButtons];
    }
    /* 
      AVPlayer "currentItem" property observer.
      Called when the AVPlayer replaceCurrentItemWithPlayerItem:
      replacement will/did occur. 
     */
    else if (context == AVARLDelegateDemoViewControllerCurrentItemObservationContext)
    {
        AVPlayerItem *newPlayerItem = [change objectForKey:NSKeyValueChangeNewKey];
        
        /* Is the new player item null? */
        if (newPlayerItem == (id)[NSNull null])
        {
            [self disablePlayerButtons];
        }
        else /* Replacement of player currentItem has occurred */
        {
            /* Set the AVPlayer for which the player layer displays visual output. */
            [self.playView setPlayer:self.player];
            
            /* Specifies that the player should preserve the video’s aspect ratio and
             fit the video within the layer’s bounds. */
            [self.playView setVideoFillMode:AVLayerVideoGravityResizeAspect];
            
            [self syncPlayPauseButtons];
        }
    }
    else
    {
        [super observeValueForKeyPath:path ofObject:object change:change context:context];
    }
 
}
 
/*!
 *  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 -[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 contents of the resource at the specified URL are not playable.", @"Item cannot be played failure reason");
        NSDictionary *errorDict = [NSDictionary dictionaryWithObjectsAndKeys:
                                   localizedDescription, NSLocalizedDescriptionKey,
                                   localizedFailureReason, NSLocalizedFailureReasonErrorKey,
                                   nil];
        NSError *assetCannotBePlayedError = [NSError errorWithDomain:[[NSBundle mainBundle] bundleIdentifier] 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. */
    
    /* 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:AVARLDelegateDemoViewControllerStatusObservationContext];
    
    /* 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:AVARLDelegateDemoViewControllerCurrentItemObservationContext];
        
        /* Observe the AVPlayer "rate" property to update the scrubber control. */
        [self.player addObserver:self
                      forKeyPath:kRateKey
                         options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
                         context:AVARLDelegateDemoViewControllerRateObservationContext];
    }
    
    /* 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];
    }
    
}
 
- (BOOL) isPlaying
{
    return [self.player rate] != 0.f;
}
 
/*!
 *  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 disablePlayerButtons];
    
    /* Display the error. */
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:[error localizedDescription]
                                                        message:[error localizedFailureReason]
                                                       delegate:nil
                                              cancelButtonTitle:@"OK"
                                              otherButtonTitles:nil];
    [alertView show];
}
 
/*! 
 *  Called when the player item has played to its end time.
 */
- (void) playerItemDidReachEnd:(NSNotification *)notification
{
    /* After the movie has played to its end time, seek back to time zero
     to play it again. */
    seekToZeroBeforePlay = YES;
}
@end