"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-7689/ios/sdk/src/AudioMode.m" (5 Dec 2023, 14392 Bytes) of package /linux/misc/jitsi-meet-7689.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Matlab source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file.

    1 /*
    2  * Copyright @ 2017-present 8x8, Inc.
    3  *
    4  * Licensed under the Apache License, Version 2.0 (the "License");
    5  * you may not use this file except in compliance with the License.
    6  * You may obtain a copy of the License at
    7  *
    8  *     http://www.apache.org/licenses/LICENSE-2.0
    9  *
   10  * Unless required by applicable law or agreed to in writing, software
   11  * distributed under the License is distributed on an "AS IS" BASIS,
   12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   13  * See the License for the specific language governing permissions and
   14  * limitations under the License.
   15  */
   16 
   17 #import <AVFoundation/AVFoundation.h>
   18 
   19 #import <React/RCTEventEmitter.h>
   20 #import <React/RCTLog.h>
   21 #import <WebRTC/WebRTC.h>
   22 
   23 #import "JitsiAudioSession+Private.h"
   24 
   25 
   26 // Audio mode
   27 typedef enum {
   28     kAudioModeDefault,
   29     kAudioModeAudioCall,
   30     kAudioModeVideoCall
   31 } JitsiMeetAudioMode;
   32 
   33 // Events
   34 static NSString * const kDevicesChanged = @"org.jitsi.meet:features/audio-mode#devices-update";
   35 
   36 // Device types (must match JS and Java)
   37 static NSString * const kDeviceTypeBluetooth  = @"BLUETOOTH";
   38 static NSString * const kDeviceTypeCar        = @"CAR";
   39 static NSString * const kDeviceTypeEarpiece   = @"EARPIECE";
   40 static NSString * const kDeviceTypeHeadphones = @"HEADPHONES";
   41 static NSString * const kDeviceTypeSpeaker    = @"SPEAKER";
   42 static NSString * const kDeviceTypeUnknown    = @"UNKNOWN";
   43 
   44 
   45 @interface AudioMode : RCTEventEmitter<RTCAudioSessionDelegate>
   46 
   47 @property(nonatomic, strong) dispatch_queue_t workerQueue;
   48 
   49 @end
   50 
   51 @implementation AudioMode {
   52     JitsiMeetAudioMode activeMode;
   53     RTCAudioSessionConfiguration *defaultConfig;
   54     RTCAudioSessionConfiguration *audioCallConfig;
   55     RTCAudioSessionConfiguration *videoCallConfig;
   56     RTCAudioSessionConfiguration *earpieceConfig;
   57     BOOL forceSpeaker;
   58     BOOL forceEarpiece;
   59     BOOL isSpeakerOn;
   60     BOOL isEarpieceOn;
   61 }
   62 
   63 RCT_EXPORT_MODULE();
   64 
   65 + (BOOL)requiresMainQueueSetup {
   66     return NO;
   67 }
   68 
   69 - (NSArray<NSString *> *)supportedEvents {
   70     return @[ kDevicesChanged ];
   71 }
   72 
   73 - (NSDictionary *)constantsToExport {
   74     return @{
   75         @"DEVICE_CHANGE_EVENT": kDevicesChanged,
   76         @"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall],
   77         @"DEFAULT"    : [NSNumber numberWithInt: kAudioModeDefault],
   78         @"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall]
   79     };
   80 };
   81 
   82 - (instancetype)init {
   83     self = [super init];
   84     if (self) {
   85         dispatch_queue_attr_t attributes =
   86         dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
   87         _workerQueue = dispatch_queue_create("AudioMode.queue", attributes);
   88 
   89         activeMode = kAudioModeDefault;
   90 
   91         defaultConfig = [[RTCAudioSessionConfiguration alloc] init];
   92         defaultConfig.category = AVAudioSessionCategoryAmbient;
   93         defaultConfig.categoryOptions = 0;
   94         defaultConfig.mode = AVAudioSessionModeDefault;
   95 
   96         audioCallConfig = [[RTCAudioSessionConfiguration alloc] init];
   97         audioCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
   98         audioCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker;
   99         audioCallConfig.mode = AVAudioSessionModeVoiceChat;
  100 
  101         videoCallConfig = [[RTCAudioSessionConfiguration alloc] init];
  102         videoCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
  103         videoCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
  104         videoCallConfig.mode = AVAudioSessionModeVideoChat;
  105 
  106         // Manually routing audio to the earpiece doesn't quite work unless one disables BT (weird, I know).
  107         earpieceConfig = [[RTCAudioSessionConfiguration alloc] init];
  108         earpieceConfig.category = AVAudioSessionCategoryPlayAndRecord;
  109         earpieceConfig.categoryOptions = 0;
  110         earpieceConfig.mode = AVAudioSessionModeVoiceChat;
  111 
  112         forceSpeaker = NO;
  113         forceEarpiece = NO;
  114         isSpeakerOn = NO;
  115         isEarpieceOn = NO;
  116 
  117         RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  118         [session addDelegate:self];
  119     }
  120 
  121     return self;
  122 }
  123 
  124 - (dispatch_queue_t)methodQueue {
  125     // Use a dedicated queue for audio mode operations.
  126     return _workerQueue;
  127 }
  128 
  129 - (BOOL)setConfigWithoutLock:(RTCAudioSessionConfiguration *)config
  130                        error:(NSError * _Nullable *)outError {
  131     RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  132 
  133     return [session setConfiguration:config error:outError];
  134 }
  135 
  136 - (BOOL)setConfig:(RTCAudioSessionConfiguration *)config
  137             error:(NSError * _Nullable *)outError {
  138 
  139     RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  140     [session lockForConfiguration];
  141     BOOL success = [self setConfigWithoutLock:config error:outError];
  142     [session unlockForConfiguration];
  143 
  144     return success;
  145 }
  146 
  147 #pragma mark - Exported methods
  148 
  149 RCT_EXPORT_METHOD(setMode:(int)mode
  150                   resolve:(RCTPromiseResolveBlock)resolve
  151                    reject:(RCTPromiseRejectBlock)reject) {
  152     RTCAudioSessionConfiguration *config = [self configForMode:mode];
  153     NSError *error;
  154 
  155     if (config == nil) {
  156         reject(@"setMode", @"Invalid mode", nil);
  157         return;
  158     }
  159 
  160     // Reset.
  161     if (mode == kAudioModeDefault) {
  162         forceSpeaker = NO;
  163         forceEarpiece = NO;
  164     }
  165 
  166     activeMode = mode;
  167 
  168     if ([self setConfig:config error:&error]) {
  169         resolve(nil);
  170     } else {
  171         reject(@"setMode", error.localizedDescription, error);
  172     }
  173     
  174     [self notifyDevicesChanged];
  175 }
  176 
  177 RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
  178                   resolve:(RCTPromiseResolveBlock)resolve
  179                   reject:(RCTPromiseRejectBlock)reject) {
  180     RCTLogInfo(@"[AudioMode] Selected device: %@", device);
  181     
  182     RTCAudioSession *session = JitsiAudioSession.rtcAudioSession;
  183     [session lockForConfiguration];
  184     BOOL success;
  185     NSError *error = nil;
  186     
  187     // Reset these, as we are about to compute them.
  188     forceSpeaker = NO;
  189     forceEarpiece = NO;
  190     
  191     // The speaker is special, so test for it first.
  192     if ([device isEqualToString:kDeviceTypeSpeaker]) {
  193         forceSpeaker = YES;
  194         success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
  195     } else {
  196         // Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
  197         AVAudioSession *_session = [AVAudioSession sharedInstance];
  198         AVAudioSessionPortDescription *port = nil;
  199 
  200         // Find the matching input device.
  201         for (AVAudioSessionPortDescription *portDesc in _session.availableInputs) {
  202             if ([portDesc.UID isEqualToString:device]) {
  203                 port = portDesc;
  204                 break;
  205             }
  206         }
  207 
  208         if (port != nil) {
  209             // First remove the override if we are going to select a different device.
  210             if (isSpeakerOn) {
  211                 [session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
  212             }
  213             
  214             // Special case for the earpiece.
  215             if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
  216                 forceEarpiece = YES;
  217                 [self setConfigWithoutLock:earpieceConfig error:nil];
  218             } else if (isEarpieceOn) {
  219                 // Reset the config.
  220                 RTCAudioSessionConfiguration *config = [self configForMode:activeMode];
  221                 [self setConfigWithoutLock:config error:nil];
  222             }
  223 
  224             // Select our preferred input.
  225             success = [session setPreferredInput:port error:&error];
  226         } else {
  227             success = NO;
  228             error = RCTErrorWithMessage(@"Could not find audio device");
  229         }
  230     }
  231     
  232     [session unlockForConfiguration];
  233     
  234     if (success) {
  235         resolve(nil);
  236     } else {
  237         reject(@"setAudioDevice", error != nil ? error.localizedDescription : @"", error);
  238     }
  239 }
  240 
  241 RCT_EXPORT_METHOD(updateDeviceList) {
  242     [self notifyDevicesChanged];
  243 }
  244 
  245 #pragma mark - RTCAudioSessionDelegate
  246 
  247 - (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
  248                             reason:(AVAudioSessionRouteChangeReason)reason
  249                      previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
  250     RCTLogInfo(@"[AudioMode] Route changed, reason: %lu", (unsigned long)reason);
  251 
  252     // Update JS about the changes.
  253     [self notifyDevicesChanged];
  254 
  255     dispatch_async(_workerQueue, ^{
  256         switch (reason) {
  257             case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
  258             case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
  259                 // If the device list changed, reset our overrides.
  260                 self->forceSpeaker = NO;
  261                 self->forceEarpiece = NO;
  262                 break;
  263             case AVAudioSessionRouteChangeReasonCategoryChange:
  264                 // The category has changed, re-apply our config.
  265                 // NB: It's tempting to doa  category check here and skip the processing,
  266                 // but that won't work. If the config changes but the category remains
  267                 // the same we'll still find ourselves here.
  268                 break;
  269             default:
  270                 return;
  271         }
  272 
  273         // We don't want to touch the category when in default mode.
  274         // This is to play well with other components which could be integrated
  275         // into the final application.
  276         if (self->activeMode != kAudioModeDefault) {
  277             RCTLogInfo(@"[AudioMode] Route changed, reapplying RTCAudioSession config");
  278             RTCAudioSessionConfiguration *config = [self configForMode:self->activeMode];
  279             [self setConfig:config error:nil];
  280             if (self->forceSpeaker && !self->isSpeakerOn) {
  281                 [session lockForConfiguration];
  282                 [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
  283                 [session unlockForConfiguration];
  284             }
  285         }
  286     });
  287 }
  288 
  289 - (void)audioSession:(RTCAudioSession *)audioSession didSetActive:(BOOL)active {
  290     RCTLogInfo(@"[AudioMode] Audio session didSetActive:%d", active);
  291 }
  292 
  293 #pragma mark - Helper methods
  294 
  295 - (RTCAudioSessionConfiguration *)configForMode:(int) mode {
  296     if (mode != kAudioModeDefault && forceEarpiece) {
  297         return earpieceConfig;
  298     }
  299 
  300     switch (mode) {
  301         case kAudioModeAudioCall:
  302             return audioCallConfig;
  303         case kAudioModeDefault:
  304             return defaultConfig;
  305         case kAudioModeVideoCall:
  306             return videoCallConfig;
  307         default:
  308             return nil;
  309     }
  310 }
  311 
  312 // Here we convert input and output port types into a single type.
  313 - (NSString *)portTypeToString:(AVAudioSessionPort) portType {
  314     if ([portType isEqualToString:AVAudioSessionPortHeadphones]
  315             || [portType isEqualToString:AVAudioSessionPortHeadsetMic]) {
  316         return kDeviceTypeHeadphones;
  317     } else if ([portType isEqualToString:AVAudioSessionPortBuiltInMic]
  318             || [portType isEqualToString:AVAudioSessionPortBuiltInReceiver]) {
  319         return kDeviceTypeEarpiece;
  320     } else if ([portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
  321         return kDeviceTypeSpeaker;
  322     } else if ([portType isEqualToString:AVAudioSessionPortBluetoothHFP]
  323             || [portType isEqualToString:AVAudioSessionPortBluetoothLE]
  324             || [portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) {
  325         return kDeviceTypeBluetooth;
  326     } else if ([portType isEqualToString:AVAudioSessionPortCarAudio]) {
  327         return kDeviceTypeCar; 
  328     } else {
  329         return kDeviceTypeUnknown;
  330     }
  331 }
  332 
  333 - (void)notifyDevicesChanged {
  334     dispatch_async(_workerQueue, ^{
  335         NSMutableArray *data = [[NSMutableArray alloc] init];
  336         // Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
  337         AVAudioSession *session = [AVAudioSession sharedInstance];
  338         NSString *currentPort = @"";
  339         AVAudioSessionRouteDescription *currentRoute = session.currentRoute;
  340         
  341         // Check what the current device is. Because the speaker is somewhat special, we need to
  342         // check for it first.
  343         if (currentRoute != nil) {
  344             AVAudioSessionPortDescription *output = currentRoute.outputs.firstObject;
  345             AVAudioSessionPortDescription *input = currentRoute.inputs.firstObject;
  346             if (output != nil && [output.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
  347                 currentPort = kDeviceTypeSpeaker;
  348                 self->isSpeakerOn = YES;
  349             } else if (input != nil) {
  350                 currentPort = input.UID;
  351                 self->isSpeakerOn = NO;
  352                 self->isEarpieceOn = [input.portType isEqualToString:AVAudioSessionPortBuiltInMic];
  353             }
  354         }
  355         
  356         BOOL headphonesAvailable = NO;
  357         for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
  358             if ([portDesc.portType isEqualToString:AVAudioSessionPortHeadsetMic] || [portDesc.portType isEqualToString:AVAudioSessionPortHeadphones]) {
  359                 headphonesAvailable = YES;
  360                 break;
  361             }
  362         }
  363 
  364         for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
  365             // Skip "Phone" if headphones are present.
  366             if (headphonesAvailable && [portDesc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
  367                 continue;
  368             }
  369             id deviceData
  370                 = @{
  371                     @"type": [self portTypeToString:portDesc.portType],
  372                     @"name": portDesc.portName,
  373                     @"uid": portDesc.UID,
  374                     @"selected": [NSNumber numberWithBool:[portDesc.UID isEqualToString:currentPort]]
  375                 };
  376             [data addObject:deviceData];
  377         }
  378 
  379         // We need to manually add the speaker because it will never show up in the
  380         // previous list, as it's not an input.
  381         [data addObject:
  382             @{ @"type": kDeviceTypeSpeaker,
  383                @"name": @"Speaker",
  384                @"uid": kDeviceTypeSpeaker,
  385                @"selected": [NSNumber numberWithBool:[kDeviceTypeSpeaker isEqualToString:currentPort]]
  386         }];
  387         
  388         [self sendEventWithName:kDevicesChanged body:data];
  389     });
  390 }
  391 
  392 @end