@levelchat/react-native
v0.2.0
Published
LevelChat React Native SDK — WebRTC rooms, simulcast/SVC, real-time chat and recording for iOS and Android. Mirrors the @levelchat/web public API.
Readme
@levelchat/react-native
LevelChat React Native SDK — production-grade WebRTC rooms for iOS and Android.
Mirrors the public API of @levelchat/web, so a multi-platform
app writes one integration layer and switches at the import.
import { LevelChat } from '@levelchat/react-native';
import { useRoomState, useParticipantViews } from '@levelchat/react-native/hooks';Features
- 🎥 Real WebRTC publish + subscribe via
react-native-webrtc - 🧱 Mediasoup transport — exactly the same wire protocol as the Web SDK
- 📺 Simulcast (
q/h/flayers) and SVC (L3T3_KEY) on supported codecs - 🎙️ AVAudioSession (iOS) + AudioManager (Android) configured for video calls
- 🔄 Auto-reconnect with capped exponential backoff + jitter
- 📱 Lifecycle aware — pauses local camera in background, resumes on foreground
- 📡 Network quality scoring with transition events (
excellent → critical) - 🪝 First-class React hooks (
useRoomState,useLocalParticipant, …) - 🧪 Strict TypeScript, exact-optional, no
anyin public types - ⚖️ MIT licensed
Install
yarn add @levelchat/react-native react-native-webrtc
# or
npm install @levelchat/react-native react-native-webrtcThe react-native-webrtc peer dep ships native modules that need a native
build; follow its
install guide
for iOS pods and Android gradle setup.
iOS additional setup
Add the privacy strings to Info.plist:
<key>NSCameraUsageDescription</key>
<string>This app uses the camera for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone for voice calls.</string>For background audio (calls keep flowing when the user locks the screen) add
audio under UIBackgroundModes. For screen sharing you also need a
Broadcast Upload Extension target — see Apple's ReplayKit docs.
Android additional setup
In AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Screen sharing -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />Request runtime permissions before joining a room — the SDK does not prompt on your behalf.
Quick start
import { useEffect, useState } from 'react';
import { View } from 'react-native';
import { RTCView } from 'react-native-webrtc';
import { LevelChat, type Room } from '@levelchat/react-native';
import { useParticipantViews, useRoomState } from '@levelchat/react-native/hooks';
const client = new LevelChat({ region: 'eu-fsn' });
export function CallScreen({ token, roomId }: { token: string; roomId: string }) {
const [room, setRoom] = useState<Room | null>(null);
const { connected } = useRoomState(room);
const participants = useParticipantViews(room);
useEffect(() => {
let cancelled = false;
client.joinRoom(roomId, { token }).then(async (r) => {
if (cancelled) {
await r.close();
return;
}
setRoom(r);
await r.publishMicrophone();
await r.publishCamera({ resolution: '720p' });
});
return () => {
cancelled = true;
room?.close();
};
}, [roomId, token]);
return (
<View style={{ flex: 1 }}>
{connected &&
participants.map((p) => {
const cam = p.tracks.find((t) => t.source === 'camera');
if (!cam) return null;
return (
<RTCView
key={p.id}
streamURL={(cam.nativeTrack as { id: string }).id}
style={{ width: 320, height: 180 }}
/>
);
})}
</View>
);
}Token issuance
Production: mint room JWTs from your backend by calling
POST /v1/rooms/:id/tokens with your project's API key. The mobile app should
never see the API key.
Development: the SDK ships an issueToken helper that calls the same
endpoint from the device. It refuses to run without apiKey: set on the
client config and prints a docs link warning that this path is dev-only.
API surface
| Class / function | Purpose |
| ---------------------- | ------------------------------------------------------------- |
| LevelChat | Client config + factory; mints dev tokens, hands back Rooms |
| Room | Room lifecycle + publish/subscribe |
| LocalParticipant | The local user; tracks are LocalTracks |
| RemoteParticipant | A remote user; tracks are RemoteTracks |
| LocalTrack | Owns a producer; pause(), resume(), close() |
| RemoteTrack | Owns a consumer; pause(), resume(), setPreferredLayer() |
| enumerateDevices | Lists cameras + mics with normalised facing markers |
| applyAudioMode | Manual override of the iOS/Android audio session |
| setSpeakerphone | Toggle the loudspeaker route |
| captureScreen | Acquire a screen-capture stream (iOS Picker / Android prompt) |
| NetworkQualityScorer | Stand-alone scorer (scoreSample is a pure function) |
| LifecycleObserver | AppState wrapper — emits paused / resumed |
Hooks (@levelchat/react-native/hooks)
| Hook | Returns |
| -------------------------------- | --------------------------------------------------- |
| useRoomState(room) | { state, connected } |
| useLocalParticipant(room) | LocalParticipant \| null |
| useRemoteParticipants(room) | RemoteParticipant[] |
| useParticipantViews(room) | ParticipantView[] — local + remotes for rendering |
| useParticipantTracks(room, id) | TrackView[] for one participant |
Errors
Every thrown value extends LevelChatError with a stable code. Apps
should switch on code, not on message:
import { isLevelChatError } from '@levelchat/react-native';
try {
await room.publishCamera();
} catch (err) {
if (isLevelChatError(err) && err.code === 'media/permission-denied') {
showPermissionPrimer();
} else {
showGenericError(err);
}
}The full list lives in src/errors.ts.
Limitations vs @levelchat/web
- E2EE in-band frame encryption: not supported by
react-native-webrtcyet. The library does not expose theRTCRtpScriptTransform(Web) orFrameCryptor(libwebrtc) APIs through the React Native bridge. We track this as an upstream blocker on the react-native-webrtc issue tracker. As a workaround the SDK ships the AES-GCM SFrame primitives (SFrame.encrypt/SFrame.decrypt+EncryptionContext) for apps that want to encrypt payloads before publishing — typical use cases: data-channel messages, out-of-band recording. The SDK accepts thee2ee: truejoin flag for API parity with the Web SDK and logs a warning at runtime that the in-band path falls back to SRTP. For end-to-end encrypted media flow today, use the Web SDK or the Android SDK (Android shipped the libwebrtc FrameCryptor binding in W2.9.1). - AV1: depends on the device — react-native-webrtc 124+ supports it on recent iOS/Android hardware. The SDK negotiates VP9/H.264 fallback automatically.
- Live mode (
@levelchat/web/live): not yet bridged. Use a regularRoomwith the broadcaster role for now.
Testing
pnpm --filter @levelchat/react-native test # unit tests
pnpm --filter @levelchat/react-native typecheck # tsc --noEmit
pnpm --filter @levelchat/react-native build # emit dist/Real device tests run via Detox / Maestro against the example app under
./example/.
License
MIT — see LICENSE.
See docs/api/02-sdk-parity.md for the cross-platform API parity matrix.
