import React, { useEffect, useRef, useState } from "react";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import { io } from "socket.io-client";
import "../styles/VC.css";
const server_Url = "http://localhost:8080";
let connections = {};
let makingOffer = {};
let ignoreOffer = {};
let isPolite = {};
const peerConfigConnections = {
iceServers: [{ urls: "stun:stun1.l.google.com:19302" }],
};
export default function VideoConference() {
const socketRef = useRef();
const socketIdRef = useRef();
const localVideoRef = useRef();
const videoRef = useRef([]);
const [videoAvailable, setVideoAvailable] = useState(true);
const [audioAvailable, setAudioAvailable] = useState(true);
const [video, setVideo] = useState([]);
const [audio, setAudio] = useState();
const [screenAvailable, setScreenAvailable] = useState();
const [askForUsername, setAskForUsername] = useState(true);
const [username, setUsername] = useState("");
const [videos, setVideos] = useState([]);
useEffect(() => {
getPermissions();
}, []);
const getPermissions = async () => {
try {
const videoStream = await navigator.mediaDevices.getUserMedia({
video: true,
});
setVideoAvailable(true);
console.log("video permission granted!");
const audioStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});
setAudioAvailable(true);
console.log("audio permission granted!");
setScreenAvailable(!!navigator.mediaDevices.getDisplayMedia);
const userMediaStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
window.localStream = userMediaStream;
if (localVideoRef.current) {
localVideoRef.current.srcObject = userMediaStream;
}
} catch (e) {
console.log("Permission error:", e);
setVideoAvailable(false);
setAudioAvailable(false);
}
};
useEffect(() => {
if (video !== undefined || audio !== undefined) {
getUserMedia();
}
}, [audio, video]);
const silence = () => {
let ctx = new AudioContext();
let oscillator = ctx.createOscillator();
oscillator.connect(ctx.destination);
oscillator.start();
ctx.resume();
return Object.assign(new MediaStreamTrack(), { enabled: false });
};
const black = ({ width = 333, height = 600 } = {}) => {
let canvas = Object.assign(document.createElement("canvas"), {
width,
height,
});
canvas.getContext("2d").fillRect(0, 0, width, height);
let stream = canvas.captureStream();
return Object.assign(stream.getVideoTracks()[0], { enabled: false });
};
const connect = () => {
setAskForUsername(false);
setVideo(videoAvailable);
setAudio(audioAvailable);
connectToSocketServer();
getUserMedia();
};
const connectToSocketServer = () => {
socketRef.current = io(server_Url);
socketRef.current.on("connect", () => {
socketIdRef.current = socketRef.current.id;
socketRef.current.emit("join-call", window.location.href);
});
socketRef.current.on("signal", gotMessageFromServer);
socketRef.current.on("user-left", (id) => {
setVideos((videos) => videos.filter((video) => video.socketId !== id));
});
socketRef.current.on("user-joined", (id, clients) => {
clients.forEach((socketListId) => {
if (connections[socketListId]) return;
// CREATING CONNECTION
connections[socketListId] = new RTCPeerConnection(
peerConfigConnections
);
makingOffer[socketListId] = false;
ignoreOffer[socketListId] = false;
isPolite[socketListId] = socketIdRef.current < socketListId;
connections[socketListId].onicecandidate = (event) => {
if (event.candidate) {
socketRef.current.emit(
"signal",
socketListId,
JSON.stringify({ ice: event.candidate })
);
}
};
connections[socketListId].onnegotiationneeded = async () => {
try {
makingOffer[socketListId] = true;
const offer = await connections[socketListId].createOffer();
await connections[socketListId].setLocalDescription(offer);
socketRef.current.emit(
"signal",
socketListId,
JSON.stringify({
sdp: connections[socketListId].localDescription,
})
);
} catch (err) {
console.error(err);
} finally { WITHOUT THIS BLOCK OUR APP ASSUMES THAT WE ARE STILL
makingOffer[socketListId] = false; SENDING SIGNAL CAUSING OFFER COLLISION
}
};
connections[socketListId].ontrack = (event) => {
// TO AVOID DUPLICATES
setVideos((prev) => {
const updated = prev.filter((v) => v.socketId !== socketListId);
return [
...updated,
{ socketId: socketListId, stream: event.streams[0] },
];
});
};
// const localStream = window.localStream || new MediaStream([black(), silence()]);
const localStream = window.localStream;
localStream
.getTracks()
.forEach((track) =>
connections[socketListId].addTrack(track, localStream)
);
});
});
};
const gotMessageFromServer = async (fromId, message) => {
const signal = JSON.parse(message);
const pc = connections[fromId];
const polite = isPolite[fromId];
try {
if (signal.sdp) {
const desc = new RTCSessionDescription(signal.sdp);
const offerCollision =
signal.sdp.type === "offer" &&
(makingOffer[fromId] || pc.signalingState !== "stable");
ignoreOffer[fromId] = !polite && offerCollision;
if (ignoreOffer[fromId]) return; AVOIDING COLLISION
await pc.setRemoteDescription(desc);
if (signal.sdp.type === "offer") {
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socketRef.current.emit(
"signal",
fromId,
JSON.stringify({ sdp: pc.localDescription })
);
}
}
if (signal.ice) {
try {
await pc.addIceCandidate(new RTCIceCandidate(signal.ice));
} catch (err) {
if (!ignoreOffer[fromId]) console.error("Error adding ICE", err);
}
}
} catch (err) {
console.error("Signal error from", fromId, err);
}
};
const getUserMediaSuccess = (stream) => {
try {
window.localStream?.getTracks().forEach((track) => track.stop());
} catch {}
window.localStream = stream;
if (localVideoRef.current) localVideoRef.current.srcObject = stream;
Object.entries(connections).forEach(([id, conn]) => {
if (id !== socketIdRef.current) {
// conn.getSenders().forEach((sender) => sender.replaceTrack(null));
// stream.getTracks().forEach((track) => conn.addTrack(track, stream));
const senders = conn.getSenders();
stream.getTracks().forEach((track) => {
const sender = senders.find((s) => s.track?.kind === track.kind);
if (sender) sender.replaceTrack(track);
else conn.addTrack(track, stream);
});
}
});
};
const getUserMedia = () => {
if ((video && videoAvailable) || (audio && audioAvailable)) {
navigator.mediaDevices
.getUserMedia({ video, audio })
.then(getUserMediaSuccess)
.catch((e) => console.log("getUserMedia error", e));
} else {
try {
// if (
// localVideoRef.current &&
// localVideoRef.current.srcObject &&
// typeof localVideoRef.current.srcObject.getTracks === 'function'
// ) {
// localVideoRef.current.srcObject.getTracks().forEach((track) => track.stop());
// } we can write this as
localVideoRef.current?.srcObject
?.getTracks()
.forEach((track) => track.stop());
} catch {}
}
};
return (
<>
{askForUsername ? (
<div>
<h2>Enter into lobby</h2>
<TextField
className="inputsRequired"
label="Username"
value={username}
variant="outlined"
onChange={(e) => setUsername(e.target.value)}
/>
<Button variant="contained" onClick={connect}>
Connect
</Button>
<div>
<video ref={localVideoRef} autoPlay muted></video>
</div>
</div>
) : (
<div>
<video ref={localVideoRef} autoPlay muted></video>
{videos.map((video) => (
<div key={video.socketId}>
<h2>{video.socketId}</h2>
<video
data-socket={video.socketId}
ref={(ref) => {
if (ref && video.stream) {
ref.srcObject = video.stream;
}
}}
autoPlay
playsInline
/>
</div>
))}
</div>
)}
</>
);
}
Comments
Post a Comment