I’m try to implement 1 to 1 video call in latest react-native version with socket.io-client but can’t render remote string. I don’t know is it because of latest version of react-native or anything else. Here is my server and room file code.
Server.js
const express = require("express");
const { createServer } = require("http");
const { Server } = require('socket.io');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: "*" },
// transports: ["websocket"],
connectTimeout: 2 * 60 * 1000,
});
const port = process.env.PORT || 8089;
httpServer.listen(port, () => {
console.log('Server listening at port %d', port);
});
app.get("/", (req, res) => {
return res.send("<h1>Connected</h1>");
});
io.on('connection', (socket) => {
console.log("connected", socket.id)
socket.on('disconnect', () => {
console.log(`Disconnected: ${socket.id}`);
});
socket.on("join", (roomId) => {
socket.join(roomId);
});
socket.on('offer', function (data) {
const { roomId = '' } = data;
io.to(roomId).emit('received-offer', data);
});
socket.on('offers-answer', function (data) {
const { roomId = '' } = data;
io.to(roomId).emit('offers-answer', data);
});
socket.on('candidate', function (data) {
const { roomId = '' } = data;
io.to(roomId).emit('candidate', data);
});
socket.on('peer:nego:needed', function (data) {
const { roomId = '' } = data;
io.to(roomId).emit('peer:nego:needed', data);
});
socket.on('peer:nego:done', function (data) {
const { roomId = '' } = data;
io.to(roomId).emit('peer:nego:done', data);
});
socket.on("connection_error", (err) => {
console.log("connection_error", err);
})
});
server package.json
{
"name": "wcam",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"nodemon": "^3.0.1",
"socket.io": "^4.7.1"
}
}
React Native
Room.tsx
import React, {
useRef,
useState,
useEffect,
useCallback,
MutableRefObject,
} from "react";
import {
Text,
View,
Pressable,
StyleSheet,
SafeAreaView,
} from "react-native";
import {
RTCView,
MediaStream,
mediaDevices,
RTCIceCandidate,
RTCPeerConnection,
RTCSessionDescription,
} from "react-native-webrtc";
import io, { Socket } from "socket.io-client";
import { RTCSessionDescriptionInit } from "react-native-webrtc/lib/typescript/RTCSessionDescription";
import { RoomScreenProps } from "../navigation/NavigationType";
const options = {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true,
VoiceActivityDetection: true
}
};
const servers = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
],
};
const RoomView = ({ navigation, route }: RoomScreenProps) => {
const roomId = "ABC123";
const { type } = route?.params ?? {};
const socket = useRef(null) as MutableRefObject<Socket | null>;
const peer = useRef(null) as MutableRefObject<RTCPeerConnection | null>;
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null);
const [remoteCandidates, setRemoteCandidates] = useState<RTCIceCandidate[]>([]);
let connected = false;
const handleTracks = useCallback((event: any) => {
// console.log("GOT TRACKS!!", JSON.stringify(event));
const [stream] = event?.streams ?? [];
setRemoteStream(stream);
}, []);
useEffect(() => {
connectSocket();
getLocalMedia();
return () => {
if (peer.current) {
peer.current.close();
}
if (socket.current) {
socket.current?.disconnect();
}
connected = false;
}
}, []);
useEffect(() => {
if (localStream) {
setPeerConnection();
}
return () => {
if (localStream?.getTracks().length) {
localStream?.getTracks().forEach(t => t.stop());
}
}
}, [localStream]);
const getLocalMedia = async () => {
try {
const local = await mediaDevices.getUserMedia({
video: true,
audio: true,
});
setLocalStream(local);
} catch (error) {
console.log("getLocalMedia error", error);
}
};
const handleNegoNeeded = async () => {
if (!peer.current) return;
try {
const offer = await peer.current?.createOffer(options);
await peer.current.setLocalDescription(new RTCSessionDescription(offer));
socket.current?.emit("peer:nego:needed", { offer, roomId });
} catch (error) {
console.log("handleNegoNeeded error", error);
}
};
useEffect(() => {
if (connected && localStream && localStream?.getTracks().length) {
peer.current?.addEventListener("negotiationneeded", handleNegoNeeded);
}
return () => {
if (connected && localStream && localStream?.getTracks().length) {
peer.current?.removeEventListener("negotiationneeded", handleNegoNeeded);
}
};
}, [handleNegoNeeded, peer.current, localStream, connected]);
const connectSocket = () => {
if (socket.current != null) socket.current = null;
socket.current = io("http://192.168.0.195:8089", {
path: "/socket.io",
transports: ["polling"],
forceNew: true,
upgrade: true,
reconnectionAttempts: 3,
timeout: 2 * 60 * 1000,
});
socket.current.connect();
socket.current.on("connect", () => {
socket.current?.emit("join", roomId);
setTimeout(() => {
connected = true;
}, 1000);
});
socket.current.on("connect_error", (err: any) => {
console.log("socket error", err);
if (err == 'timeout') {
socket.current?.connect();
}
});
};
const setPeerConnection = async () => {
try {
peer.current = new RTCPeerConnection(servers);
peer.current?.addEventListener("track", handleTracks);
peer.current?.addEventListener("icecandidate", (e: any) => {
if (e?.candidate) {
socket.current?.emit("candidate", { roomId, candidate: e?.candidate ?? {} })
}
});
localStream?.getTracks().forEach(track => {
peer.current?.addTrack(track, localStream);
});
const offer = await peer.current?.createOffer(options);
await peer.current?.setLocalDescription(new RTCSessionDescription(offer));
socket.current?.emit("offer", { roomId, offer });
} catch (error) {
console.log("setPeerConnection error", error);
}
};
const processCandidates = () => {
if (remoteCandidates.length < 1) { return; };
remoteCandidates.map((candidate: any) => peer.current?.addIceCandidate(candidate));
setRemoteCandidates([]);
};
const handleReceivedOffer = useCallback(async ({ offer }: { offer: RTCSessionDescription | RTCSessionDescriptionInit }) => {
if (!peer.current) return;
try {
await peer.current.setRemoteDescription(offer);
const ans = await peer.current.createAnswer();
await peer.current.setLocalDescription(new RTCSessionDescription(ans));
processCandidates();
socket.current?.emit("offers-answer", { roomId, ans });
} catch (error) {
console.log("handleReceivedOffer error", error);
}
}, [socket.current]);
const handleOfferAnswer = useCallback(async ({ ans }: { ans: RTCSessionDescriptionInit }) => {
if (!peer.current) return;
try {
await peer.current.setLocalDescription(ans);
} catch (error) { }
}, []);
const handleNegoNeedIncomming = useCallback(async ({ offer }: { offer: RTCSessionDescription | RTCSessionDescriptionInit }) => {
if (!peer.current) return;
try {
await peer.current.setRemoteDescription(offer);
const ans = await peer.current.createAnswer();
await peer.current.setLocalDescription(new RTCSessionDescription(ans));
socket.current?.emit("peer:nego:done", { roomId, ans });
} catch (error) {
console.log("handleNegoNeedIncomming error", error);
}
}, [socket.current]);
const handleNegoNeedFinal = useCallback(async ({ ans }: { ans: RTCSessionDescriptionInit }) => {
if (!peer.current) return;
try {
await peer.current.setLocalDescription(ans);
} catch (error) {
console.log("handleNegoNeedFinal error", error);
}
}, []);
const handleCandidate = useCallback(({ candidate }: any) => {
try {
const iceCandidate = new RTCIceCandidate(candidate);
if (peer.current?.remoteDescription == null) {
setRemoteCandidates(prevCandidate => [...prevCandidate, iceCandidate]);
return;
}
return peer.current?.addIceCandidate(iceCandidate);
} catch (error) {
console.log("handleCandidate error", error);
}
}, []);
useEffect(() => {
if (socket.current) {
socket.current.on("received-offer", handleReceivedOffer);
socket.current.on("offers-answer", handleOfferAnswer);
socket.current.on("peer:nego:needed", handleNegoNeedIncomming);
socket.current.on("peer:nego:done", handleNegoNeedFinal);
socket.current.on("candidate", handleCandidate);
}
return () => {
if (socket.current) {
socket.current.off("received-offer", handleReceivedOffer);
socket.current.off("offers-answer", handleOfferAnswer);
socket.current.off("peer:nego:needed", handleNegoNeedIncomming);
socket.current.off("peer:nego:done", handleNegoNeedFinal);
socket.current.off("candidate", handleCandidate);
}
}
}, [
socket.current,
handleReceivedOffer,
handleOfferAnswer,
handleCandidate,
handleNegoNeedIncomming,
handleNegoNeedFinal,
]);
return (
<SafeAreaView style={styles.container}>
<Pressable onPress={() => navigation.pop()}>
<Text style={{ color: "#000" }}>Back</Text>
</Pressable>
<View style={styles.videoViewStyle}>
{
localStream ? (
<>
<View style={{ borderWidth: 5, height: "49%", width: "100%" }}>
<RTCView
zOrder={0}
objectFit="contain"
style={{ flex: 1 }}
streamURL={localStream.toURL()}
/>
</View>
</>
) : null
}
{
remoteStream ? (
<View style={{ borderWidth: 5, height: "49%", width: "100%" }}>
<RTCView
zOrder={1}
style={{ flex: 1 }}
streamURL={remoteStream.toURL()}
/>
</View>
) : null
}
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
videoViewStyle: {
flex: 1,
padding: 10,
justifyContent: "space-between",
},
});
export default RoomView;
package.json
{
"name": "WCam",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
},
"dependencies": {
"@react-navigation/native": "^6.1.7",
"@react-navigation/native-stack": "^6.9.13",
"react": "18.2.0",
"react-native": "0.72.3",
"react-native-safe-area-context": "^4.7.1",
"react-native-screens": "^3.22.1",
"react-native-webrtc": "^111.0.3",
"socket.io-client": "^4.7.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@react-native/eslint-config": "^0.72.2",
"@react-native/metro-config": "^0.72.9",
"@tsconfig/react-native": "^3.0.0",
"@types/react": "^18.0.24",
"@types/react-test-renderer": "^18.0.0",
"babel-jest": "^29.2.1",
"eslint": "^8.19.0",
"jest": "^29.2.1",
"metro-react-native-babel-preset": "0.76.7",
"prettier": "^2.4.1",
"react-test-renderer": "18.2.0",
"typescript": "4.8.4"
},
"engines": {
"node": ">=16"
}
}
System:
OS: macOS 12.6
CPU: (8) arm64 Apple M1
Memory: 94.75 MB / 8.00 GB
Shell:
version: 5.8.1
path: /bin/zsh
Binaries:
Node:
version: 16.20.1
path: /usr/local/bin/node
Yarn:
version: 1.22.19
path: /opt/homebrew/bin/yarn
npm:
version: 9.7.2
path: /opt/homebrew/bin/npm
Watchman:
version: 2023.06.12.00
path: /opt/homebrew/bin/watchman
Managers:
CocoaPods:
version: 1.12.1
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 22.2
- iOS 16.2
- macOS 13.1
- tvOS 16.1
- watchOS 9.1
Android SDK: Not Found
IDEs:
Android Studio: 2022.1 AI-221.6008.13.2211.9619390
Xcode:
version: 14.2/14C18
path: /usr/bin/xcodebuild
Languages:
Java:
version: 11.0.19
path: /usr/bin/javac
Ruby:
version: 3.2.2
path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 18.2.0
wanted: 18.2.0
react-native:
installed: 0.72.3
wanted: 0.72.3
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: false
iOS:
hermesEnabled: true
newArchEnabled: false