Browse Source

audio-mode: refactor device handling

This commit refactors device selection (more heavily on iOS) to make it
consistent across platforms.

Due to its complexity I couldn't break out each step into separate commits,
apologies to the reviewer.

Changes made to device handling:

- speaker is always the default, regardless of the mode
- "Phone" shows as a selectable option, even in video call mode
- "Phone" is not displayed when wired headphones are present
- Shared device picker between iOS and Android
- Runtime device updates while the picker is open
master
Saúl Ibarra Corretgé 5 years ago
parent
commit
1c1e8a942b

+ 23
- 24
android/sdk/src/main/java/org/jitsi/meet/sdk/AudioModeModule.java View File

@@ -256,6 +256,11 @@ class AudioModeModule extends ReactContextBaseJavaModule
256 256
     private static final String DEVICE_HEADPHONES = "HEADPHONES";
257 257
     private static final String DEVICE_SPEAKER    = "SPEAKER";
258 258
 
259
+    /**
260
+     * Device change event.
261
+     */
262
+    private static final String DEVICE_CHANGE_EVENT = "org.jitsi.meet:features/audio-mode#devices-update";
263
+
259 264
     /**
260 265
      * List of currently available audio devices.
261 266
      */
@@ -303,7 +308,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
303 308
 
304 309
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
305 310
                 // Do an initial detection on Android >= M.
306
-                runInAudioThread(onAudioDeviceChangeRunner);
311
+                onAudioDeviceChange();
307 312
             } else {
308 313
                 // On Android < M, detect if we have an earpiece.
309 314
                 PackageManager pm = reactContext.getPackageManager();
@@ -327,6 +332,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
327 332
     public Map<String, Object> getConstants() {
328 333
         Map<String, Object> constants = new HashMap<>();
329 334
 
335
+        constants.put("DEVICE_CHANGE_EVENT", DEVICE_CHANGE_EVENT);
330 336
         constants.put("AUDIO_CALL", AUDIO_CALL);
331 337
         constants.put("DEFAULT", DEFAULT);
332 338
         constants.put("VIDEO_CALL", VIDEO_CALL);
@@ -335,31 +341,26 @@ class AudioModeModule extends ReactContextBaseJavaModule
335 341
     }
336 342
 
337 343
     /**
338
-     * Gets the list of available audio device categories, i.e. 'bluetooth',
339
-     * 'earpiece ', 'speaker', 'headphones'.
340
-     *
341
-     * @param promise a {@link Promise} which will be resolved with an object
342
-     *                containing a 'devices' key with a list of devices, plus a
343
-     *                'selected' key with the selected one.
344
+     * Notifies JS land that the devices list has changed.
344 345
      */
345
-    @ReactMethod
346
-    public void getAudioDevices(final Promise promise) {
346
+    private void notifyDevicesChanged() {
347 347
         runInAudioThread(new Runnable() {
348 348
             @Override
349 349
             public void run() {
350
-                WritableMap map = Arguments.createMap();
351
-                map.putString("selected", selectedDevice);
352
-                WritableArray devices = Arguments.createArray();
350
+                WritableArray data = Arguments.createArray();
351
+                final boolean hasHeadphones = availableDevices.contains(DEVICE_HEADPHONES);
353 352
                 for (String device : availableDevices) {
354
-                    if (mode == VIDEO_CALL && device.equals(DEVICE_EARPIECE)) {
355
-                        // Skip earpiece when in video call mode.
353
+                    if (hasHeadphones && device.equals(DEVICE_EARPIECE)) {
354
+                        // Skip earpiece when headphones are plugged in.
356 355
                         continue;
357 356
                     }
358
-                    devices.pushString(device);
357
+                    WritableMap deviceInfo = Arguments.createMap();
358
+                    deviceInfo.putString("type", device);
359
+                    deviceInfo.putBoolean("selected", device.equals(selectedDevice));
360
+                    data.pushMap(deviceInfo);
359 361
                 }
360
-                map.putArray("devices", devices);
361
-
362
-                promise.resolve(map);
362
+                ReactInstanceManagerHolder.emitEvent(DEVICE_CHANGE_EVENT, data);
363
+                Log.i(TAG, "Updating audio device list");
363 364
             }
364 365
         });
365 366
     }
@@ -584,7 +585,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
584 585
             return;
585 586
         }
586 587
 
587
-        Runnable r = new Runnable() {
588
+        runInAudioThread(new Runnable() {
588 589
             @Override
589 590
             public void run() {
590 591
                 boolean success;
@@ -607,8 +608,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
607 608
                             "Failed to set audio mode to " + mode);
608 609
                 }
609 610
             }
610
-        };
611
-        runInAudioThread(r);
611
+        });
612 612
     }
