@livqeno/react-native is for iOS and Android apps built with React
Native. It adds calls, chat, and live streaming. If you know the
Livqeno Client for the browser, you already know this one.
It runs on the user's phone and holds only a token, never your API
key. The token comes from your backend, made with the TypeScript,
Python, Go, Java or PHP SDK. So the flow is: your app calls your backend
endpoint (for example POST /join-room), gets back the grant, and passes it
to new Livqeno(...).
Why a backend, and how to write it: the API key can't live in an app, so one small route on your server makes the token. Build your token endpoint has the full route in TypeScript, Python, Go, Java, PHP and Dart, plus the plain HTTP call for any other language. You don't need Node.js unless your backend already uses it.
The SDK also handles the phone-specific parts a browser doesn't have: camera and mic permission prompts, the app going to the background, speaker vs. earpiece audio, and switching between Wi-Fi and mobile data.
In your React Native app:
import { Livqeno, LivqenoVideoView } from '@livqeno/react-native';
const livqeno = new Livqeno({ token, endpoint, iceServers }); // all three from your backend's grant
const room = await livqeno.join('room_123');
await room.enableCamera();
await room.enableMicrophone();The Room you get back is the same class that @livqeno/rtc gives you
in a browser. Rooms, participants, tracks, and events all work the same.
Install
pnpm add @livqeno/react-native @livqeno/rtc @livqeno/chat react-native-webrtc react-native-incall-managernpm install @livqeno/react-native @livqeno/rtc @livqeno/chat react-native-webrtc react-native-incall-manageryarn add @livqeno/react-native @livqeno/rtc @livqeno/chat react-native-webrtc react-native-incall-managerbun add @livqeno/react-native @livqeno/rtc @livqeno/chat react-native-webrtc react-native-incall-managerThen, for iOS only:
cd ios && pod install| Package | Required? | Why |
|---|---|---|
react-native-webrtc | Yes | The native WebRTC code. Install it in your app so it builds for iOS and Android. You never import it. |
react-native-incall-manager | No | Routes call audio (earpiece/speaker, proximity). Without it, audio.* throws NOT_SUPPORTED and everything else works. You can plug in your own module with audio.setAdapter(). |
Add permissions
Phones won't let an app use the camera or mic unless the app declares it up front. These lines go in your native project files, which the SDK can't edit for you.
iOS: ios/YourApp/Info.plist
<key>NSCameraUsageDescription</key>
<string>Camera access is used for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is used for calls.</string>Android: android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />Set up
Pass the credentials for what you use. RTC and chat are separate tokens,
both made by your backend. The simplest way is to pass the whole grant from
your backend, like the Quickstart does with
new Livqeno(grant), so iceServers is included:
new Livqeno({ token, endpoint, iceServers }); // a call
new Livqeno({ token, endpoint, iceServers, chatToken, chatApiUrl }); // a call and chat
new Livqeno({ chatToken, chatApiUrl }); // messaging only — no RTC connection is ever createdUse livqeno.hasRtc to check whether calls are available, for example to
decide whether to show call buttons. Calling join() on a chat-only
instance throws right away, before any permission prompt.
Join and leave
const room = await livqeno.join('room_123');
await livqeno.leave(); // leaves the room, keeps chat connected
await livqeno.dispose(); // tears down everything, including chatjoin() asks for camera and mic permission by default. If the user says no,
they can still join, watch, and listen. That way a user who denies the
camera can still take part, instead of hitting a dead end.
Show video
<LivqenoVideoView participant={remote} room={room} style={{ flex: 1 }} />
<LivqenoVideoView participant={room.localParticipant} room={room} style={pip} zOrder={1} />Pass room and the view updates itself when tracks change (published,
unpublished, muted, resubscribed).
Tell the view when it's off screen, and remote video nobody can see is paused at the server, so it stops using data and battery. It resumes the moment the view is visible again:
<FlatList
data={participants}
onViewableItemsChanged={({ viewableItems }) => setVisibleIds(new Set(viewableItems.map((i) => i.key)))}
renderItem={({ item }) => (
<LivqenoVideoView participant={item} room={room} visible={visibleIds.has(item.identity)} />
)}
/>A view laid out at zero size, or unmounted, counts as hidden too. If the
same person is shown in two views, their video stays on while either one is
visible. This needs adaptiveStream, which is on by default. For a big
room, new Livqeno({ ..., autoSubscribe: false }) starts every remote video
paused until a view shows it.
The call-quality hooks match @livqeno/react, with the room passed in:
useActiveSpeakers(room), useIsSpeaking(participant),
useConnectionQuality(participant) and useTrackSubscriptionState(track, room).
runConnectionTest() runs the pre-call test.
Hooks reference
These hooks all take the same room value you already have from
useRoom() or livqeno.join(). There's nothing extra to set up.
| Hook | Returns |
|---|---|
useRoom(livqeno, roomId, { autoJoin? }) | { room?, joining, error? }. Joins when the component mounts and leaves when it unmounts, so navigating away from a screen doesn't leave a call running (and draining the battery) in the background. |
useConnectionState(room) | The connection state, as a string. |
useParticipants(room) / useRemoteParticipants(room) | Everyone in the room / everyone except you. |
useCamera(room) / useMicrophone(room) | MediaToggle: { enabled, track?, enable(), disable(), error? }. |
useLivqenoError(room) | The last RTCError this room ran into. |
useParticipantMetadata(participant) | A participant's metadata, kept up to date as it changes. |
useDataReceived(room, handler, { topic? }) | Calls your handler(payload, participant?, topic?) for every data message. Pass topic to only hear messages on that topic. participant is undefined for a message from the server. It's safe to pass a new function on every render — the hook always calls your latest one. |
useTranscription(room, options?) | Live caption text, built from the room's transcription events. |
useAgentState(participant) | An AI voice agent's current state: 'listening' | 'thinking' | 'speaking' | 'ended' | undefined. |
Permissions
permissions (from @livqeno/react-native) lets you check and ask for
camera and microphone access from your JavaScript code, to go with the
native permission declarations you added above:
import { permissions } from '@livqeno/react-native';
const status = await permissions.check(); // { camera, microphone }, no prompt
const granted = await permissions.request(); // prompts for both; pass ['camera'] for just one
await permissions.require(['camera', 'microphone']); // throws LivqenoPermissionError if either isn't grantedEach status is one of 'granted' | 'denied' | 'blocked' | 'undetermined' | 'unavailable'.
'blocked' means the user has to go turn it on in Settings themselves —
asking again does nothing. iOS has no way to report status without
prompting, so check() always answers undetermined there. Use request()
on iOS instead.
If a permission isn't granted, require() throws a LivqenoPermissionError.
You can check for that specific error with isLivqenoPermissionError().
LivqenoPermissionError is actually a kind of RTCError — the same error
class that call and join failures throw — so isRTCError() catches it too.
Audio routing
You don't need to think about audio routing most of the time: Livqeno
starts and stops the audio session around a room for you automatically.
Reach for audio (from @livqeno/react-native) only when you want to
override that yourself — for example, to add your own speakerphone button:
import { audio } from '@livqeno/react-native';
await audio.setSpeakerphone(true); // switch to the loudspeaker; false hands routing back to the OS
await audio.setOutput('earpiece'); // pick a specific output; Android supports more than iOS does
const outputs = await audio.getOutputs(); // what's available right now — don't cache this list
await audio.showRoutePicker(); // iOS's AirPlay-style system pickerBy default, this works through the optional package
react-native-incall-manager. Two methods need more than that package can
do: getOutputs() and showRoutePicker() reject with an RTCError
(NOT_SUPPORTED) unless you give them an adapter that implements those,
using audio.setAdapter().
Chat
If you passed a chatToken, you get livqeno.chat:
const livqeno = new Livqeno({ chatToken: session.token, chatApiUrl: session.apiUrl });
await livqeno.chat!.connect('room_123');
await livqeno.chat!.send('Hello everyone!');Everything else is the normal @livqeno/chat API: messages.list(),
startTyping(), markAsRead(), presence, threads. See Chat.
Reconnection
Phones lose connection all the time: a lift, a tunnel, walking out of Wi-Fi range. Reconnecting is done for you. The SDK retries with backoff, re-joins the room, and gives up after a limit instead of looping forever.
Short drops resume the session without rebuilding the media connection, and a media connection that fails on its own is rebuilt automatically.
Optional: install @react-native-community/netinfo. Then a Wi-Fi → cellular
switch recovers straight away instead of waiting for a timeout: a pending
reconnect runs at once and a live call restarts ICE onto the new network.
onNetworkReconnect still fires when the network returns to a room that
already gave up.
Pass refreshToken to new Livqeno({ … }) to keep calls alive past the
token's expiry. See Keep long calls alive.
When the app goes to the background
The SDK does not disconnect when the app goes to the background. Otherwise, checking a notification would kick the user out of the call.
- Video capture stops in the background (the OS pauses the camera) and starts again when the user comes back.
- If you want users to leave when backgrounded, do it yourself:
new Livqeno({
onAppStateChange: (state) => {
if (state === 'background') void livqeno.leave();
},
});Live streaming
Use this for broadcasts from a phone, like a creator going live to their
followers. Your backend decides the role first: addHost() for a host,
createViewerToken() for a viewer.
LivqenoLiveStream is built on Livqeno. Hosts and viewers are normal
participants in one room, and the stream's chat is normal chat. So
permissions, audio, background handling, and reconnects all work the same as
Livqeno.join().
import { joinLiveStream, useLiveStream, useCamera } from '@livqeno/react-native';
const stream = await joinLiveStream(credentials);
// or, inside a component:
const { room, joining, error } = useLiveStream(stream);
const camera = useCamera(room);
if (stream.isHost) {
await camera.enable();
}
await stream.react('❤️');
await stream.leave();Good to know:
credentialsis exactly whataddHost()/createViewerToken()returns on your server.- A
VIEWERis never asked for camera/mic permission, since they can't publish anyway. stream.roomandstream.chatare the normalRoomandLivqenoChatHandle.useParticipants(stream.room),useCamera(stream.room), andstream.chat!.messages.list(...)all work.- No extra package needed. Credentials are a plain object:
streamId,role,rtc,chat?,chatRootMessageId?.
A BROADCAST stream is for large audiences: instead of joining the room,
viewers just watch an HLS video stream (see
Scaling the audience). For that, use
<LiveStreamPlayer> instead of joining a Room:
import { LiveStreamPlayer } from '@livqeno/react-native';
<LiveStreamPlayer
url={stream.playbackUrl}
lowLatencyUrl={stream.lowLatencyPlaybackUrl} // LL-HLS, ~2s behind live; falls back to `url` on error
resizeMode="contain" // or 'cover' to crop and fill
beacon={stream.playbackBeacon} // watch-time heartbeat for viewer-minute billing
/>;Production notes
- Background audio (iOS): add the
audiobackground mode toInfo.plist. Otherwise calls end when the app goes to the background. - Simulators have no camera. The iOS Simulator and most Android emulators can't capture video. Test on a real device.
- Use
wss://andhttps://. Android blocks plainhttpby default. Don't work around it.
Troubleshooting
| Problem | Fix |
|---|---|
| "RTCPeerConnection is not defined" | Creating a Livqeno sets this up. If you use WebRTC before that, call bootstrapLivqenoNative() in index.js. |
| Black video, no error | Almost always permissions. Call permissions.request() (or require()) and look for blocked. check() never shows blocked, because it doesn't prompt. |
| Works on Wi-Fi, fails on cellular | Mobile networks need TURN. Pass iceServers from your token response. |
The full permissions and audio routing API is above: Permissions, Audio routing.
Next steps
- Set up permissions properly on both platforms: Permissions.
- Learn the shared call API (same as the web): Rooms & participants.
- Keep calls going in the background: Background audio.
- Handle network changes: Reconnection.