How To Setup WebRTC For Audio & Video Calling ?
Table Of Contents:
- Install Required Libraries.
Referred Websites
https://webrtc.org/getting-started/overview
(1) Install Required Libraries
npm install react-native-webrtc – save

(2) Add Required Permission On – AndroidManifest.xml
- Add these below permissions in in
android/app/src/main/AndroidManifest.xml
:
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature android:name="android.hardware.audio.output" />
<uses-feature android:name="android.hardware.microphone" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET" />
<! – Bluetooth permissions for headsets – >
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<! – Screen-sharing on Android 14+ – >
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

(3) Enable Java 8 Support
- In
android/app/build.gradle
add the following inside theandroid
section.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

(4) Audio Category Setup – Set Audio as Media
In Android, audio output categories define how your app’s audio is treated by the system. Different categories affect volume controls, audio routing, and interruptions (like phone calls).
When using WebRTC in React Native, the default audio output category is usually treated as a call/voice communication stream. This is because WebRTC is often used for voice and video calls.

- if your Android files are written in Java, modify
MainApplication.java
: - if your Android files are written in Kotlin, modify
MainApplication.kt
:

// add imports
import com.oney.WebRTCModule.WebRTCModuleOptions;
import android.media.AudioAttributes
import org.webrtc.audio.JavaAudioDeviceModule;
class MainApplication : Application(), ReactApplication {
override fun onCreate() {
// append this before WebRTCModule initializes
val options = WebRTCModuleOptions.getInstance()
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
options.audioDeviceModule = JavaAudioDeviceModule.builder(this)
.setAudioAttributes(audioAttributes)
.createAudioDeviceModule()
}
}

(5) Enable Screen Sharing
Starting with version 118.0.2 a foreground service is included in this library in order to make screen-sharing possible under Android 14 rules.
If you want to enable it, first declare the following permissions:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />

- Then enable the builtin service as follows, by adding the following code very early in your application, in your main activity’s
onCreate
for instance: - MainActivity.kt file Need to be changed.
// Import WebRTCModuleOptions
import com.oney.WebRTCModule.WebRTCModuleOptions
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ✅ Enable Screen Sharing Foreground Service for Android 14+
val options = WebRTCModuleOptions.getInstance()
options.enableMediaProjectionService = true
}

(6) Fix UnsatisfiedLinkError Crash
- In
android/gradle.properties
: add this line. - This prevents an issue with WebRTC native libraries not loading correctly on some devices.
android.useFullClasspathForDexingTransform=true

(7) Test And Verify
- After setting this up, clean and rebuild your project:
cd android
./gradlew clean
./gradlew assembleDebug
(8) Project Setup For Audio & Video Call
Step-1: Configure ICE Servers (STUN/TURN Servers) Create WebrtcConfig.ts File Under src/screens/WebrtcConfig.ts
interface RTCIceServer {
urls: string | string[];
username?: string;
credential?: string;
}
interface RTCConfiguration {
iceServers: RTCIceServer[];
iceCandidatePoolSize?: number;
}
export const configuration: RTCConfiguration = {
iceServers: [
{ urls: ['stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302'] },
],
iceCandidatePoolSize: 10,
};

Step-2: Create CallActionBox.tsx Component Under src/components/CallActionBox.tsx
import React from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
interface CallActionBoxProps {
switchCamera: () => void;
toggleMute: () => void;
toggleCamera: () => void;
endCall: () => void;
}
const CallActionBox: React.FC<CallActionBoxProps> = ({
switchCamera,
toggleMute,
toggleCamera,
endCall,
}) => {
return (
<View style={styles.actionContainer}>
<TouchableOpacity style={styles.greenButton} onPress={switchCamera}>
<Text style={styles.buttonText}>Switch Camera</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.yellowButton} onPress={toggleMute}>
<Text style={styles.buttonText}>Toggle Mute</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.yellowButton} onPress={toggleCamera}>
<Text style={styles.buttonText}>Toggle Camera</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.redButton} onPress={endCall}>
<Text style={styles.buttonText}>End Call</Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
actionContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#1F2937', // Equivalent to bg-gray-800
padding: 16, // Equivalent to p-4
borderTopLeftRadius: 16, // Equivalent to rounded-t-2xl
borderTopRightRadius: 16,
},
greenButton: {
backgroundColor: '#10B981', // Equivalent to bg-green-500
padding: 12, // Equivalent to p-3
borderRadius: 8, // Equivalent to rounded-lg
},
yellowButton: {
backgroundColor: '#F59E0B', // Equivalent to bg-yellow-500
padding: 12,
borderRadius: 8,
},
redButton: {
backgroundColor: '#EF4444', // Equivalent to bg-red-500
padding: 12,
borderRadius: 8,
},
buttonText: {
color: 'white',
fontWeight: 'bold',
},
});
export default CallActionBox;