613 613
 
614 614
     /**
@@ -690,6 +690,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
690 690
             selectedDevice = null;
691 691
             userSelectedDevice = null;
692 692
 
693
+            notifyDevicesChanged();
693 694
             return true;
694 695
         }
695 696
 
@@ -708,7 +709,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
708 709
         }
709 710
 
710 711
         boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
711
-        boolean earpieceAvailable = availableDevices.contains(DEVICE_EARPIECE);
712 712
         boolean headsetAvailable = availableDevices.contains(DEVICE_HEADPHONES);
713 713
 
714 714
         // Pick the desired device based on what's available and the mode.
@@ -717,8 +717,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
717 717
             audioDevice = DEVICE_BLUETOOTH;
718 718
         } else if (headsetAvailable) {
719 719
             audioDevice = DEVICE_HEADPHONES;
720
-        } else if (mode == AUDIO_CALL && earpieceAvailable) {
721
-            audioDevice = DEVICE_EARPIECE;
722 720
         } else {
723 721
             audioDevice = DEVICE_SPEAKER;
724 722
         }
@@ -744,6 +742,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
744 742
             setAudioRoutePreO(audioDevice);
745 743
         }
746 744
 
745
+        notifyDevicesChanged();
747 746
         return true;
748 747
     }
749 748
 }

+ 0
- 4
ios/sdk/sdk.xcodeproj/project.pbxproj View File

@@ -10,7 +10,6 @@
10 10
 		0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */; settings = {ATTRIBUTES = (Public, ); }; };
11 11
 		0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */; };
12 12
 		0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
13
-		0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */; };
14 13
 		0B49424520AD8DBD00BD2DE0 /* outgoingStart.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424320AD8DBD00BD2DE0 /* outgoingStart.wav */; };
15 14
 		0B49424620AD8DBD00BD2DE0 /* outgoingRinging.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */; };
16 15
 		0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
@@ -59,7 +58,6 @@
59 58
 		0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JitsiMeetView.h; sourceTree = "<group>"; };
60 59
 		0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetView.m; sourceTree = "<group>"; };
61 60
 		0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewDelegate.h; sourceTree = "<group>"; };
62
-		0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPVolumeViewManager.m; sourceTree = "<group>"; };
63 61
 		0B49424320AD8DBD00BD2DE0 /* outgoingStart.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = outgoingStart.wav; path = ../../sounds/outgoingStart.wav; sourceTree = "<group>"; };
64 62
 		0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = outgoingRinging.wav; path = ../../sounds/outgoingRinging.wav; sourceTree = "<group>"; };
65 63
 		0B93EF7A1EC608550030D24D /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; };
@@ -195,7 +193,6 @@
195 193
 				C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
196 194
 				0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
197 195
 				DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
198
-				0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */,
199 196
 				C6A3426B204F127900E062DD /* picture-in-picture */,
200 197
 				0BCA495D1EC4B6C600B793EE /* POSIX.m */,
201 198
 				0BCA495E1EC4B6C600B793EE /* Proximity.m */,
@@ -509,7 +506,6 @@
509 506
 				C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
510 507
 				DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */,
511 508
 				0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
512
-				0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
513 509
 				0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
514 510
 				A480429C21EE335600289B73 /* AmplitudeModule.m in Sources */,
515 511
 				C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */,

+ 247
- 34
ios/sdk/src/AudioMode.m View File

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

+ 0
- 62
ios/sdk/src/MPVolumeViewManager.m View File

