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!
This is solution ROCKS!!! thank you so much!!
good approach…!!
Hi am having list of songs url in UITableView how can i play the songs one by one
Hi Sancho,
Using AVPlayer, you could observe AVPlayerItemDidPlayToEndTimeNotification to handle when each song finishes playing, then instantiate a new AVPlayer for the next song URL. The accepted answer on this stackoverflow post describes that approach: http://stackoverflow.com/questions/8102201/how-to-play-multiple-audio-files-in-a-row-with-avaudioplayer
However, a better solution might be to use AVQueuePlayer, which is a subclass of AVPlayer that plays a number of items in sequence:
https://developer.apple.com/library/ios/documentation/AVFoundation/Reference/AVQueuePlayer_Class/index.html#//apple_ref/swift/cl/AVQueuePlayer
First, create AVPlayerItem objects for each of your song URLs:
AVPlayerItem *aSong = [[AVPlayerItem alloc] initWithURL:aUrl];
Add those AVPlayerItem objects to an array (I’m assuming an array called “items” here). Then a new AVQueuePlayer can be initialized with the array of AVPlayerItem objects:
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];
Then to play the song in sequence, just call play in AVQueuePlayer:
[queuePlayer play];
Hope that helps!