I have an audio stream that will play, then KVO observes playbackLikelyToKeepUp twice, then stops playing instantly observing playbackBufferEmpty twice.
Firstly I don't see why observeValueForKeyPath should be called twice for each message. And secondly, the presence of playbackLikelyToKeepUp should indicate playbackBufferEmpty shouldn't happen.
I should note this is happening in the simulator and the device.
- (void)setRetryConnectionTimer:(NSTimer *)newTimer
{
if (_retryConnectionTimer) { // Take care of releasing old Timers here to make things easy.
NSLog(#"[Timer Setter] Timer Exists. Invalidating and releasing.");
[_retryConnectionTimer invalidate];
[_retryConnectionTimer release];
_retryConnectionTimer = nil;
}
_retryConnectionTimer = [newTimer retain];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context {
if (object == self.audioStream && [keyPath isEqualToString:#"status"])
{
if(self.audioStream.status == AVPlayerStatusReadyToPlay)
{
//AVPlayer ready. Kill retry timer.
if(self.retryConnectionTimer){
if([self.retryConnectionTimer isValid]){
NSLog(#"Timer is valid. invalidating and setting to nil.");
[self.retryConnectionTimer invalidate];
self.retryConnectionTimer = nil;
}
}
NSLog(#"Buffering..."); // Allow the buffer to fill some by delaying play message.
[self.audioPlayer performSelector:#selector(play) withObject:nil afterDelay:kBufferSize];
}
}
if ([keyPath isEqualToString:#"playbackBufferEmpty"]){
NSLog(#"BUFFER EMPTY!!!"); // Buffer empty.... why when playbackShouldKeep up was just sent milliseconds ago.
_retries = 0;
[self.audioPlayer pause];
self.retryConnectionTimer = [NSTimer scheduledTimerWithTimeInterval:20.0f
target:self
selector:#selector(tryReconnect:)
userInfo:nil
repeats:YES];
}
if ([keyPath isEqualToString:#"playbackLikelyToKeepUp"]){
NSLog(#"playbackLikelyToKeepUp");
}
}
- (void)tryReconnect:(NSTimer *)sender {
NSLog(#"tryReconnect Called. Retry: %i", _retries);
if (_retries <= kReconnectRetries) {
[self restartStream];
_retries ++;
} else {
NSLog(#"Connection Dropped: invalidating Timer.");
[self.retryConnectionTimer invalidate];
self.retryConnectionTimer = nil;
_retries = 0;
}
}
- (void)restartStream
{
[self.audioStream removeObserver:self forKeyPath:#"status"];
[self.audioStream removeObserver:self forKeyPath:#"playbackBufferEmpty"];
[self.audioStream removeObserver:self forKeyPath:#"playbackLikelyToKeepUp"];
self.audioStream = [AVPlayerItem playerItemWithURL:[NSURL URLWithString:#"http://myaudiostream.com:9094"]];
[self.audioStream addObserver:self forKeyPath:#"status" options:0 context:nil];
[self.audioStream addObserver:self forKeyPath:#"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
[self.audioStream addObserver:self forKeyPath:#"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
NSLog(#"Playing audioPlayer with new AVPlayerItem");
self.audioPlayer = [AVPlayer playerWithPlayerItem:self.audioStream];
}
From the logs.
2014-01-04 13:32:48.121 Audio Test [833:70b] Playing audioPlayer with new AVPlayerItem
2014-01-04 13:32:55.749 Audio Test [833:70b] Buffering...
2014-01-04 13:32:55.752 Audio Test [833:70b] playbackLikelyToKeepUp
2014-01-04 13:36:00.266 Audio Test [833:70b] playbackLikelyToKeepUp
2014-01-04 13:36:00.267 Audio Test [833:70b] BUFFER EMPTY!!!
2014-01-04 13:36:00.267 Audio Test [833:70b] Creating new timer and assigning to self.retryConnectionTimer
2014-01-04 13:36:03.701 Audio Test [833:70b] BUFFER EMPTY!!!
2014-01-04 13:36:03.702 Audio Test [833:70b] Creating new timer and assigning to self.retryConnectionTimer
2014-01-04 13:36:03.702 Audio Test [833:70b] [Timer Setter] Timer Exists. Invalidating and releasing.
2014-01-04 13:36:07.171 Audio Test [833:70b] playbackLikelyToKeepUp
2014-01-04 13:36:23.697 Audio Test [833:70b] tryReconnect Called. Retry: 0
2014-01-04 13:36:23.698 Audio Test [833:70b] Playing audioPlayer with new AVPlayerItem
2014-01-04 13:36:34.002 Audio Test [833:70b] Buffering...
2014-01-04 13:36:34.002 Audio Test [833:70b] Timer is valid. invalidating and setting to nil.
2014-01-04 13:36:34.002 Audio Test [833:70b] [Timer Setter] Timer Exists. Invalidating and releasing.
Are you actually checking the result? The playbackLikelyToKeepUp is a BOOL so it could very well be NO.
Related
I am facing memory leaks when we play a video and return back to the parent window. See the screenshot below of readings from allocations tool. Every time, when I pop the view controller (showing video) there are some objects related to AVFoundation holding the memory.
Interestingly the responsible library of all these objects is AVFoundation. None of the increase in memory is due to objects created in the APP. It is very highly unlikely that there is some problem with such a popular framework. I saw a few examples of AVPlayerViewController over the web but they seem to have the same problem.
Does anyone have any idea what/where is the problem? If anyone wants to replicate this then he can download any of the 2 projects given above. You have to make minor changes in the storyboard for creating root view controller with the navigation controller.
http://www.modejong.com/blog/post13_iOS8_SunSpot/index.html
https://github.com/coolioxlr/PageView-AVPlayer
This is how I am clearing the memory:
-(void) dealloc{
[self clearCurrentVideo];
}
-(void)clearCurrentVideo {
[_playerItem removeObserver:self forKeyPath:#"status"];
[_currentVideoPlayerViewController.player removeObserver:self forKeyPath:#"rate"];
[_currentVideoPlayerViewController.player pause];
_currentVideoPlayerViewController.delegate=nil;
_currentVideoPlayerViewController.player=nil;
_playerItem=nil;
_currentVideoPlayerViewController = nil;
}
This is how I load the asset for videos:
-(void)playtheAsset:(AVAsset *)asset{
[asset loadValuesAsynchronouslyForKeys:#[#"playable"] completionHandler:
^{
dispatch_async( dispatch_get_main_queue(),
^{
[self loadTheAsset:asset withKeys:#[#"playable"]];
});
}];
}
- (void)loadTheAsset:(AVAsset *)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];
if([thisKey isEqualToString:#"playable"]){
[self showNetworkErrorLabel];
}
return;
} else if ((keyStatus == AVKeyValueStatusLoaded) || ( keyStatus == AVKeyValueStatusLoading )){
[self removeNetworkLabel ];
}
}
/* 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];
NSLog(#"%#",assetCannotBePlayedError);
[self showNetworkErrorLabel];
/* 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 (_playerItem)
{
/* Remove existing player item key value observers and notifications. */
[_playerItem removeObserver:self forKeyPath:#"status"];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:AVPlayerItemDidPlayToEndTimeNotification
object:_playerItem];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:AVPlayerItemPlaybackStalledNotification
object:_playerItem];
}
/* Create a new instance of AVPlayerItem from the now successfully loaded AVAsset. */
_playerItem = [AVPlayerItem playerItemWithAsset:asset];
/* Observe the player item "status" key to determine when it is ready to play. */
[_playerItem addObserver:self
forKeyPath:#"status"
options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context:AVPlayerDemoPlaybackViewControllerStatusObservationContext];
/* 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:_playerItem];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(playerItemFailedToPlayToEndTime:) name:AVPlayerItemPlaybackStalledNotification object:_playerItem];
// Remove the movie player view controller from the "playback did finish" notification observers
// Observe ourselves so we can get it to use the crossfade transition
[[NSNotificationCenter defaultCenter] removeObserver:_currentVideoPlayerViewController
name:kPlayerViewDismissedNotification
object:_currentVideoPlayerViewController.player];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(videoFinishedCallback:)
name:kPlayerViewDismissedNotification
object:_currentVideoPlayerViewController.player];
/* Create new player, if we don't already have one. */
if (!_currentVideoPlayerViewController.player)
{
/* Get a new AVPlayer initialized to play the specified player item. */
_currentVideoPlayerViewController.player=[AVPlayer playerWithPlayerItem:self->_playerItem];
[_currentVideoPlayerViewController.player addObserver:self
forKeyPath:#"rate"
options:NSKeyValueObservingOptionNew
context:AVPlayerDemoPlaybackViewControllerRateObservationContext];
}
}
I couldn't figure out the reason behind this. I tried using AVPlayer instead and created my own UI using (reference Apple AVPlayer Demo app.) and I couldn't find any leak there. It just worked.
If someone gets stuck with similar problem just give a try to reference code from AVPlayer Demo app.
And if someone knows the answer for the issue I on this thread. Please let me know.
I'm working on an app that plays audio in the background while using other apps using an AVAudioPlayer. When the camera is opened the music is silenced but none of the methods in the AppDelegate for app lifecycle get called so I can't save the playlist position or the playback time for the song.
In addition when the camera is dismissed I would like to have the app resume playing background music but again I haven't found any callback method to allow my to observe this change.
Do you know how to observe that the camera did become active and that the camera was dismissed while the app is running in background mode?
Here's how i solved it.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(handleAudioSessionInterruption:)
name:AVAudioSessionInterruptionNotification
object:[AVAudioSession sharedInstance]];
Handle the interuption.
-(void)handleAudioSessionInterruption:(NSNotification*)notification
{
//NSLog(#"%#",notification);
NSNumber *interruptionType = [[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey]; //1 Interuption Start, 0 Interuption Ends
NSNumber *interruptionOption = [[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey];
if ([interruptionType integerValue] == AVAudioSessionInterruptionTypeBegan)
{
NSLog(#"Player %d interupted",playerNumber);
// • Audio has stopped, already inactive
// • Change state of UI, etc., to reflect non-playing state
[self.playPauseButton setTitle:#">" forState:UIControlStateNormal];
self.playingAudio = NO;
return;
}
if ([interruptionType integerValue] == AVAudioSessionInterruptionTypeEnded)
{
if ([interruptionOption integerValue] == AVAudioSessionInterruptionOptionShouldResume)
{
NSLog(#"Player %d Resume",playerNumber);
[self.playPauseButton setTitle:#"||" forState:UIControlStateNormal];
self.playingAudio = YES;
NSError *error = nil;
AVAudioSession *aSession = [AVAudioSession sharedInstance];
[aSession setMode:AVAudioSessionModeDefault error:&error]; //& means value at address
[aSession setCategory:AVAudioSessionCategoryPlayback error:&error];
//[aSession setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
//[aSession setMode:AVAudioSessionModeSpokenAudio error:&error];
[aSession setActive: YES error: &error];
[self.audioPlayer play];
}
// • Make session active
// • Update user interface
// • AVAudioSessionInterruptionOptionShouldResume option
}
I've searched all over but cannot get this to work consistently. I want to play audio when a remote push notification arrives while the app is in the background or lock screen and the ringer is off.
Steps I've followed:
1) Set Required Background Modes to "App plays audio" into info.plist.
2) In application:didFinishLaunchingWithOptions:
a) set Audio Session category to Playback
b) make Audio Session active.
c) make app receive remote control events and become first responder
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// https://developer.apple.com/library/ios/qa/qa1668/_index.html
// For playback to continue when the screen locks, or when the Ring/Silent switch is set to silent, use the AVAudioSessionCategoryPlayback
BOOL result = [audioSession setCategory:AVAudioSessionCategoryPlayback error:&sessionError]; // TODO AVAudioSessionCategorySoloAmbient
if (!result && sessionError) {
NSLog(#"AppDelegate error setting session category. error %#", sessionError);
}
else {
NSLog(#"AppDelegate setting session category is successful");
}
result = [audioSession setActive:YES error:&sessionError];
if (!result && sessionError) {
NSLog(#"AppDelegate error activating audio session. error %#", sessionError);
}
else {
NSLog(#"AppDelegate setting session active is successful");
}
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
}
3) In applicationDidEnterBackground:
a) begin background task
b) receive remote control events
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSLog(#"AppDelegate applicationDidEnterBackground: called");
NSLog(#"AppDelegate applicationDidEnterBackground: is calling beginBackgroundTaskWithExpirationHandler:");
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
NSLog(#"AppDelegate applicationDidEnterBackground: is calling beginReceivingRemoteControlEvents");
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}
4) When the remote notification comes in, play an audio file
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
_currentItem = [AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:#"Audio" withExtension:#"wav"]];
_audioPlayer = [AVPlayer playerWithPlayerItem:_currentItem];
[_currentItem addObserver:self forKeyPath:#"status" options:NSKeyValueObservingOptionNew context:nil];
[_audioPlayer addObserver:self forKeyPath:#"status" options:NSKeyValueObservingOptionNew context:nil];
NSLog(#"playAudio: calling [_audioPlayer play] on audioPlayer %#", _audioPlayer);
[_audioPlayer play];
}
It works sometimes, but not always. Any ideas how to make this work consistently?
I think that you can't start playing background audio while already in the background, even starting in applicationDidEnterBackground is too late. A work around you could do is play silent audio or pause audio maybe. You could use the queue player to achieve this by looping the silent audio and seeking to the beginning when it ends, then enqueue the audio you'd like to play after it.
I want to make app which continuously records voice in background. App like heard does exactly that.
But I am stuck on interruptions. The app fail to resume recording when it faces interruptions.
I have used this for notification :
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(handleAudioSessionInterruption:)
name:AVAudioSessionInterruptionNotification
object:nil];
and to resume recording I have:
- (void)handleAudioSessionInterruption:(NSNotification*)notification {
NSNumber *interruptionType = [[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey];
NSNumber *interruptionOption = [[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey];
switch (interruptionType.unsignedIntegerValue) {
case AVAudioSessionInterruptionTypeBegan:{
// • Audio has stopped, already inactive
// • Change state of UI, etc., to reflect non-playing state
NSLog(#"Audio has stopped, already inactive");
NSLog(#"is recording: %#",recorder.isRecording?#"yes":#"no");
[self.recorder pause];
NSLog(#"is recording after pause: %#",recorder.isRecording?#"yes":#"no");
//[recorder pause];
} break;
case AVAudioSessionInterruptionTypeEnded:{
// • Make session active
// • Update user interface
// • AVAudioSessionInterruptionOptionShouldResume option
if (interruptionOption.unsignedIntegerValue == AVAudioSessionInterruptionOptionShouldResume) {
// Here you should continue playback.
//NSLog(#"completed");
NSLog(#"start recording again %d");
//NSLog(#"completed: %d",[recorder record]);
// Set the audio file
[self.recorder record];
NSLog(#"is recording after again : %#",recorder.isRecording?#"yes":#"no");
//[player play];
}
} break;
default:
break;
}
}
Add in the plist the UIBackgroundModes an 'audio' as Item 0. Hope it will help - at least it works for me when playing audio.
I'm trying to catch a moment when AVPlayer is unable to continue playback in case no more media available (too slow network, signal loss, etc). As described in documentation and different examples I'm using KVO to detect this:
item = [[AVPlayerItem alloc] initWithURL:audioURL];
player = [AVPlayer playerWithPlayerItem:item];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onItemNotification:) name:AVPlayerItemPlaybackStalledNotification object:item];
[item addObserver:self forKeyPath:#"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
[item addObserver:self forKeyPath:#"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
...
- (void) onItemNotification:(NSNotification*)not
{
NSLog(#"Item notification: %#", not.name);
}
...
- (void) observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
NSLog(#"Observe keyPath %#", keyPath);
}
I'm starting a playback and turn WiFi off after that. Unfortunately neither 'playbackBufferEmpty' nor 'AVPlayerItemPlaybackStalledNotification' comes. At the moment when playback stops I receive only one AVPlayerItemTimeJumpedNotification and that's all.
However there were at least 2 times when I got these notifications. But I can't figure out how to get them every time when playback is stalled.
Am I doing something wrong?
First try to disconnect internet from your router and you will get playbackBufferEmpty notification.
To handle network switching you will need to implemented Reachability
There are 2 cases where player can get stuck: it never starts or it runs out of buffered data. I use the following to handle both cases:
When you create a AVPlayer instance:
[_player addObserver:self forKeyPath:#"rate" options:0 context:nil];
[_player.currentItem addObserver:self forKeyPath:#"status" options:0 context:nil];
This is the handler:
-(void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if ([keyPath isEqualToString:#"status"]) {
if (_player.status == AVPlayerStatusFailed) {
//Failed to start.
//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 && //playback rate is 0
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, >, kCMTimeZero) && //video has started
CMTIME_COMPARE_INLINE(_player.currentItem.currentTime, <, _player.currentItem.duration) && //video hasn't reached the end
_isPlaying) { //instance variable to track playback state
//Video stalled. Possible connection loss or connection is too slow.
}
}
}
Don't forget to remove observers when you are done:
[_player.currentItem removeObserver:self forKeyPath:#"status"];
[_player removeObserver:self forKeyPath:#"rate"];
See my answer here to see how I handle stalled video: AVPlayer stops playing and doesn't resume again