Alon Gubkin
Alon Gubkin

Reputation: 57119

How to redirect audio to speakers in the AppRTC iOS example?

I'm trying to redirect audio to speakers in the AppRTC iOS example.

I tried:

AVAudioSession* session = [AVAudioSession sharedInstance];

//error handling
BOOL success;
NSError* error;

//set the audioSession category. 
//Needs to be Record or PlayAndRecord to use audioRouteOverride:  

success = [session setCategory:AVAudioSessionCategoryPlayAndRecord
                         error:&error];

if (!success)  NSLog(@"AVAudioSession error setting category:%@",error);

//set the audioSession override
success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker
                                      error:&error];
if (!success)  NSLog(@"AVAudioSession error overrideOutputAudioPort:%@",error);

//activate the audio session
success = [session setActive:YES error:&error];
if (!success) NSLog(@"AVAudioSession error activating: %@",error);
else NSLog(@"audioSession active");

There are no errors, but it doesn't work. How can I fix this?

Upvotes: 15

Views: 8431

Answers (6)

Sreekuttan
Sreekuttan

Reputation: 1954

I have been facing this issue for a while and I have found a solution for this. The problem was caused when we set AVAudioSessionCategory before the WebRTC completed its configurations. We can fix this by updating the audio configurations on AudioSessionRouteChange happens. There are two approaches to finding the AudioSessionRouteChange and update the configurations.

1. Using RTCAudioSessionDelegate

let rtcAudioSession = RTCAudioSession.sharedInstance()

// set RTCAudioSessionDelegate to your class
rtcAudioSession.add(self)

// MARK: - RTCAudioSessionDelegate
extension YourClass: RTCAudioSessionDelegate {
    
    func audioSessionDidChangeRoute(_ session: RTCAudioSession,
                                    reason: AVAudioSession.RouteChangeReason,
                                    previousRoute: AVAudioSessionRouteDescription) {
        // set audio configurations
        rtcAudioSession.lockForConfiguration()
        do {
            try rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetooth])
            try rtcAudioSession.setMode(AVAudioSession.Mode.videoChat.rawValue)
            try rtcAudioSession.setActive(true)
            
        } catch let error {
            debugPrint("Error changeing AVAudioSession category: \(error)")
        }
        rtcAudioSession.unlockForConfiguration()
    }
    
}

1. AVAudioSession.routeChangeNotification

let rtcAudioSession = RTCAudioSession.sharedInstance()

NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification,
                                       object: nil,
                                       queue: nil,
                                       using: onRouteChange)

private func onRouteChange(_ notification: Notification) {
    // set audio configurations
    rtcAudioSession.lockForConfiguration()
    do {
        try rtcAudioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue, with: [.defaultToSpeaker, .allowBluetoothA2DP, .allowBluetooth])
        try rtcAudioSession.setMode(AVAudioSession.Mode.videoChat.rawValue)
        try rtcAudioSession.setActive(true)
        
    } catch let error {
        debugPrint("Error changeing AVAudioSession category: \(error)")
    }
    rtcAudioSession.unlockForConfiguration()
}

Upvotes: 1

Martin
Martin

Reputation: 1191

For anyone who came here, searching for a solution in Swift that also accounts for changes back from (BT-)earphones. The below sample (Swift 5) does that.
Adopted in part from @Teivaz

@objc func handleRouteChange(notification: Notification) {
    guard let info = notification.userInfo,
        let value = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let reason = AVAudioSession.RouteChangeReason(rawValue: value) else { return }

    switch reason {
    case .categoryChange:
        try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
    case .oldDeviceUnavailable:
        try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
    default:
        l.debug("other")
    }
}

Upvotes: 5

Joe Maher
Joe Maher

Reputation: 5460

Still seems to be an issue, phuongle answer worked for me. Swift 4 version:

NotificationCenter.default.addObserver(forName: .AVAudioSessionRouteChange, object: nil, queue: nil, using: routeChange)    

private func routeChange(_ n: Notification) {
    guard let info = n.userInfo,
        let value = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
        let reason = AVAudioSessionRouteChangeReason(rawValue: value) else { return }
    switch reason {
    case .categoryChange: try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
    default: break
    }
}

Upvotes: 6

Teivaz
Teivaz

Reputation: 5665

phuongle's answer is correct. Though when you enable override this will actually override audio output even when user plugs in headphones. There's not much sense in playing audio through loudspeaker when user is using headphones. For this purpose use following code:

- (void)didSessionRouteChange:(NSNotification *)notification
{
    NSDictionary *interuptionDict = notification.userInfo;
    const NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];

    if (routeChangeReason == AVAudioSessionRouteChangeReasonRouteConfigurationChange) {
        [self enableLoudspeaker];
    }
}

- (void)enableLoudspeaker {
    AVAudioSession *audioSession = [AVAudioSession sharedInstance];
    AVAudioSessionCategoryOptions options = audioSession.categoryOptions;
    if (options & AVAudioSessionCategoryOptionDefaultToSpeaker) return;
    options |= AVAudioSessionCategoryOptionDefaultToSpeaker;
    [audioSession setActive:YES error:nil];
    [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:options error:nil];
}

Upvotes: 1

phuongle
phuongle

Reputation: 1186

I solved it by the solution. Just listen AVAudioSessionRouteChangeNotification

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didSessionRouteChange:) name:AVAudioSessionRouteChangeNotification object:nil];

And using the didSessionRouteChange selector as below:

- (void)didSessionRouteChange:(NSNotification *)notification
{
  NSDictionary *interuptionDict = notification.userInfo;
  NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];

  switch (routeChangeReason) {
      case AVAudioSessionRouteChangeReasonCategoryChange: {
          // Set speaker as default route
          NSError* error;
          [[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
      }
      break;

    default:
      break;
  }
}

Upvotes: 15

Josip B.
Josip B.

Reputation: 2464

I found solution in the end.

Reason was that you need to set AVAudioSession category to AVAudioSessionCategoryPlayback. But for some reason after establishing webRTC call it was set back to AVAudioSessionCategoryPlayAndRecord. In the end I decided to add observer for AVAudioSessionRouteChangeNotification and switch to AVAudioSessionCategoryPlayback each time I detected unwanted category change. A bit of hack solution but worked in the end. You can check it here.

Upvotes: 2

Related Questions