Skip to main content

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).

Works out of the box on React Native

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 install after 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. PermissionsAndroid or 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​

SymptomCause / fix
App crashes on launch / RTCPeerConnection is not a constructor in ExpoRunning in Expo Go. Use a dev build with the config plugin.
Events never fire / addEventListener is not a functionOld react-native-webrtc. Use a current major (≥ 100).
stream-removed doesn't fire when a remote track endsDepends on react-native-webrtc emitting the track ended event; pin a current version and test.
No video, no permission promptRequest camera/mic at runtime before getUserMedia; verify the Info.plist / manifest entries.

See also​