Detect currently playing song information in native iOS Music App - ios

I'm having a some issues to get the current playing song information like (title, artist...) in the native iOS Music App.
This is the exact problem, my app is using a MPMusicPlayerController with iPodMusicPlayer this property is call myPlayer. When the user is controlling the music within my app I'm able to display the current song playing information in this way...
- (void)getTrackDescription {
// getting whats currently playing
self.nowPlayingItem = [myPlayer nowPlayingItem];
// song title currently playing
self.title = [self.nowPlayingItem valueForProperty:MPMediaItemPropertyTitle];
// if title is not fund Unknown will be displayed
if (title == (id)[NSNull null] || title.length == 0) {
title = #"Unknown";
}
// artist currently playing
self.artist = [self.nowPlayingItem valueForProperty:MPMediaItemPropertyArtist];
// if artist is not fund Unknown will be displayed
if (artist == (id)[NSNull null] || artist.length == 0) {
artist = #"Unknown";
}
//[self.currentlyPlaying setLineBreakMode:NSLineBreakByWordWrapping];
// displaying current artist and title song playing
[self.currentlyPlaying setText:[NSString stringWithFormat:#"%# - %#", artist, title]];
}
But, the problem comes when the user leave the app in the background or just use the Control Center to change the song.. My app still displays the previous song information and nothing gets update if the user continues using Control Center or the Music app itself.
I tried to solve the problem in this way...
- (void)viewDidLoad {
[super viewDidLoad];
if (self.myPlayer.playbackState == MPMusicPlaybackStatePlaying || self.myPlayer.playbackState == MPMusicPlaybackStateSeekingForward || self.myPlayer.playbackState == MPMusicPlaybackStateSeekingBackward){
// updating track name regardless the user uses app controllers or not
[self getTrackDescription];
}
}
I even tried commenting the if condition to see if I get the song information in that way but it was the same problem.
Well, I hope someone can help me out and explain what I'm doing wrong..
Thanks in advance...!
UPDATE
This what I have at the moment... My getTrackDescription logic is the same but the signature changed to this - (void)getTrackDescription:(id)notification
- (void)viewWillAppear:(BOOL)animated{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:#selector(getTrackDescription:)
name:MPMusicPlayerControllerPlaybackStateDidChangeNotification
object:self.myPlayer];
[self.myPlayer beginGeneratingPlaybackNotifications];
}
-(void)viewWillDisappear:(BOOL)animated{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:MPMusicPlayerControllerPlaybackStateDidChangeNotification
object:self.myPlayer];
[self.myPlayer endGeneratingPlaybackNotifications];
}

What you're going to want to do is add your view controller as an observer to the MPMusicPlayerControllerNowPlayingItemDidChangeNotification notification, which as it sounds, gets called every time the track changes in the music player. Don't forget to specify when the player should begin/end generating these notifications with the following methods.
[[MPMusicPlayerController iPodMusicPlayer] beginGeneratingPlaybackNotifications];
[[MPMusicPlayerController iPodMusicPlayer] endGeneratingPlaybackNotifications];