Step-3: Create StartCallScreen.tsf File Under src/screens/StartCallScreen.tsx
import React, { useState, useEffect } from 'react';
import { Text, StyleSheet, Button, View } from 'react-native';
import {
RTCPeerConnection,
RTCView,
RTCIceCandidate,
RTCSessionDescription,
MediaStream,
mediaDevices,
} from 'react-native-webrtc';
import firestore from '@react-native-firebase/firestore';
import CallActionBox from '../components/CallActionBox'
import { configuration } from '../config/webrtcConfig';
type StartCallScreenProps = {
roomId: string;
screens: { ROOM: string; CALL: string };
setScreen: (screen: string) => void;
};
interface MediaDeviceInfo {
deviceId: string;
kind: 'audioinput' | 'audiooutput' | 'videoinput';
label: string;
groupId?: string;
facing?: 'front' | 'environment';
}
export default function StartCallScreen({ roomId, screens, setScreen }: StartCallScreenProps) {
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
const [cachedLocalPC, setCachedLocalPC] = useState<RTCPeerConnection | null>(null);
const [isMuted, setIsMuted] = useState(false);
const [isOffCam, setIsOffCam] = useState(false);
useEffect(() => {
startLocalStream();
}, []);
useEffect(() => {
if (localStream) {
startCall(roomId);
}
}, [localStream]);
const endCall = async () => {
if (cachedLocalPC) {
const senders = cachedLocalPC.getSenders();
senders.forEach((sender) => cachedLocalPC.removeTrack(sender));
cachedLocalPC.close();
}
const roomRef = firestore().collection('room').doc(roomId);
await roomRef.update({ answer: firestore.FieldValue.delete(), connected: false });
setLocalStream(null);
setRemoteStream(null);
setCachedLocalPC(null);
setScreen(screens.ROOM);
};
const startLocalStream = async () => {
const isFront = true;
const devices = await mediaDevices.enumerateDevices() as MediaDeviceInfo[];
const facingMode = isFront ? 'user' : 'environment';
const videoSourceId = devices.find(
(device) => device.kind === 'videoinput' && device.facing === (isFront ? 'front' : 'environment')
);
const constraints = {
audio: true,
video: {
mandatory: {
minWidth: 500,
minHeight: 300,
minFrameRate: 30,
},
facingMode,
optional: videoSourceId ? [{ sourceId: videoSourceId.deviceId }] : [],
},
};
const newStream = await mediaDevices.getUserMedia(constraints);
setLocalStream(newStream);
};
const startCall = async (id: string) => {
const roomRef = firestore().collection('room').doc(id);
const localPC: RTCPeerConnection = new RTCPeerConnection(configuration);
localStream?.getTracks().forEach((track) => {
localPC.addTrack(track, localStream);
});
const callerCandidatesCollection = roomRef.collection('callerCandidates');
const calleeCandidatesCollection = roomRef.collection('calleeCandidates');
(localPC as any).onicecandidate = (event: { candidate: RTCIceCandidate | null }) => {
if (event.candidate) {
callerCandidatesCollection.add(event.candidate.toJSON());
}
};
(localPC as any).ontrack = (event: any) => {
const newStream = new MediaStream();
event.streams[0].getTracks().forEach((track: any) => {
newStream.addTrack(track);
});
setRemoteStream(newStream);
};
const offer = await localPC.createOffer({});
await localPC.setLocalDescription(offer);
await roomRef.set({ offer, connected: false });
roomRef.onSnapshot(async (snapshot) => {
const data = snapshot.data();
if (!localPC.remoteDescription && data?.answer) {
const answer = new RTCSessionDescription(data.answer);
await localPC.setRemoteDescription(answer);
}
});
calleeCandidatesCollection.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
const data = change.doc.data();
localPC.addIceCandidate(new RTCIceCandidate(data));
}
});
});
setCachedLocalPC(localPC);
};
const switchCamera = () => {
localStream?.getVideoTracks().forEach((track) => track._switchCamera());
};
const toggleMute = () => {
localStream?.getAudioTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsMuted(!track.enabled);
});
};
const toggleCamera = () => {
localStream?.getVideoTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsOffCam(!track.enabled);
});
};
return (
<View style={{ flex: 1 }}>
{remoteStream && <RTCView style={{ flex: 1 }} streamURL={remoteStream.toURL()} objectFit="cover" />}
{localStream && !isOffCam && (
<RTCView
style={{ width: 100, height: 150, position: 'absolute', top: 10, right: 10 }}
streamURL={localStream.toURL()}
/>
)}
<View style={{ position: 'absolute', bottom: 0, width: '100%' }}>
<CallActionBox switchCamera={switchCamera} toggleMute={toggleMute} toggleCamera={toggleCamera} endCall={endCall} />
</View>
</View>
);
}
Step-4: Create JoinCallScreen.tsf File Under src/screens/Audio&VideoScreens/StartCallScreen.tsx
import { useEffect, useState } from 'react';
import { View, Text, Button, Alert } from 'react-native';
import { RTCPeerConnection, mediaDevices, RTCIceCandidate, RTCSessionDescription, MediaStream, MediaStreamTrack } from 'react-native-webrtc';
import firestore from '@react-native-firebase/firestore';
import { configuration } from '../../config/webrtcConfig';
interface JoinScreenProps {
roomId: string;
setScreen: (screen: string) => void;
}
export default function JoinScreen({ roomId, setScreen }: JoinScreenProps) {
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
const [peerConnection, setPeerConnection] = useState<RTCPeerConnection | null>(null);
useEffect(() => {
startLocalStream();
}, []);
async function startLocalStream() {
try {
const stream = await mediaDevices.getUserMedia({ video: true, audio: true });
setLocalStream(stream);
} catch (error) {
console.error('Failed to get local stream:', error);
}
}
async function joinCall() {
const roomRef = firestore().collection('room').doc(roomId);
const roomSnapshot = await roomRef.get();
if (!roomSnapshot.exists) {
Alert.alert('Room does not exist!');
return;
}
const pc = new RTCPeerConnection(configuration);
localStream?.getTracks().forEach((track: MediaStreamTrack) => {
pc.addTrack(track, localStream);
});
const callerCandidatesCollection = roomRef.collection('callerCandidates');
const calleeCandidatesCollection = roomRef.collection('calleeCandidates');
(pc as any).onicecandidate = (event: { candidate: RTCIceCandidate | null }) => {
if (event.candidate) {
callerCandidatesCollection.add(event.candidate.toJSON());
}
};
(pc as any).ontrack = (event: any) => {
const newStream = new MediaStream();
event.streams[0].getTracks().forEach((track: any) => {
newStream.addTrack(track);
});
setRemoteStream(newStream);
};
const offer = roomSnapshot.data()?.offer;
if (!offer) {
console.error('Offer not found in room data');
return;
}
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
await roomRef.update({ answer });
callerCandidatesCollection.onSnapshot((snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
const candidate = new RTCIceCandidate(change.doc.data());
pc.addIceCandidate(candidate);
}
});
});
setPeerConnection(pc);
}
return (
<View>
<Button title="Join Call" onPress={joinCall} />
{remoteStream && <Text>Connected to Call</Text>}
</View>
);
}
Step-5: Define IncommingCallListener.ts file Under src/Services/IncommingCallListner.ts
- You need to define the IncommingCallListner.ts file to listen the incoming call coming for the particular mobile number.
- When you make a call from your device it will create a collection called ‘Calls’ in the firestore database.
- It will check the receiverId if it is matching with the userContact number then the joinCall screen will be displayed.


