I am having a video chat application using react js in the frontend and i have one SFU server which is based on Node js with WRTC library for supporting webRTC apis in the server side(M87).
App.js
import React, { useState, useRef, useEffect, useCallback } from "react";
import io from "socket.io-client";
import Video from "./Components/Video";
import { WebRTCUser } from "./types";
const pc_config = {
iceServers: [
// {
// urls: 'stun:[STUN_IP]:[PORT]',
// 'credentials': '[YOR CREDENTIALS]',
// 'username': '[USERNAME]'
// },
{
urls: "stun:stun.l.google.com:19302",
},
],
};
const SOCKET_SERVER_URL = "https://192.168.132.29:8080";
const App = () => {
const socketRef = useRef<SocketIOClient.Socket>();
const localStreamRef = useRef<MediaStream>();
const sendPCRef = useRef<RTCPeerConnection>();
const receivePCsRef = useRef<{ [socketId: string]: RTCPeerConnection }>({});
const [users, setUsers] = useState<Array<WebRTCUser>>([]);
const localVideoRef = useRef<HTMLVideoElement>(null);
const closeReceivePC = useCallback((id: string) => {
if (!receivePCsRef.current[id]) return;
receivePCsRef.current[id].close();
delete receivePCsRef.current[id];
}, []);
const createReceiverOffer = useCallback(
async (pc: RTCPeerConnection, senderSocketID: string) => {
try {
const sdp = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
console.log("create receiver offer success");
await pc.setLocalDescription(new RTCSessionDescription(sdp));
if (!socketRef.current) return;
socketRef.current.emit("receiverOffer", {
sdp,
receiverSocketID: socketRef.current.id,
senderSocketID,
roomID: "1234",
});
} catch (error) {
console.log(error);
}
},
[]
);
const createReceiverPeerConnection = useCallback((socketID: string) => {
try {
const pc = new RTCPeerConnection(pc_config);
// add pc to peerConnections object
receivePCsRef.current = { ...receivePCsRef.current, [socketID]: pc };
pc.onicecandidate = (e) => {
if (!(e.candidate && socketRef.current)) return;
console.log("receiver PC onicecandidate");
socketRef.current.emit("receiverCandidate", {
candidate: e.candidate,
receiverSocketID: socketRef.current.id,
senderSocketID: socketID,
});
};
pc.oniceconnectionstatechange = (e) => {
console.log(e);
};
pc.ontrack = (e) => {
console.log("ontrack success");
setUsers((oldUsers) =>
oldUsers
.filter((user) => user.id !== socketID)
.concat({
id: socketID,
stream: e.streams[0],
})
);
};
// return pc
return pc;
} catch (e) {
console.error(e);
return undefined;
}
}, []);
const createReceivePC = useCallback(
(id: string) => {
try {
console.log(`socketID(${id}) user entered`);
const pc = createReceiverPeerConnection(id);
if (!(socketRef.current && pc)) return;
createReceiverOffer(pc, id);
} catch (error) {
console.log(error);
}
},
[createReceiverOffer, createReceiverPeerConnection]
);
const createSenderOffer = useCallback(async () => {
try {
if (!sendPCRef.current) return;
const sdp = await sendPCRef.current.createOffer({
offerToReceiveAudio: false,
offerToReceiveVideo: false,
});
console.log("create sender offer success");
await sendPCRef.current.setLocalDescription(
new RTCSessionDescription(sdp)
);
if (!socketRef.current) return;
socketRef.current.emit("senderOffer", {
sdp,
senderSocketID: socketRef.current.id,
roomID: "1234",
});
} catch (error) {
console.log(error);
}
}, []);
const createSenderPeerConnection = useCallback(() => {
const pc = new RTCPeerConnection(pc_config);
pc.onicecandidate = (e) => {
if (!(e.candidate && socketRef.current)) return;
console.log("sender PC onicecandidate");
socketRef.current.emit("senderCandidate", {
candidate: e.candidate,
senderSocketID: socketRef.current.id,
});
};
pc.oniceconnectionstatechange = (e) => {
console.log(e);
};
if (localStreamRef.current) {
console.log("add local stream");
localStreamRef.current.getTracks().forEach((track) => {
if (!localStreamRef.current) return;
pc.addTrack(track, localStreamRef.current);
});
} else {
console.log("no local stream");
}
sendPCRef.current = pc;
}, []);
const getLocalStream = useCallback(async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: {
width: 240,
height: 240,
},
});
localStreamRef.current = stream;
if (localVideoRef.current) localVideoRef.current.srcObject = stream;
if (!socketRef.current) return;
createSenderPeerConnection();
await createSenderOffer();
socketRef.current.emit("joinRoom", {
id: socketRef.current.id,
roomID: "1234",
});
} catch (e) {
console.log(`getUserMedia error: ${e}`);
}
}, [createSenderOffer, createSenderPeerConnection]);
useEffect(() => {
socketRef.current = io.connect(SOCKET_SERVER_URL);
getLocalStream();
socketRef.current.on("userEnter", (data: { id: string }) => {
createReceivePC(data.id);
});
socketRef.current.on(
"allUsers",
(data: { users: Array<{ id: string }> }) => {
data.users.forEach((user) => createReceivePC(user.id));
}
);
socketRef.current.on("userExit", (data: { id: string }) => {
closeReceivePC(data.id);
setUsers((users) => users.filter((user) => user.id !== data.id));
});
socketRef.current.on(
"getSenderAnswer",
async (data: { sdp: RTCSessionDescription }) => {
try {
if (!sendPCRef.current) return;
console.log("get sender answer");
console.log(data.sdp);
await sendPCRef.current.setRemoteDescription(
new RTCSessionDescription(data.sdp)
);
} catch (error) {
console.log(error);
}
}
);
socketRef.current.on(
"getSenderCandidate",
async (data: { candidate: RTCIceCandidateInit }) => {
try {
if (!(data.candidate && sendPCRef.current)) return;
console.log("get sender candidate");
await sendPCRef.current.addIceCandidate(
new RTCIceCandidate(data.candidate)
);
console.log("candidate add success");
} catch (error) {
console.log(error);
}
}
);
socketRef.current.on(
"getReceiverAnswer",
async (data: { id: string; sdp: RTCSessionDescription }) => {
try {
console.log(`get socketID(${data.id})'s answer`);
const pc: RTCPeerConnection = receivePCsRef.current[data.id];
if (!pc) return;
await pc.setRemoteDescription(data.sdp);
console.log(`socketID(${data.id})'s set remote sdp success`);
} catch (error) {
console.log(error);
}
}
);
socketRef.current.on(
"getReceiverCandidate",
async (data: { id: string; candidate: RTCIceCandidateInit }) => {
try {
console.log(data);
console.log(`get socketID(${data.id})'s candidate`);
const pc: RTCPeerConnection = receivePCsRef.current[data.id];
if (!(pc && data.candidate)) return;
await pc.addIceCandidate(new RTCIceCandidate(data.candidate));
console.log(`socketID(${data.id})'s candidate add success`);
} catch (error) {
console.log(error);
}
}
);
return () => {
if (socketRef.current) {
socketRef.current.disconnect();
}
if (sendPCRef.current) {
sendPCRef.current.close();
}
users.forEach((user) => closeReceivePC(user.id));
};
// eslint-disable-next-line
}, [
closeReceivePC,
createReceivePC,
createSenderOffer,
createSenderPeerConnection,
getLocalStream,
]);
return (
<div>
<video
style={{
width: 240,
height: 240,
margin: 5,
backgroundColor: "black",
}}
muted
ref={localVideoRef}
autoPlay
/>
{users.map((user, index) => (
<Video key={index} stream={user.stream} />
))}
</div>
);
};
export default App;
SFU server
let http = require("http");
let express = require("express");
let cors = require("cors");
let socketio = require("socket.io");
let wrtc = require("wrtc");
const fs = require("fs");
let https = require("https");
const port=8080;
const options = {
key: fs.readFileSync("../cert/cert.priv.key"),
cert: fs.readFileSync("../cert/cert.chain.pem"),
};
const app = express();
const server = https.createServer(options, app);
app.use(cors());
let receiverPCs = {};
let senderPCs = {};
let users = {};
let socketToRoom = {};
const pc_config = {
iceServers: [
// {
// urls: 'stun:[STUN_IP]:[PORT]',
// 'credentials': '[YOR CREDENTIALS]',
// 'username': '[USERNAME]'
// },
{
urls: "stun:stun.l.google.com:19302",
},
],
};
const isIncluded = (array, id) => array.some((item) => item.id === id);
const createReceiverPeerConnection = (socketID, socket, roomID) => {
const pc = new wrtc.RTCPeerConnection(pc_config);
if (receiverPCs[socketID]) receiverPCs[socketID] = pc;
else receiverPCs = { ...receiverPCs, [socketID]: pc };
pc.onicecandidate = (e) => {
//console.log(`socketID: ${socketID}'s receiverPeerConnection icecandidate`);
socket.to(socketID).emit("getSenderCandidate", {
candidate: e.candidate,
});
};
pc.oniceconnectionstatechange = (e) => {
//console.log(e);
};
pc.ontrack = (e) => {
if (users[roomID]) {
if (!isIncluded(users[roomID], socketID)) {
users[roomID].push({
id: socketID,
stream: e.streams[0],
});
} else return;
} else {
users[roomID] = [
{
id: socketID,
stream: e.streams[0],
},
];
}
socket.broadcast.to(roomID).emit("userEnter", { id: socketID });
};
return pc;
};
const createSenderPeerConnection = (
receiverSocketID,
senderSocketID,
socket,
roomID
) => {
const pc = new wrtc.RTCPeerConnection(pc_config);
if (senderPCs[senderSocketID]) {
senderPCs[senderSocketID].filter((user) => user.id !== receiverSocketID);
senderPCs[senderSocketID].push({ id: receiverSocketID, pc });
} else
senderPCs = {
...senderPCs,
[senderSocketID]: [{ id: receiverSocketID, pc }],
};
pc.onicecandidate = (e) => {
//console.log(`socketID: ${receiverSocketID}'s senderPeerConnection icecandidate`);
socket.to(receiverSocketID).emit("getReceiverCandidate", {
id: senderSocketID,
candidate: e.candidate,
});
};
pc.oniceconnectionstatechange = (e) => {
//console.log(e);
};
const sendUser = users[roomID].filter(
(user) => user.id === senderSocketID
)[0];
sendUser.stream.getTracks().forEach((track) => {
pc.addTrack(track, sendUser.stream);
});
return pc;
};
const getOtherUsersInRoom = (socketID, roomID) => {
let allUsers = [];
if (!users[roomID]) return allUsers;
allUsers = users[roomID]
.filter((user) => user.id !== socketID)
.map((otherUser) => ({ id: otherUser.id }));
return allUsers;
};
const deleteUser = (socketID, roomID) => {
if (!users[roomID]) return;
users[roomID] = users[roomID].filter((user) => user.id !== socketID);
if (users[roomID].length === 0) {
delete users[roomID];
}
delete socketToRoom[socketID];
};
const closeReceiverPC = (socketID) => {
if (!receiverPCs[socketID]) return;
receiverPCs[socketID].close();
delete receiverPCs[socketID];
};
const closeSenderPCs = (socketID) => {
if (!senderPCs[socketID]) return;
senderPCs[socketID].forEach((senderPC) => {
senderPC.pc.close();
const eachSenderPC = senderPCs[senderPC.id].filter(
(sPC) => sPC.id === socketID
)[0];
if (!eachSenderPC) return;
eachSenderPC.pc.close();
senderPCs[senderPC.id] = senderPCs[senderPC.id].filter(
(sPC) => sPC.id !== socketID
);
});
delete senderPCs[socketID];
};
const io = socketio.listen(server);
io.sockets.on("connection", (socket) => {
socket.on("joinRoom", (data) => {
try {
let allUsers = getOtherUsersInRoom(data.id, data.roomID);
io.to(data.id).emit("allUsers", { users: allUsers });
} catch (error) {
console.log(error);
}
});
socket.on("senderOffer", async (data) => {
try {
socketToRoom[data.senderSocketID] = data.roomID;
let pc = createReceiverPeerConnection(
data.senderSocketID,
socket,
data.roomID
);
await pc.setRemoteDescription(data.sdp);
let sdp = await pc.createAnswer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
await pc.setLocalDescription(sdp);
socket.join(data.roomID);
io.to(data.senderSocketID).emit("getSenderAnswer", { sdp });
} catch (error) {
console.log(error);
}
});
socket.on("senderCandidate", async (data) => {
try {
let pc = receiverPCs[data.senderSocketID];
await pc.addIceCandidate(new wrtc.RTCIceCandidate(data.candidate));
} catch (error) {
console.log(error);
}
});
socket.on("receiverOffer", async (data) => {
try {
let pc = createSenderPeerConnection(
data.receiverSocketID,
data.senderSocketID,
socket,
data.roomID
);
await pc.setRemoteDescription(data.sdp);
let sdp = await pc.createAnswer({
offerToReceiveAudio: false,
offerToReceiveVideo: false,
});
await pc.setLocalDescription(sdp);
io.to(data.receiverSocketID).emit("getReceiverAnswer", {
id: data.senderSocketID,
sdp,
});
} catch (error) {
console.log(error);
}
});
socket.on("receiverCandidate", async (data) => {
try {
const senderPC = senderPCs[data.senderSocketID].filter(
(sPC) => sPC.id === data.receiverSocketID
)[0];
await senderPC.pc.addIceCandidate(
new wrtc.RTCIceCandidate(data.candidate)
);
} catch (error) {
console.log(error);
}
});
socket.on("disconnect", () => {
try {
let roomID = socketToRoom[socket.id];
deleteUser(socket.id, roomID);
closeReceiverPC(socket.id);
closeSenderPCs(socket.id);
socket.broadcast.to(roomID).emit("userExit", { id: socket.id });
} catch (error) {
console.log(error);
}
});
});
startServer(port);
function startServer(port) {
server.listen(port, () => {
console.log(`[INFO] Server app listening on port ${port}`);
});
}
The app works super fine in almost all browsers in windows, linux and android
But the pc.ontrack event in the client side is not working on iOS based devices like ipad, iphones
I have tested in iOS 16, 15
pc.ontrack : https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/track_event
why the pc.ontrack function is not triggering in ios devices?
Some additional information: In other devices, we printed sdpoffer it is creating
const sdp = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: false,
});
console.log(sdp.sdp);
In Ipad (chrome,safari), it prints:
v=0\r\no=- 3589652411242909067 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS\r\n
But in windows (chrome)
o=- 3108997868662078948 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=extmap-allow-mixed
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 613 91 01 81 131 110 126
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:01c2
a=ice-pwd:ZfgdfZTGRY4A6dasdyAdsfsdfy2geY0JP3VK6q
a=ice-options:trickle
a=fingerprint:sha-256 3F:F0:44:20:11:A6:C5:9D:D7:46:08:AD:4C:7F:60:5C:00:A1:F8
a=setup:actpass
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
a=recvonly
a=rtcp-mux
a=rtpmap:111 opus/48000/2
a=rtcp-fb:111 transport-cc
a=fmtp:111 minptime=10;useinbandfec=1
a=rtpmap:63 red/48000/2
a=fmtp:63 111/111
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:13 CN/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:126 telephone-event/8000