React Native
The same @metered-ca/realtime package you use in the browser runs on
React Native. React Native executes JavaScript, so there's no separate
fork — you supply a WebRTC implementation with
react-native-webrtc
and the SDK drives it through the rtcPeerConnectionFactory option.
WebSocket is already a global in React Native, so the signalling layer
is unchanged.
That means everything else carries over verbatim: channel-based peer
discovery, perfect negotiation, the ICE-restart ladder, identity-
preserving reconcile, multi-stream metadata, and the same event surface
(MeteredPeer /
RemotePeer).
The SDK measures payload sizes without a global TextEncoder, which some
React Native / Hermes versions don't provide — so no polyfill is needed.
npm install @metered-ca/realtime@latest.
Install
npm install @metered-ca/realtime react-native-webrtc
react-native-webrtc contains native code, so it needs a native build:
- Expo: it does not run in Expo Go. Use a dev build with the config plugin (below).
- Bare React Native:
cd ios && pod installafter install.
Wire up WebRTC
Two equivalent options — pick one.
Option A — explicit factory (no globals)
Hand the SDK react-native-webrtc's RTCPeerConnection. It exports
RTCPeerConnectionLike for exactly this boundary:
import { MeteredPeer, type RTCPeerConnectionLike } from "@metered-ca/realtime";
import { RTCPeerConnection } from "react-native-webrtc";
const peer = new MeteredPeer({
apiKey: "pk_live_…",
rtcPeerConnectionFactory: (cfg) =>
new RTCPeerConnection(cfg as object) as unknown as RTCPeerConnectionLike,
});
Option B — registerGlobals()
Install react-native-webrtc's classes as globals once at app startup;
the SDK then picks up the global RTCPeerConnection and you can drop the
factory:
import { registerGlobals } from "react-native-webrtc";
registerGlobals();
import { MeteredPeer } from "@metered-ca/realtime";
const peer = new MeteredPeer({ apiKey: "pk_live_…" });
Either way, capture media with react-native-webrtc's mediaDevices and
pass it to peer.addStream(...) — identical to the browser, just a
different getUserMedia source.
Platform setup
Expo (config plugin)
Add the @config-plugins/react-native-webrtc
plugin to app.json — it writes the camera/mic permission strings and
native build settings during expo prebuild:
{
"expo": {
"plugins": [
["@config-plugins/react-native-webrtc", {
"cameraPermission": "Camera access is used for video calls.",
"microphonePermission": "Microphone access is used for voice and video calls."
}]
]
}
}
npx expo install react-native-webrtc expo-dev-client
npx expo run:ios # or run:android — NOT Expo Go
Bare React Native
iOS — ios/<App>/Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access is used for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is used for voice and video calls.</string>
Keep the default platform :ios, min_ios_version_supported in ios/Podfile
(it resolves to 15.1 on RN 0.76 — don't lower it), then cd ios && pod install.
Android — android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
Set minSdkVersion = 24 (or higher) in android/build.gradle, and for
release builds keep the WebRTC classes in proguard-rules.pro:
-keep class org.webrtc.** { *; }
Runtime permissions: on Android 6+ and iOS, request camera/mic at runtime before calling
getUserMedia(e.g.PermissionsAndroidor a permissions library). The OS prompt uses the strings above.
Capture + render
Browsers bind a MediaStream to <video>.srcObject; React Native
renders it with <RTCView streamURL={stream.toURL()} />:
import { mediaDevices, RTCView } from "react-native-webrtc";
const stream = await mediaDevices.getUserMedia({ audio: true, video: true });
peer.addStream(stream as never, { role: "camera" });
// elsewhere, to show a remote peer:
// <RTCView streamURL={remoteStream.toURL()} objectFit="cover" style={...} />
Re-read .toURL() on every stream-added — it re-fires after a
reconnect/reconcile with a fresh MediaStream object (same stream.id,
new instance). The example wires this up.
Reconnect + app lifecycle
The SDK's three-layer reconnect (WebSocket backoff, ICE-restart ladder,
identity-preserving reconcile) works the same on mobile — and matters
more, because phones background apps and switch Wi-Fi ↔ cellular
constantly. Your RemotePeer references survive these drops; only the
underlying RTCPeerConnection is swapped. See
Reconnect Best Practices.
Notes specific to mobile:
- Backgrounding — iOS suspends timers/sockets when your app is
backgrounded; the SDK reconnects when it returns to the foreground.
Drive your "reconnecting…" UI off
state-change. - CallKit / ConnectionService — for calls that must stay live in the background, integrate the platform calling APIs (out of scope for the SDK).
TypeScript at the boundary
The SDK types its WebRTC surface against the DOM lib (MediaStream,
RTCPeerConnection). react-native-webrtc's equivalents are runtime-
compatible but distinct types, so you'll cast at two spots: the
rtcPeerConnectionFactory return (to RTCPeerConnectionLike) and the
MediaStream you pass to addStream / read on stream-added. Both are
marked in the example.
Gotchas
| Symptom | Cause / fix |
|---|---|
App crashes on launch / RTCPeerConnection is not a constructor in Expo | Running in Expo Go. Use a dev build with the config plugin. |
Events never fire / addEventListener is not a function | Old react-native-webrtc. Use a current major (≥ 100). |
stream-removed doesn't fire when a remote track ends | Depends on react-native-webrtc emitting the track ended event; pin a current version and test. |
| No video, no permission prompt | Request camera/mic at runtime before getUserMedia; verify the Info.plist / manifest entries. |
See also
- Example — React Native — copy-paste hook +
<RTCView>components, plus runnable Expo and bare apps. - WebRTC Video Call — same outcome, browser JS.
- Reconnect Best Practices.
- Troubleshooting — RN-specific issues.