UNPKG

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
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"); } }