4
votes

I'm making an app that supports both video playback and recording. I always want to allow background audio mixing except for during video playback (during playback, background audio should be muted). Therefore, I use the two methods below while changing the state of playback. When my AVPlayer starts loading, I call MuteBackgroundAudio, and when I dismiss the view controller containing it, I call ResumeBackgroundAudio. This works as expected, and the audio returns successfully after leaving playback.

The issue is that after doing this at least once, whenever I record anything using AVCaptureSession, no sounds gets recorded. My session is configured like so:

AVCaptureDevice *audioDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject];
AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error];

if (error)
{
    NSLog(@"%@", error);
}

if ([self.session canAddInput:audioDeviceInput])
{
    [self.session addInput:audioDeviceInput];
}

[self.session setAutomaticallyConfiguresApplicationAudioSession:NO];

// ... videoDeviceInput

Note that I have not set usesApplicationAudioSession, so it defaults to YES.

void MuteBackgroundAudio(void)
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        if ([[AVAudioSession sharedInstance] isOtherAudioPlaying] && !isMuted)
        {
            isMuted = YES;

            NSError *error = nil;
            [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
                                             withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker
                                                   error:&error];
            if (error)
            {
                NSLog(@"DEBUG - Set category error %ld, %@", (long)error.code, error.localizedDescription);
            }

            NSError *error2 = nil;
            [[AVAudioSession sharedInstance] setActive:YES
                                           withOptions:0
                                                 error:&error2];
            if (error2)
            {
                NSLog(@"DEBUG - Set active error 2 %ld, %@", (long)error.code, error.localizedDescription);
            }
        }
    });
}

void ResumeBackgroundAudio(void)
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        if (isMuted)
        {
            AVAudioSession *audioSession = [AVAudioSession sharedInstance];

            NSError *deactivationError = nil;
            [audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&deactivationError];

            if (deactivationError)
            {
                NSLog(@"DEBUG - Failed at deactivating audio session, retrying...");
                ResumeBackgroundAudio();
                return;
            }

            isMuted = NO;
            NSLog(@"DEBUG - Audio session deactivated");

            NSError *categoryError = nil;
            [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord
                          withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers
                                error:&categoryError];

            if (categoryError)
            {
                NSLog(@"DEBUG - Failed at setting category");
                return;
            }

            NSLog(@"DEBUG - Audio session category set to mix with others");

            NSError *activationError = nil;
            [audioSession setActive:YES error:&activationError];

            if (activationError)
            {
                NSLog(@"DEBUG - Failed at activating audio session");
                return;
            }

            NSLog(@"DEBUG - Audio session activated");
        }
    });
}

Debugging

I have noticed that the audioSession always needs two tries to successfully deactivate after calling ResumeBackgroundAudio. It seems my AVPlayer does not get deallocated or stopped in time, due to this comment in AVAudioSession.h:

Note that this method will throw an exception in apps linked on or after iOS 8 if the session is set inactive while it has running or paused I/O (e.g. audio queues, players, recorders, converters, remote I/Os, etc.).

The fact that no sound gets recorded bring me to believe the audioSession does not actually get activated, but my logging says it does (always in the second iteration of the recursion).

I got the idea of using recursion to solve this problem from this post.

To clarify, the flow that causes the problem is the following:

  1. Open app with Spotify playing
  2. Begin playback of any content in the app
  3. Spotify gets muted, playback begins (MuteBackgroundAudio)
  4. Playback ends, Spotify starts playing again (ResumeBackgroundAudio)
  5. Start recording
  6. Stop recording, get mad that there is no audio
1
I am having this exact issue. Only my audio never works the first time regardless of the circumstance and I need to remove and re-add the audio input/output to get anything.Josh Bernfeld

1 Answers

5
votes

I've had the exact same issue as you're describing, down to the very last detail ([audioSession setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&deactivationError]; failed the first time no matter what)

The only acceptable way, for my case, was to stop then start the AVCaptureSession. I'm not entirely sure but I think this is the same exact way Whatsapp handles it - their camera behaves exactly like mine with the solution I'm suggesting.

Removing and adding the same audio input on the already running session also seemed to work but I got a terrible camera freeze - nothing compared to the session start / stop.

The recursion solution is nice, but I think it would be a great idea to 'throttle' that recursion call (add a short delay & a max retry count of some sort) in case for a legit reason the set fails every time. Otherwise your stack will overflow and your app will crash.

If you or anyone found a better solution to this I would love to know it.