When i click start meeting, i see the following error in terminal. When i call first time i dont see error, but after ending call when i start meeting second time then i see this error:
WARN Possible Unhandled Promise Rejection (id: 115):
Error: The remote description was null
after this error when other user joins call he get’s thefollowing error:
Your app just crashed. See the error below. java.lang.NullPointerException: Attempt to invoke virtual method 'org.webrtc.PeerConnection com.oney.WebRTCModule.PeerConnectionObserver.getPeerConnection()' on a null object reference com.oney.WebRTCModule.WebRTCModule.lambda$peerConnectionSetRemoteDescription$25$com-oney-WebRTCModule-WebRTCModule(WebRTCModule.java:1051) com.oney.WebRTCModule.WebRTCModule$$ExternalSyntheticLambda21.run(Unknown Source:8) java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) java.lang.Thread.run(Thread.java:1012)
I do i get rid of these errors
Here’re my Video Call Components:
JoinScreen.jsx :
import React, { useState, useEffect } from "react";
import { Text, StyleSheet, Button, View } from "react-native";
import {
RTCPeerConnection,
RTCView,
mediaDevices,
RTCIceCandidate,
RTCSessionDescription,
MediaStream,
} from "react-native-webrtc";
import { db } from "../firebase";
import {
addDoc,
collection,
doc,
setDoc,
getDoc,
updateDoc,
onSnapshot,
deleteField,
} from "firebase/firestore";
import CallActionBox from "./CallActionBox";
const configuration = {
iceServers: [
{
urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"],
},
],
iceCandidatePoolSize: 10,
};
export default function JoinScreen({ roomId, screens, setScreen }) {
const [localStream, setLocalStream] = useState();
const [remoteStream, setRemoteStream] = useState();
const [cachedLocalPC, setCachedLocalPC] = useState();
const [isMuted, setIsMuted] = useState(false);
const [isOffCam, setIsOffCam] = useState(false);
//Automatically start stream
useEffect(() => {
startLocalStream();
}, []);
useEffect(() => {
if (localStream) {
joinCall(roomId);
}
}, [localStream]);
//End call button
async function endCall() {
if (cachedLocalPC) {
const senders = cachedLocalPC.getSenders();
senders.forEach((sender) => {
cachedLocalPC.removeTrack(sender);
});
cachedLocalPC.close();
}
const roomRef = doc(db, "room", roomId);
await updateDoc(roomRef, { answer: deleteField(), connected: false });
setLocalStream();
setRemoteStream(); // set remoteStream to null or empty when callee leaves the call
setCachedLocalPC();
// cleanup
setScreen(screens.ROOM); //go back to room screen
}
//start local webcam on your device
const startLocalStream = async () => {
// isFront will determine if the initial camera should face user or environment
const isFront = true;
const devices = await mediaDevices.enumerateDevices();
const facing = isFront ? "front" : "environment";
const videoSourceId = devices.find(
(device) => device.kind === "videoinput" && device.facing === facing
);
const facingMode = isFront ? "user" : "environment";
const constraints = {
audio: true,
video: {
mandatory: {
minWidth: 500, // Provide your own width, height and frame rate here
minHeight: 300,
minFrameRate: 30,
},
facingMode,
optional: videoSourceId ? [{ sourceId: videoSourceId }] : [],
},
};
const newStream = await mediaDevices.getUserMedia(constraints);
setLocalStream(newStream);
};
//join call function
const joinCall = async (id) => {
const roomRef = doc(db, "room", id);
const roomSnapshot = await getDoc(roomRef);
if (!roomSnapshot.exists) return;
const localPC = new RTCPeerConnection(configuration);
localStream.getTracks().forEach((track) => {
localPC.addTrack(track, localStream);
});
const callerCandidatesCollection = collection(roomRef, "callerCandidates");
const calleeCandidatesCollection = collection(roomRef, "calleeCandidates");
localPC.addEventListener("icecandidate", (e) => {
if (!e.candidate) {
console.log("Got final candidate!");
return;
}
addDoc(calleeCandidatesCollection, e.candidate.toJSON());
});
localPC.ontrack = (e) => {
const newStream = new MediaStream();
e.streams[0].getTracks().forEach((track) => {
newStream.addTrack(track);
});
setRemoteStream(newStream);
};
const offer = roomSnapshot.data().offer;
await localPC.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await localPC.createAnswer();
await localPC.setLocalDescription(answer);
await updateDoc(roomRef, { answer, connected: true }, { merge: true });
onSnapshot(callerCandidatesCollection, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
let data = change.doc.data();
localPC.addIceCandidate(new RTCIceCandidate(data));
}
});
});
onSnapshot(roomRef, (doc) => {
const data = doc.data();
if (!data.answer) {
setScreen(screens.ROOM);
}
});
setCachedLocalPC(localPC);
};
const switchCamera = () => {
localStream.getVideoTracks().forEach((track) => track._switchCamera());
};
// Mutes the local's outgoing audio
const toggleMute = () => {
if (!remoteStream) {
return;
}
localStream.getAudioTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsMuted(!track.enabled);
});
};
const toggleCamera = () => {
localStream.getVideoTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsOffCam(!isOffCam);
});
};
return (
<View className="flex-1">
<RTCView
className="flex-1"
streamURL={remoteStream && remoteStream.toURL()}
objectFit={"cover"}
/>
{remoteStream && !isOffCam && (
<RTCView
className="w-32 h-48 absolute right-6 top-8"
streamURL={localStream && localStream.toURL()}
/>
)}
<View className="absolute bottom-0 w-full">
<CallActionBox
switchCamera={switchCamera}
toggleMute={toggleMute}
toggleCamera={toggleCamera}
endCall={endCall}
/>
</View>
</View>
);
}
RoomScreen.jsx :
import React, { useEffect, useState } from "react";
import { Text, View, TextInput, TouchableOpacity, Alert } from "react-native";
import { db } from "../firebase";
import {
addDoc,
collection,
doc,
setDoc,
getDoc,
updateDoc,
onSnapshot,
deleteField,
} from "firebase/firestore";
import { useDataContext } from "./DataContext";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function RoomScreen({ setScreen, screens, setRoomId, roomId }) {
const { userData, currentUserData, videoCallEmail, setVideoCallEmail, serverAddress, fetchCurrentUserByEmail } = useDataContext()
const onCallOrJoin = async (screen) => {
if (roomId.length > 0) {
setScreen(screen);
}
try {
const response = await fetch(`${serverAddress}/api/update-receiving-call/${videoCallEmail}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
console.log('Updated Receiving Call of another user');
} else {
console.error('Error updating receiving call status');
}
} catch (error) {
console.error('Error updating receiving call status:', error);
}
};
//checks if room is existing
const checkMeeting = async () => {
if (roomId) {
const roomRef = doc(db, "room", roomId);
const roomSnapshot = await getDoc(roomRef);
// console.log(roomSnapshot.data());
if (!roomSnapshot.exists() || roomId === "") {
// console.log(`Room ${roomId} does not exist.`);
Alert.alert("Wait for your instructor to start the meeting.");
return;
} else {
onCallOrJoin(screens.JOIN);
}
} else {
Alert.alert("Provide a valid Room ID.");
}
};
return (
<View>
{/* <Text className="text-2xl font-bold text-center">Enter Room ID:</Text>
<TextInput
className="bg-white border-sky-600 border-2 mx-5 my-3 p-2 rounded-md"
value={roomId}
onChangeText={setRoomId}
/> */}
<View className="gap-y-3 mx-5 mt-2">
<TouchableOpacity
className="bg-sky-300 p-2 rounded-md"
onPress={() => onCallOrJoin(screens.CALL)}
>
<Text className="color-black text-center text-xl font-bold ">
Start meeting
</Text>
</TouchableOpacity>
{
(currentUserData.email == videoCallEmail) &&
<TouchableOpacity
className="bg-sky-300 p-2 rounded-md"
onPress={() => checkMeeting()}
>
<Text className="color-black text-center text-xl font-bold ">
Join meeting
</Text>
</TouchableOpacity>
}
</View>
</View>
);
}
VideoCall.jsx:
import React, { useState } from "react";
import { Text, SafeAreaView } from "react-native";
import RoomScreen from "./RoomScreen";
import CallScreen from "./CallScreen";
import JoinScreen from "./JoinScreen";
// Just to handle navigation
export default function VideoCall() {
const screens = {
ROOM: "JOIN_ROOM",
CALL: "CALL",
JOIN: "JOIN",
};
const [screen, setScreen] = useState(screens.ROOM);
const [roomId, setRoomId] = useState("myroom");
let content;
switch (screen) {
case screens.ROOM:
content = (
<RoomScreen
roomId={roomId}
setRoomId={setRoomId}
screens={screens}
setScreen={setScreen}
/>
);
break;
case screens.CALL:
content = (
<CallScreen roomId={roomId} screens={screens} setScreen={setScreen} />
);
break;
case screens.JOIN:
content = (
<JoinScreen roomId={roomId} screens={screens} setScreen={setScreen} />
);
break;
default:
content = <Text>Wrong Screen</Text>;
}
return (
<SafeAreaView className="flex-1 justify-center ">{content}</SafeAreaView>
);
}
**CallScreen.jsx**:
import React, { useState, useEffect } from "react";
import { View } from "react-native";
import {
RTCPeerConnection,
RTCView,
mediaDevices,
RTCIceCandidate,
RTCSessionDescription,
MediaStream,
} from "react-native-webrtc";
import { db } from "../firebase";
import {
addDoc,
collection,
doc,
setDoc,
getDoc,
updateDoc,
onSnapshot,
deleteField,
} from "firebase/firestore";
import CallActionBox from "./CallActionBox";
const configuration = {
iceServers: [
],
iceCandidatePoolSize: 10,
};
export default function CallScreen({ roomId, screens, setScreen }) {
const [localStream, setLocalStream] = useState();
const [remoteStream, setRemoteStream] = useState();
const [cachedLocalPC, setCachedLocalPC] = useState();
const [isMuted, setIsMuted] = useState(false);
const [isOffCam, setIsOffCam] = useState(false);
useEffect(() => {
return () => {
setLocalStream(null);
setRemoteStream(null);
setCachedLocalPC(null);
};
}, []);
useEffect(() => {
startLocalStream();
}, []);
useEffect(() => {
if (localStream && roomId) {
startCall(roomId);
}
}, [localStream, roomId]);
//End call button
async function endCall() {
try {
if (cachedLocalPC) {
const senders = cachedLocalPC.getSenders();
senders.forEach((sender) => {
cachedLocalPC.removeTrack(sender);
});
cachedLocalPC.close();
}
const roomRef = doc(db, "room", roomId);
await updateDoc(roomRef, { answer: deleteField() });
setLocalStream(null);
setRemoteStream(null);
setCachedLocalPC(null);
setScreen(screens.ROOM);
} catch (error) {
console.error("Error ending call:", error);
}
}
//start local webcam on your device
const startLocalStream = async () => {
try {
const isFront = true;
const devices = await mediaDevices.enumerateDevices();
const facing = isFront ? "front" : "environment";
const videoSourceId = devices.find(
(device) => device.kind === "videoinput" && device.facing === facing
);
const facingMode = isFront ? "user" : "environment";
const constraints = {
audio: true,
video: {
mandatory: {
minWidth: 500,
minHeight: 300,
minFrameRate: 30,
},
facingMode,
optional: videoSourceId ? [{ sourceId: videoSourceId }] : [],
},
};
const newStream = await mediaDevices.getUserMedia(constraints);
setLocalStream(newStream);
} catch (error) {
console.error("Error starting local stream:", error);
}
};
const startCall = async (id) => {
const localPC = new RTCPeerConnection(configuration);
localStream.getTracks().forEach((track) => {
localPC.addTrack(track, localStream);
});
const roomRef = doc(db, "room", id);
const callerCandidatesCollection = collection(roomRef, "callerCandidates");
const calleeCandidatesCollection = collection(roomRef, "calleeCandidates");
localPC.addEventListener("icecandidate", (e) => {
if (!e.candidate) {
console.log("Got final candidate!");
return;
}
addDoc(callerCandidatesCollection, e.candidate.toJSON());
});
localPC.ontrack = (e) => {
const newStream = new MediaStream();
e.streams[0].getTracks().forEach((track) => {
newStream.addTrack(track);
});
setRemoteStream(newStream);
};
const offer = await localPC.createOffer();
await localPC.setLocalDescription(offer);
await setDoc(roomRef, { offer, connected: false }, { merge: true });
// Listen for remote answer
onSnapshot(roomRef, (doc) => {
const data = doc.data();
if (!localPC.currentRemoteDescription && data.answer) {
try {
const rtcSessionDescription = new RTCSessionDescription(data.answer);
localPC.setRemoteDescription(rtcSessionDescription);
} catch (error) {
console.error('Error setting remote description:', error);
}
}
else {
setRemoteStream(null); // set remoteStream to null when caller leaves the call
}
});
// when answered, add candidate to peer connection
onSnapshot(calleeCandidatesCollection, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
let data = change.doc.data();
localPC.addIceCandidate(new RTCIceCandidate(data));
}
});
});
setCachedLocalPC(localPC);
};
const switchCamera = () => {
localStream.getVideoTracks().forEach((track) => track._switchCamera());
};
// Mutes the local's outgoing audio
const toggleMute = () => {
if (!remoteStream) {
return;
}
localStream.getAudioTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsMuted(!track.enabled);
});
};
const toggleCamera = () => {
localStream.getVideoTracks().forEach((track) => {
track.enabled = !track.enabled;
setIsOffCam(!isOffCam);
});
};
return (
<View className="flex-1 bg-red-600">
{!remoteStream && (
<RTCView
className="flex-1"
streamURL={localStream && localStream.toURL()}
objectFit={"cover"}
/>
)}
{remoteStream && (
<>
<RTCView
className="flex-1"
streamURL={remoteStream && remoteStream.toURL()}
objectFit={"cover"}
/>
{!isOffCam && (
<RTCView
className="w-32 h-48 absolute right-6 top-8"
streamURL={localStream && localStream.toURL()}
/>
)}
</>
)}
<View className="absolute bottom-0 w-full">
<CallActionBox
switchCamera={switchCamera}
toggleMute={toggleMute}
toggleCamera={toggleCamera}
endCall={endCall}
/>
</View>
</View>
);
}
I’m using expo 49 with firebase