mirror of https://github.com/jitsi/jitsi-meet
This module chooses the most appropriate audio default based on the specified mode.pull/1266/head
parent
113e50c074
commit
2edaaac7bf
@ -0,0 +1,305 @@ |
||||
package org.jitsi.meet.audiomode; |
||||
|
||||
import android.annotation.TargetApi; |
||||
import android.content.BroadcastReceiver; |
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.content.IntentFilter; |
||||
import android.media.AudioDeviceInfo; |
||||
import android.media.AudioManager; |
||||
import android.os.Build; |
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.util.Log; |
||||
|
||||
import com.facebook.react.bridge.Promise; |
||||
import com.facebook.react.bridge.ReactApplicationContext; |
||||
import com.facebook.react.bridge.ReactContext; |
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule; |
||||
import com.facebook.react.bridge.ReactMethod; |
||||
|
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
|
||||
/** |
||||
* Module implementing a simple API to select the appropriate audio device for a conference call. |
||||
* |
||||
* Audio calls should use <tt>AudioModeModule.AUDIO_CALL</tt>, which uses the builtin earpiece, |
||||
* wired headset or bluetooth headset. The builtin earpiece is the default audio device. |
||||
* |
||||
* Video calls should should use <tt>AudioModeModule.VIDEO_CALL</tt>, which uses the builtin |
||||
* speaker, earpiece, wired headset or bluetooth headset. The builtin speaker is the default |
||||
* audio device. |
||||
* |
||||
* Before a call has started and after it has ended the <tt>AudioModeModule.DEFAULT</tt> mode |
||||
* should be used. |
||||
*/ |
||||
public class AudioModeModule extends ReactContextBaseJavaModule { |
||||
/** |
||||
* Constants representing the audio mode. |
||||
* - DEFAULT: Used before and after every call. It represents the default audio routing scheme. |
||||
* - AUDIO_CALL: Used for audio only calls. It will use the earpiece by default, unless a |
||||
* wired or Bluetooth headset is connected. |
||||
* - VIDEO_CALL: Used for video calls. It will use the speaker by default, unless a wired or |
||||
* Bluetooth headset is connected. |
||||
*/ |
||||
private static final int DEFAULT = 0; |
||||
private static final int AUDIO_CALL = 1; |
||||
private static final int VIDEO_CALL = 2; |
||||
|
||||
/** |
||||
* React Native module name. |
||||
*/ |
||||
private static final String MODULE_NAME = "AudioMode"; |
||||
|
||||
/** |
||||
* Tag used when logging messages. |
||||
*/ |
||||
static final String TAG = MODULE_NAME; |
||||
|
||||
/** |
||||
* Audio mode currently in use. |
||||
*/ |
||||
private int mode = -1; |
||||
|
||||
/** |
||||
* {@link AudioManager} instance used to interact with the Android audio subsystem. |
||||
*/ |
||||
private final AudioManager audioManager; |
||||
|
||||
/** |
||||
* {@link Handler} for running all operations on the main thread. |
||||
*/ |
||||
private final Handler mainThreadHandler; |
||||
|
||||
/** |
||||
* {@link Runnable} for running update operation on the main thread. |
||||
*/ |
||||
private final Runnable mainThreadRunner; |
||||
|
||||
/** |
||||
* {@link BluetoothHeadsetMonitor} for detecting Bluetooth device changes in old (< M) |
||||
* Android versions. |
||||
*/ |
||||
private BluetoothHeadsetMonitor bluetoothHeadsetMonitor; |
||||
|
||||
/** |
||||
* |
||||
*/ |
||||
private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) ? |
||||
AudioManager.ACTION_HEADSET_PLUG : Intent.ACTION_HEADSET_PLUG; |
||||
|
||||
/** |
||||
* Initializes a new module instance. There shall be a single instance of this module throughout |
||||
* the lifetime of the application. |
||||
* |
||||
* @param reactContext the {@link ReactApplicationContext} where this module is created. |
||||
*/ |
||||
public AudioModeModule(ReactApplicationContext reactContext) { |
||||
super(reactContext); |
||||
|
||||
audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE)); |
||||
mainThreadHandler = new Handler(Looper.getMainLooper()); |
||||
mainThreadRunner = new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
if (mode != -1) { |
||||
updateAudioRoute(mode); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
// Setup runtime device change detection
|
||||
setupAudioRouteChangeDetection(); |
||||
} |
||||
|
||||
/** |
||||
* Gets the name for this module, to be used in the React Native bridge. |
||||
* |
||||
* @return a string with the module name. |
||||
*/ |
||||
@Override |
||||
public String getName() { |
||||
return MODULE_NAME; |
||||
} |
||||
|
||||
/** |
||||
* Gets a mapping with the constants this module is exporting. |
||||
* |
||||
* @return a {@link Map} mapping the constants to be exported with their values. |
||||
*/ |
||||
@Override |
||||
public Map<String, Object> getConstants() { |
||||
Map<String, Object> constants = new HashMap<>(); |
||||
constants.put("DEFAULT", DEFAULT); |
||||
constants.put("AUDIO_CALL", AUDIO_CALL); |
||||
constants.put("VIDEO_CALL", VIDEO_CALL); |
||||
return constants; |
||||
} |
||||
|
||||
/** |
||||
* Updates the audio route for the given mode. |
||||
* |
||||
* @param mode the audio mode to be used when computing the audio route. |
||||
* @return true if the audio route was updated successfully, false otherwise. |
||||
*/ |
||||
private boolean updateAudioRoute(int mode) { |
||||
Log.d(TAG, "Update audio route for mode: " + mode); |
||||
|
||||
if (mode == DEFAULT) { |
||||
audioManager.setMode(AudioManager.MODE_NORMAL); |
||||
audioManager.abandonAudioFocus(null); |
||||
audioManager.setSpeakerphoneOn(false); |
||||
audioManager.setMicrophoneMute(true); |
||||
setBluetoothAudioRoute(false); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); |
||||
audioManager.setMicrophoneMute(false); |
||||
|
||||
if (audioManager.requestAudioFocus( |
||||
null, |
||||
AudioManager.STREAM_VOICE_CALL, |
||||
AudioManager.AUDIOFOCUS_GAIN) |
||||
== AudioManager.AUDIOFOCUS_REQUEST_FAILED) { |
||||
Log.d(TAG, "Audio focus request failed"); |
||||
return false; |
||||
} |
||||
|
||||
boolean useSpeaker = (mode == VIDEO_CALL); |
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
||||
// On Android >= M we use the AudioDeviceCallback API, so turn on Bluetooth SCO
|
||||
// from the start.
|
||||
if (audioManager.isBluetoothScoAvailableOffCall()) { |
||||
audioManager.startBluetoothSco(); |
||||
} |
||||
} else { |
||||
// On older Android versions we must set the Bluetooth route manually. Also disable
|
||||
// the speaker in that case.
|
||||
setBluetoothAudioRoute(bluetoothHeadsetMonitor.isHeadsetAvailable()); |
||||
if (bluetoothHeadsetMonitor.isHeadsetAvailable()) { |
||||
useSpeaker = false; |
||||
} |
||||
} |
||||
|
||||
// XXX: isWiredHeadsetOn is not deprecated when used just for knowing if there is a wired
|
||||
// headset connected, regardless of audio being routed to it.
|
||||
audioManager.setSpeakerphoneOn(useSpeaker && |
||||
!(audioManager.isWiredHeadsetOn() || audioManager.isBluetoothScoOn())); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Public method to set the current audio mode. |
||||
* |
||||
* @param mode the desired audio mode. |
||||
* @param promise a {@link Promise} which will be resolved if the audio mode could be updated |
||||
* successfully, and it will be rejected otherwise. |
||||
*/ |
||||
@ReactMethod |
||||
public void setMode(final int mode, final Promise promise) { |
||||
if (mode != DEFAULT && mode != AUDIO_CALL && mode != VIDEO_CALL) { |
||||
promise.reject("setMode", "Invalid audio mode " + mode); |
||||
return; |
||||
} |
||||
|
||||
Runnable r = new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
if (updateAudioRoute(mode)) { |
||||
AudioModeModule.this.mode = mode; |
||||
promise.resolve(null); |
||||
} else { |
||||
promise.reject("setMode", "Failed to set the requested audio mode"); |
||||
} |
||||
} |
||||
}; |
||||
mainThreadHandler.post(r); |
||||
} |
||||
|
||||
/** |
||||
* Setup the audio route change detection mechanism. We use the |
||||
* {@link android.media.AudioDeviceCallback} API on Android >= 23 only. |
||||
*/ |
||||
private void setupAudioRouteChangeDetection() { |
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
||||
setupAudioRouteChangeDetectionM(); |
||||
} else { |
||||
setupAudioRouteChangeDetectionOld(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Audio route change detection mechanism for Android API >= 23. |
||||
*/ |
||||
@TargetApi(Build.VERSION_CODES.M) |
||||
private void setupAudioRouteChangeDetectionM() { |
||||
android.media.AudioDeviceCallback audioDeviceCallback = |
||||
new android.media.AudioDeviceCallback() { |
||||
@Override |
||||
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { |
||||
Log.d(TAG, "Audio devices added"); |
||||
onAudioDeviceChange(); |
||||
} |
||||
|
||||
@Override |
||||
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { |
||||
Log.d(TAG, "Audio devices removed"); |
||||
onAudioDeviceChange(); |
||||
} |
||||
}; |
||||
|
||||
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null); |
||||
} |
||||
|
||||
/** |
||||
* Audio route change detection mechanism for Android API < 23. |
||||
*/ |
||||
private void setupAudioRouteChangeDetectionOld() { |
||||
ReactContext reactContext = getReactApplicationContext(); |
||||
|
||||
// Detect changes in wired headset connections
|
||||
IntentFilter wiredHeadSetFilter = new IntentFilter(ACTION_HEADSET_PLUG); |
||||
BroadcastReceiver wiredHeadsetReceiver = new BroadcastReceiver() { |
||||
@Override |
||||
public void onReceive(Context context, Intent intent) { |
||||
Log.d(TAG, "Wired headset added / removed"); |
||||
onAudioDeviceChange(); |
||||
} |
||||
}; |
||||
reactContext.registerReceiver(wiredHeadsetReceiver, wiredHeadSetFilter); |
||||
|
||||
// Detect Bluetooth device changes
|
||||
bluetoothHeadsetMonitor = |
||||
new BluetoothHeadsetMonitor(this, this.getReactApplicationContext()); |
||||
bluetoothHeadsetMonitor.start(); |
||||
} |
||||
|
||||
/** |
||||
* Helper method to set the output route to a Bluetooth device. |
||||
* @param enabled true if Bluetooth should use used, false otherwise. |
||||
*/ |
||||
private void setBluetoothAudioRoute(boolean enabled) { |
||||
if (enabled) { |
||||
audioManager.startBluetoothSco(); |
||||
audioManager.setBluetoothScoOn(true); |
||||
} else { |
||||
audioManager.setBluetoothScoOn(false); |
||||
audioManager.stopBluetoothSco(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Helper method to trigger an audio route update when devices change. It makes sure the |
||||
* operation is performed on the main thread. |
||||
*/ |
||||
void onAudioDeviceChange() { |
||||
mainThreadHandler.post(mainThreadRunner); |
||||
} |
||||
} |
@ -0,0 +1,41 @@ |
||||
package org.jitsi.meet.audiomode; |
||||
|
||||
import com.facebook.react.ReactPackage; |
||||
import com.facebook.react.bridge.JavaScriptModule; |
||||
import com.facebook.react.bridge.NativeModule; |
||||
import com.facebook.react.bridge.ReactApplicationContext; |
||||
import com.facebook.react.uimanager.ViewManager; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
public class AudioModePackage implements ReactPackage { |
||||
/** |
||||
* {@inheritDoc} |
||||
* @return List of native modules to be exposed by React Native. |
||||
*/ |
||||
@Override |
||||
public List<NativeModule> createNativeModules( |
||||
ReactApplicationContext reactContext) { |
||||
List<NativeModule> modules = new ArrayList<>(); |
||||
modules.add(new AudioModeModule(reactContext)); |
||||
return modules; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
public List<Class<? extends JavaScriptModule>> createJSModules() { |
||||
return Collections.emptyList(); |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { |
||||
return Collections.emptyList(); |
||||
} |
||||
} |
@ -0,0 +1,192 @@ |
||||
package org.jitsi.meet.audiomode; |
||||
|
||||
import android.bluetooth.BluetoothAdapter; |
||||
import android.bluetooth.BluetoothDevice; |
||||
import android.bluetooth.BluetoothHeadset; |
||||
import android.bluetooth.BluetoothProfile; |
||||
import android.content.BroadcastReceiver; |
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.content.IntentFilter; |
||||
import android.media.AudioManager; |
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.util.Log; |
||||
|
||||
import com.facebook.react.bridge.ReactContext; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Helper class to detect and handle Bluetooth device changes. It monitors Bluetooth headsets |
||||
* being connected / disconnected and notifies the module about device changes when this occurs. |
||||
*/ |
||||
public class BluetoothHeadsetMonitor { |
||||
/** |
||||
* {@link AudioModeModule} where this monitor reports. |
||||
*/ |
||||
private final AudioModeModule AudioModeModule; |
||||
|
||||
/** |
||||
* {@link AudioManager} instance used to interact with the Android audio subsystem. |
||||
*/ |
||||
private final AudioManager audioManager; |
||||
|
||||
/** |
||||
* {@link ReactContext} instance where the main module runs. |
||||
*/ |
||||
private final ReactContext reactContext; |
||||
|
||||
/** |
||||
* Reference to the Bluetooth adapter, needed for managing <tt>BluetoothProfile.HEADSET</tt> |
||||
* devices. |
||||
*/ |
||||
private BluetoothAdapter bluetoothAdapter; |
||||
|
||||
/** |
||||
* Reference to a proxy object which allows us to query connected devices. |
||||
*/ |
||||
private BluetoothHeadset bluetoothHeadset; |
||||
|
||||
/** |
||||
* Listener for Bluetooth service profiles, allows us to get the proxy object to |
||||
* {@link BluetoothHeadset}. |
||||
*/ |
||||
private BluetoothProfile.ServiceListener bluetoothProfileListener; |
||||
|
||||
/** |
||||
* {@link Handler} for running all operations on the main thread. |
||||
*/ |
||||
private final Handler mainThreadHandler; |
||||
|
||||
/** |
||||
* Helper for running Bluetooth operations on the main thread. |
||||
*/ |
||||
private Runnable bluetoothRunnable; |
||||
|
||||
/** |
||||
* Flag indicating if there are any Bluetooth headset devices currently available. |
||||
*/ |
||||
private boolean headsetAvailable = false; |
||||
|
||||
public BluetoothHeadsetMonitor(AudioModeModule audioModeModule, ReactContext reactContext) { |
||||
this.AudioModeModule = audioModeModule; |
||||
this.reactContext = reactContext; |
||||
|
||||
audioManager = ((AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE)); |
||||
bluetoothAdapter = null; |
||||
bluetoothHeadset = null; |
||||
bluetoothProfileListener = null; |
||||
mainThreadHandler = new Handler(Looper.getMainLooper()); |
||||
} |
||||
|
||||
/** |
||||
* Start monitoring Bluetooth device activity. |
||||
*/ |
||||
public void start() { |
||||
bluetoothRunnable = new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
if (bluetoothHeadset == null) { |
||||
headsetAvailable = false; |
||||
} else { |
||||
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices(); |
||||
headsetAvailable = !devices.isEmpty(); |
||||
} |
||||
BluetoothHeadsetMonitor.this.AudioModeModule.onAudioDeviceChange(); |
||||
} |
||||
}; |
||||
|
||||
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
||||
if (bluetoothAdapter == null) { |
||||
Log.w(AudioModeModule.TAG, "Device doesn't support Bluetooth"); |
||||
return; |
||||
} |
||||
|
||||
if (!audioManager.isBluetoothScoAvailableOffCall()) { |
||||
Log.w(AudioModeModule.TAG, "Bluetooth SCO is not available"); |
||||
return; |
||||
} |
||||
|
||||
// XXX: The profile listener listens for system services of the given type being available
|
||||
// to the application. That is, if our Bluetooth adapter has the "headset" profile.
|
||||
bluetoothProfileListener = new BluetoothProfile.ServiceListener() { |
||||
@Override |
||||
public void onServiceConnected(int profile, BluetoothProfile proxy) { |
||||
if (profile == BluetoothProfile.HEADSET) { |
||||
bluetoothHeadset = (BluetoothHeadset) proxy; |
||||
updateDevices(); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onServiceDisconnected(int profile) { |
||||
if (profile == BluetoothProfile.HEADSET) { |
||||
bluetoothHeadset = null; |
||||
updateDevices(); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
bluetoothAdapter.getProfileProxy(reactContext, |
||||
bluetoothProfileListener, BluetoothProfile.HEADSET); |
||||
|
||||
IntentFilter bluetoothFilter = new IntentFilter(); |
||||
bluetoothFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); |
||||
bluetoothFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); |
||||
BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() { |
||||
@Override |
||||
public void onReceive(Context context, Intent intent) { |
||||
final String action = intent.getAction(); |
||||
if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { |
||||
// XXX: This action will be fired when a Bluetooth headset is connected or
|
||||
// disconnected to the system. This is not related to audio routing.
|
||||
final int state = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, -99); |
||||
switch (state) { |
||||
case BluetoothHeadset.STATE_CONNECTED: |
||||
case BluetoothHeadset.STATE_DISCONNECTED: |
||||
Log.d(AudioModeModule.TAG, "BT headset connection state changed: " + state); |
||||
updateDevices(); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
} else if (action.equals(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) { |
||||
// XXX: This action will be fired when the connection established with a
|
||||
// Bluetooth headset (called a SCO connection) changes state. When the SCO
|
||||
// connection is active we route audio to it.
|
||||
final int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -99); |
||||
switch (state) { |
||||
case AudioManager.SCO_AUDIO_STATE_CONNECTED: |
||||
case AudioManager.SCO_AUDIO_STATE_DISCONNECTED: |
||||
Log.d(AudioModeModule.TAG, "BT SCO connection state changed: " + state); |
||||
updateDevices(); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
reactContext.registerReceiver(bluetoothReceiver, bluetoothFilter); |
||||
|
||||
// Initial detection
|
||||
updateDevices(); |
||||
} |
||||
|
||||
/** |
||||
* Detect if there are new devices connected / disconnected and fires the |
||||
* <tt>onAudioDeviceChange</tt> callback. |
||||
*/ |
||||
private void updateDevices() { |
||||
mainThreadHandler.post(bluetoothRunnable); |
||||
} |
||||
|
||||
/** |
||||
* Returns the current headset availability. |
||||
* @return true if there is a Bluetooth headset connected, false otherwise. |
||||
*/ |
||||
public boolean isHeadsetAvailable() { |
||||
return headsetAvailable; |
||||
} |
||||
} |
Loading…
Reference in new issue