Compare commits

...

1 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
500b326780 ios: fix not activating Audio Unit in some corner cases
When a user was in a conference, and then goes bacck to being alone the Audio
Unit may not work properly if this state lasted for too long (around the 10
minute mark).

The solution this commit implements is based on the use of manual audio, that
is, WebRTC will no longer activate / deactivate the audio unit by itself, we
have to. Thus the Audio Unit will not be enabled until there are at least 2
participants in the meeting.

This is not enough, however, since CallKit will put the AVAudioSession in a
weird state (setActive will jusst fail) after about 10 minutes, so we have to
add another workaround: "hold" the call in CallKit while we are alone in a
meeting.
2020-05-12 18:35:31 +02:00
7 changed files with 150 additions and 3 deletions

View File

@@ -115,6 +115,9 @@ RCT_EXPORT_MODULE();
RTCAudioSession *session = [RTCAudioSession sharedInstance];
[session addDelegate:self];
// Don't let WebRTC manage the audio unit, we'll do it by hand.
session.useManualAudio = YES;
}
return self;
@@ -155,6 +158,8 @@ RCT_EXPORT_METHOD(setMode:(int)mode
forceEarpiece = NO;
}
DDLogInfo(@"[AudioMode] setMode: %d", mode);
activeMode = mode;
if ([self setConfig:config error:&error]) {
@@ -230,6 +235,47 @@ RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
}
}
RCT_EXPORT_METHOD(setAudioEnabled:(BOOL)enabled) {
DDLogInfo(@"[AudioMode] setAudioEnabled");
RTCAudioSession *session = [RTCAudioSession sharedInstance];
if (enabled) {
// CallKit should have activated the session, but it's possible it got deeactivated when
// a user who was in a conferene is left alone for a long period of time (~10 minutes).
if (!session.isActive) {
DDLogInfo(@"[AudioMode][setAudioEnabled] Session is not active, activating...");
// Pretend an interruption ended so RTCAudioSession updates its own internal state.
// It will re-activate by itself. We need to do this because otherwisse there is no
// way to get rid of an internal `isInterrupted` flag.
NSDictionary *userInfo
= @{
AVAudioSessionInterruptionTypeKey: [NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded],
AVAudioSessionInterruptionOptionKey: [NSNumber numberWithInt:AVAudioSessionInterruptionOptionShouldResume]
};
[[NSNotificationCenter defaultCenter] postNotificationName:AVAudioSessionInterruptionNotification object:nil userInfo:userInfo];
RTCAudioSessionConfiguration *config = [self configForMode:activeMode];
NSError *error = nil;
[self setConfig:config error:&error];
if (error != nil) {
DDLogError(@"[AudioMode][setAudioEnabled] setConfig: %@", error);
}
}
// Make sure the Audio Unit is started. This is idempotent, so we don't need to worry about always setting it.
DDLogInfo(@"[AudioMode][setAudioEnabled] Enabling Audio Unit, current sttate: %d", session.isAudioEnabled);
session.isAudioEnabled = YES;
} else {
DDLogInfo(@"[AudioMode][setAudioEnabled] Disabling Audio Unit, current sttate: %d", session.isAudioEnabled);
session.isAudioEnabled = NO;
// Ignore the AVAudioSession activation state. CallKit or the system will disable it.
}
}
RCT_EXPORT_METHOD(updateDeviceList) {
[self notifyDevicesChanged];
}

View File

