react-video-call
Version:
ReactVideoCall is a simple yet powerful WebRTC-based video call component for React. It uses Firebase for signaling and supports both desktop and mobile views with minimal setup.
290 lines (242 loc) • 11.1 kB
text/typescript
import { RefObject } from "react";
import { FirebaseWrapper } from "./firebaseLib";
import { DocumentData, Unsubscribe } from "firebase/firestore";
import { FirebaseOptions } from "firebase/app";
type OnUpdateStateCalBack = (state: RTCPeerConnectionState, event: Event) => void;
export interface Idevice {
value: string;
label: string;
data: MediaDeviceInfo;
}
export class WebRTCManager {
ref: string = new Date().toISOString();
fireBaseDb: FirebaseWrapper;
peerConnection: RTCPeerConnection;
localStream?: MediaStream;
remoteStream?: MediaStream;
onStateChange?: OnUpdateStateCalBack;
localVidEle: RefObject<HTMLVideoElement>;
remortVidEle: RefObject<HTMLVideoElement>;
roomUnSub?: Unsubscribe;
roomRole?: "owner" | "joiner";
constructor(
firebaseConfig: FirebaseOptions,
localVidEle: RefObject<HTMLVideoElement>,
remortVidEle: RefObject<HTMLVideoElement>,
RTCConfiguration: RTCConfiguration
) {
this.fireBaseDb = new FirebaseWrapper(firebaseConfig);
this.localVidEle = localVidEle;
this.remortVidEle = remortVidEle;
this.peerConnection = new RTCPeerConnection(RTCConfiguration);
this.peerConnection.ontrack = (event) => {
this.onRemortTrack(event);
}
this.peerConnection.onconnectionstatechange = (event) => {
console.debug("connectionState", this.peerConnection.connectionState);
if (this.onStateChange) {
this.onStateChange(this.peerConnection.connectionState, event)
}
}
const videoElement = localVidEle.current;
let isDragging = false;
let offsetX: number, offsetY: number;
// Handle start of dragging (both mouse and touch)
function startDrag(e: TouchEvent | MouseEvent) {
isDragging = true;
let clientX: number;
let clientY: number;
if (e instanceof TouchEvent) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
offsetX = clientX - videoElement.offsetLeft;
offsetY = clientY - videoElement.offsetTop;
videoElement.style.cursor = 'grabbing';
}
// Handle dragging (both mouse and touch)
function drag(e: TouchEvent | MouseEvent) {
if (!isDragging) return;
let clientX: number;
let clientY: number;
if (e instanceof TouchEvent) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
// Calculate the new position
let newX = clientX - offsetX;
let newY = clientY - offsetY;
// Keep the video within the container boundaries
if (!videoElement.parentElement) return;
const rect = videoElement.parentElement.getBoundingClientRect();
const videoRect = videoElement.getBoundingClientRect();
if (newX < 0) newX = 0;
if (newY < 0) newY = 0;
if (newX + videoRect.width > rect.width) newX = rect.width - videoRect.width;
if (newY + videoRect.height > rect.height) newY = rect.height - videoRect.height;
// Update the position
videoElement.style.left = `${newX}px`;
videoElement.style.top = `${newY}px`;
}
// Handle end of dragging (both mouse and touch)
function endDrag() {
isDragging = false;
videoElement.style.cursor = 'grab';
}
// Mouse events
videoElement.addEventListener('mousedown', startDrag);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', endDrag);
// Touch events
videoElement.addEventListener('touchstart', startDrag);
document.addEventListener('touchmove', drag);
document.addEventListener('touchend', endDrag);
}
async joinOrStartRoom(name: string) {
this.peerConnection.onicecandidate = async (event) => {
//Event that fires off when a new answer ICE candidate is created
if (event.candidate) {
await this.fireBaseDb.updateRoom(this.roomRole==="owner"?"offer": "answer", JSON.stringify(this.peerConnection.localDescription));
}
};
this.roomUnSub = await this.fireBaseDb.joinRoom(name, async (data: DocumentData | undefined) => {
if (!this.roomRole && (!data || (!data.create && !data.offer))) {
console.debug("As Owner creating offer.")
this.roomRole = "owner";
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
await this.fireBaseDb.createRoom(name,this.ref);
} else if (
(this.roomRole === "owner" && data && data?.create && data?.create?.ref !==this.ref) ||
(!this.roomRole && (data && data.offer && !data.completed2))
) {
if(this.roomRole === "owner")
console.debug("Got racecondition and moving to joiner")
console.debug("Joiner creating answer")
this.roomRole = "joiner";
await this.peerConnection.setRemoteDescription(JSON.parse(data.offer));
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);
await this.fireBaseDb.updateRoom("completed2", true);
} else if (this.roomRole ==="owner" && data && data.answer && !data.completed1) {
console.debug("OWNER GOT Answer",this.ref)
this.peerConnection.setRemoteDescription(JSON.parse(data.answer));
await this.fireBaseDb.updateRoom("completed1", true)
}
});
}
onRemortTrack(event: RTCTrackEvent) {
if (event.track.kind === "video") {
return;
}
console.log("onRemortTrack");
if (this.remoteStream) {
this.remoteStream.getTracks().forEach(track => {
track.stop();
});
}
const newstrema = new MediaStream();
event.streams[0].getTracks().forEach((track) => {
newstrema.addTrack(track);
});
this.remoteStream = newstrema;
this.remortVidEle.current.srcObject = this.remoteStream;
// this.remortVidEle.current.volume = 0;
this.remortVidEle.current.play();
}
async getDevices(): Promise<{ audioDevices: Idevice[]; videoDevices: Idevice[]; }> {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
console.debug("Media devices not supported in this browser.");
return { audioDevices: [], videoDevices: [] };
}
await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const deviceInfos: MediaDeviceInfo[] = await navigator.mediaDevices.enumerateDevices();
const audioDevices = [], videoDevices = [];
for (const deviceInfo of deviceInfos) {
if (deviceInfo.kind === "audioinput") {
audioDevices.push({
value: deviceInfo.deviceId,
label: deviceInfo.label,
data: deviceInfo
})
} else if (deviceInfo.kind === "videoinput") {
videoDevices.push({
value: deviceInfo.deviceId,
label: deviceInfo.label,
data: deviceInfo
})
}
}
return { audioDevices, videoDevices }
}
async selectDevice(camera: string | undefined, mic: string | undefined) {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
console.debug("Media devices not supported in this browser.");
return;
}
const constraints = {
audio: mic ? { deviceId: mic ? { exact: mic } : undefined } : true,
video: camera ? { deviceId: camera ? { exact: camera } : undefined, facingMode: "user" } : true
};
navigator.mediaDevices.getUserMedia(constraints).
then((stream) => {
console.debug("Setting local Devices.")
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
track.stop();
});
}
this.localStream = stream;
if (this.localStream) {
this.localStream.getTracks().forEach((track) => {
if (this.localStream) {
console.debug("added local track")
this.peerConnection.addTrack(track, this.localStream);
}
});
this.localVidEle.current.srcObject = this.localStream;
this.localVidEle.current.volume = 0;
this.localVidEle.current.play();
this.localVidEle.current.style.transform = "scaleX(" + "-1" + ")";
}
}).catch((err) => {
console.debug("Could not load stream", err)
});
}
disconnectAll() {
if (this.roomUnSub) {
this.roomUnSub();
}
this.peerConnection.close();
if (this.localStream) {
// Stop all media tracks
this.localStream.getTracks().forEach((track) => track.stop());
this.localStream = undefined;
}
if (this.remoteStream) {
// Stop all media tracks
this.remoteStream.getTracks().forEach((track) => track.stop());
this.remoteStream = undefined;
}
if (this.peerConnection) {
this.peerConnection.onicecandidate = null;
this.peerConnection.ontrack = null;
this.peerConnection.oniceconnectionstatechange = null;
this.peerConnection.onnegotiationneeded = null;
this.peerConnection.onsignalingstatechange = null;
this.peerConnection.onconnectionstatechange = null;
}
this.fireBaseDb.leaveRoom();
console.debug('Connection Closed...');
}
muteMic(isMiute: boolean) {
this.localStream?.getAudioTracks().forEach(track => track.enabled = !isMiute);
console.debug("Microphone is muted");
}
}