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
This commit is contained in:
parent
9721d99918
commit
1c1e8a942b
|
@ -256,6 +256,11 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
private static final String DEVICE_HEADPHONES = "HEADPHONES";
|
||||
private static final String DEVICE_SPEAKER = "SPEAKER";
|
||||
|
||||
/**
|
||||
* Device change event.
|
||||
*/
|
||||
private static final String DEVICE_CHANGE_EVENT = "org.jitsi.meet:features/audio-mode#devices-update";
|
||||
|
||||
/**
|
||||
* List of currently available audio devices.
|
||||
*/
|
||||
|
@ -303,7 +308,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Do an initial detection on Android >= M.
|
||||
runInAudioThread(onAudioDeviceChangeRunner);
|
||||
onAudioDeviceChange();
|
||||
} else {
|
||||
// On Android < M, detect if we have an earpiece.
|
||||
PackageManager pm = reactContext.getPackageManager();
|
||||
|
@ -327,6 +332,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
public Map<String, Object> getConstants() {
|
||||
Map<String, Object> constants = new HashMap<>();
|
||||
|
||||
constants.put("DEVICE_CHANGE_EVENT", DEVICE_CHANGE_EVENT);
|
||||
constants.put("AUDIO_CALL", AUDIO_CALL);
|
||||
constants.put("DEFAULT", DEFAULT);
|
||||
constants.put("VIDEO_CALL", VIDEO_CALL);
|
||||
|
@ -335,31 +341,26 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the list of available audio device categories, i.e. 'bluetooth',
|
||||
* 'earpiece ', 'speaker', 'headphones'.
|
||||
*
|
||||
* @param promise a {@link Promise} which will be resolved with an object
|
||||
* containing a 'devices' key with a list of devices, plus a
|
||||
* 'selected' key with the selected one.
|
||||
* Notifies JS land that the devices list has changed.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void getAudioDevices(final Promise promise) {
|
||||
private void notifyDevicesChanged() {
|
||||
runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
WritableMap map = Arguments.createMap();
|
||||
map.putString("selected", selectedDevice);
|
||||
WritableArray devices = Arguments.createArray();
|
||||
WritableArray data = Arguments.createArray();
|
||||
final boolean hasHeadphones = availableDevices.contains(DEVICE_HEADPHONES);
|
||||
for (String device : availableDevices) {
|
||||
if (mode == VIDEO_CALL && device.equals(DEVICE_EARPIECE)) {
|
||||
// Skip earpiece when in video call mode.
|
||||
if (hasHeadphones && device.equals(DEVICE_EARPIECE)) {
|
||||
// Skip earpiece when headphones are plugged in.
|
||||
continue;
|
||||
}
|
||||
devices.pushString(device);
|
||||
WritableMap deviceInfo = Arguments.createMap();
|
||||
deviceInfo.putString("type", device);
|
||||
deviceInfo.putBoolean("selected", device.equals(selectedDevice));
|
||||
data.pushMap(deviceInfo);
|
||||
}
|
||||
map.putArray("devices", devices);
|
||||
|
||||
promise.resolve(map);
|
||||
ReactInstanceManagerHolder.emitEvent(DEVICE_CHANGE_EVENT, data);
|
||||
Log.i(TAG, "Updating audio device list");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -584,7 +585,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
return;
|
||||
}
|
||||
|
||||
Runnable r = new Runnable() {
|
||||
runInAudioThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean success;
|
||||
|
@ -607,8 +608,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
"Failed to set audio mode to " + mode);
|
||||
}
|
||||
}
|
||||
};
|
||||
runInAudioThread(r);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -690,6 +690,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
selectedDevice = null;
|
||||
userSelectedDevice = null;
|
||||
|
||||
notifyDevicesChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -708,7 +709,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
}
|
||||
|
||||
boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
|
||||
boolean earpieceAvailable = availableDevices.contains(DEVICE_EARPIECE);
|
||||
boolean headsetAvailable = availableDevices.contains(DEVICE_HEADPHONES);
|
||||
|
||||
// Pick the desired device based on what's available and the mode.
|
||||
|
@ -717,8 +717,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
audioDevice = DEVICE_BLUETOOTH;
|
||||
} else if (headsetAvailable) {
|
||||
audioDevice = DEVICE_HEADPHONES;
|
||||
} else if (mode == AUDIO_CALL && earpieceAvailable) {
|
||||
audioDevice = DEVICE_EARPIECE;
|
||||
} else {
|
||||
audioDevice = DEVICE_SPEAKER;
|
||||
}
|
||||
|
@ -744,6 +742,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
|||
setAudioRoutePreO(audioDevice);
|
||||
}
|
||||
|
||||
notifyDevicesChanged();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */; };
|
||||
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */; };
|
||||
0B49424520AD8DBD00BD2DE0 /* outgoingStart.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424320AD8DBD00BD2DE0 /* outgoingStart.wav */; };
|
||||
0B49424620AD8DBD00BD2DE0 /* outgoingRinging.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */; };
|
||||
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
|
||||
|
@ -59,7 +58,6 @@
|
|||
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JitsiMeetView.h; sourceTree = "<group>"; };
|
||||
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetView.m; sourceTree = "<group>"; };
|
||||
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewDelegate.h; sourceTree = "<group>"; };
|
||||
0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPVolumeViewManager.m; sourceTree = "<group>"; };
|
||||
0B49424320AD8DBD00BD2DE0 /* outgoingStart.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = outgoingStart.wav; path = ../../sounds/outgoingStart.wav; sourceTree = "<group>"; };
|
||||
0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = outgoingRinging.wav; path = ../../sounds/outgoingRinging.wav; sourceTree = "<group>"; };
|
||||
0B93EF7A1EC608550030D24D /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; };
|
||||
|
@ -195,7 +193,6 @@
|
|||
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
|
||||
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
|
||||
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
|
||||
0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */,
|
||||
C6A3426B204F127900E062DD /* picture-in-picture */,
|
||||
0BCA495D1EC4B6C600B793EE /* POSIX.m */,
|
||||
0BCA495E1EC4B6C600B793EE /* Proximity.m */,
|
||||
|
@ -509,7 +506,6 @@
|
|||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
|
||||
DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */,
|
||||
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
|
||||
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
|
||||
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
|
||||
A480429C21EE335600289B73 /* AmplitudeModule.m in Sources */,
|
||||
C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */,
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
||||
* Copyright @ 2017-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -17,17 +16,30 @@
|
|||
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
#import <React/RCTLog.h>
|
||||
#import <WebRTC/WebRTC.h>
|
||||
|
||||
|
||||
// Audio mode
|
||||
typedef enum {
|
||||
kAudioModeDefault,
|
||||
kAudioModeAudioCall,
|
||||
kAudioModeVideoCall
|
||||
} JitsiMeetAudioMode;
|
||||
|
||||
@interface AudioMode : NSObject<RCTBridgeModule, RTCAudioSessionDelegate>
|
||||
// Events
|
||||
static NSString * const kDevicesChanged = @"org.jitsi.meet:features/audio-mode#devices-update";
|
||||
|
||||
// Device types (must match JS and Java)
|
||||
static NSString * const kDeviceTypeHeadphones = @"HEADPHONES";
|
||||
static NSString * const kDeviceTypeBluetooth = @"BLUETOOTH";
|
||||
static NSString * const kDeviceTypeEarpiece = @"EARPIECE";
|
||||
static NSString * const kDeviceTypeSpeaker = @"SPEAKER";
|
||||
static NSString * const kDeviceTypeUnknown = @"UNKNOWN";
|
||||
|
||||
|
||||
@interface AudioMode : RCTEventEmitter<RTCAudioSessionDelegate>
|
||||
|
||||
@property(nonatomic, strong) dispatch_queue_t workerQueue;
|
||||
|
||||
|
@ -38,6 +50,11 @@ typedef enum {
|
|||
RTCAudioSessionConfiguration *defaultConfig;
|
||||
RTCAudioSessionConfiguration *audioCallConfig;
|
||||
RTCAudioSessionConfiguration *videoCallConfig;
|
||||
RTCAudioSessionConfiguration *earpieceConfig;
|
||||
BOOL forceSpeaker;
|
||||
BOOL forceEarpiece;
|
||||
BOOL isSpeakerOn;
|
||||
BOOL isEarpieceOn;
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
@ -46,8 +63,13 @@ RCT_EXPORT_MODULE();
|
|||
return NO;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[ kDevicesChanged ];
|
||||
}
|
||||
|
||||
- (NSDictionary *)constantsToExport {
|
||||
return @{
|
||||
@"DEVICE_CHANGE_EVENT": kDevicesChanged,
|
||||
@"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall],
|
||||
@"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault],
|
||||
@"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall]
|
||||
|
@ -58,8 +80,7 @@ RCT_EXPORT_MODULE();
|
|||
self = [super init];
|
||||
if (self) {
|
||||
dispatch_queue_attr_t attributes =
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,
|
||||
QOS_CLASS_USER_INITIATED, -1);
|
||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
|
||||
_workerQueue = dispatch_queue_create("AudioMode.queue", attributes);
|
||||
|
||||
activeMode = kAudioModeDefault;
|
||||
|
@ -71,7 +92,7 @@ RCT_EXPORT_MODULE();
|
|||
|
||||
audioCallConfig = [[RTCAudioSessionConfiguration alloc] init];
|
||||
audioCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
|
||||
audioCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
|
||||
audioCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker;
|
||||
audioCallConfig.mode = AVAudioSessionModeVoiceChat;
|
||||
|
||||
videoCallConfig = [[RTCAudioSessionConfiguration alloc] init];
|
||||
|
@ -79,6 +100,17 @@ RCT_EXPORT_MODULE();
|
|||
videoCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
|
||||
videoCallConfig.mode = AVAudioSessionModeVideoChat;
|
||||
|
||||
// Manually routing audio to the earpiece doesn't quite work unless one disables BT (weird, I know).
|
||||
earpieceConfig = [[RTCAudioSessionConfiguration alloc] init];
|
||||
earpieceConfig.category = AVAudioSessionCategoryPlayAndRecord;
|
||||
earpieceConfig.categoryOptions = 0;
|
||||
earpieceConfig.mode = AVAudioSessionModeVoiceChat;
|
||||
|
||||
forceSpeaker = NO;
|
||||
forceEarpiece = NO;
|
||||
isSpeakerOn = NO;
|
||||
isEarpieceOn = NO;
|
||||
|
||||
RTCAudioSession *session = [RTCAudioSession sharedInstance];
|
||||
[session addDelegate:self];
|
||||
}
|
||||
|
@ -107,24 +139,20 @@ RCT_EXPORT_MODULE();
|
|||
RCT_EXPORT_METHOD(setMode:(int)mode
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
RTCAudioSessionConfiguration *config;
|
||||
RTCAudioSessionConfiguration *config = [self configForMode:mode];
|
||||
NSError *error;
|
||||
|
||||
switch (mode) {
|
||||
case kAudioModeAudioCall:
|
||||
config = audioCallConfig;
|
||||
break;
|
||||
case kAudioModeDefault:
|
||||
config = defaultConfig;
|
||||
break;
|
||||
case kAudioModeVideoCall:
|
||||
config = videoCallConfig;
|
||||
break;
|
||||
default:
|
||||
if (config == nil) {
|
||||
reject(@"setMode", @"Invalid mode", nil);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset.
|
||||
if (mode == kAudioModeDefault) {
|
||||
forceSpeaker = NO;
|
||||
forceEarpiece = NO;
|
||||
}
|
||||
|
||||
activeMode = mode;
|
||||
|
||||
if ([self setConfig:config error:&error]) {
|
||||
|
@ -132,6 +160,76 @@ RCT_EXPORT_METHOD(setMode:(int)mode
|
|||
} else {
|
||||
reject(@"setMode", error.localizedDescription, error);
|
||||
}
|
||||
|
||||
[self notifyDevicesChanged];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setAudioDevice:(NSString *)device
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
NSLog(@"[AudioMode] Selected device: %@", device);
|
||||
|
||||
RTCAudioSession *session = [RTCAudioSession sharedInstance];
|
||||
[session lockForConfiguration];
|
||||
BOOL success;
|
||||
NSError *error = nil;
|
||||
|
||||
// Reset these, as we are about to compute them.
|
||||
forceSpeaker = NO;
|
||||
forceEarpiece = NO;
|
||||
|
||||
// The speaker is special, so test for it first.
|
||||
if ([device isEqualToString:kDeviceTypeSpeaker]) {
|
||||
forceSpeaker = NO;
|
||||
success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
|
||||
} else {
|
||||
// Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
|
||||
AVAudioSession *_session = [AVAudioSession sharedInstance];
|
||||
AVAudioSessionPortDescription *port = nil;
|
||||
|
||||
// Find the matching input device.
|
||||
for (AVAudioSessionPortDescription *portDesc in _session.availableInputs) {
|
||||
if ([portDesc.UID isEqualToString:device]) {
|
||||
port = portDesc;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (port != nil) {
|
||||
// First remove the override if we are going to select a different device.
|
||||
if (isSpeakerOn) {
|
||||
[session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
|
||||
}
|
||||
|
||||
// Special case for the earpiece.
|
||||
if ([port.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
|
||||
forceEarpiece = YES;
|
||||
[self setConfig:earpieceConfig error:nil];
|
||||
} else if (isEarpieceOn) {
|
||||
// Reset the config.
|
||||
RTCAudioSessionConfiguration *config = [self configForMode:activeMode];
|
||||
[self setConfig:config error:nil];
|
||||
}
|
||||
|
||||
// Select our preferred input.
|
||||
success = [session setPreferredInput:port error:&error];
|
||||
} else {
|
||||
success = NO;
|
||||
error = RCTErrorWithMessage(@"Could not find audio device");
|
||||
}
|
||||
}
|
||||
|
||||
[session unlockForConfiguration];
|
||||
|
||||
if (success) {
|
||||
resolve(nil);
|
||||
} else {
|
||||
reject(@"setAudioDevice", error != nil ? error.localizedDescription : @"", error);
|
||||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(updateDeviceList) {
|
||||
[self notifyDevicesChanged];
|
||||
}
|
||||
|
||||
#pragma mark - RTCAudioSessionDelegate
|
||||
|
@ -139,26 +237,141 @@ RCT_EXPORT_METHOD(setMode:(int)mode
|
|||
- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
|
||||
reason:(AVAudioSessionRouteChangeReason)reason
|
||||
previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
|
||||
if (reason == AVAudioSessionRouteChangeReasonCategoryChange) {
|
||||
// The category has changed. Check if it's the one we want and adjust as
|
||||
// needed. This notification is posted on a secondary thread, so make
|
||||
// sure we switch to our worker thread.
|
||||
dispatch_async(_workerQueue, ^{
|
||||
// We don't want to touch the category when in default mode.
|
||||
// This is to play well with other components which could be integrated
|
||||
// into the final application.
|
||||
if (self->activeMode != kAudioModeDefault) {
|
||||
NSLog(@"Audio route changed, reapplying RTCAudioSession config");
|
||||
RTCAudioSessionConfiguration *config
|
||||
= self->activeMode == kAudioModeAudioCall ? self->audioCallConfig : self->videoCallConfig;
|
||||
[self setConfig:config error:nil];
|
||||
// Update JS about the changes.
|
||||
[self notifyDevicesChanged];
|
||||
|
||||
dispatch_async(_workerQueue, ^{
|
||||
switch (reason) {
|
||||
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
|
||||
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
|
||||
// If the device list changed, reset our overrides.
|
||||
self->forceSpeaker = NO;
|
||||
self->forceEarpiece = NO;
|
||||
break;
|
||||
case AVAudioSessionRouteChangeReasonCategoryChange:
|
||||
// The category has changed. Check if it's the one we want and adjust as
|
||||
// needed.
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't want to touch the category when in default mode.
|
||||
// This is to play well with other components which could be integrated
|
||||
// into the final application.
|
||||
if (self->activeMode != kAudioModeDefault) {
|
||||
NSLog(@"[AudioMode] Route changed, reapplying RTCAudioSession config");
|
||||
RTCAudioSessionConfiguration *config = [self configForMode:self->activeMode];
|
||||
[self setConfig:config error:nil];
|
||||
if (self->forceSpeaker && !self->isSpeakerOn) {
|
||||
RTCAudioSession *session = [RTCAudioSession sharedInstance];
|
||||
[session lockForConfiguration];
|
||||
[session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
|
||||
[session unlockForConfiguration];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)audioSession:(RTCAudioSession *)audioSession didSetActive:(BOOL)active {
|
||||
NSLog(@"[AudioMode] Audio session didSetActive:%d", active);
|
||||
}
|
||||
|
||||
#pragma mark - Helper methods
|
||||
|
||||
- (RTCAudioSessionConfiguration *)configForMode:(int) mode {
|
||||
if (mode != kAudioModeDefault && forceEarpiece) {
|
||||
return earpieceConfig;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case kAudioModeAudioCall:
|
||||
return audioCallConfig;
|
||||
case kAudioModeDefault:
|
||||
return defaultConfig;
|
||||
case kAudioModeVideoCall:
|
||||
return videoCallConfig;
|
||||
default:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// Here we convert input and output port types into a single type.
|
||||
- (NSString *)portTypeToString:(AVAudioSessionPort) portType {
|
||||
if ([portType isEqualToString:AVAudioSessionPortHeadphones]
|
||||
|| [portType isEqualToString:AVAudioSessionPortHeadsetMic]) {
|
||||
return kDeviceTypeHeadphones;
|
||||
} else if ([portType isEqualToString:AVAudioSessionPortBuiltInMic]
|
||||
|| [portType isEqualToString:AVAudioSessionPortBuiltInReceiver]) {
|
||||
return kDeviceTypeEarpiece;
|
||||
} else if ([portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
|
||||
return kDeviceTypeSpeaker;
|
||||
} else if ([portType isEqualToString:AVAudioSessionPortBluetoothHFP]
|
||||
|| [portType isEqualToString:AVAudioSessionPortBluetoothLE]
|
||||
|| [portType isEqualToString:AVAudioSessionPortBluetoothA2DP]) {
|
||||
return kDeviceTypeBluetooth;
|
||||
} else {
|
||||
return kDeviceTypeUnknown;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)notifyDevicesChanged {
|
||||
dispatch_async(_workerQueue, ^{
|
||||
NSMutableArray *data = [[NSMutableArray alloc] init];
|
||||
// Here we use AVAudioSession because RTCAudioSession doesn't expose availableInputs.
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
NSString *currentPort = @"";
|
||||
AVAudioSessionRouteDescription *currentRoute = session.currentRoute;
|
||||
|
||||
// Check what the current device is. Because the speaker is somewhat special, we need to
|
||||
// check for it first.
|
||||
if (currentRoute != nil) {
|
||||
AVAudioSessionPortDescription *output = currentRoute.outputs.firstObject;
|
||||
AVAudioSessionPortDescription *input = currentRoute.inputs.firstObject;
|
||||
if (output != nil && [output.portType isEqualToString:AVAudioSessionPortBuiltInSpeaker]) {
|
||||
currentPort = kDeviceTypeSpeaker;
|
||||
self->isSpeakerOn = YES;
|
||||
} else if (input != nil) {
|
||||
currentPort = input.UID;
|
||||
self->isSpeakerOn = NO;
|
||||
self->isEarpieceOn = [input.portType isEqualToString:AVAudioSessionPortBuiltInMic];
|
||||
}
|
||||
}
|
||||
|
||||
BOOL headphonesAvailable = NO;
|
||||
for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
|
||||
if ([portDesc.portType isEqualToString:AVAudioSessionPortHeadsetMic] || [portDesc.portType isEqualToString:AVAudioSessionPortHeadphones]) {
|
||||
headphonesAvailable = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (AVAudioSessionPortDescription *portDesc in session.availableInputs) {
|
||||
// Skip "Phone" if headphones are present.
|
||||
if (headphonesAvailable && [portDesc.portType isEqualToString:AVAudioSessionPortBuiltInMic]) {
|
||||
continue;
|
||||
}
|
||||
id deviceData
|
||||
= @{
|
||||
@"type": [self portTypeToString:portDesc.portType],
|
||||
@"name": portDesc.portName,
|
||||
@"uid": portDesc.UID,
|
||||
@"selected": [NSNumber numberWithBool:[portDesc.UID isEqualToString:currentPort]]
|
||||
};
|
||||
[data addObject:deviceData];
|
||||
}
|
||||
|
||||
// We need to manually add the speaker because it will never show up in the
|
||||
// previous list, as it's not an input.
|
||||
[data addObject:
|
||||
@{ @"type": kDeviceTypeSpeaker,
|
||||
@"name": @"Speaker",
|
||||
@"uid": kDeviceTypeSpeaker,
|
||||
@"selected": [NSNumber numberWithBool:[kDeviceTypeSpeaker isEqualToString:currentPort]]
|
||||
}];
|
||||
|
||||
[self sendEventWithName:kDevicesChanged body:data];
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTUIManager.h>
|
||||
#import <React/RCTViewManager.h>
|
||||
|
||||
@import MediaPlayer;
|
||||
|
||||
|
||||
@interface MPVolumeViewManager : RCTViewManager
|
||||
@end
|
||||
|
||||
@implementation MPVolumeViewManager
|
||||
|
||||
RCT_EXPORT_MODULE()
|
||||
|
||||
- (UIView *)view {
|
||||
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
|
||||
volumeView.showsRouteButton = YES;
|
||||
volumeView.showsVolumeSlider = NO;
|
||||
|
||||
return (UIView *) volumeView;
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(show:(nonnull NSNumber *)reactTag) {
|
||||
[self.bridge.uiManager addUIBlock:^(
|
||||
__unused RCTUIManager *uiManager,
|
||||
NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
id view = viewRegistry[reactTag];
|
||||
if (![view isKindOfClass:[MPVolumeView class]]) {
|
||||
RCTLogError(@"Invalid view returned from registry, expecting \
|
||||
MPVolumeView, got: %@", view);
|
||||
} else {
|
||||
// Simulate a click
|
||||
UIButton *btn = nil;
|
||||
for (UIView *buttonView in ((UIView *) view).subviews) {
|
||||
if ([buttonView isKindOfClass:[UIButton class]]) {
|
||||
btn = (UIButton *) buttonView;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (btn != nil) {
|
||||
[btn sendActionsForControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
|
@ -21,7 +21,8 @@
|
|||
"bluetooth": "Bluetooth",
|
||||
"headphones": "Headphones",
|
||||
"phone": "Phone",
|
||||
"speaker": "Speaker"
|
||||
"speaker": "Speaker",
|
||||
"none": "No audio devices available"
|
||||
},
|
||||
"audioOnly": {
|
||||
"audioOnly": "Low bandwidth"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* The type of redux action to set Audio Mode device list.
|
||||
*
|
||||
* {
|
||||
* type: _SET_AUDIOMODE_DEVICES,
|
||||
* devices: Array
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _SET_AUDIOMODE_DEVICES = '_SET_AUDIOMODE_DEVICES';
|
||||
|
||||
/**
|
||||
* The type of redux action to set Audio Mode module's subscriptions.
|
||||
*
|
||||
* {
|
||||
* type: _SET_AUDIOMODE_SUBSCRIPTIONS,
|
||||
* subscriptions: Array|undefined
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _SET_AUDIOMODE_SUBSCRIPTIONS = '_SET_AUDIOMODE_SUBSCRIPTIONS';
|
|
@ -1,13 +1,5 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
findNodeHandle,
|
||||
NativeModules,
|
||||
requireNativeComponent,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { connect } from '../../../base/redux';
|
||||
|
@ -16,19 +8,6 @@ import type { AbstractButtonProps } from '../../../base/toolbox';
|
|||
|
||||
import AudioRoutePickerDialog from './AudioRoutePickerDialog';
|
||||
|
||||
/**
|
||||
* The {@code MPVolumeView} React {@code Component}. It will only be available
|
||||
* on iOS.
|
||||
*/
|
||||
const MPVolumeView
|
||||
= NativeModules.MPVolumeViewManager
|
||||
&& requireNativeComponent('MPVolumeView');
|
||||
|
||||
/**
|
||||
* The style required to hide the {@code MPVolumeView}, since it's displayed
|
||||
* programmatically.
|
||||
*/
|
||||
const HIDE_VIEW_STYLE = { display: 'none' };
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
|
||||
|
@ -47,30 +26,6 @@ class AudioRouteButton extends AbstractButton<Props, *> {
|
|||
iconName = 'icon-volume';
|
||||
label = 'toolbar.audioRoute';
|
||||
|
||||
_volumeComponent: ?Object;
|
||||
|
||||
/**
|
||||
* Initializes a new {@code AudioRouteButton} instance.
|
||||
*
|
||||
* @param {Props} props - The React {@code Component} props to initialize
|
||||
* the new {@code AudioRouteButton} instance with.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
/**
|
||||
* The internal reference to the React {@code MPVolumeView} for
|
||||
* showing the volume control view.
|
||||
*
|
||||
* @private
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
this._volumeComponent = null;
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._setVolumeComponent = this._setVolumeComponent.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
||||
*
|
||||
|
@ -78,52 +33,7 @@ class AudioRouteButton extends AbstractButton<Props, *> {
|
|||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
if (MPVolumeView) {
|
||||
NativeModules.MPVolumeViewManager.show(
|
||||
findNodeHandle(this._volumeComponent));
|
||||
} else if (AudioRoutePickerDialog) {
|
||||
this.props.dispatch(openDialog(AudioRoutePickerDialog));
|
||||
}
|
||||
}
|
||||
|
||||
_setVolumeComponent: (?Object) => void;
|
||||
|
||||
/**
|
||||
* Sets the internal reference to the React Component wrapping the
|
||||
* {@code MPVolumeView} component.
|
||||
*
|
||||
* @param {ReactElement} component - React Component.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setVolumeComponent(component) {
|
||||
this._volumeComponent = component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
render() {
|
||||
if (!MPVolumeView && !AudioRoutePickerDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const element = super.render();
|
||||
|
||||
return (
|
||||
<View>
|
||||
{ element }
|
||||
{
|
||||
MPVolumeView
|
||||
&& <MPVolumeView
|
||||
ref = { this._setVolumeComponent }
|
||||
style = { HIDE_VIEW_STYLE } />
|
||||
}
|
||||
</View>
|
||||
);
|
||||
this.props.dispatch(openDialog(AudioRoutePickerDialog));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ import { ColorPalette, type StyleType } from '../../../base/styles';
|
|||
|
||||
import styles from './styles';
|
||||
|
||||
const { AudioMode } = NativeModules;
|
||||
|
||||
/**
|
||||
* Type definition for a single entry in the device list.
|
||||
*/
|
||||
|
@ -37,7 +39,38 @@ type Device = {
|
|||
/**
|
||||
* Device type.
|
||||
*/
|
||||
type: string
|
||||
type: string,
|
||||
|
||||
/**
|
||||
* Unique device ID.
|
||||
*/
|
||||
uid: ?string
|
||||
};
|
||||
|
||||
/**
|
||||
* "Raw" device, as returned by native.
|
||||
*/
|
||||
type RawDevice = {
|
||||
|
||||
/**
|
||||
* Display name for the device.
|
||||
*/
|
||||
name: ?string,
|
||||
|
||||
/**
|
||||
* is this device selected?
|
||||
*/
|
||||
selected: boolean,
|
||||
|
||||
/**
|
||||
* Device type.
|
||||
*/
|
||||
type: string,
|
||||
|
||||
/**
|
||||
* Unique device ID.
|
||||
*/
|
||||
uid: ?string
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -50,6 +83,11 @@ type Props = {
|
|||
*/
|
||||
_bottomSheetStyles: StyleType,
|
||||
|
||||
/**
|
||||
* Object describing available devices.
|
||||
*/
|
||||
_devices: Array<RawDevice>,
|
||||
|
||||
/**
|
||||
* Used for hiding the dialog when the selection was completed.
|
||||
*/
|
||||
|
@ -72,8 +110,6 @@ type State = {
|
|||
devices: Array<Device>
|
||||
};
|
||||
|
||||
const { AudioMode } = NativeModules;
|
||||
|
||||
/**
|
||||
* Maps each device type to a display name and icon.
|
||||
*/
|
||||
|
@ -101,11 +137,9 @@ const deviceInfoMap = {
|
|||
};
|
||||
|
||||
/**
|
||||
* The exported React {@code Component}. {@code AudioRoutePickerDialog} is
|
||||
* exported only if the {@code AudioMode} module has the capability to get / set
|
||||
* audio devices.
|
||||
* The exported React {@code Component}.
|
||||
*/
|
||||
let AudioRoutePickerDialog_;
|
||||
let AudioRoutePickerDialog_; // eslint-disable-line prefer-const
|
||||
|
||||
/**
|
||||
* Implements a React {@code Component} which prompts the user when a password
|
||||
|
@ -115,11 +149,47 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
state = {
|
||||
/**
|
||||
* Available audio devices, it will be set in
|
||||
* {@link #componentDidMount()}.
|
||||
* {@link #getDerivedStateFromProps()}.
|
||||
*/
|
||||
devices: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#getDerivedStateFromProps()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
static getDerivedStateFromProps(props: Props) {
|
||||
const { _devices: devices } = props;
|
||||
|
||||
if (!devices) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const audioDevices = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const infoMap = deviceInfoMap[device.type];
|
||||
const text = device.type === 'BLUETOOTH' && device.name ? device.name : infoMap.text;
|
||||
|
||||
if (infoMap) {
|
||||
const info = {
|
||||
...infoMap,
|
||||
selected: Boolean(device.selected),
|
||||
text: props.t(text),
|
||||
uid: device.uid
|
||||
};
|
||||
|
||||
audioDevices.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure devices is alphabetically sorted.
|
||||
return {
|
||||
devices: _.sortBy(audioDevices, 'text')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new {@code PasswordRequiredPrompt} instance.
|
||||
*
|
||||
|
@ -131,36 +201,9 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the device list by querying {@code AudioMode}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
AudioMode.getAudioDevices().then(({ devices, selected }) => {
|
||||
const audioDevices = [];
|
||||
|
||||
if (devices) {
|
||||
for (const device of devices) {
|
||||
if (deviceInfoMap[device]) {
|
||||
const info = Object.assign({}, deviceInfoMap[device]);
|
||||
|
||||
info.selected = device === selected;
|
||||
info.text = this.props.t(info.text);
|
||||
audioDevices.push(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (audioDevices) {
|
||||
// Make sure devices is alphabetically sorted.
|
||||
this.setState({
|
||||
devices: _.sortBy(audioDevices, 'text')
|
||||
});
|
||||
}
|
||||
});
|
||||
// Trigger an initial update.
|
||||
AudioMode.updateDeviceList && AudioMode.updateDeviceList();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -197,7 +240,7 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
_onSelectDeviceFn(device: Device) {
|
||||
return () => {
|
||||
this._hide();
|
||||
AudioMode.setAudioDevice(device.type);
|
||||
AudioMode.setAudioDevice(device.uid || device.type);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -230,6 +273,27 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a "fake" device row indicating there are no devices.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderNoDevices() {
|
||||
const { _bottomSheetStyles, t } = this.props;
|
||||
|
||||
return (
|
||||
<View style = { styles.deviceRow } >
|
||||
<Icon
|
||||
name = { deviceInfoMap.SPEAKER.iconName }
|
||||
style = { [ styles.deviceIcon, _bottomSheetStyles.iconStyle ] } />
|
||||
<Text style = { [ styles.deviceText, _bottomSheetStyles.labelStyle ] } >
|
||||
{ t('audioDevices.none') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
|
@ -238,14 +302,17 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
*/
|
||||
render() {
|
||||
const { devices } = this.state;
|
||||
let content;
|
||||
|
||||
if (!devices.length) {
|
||||
return null;
|
||||
if (devices.length === 0) {
|
||||
content = this._renderNoDevices();
|
||||
} else {
|
||||
content = this.state.devices.map(this._renderDevice, this);
|
||||
}
|
||||
|
||||
return (
|
||||
<BottomSheet onCancel = { this._onCancel }>
|
||||
{ this.state.devices.map(this._renderDevice, this) }
|
||||
{ content }
|
||||
</BottomSheet>
|
||||
);
|
||||
}
|
||||
|
@ -259,14 +326,11 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
|||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet')
|
||||
_bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet'),
|
||||
_devices: state['features/mobile/audio-mode'].devices
|
||||
};
|
||||
}
|
||||
|
||||
// Only export the dialog if we have support for getting / setting audio devices
|
||||
// in AudioMode.
|
||||
if (AudioMode.getAudioDevices && AudioMode.setAudioDevice) {
|
||||
AudioRoutePickerDialog_ = translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
|
||||
}
|
||||
AudioRoutePickerDialog_ = translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
|
||||
|
||||
export default AudioRoutePickerDialog_;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export * from './components';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// @flow
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||
|
||||
import { SET_AUDIO_ONLY } from '../../base/audio-only';
|
||||
import { APP_WILL_MOUNT } from '../../base/app';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_LEFT,
|
||||
|
@ -12,7 +12,10 @@ import {
|
|||
} from '../../base/conference';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
|
||||
import { _SET_AUDIOMODE_DEVICES, _SET_AUDIOMODE_SUBSCRIPTIONS } from './actionTypes';
|
||||
|
||||
const { AudioMode } = NativeModules;
|
||||
const AudioModeEmitter = new NativeEventEmitter(AudioMode);
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
|
@ -23,55 +26,127 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
|
|||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(({ getState }) => next => action => {
|
||||
const result = next(action);
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
/* eslint-disable no-fallthrough */
|
||||
|
||||
if (AudioMode) {
|
||||
let mode;
|
||||
switch (action.type) {
|
||||
case _SET_AUDIOMODE_SUBSCRIPTIONS:
|
||||
_setSubscriptions(store);
|
||||
break;
|
||||
case APP_WILL_UNMOUNT: {
|
||||
store.dispatch({
|
||||
type: _SET_AUDIOMODE_SUBSCRIPTIONS,
|
||||
subscriptions: undefined
|
||||
});
|
||||
break;
|
||||
}
|
||||
case APP_WILL_MOUNT:
|
||||
_appWillMount(store);
|
||||
case CONFERENCE_FAILED: // eslint-disable-line no-fallthrough
|
||||
case CONFERENCE_LEFT:
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
case CONFERENCE_FAILED:
|
||||
case CONFERENCE_LEFT: {
|
||||
const conference = getCurrentConference(getState());
|
||||
/*
|
||||
* NOTE: We moved the audio mode setting from CONFERENCE_WILL_JOIN to
|
||||
* CONFERENCE_JOINED because in case of a locked room, the app goes
|
||||
* through CONFERENCE_FAILED state and gets to CONFERENCE_JOINED only
|
||||
* after a correct password, so we want to make sure we have the correct
|
||||
* audio mode set up when we finally get to the conf, but also make sure
|
||||
* that the app is in the right audio mode if the user leaves the
|
||||
* conference after the password prompt appears.
|
||||
*/
|
||||
case CONFERENCE_JOINED:
|
||||
case SET_AUDIO_ONLY:
|
||||
return _updateAudioMode(store, next, action);
|
||||
|
||||
if (typeof conference === 'undefined') {
|
||||
mode = AudioMode.DEFAULT;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE: We moved the audio mode setting from CONFERENCE_WILL_JOIN to
|
||||
* CONFERENCE_JOINED because in case of a locked room, the app goes
|
||||
* through CONFERENCE_FAILED state and gets to CONFERENCE_JOINED only
|
||||
* after a correct password, so we want to make sure we have the correct
|
||||
* audio mode set up when we finally get to the conf, but also make sure
|
||||
* that the app is in the right audio mode if the user leaves the
|
||||
* conference after the password prompt appears.
|
||||
*/
|
||||
case CONFERENCE_JOINED:
|
||||
case SET_AUDIO_ONLY: {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
|
||||
conference
|
||||
&& (mode = audioOnly
|
||||
? AudioMode.AUDIO_CALL
|
||||
: AudioMode.VIDEO_CALL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof mode !== 'undefined') {
|
||||
AudioMode.setMode(mode)
|
||||
.catch(err =>
|
||||
logger.error(
|
||||
`Failed to set audio mode ${String(mode)}: ${err}`));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
/* eslint-enable no-fallthrough */
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Notifies this feature that the action {@link APP_WILL_MOUNT} is being
|
||||
* dispatched within a specific redux {@code store}.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _appWillMount(store) {
|
||||
const subscriptions = [
|
||||
AudioModeEmitter.addListener(AudioMode.DEVICE_CHANGE_EVENT, _onDevicesUpdate, store)
|
||||
];
|
||||
|
||||
store.dispatch({
|
||||
type: _SET_AUDIOMODE_SUBSCRIPTIONS,
|
||||
subscriptions
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles audio device changes. The list will be stored on the redux store.
|
||||
*
|
||||
* @param {Object} devices - The current list of devices.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onDevicesUpdate(devices) {
|
||||
const { dispatch } = this; // eslint-disable-line no-invalid-this
|
||||
|
||||
dispatch({
|
||||
type: _SET_AUDIOMODE_DEVICES,
|
||||
devices
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this feature that the action
|
||||
* {@link _SET_AUDIOMODE_SUBSCRIPTIONS} is being dispatched within
|
||||
* a specific redux {@code store}.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _setSubscriptions({ getState }) {
|
||||
const { subscriptions } = getState()['features/mobile/audio-mode'];
|
||||
|
||||
if (subscriptions) {
|
||||
for (const subscription of subscriptions) {
|
||||
subscription.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the audio mode based on the current (redux) state.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} in the specified {@code store}.
|
||||
* @param {Action} action - The redux action which is
|
||||
* being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _updateAudioMode({ getState }, next, action) {
|
||||
const result = next(action);
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const { enabled: audioOnly } = state['features/base/audio-only'];
|
||||
let mode;
|
||||
|
||||
if (conference) {
|
||||
mode = audioOnly ? AudioMode.AUDIO_CALL : AudioMode.VIDEO_CALL;
|
||||
} else {
|
||||
mode = AudioMode.DEFAULT;
|
||||
}
|
||||
|
||||
AudioMode.setMode(mode).catch(err => logger.error(`Failed to set audio mode ${String(mode)}: ${err}`));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// @flow
|
||||
|
||||
import { equals, set, ReducerRegistry } from '../../base/redux';
|
||||
|
||||
import { _SET_AUDIOMODE_DEVICES, _SET_AUDIOMODE_SUBSCRIPTIONS } from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
devices: [],
|
||||
subscriptions: []
|
||||
};
|
||||
|
||||
ReducerRegistry.register('features/mobile/audio-mode', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case _SET_AUDIOMODE_DEVICES: {
|
||||
const { devices } = action;
|
||||
|
||||
if (equals(state.devices, devices)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return set(state, 'devices', devices);
|
||||
}
|
||||
case _SET_AUDIOMODE_SUBSCRIPTIONS:
|
||||
return set(state, 'subscriptions', action.subscriptions);
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
Loading…
Reference in New Issue