"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