@@ -1,62 +0,0 @@
1
-/*
2
- * Copyright @ 2017-present Atlassian Pty Ltd
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 <React/RCTUIManager.h>
18
-#import <React/RCTViewManager.h>
19
-
20
-@import MediaPlayer;
21
-
22
-
23
-@interface MPVolumeViewManager : RCTViewManager
24
-@end
25
-
26
-@implementation MPVolumeViewManager
27
-
28
-RCT_EXPORT_MODULE()
29
-
30
-- (UIView *)view {
31
-    MPVolumeView *volumeView = [[MPVolumeView alloc] init];
32
-    volumeView.showsRouteButton = YES;
33
-    volumeView.showsVolumeSlider = NO;
34
-
35
-    return (UIView *) volumeView;
36
-}
37
-
38
-RCT_EXPORT_METHOD(show:(nonnull NSNumber *)reactTag) {
39
-    [self.bridge.uiManager addUIBlock:^(
40
-                __unused RCTUIManager *uiManager,
41
-                NSDictionary<NSNumber *, UIView *> *viewRegistry) {
42
-        id view = viewRegistry[reactTag];
43
-        if (![view isKindOfClass:[MPVolumeView class]]) {
44
-            RCTLogError(@"Invalid view returned from registry, expecting \
45
-                        MPVolumeView, got: %@", view);
46
-        } else {
47
-            // Simulate a click
48
-            UIButton *btn = nil;
49
-            for (UIView *buttonView in ((UIView *) view).subviews) {
50
-                if ([buttonView isKindOfClass:[UIButton class]]) {
51
-                    btn = (UIButton *) buttonView;
52
-                    break;
53
-                }
54
-            }
55
-            if (btn != nil) {
56
-                [btn sendActionsForControlEvents:UIControlEventTouchUpInside];
57
-            }
58
-        }
59
-    }];
60
-}
61
-
62
-@end

+ 2
- 1
lang/main.json View File

@@ -21,7 +21,8 @@
21 21
         "bluetooth": "Bluetooth",
22 22
         "headphones": "Headphones",
23 23
         "phone": "Phone",
24
-        "speaker": "Speaker"
24
+        "speaker": "Speaker",
25
+        "none": "No audio devices available"
25 26
     },
26 27
     "audioOnly": {
27 28
         "audioOnly": "Low bandwidth"

+ 23
- 0
react/features/mobile/audio-mode/actionTypes.js View File

@@ -0,0 +1,23 @@
1
+/**
2
+ * The type of redux action to set Audio Mode device list.
3
+ *
4
+ * {
5
+ *     type: _SET_AUDIOMODE_DEVICES,
6
+ *     devices: Array
7
+ * }
8
+ *
9
+ * @protected
10
+ */
11
+export const _SET_AUDIOMODE_DEVICES = '_SET_AUDIOMODE_DEVICES';
12
+
13
+/**
14
+ * The type of redux action to set Audio Mode module's subscriptions.
15
+ *
16
+ * {
17
+ *     type: _SET_AUDIOMODE_SUBSCRIPTIONS,
18
+ *     subscriptions: Array|undefined
19
+ * }
20
+ *
21
+ * @protected
22
+ */
23
+export const _SET_AUDIOMODE_SUBSCRIPTIONS = '_SET_AUDIOMODE_SUBSCRIPTIONS';

+ 1
- 91
react/features/mobile/audio-mode/components/AudioRouteButton.js View File

@@ -1,13 +1,5 @@
1 1
 // @flow
2 2
 
3
-import React from 'react';
4
-import {
5
-    findNodeHandle,
6
-    NativeModules,
7
-    requireNativeComponent,
8
-    View
9
-} from 'react-native';
10
-
11 3
 import { openDialog } from '../../../base/dialog';
12 4
 import { translate } from '../../../base/i18n';
13 5
 import { connect } from '../../../base/redux';
@@ -16,19 +8,6 @@ import type { AbstractButtonProps } from '../../../base/toolbox';
16 8
 
17 9
 import AudioRoutePickerDialog from './AudioRoutePickerDialog';
18 10
 
