"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-6193/ios/sdk/src/AudioMode.m" (20 May 2022, 14458 Bytes) of package /linux/misc/jitsi-meet-6193.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. See also the last Fossies "Diffs" side-by-side code changes report for "AudioMode.m": jitsi-meet_7001_vs_jitsi-meet_7210.

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