@@ -38,6 +38,8 @@ static NSString * const RNCallKitPerformAnswerCallAction
= @"performAnswerCallAction";
static NSString * const RNCallKitPerformEndCallAction
= @"performEndCallAction";
static NSString * const RNCallKitPerformSetHeldCallAction
= @"performSetHeldCallAction";
static NSString * const RNCallKitPerformSetMutedCallAction
= @"performSetMutedCallAction";
static NSString * const RNCallKitProviderDidReset
@@ -54,6 +56,7 @@ RCT_EXPORT_MODULE();
return @[
RNCallKitPerformAnswerCallAction,
RNCallKitPerformEndCallAction,
RNCallKitPerformSetHeldCallAction,
RNCallKitPerformSetMutedCallAction,
RNCallKitProviderDidReset
];
@@ -120,6 +123,25 @@ RCT_EXPORT_METHOD(setProviderConfiguration:(NSDictionary *)dictionary) {
[JMCallKitProxy addListener:self];
}
RCT_EXPORT_METHOD(setCallOnHold:(NSString *)callUUID
onHold:(BOOL)onHold
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
DDLogInfo(@"[RNCallKit][startCall] setCallOnHold = %@", callUUID);
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
if (!callUUID_) {
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
return;
}
CXSetHeldCallAction *action
= [[CXSetHeldCallAction alloc] initWithCallUUID:callUUID_ onHold:onHold];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
}
// Start outgoing call
RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
handle:(NSString *)handle
@@ -295,6 +317,18 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
body:@{ @"callUUID": UUID.UUIDString }];
}
// Handle holdd from CallKit view
- (void) performSetHeldCallWithUUID:(NSUUID *)UUID
isOnHold:(BOOL)isOnHold {
DDLogInfo(@"[RNCallKit][CXProviderDelegate][provider:performSetHeldCallAction:]");
[self sendEventWithName:RNCallKitPerformSetHeldCallAction
body:@{
@"callUUID": UUID.UUIDString,
@"onHold": @(isOnHold)
}];
}
// Handle audio mute from CallKit view
- (void) performSetMutedCallWithUUID:(NSUUID *)UUID
isMuted:(BOOL)isMuted {

View File

@@ -71,6 +71,15 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
listeners.forEach {
let listener = $0 as! JMCallKitListener
listener.performSetHeldCall?(UUID: action.callUUID, isOnHold: action.isOnHold)
}
action.fulfill()
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
let uuid = pendingMuteActions.remove(action.uuid)

View File

@@ -26,6 +26,8 @@ import Foundation
@objc optional func performEndCall(UUID: UUID)
@objc optional func performSetHeldCall(UUID: UUID, isOnHold: Bool)
@objc optional func performSetMutedCall(UUID: UUID, isMuted: Bool)
@objc optional func performStartCall(UUID: UUID, isVideo: Bool)

View File

@@ -10,12 +10,13 @@ import {
CONFERENCE_JOINED,
getCurrentConference
} from '../../base/conference';
import { MiddlewareRegistry } from '../../base/redux';
import { getParticipantCount } from '../../base/participants';
import { MiddlewareRegistry, StateListenerRegistry } from '../../base/redux';
import { _SET_AUDIOMODE_DEVICES, _SET_AUDIOMODE_SUBSCRIPTIONS } from './actionTypes';
import logger from './logger';
const { AudioMode } = NativeModules;
const { AudioMode, RNCallKit } = NativeModules;
const AudioModeEmitter = new NativeEventEmitter(AudioMode);
/**
@@ -42,7 +43,7 @@ MiddlewareRegistry.register(store => next => action => {
}
case APP_WILL_MOUNT:
_appWillMount(store);
case CONFERENCE_FAILED: // eslint-disable-line no-fallthrough
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
/*
@@ -65,6 +66,16 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
StateListenerRegistry.register(
/* selector */ state => {
const conference = getCurrentConference(state);
const participantCount = getParticipantCount(state);
return conference && participantCount > 1;
},
/* listener */ (enableAudio, store) => _maybeEnableAudio(enableAudio, store)
);
/**
* Notifies this feature that the action {@link APP_WILL_MOUNT} is being
* dispatched within a specific redux {@code store}.
@@ -85,6 +96,28 @@ function _appWillMount(store) {
});
}
/**
* Applies only to iOS. Enables / disables the Audio Unit. When the audio unit is
* enabled audio will flow on both directions.
*
* @param {boolean} enabled - Whether audio should be enabled or not.
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _maybeEnableAudio(enabled, { getState }) {
// XXX: Ideally we'd want to call AudioMode.setAudioEnabled here, but that too won't work
// without telling CallKit to hold the call. Yeah, I know we don't support it, but hey
// thiss was the only workaround that ddid the job.
if (RNCallKit) {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
RNCallKit.setCallOnHold(conference.callUUID, !enabled);
}
}
}
/**
* Handles audio device changes. The list will be stored on the redux store.
*

View File

@@ -46,6 +46,10 @@ if (CallKit) {
'performEndCallAction',
delegate._onPerformEndCallAction,
context),
CallKit.addListener(
'performSetHeldCallAction',
delegate._onPerformSetHeldCallAction,
context),
CallKit.addListener(
'performSetMutedCallAction',
delegate._onPerformSetMutedCallAction,

View File

@@ -113,6 +113,7 @@ function _appWillMount({ dispatch, getState }, next, action) {
};
const delegate = {
_onPerformSetHeldCallAction,
_onPerformSetMutedCallAction,
_onPerformEndCallAction
};
@@ -363,6 +364,24 @@ function _onPerformEndCallAction({ callUUID }) {
}
}
/**
* Handles CallKit's event {@code performSetHeldCallAction}.
*
* @param {Object} event - The details of the CallKit event
* {@code performSetHeldCallAction}.
* @returns {void}
*/
function _onPerformSetHeldCallAction({ callUUID, onHold }) {
const { getState } = this; // eslint-disable-line no-invalid-this
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === callUUID) {
if (AudioMode.setAudioEnabled) {
AudioMode.setAudioEnabled(!onHold);
}
}
}
/**
* Handles CallKit's event {@code performSetMutedCallAction}.
*