19
-/**
20
- * The {@code MPVolumeView} React {@code Component}. It will only be available
21
- * on iOS.
22
- */
23
-const MPVolumeView
24
-    = NativeModules.MPVolumeViewManager
25
-        && requireNativeComponent('MPVolumeView');
26
-
27
-/**
28
- * The style required to hide the {@code MPVolumeView}, since it's displayed
29
- * programmatically.
30
- */
31
-const HIDE_VIEW_STYLE = { display: 'none' };
32 11
 
33 12
 type Props = AbstractButtonProps & {
34 13
 
@@ -47,30 +26,6 @@ class AudioRouteButton extends AbstractButton<Props, *> {
47 26
     iconName = 'icon-volume';
48 27
     label = 'toolbar.audioRoute';
49 28
 
50
-    _volumeComponent: ?Object;
51
-
52
-    /**
53
-     * Initializes a new {@code AudioRouteButton} instance.
54
-     *
55
-     * @param {Props} props - The React {@code Component} props to initialize
56
-     * the new {@code AudioRouteButton} instance with.
57
-     */
58
-    constructor(props: Props) {
59
-        super(props);
60
-
61
-        /**
62
-         * The internal reference to the React {@code MPVolumeView} for
63
-         * showing the volume control view.
64
-         *
65
-         * @private
66
-         * @type {ReactElement}
67
-         */
68
-        this._volumeComponent = null;
69
-
70
-        // Bind event handlers so they are only bound once per instance.
71
-        this._setVolumeComponent = this._setVolumeComponent.bind(this);
72
-    }
73
-
74 29
     /**
75 30
      * Handles clicking / pressing the button, and opens the appropriate dialog.
76 31
      *
@@ -78,52 +33,7 @@ class AudioRouteButton extends AbstractButton<Props, *> {
78 33
      * @returns {void}
79 34
      */
80 35
     _handleClick() {
81
-        if (MPVolumeView) {
82
-            NativeModules.MPVolumeViewManager.show(
83
-                findNodeHandle(this._volumeComponent));
84
-        } else if (AudioRoutePickerDialog) {
85
-            this.props.dispatch(openDialog(AudioRoutePickerDialog));
86
-        }
87
-    }
88
-
89
-    _setVolumeComponent: (?Object) => void;
90
-
91
-    /**
92
-     * Sets the internal reference to the React Component wrapping the
93
-     * {@code MPVolumeView} component.
94
-     *
95
-     * @param {ReactElement} component - React Component.
96
-     * @private
97
-     * @returns {void}
98
-     */
99
-    _setVolumeComponent(component) {
100
-        this._volumeComponent = component;
101
-    }
102
-
103
-    /**
104
-     * Implements React's {@link Component#render()}.
105
-     *
106
-     * @inheritdoc
107
-     * @returns {React$Node}
108
-     */
109
-    render() {
110
-        if (!MPVolumeView && !AudioRoutePickerDialog) {
111
-            return null;
112
-        }
113
-
114
-        const element = super.render();
115
-
116
-        return (
117
-            <View>
118
-                { element }
119
-                {
120
-                    MPVolumeView
121
-                        && <MPVolumeView
122
-                            ref = { this._setVolumeComponent }
123
-                            style = { HIDE_VIEW_STYLE } />
124
-                }
125
-            </View>
126
-        );
36
+        this.props.dispatch(openDialog(AudioRoutePickerDialog));
127 37
     }
128 38
 }
129 39
 

+ 111
- 47
react/features/mobile/audio-mode/components/AudioRoutePickerDialog.js View File

@@ -13,6 +13,8 @@ import { ColorPalette, type StyleType } from '../../../base/styles';
13 13
 
14 14
 import styles from './styles';
15 15
 
16
+const { AudioMode } = NativeModules;
17
+
16 18
 /**
17 19
  * Type definition for a single entry in the device list.
18 20
  */
@@ -37,7 +39,38 @@ type Device = {
37 39
     /**
38 40
      * Device type.
39 41
      */
40
-    type: string
42
+    type: string,
43
+
44
+    /**
45
+     * Unique device ID.
46
+     */
47
+    uid: ?string
48
+};
49
+
50
+/**
51
+ * "Raw" device, as returned by native.
52
+ */
53
+type RawDevice = {
54
+
55
+    /**
56
+     * Display name for the device.
57
+     */
58
+    name: ?string,
59
+
60
+    /**
61
+     * is this device selected?
62
+     */
63
+    selected: boolean,
64
+
65
+    /**
66
+     * Device type.
67
+     */
68
+    type: string,
69
+
70
+    /**
71
+     * Unique device ID.
72
+     */
73
+    uid: ?string
41 74
 };
42 75
 
43 76
 /**
@@ -50,6 +83,11 @@ type Props = {
50 83
      */
51 84
     _bottomSheetStyles: StyleType,
52 85
 
86
+    /**
87
+     * Object describing available devices.
88
+     */
89
+    _devices: Array<RawDevice>,
90
+
53 91
     /**
54 92
      * Used for hiding the dialog when the selection was completed.
55 93
      */
@@ -72,8 +110,6 @@ type State = {
72 110
     devices: Array<Device>
73 111
 };
74 112
 
75
-const { AudioMode } = NativeModules;
76
-
77 113
 /**
78 114
  * Maps each device type to a display name and icon.
79 115
  */
@@ -101,11 +137,9 @@ const deviceInfoMap = {
101 137
 };
102 138
 
103 139
 /**
104
- * The exported React {@code Component}. {@code AudioRoutePickerDialog} is
105
- * exported only if the {@code AudioMode} module has the capability to get / set
106
- * audio devices.
140
+ * The exported React {@code Component}.
107 141
  */
108
-let AudioRoutePickerDialog_;
142
+let AudioRoutePickerDialog_; // eslint-disable-line prefer-const
109 143
 
110 144
 /**
111 145
  * Implements a React {@code Component} which prompts the user when a password
@@ -115,11 +149,47 @@ class AudioRoutePickerDialog extends Component<Props, State> {
115 149
     state = {
116 150
         /**
117 151
          * Available audio devices, it will be set in
118
-         * {@link #componentDidMount()}.
152
+         * {@link #getDerivedStateFromProps()}.
119 153
          */
120 154
         devices: []
121 155
     };