/**
* @fileoverview IncomingCallListner.tsx Will Listen The Incoming Calls From Firestore And Navigate To JoinCallScreen.tsx
* @author Subrat Kumar Sahoo
* @date 2025-01-30
* @version 1.0.0
*/
import { useEffect } from "react";
import { useNavigation, NavigationContainerRef } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import firestore from "@react-native-firebase/firestore";
import { RootStackParamList } from '../navigation/navigationTypes';
import StartCallScreen from "../screens/Audio&VideoScreens/StartCallScreen";
import JoinCallScreen from "../screens/Audio&VideoScreens/JoinCallScreen";
import { useUser } from '../context/UserContext';
type JoinCallScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'JoinCallScreen'>;
type ContactsScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'ContactsScreen'>;
const IncomingCallListener = ({ navigationRef }: { navigationRef: NavigationContainerRef<any> }) => {
const joinCallScreenNavigation = useNavigation<JoinCallScreenNavigationProp>();
const contactsScreenNavigation = useNavigation<ContactsScreenNavigationProp>();
const { userId, userName, userContact } = useUser();
// ✅ Without Country Code
const removeCountryCode = (phoneNumber: string) => {
return phoneNumber.replace(/^\+?\d+\s?/, "").replace(/\s/g, "");
};
// ✅ With Country Code
const removeNonNumeric = (phoneNumber: string) => {
return phoneNumber.replace(/\D/g, "");
};
useEffect(() => {
const unsubscribe = firestore()
.collection("calls")
.where('receiverId', 'in', [removeCountryCode(userContact), removeNonNumeric(userContact)])
.where("status", "==", "incoming") // Listen for active incoming calls
.onSnapshot((querySnapshot) => {
querySnapshot.forEach((doc) => {
const callData = doc.data();
if (callData) {
console.log("📞 Incoming Call Detected:", callData);
// ➡️ Auto Navigate To JoinCallScreen.
navigationRef.navigate("JoinCallScreen", { callerId: callData.callerId, callerName: callData.callerName });
}
});
});
return () => unsubscribe(); // Cleanup listener
}, [userContact]);
return null;
};
export default IncomingCallListener;
- Then you need to define the IncommingCallListner in the App.tsx file globally to listen to the incoming calls.

