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

iOSios/<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.

Androidandroid/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