122 156
 
157
+    /**
158
+     * Implements React's {@link Component#getDerivedStateFromProps()}.
159
+     *
160
+     * @inheritdoc
161
+     */
162
+    static getDerivedStateFromProps(props: Props) {
163
+        const { _devices: devices } = props;
164
+
165
+        if (!devices) {
166
+            return null;
167
+        }
168
+
169
+        const audioDevices = [];
170
+
171
+        for (const device of devices) {
172
+            const infoMap = deviceInfoMap[device.type];
173
+            const text = device.type === 'BLUETOOTH' && device.name ? device.name : infoMap.text;
174
+
175
+            if (infoMap) {
176
+                const info = {
177
+                    ...infoMap,
178
+                    selected: Boolean(device.selected),
179
+                    text: props.t(text),
180
+                    uid: device.uid
181
+                };
182
+
183
+                audioDevices.push(info);
184
+            }
185
+        }
186
+
187
+        // Make sure devices is alphabetically sorted.
188
+        return {
189
+            devices: _.sortBy(audioDevices, 'text')
190
+        };
191
+    }
192
+
123 193
     /**
124 194
      * Initializes a new {@code PasswordRequiredPrompt} instance.
125 195
      *
@@ -131,36 +201,9 @@ class AudioRoutePickerDialog extends Component<Props, State> {
131 201
 
132 202
         // Bind event handlers so they are only bound once per instance.
133 203
         this._onCancel = this._onCancel.bind(this);
134
-    }
135
-
136
-    /**
137
-     * Initializes the device list by querying {@code AudioMode}.
138
-     *
139
-     * @inheritdoc
140
-     */
141
-    componentDidMount() {
142
-        AudioMode.getAudioDevices().then(({ devices, selected }) => {
143
-            const audioDevices = [];
144
-
145
-            if (devices) {
146
-                for (const device of devices) {
147
-                    if (deviceInfoMap[device]) {
148
-                        const info = Object.assign({}, deviceInfoMap[device]);
149
-
150
-                        info.selected = device === selected;
151
-                        info.text = this.props.t(info.text);
152
-                        audioDevices.push(info);
153
-                    }
154
-                }
155
-            }
156 204
 
157
-            if (audioDevices) {
158
-                // Make sure devices is alphabetically sorted.
159
-                this.setState({
160
-                    devices: _.sortBy(audioDevices, 'text')
161
-                });
162
-            }
163
-        });
205
+        // Trigger an initial update.
206
+        AudioMode.updateDeviceList && AudioMode.updateDeviceList();
164 207
     }