This is my complete answer to the problem..
1) I set these two methods.. and set an Observer MPMusicPlayerControllerNowPlayingItemDidChangeNotification which will take care of updating my song information regardless.. with getTrackDescription which still the same as my previous post..
- (void)viewWillAppear:(BOOL)animated{
// creating simple audio player
self.myPlayer = [MPMusicPlayerController iPodMusicPlayer];
// assing a playback queue containing all media items on the device
[self.myPlayer setQueueWithQuery:[MPMediaQuery songsQuery]];
self.notificationCenter = [NSNotificationCenter defaultCenter];
[self.notificationCenter addObserver:self
selector:#selector(getTrackDescription:)
name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification
object:self.myPlayer];
[self.notificationCenter addObserver:self
selector:#selector(handle_PlayBackNotification:)
name:MPMusicPlayerControllerPlaybackStateDidChangeNotification
object:self.myPlayer];
[self.myPlayer beginGeneratingPlaybackNotifications];
}
-(void)viewWillDisappear:(BOOL)animated {
[self.notificationCenter removeObserver:self
name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification
object:self.myPlayer];
[self.notificationCenter removeObserver:self
name:MPMusicPlayerControllerPlaybackStateDidChangeNotification
object:self.myPlayer];
[self.myPlayer endGeneratingPlaybackNotifications];
}
Then the magic will happen by itself.. it just basically what #0x7fffffff recommended me to do but I had to do some digging because I wasn't familiar with notifications at all..
So, just in case if you want to see how I toggle my play/pause button for the Observer MPMusicPlayerControllerPlaybackStateDidChangeNotification this is how I did it..
- (void)handle_PlayBackNotification:(id)notification{
if(myPlayer.playbackState == 1){
// sets the pause image in play button
[self.pauseBtn setBackgroundImage:[UIImage imageNamed:#"pauseBtn2.png"] forState:(UIControlStateNormal)];
} else {
// resets the image to normal play image
[self.pauseBtn setBackgroundImage: nil forState:(UIControlStateNormal)];
}
}
just in case this my getTrackDescription ...
- (void)getTrackDescription:(id)notification {
// getting whats currently playing
self.nowPlayingItem = self.myPlayer.nowPlayingItem;
// song title currently playing
self.title = [self.nowPlayingItem valueForProperty:MPMediaItemPropertyTitle];
// if title is not fund Unknown will be displayed
if (title == (id)[NSNull null] || title.length == 0) {
title = #"Unknown";
}
// artist currently playing
self.artist = [self.nowPlayingItem valueForProperty:MPMediaItemPropertyArtist];
// if artist is not fund Unknown will be displayed
if (artist == (id)[NSNull null] || artist.length == 0) {
artist = #"Unknown";
}
// displaying current artist and title song playing
[self.currentlyPlaying setText:[NSString stringWithFormat:#"%# - %#", artist, title]];
}

Related

Crashed: AVAudioSession Notify Thread in iOS

I am getting EXC_BAD_ACCESS crash that occurs in the AudioToolBox. How to handle interrupts properly?
Please have a look at crashlytics screenshot for more info.
My audio streaming player was crashing when I get a phone call/ faceTime. It was actually an older class with Non ARC. Simply Add an InterruptionNotification Observer for the streaming class, and if the streaming class is playing we need to pause the player instance while interrupt begins.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleInterruptionChangeToState:) name:#"ASAudioSessionInterruptionOccuredNotification" object:nil];
- (void)handleInterruptionChangeToState:(NSNotification *)notification {
AudioQueuePropertyID inInterruptionState = (AudioQueuePropertyID) [notification.object unsignedIntValue];
if (inInterruptionState == kAudioSessionBeginInterruption){
if ([self isPlaying]) {
[self pause];
pausedByInterruption = YES; //a global Bool variable
}
}
else if (inInterruptionState == kAudioSessionEndInterruption){
AudioSessionSetActive( true );
if ([self isPaused] && pausedByInterruption) {
[self pause]; // this is actually resume
pausedByInterruption = NO;
}
}
}
Hope it helps.

How to update MPMusicPlayerController when song changes

I was wondering you would update the UI when the song changes in MPMusicPlayerController. For example, if you have an image view with the album cover, but a user presses the skip button, how would you update the image view's image with the new album cover?
To control music playback, we use an instance of MPMusicPlayerController. There are two types of music players. The iPodMusicPlayer is a reference to the music player instance used by the iPod app. Any settings you change, such as the shuffle or repeat modes, will be changed in the iPod app, too. If the iPod is playing when your application starts, the music will continue playing and you can access the current song and skip back and forward through the currently active playlist. When your app quits, the music will continue playing. I imagine this mode is very handy for most utility apps that try to improve your music listening experience by interacting with the iPod.
In contrast, applicationMusicPlayer gives you a music player whose settings you can change independently of the iPod app. This is probably the way to go if your app is a game and you want to give the user the ability to choose the background music from their library. In Songtext, we’ll use iPodMusicPlayer because we want to know which song is playing when our app launches:
#property (nonatomic, strong) MPMusicPlayerController *musicPlayer;
self.musicPlayer = [MPMusicPlayerController iPodMusicPlayer];
The music player uses notifications to inform you about changes of:
- The current song (MPMusicPlayerControllerNowPlayingItemDidChangeNotification),
- The play/paused/stopped state (MPMusicPlayerControllerPlaybackStateDidChangeNotification), or
- The volume (MPMusicPlayerControllerVolumeDidChangeNotification).
So the next thing you typically do is to register yourself as an observer for the notifications you are interested in, e.g. in viewDidLoad. We want to receive all 3 notifications:
// Register for music player notifications
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:#selector(handleNowPlayingItemChanged:)
name:MPMusicPlayerControllerNowPlayingItemDidChangeNotification
object:self.musicPlayer];
[notificationCenter addObserver:self
selector:#selector(handlePlaybackStateChanged:)
name:MPMusicPlayerControllerPlaybackStateDidChangeNotification
object:self.musicPlayer];
[notificationCenter addObserver:self
selector:#selector(handleExternalVolumeChanged:)
name:MPMusicPlayerControllerVolumeDidChangeNotification
object:self.musicPlayer];
[self.musicPlayer beginGeneratingPlaybackNotifications];
There is one other related notification that is sent by the iPod media library when the contents of the library change, e.g. when you sync your device with iTunes. You must listen to this notification if your app creates its playlists that need to be updated after library changes. To do so, register yourself as an observer for MPMediaLibraryDidChangeNotification notifications and call:
[[MPMediaLibrary defaultMediaLibrary] beginGeneratingLibraryChangeNotifications]
The notification handlers are where you update your UI in response to changes in the player’s state:
// When the now playing item changes, update song info labels and artwork display.
- (void)handleNowPlayingItemChanged:(id)notification {
// Ask the music player for the current song.
MPMediaItem *currentItem = self.musicPlayer.nowPlayingItem;
// Display the artist, album, and song name for the now-playing media item.
// These are all UILabels.
self.songLabel.text = [currentItem valueForProperty:MPMediaItemPropertyTitle];
self.artistLabel.text = [currentItem valueForProperty:MPMediaItemPropertyArtist];
self.albumLabel.text = [currentItem valueForProperty:MPMediaItemPropertyAlbumTitle];
// Display album artwork. self.artworkImageView is a UIImageView.
CGSize artworkImageViewSize = self.artworkImageView.bounds.size;
MPMediaItemArtwork *artwork = [currentItem valueForProperty:MPMediaItemPropertyArtwork];
if (artwork != nil) {
self.artworkImageView.image = [artwork imageWithSize:artworkImageViewSize];
} else {
self.artworkImageView.image = nil;
}
}
// When the playback state changes, set the play/pause button appropriately.
- (void)handlePlaybackStateChanged:(id)notification {
MPMusicPlaybackState playbackState = self.musicPlayer.playbackState;
if (playbackState == MPMusicPlaybackStatePaused || playbackState == MPMusicPlaybackStateStopped) {
[self.playPauseButton setTitle:#"Play" forState:UIControlStateNormal];
} else if (playbackState == MPMusicPlaybackStatePlaying) {
[self.playPauseButton setTitle:#"Pause" forState:UIControlStateNormal];
}
}
// When the volume changes, sync the volume slider
- (void)handleExternalVolumeChanged:(id)notification {
// self.volumeSlider is a UISlider used to display music volume.
// self.musicPlayer.volume ranges from 0.0 to 1.0.
[self.volumeSlider setValue:self.musicPlayer.volume animated:YES];
}
In the case you are looking for the lock screen playing info:
Have a look at MPNowPlayingInfoCenter
You need to pass a NSDictionary, here's an example:
-(void)updateNowPlayingInfo
{
//Set Values for MPNowPlayingInfoCenter
NSArray *keys = [NSArray arrayWithObjects:
MPMediaItemPropertyTitle,
MPMediaItemPropertyPlaybackDuration,
MPNowPlayingInfoPropertyPlaybackRate,
MPNowPlayingInfoPropertyElapsedPlaybackTime,
nil];
NSArray *values = [NSArray arrayWithObjects:
self.currentPlayingVideo.title,
[NSNumber numberWithFloat:self.currentPlayingVideo.duration],
[NSNumber numberWithInt:1],
[NSNumber numberWithDouble:self.currentVideoPlayView.videoPlayerViewController.moviePlayer.currentPlaybackTime],
nil];
NSDictionary *mediaInfo = [NSDictionary dictionaryWithObjects:values forKeys:keys];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:mediaInfo];
}

MPMoviePlayerController seek forward button stops the video in IOS7?

I am facing an issue with MPMoviePlayerController in iOS 7. When i single tap on the forward seek button the video stops and not allow to do anything like to play again full screen and slider change.
Here is my code.
remove the Observer for the MPMoviePlayerPlaybackDidFinishNotification
[[NSNotificationCenter defaultCenter] removeObserver:moviePlayerViewController name:MPMoviePlayerPlaybackDidFinishNotification object:moviePlayerViewController.moviePlayer];
and add New Notification MPMoviePlayerPlaybackDidFinishNotification
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(videoFinished:) name:MPMoviePlayerPlaybackDidFinishNotification object:nil];
Here is my custom method to handle the MPMoviePlayerPlaybackDidFinishNotification
-(void)videoFinished:(NSNotification*)aNotification{
MPMoviePlayerController *moviePlayer = [aNotification object];
NSLog(#"%f",moviePlayer.currentPlaybackTime);
int reason = [[[aNotification userInfo] valueForKey:MPMoviePlayerPlaybackDidFinishReasonUserInfoKey] intValue];
if (reason == MPMovieFinishReasonPlaybackEnded) {
}else if (reason == MPMovieFinishReasonUserExited) {
[self performSelector:#selector(dismiss:) withObject:aNotification afterDelay:0.5];
}else if (reason == MPMovieFinishReasonPlaybackError) {
}
}
I need to stop this strange behaviour on single click and continue to play.
Anyone know how to do this?
Thanks.
I think there are no any notifications or event are available on user
interaction with the standard player buttons, and i have to implement
own UI for the player controls. by this way we can then determine the
actions for a single touch, long touch, etc. Then, we can add whatever
functionality like increasing the play rate, or simply seeking to a
time.

iOS 7 MPMoviePlayerController seek forward button brings the video to the End and displays Black screen

I am facing an issue with MPMoviePlayerController in iOS 7. I enter the fullscreen and then click (just a single tap) on seek forward button (>>|) , and the video playback ends and gives a black screen with a text "Loading" on the header.
I registered notification for "MPMoviePlayerPlaybackStateDidChangeNotification".
**[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerPlaybackStateDidChange:)
name:MPMoviePlayerPlaybackStateDidChangeNotification
object:self.player];**
It does not get fired on a single click of seek forward button.
Also on registration of "MPMoviePlayerPlaybackDidFinishNotification"
**[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerPlaybackDidFinish:)
name:MPMoviePlayerPlaybackDidFinishNotification
object:nil];**
I get "MPMovieFinishReasonPlaybackEnded" event fired on that single click of seek forward button.
Any one knows the reason why? Is this a bug in apple?
I need to either stop this behavior of showing a black screen on single click , or just disable single click of seek forward button so that nothing happens.
Any one knows how to achieve this?
I fixed this by removing the MPMoviePlayer object completely, setting it to nil, removing it from it's superview and re-adding it using the original video Url. Code below:
- (void)addPlayerForUrl:(NSURL *)url {
self.player = [[MPMoviePlayerController alloc] initWithContentURL:url];
self.player.view.frame = self.videoView.bounds;
self.player.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.player.controlStyle = MPMovieControlStyleDefault;
[self.videoView insertSubview:self.player.view atIndex:0];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerLoadStateDidChangedNotification:)
name:MPMoviePlayerReadyForDisplayDidChangeNotification
object:self.player];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePlayerPlaybackStateDidChangeNotification:)
name:MPMoviePlayerPlaybackStateDidChangeNotification
object:self.player];
}
#pragma mark - Notifications
- (void)moviePlayerLoadStateDidChangedNotification:(NSNotification *)notification {
self.isVideoPreloaded = YES;
self.videoPlayButton.hidden = YES;
self.photoImageView.hidden = YES;
self.videoLoadingImageView.hidden = YES;
}
- (void)moviePlayerPlaybackStateDidChangeNotification:(NSNotification *)notification {
NSURL *url = self.player.contentURL;
switch (self.player.playbackState) {
case MPMoviePlaybackStateSeekingBackward:
case MPMoviePlaybackStateSeekingForward:
break;
case MPMoviePlaybackStatePlaying:
self.videoPlayButton.hidden = YES;
if (!self.isVideoPreloaded) {
self.videoLoadingImageView.hidden = NO;
[self.videoLoadingImageView startAnimating];
} else {
self.videoLoadingImageView.hidden = YES;
}
break;
case MPMoviePlaybackStatePaused:
case MPMoviePlaybackStateStopped:
self.videoPlayButton.hidden = NO;
self.videoLoadingImageView.hidden = YES;
[self.player endSeeking];
[self.player.view removeFromSuperview];
[self.player setFullscreen:NO];
self.player = nil;
[self addPlayerForUrl:url];
break;
default:
break;
}
}
Notice how I keep the NSURL, right before the switch statement in the moviePlayerPlaybackStateDidChangeNotification. That way, I can re-initialize and re-add the MPMoviePlayer object.
Btw, my mpmovieplayer is on a tableviewCell if you're wondering. Hope this helps and let me know if you have questions. Good luck!
MPMoviePlayerLoadStateDidChangeNotification will be called when you single tap on the fast-forward or rewind button. You should check the loadState and just give it the path to your video and prepareToPlay again.
- (void)moviePlayerLoadStateChanged:(NSNotification *)notification {
MPMoviePlayerController *moviePlayer = notification.object;
MPMovieLoadState loadState = moviePlayer.loadState;
if(loadState == MPMovieLoadStateUnknown) {
moviePlayer.contentURL = [NSURL fileURLWithPath:videoPath]
[moviePlayer prepareToPlay];
}
.....
}
The reason you're getting MPMovieFinishReasonPlaybackEnded is because playback reached the end of the video (sorry if this is obvious). So it seem's your seek forward actions are seeking all the way to the end of the video. You can check the playback state with MPMoviePlaybackStateSeekingForward.
A quick solution could be to create your own forward button that seek's ahead by a specified time (ie. 5 seconds). But perhaps this isn't the functionality you're looking for.

