react-native-call-audio
v0.2.0
Published
In-call audio session ownership + output routing (earpiece/speaker/bluetooth/wired) and proximity-to-screen-off for React Native (Expo Modules, New Architecture). Works with LiveKit, react-native-webrtc, or any call stack.
Maintainers
Readme
react-native-call-audio
In-call audio session ownership and output routing (earpiece / speaker / bluetooth / wired) plus proximity-to-screen-off, for React Native (Expo Modules API, New Architecture). One JS API, native implementations per platform:
- Android —
AudioManager.setCommunicationDevice(API 31+),AudioDeviceCallbackfor real device-change events,PROXIMITY_SCREEN_OFF_WAKE_LOCK. - iOS —
AVAudioSessionrouting + speaker override,routeChangeNotification,isProximityMonitoringEnabled.
It depends on no calling SDK. It works with LiveKit, react-native-webrtc, Twilio, Agora, Daily, SIP, or no WebRTC at all.
Install
npm install react-native-call-audio
npx expo prebuildPeer deps: expo, react, react-native. No LiveKit / WebRTC dependency.
Usage
import {
startCallAudio,
stopCallAudio,
setRoute,
getDevices,
setProximityEnabled,
hasBluetoothPermission,
addDevicesChangedListener,
} from "react-native-call-audio";
// On call connect:
startCallAudio(); // owns the session, starts on earpiece
setProximityEnabled(true); // ear-to-phone blanks the screen
const sub = addDevicesChangedListener(({ available, selected }) => {
// available: ("earpiece" | "speaker" | "bluetooth" | "wired")[]
// selected: the current output
});
setRoute("speaker"); // returns the route applied
// On call end:
sub.remove();
setProximityEnabled(false);
stopCallAudio();startCallAudio() / stopCallAudio() are idempotent — calling start twice (or
stop twice) is a no-op, so they're safe to wire to call lifecycle events that may
fire more than once.
API
| Function | Returns | Notes |
| --- | --- | --- |
| startCallAudio() | void | Own the session (MODE_IN_COMMUNICATION / playAndRecord), start on earpiece. Idempotent. |
| stopCallAudio() | void | Release the session, restore prior mode. Idempotent. |
| setRoute(route) | CallAudioRoute | Force output; returns the route actually applied. |
| getDevices() | { available, selected } | Current routes (same shape as onAudioDevicesChanged). |
| setProximityEnabled(enabled) | void | Proximity-to-screen-off, held only during a call. |
| hasBluetoothPermission() | boolean | See Bluetooth note below. Always true on iOS / Android ≤11. |
| addDevicesChangedListener(cb) | EventSubscription | Fires on headset/BT connect & drop and on route changes. |
| addProximityListener(cb) | EventSubscription | { near: boolean }. |
route is "earpiece" | "speaker" | "bluetooth" | "wired". "wired" covers wired
headsets/headphones, USB-C audio, and CarPlay.
⚠️ Using with a WebRTC SDK (LiveKit, react-native-webrtc, etc.)
This module owns the call audio session. If your WebRTC SDK also manages the audio session (most do by default), the two will fight — last writer wins, routing becomes non-deterministic, and on Android the device list comes back empty. You MUST tell the WebRTC SDK to NOT manage audio so this module is the sole owner.
LiveKit (@livekit/react-native) — before any LiveKit usage (e.g. index.ts):
import { registerGlobals } from "@livekit/react-native";
// Let react-native-call-audio own the session; LiveKit must not also manage it.
registerGlobals({ autoConfigureAudioSession: false });Do not call setupIOSAudioManagement or AudioSession.startAudioSession() /
configureAudio() — those hand the session back to LiveKit and reintroduce the
conflict.
Other WebRTC stacks — disable their built-in audio-session/route management (the equivalent of the flag above) and drive routing only through this module.
Platform support
- Android 12+ (API 31+): modern
setCommunicationDevice+AudioDeviceCallback. - Android 5–11 (API 21–30): legacy
setSpeakerphoneOn+ Bluetooth SCO, with aBroadcastReceiverfor device-change events. Floor is API 21. - iOS 15.1+ (the APIs used go back further; the floor just matches typical apps).
Platform notes
iOS: BT/wired auto-route when connected;
setRoute("bluetooth"/"wired")clears the speaker override and lets the OS route to the connected device. No per-app permission is needed for output routing, sohasBluetoothPermission()is alwaystrueon iOS.Android Bluetooth (API 31+): the module declares
BLUETOOTH_CONNECTvia manifest merge, but on Android 12+ that permission is runtime — the manifest entry alone is not enough. Until the user grants it,setCommunicationDevicecan't see BT devices andsetRoute("bluetooth")silently falls back to the current route.The library does not request the permission for you — requesting it is the app's responsibility. Request
BLUETOOTH_CONNECTyourself (the module already declares it, so no manifest edits are needed), then usehasBluetoothPermission()to gate your Bluetooth control:import { PermissionsAndroid, Platform } from "react-native"; import { hasBluetoothPermission, setRoute } from "react-native-call-audio"; async function enableBluetooth() { if (Platform.OS === "android" && !hasBluetoothPermission()) { const result = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, ); if (result !== PermissionsAndroid.RESULTS.GRANTED) return; // user denied } setRoute("bluetooth"); }On Android ≤11 the install-time
BLUETOOTHpermission applies andhasBluetoothPermission()returnstrue, so no runtime request is required.
Changelog
0.2.0
- Add
hasBluetoothPermission()— on Android 12+ theBLUETOOTH_CONNECTgrant is required at runtime for BT routing; use this to gate your Bluetooth control. "wired"routing now also covers USB-C audio and CarPlay (iOSusbAudio/carAudio, AndroidTYPE_USB_HEADSET).startCallAudio()/stopCallAudio()are now idempotent (a duplicatestartno longer corrupts the saved audio mode restored onstop).
0.1.0
- Initial release: session ownership, earpiece/speaker/bluetooth/wired routing, device-change events, proximity-to-screen-off.
