"Fossies" - the Fresh Open Source Software Archive

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