AVPlayer stops playing and doesn't resume again

In my application I have to play audio files stored on a web server. I'm using AVPlayer for it. I have all the play/pause controls and all delegates and observers there which work perfectly fine. On playing small audio files everything works great.
When a long audio file is played it also starts playing fine but after some seconds the AVPlayer pauses the playing (most probably to buffer it). The issue is it doesn't resume on its own again. It keeps in a pause state and if I manually press the play button again it plays smoothly again.
I want to know why AVPlayer doesn't resume automatically and how can I manage to resume the audio again without user pressing the play button again? Thanks.
Yes, it stops because the buffer is empty so it has to wait to load more video. After that you have to manually ask for start again. To solve the problem I followed these steps:
1) Detection: To detect when the player has stopped I use the KVO with the rate property of the value:
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:#"rate"] )
{
if (self.player.rate == 0 && CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime) && self.videoPlaying)
{
[self continuePlaying];
}
}
}
This condition: CMTimeGetSeconds(self.playerItem.duration) != CMTimeGetSeconds(self.playerItem.currentTime) is to detect the difference between arriving at the end of the video or stopping in the middle
2) Wait for the video to load - If you continue playing directly you will not have enough buffer to continue playing without interruption. To know when to start you have to observe the value playbackLikelytoKeepUp from the playerItem (here I use a library to observe with blocks but I think it makes the point):
-(void)continuePlaying
{
if (!self.playerItem.playbackLikelyToKeepUp)
{
self.loadingView.hidden = NO;
__weak typeof(self) wSelf = self;
self.playbackLikelyToKeepUpKVOToken = [self.playerItem addObserverForKeyPath:#keypath(_playerItem.playbackLikelyToKeepUp) block:^(id obj, NSDictionary *change) {
__strong typeof(self) sSelf = wSelf;
if(sSelf)
{
if (sSelf.playerItem.playbackLikelyToKeepUp)
{
[sSelf.playerItem removeObserverForKeyPath:#keypath(_playerItem.playbackLikelyToKeepUp) token:self.playbackLikelyToKeepUpKVOToken];
sSelf.playbackLikelyToKeepUpKVOToken = nil;
[sSelf continuePlaying];
}
}
}];
}
And that's it! problem solved
Edit: By the way the library used is libextobjc
I am working with video files, so there's more to my code than you need, but the following solution should pause the player when it hangs, then check every 0.5 second to see whether we've buffered enough to keep up. If so, it restarts the player. If the player hangs for more than 10 seconds without restarting, we stop the player and apologize to the user. This means you need the right observers in place. The code below is working pretty well for me.
properties defined / init'd in a .h file or elsewhere:
AVPlayer *player;
int playerTryCount = -1; // this should get set to 0 when the AVPlayer starts playing
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
partial .m:
- (AVPlayer *)initializePlayerFromURL:(NSURL *)movieURL {
// create AVPlayer
AVPlayerItem *videoItem = [AVPlayerItem playerItemWithURL:movieURL];
AVPlayer *videoPlayer = [AVPlayer playerWithPlayerItem:videoItem];
// add Observers
[videoItem addObserver:self forKeyPath:#"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
[self startNotificationObservers]; // see method below
// I observe a bunch of other stuff, but this is all you need for this to work
return videoPlayer;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:#"playbackLikelyToKeepUp"]) {
if (self.player.currentItem.playbackLikelyToKeepUp == NO &&
CMTIME_COMPARE_INLINE(self.player.currentTime, >, kCMTimeZero) &&
CMTIME_COMPARE_INLINE(self.player.currentTime, !=, self.player.currentItem.duration)) {
// if so, post the playerHanging notification
[self.notificationCenter postNotificationName:PlayerHangingNotification object:self.videoPlayer];
}
}
}
- (void)startNotificationObservers {
[self.notificationCenter addObserver:self
selector:#selector(playerContinue)
name:PlayerContinueNotification
object:nil];
[self.notificationCenter addObserver:self
selector:#selector(playerHanging)
name:PlayerHangingNotification
object:nil];
}
// playerHanging simply decides whether to wait 0.5 seconds or not
// if so, it pauses the player and sends a playerContinue notification
// if not, it puts us out of our misery
- (void)playerHanging {
if (playerTryCount <= 10) {
playerTryCount += 1;
[self.player pause];
// start an activity indicator / busy view
[self.notificationCenter postNotificationName:PlayerContinueNotification object:self.player];
} else { // this code shouldn't actually execute, but I include it as dummyproofing
[self stopPlaying]; // a method where I clean up the AVPlayer,
// which is already paused
// Here's where I'd put up an alertController or alertView
// to say we're sorry but we just can't go on like this anymore
}
}
// playerContinue does the actual waiting and restarting
- (void)playerContinue {
if (CMTIME_COMPARE_INLINE(self.player.currentTime, ==, self.player.currentItem.duration)) { // we've reached the end
[self stopPlaying];
} else if (playerTryCount > 10) // stop trying
[self stopPlaying];
// put up "sorry" alert
} else if (playerTryCount == 0) {
return; // protects against a race condition
} else if (self.player.currentItem.playbackLikelyToKeepUp == YES) {
// Here I stop/remove the activity indicator I put up in playerHanging
playerTryCount = 0;
[self.player play]; // continue from where we left off
} else { // still hanging, not at end
// create a 0.5-second delay to see if buffering catches up
// then post another playerContinue notification to call this method again
// in a manner that attempts to avoid any recursion or threading nightmares
playerTryCount += 1;
double delayInSeconds = 0.5;
dispatch_time_t executeTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(executeTime, dispatch_get_main_queue(), ^{
// test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
if (playerTryCount > 0) {
if (playerTryCount <= 10) {
[self.notificationCenter postNotificationName:PlayerContinueNotification object:self.videoPlayer];
} else {
[self stopPlaying];
// put up "sorry" alert
}
}
});
}
Hope it helps!
Accepted answer gives a possible solution to the problem, but it lacks flexibility, also it's hard to read. Here's more flexible solution.
Add observers:
//_player is instance of AVPlayer
[_player.currentItem addObserver:self forKeyPath:#"status" options:0 context:nil];
[_player addObserver:self forKeyPath:#"rate" options:0 context:nil];
Handler:
-(void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if ([keyPath isEqualToString:#"status"]) {
if (_player.status == AVPlayerStatusFailed) {
//Possibly show error message or attempt replay from tart
//Description from the docs:
// Indicates that the player can no longer play AVPlayerItem instances because of an error. The error is described by
// the value of the player's error property.
}
}else if ([keyPath isEqualToString:#"rate"]) {
if (_player.rate == 0 && //if player rate dropped to 0
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, >, kCMTimeZero) && //if video was started
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, <, _player.currentItem.duration) && //but not yet finished
_isPlaying) { //instance variable to handle overall state (changed to YES when user triggers playback)
[self handleStalled];
}
}
}
Magic:
-(void)handleStalled {
NSLog(#"Handle stalled. Available: %lf", [self availableDuration]);
if (_player.currentItem.playbackLikelyToKeepUp || //
[self availableDuration] - CMTimeGetSeconds(_player.currentItem.currentTime) > 10.0) {
[_player play];
} else {
[self performSelector:#selector(handleStalled) withObject:nil afterDelay:0.5]; //try again
}
}
The "[self availableDuration]" is optional, but you can manually launch playback based on amount of video available. You can change how often the code checks whether enough video is buffered. If you decide to use the optional part, here's the method implementation:
- (NSTimeInterval) availableDuration
{
NSArray *loadedTimeRanges = [[_player currentItem] loadedTimeRanges];
CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];
Float64 startSeconds = CMTimeGetSeconds(timeRange.start);
Float64 durationSeconds = CMTimeGetSeconds(timeRange.duration);
NSTimeInterval result = startSeconds + durationSeconds;
return result;
}
Don't forget the cleanup. Remove observers:
[_player.currentItem removeObserver:self forKeyPath:#"status"];
[_player removeObserver:self forKeyPath:#"rate"];
And possible pending calls to handle stalled video:
[UIView cancelPreviousPerformRequestsWithTarget:self selector:#selector(handleStalled) object:nil];
I had a similar issue.
I had some local files i wanted to play, configured the AVPlayer and called [player play], the player stops at frame 0 and wouldn't play anymore until i called play again manually.
The accepted answer was impossible for me to implement due to faulty explanation, then i just tried delaying the play and magically worked
[self performSelector:#selector(startVideo) withObject:nil afterDelay:0.2];
-(void)startVideo{
[self.videoPlayer play];
}
For web videos i also had the problem, i solve it using wallace's answer.
When creating the AVPlayer add an observer:
[self.videoItem addObserver:self forKeyPath:#"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
// check that all conditions for a stuck player have been met
if ([keyPath isEqualToString:#"playbackLikelyToKeepUp"]) {
if (self.videoPlayer.currentItem.playbackLikelyToKeepUp == NO &&
CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, >, kCMTimeZero) &&
CMTIME_COMPARE_INLINE(self.videoPlayer.currentTime, !=, self.videoPlayer.currentItem.duration)) {
NSLog(#"hanged");
[self performSelector:#selector(startVideo) withObject:nil afterDelay:0.2];
}
}
}
Remember to remove observer before dismissing the view
[self.videoItem removeObserver:self forKeyPath:#"playbackLikelyToKeepUp"]
I think use AVPlayerItemPlaybackStalledNotification to detect the stalled is a better way.
First I observe for playback stalling
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(playerStalled),
name: AVPlayerItemPlaybackStalledNotification, object: videoPlayer.currentItem)
Then I force playback continuation
func playerStalled(note: NSNotification) {
let playerItem = note.object as! AVPlayerItem
if let player = playerItem.valueForKey("player") as? AVPlayer{
player.play()
}
}
This is probably not the best way of doing it, but I'm using it until I find something better :)
I also ran into this issue as described here
I tested this answer below multiple times and it worked every time so far.
Here is what I came up with for the Swift 5 version of #wallace's answer.
1- instead of observing the keyPath "playbackLikelyToKeepUp" I use the .AVPlayerItemPlaybackStalled Notification and inside there I check to see if the buffer is full or not via if !playerItem.isPlaybackLikelyToKeepUp {...}
2- instead of using his PlayerHangingNotification I use a function named playerIsHanging()
3- instead of using his PlayerContinueNotification I use a function named checkPlayerTryCount()
4- and inside checkPlayerTryCount() I do everything the same as his (void)playerContinue function except when I ran into } else if playerTryCount == 0 { nothing would happen. To avoid that I added 2 lines of code above the return statement
5- like #PranoyC suggested under #wallace's comments I set the playerTryCount to a max of 20 instead of 10. I also set it as a class property let playerTryCountMaxLimit = 20
You have to add/remove your activity indicator/spinner where the comment suggests to do so
code:
NotificationCenter.default.addObserver(self, selector: #selector(self.playerItemPlaybackStalled(_:)),
name: NSNotification.Name.AVPlayerItemPlaybackStalled,
object: playerItem)
#objc func playerItemPlaybackStalled(_ notification: Notification) {
// The system may post this notification on a thread other than the one used to registered the observer: https://developer.apple.com/documentation/foundation/nsnotification/name/1387661-avplayeritemplaybackstalled
guard let playerItem = notification.object as? AVPlayerItem else { return }
// playerItem.isPlaybackLikelyToKeepUp == false && if the player's current time is greater than zero && the player's current time is not equal to the player's duration
if (!playerItem.isPlaybackLikelyToKeepUp) && (CMTimeCompare(playerItem.currentTime(), .zero) == 1) && (CMTimeCompare(playerItem.currentTime(), playerItem.duration) != 0) {
DispatchQueue.main.async { [weak self] in
self?.playerIsHanging()
}
}
}
var playerTryCount = -1 // this should get set to 0 when the AVPlayer starts playing
let playerTryCountMaxLimit = 20
func playerIsHanging() {
if playerTryCount <= playerTryCountMaxLimit {
playerTryCount += 1
// show spinner
checkPlayerTryCount()
} else {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("1.-----> PROBLEM")
}
}
func checkPlayerTryCount() {
guard let player = player, let playerItem = player.currentItem else { return }
// if the player's current time is equal to the player's duration
if CMTimeCompare(playerItem.currentTime(), playerItem.duration) == 0 {
// show spinner or better yet remove spinner and show a replayButton or auto rewind to the beginning ***BE SURE TO RESET playerTryCount = 0 ***
} else if playerTryCount > playerTryCountMaxLimit {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("2.-----> PROBLEM")
} else if playerTryCount == 0 {
// *** in his answer he has nothing but a return statement here but when it would hit this condition nothing would happen. I had to add these 2 lines of code for it to continue ***
playerTryCount += 1
retryCheckPlayerTryCountAgain()
return // protects against a race condition
} else if playerItem.isPlaybackLikelyToKeepUp {
// remove spinner and reset playerTryCount to zero
playerTryCount = 0
player?.play()
} else { // still hanging, not at end
playerTryCount += 1
/*
create a 0.5-second delay using .asyncAfter to see if buffering catches up
then call retryCheckPlayerTryCountAgain() in a manner that attempts to avoid any recursion or threading nightmares
*/
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
DispatchQueue.main.async { [weak self] in
// test playerTryCount again to protect against changes that might have happened during the 0.5 second delay
if self!.playerTryCount > 0 {
if self!.playerTryCount <= self!.playerTryCountMaxLimit {
self!.retryCheckPlayerTryCountAgain()
} else {
// show spinner, show alert, or possibly use player?.replaceCurrentItem(with: playerItem) to start over ***BE SURE TO RESET playerTryCount = 0 ***
print("3.-----> PROBLEM")
}
}
}
}
}
}
func retryCheckPlayerTryCountAgain() {
checkPlayerTryCount()
}
in very bad network playbackLikelyToKeepUp most probably to be false.
use kvo to observe playbackBufferEmpty is better, more sensitive to if existing buffer data that can be used for playback .if the value change to true you can call play method to continue playback.
In my case,
I was trying to record video with imagePickerController and playback recorded video with AVPlayerController. But it starts playing video and gets stops after 1 second. Somehow it gets time to save video and if you playback it immediately, it won't play.
So solution is ,
call play video after 0.5 seconds (delay). like below
-(void)imagePickerController:(UIImagePickerController *)picker
didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self performSelector:#selector(playVideo) withObject:self
afterDelay:0.5];
}
-(void) playVideo {
self.avPlayerViewController = [[AVPlayerViewController alloc] init];
if(self.avPlayerViewController != nil)
{
AVPlayerItem* playerItem = [AVPlayerItem playerItemWithURL:Vpath];
AVPlayer* player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
self.avPlayerViewController.player = player;
self.avPlayerViewController.showsPlaybackControls = NO;
[self.avPlayerViewController setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[self.avPlayerViewController.view setFrame:[[UIScreen mainScreen] bounds]];
self.avPlayerViewController.view.clipsToBounds = YES;
self.avPlayerViewController.delegate = self;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(playerDidFinishPlaying) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
[self.viewVideoHolder addSubview:self.avPlayerViewController.view];
[self.avPlayerViewController.player play];
}
}
-(void) playerDidFinishPlaying
{
[avPlayer pause];
}
swift ios

Resources