123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- /*
- * Copyright @ 2017-present 8x8, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
- #import <AVFoundation/AVFoundation.h>
-
- #import <React/RCTEventEmitter.h>
- #import <React/RCTLog.h>
- #import <WebRTC/WebRTC.h>
-
- #import "LogUtils.h"
-
-
- // Audio mode
- typedef enum {
- kAudioModeDefault,
- kAudioModeAudioCall,
- kAudioModeVideoCall
- } JitsiMeetAudioMode;
-
- // Events
- static NSString * const kDevicesChanged = @"org.jitsi.meet:features/audio-mode#devices-update";
-
- // Device types (must match JS and Java)
- static NSString * const kDeviceTypeHeadphones = @"HEADPHONES";
- static NSString * const kDeviceTypeBluetooth = @"BLUETOOTH";
- static NSString * const kDeviceTypeEarpiece = @"EARPIECE";
- static NSString * const kDeviceTypeSpeaker = @"SPEAKER";
- static NSString * const kDeviceTypeUnknown = @"UNKNOWN";
-
-
- @interface AudioMode : RCTEventEmitter<RTCAudioSessionDelegate>
-
- @property(nonatomic, strong) dispatch_queue_t workerQueue;
-
- @end
-
- @implementation AudioMode {
- JitsiMeetAudioMode activeMode;
- RTCAudioSessionConfiguration *defaultConfig;
- RTCAudioSessionConfiguration *audioCallConfig;
- RTCAudioSessionConfiguration *videoCallConfig;
- RTCAudioSessionConfiguration *earpieceConfig;
- BOOL forceSpeaker;
- BOOL forceEarpiece;
- BOOL isSpeakerOn;
- BOOL isEarpieceOn;
- }
-
- RCT_EXPORT_MODULE();
-
- + (BOOL)requiresMainQueueSetup {
- return NO;
- }
-
- - (NSArray<NSString *> *)supportedEvents {
- return @[ kDevicesChanged ];
- }
-
- - (NSDictionary *)constantsToExport {
- return @{
- @"DEVICE_CHANGE_EVENT": kDevicesChanged,
- @"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall],
- @"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault],
- @"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall]
- };
- };
-
- - (instancetype)init {
- self = [super init];
- if (self) {
- dispatch_queue_attr_t attributes =
- dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
- _workerQueue = dispatch_queue_create("AudioMode.queue", attributes);
-
- activeMode = kAudioModeDefault;
-
- defaultConfig = [[RTCAudioSessionConfiguration alloc] init];
- defaultConfig.category = AVAudioSessionCategoryAmbient;
- defaultConfig.categoryOptions = 0;
- defaultConfig.mode = AVAudioSessionModeDefault;
-
- audioCallConfig = [[RTCAudioSessionConfiguration alloc] init];
- audioCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
- audioCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker;
- audioCallConfig.mode = AVAudioSessionModeVoiceChat;
-
- videoCallConfig = [[RTCAudioSessionConfiguration alloc] init];
- videoCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
- videoCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
- videoCallConfig.mode = AVAudioSessionModeVideoChat;
-
- // Manually routing audio to the earpiece doesn't quite work unless one disables BT (weird, I know).
- earpieceConfig = [[RTCAudioSessionConfiguration alloc] init];
- earpieceConfig.category = AVAudioSessionCategoryPlayAndRecord;
- earpieceConfig.categoryOptions = 0;
- earpieceConfig.mode = AVAudioSessionModeVoiceChat;
-
- forceSpeaker = NO;
- forceEarpiece = NO;
- isSpeakerOn = NO;
- isEarpieceOn = NO;
-
- RTCAudioSession *session = [RTCAudioSession sharedInstance];
- [session addDelegate:self];
- }
-
- return self;
- }
-
- - (dispatch_queue_t)methodQueue {
- // Use a dedicated queue for audio mode operations.
- return _workerQueue;
- }
-
- - (BOOL)setConfig:(RTCAudioSessionConfiguration *)config
- error:(NSError * _Nullable *)outError {
-
- RTCAudioSession *session = [RTCAudioSession sharedInstance];
- [session lockForConfiguration];
- BOOL success = [session setConfiguration:config error:outError];
- [session unlockForConfiguration];
-
- return success;
- }
-
- #pragma mark - Exported methods
-
- RCT_EXPORT_METHOD(setMode:(int)mode
- resolve:(RCTPromiseResolveBlock)resolve
- reject:(RCTPromiseRejectBlock)reject) {
- RTCAudioSessionConfiguration *config = [self configForMode:mode];
- NSError *error;
-
- if (config == nil) {
- reject(@"setMode", @"Invalid mode", nil);
- return;
- }
-
- // Reset.
- if (mode == kAudioModeDefault) {
- forceSpeaker = NO;
- forceEarpiece = NO;
- }
-
- activeMode = mode;
-
- if ([self setConfig:config error:&error]) {
- resolve(nil);
- } else {
- reject(@"setMode", error.localizedDescription, error);
- }
-
- [self notifyDevicesChanged];
- }
-
- RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
- resolve:(RCTPromiseResolveBlock)resolve
- reject:(RCTPromiseRejectBlock)reject) {
- DDLogInfo(@"[AudioMode] Selected device: %@", device);
-
- RTCAudioSession *session = [RTCAudioSession sharedInstance];
- [session lockForConfiguration];
- BOOL success;
- NSError *error = nil;
-
- // Reset these, as we are about to compute them.
- forceSpeaker = NO;
- forceEarpiece = NO;
-
- // The speaker is special, so test for it first.
- if ([device isEqualToString:kDeviceTypeSpeaker]) {
- forceSpeaker = NO;
- success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
- } else {
- // Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
- AVAudioSession *_session = [AVAudioSession sharedInstance];
- AVAudioSessionPortDescription *port = nil;
-
- // Find the matching input device.
- for (AVAudioSessionPortDescription *portDesc in _session.availableInputs) {
- if ([portDesc.UID isEqualToString:device]) {
- port = portDesc;
- break;
- }
- }
-
- if (port != nil) {
- // First remove the override if we are going to select a different device.
- if (isSpeakerOn) {
- [session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
- }
-
- // Special case for the earpiece.
- if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
- forceEarpiece = YES;
- [self setConfig:earpieceConfig error:nil];
- } else if (isEarpieceOn) {
- // Reset the config.
- RTCAudioSessionConfiguration *config = [self configForMode:activeMode];
- [self setConfig:config error:nil];
- }
-
- // Select our preferred input.
- success = [session setPreferredInput:port error:&error];
- } else {
- success = NO;
- error = RCTErrorWithMessage(@"Could not find audio device");
- }
- }
-
- [session unlockForConfiguration];
-
- if (success) {
- resolve(nil);
- } else {
- reject(@"setAudioDevice", error != nil ? error.localizedDescription : @"", error);
- }
- }
-
- RCT_EXPORT_METHOD(updateDeviceList) {
- [self notifyDevicesChanged];
- }
-
- #pragma mark - RTCAudioSessionDelegate
-
- - (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
- reason:(AVAudioSessionRouteChangeReason)reason
- previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
- // Update JS about the changes.
- [self notifyDevicesChanged];
-
- dispatch_async(_workerQueue, ^{
- switch (reason) {
- case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
- case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
- // If the device list changed, reset our overrides.
- self->forceSpeaker = NO;
- self->forceEarpiece = NO;
- break;
- case AVAudioSessionRouteChangeReasonCategoryChange:
- // The category has changed. Check if it's the one we want and adjust as
- // needed.
- break;
- default:
- return;
- }
-
- // We don't want to touch the category when in default mode.
- // This is to play well with other components which could be integrated
- // into the final application.
- if (self->activeMode != kAudioModeDefault) {
- DDLogInfo(@"[AudioMode] Route changed, reapplying RTCAudioSession config");
- RTCAudioSessionConfiguration *config = [self configForMode:self->activeMode];
- [self setConfig:config error:nil];
- if (self->forceSpeaker && !self->isSpeakerOn) {
- RTCAudioSession *session = [RTCAudioSession sharedInstance];
- [session lockForConfiguration];
- [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
- [session unlockForConfiguration];
- }
- }
- });
- }
-
- - (void)audioSession:(RTCAudioSession *)audioSession didSetActive:(BOOL)active {
- DDLogInfo(@"[AudioMode] Audio session didSetActive:%d", active);
- }
-
- #pragma mark - Helper methods
-
- - (RTCAudioSessionConfiguration *)configForMode:(int) mode {
- if (mode != kAudioModeDefault && forceEarpiece) {
- return earpieceConfig;
- }
-
- switch (mode) {
- case kAudioModeAudioCall:
- return audioCallConfig;
- case kAudioModeDefault:
- return defaultConfig;
- case kAudioModeVideoCall:
- return videoCallConfig;
- default:
- return nil;
- }
- }
-
- // Here we convert input and output port types into a single type.
- - (NSString *)portTypeToString:(AVAudioSessionPort) portType {
- if ([portType isEqualToString:AVAudioSessionPortHeadphones]
- || [portType isEqualToString:AVAudioSessionPortHeadsetMic]) {
- return kDeviceTypeHeadphones;
- } else if ([portType isEqualToString:AVAudioSessionPortBuiltInMic]
- || [portType isEqualToString:AVAudioSessionPortBuiltInReceiver]) {
- return kDeviceTypeEarpiece;
- } else if ([portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
- return kDeviceTypeSpeaker;
- } else if ([portType isEqualToString:AVAudioSessionPortBluetoothHFP]
- || [portType isEqualToString:AVAudioSessionPortBluetoothLE]
- || [portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) {
- return kDeviceTypeBluetooth;
- } else {
- return kDeviceTypeUnknown;
- }
- }
-
- - (void)notifyDevicesChanged {
- dispatch_async(_workerQueue, ^{
- NSMutableArray *data = [[NSMutableArray alloc] init];
- // Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
- AVAudioSession *session = [AVAudioSession sharedInstance];
- NSString *currentPort = @"";
- AVAudioSessionRouteDescription *currentRoute = session.currentRoute;
-
- // Check what the current device is. Because the speaker is somewhat special, we need to
- // check for it first.
- if (currentRoute != nil) {
- AVAudioSessionPortDescription *output = currentRoute.outputs.firstObject;
- AVAudioSessionPortDescription *input = currentRoute.inputs.firstObject;
- if (output != nil && [output.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
- currentPort = kDeviceTypeSpeaker;
- self->isSpeakerOn = YES;
- } else if (input != nil) {
- currentPort = input.UID;
- self->isSpeakerOn = NO;
- self->isEarpieceOn = [input.portType isEqualToString:AVAudioSessionPortBuiltInMic];
- }
- }
-
- BOOL headphonesAvailable = NO;
- for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
- if ([portDesc.portType isEqualToString:AVAudioSessionPortHeadsetMic] || [portDesc.portType isEqualToString:AVAudioSessionPortHeadphones]) {
- headphonesAvailable = YES;
- break;
- }
- }
-
- for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
- // Skip "Phone" if headphones are present.
- if (headphonesAvailable && [portDesc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
- continue;
- }
- id deviceData
- = @{
- @"type": [self portTypeToString:portDesc.portType],
- @"name": portDesc.portName,
- @"uid": portDesc.UID,
- @"selected": [NSNumber numberWithBool:[portDesc.UID isEqualToString:currentPort]]
- };
- [data addObject:deviceData];
- }
-
- // We need to manually add the speaker because it will never show up in the
- // previous list, as it's not an input.
- [data addObject:
- @{ @"type": kDeviceTypeSpeaker,
- @"name": @"Speaker",
- @"uid": kDeviceTypeSpeaker,
- @"selected": [NSNumber numberWithBool:[kDeviceTypeSpeaker isEqualToString:currentPort]]
- }];
-
- [self sendEventWithName:kDevicesChanged body:data];
- });
- }
-
- @end
|