"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