165 208
 
166 209
     /**
@@ -197,7 +240,7 @@ class AudioRoutePickerDialog extends Component<Props, State> {
197 240
     _onSelectDeviceFn(device: Device) {
198 241
         return () => {
199 242
             this._hide();
200
-            AudioMode.setAudioDevice(device.type);
243
+            AudioMode.setAudioDevice(device.uid || device.type);
201 244
         };
202 245
     }
203 246
 
@@ -230,6 +273,27 @@ class AudioRoutePickerDialog extends Component<Props, State> {
230 273
         );
231 274
     }
232 275
 
276
+    /**
277
+     * Renders a "fake" device row indicating there are no devices.
278
+     *
279
+     * @private
280
+     * @returns {ReactElement}
281
+     */
282
+    _renderNoDevices() {
283
+        const { _bottomSheetStyles, t } = this.props;
284
+
285
+        return (
286
+            <View style = { styles.deviceRow } >
287
+                <Icon
288
+                    name = { deviceInfoMap.SPEAKER.iconName }
289
+                    style = { [ styles.deviceIcon, _bottomSheetStyles.iconStyle ] } />
290
+                <Text style = { [ styles.deviceText, _bottomSheetStyles.labelStyle ] } >
291
+                    { t('audioDevices.none') }
292
+                </Text>
293
+            </View>
294
+        );
295
+    }
296
+
233 297
     /**
234 298
      * Implements React's {@link Component#render()}.
235 299
      *
@@ -238,14 +302,17 @@ class AudioRoutePickerDialog extends Component<Props, State> {
238 302
      */
