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_HEADPHONES = "HEADPHONES";
|
||||||
private static final String DEVICE_SPEAKER = "SPEAKER";
|
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.
|
* List of currently available audio devices.
|
||||||
*/
|
*/
|
||||||
|
@ -303,7 +308,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
// Do an initial detection on Android >= M.
|
// Do an initial detection on Android >= M.
|
||||||
runInAudioThread(onAudioDeviceChangeRunner);
|
onAudioDeviceChange();
|
||||||
} else {
|
} else {
|
||||||
// On Android < M, detect if we have an earpiece.
|
// On Android < M, detect if we have an earpiece.
|
||||||
PackageManager pm = reactContext.getPackageManager();
|
PackageManager pm = reactContext.getPackageManager();
|
||||||
|
@ -327,6 +332,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||||
public Map<String, Object> getConstants() {
|
public Map<String, Object> getConstants() {
|
||||||
Map<String, Object> constants = new HashMap<>();
|
Map<String, Object> constants = new HashMap<>();
|
||||||
|
|
||||||
|
constants.put("DEVICE_CHANGE_EVENT", DEVICE_CHANGE_EVENT);
|
||||||
constants.put("AUDIO_CALL", AUDIO_CALL);
|
constants.put("AUDIO_CALL", AUDIO_CALL);
|
||||||
constants.put("DEFAULT", DEFAULT);
|
constants.put("DEFAULT", DEFAULT);
|
||||||
constants.put("VIDEO_CALL", VIDEO_CALL);
|
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',
|
* Notifies JS land that the devices list has changed.
|
||||||
* '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.
|
|
||||||
*/
|
*/
|
||||||
@ReactMethod
|
private void notifyDevicesChanged() {
|
||||||
public void getAudioDevices(final Promise promise) {
|
|
||||||
runInAudioThread(new Runnable() {
|
runInAudioThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
WritableMap map = Arguments.createMap();
|
WritableArray data = Arguments.createArray();
|
||||||
map.putString("selected", selectedDevice);
|
final boolean hasHeadphones = availableDevices.contains(DEVICE_HEADPHONES);
|
||||||
WritableArray devices = Arguments.createArray();
|
|
||||||
for (String device : availableDevices) {
|
for (String device : availableDevices) {
|
||||||
if (mode == VIDEO_CALL && device.equals(DEVICE_EARPIECE)) {
|
if (hasHeadphones && device.equals(DEVICE_EARPIECE)) {
|
||||||
// Skip earpiece when in video call mode.
|
// Skip earpiece when headphones are plugged in.
|
||||||
continue;
|
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);
|
ReactInstanceManagerHolder.emitEvent(DEVICE_CHANGE_EVENT, data);
|
||||||
|
Log.i(TAG, "Updating audio device list");
|
||||||
promise.resolve(map);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -584,7 +585,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Runnable r = new Runnable() {
|
runInAudioThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean success;
|
boolean success;
|
||||||
|
@ -607,8 +608,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||||
"Failed to set audio mode to " + mode);
|
"Failed to set audio mode to " + mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
runInAudioThread(r);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -690,6 +690,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||||
selectedDevice = null;
|
selectedDevice = null;
|
||||||
userSelectedDevice = null;
|
userSelectedDevice = null;
|
||||||
|
|
||||||
|
notifyDevicesChanged();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -708,7 +709,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
|
boolean bluetoothAvailable = availableDevices.contains(DEVICE_BLUETOOTH);
|
||||||
boolean earpieceAvailable = availableDevices.contains(DEVICE_EARPIECE);
|
|
||||||
boolean headsetAvailable = availableDevices.contains(DEVICE_HEADPHONES);
|
boolean headsetAvailable = availableDevices.contains(DEVICE_HEADPHONES);
|
||||||
|
|
||||||
// Pick the desired device based on what's available and the mode.
|
// Pick the desired device based on what's available and the mode.
|
||||||
|
@ -717,8 +717,6 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||||
audioDevice = DEVICE_BLUETOOTH;
|
audioDevice = DEVICE_BLUETOOTH;
|
||||||
} else if (headsetAvailable) {
|
} else if (headsetAvailable) {
|
||||||
audioDevice = DEVICE_HEADPHONES;
|
audioDevice = DEVICE_HEADPHONES;
|
||||||
} else if (mode == AUDIO_CALL && earpieceAvailable) {
|
|
||||||
audioDevice = DEVICE_EARPIECE;
|
|
||||||
} else {
|
} else {
|
||||||
audioDevice = DEVICE_SPEAKER;
|
audioDevice = DEVICE_SPEAKER;
|
||||||
}
|
}
|
||||||
|
@ -744,6 +742,7 @@ class AudioModeModule extends ReactContextBaseJavaModule
|
||||||
setAudioRoutePreO(audioDevice);
|
setAudioRoutePreO(audioDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyDevicesChanged();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
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 */; };
|
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, ); }; };
|
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 */; };
|
0B49424520AD8DBD00BD2DE0 /* outgoingStart.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424320AD8DBD00BD2DE0 /* outgoingStart.wav */; };
|
||||||
0B49424620AD8DBD00BD2DE0 /* outgoingRinging.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */; };
|
0B49424620AD8DBD00BD2DE0 /* outgoingRinging.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */; };
|
||||||
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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 */,
|
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
|
||||||
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
|
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
|
||||||
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
|
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
|
||||||
0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */,
|
|
||||||
C6A3426B204F127900E062DD /* picture-in-picture */,
|
C6A3426B204F127900E062DD /* picture-in-picture */,
|
||||||
0BCA495D1EC4B6C600B793EE /* POSIX.m */,
|
0BCA495D1EC4B6C600B793EE /* POSIX.m */,
|
||||||
0BCA495E1EC4B6C600B793EE /* Proximity.m */,
|
0BCA495E1EC4B6C600B793EE /* Proximity.m */,
|
||||||
|
@ -509,7 +506,6 @@
|
||||||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
|
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
|
||||||
DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */,
|
DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */,
|
||||||
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
|
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
|
||||||
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
|
|
||||||
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
|
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
|
||||||
A480429C21EE335600289B73 /* AmplitudeModule.m in Sources */,
|
A480429C21EE335600289B73 /* AmplitudeModule.m in Sources */,
|
||||||
C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */,
|
C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright @ 2018-present 8x8, Inc.
|
* Copyright @ 2017-present 8x8, Inc.
|
||||||
* Copyright @ 2017-2018 Atlassian Pty Ltd
|
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -17,17 +16,30 @@
|
||||||
|
|
||||||
#import <AVFoundation/AVFoundation.h>
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
|
||||||
#import <React/RCTBridgeModule.h>
|
#import <React/RCTEventEmitter.h>
|
||||||
#import <React/RCTLog.h>
|
#import <React/RCTLog.h>
|
||||||
#import <WebRTC/WebRTC.h>
|
#import <WebRTC/WebRTC.h>
|
||||||
|
|
||||||
|
|
||||||
|
// Audio mode
|
||||||
typedef enum {
|
typedef enum {
|
||||||
kAudioModeDefault,
|
kAudioModeDefault,
|
||||||
kAudioModeAudioCall,
|
kAudioModeAudioCall,
|
||||||
kAudioModeVideoCall
|
kAudioModeVideoCall
|
||||||
} JitsiMeetAudioMode;
|
} 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;
|
@property(nonatomic, strong) dispatch_queue_t workerQueue;
|
||||||
|
|
||||||
|
@ -38,6 +50,11 @@ typedef enum {
|
||||||
RTCAudioSessionConfiguration *defaultConfig;
|
RTCAudioSessionConfiguration *defaultConfig;
|
||||||
RTCAudioSessionConfiguration *audioCallConfig;
|
RTCAudioSessionConfiguration *audioCallConfig;
|
||||||
RTCAudioSessionConfiguration *videoCallConfig;
|
RTCAudioSessionConfiguration *videoCallConfig;
|
||||||
|
RTCAudioSessionConfiguration *earpieceConfig;
|
||||||
|
BOOL forceSpeaker;
|
||||||
|
BOOL forceEarpiece;
|
||||||
|
BOOL isSpeakerOn;
|
||||||
|
BOOL isEarpieceOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_EXPORT_MODULE();
|
RCT_EXPORT_MODULE();
|
||||||
|
@ -46,8 +63,13 @@ RCT_EXPORT_MODULE();
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSArray<NSString *> *)supportedEvents {
|
||||||
|
return @[ kDevicesChanged ];
|
||||||
|
}
|
||||||
|
|
||||||
- (NSDictionary *)constantsToExport {
|
- (NSDictionary *)constantsToExport {
|
||||||
return @{
|
return @{
|
||||||
|
@"DEVICE_CHANGE_EVENT": kDevicesChanged,
|
||||||
@"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall],
|
@"AUDIO_CALL" : [NSNumber numberWithInt: kAudioModeAudioCall],
|
||||||
@"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault],
|
@"DEFAULT" : [NSNumber numberWithInt: kAudioModeDefault],
|
||||||
@"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall]
|
@"VIDEO_CALL" : [NSNumber numberWithInt: kAudioModeVideoCall]
|
||||||
|
@ -58,8 +80,7 @@ RCT_EXPORT_MODULE();
|
||||||
self = [super init];
|
self = [super init];
|
||||||
if (self) {
|
if (self) {
|
||||||
dispatch_queue_attr_t attributes =
|
dispatch_queue_attr_t attributes =
|
||||||
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL,
|
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
|
||||||
QOS_CLASS_USER_INITIATED, -1);
|
|
||||||
_workerQueue = dispatch_queue_create("AudioMode.queue", attributes);
|
_workerQueue = dispatch_queue_create("AudioMode.queue", attributes);
|
||||||
|
|
||||||
activeMode = kAudioModeDefault;
|
activeMode = kAudioModeDefault;
|
||||||
|
@ -71,7 +92,7 @@ RCT_EXPORT_MODULE();
|
||||||
|
|
||||||
audioCallConfig = [[RTCAudioSessionConfiguration alloc] init];
|
audioCallConfig = [[RTCAudioSessionConfiguration alloc] init];
|
||||||
audioCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
|
audioCallConfig.category = AVAudioSessionCategoryPlayAndRecord;
|
||||||
audioCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
|
audioCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker;
|
||||||
audioCallConfig.mode = AVAudioSessionModeVoiceChat;
|
audioCallConfig.mode = AVAudioSessionModeVoiceChat;
|
||||||
|
|
||||||
videoCallConfig = [[RTCAudioSessionConfiguration alloc] init];
|
videoCallConfig = [[RTCAudioSessionConfiguration alloc] init];
|
||||||
|
@ -79,6 +100,17 @@ RCT_EXPORT_MODULE();
|
||||||
videoCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
|
videoCallConfig.categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth;
|
||||||
videoCallConfig.mode = AVAudioSessionModeVideoChat;
|
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];
|
RTCAudioSession *session = [RTCAudioSession sharedInstance];
|
||||||
[session addDelegate:self];
|
[session addDelegate:self];
|
||||||
}
|
}
|
||||||
|
@ -107,24 +139,20 @@ RCT_EXPORT_MODULE();
|
||||||
RCT_EXPORT_METHOD(setMode:(int)mode
|
RCT_EXPORT_METHOD(setMode:(int)mode
|
||||||
resolve:(RCTPromiseResolveBlock)resolve
|
resolve:(RCTPromiseResolveBlock)resolve
|
||||||
reject:(RCTPromiseRejectBlock)reject) {
|
reject:(RCTPromiseRejectBlock)reject) {
|
||||||
RTCAudioSessionConfiguration *config;
|
RTCAudioSessionConfiguration *config = [self configForMode:mode];
|
||||||
NSError *error;
|
NSError *error;
|
||||||
|
|
||||||
switch (mode) {
|
if (config == nil) {
|
||||||
case kAudioModeAudioCall:
|
|
||||||
config = audioCallConfig;
|
|
||||||
break;
|
|
||||||
case kAudioModeDefault:
|
|
||||||
config = defaultConfig;
|
|
||||||
break;
|
|
||||||
case kAudioModeVideoCall:
|
|
||||||
config = videoCallConfig;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
reject(@"setMode", @"Invalid mode", nil);
|
reject(@"setMode", @"Invalid mode", nil);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset.
|
||||||
|
if (mode == kAudioModeDefault) {
|
||||||
|
forceSpeaker = NO;
|
||||||
|
forceEarpiece = NO;
|
||||||
|
}
|
||||||
|
|
||||||
activeMode = mode;
|
activeMode = mode;
|
||||||
|
|
||||||
if ([self setConfig:config error:&error]) {
|
if ([self setConfig:config error:&error]) {
|
||||||
|
@ -132,6 +160,76 @@ RCT_EXPORT_METHOD(setMode:(int)mode
|
||||||
} else {
|
} else {
|
||||||
reject(@"setMode", error.localizedDescription, error);
|
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
|
#pragma mark - RTCAudioSessionDelegate
|
||||||
|
@ -139,26 +237,141 @@ RCT_EXPORT_METHOD(setMode:(int)mode
|
||||||
- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
|
- (void)audioSessionDidChangeRoute:(RTCAudioSession *)session
|
||||||
reason:(AVAudioSessionRouteChangeReason)reason
|
reason:(AVAudioSessionRouteChangeReason)reason
|
||||||
previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
|
previousRoute:(AVAudioSessionRouteDescription *)previousRoute {
|
||||||
if (reason == AVAudioSessionRouteChangeReasonCategoryChange) {
|
// Update JS about the changes.
|
||||||
// The category has changed. Check if it's the one we want and adjust as
|
[self notifyDevicesChanged];
|
||||||
// needed. This notification is posted on a secondary thread, so make
|
|
||||||
// sure we switch to our worker thread.
|
dispatch_async(_workerQueue, ^{
|
||||||
dispatch_async(_workerQueue, ^{
|
switch (reason) {
|
||||||
// We don't want to touch the category when in default mode.
|
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
|
||||||
// This is to play well with other components which could be integrated
|
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
|
||||||
// into the final application.
|
// If the device list changed, reset our overrides.
|
||||||
if (self->activeMode != kAudioModeDefault) {
|
self->forceSpeaker = NO;
|
||||||
NSLog(@"Audio route changed, reapplying RTCAudioSession config");
|
self->forceEarpiece = NO;
|
||||||
RTCAudioSessionConfiguration *config
|
break;
|
||||||
= self->activeMode == kAudioModeAudioCall ? self->audioCallConfig : self->videoCallConfig;
|
case AVAudioSessionRouteChangeReasonCategoryChange:
|
||||||
[self setConfig:config error:nil];
|
// 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 {
|
- (void)audioSession:(RTCAudioSession *)audioSession didSetActive:(BOOL)active {
|
||||||
NSLog(@"[AudioMode] Audio session didSetActive:%d", 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
|
@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",
|
"bluetooth": "Bluetooth",
|
||||||
"headphones": "Headphones",
|
"headphones": "Headphones",
|
||||||
"phone": "Phone",
|
"phone": "Phone",
|
||||||
"speaker": "Speaker"
|
"speaker": "Speaker",
|
||||||
|
"none": "No audio devices available"
|
||||||
},
|
},
|
||||||
"audioOnly": {
|
"audioOnly": {
|
||||||
"audioOnly": "Low bandwidth"
|
"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
|
// @flow
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
findNodeHandle,
|
|
||||||
NativeModules,
|
|
||||||
requireNativeComponent,
|
|
||||||
View
|
|
||||||
} from 'react-native';
|
|
||||||
|
|
||||||
import { openDialog } from '../../../base/dialog';
|
import { openDialog } from '../../../base/dialog';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
@ -16,19 +8,6 @@ import type { AbstractButtonProps } from '../../../base/toolbox';
|
||||||
|
|
||||||
import AudioRoutePickerDialog from './AudioRoutePickerDialog';
|
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 & {
|
type Props = AbstractButtonProps & {
|
||||||
|
|
||||||
|
@ -47,30 +26,6 @@ class AudioRouteButton extends AbstractButton<Props, *> {
|
||||||
iconName = 'icon-volume';
|
iconName = 'icon-volume';
|
||||||
label = 'toolbar.audioRoute';
|
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.
|
* Handles clicking / pressing the button, and opens the appropriate dialog.
|
||||||
*
|
*
|
||||||
|
@ -78,52 +33,7 @@ class AudioRouteButton extends AbstractButton<Props, *> {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
_handleClick() {
|
_handleClick() {
|
||||||
if (MPVolumeView) {
|
this.props.dispatch(openDialog(AudioRoutePickerDialog));
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { ColorPalette, type StyleType } from '../../../base/styles';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
|
const { AudioMode } = NativeModules;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type definition for a single entry in the device list.
|
* Type definition for a single entry in the device list.
|
||||||
*/
|
*/
|
||||||
|
@ -37,7 +39,38 @@ type Device = {
|
||||||
/**
|
/**
|
||||||
* Device type.
|
* 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,
|
_bottomSheetStyles: StyleType,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object describing available devices.
|
||||||
|
*/
|
||||||
|
_devices: Array<RawDevice>,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used for hiding the dialog when the selection was completed.
|
* Used for hiding the dialog when the selection was completed.
|
||||||
*/
|
*/
|
||||||
|
@ -72,8 +110,6 @@ type State = {
|
||||||
devices: Array<Device>
|
devices: Array<Device>
|
||||||
};
|
};
|
||||||
|
|
||||||
const { AudioMode } = NativeModules;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps each device type to a display name and icon.
|
* Maps each device type to a display name and icon.
|
||||||
*/
|
*/
|
||||||
|
@ -101,11 +137,9 @@ const deviceInfoMap = {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The exported React {@code Component}. {@code AudioRoutePickerDialog} is
|
* The exported React {@code Component}.
|
||||||
* exported only if the {@code AudioMode} module has the capability to get / set
|
|
||||||
* audio devices.
|
|
||||||
*/
|
*/
|
||||||
let AudioRoutePickerDialog_;
|
let AudioRoutePickerDialog_; // eslint-disable-line prefer-const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements a React {@code Component} which prompts the user when a password
|
* Implements a React {@code Component} which prompts the user when a password
|
||||||
|
@ -115,11 +149,47 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||||
state = {
|
state = {
|
||||||
/**
|
/**
|
||||||
* Available audio devices, it will be set in
|
* Available audio devices, it will be set in
|
||||||
* {@link #componentDidMount()}.
|
* {@link #getDerivedStateFromProps()}.
|
||||||
*/
|
*/
|
||||||
devices: []
|
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.
|
* 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.
|
// Bind event handlers so they are only bound once per instance.
|
||||||
this._onCancel = this._onCancel.bind(this);
|
this._onCancel = this._onCancel.bind(this);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Trigger an initial update.
|
||||||
* Initializes the device list by querying {@code AudioMode}.
|
AudioMode.updateDeviceList && AudioMode.updateDeviceList();
|
||||||
*
|
|
||||||
* @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')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -197,7 +240,7 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||||
_onSelectDeviceFn(device: Device) {
|
_onSelectDeviceFn(device: Device) {
|
||||||
return () => {
|
return () => {
|
||||||
this._hide();
|
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()}.
|
* Implements React's {@link Component#render()}.
|
||||||
*
|
*
|
||||||
|
@ -238,14 +302,17 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||||
*/
|
*/
|
||||||
render() {
|
render() {
|
||||||
const { devices } = this.state;
|
const { devices } = this.state;
|
||||||
|
let content;
|
||||||
|
|
||||||
if (!devices.length) {
|
if (devices.length === 0) {
|
||||||
return null;
|
content = this._renderNoDevices();
|
||||||
|
} else {
|
||||||
|
content = this.state.devices.map(this._renderDevice, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BottomSheet onCancel = { this._onCancel }>
|
<BottomSheet onCancel = { this._onCancel }>
|
||||||
{ this.state.devices.map(this._renderDevice, this) }
|
{ content }
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -259,14 +326,11 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||||
*/
|
*/
|
||||||
function _mapStateToProps(state) {
|
function _mapStateToProps(state) {
|
||||||
return {
|
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
|
AudioRoutePickerDialog_ = translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
|
||||||
// in AudioMode.
|
|
||||||
if (AudioMode.getAudioDevices && AudioMode.setAudioDevice) {
|
|
||||||
AudioRoutePickerDialog_ = translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AudioRoutePickerDialog_;
|
export default AudioRoutePickerDialog_;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
export * from './components';
|
export * from './components';
|
||||||
|
|
||||||
import './middleware';
|
import './middleware';
|
||||||
|
import './reducer';
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import { NativeModules } from 'react-native';
|
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||||
|
|
||||||
import { SET_AUDIO_ONLY } from '../../base/audio-only';
|
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 {
|
import {
|
||||||
CONFERENCE_FAILED,
|
CONFERENCE_FAILED,
|
||||||
CONFERENCE_LEFT,
|
CONFERENCE_LEFT,
|
||||||
|
@ -12,7 +12,10 @@ import {
|
||||||
} from '../../base/conference';
|
} from '../../base/conference';
|
||||||
import { MiddlewareRegistry } from '../../base/redux';
|
import { MiddlewareRegistry } from '../../base/redux';
|
||||||
|
|
||||||
|
import { _SET_AUDIOMODE_DEVICES, _SET_AUDIOMODE_SUBSCRIPTIONS } from './actionTypes';
|
||||||
|
|
||||||
const { AudioMode } = NativeModules;
|
const { AudioMode } = NativeModules;
|
||||||
|
const AudioModeEmitter = new NativeEventEmitter(AudioMode);
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
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.
|
* @param {Store} store - The redux store.
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
MiddlewareRegistry.register(({ getState }) => next => action => {
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
const result = next(action);
|
/* eslint-disable no-fallthrough */
|
||||||
|
|
||||||
if (AudioMode) {
|
switch (action.type) {
|
||||||
let mode;
|
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:
|
* NOTE: We moved the audio mode setting from CONFERENCE_WILL_JOIN to
|
||||||
case CONFERENCE_FAILED:
|
* CONFERENCE_JOINED because in case of a locked room, the app goes
|
||||||
case CONFERENCE_LEFT: {
|
* through CONFERENCE_FAILED state and gets to CONFERENCE_JOINED only
|
||||||
const conference = getCurrentConference(getState());
|
* 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