AVQueuePlayer and audio session issue - iphone

I am going to try to give a detailed account of my issue.
I have an app that is in the store that uses in app sound. Currently I am using AVQueuePlayer because some of the sound will overlap and allow it to play in order. A lot of this sound is being played while I am playing embedded videos using AVPlayer which may not matter at all. The problem is that I am having reports of the sound stopping across the entire app. I am unable to reproduce this myself but we have a lot of active users and it is reported by some. Whenever it is reported and we determine its not just the silent switch or the sound volume down restarting the app always solves the problem. Occasionally we've heard of the sound magically returning with no changes. I have also had a couple of reports that it happens when using airplay and bluetooth but that may just be an complication of the problem or coincidence.
Below is the code that I am using and maybe I'm just using a setting wrong or not using a setting that I should be but this code works 99.9% of the time.
I use ducking for all sounds I play to lower the volume of the user's iPod music.
Here is my initialization in appDidFinishLaunchingWithOptions (Maybe its not needed at all in the start and sorry for the mixing of conventions):
AudioSessionInitialize (NULL, NULL, NULL, NULL);
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
UInt32 sessionCategory = kAudioSessionCategory_AmbientSound;
AudioSessionSetProperty (kAudioSessionProperty_AudioCategory, sizeof (sessionCategory), &sessionCategory);
[[AVAudioSession sharedInstance] setActive:YES withFlags:AVAudioSessionSetActiveFlags_NotifyOthersOnDeactivation error:nil];
When I play a sound:
-(void)playSound: (NSString *)soundString
{
OSStatus propertySetError = 0;
UInt32 allowMixing = true;
propertySetError |= AudioSessionSetProperty(kAudioSessionProperty_OtherMixableAudioShouldDuck, sizeof(allowMixing), &allowMixing);
[[AVAudioSession sharedInstance] setActive:YES withFlags:AVAudioSessionSetActiveFlags_NotifyOthersOnDeactivation error:nil];
NSURL *thisUrl = [NSURL fileURLWithPath:[NSString stringWithFormat:#"%#/%#.caf", [[NSBundle mainBundle] resourcePath], soundString]];
AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:thisUrl];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reachedEndOfItem:)
name:AVPlayerItemDidPlayToEndTimeNotification
object:item];
if(_audioPlayerQueue == nil)
{
_audioPlayerQueue = [[AVQueuePlayer alloc] initWithItems:[NSArray arrayWithObject:item]];
}
else
{
if([_audioPlayerQueue canInsertItem:item afterItem:nil])
{
[_audioPlayerQueue insertItem:item afterItem:nil];
}
}
if(_audioPlayerQueue == nil)
{
NSLog(#"error");
}
else
{
[_audioPlayerQueue play];
}
return;
}
When the sound finishes playing:
- (void)reachedEndOfItem: (AVPlayerItem*)item
{
[self performSelector:#selector(turnOffDucking) withObject:nil afterDelay:0.5f];
}
- (void)turnOffDucking
{
NSLog(#"reached end");
[[AVAudioSession sharedInstance] setActive:NO withFlags:AVAudioSessionSetActiveFlags_NotifyOthersOnDeactivation error:nil];
OSStatus propertySetError = 0;
UInt32 allowMixing = false;
propertySetError |= AudioSessionSetProperty(kAudioSessionProperty_OtherMixableAudioShouldDuck, sizeof(allowMixing), &allowMixing);
}
Any insight on what I am doing wrong, what settings I should be using for the audio session or known bugs/problems would be very helpful. I would be willing to look into using a different audio engine as this can have some slight performance issues when playing a video and having iPod music playing in tandem but I'd rather stick with this method of playing audio.
Thank you for any help you can provide.
-Ryan

I had a similar issue in past and found out that concurrent thread access was the reason.
Specifically, I think calling performSelectorAfterDelay could be the reason if another thread tries to modify the audio session at the same time the delay ends, as then we'll have two different threads trying to access the audio session.
So, I suggest to check your code again and make sure all calls to playSound are made from the main thread. Also, it may be better to use performSelectorOnMainThread instead of performSelectorAfterDelay as the docs say:
Invocations of blocks, key-value observers, or notification handlers are not guaranteed to be made on any particular thread or queue. Instead, AV Foundation invokes these handlers on threads or queues on which it performs its internal tasks.

Related

AVPlayer doesn't resume playback on endinterruption on iPhone but does so on iPad

I'm writing a radio app for both iPhone and iPad and am running into some weird behaviour when handling pausing and playing the audio with interruptions. I'm using the AVAudioSessionDelegate methods beginInterruption and endInterruption to respectively pause and play the AVPlayer. Below is the relevant play code.
Now, the following situations seem to happen consistently:
On iPad: If I force an interruption (Facetime call), beginInterruption is called as expected and playback stops. If the interruption stops, endInterruption is called as expected, and the playback resumes as expected.
On iPhone: Pressing the play and pause button, triggers pause and play exactly the same as beginInterruption and endInterruption would. Playback behaves as expected.
On iPhone: Forcing an interruption (by calling the phone), calls endInterruption as expected and playback pauses as expected. However, when the interruption is finished, beginInterruption is called as expected, play is called as expected, it actually reaches and executes the [self.player play] line, but playback does not resume! I hear nothing.
Situation 3 above is extremely weird, so I'm wondering if I may have overlooked something. Any ideas?
Play code
- (void)play {
NSError* error = nil;
AVKeyValueStatus keyStatus = [currentAsset statusOfValueForKey:#"tracks" error:&error];
if(error){
DLog(#"Error %#", [error localizedDescription]);
}
else {
DLog(#"Current Key Status: %i", keyStatus);
if(keyStatus == AVKeyValueStatusLoaded){
DLog(#"Continue playing source: %#", self.source);
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
if (error) {
DLog(#"Error during play while setting AVAudioSessionCategory: %#", [error localizedDescription]);
}
[[AVAudioSession sharedInstance] setActive:YES error:&error];
if (error) {
DLog(#"Error during play while setting AVAudioSessionCategory: %#", [error localizedDescription]);
}
[[AVAudioSession sharedInstance] setDelegate:self];
if(backgroundTaskID != UIBackgroundTaskInvalid){
[[UIApplication sharedApplication] endBackgroundTask:backgroundTaskID];
}
backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:NULL];
[self.player play];
[self setStatus:kNPOMediaPlayerStatusPlaying];
}
else {
DLog(#"PlaySource: %#", self.source);
[self playSource:self.source];
}
}}
I had the same issue. Issue disappeared after I implemented remote control event handling. I called [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];when starting playback.
It turns out that this is a known bug in iOS, which requires some careful work-arounds in the applicationDidBecomeActive to handle beginInterruption. Sadly, I couldn't figure out another solution.

How to Play a sound using AVAudioPlayer when in Silent Mode in iPhone

I want to play a sound even in silent mode in iPhone.
Can it be done by using AVAudioPlayer (Without using AVAudioSession)
(For ios 3.0+)
Thanks in advance.
Actually, you can do this. It is controlled via the Audio Session and has nothing to do with AVAudioPlayer itself. Why don't you want to use AudioSession? They play nice together...
In your app, you should initialize the Audio Session, and then you can also tell indicate what kind of audio you intend to play. If you're a music player, then it sort of makes sense that the user would want to hear the audio even with the ring/silent switch enabled.
AudioSessionInitialize (NULL, NULL, NULL, NULL);
AudioSessionSetActive(true);
// Allow playback even if Ring/Silent switch is on mute
UInt32 sessionCategory = kAudioSessionCategory_MediaPlayback;
AudioSessionSetProperty (kAudioSessionProperty_AudioCategory,
sizeof(sessionCategory),&sessionCategory);
I have an app that I do this very thing, and use AVAudioPlayer to play audio, and with the ring/silent switch enabled, I can hear the audio.
UPDATE (11/6/2013)
In the app I mentioned above, where I used the code above successfully, I have (for some time) been using the following code instead to achieve the same result:
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
NSError *error = nil;
BOOL result = NO;
if ([audioSession respondsToSelector:#selector(setActive:withOptions:error:)]) {
result = [audioSession setActive:YES withOptions:0 error:&error]; // iOS6+
} else {
[audioSession setActive:YES withFlags:0 error:&error]; // iOS5 and below
}
if (!result && error) {
// deal with the error
}
error = nil;
result = [audioSession setCategory:AVAudioSessionCategoryPlayback error:&error];
if (!result && error) {
// deal with the error
}
I thought I'd post this as an alternative, in light of the most recent comment to this answer. :-)
MarkGranoff's solution is correct. However, if you prefer to do it in Obj-c instead of C, the following works as well:
NSError *error = nil;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
[[AVAudioSession sharedInstance] setActive:YES error:&error];
The above answers are correct. Following is the Swift version.
do {
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
//print("AVAudioSession Category Playback OK")
do {
try AVAudioSession.sharedInstance().setActive(true)
//print("AVAudioSession is Active")
} catch _ as NSError {
//print(error.localizedDescription)
}
} catch _ as NSError {
//print(error.localizedDescription)
}
Swift 4 simple version:
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
This will simply do the trick (using AVAudioSession)
try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
try AVAudioSession.sharedInstance().setActive(true)

AVAssetWriter / AVAudioPlayer Conflict?

Weeks ago, I posted this thread regarding problems I was having with AVAssetWriter: AVAssetWriter Woes
Further research seems to lead to a conflict using AVAssetWriter while playing audio with AVAudioPlayer or, really, any audio system. I tried with OpenAL as well.
Here's the background:
Using AVAssetWriter to write frames to a video from an image or set of images works fine UNTIL [AVAudioPlayer play] is called.
This only happens on the device, not the sim.
The error occurs when attempting to create a pixel buffer from CVPixelBufferPoolCreatePixelBuffer.
Once the audio starts playing, the AVAssetWriterInputPixelBufferAdaptor.pixelBufferPool which existed before suddely becomes nil.
You can download the representative project here: http://www.mediafire.com/?5k7kqyvtbfdgdgv
Comment out AVAudioPlayer play and it will work on the device.
Any clues are appreciated.
I've found the solution to this issue.
If you want to have AVAudioPlayer and AVAssetWriter behave correctly together, you must have and audio session category that is 'mixable'.
You can use a category that is mixable like AVAudioSessionCategoryAmbient.
However, I needed to use AVAudioSessionCategoryPlayAndRecord.
You can set any category to be mixable by implementing this:
OSStatus propertySetError = 0;
UInt32 allowMixing = true;
propertySetError = AudioSessionSetProperty (
kAudioSessionProperty_OverrideCategoryMixWithOthers, // 1
sizeof (allowMixing), // 2
&allowMixing // 3
);
This above answer is in complete. It doesn't work. Do this instead
// Setup to be able to record global sounds (preexisting app sounds)
NSError *sessionError = nil;
if ([[AVAudioSession sharedInstance] respondsToSelector:#selector(setCategory:withOptions:error:)])
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionDuckOthers error:&sessionError];
else
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&sessionError];
// Set the audio session to be active
[[AVAudioSession sharedInstance] setActive:YES error:&sessionError];
//then call your asset writer
movieWriter = [[AVAssetWriter alloc] initWithMovieURL:movieURL size:CGSizeMake(480.0, 640.0)];

Playing sound while the app is inactive/in background with respect to mute/ring-switch

I want to play sound while my app is in the background and respect the mute-ring-switch.
I managed to play sound while my app is not in the foreground. I use basically the following code to play my sound:
sound = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];
sound.numberOfLoops = -1;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil];
/* ... */
[sound playAtTime:0];
AVAudioSessionCategoryPlayback causes my application to ignore the silent/ring-switch and play sound in background.
Since I want to have the last behavior without the first, I searched for a solution to query the state of the silent/rind-switch. I tried different approaches but this seemed to work for most of the users:
-(BOOL)silenced {
#if TARGET_IPHONE_SIMULATOR
// return NO in simulator. Code causes crashes for some reason.
return NO;
#endif
CFStringRef state;
UInt32 propertySize = sizeof(CFStringRef);
AudioSessionInitialize(NULL, NULL, NULL, NULL);
AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &state);
if(CFStringGetLength(state) > 0)
return NO;
else
return YES;
}
I found it here:
How to programmatically sense the iPhone mute switch?
This code isn't working for me. Despite the phone is muted "state" is not empty.
Does anyone have an idea, why this isn't working for me? I am developing and testing for iOS 4.3.
Thx.
Edit I used CFShow to show the content of the kAudioSessionProperty_AudioRoute and it is "Speaker" everytime, nontheless I changed the state of the mute/ring switch.

AVAssetReader and Audio Queue streaming problem

I have a problem with the AVAssetReader here to get samples from the iPod library and stream it via Audio Queue. I have not been able to find any such example so I try to implement my own but it seems that somehow the AssetReader is "screwed up" at the callback function of audio queue. Specifically it fails when it does the copyNextSampleBuffer ie it returns null when it is not finished yet. I have made sure the pointer exists and such so it will be great if anyone can help.
Below is the callback function code that I have used. This callback function 'works' when it is not called by the AudioQueue callback.
static void HandleOutputBuffer (
void *playerStateH,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer
) {
AQPlayerState *pplayerState = (AQPlayerState *) playerStateH;
//if (pplayerState->mIsRunning == 0) return;
UInt32 bytesToRead = pplayerState->bufferByteSize;
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIF_callsample object:nil];
float * inData =(float *) inBuffer->mAudioData;
int offsetSample = 0;
//Loop until finish reading from the music data
while (bytesToRead) {
/*THIS IS THE PROBLEMATIC LINE*/
CMSampleBufferRef sampBuffer = [pplayerState->assetWrapper getNextSampleBuffer]; //the assetreader getting nextsample with copyNextSampleBuffer
if (sampBuffer == nil) {
NSLog(#"No more data to read from");
// NSLog(#"aro status after null %d",[pplayerState->ar status]);
AudioQueueStop (
pplayerState->mQueue,
false
);
pplayerState->mIsRunning = NO;
return;
}
AudioBufferList audioBufferList;
CMBlockBufferRef blockBuffer;
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, 0, &blockBuffer);
AudioBuffer audioBuffer = audioBufferList.mBuffers[0];
memcpy(inData + (2*offsetSample),audioBuffer.mData,audioBuffer.mDataByteSize);
bytesToRead = bytesToRead - audioBuffer.mDataByteSize;
offsetSample = offsetSample + audioBuffer.mDataByteSize/8;
}
inBuffer->mAudioDataByteSize = offsetSample*8;
AudioQueueEnqueueBuffer (
pplayerState->mQueue,
inBuffer,
0,
0
);
}
I was getting this same mystifying error. Sure enough, "setting up" an audio session made the error go away. This is how I set up my audio session.
- (void)setupAudio {
[[AVAudioSession sharedInstance] setDelegate:self];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:nil];
NSError *activationError = nil;
[[AVAudioSession sharedInstance] setActive: YES error:&activationError];
NSLog(#"setupAudio ACTIVATION ERROR IS %#", activationError);
[[AVAudioSession sharedInstance] setPreferredIOBufferDuration:0.1 error:&activationError];
NSLog(#"setupAudio BUFFER DURATION ERROR IS %#", activationError);
}
From the Audio Session Programming Guide, under AVAudioSessionCategoryAmbient:
This category allows audio from the iPod, Safari, and other built-in applications to play while your application is playing audio.
Using an AVAssetReader probably uses iOS' hardware decoder, which blocks the use of the AudioQueue. Setting AVAudioSessionCategoryAmbient means the audio is rendered in software, allowing both to work at the same time - however, this would have an impact on performance/battery life. (See Audio Session Programming Guide under "How Categories Affect Encoding and Decoding").
Ok I have somehow solved this weird error... Apparently it is because of the audio session not properly set. Talk about lack of documentation on this one...