239 303
     render() {
240 304
         const { devices } = this.state;
305
+        let content;
241 306
 
242
-        if (!devices.length) {
243
-            return null;
307
+        if (devices.length === 0) {
308
+            content = this._renderNoDevices();
309
+        } else {
310
+            content = this.state.devices.map(this._renderDevice, this);
244 311
         }
245 312
 
246 313
         return (
247 314
             <BottomSheet onCancel = { this._onCancel }>
248
-                { this.state.devices.map(this._renderDevice, this) }
315
+                { content }
249 316
             </BottomSheet>
250 317
         );
251 318
     }
@@ -259,14 +326,11 @@ class AudioRoutePickerDialog extends Component<Props, State> {
259 326
  */
260 327
 function _mapStateToProps(state) {
261 328
     return {
262
-        _bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet')
329
+        _bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
330
+        _devices: state['features/mobile/audio-mode'].devices
263 331
     };
264 332
 }
265 333
 
266
-// Only export the dialog if we have support for getting / setting audio devices
267
-// in AudioMode.
268
-if (AudioMode.getAudioDevices && AudioMode.setAudioDevice) {
269
-    AudioRoutePickerDialog_ = translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
270
-}
334
+AudioRoutePickerDialog_ = translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
271 335
 
272 336
 export default AudioRoutePickerDialog_;

+ 1
- 0
react/features/mobile/audio-mode/index.js View File

@@ -1,3 +1,4 @@
1 1
 export * from './components';
2 2
 
3 3
 import './middleware';
4
+import './reducer';

+ 119
- 44
react/features/mobile/audio-mode/middleware.js View File

@@ -1,9 +1,9 @@
1 1
 // @flow
2 2
 
3
-import { NativeModules } from 'react-native';
3
+import { NativeEventEmitter, NativeModules } from 'react-native';
4 4
 
5 5
 import { SET_AUDIO_ONLY } from '../../base/audio-only';
6
-import { APP_WILL_MOUNT } from '../../base/app';
6
+import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
7 7
 import {
8 8
     CONFERENCE_FAILED,
9 9
     CONFERENCE_LEFT,
@@ -12,7 +12,10 @@ import {
12 12
 } from '../../base/conference';
13 13
 import { MiddlewareRegistry } from '../../base/redux';
14 14
 
15
+import { _SET_AUDIOMODE_DEVICES, _SET_AUDIOMODE_SUBSCRIPTIONS } from './actionTypes';
16
+
15 17
 const { AudioMode } = NativeModules;
18
+const AudioModeEmitter = new NativeEventEmitter(AudioMode);
16 19
 const logger = require('jitsi-meet-logger').getLogger(__filename);
17 20
 
18 21
 /**
@@ -23,55 +26,127 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
23 26
  * @param {Store} store - The redux store.
24 27
  * @returns {Function}
25 28
  */
26
-MiddlewareRegistry.register(({ getState }) => next => action => {
27
-    const result = next(action);
29
+MiddlewareRegistry.register(store => next => action => {
30
+    /* eslint-disable no-fallthrough */
28 31
 
29
-    if (AudioMode) {
30
-        let mode;
32
+    switch (action.type) {
33
+    case _SET_AUDIOMODE_SUBSCRIPTIONS:
34
+        _setSubscriptions(store);
35
+        break;
36
+    case APP_WILL_UNMOUNT: {
37
+        store.dispatch({
38
+            type: _SET_AUDIOMODE_SUBSCRIPTIONS,
39
+            subscriptions: undefined
40
+        });
41
+        break;
42
+    }
43
+    case APP_WILL_MOUNT:
44
+        _appWillMount(store);
45
+    case CONFERENCE_FAILED: // eslint-disable-line no-fallthrough
46
+    case CONFERENCE_LEFT:
31 47
 
32
-        switch (action.type) {
33
-        case APP_WILL_MOUNT:
34
-        case CONFERENCE_FAILED:
35
-        case CONFERENCE_LEFT: {
36
-            const conference = getCurrentConference(getState());
48
+    /*
49
+    * NOTE: We moved the audio mode setting from CONFERENCE_WILL_JOIN to
50
+    * CONFERENCE_JOINED because in case of a locked room, the app goes
51
+    * through CONFERENCE_FAILED state and gets to CONFERENCE_JOINED only
52
+    * after a correct password, so we want to make sure we have the correct
53
+    * audio mode set up when we finally get to the conf, but also make sure
54
+    * that the app is in the right audio mode if the user leaves the
55
+    * conference after the password prompt appears.
56
+    */
57
+    case CONFERENCE_JOINED:
58
+    case SET_AUDIO_ONLY:
59
+        return _updateAudioMode(store, next, action);
37 60
 
38
-            if (typeof conference === 'undefined') {
39
-                mode = AudioMode.DEFAULT;
40
-            }
61
+    }
41 62
 
42
-            break;
43
-        }
63
+    /* eslint-enable no-fallthrough */
44 64
 
45
-        /*
46
-         * NOTE: We moved the audio mode setting from CONFERENCE_WILL_JOIN to
47
-         * CONFERENCE_JOINED because in case of a locked room, the app goes
48
-         * through CONFERENCE_FAILED state and gets to CONFERENCE_JOINED only
49
-         * after a correct password, so we want to make sure we have the correct
50
-         * audio mode set up when we finally get to the conf, but also make sure
51
-         * that the app is in the right audio mode if the user leaves the
52
-         * conference after the password prompt appears.
53
-         */
54
-        case CONFERENCE_JOINED:
55
-        case SET_AUDIO_ONLY: {
56
-            const state = getState();
57
-            const { conference } = state['features/base/conference'];
58
-            const { enabled: audioOnly } = state['features/base/audio-only'];
59
-
60
-            conference
61
-                && (mode = audioOnly
62
-                    ? AudioMode.AUDIO_CALL
63
-                    : AudioMode.VIDEO_CALL);
64
-            break;
65
-        }
66
-        }
65
+    return next(action);
66
+});
67 67
 
68
-        if (typeof mode !== 'undefined') {
69
-            AudioMode.setMode(mode)
70
-                .catch(err =>
71
-                    logger.error(
72
-                        `Failed to set audio mode ${String(mode)}: ${err}`));
68
+/**
69
+ * Notifies this feature that the action {@link APP_WILL_MOUNT} is being
70
+ * dispatched within a specific redux {@code store}.
71
+ *
72
+ * @param {Store} store - The redux store in which the specified {@code action}
73
+ * is being dispatched.
74
+ * @private
75
+ * @returns {void}
76
+ */
77
+function _appWillMount(store) {
78
+    const subscriptions = [
79
+        AudioModeEmitter.addListener(AudioMode.DEVICE_CHANGE_EVENT, _onDevicesUpdate, store)
80
+    ];
81
+
82
+    store.dispatch({
83
+        type: _SET_AUDIOMODE_SUBSCRIPTIONS,
84
+        subscriptions
85
+    });
86
+}
87
+
88
+/**
89
+ * Handles audio device changes. The list will be stored on the redux store.
90
+ *
91
+ * @param {Object} devices - The current list of devices.
92
+ * @private
93
+ * @returns {void}
94
+ */
95
+function _onDevicesUpdate(devices) {
96
+    const { dispatch } = this; // eslint-disable-line no-invalid-this
97
+
98
+    dispatch({
99
+        type: _SET_AUDIOMODE_DEVICES,
100
+        devices
101
+    });
102
+}
103
+
104
+/**
105
+ * Notifies this feature that the action
106
+ * {@link _SET_AUDIOMODE_SUBSCRIPTIONS} is being dispatched within
107
+ * a specific redux {@code store}.
108
+ *
109
+ * @param {Store} store - The redux store in which the specified {@code action}
110
+ * is being dispatched.
111
+ * @private
112
+ * @returns {void}
113
+ */
114
+function _setSubscriptions({ getState }) {
115
+    const { subscriptions } = getState()['features/mobile/audio-mode'];
116
+
117
+    if (subscriptions) {
118
+        for (const subscription of subscriptions) {
119
+            subscription.remove();
73 120
         }
74 121
     }
122
+}
123
+
124
+/**
125
+ * Updates the audio mode based on the current (redux) state.
126
+ *
127
+ * @param {Store} store - The redux store in which the specified {@code action}
128
+ * is being dispatched.
129
+ * @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
130
+ * specified {@code action} in the specified {@code store}.
131
+ * @param {Action} action - The redux action which is
132
+ * being dispatched in the specified {@code store}.
133
+ * @private
134
+ * @returns {*} The value returned by {@code next(action)}.
135
+ */
136
+function _updateAudioMode({ getState }, next, action) {
137
+    const result = next(action);
138
+    const state = getState();
139
+    const conference = getCurrentConference(state);
140
+    const { enabled: audioOnly } = state['features/base/audio-only'];
141
+    let mode;
142
+
143
+    if (conference) {
144
+        mode = audioOnly ? AudioMode.AUDIO_CALL : AudioMode.VIDEO_CALL;
145
+    } else {
146
+        mode = AudioMode.DEFAULT;
147
+    }
148
+
149
+    AudioMode.setMode(mode).catch(err => logger.error(`Failed to set audio mode ${String(mode)}: ${err}`));
75 150
 
76 151
     return result;
77
-});
152
+}

+ 28
- 0
react/features/mobile/audio-mode/reducer.js View File

@@ -0,0 +1,28 @@
1
+// @flow
2
+
3
+import { equals, set, ReducerRegistry } from '../../base/redux';
4
+
5
+import { _SET_AUDIOMODE_DEVICES, _SET_AUDIOMODE_SUBSCRIPTIONS } from './actionTypes';
6
+
7
+const DEFAULT_STATE = {
8
+    devices: [],
9
+    subscriptions: []
10
+};
11
+
12
+ReducerRegistry.register('features/mobile/audio-mode', (state = DEFAULT_STATE, action) => {
13
+    switch (action.type) {
14
+    case _SET_AUDIOMODE_DEVICES: {
15
+        const { devices } = action;
16
+
17
+        if (equals(state.devices, devices)) {
18
+            return state;
19
+        }
20
+
21
+        return set(state, 'devices', devices);
22
+    }
23
+    case _SET_AUDIOMODE_SUBSCRIPTIONS:
24
+        return set(state, 'subscriptions', action.subscriptions);
25
+    }
26
+
27
+    return state;
28
+});

Loading…
Cancel
Save