/**
* @fileoverview Main Entry Point of the Application
* @author Subrat Kumar Sahoo
* @date 2025-01-30
* @version 1.0.0
*/
import React, { useState, useEffect } from "react";
import { NavigationContainer, useNavigationContainerRef } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import SplashScreen from "./src/screens/SplashScreen"; // Lottie Splash Screen
import LoginScreen from "./src/screens/LoginScreen"; // Login Screen
import HomeScreen from "./src/screens/HomeScreen"; // Home Screen
import UserOnboarding from "./src/components/UserOnboarding";
import JoinCallScreen from "./src/screens/Audio&VideoScreens/JoinCallScreen";
import ContactsScreen from "./src/screens/ContactsScreen";
import DrawerNavigator from "./src/navigation/DrawerNavigator"; // Drawer Navigation
import { UserDetailsProvider } from './src/context/UserContext';
import IncomingCallListener from "./src/services/IncomingCallListener";
const Stack = createNativeStackNavigator();
const App: React.FC = () => {
const navigationRef = useNavigationContainerRef();
/************************************* Lottie Animation Logics Starts******************************* */
/**
* When the app starts, the splash screen (Lottie animation) will appear.
* After the Lottie animation finishes (after 2 seconds in this case),
* the app will navigate to the Login Screen.
*/
const [isLottieFinished, setIsLottieFinished] = useState(false);
useEffect(() => {
// Set a timer for the Lottie animation duration (e.g., 2 seconds)
const timer = setTimeout(() => {
setIsLottieFinished(true);
}, 2000);
return () => clearTimeout(timer); // Cleanup timer on unmount
}, []);
if (!isLottieFinished) {
return <SplashScreen onAnimationFinish={() => setIsLottieFinished(true)} />;
}
/* ************************************ Lottie Animation Logics Ends*********************************** */
/************************************ Application View (Navigation) Starts *********************************** */
return (
<UserDetailsProvider>
<NavigationContainer ref={navigationRef}>
<Stack.Navigator initialRouteName="LoginScreen">
{/* ✅ Login Screen ✅ */}
<Stack.Screen
name="LoginScreen"
component={LoginScreen}
options={{ headerShown: false }}
/>
{/* ✅ Drawer Navigator ✅ */}
<Stack.Screen
name="DrawerNavigator"
component={DrawerNavigator}
options={{ headerShown: false }}
/>
{/* ✅ Incoming Call Screen (Must Be in Stack) ✅ */}
<Stack.Screen
name="JoinCallScreen"
component={JoinCallScreen}
options={{ headerShown: false }}
/>
</Stack.Navigator>
{/* ✅ IncomingCallListener Runs Globally To Detect Calls ✅ */}
<IncomingCallListener navigationRef={navigationRef}/>
</NavigationContainer>
</UserDetailsProvider>
);
/************************************ Application View (Navigation) Ends *********************************** */
};
export default App;