Detect when audio playback begins with AVPlayer in iOS

Update: February 9, 2017 – We’ve revisited this technique using Swift 3, and added a sample Xcode project you can download in a new post: Detect When iOS AVPlayer Finishes Buffering Using Swift

Recently we’ve been working on an iOS App involving audio playback, and wanted to have an animation timed to begin when audio playback starts. We were using AVPlayer for audio playback, so naturally our first instinct was to observe some property on the AVPlayer object. The status property seemed a likely candidate, whose possible values are defined in the AVPlayerItemStatus enum:

 enum {
    AVPlayerItemStatusUnknown,
    AVPlayerItemStatusReadyToPlay,
    AVPlayerItemStatusFailed
 };
 typedef NSInteger AVPlayerItemStatus;

The AVPlayerItemStatusReadyToPlay status seemed promising, but it did not take long to realize that being ready to play and actually playing were quite a bit different. The “Ready to Play” state change happens before the audio is actually heard, and there is a noticeable delay while the player initializes and buffers. Responding to the actual start of playback was going to require a different approach.

After a fair amount of research, and a whole bunch of experimentation, we came across the addBoundaryTimeObserverForTimes:queue:usingBlock: method on AVPlayer.

As per Apple’s documentation, this method

Requests invocation of a block when specified times are traversed during normal playback.

Not exactly an obvious solution, but we figured if we could set up a boundary time to execute a block at the start of playback, we’d have a way to raise a notification. The general approach looked like this:

// Initialize the AVPlayer to play audio from a specified URL
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url];
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];

// Declare block scope variables to avoid retention cycles 
// from references inside the block
__block AVPlayer* blockPlayer = player;
__block id obs;

// Setup boundary time observer to trigger when audio really begins,
// specifically after 1/3 of a second playback
obs = [player addBoundaryTimeObserverForTimes:
                    @[[NSValue valueWithCMTime:CMTimeMake(1, 3)]]
              queue:NULL
              usingBlock:^{
                                            
                  // Raise a notificaiton when playback has started
                  [[NSNotificationCenter defaultCenter]
                     postNotificationName:@"PlaybackStartedNotification"
                     object:url];
                                            
                  // Remove the boundary time observer
                   [blockPlayer removeTimeObserver:obs];
              }];

In our PlaybackStartedNotificaiton, we’re passing along the URL so we know what’s started to play. To avoid leaking memory, as per Apple’s docs, every invocation of addBoundaryTimeObserverForTimes: should be paired with a corresponding call to removeTimeObserver:, so we’ve included that inside our block.

Then, elsewhere in our App, we’re able to register to receive our PlaybackStartedNotification:

[[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(receivePlaybackStartedNotification:)
        name:@"PlaybackStartedNotification"
      object:nil];

And when we receive that notification we can do something novel – in this case start up a UIImage animation:

-(void) receivePlaybackStartedNotification:(NSNotification *) notification
{
    if ([[notification name] 
            isEqualToString:@"PlaybackStartedNotification"]) {

        NSURL *url = [notification object];
        NSLog(@"PlaybackStartedNotification %@", url);
        [self.audioImage startAnimating];
    }
}

Groovy!

4 comments

Leave a Reply

Your email address will not be published.

top