Im making a mutli user video chatting app using webrtc but the remote streams that are recieved with the peer connections dont play .
this is my peerManager class :
import { v4 as uuidv4 } from "uuid";
export class PeerService {
private ws: WebSocket;
private peerList: Map<string, RTCPeerConnection>;
private localStream: MediaStream | null = null;
public remoteStreams: Map<string, MediaStream> = new Map();
constructor(soc: WebSocket, localStream?: MediaStream) {
this.peerList = new Map<string, RTCPeerConnection>();
this.ws = soc;
this.localStream = localStream || null;
}
// Add local stream to be shared with peers
async addLocalStream(stream: MediaStream) {
console.log("local stream");
this.localStream = stream;
// Add tracks to all existing peer connections
this.peerList.forEach((pc) => {
stream.getTracks().forEach((track) => {
pc.addTrack(track, stream);
});
});
}
// Remove local stream
removeLocalStream() {
if (this.localStream) {
this.peerList.forEach((pc) => {
const senders = pc.getSenders();
senders.forEach((sender) => {
if (
sender.track &&
this.localStream?.getTracks().includes(sender.track)
) {
pc.removeTrack(sender);
}
});
});
this.localStream = null;
}
}
// Get remote stream for a specific peer
getRemoteStream(peerID: string): MediaStream | null {
return this.remoteStreams.get(peerID) || null;
}
// Get all remote streams
getAllRemoteStreams(): Map<string, MediaStream> {
return this.remoteStreams;
}
private setupTrackHandlers(pc: RTCPeerConnection, peerID: string) {
// Handle incoming remote tracks
pc.ontrack = (event: RTCTrackEvent) => {
console.log("tracks adde");
const remoteStream = event.streams[0];
console.log(event.streams);
if (remoteStream) {
this.remoteStreams.set(peerID, remoteStream);
// console.log(this.remoteStreams)
window.dispatchEvent(
new CustomEvent("remoteStreamAdded", {
detail: { peerID, stream: remoteStream },
})
);
}
};
if (this.localStream) {
console.log("localllllllllll");
this.localStream.getTracks().forEach((track) => {
pc.addTrack(track, this.localStream!);
});
}
}
async addPeer() {
console.log(
"adddd poeererererewrawgdsfg hdsfg jsfoghsdfogndsofngdsangakjb"
);
const peerID = uuidv4();
const pc = new RTCPeerConnection();
console.log("add peer");
this.setupTrackHandlers(pc, peerID);
pc.onicecandidate = (event: any) => {
// console.log("ws in PEER MANAGER", this.ws);
if (event.candidate && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(
JSON.stringify({
type: "ice-candidate",
payload: {
candidate: event.candidate,
peerID,
},
})
);
}
};
let offer = await pc.createOffer();
await pc.setLocalDescription(new RTCSessionDescription(offer));
console.log(pc.signalingState);
// console.log(`during offer peer :${peerID}`,pc)
if (this.ws) {
this.ws.send(
JSON.stringify({
type: "offer",
payload: {
peerID,
sdp: offer,
},
})
);
}
this.peerList.set(peerID, pc);
}
async handleSignal(message: any) {
console.log(message);
console.log(this.peerList);
let message_type = message.type;
/* let pc;
if (message.type !== "offer"){
console.log(message)
pc = this.peerList.get(message.payload.peerID);} */
//console.log(pc );
//console.log(message)
/* if (!pc) {
console.log("peer connection not found ");
return;
} */
console.log(message_type);
switch (message_type) {
case "offer": {
const peerID = message.payload.peerID;
const peerConnection = new RTCPeerConnection();
// Optional: Monitor ICE state
peerConnection.oniceconnectionstatechange = () => {
console.log(
"ICE state for",
peerID,
"→",
peerConnection.iceConnectionState
);
};
// ✅ Add local tracks BEFORE setting remote description
this.setupTrackHandlers(peerConnection, peerID); // This must add tracks if localStream exists
// ✅ Set remote description
await peerConnection.setRemoteDescription(
new RTCSessionDescription(message.payload.sdp)
);
// ✅ Store the peer connection immediately under the correct key
this.peerList.set(peerID, peerConnection);
// ✅ Create and send answer
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(
JSON.stringify({
type: "answer",
payload: {
peerID,
sdp: answer,
},
})
);
}
break;
}
case "answer":
console.log("answer");
const pc = this.peerList.get(message.payload.peerID);
console.log(pc);
if (!pc) return;
// if(pc.connectionState === )
// console.log(pc.signalingState);
// console.log(`during asnwer peer :${message.payload.peerID}`,pc)
await pc?.setRemoteDescription(
new RTCSessionDescription(message.payload.sdp)
);
break;
case "ice-candidate": {
const pc = this.peerList.get(message.payload.peerID);
if (!pc) return;
await pc?.addIceCandidate(
new RTCIceCandidate(message.payload.candidate)
);
break;
}
}
}
closePeer(peerID: string) {
const pc = this.peerList.get(peerID);
pc?.close();
this.peerList.delete(peerID);
this.remoteStreams.delete(peerID);
window.dispatchEvent(
new CustomEvent("remoteStreamRemoved", {
detail: { peerID },
})
);
}
closeAll() {
this.peerList.forEach((pc, peerId) => {
pc.close();
});
this.peerList.clear();
this.remoteStreams.clear();
this.removeLocalStream();
}
}
export default PeerService;
VidConference.tsx
import React, { useState, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Mic,
MicOff,
Video,
VideoOff,
PhoneOff,
Users,
MessageSquare,
Share,
Settings,
} from "lucide-react";
import Button from "../ui/Button";
import { useMeetings } from "../../contexts/MeetingsContext";
import { useAuth } from "../../contexts/AuthContext";
import ParticipantGrid from "./ParticipantGrid";
import { Participant } from "../../types";
import { useSoc } from "../../hooks/usesoc";
import { PeerService } from "../../utils/peer";
const VideoConference: React.FC = () => {
const { getCurrentMeeting, leaveMeeting } = useMeetings();
const { user } = useAuth();
const meeting = getCurrentMeeting();
const [isMuted, setIsMuted] = useState(false);
const [isVideoOn, setIsVideoOn] = useState(true);
const [showParticipants, setShowParticipants] = useState(false);
const [showChat, setShowChat] = useState(false);
const [participants, setParticipants] = useState<Participant[]>([]);
const localVid = useRef<HTMLVideoElement | null>(null);
const [stat, setStat] = useState<boolean>(false);
const remoteVid = useRef<HTMLVideoElement | null>(null);
const { roomid } = useParams();
const peerManager = useRef<PeerService | null>(null);
useEffect(() => {
const mockParticipants: Participant[] = [
{
id: user?.id || "1",
name: user?.name || "You",
email: user?.email || "",
avatar: user?.avatar,
isMuted: true,
isVideoOn: true,
isHost: true,
mediaStream: localVid,
},
];
setParticipants(mockParticipants);
}, [user]);
const soc = useSoc();
async function playVideoFromCamera() {
try {
const constraints: MediaStreamConstraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 },
facingMode: "user",
},
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100,
},
};
console.log("Requesting user media with constraints:", constraints);
const stream = await navigator.mediaDevices.getUserMedia(constraints);
console.log("Media stream obtained:", stream);
console.log("Video tracks:", stream.getVideoTracks());
console.log("Audio tracks:", stream.getAudioTracks());
return stream;
} catch (error) {
console.error("Error opening video camera.", error);
return null;
}
}
function changeParticipants(s: Participant) {
setParticipants((prev: any) => {
console.log("remote stream", s.mediaStream);
return [...prev, s];
});
}
function populateRemoteStreams() {
peerManager.current?.remoteStreams.forEach((stream, peerId) => {
const alreadyExists = participants.some((p) => p.id === peerId);
if (alreadyExists) return;
console.log("pouplate");
const newRef = React.createRef<HTMLVideoElement>();
changeParticipants({
id: peerId,
name: "yo",
avatar: "isudgfius",
isMuted: true,
isVideoOn: true,
isHost: false,
mediaStream: newRef,
stream: stream,
});
});
}
// Set up video independently of WebSocket
useEffect(() => {
if (peerManager.current == null) {
playVideoFromCamera().then(async (stream) => {
if (stream && localVid.current !== null) {
localVid.current.srcObject = stream;
// Force the video to play
localVid.current
.play()
.catch((e) => console.error("Error playing video:", e));
}
if (stream && stat) {
peerManager.current = new PeerService(
soc.current as WebSocket,
stream
);
peerManager.current.addPeer().then(async () => {
// await peerManager.current?.addLocalStream(stream as MediaStream);
});
// const stream = await playVideoFromCamera()
}
});
}
}, [stat]);
useEffect(() => {
let check = peerManager.current?.remoteStreams.size;
function checkRemoteStreams() {
if (peerManager.current) {
// console.log("Remote streams:", peerManager.current.remoteStreams);
if (check != peerManager.current?.remoteStreams.size) {
populateRemoteStreams();
check = peerManager.current.remoteStreams.size;
}
}
}
const intervalId = setInterval(checkRemoteStreams, 1000);
return () => clearInterval(intervalId);
}, []);
useEffect(() => {
if (soc.current)
soc.current.onopen = () => {
setStat(true);
};
if (!soc.current || soc.current.readyState !== WebSocket.OPEN) {
return;
}
if (soc.current)
soc.current.send(
JSON.stringify({
type: "create-room",
room_id: roomid,
})
);
}, [roomid, stat]);
// WebSocket message handling
useEffect(() => {
if (!soc.current) return;
const socket = soc.current;
socket.onmessage = (m) => {
if (peerManager.current)
peerManager.current.handleSignal(JSON.parse(m.data));
};
}, [soc.current?.readyState, stat]);
// Update local participant when video stream is available
useEffect(() => {
if (localVid.current && localVid.current.srcObject) {
setParticipants((prev) =>
prev.map((p) =>
p.id === user?.id
? { ...p, isVideoOn: true, mediaStream: localVid }
: p
)
);
}
}, [localVid.current?.srcObject, user?.id]);
const handleLeaveMeeting = () => {
leaveMeeting();
// navigate("/dashboard");
};
const toggleMute = () => {
setIsMuted(!isMuted);
// Update participant state
setParticipants((prev) =>
prev.map((p) => (p.id === user?.id ? { ...p, isMuted: !isMuted } : p))
);
};
const toggleVideo = () => {
setIsVideoOn(!isVideoOn);
// Update participant state
setParticipants((prev) =>
prev.map((p) => (p.id === user?.id ? { ...p, isVideoOn: !isVideoOn } : p))
);
};
const handleShare = () => {
// Copy meeting link to clipboard
const meetingLink = `${window.location.origin}/video-conference/${roomid}`;
navigator.clipboard.writeText(meetingLink).then(() => {
// Could add a toast notification here
console.log("Meeting link copied to clipboard");
});
};
const handleSettings = () => {
// Open settings modal or panel
console.log("Settings clicked");
};
return (
<div className="h-screen bg-gray-900 flex flex-col">
{/* Header */}
<header className="bg-gray-800/50 backdrop-blur-sm border-b border-gray-700 px-6 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-lg font-semibold text-white">
{meeting?.title || `Room: ${roomid}`}
</h1>
<div className="flex items-center space-x-4 text-sm text-gray-400">
<span>Meeting ID: {meeting?.meetingCode || roomid}</span>
<span>•</span>
<span>{participants.length} participants</span>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setShowParticipants(!showParticipants)}
leftIcon={<Users className="w-4 h-4" />}
>
<span className="hidden sm:inline">Participants</span>
<span className="sm:hidden">{participants.length}</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowChat(!showChat)}
leftIcon={<MessageSquare className="w-4 h-4" />}
>
<span className="hidden sm:inline">Chat</span>
</Button>
</div>
</div>
</header>
{/* Main Content */}
<div className="flex-1 flex">
{/* Video Grid */}
<div className="flex-1 p-4">
<ParticipantGrid participants={participants} />
</div>
{/* Sidebar */}
{(showParticipants || showChat) && (
<div className="w-80 bg-gray-800/50 backdrop-blur-sm border-l border-gray-700 p-4">
{showParticipants && (
<div className="mb-6">
<h3 className="text-lg font-semibold text-white mb-4">
Participants ({participants.length})
</h3>
<div className="space-y-2">
{participants.map((participant) => (
<div
key={participant.id}
className="flex items-center space-x-3 p-2 rounded-lg hover:bg-gray-700/50"
>
{!participant.isVideoOn ? (
<img
src={
participant.avatar ||
`https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=50&h=50&dpr=2`
}
alt={participant.name}
className="w-8 h-8 rounded-full object-cover"
/>
) : (
<></>
)}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-white truncate">
{participant.name}
{participant.isHost && (
<span className="ml-2 text-xs text-blue-400">
(Host)
</span>
)}
</p>
</div>
<div className="flex items-center space-x-1">
{participant.isMuted ? (
<MicOff className="w-4 h-4 text-red-400" />
) : (
<Mic className="w-4 h-4 text-green-400" />
)}
{participant.isVideoOn ? (
<Video className="w-4 h-4 text-green-400" />
) : (
<VideoOff className="w-4 h-4 text-red-400" />
)}
</div>
</div>
))}
</div>
</div>
)}
{showChat && (
<div>
<h3 className="text-lg font-semibold text-white mb-4">Chat</h3>
<div className="bg-gray-700/50 rounded-lg p-4 text-gray-400 text-sm text-center">
Chat feature coming soon...
</div>
</div>
)}
</div>
)}
</div>
{/* Controls */}
<div className="bg-gray-800/50 backdrop-blur-sm border-t border-gray-700 px-6 py-4">
<div className="flex items-center justify-center space-x-4">
<Button
variant={isMuted ? "danger" : "secondary"}
size="lg"
onClick={toggleMute}
className="w-12 h-12 rounded-full p-0 flex items-center justify-center"
title={isMuted ? "Unmute" : "Mute"}
>
{isMuted ? (
<MicOff className="w-5 h-5" />
) : (
<Mic className="w-5 h-5" />
)}
</Button>
<Button
variant={!isVideoOn ? "danger" : "secondary"}
size="lg"
onClick={toggleVideo}
className="w-12 h-12 rounded-full p-0 flex items-center justify-center"
title={isVideoOn ? "Turn off camera" : "Turn on camera"}
>
{isVideoOn ? (
<Video className="w-5 h-5" />
) : (
<VideoOff className="w-5 h-5" />
)}
</Button>
<Button
variant="secondary"
size="lg"
onClick={handleShare}
className="w-12 h-12 rounded-full p-0 flex items-center justify-center"
title="Share meeting"
>
<Share className="w-5 h-5" />
</Button>
<Button
variant="secondary"
size="lg"
onClick={handleSettings}
className="w-12 h-12 rounded-full p-0 flex items-center justify-center"
title="Settings"
>
<Settings className="w-5 h-5" />
</Button>
<Button
variant="danger"
size="lg"
onClick={handleLeaveMeeting}
className="w-12 h-12 rounded-full p-0 flex items-center justify-center"
title="Leave meeting"
>
<PhoneOff className="w-5 h-5" />
</Button>
</div>
</div>
</div>
);
};
export default VideoConference;
Participantgrid.tsx
import React, { useEffect, useRef } from "react";
import { Mic, MicOff, Video, VideoOff, Crown } from "lucide-react";
import { Participant } from "../../types";
interface ParticipantGridProps {
participants: Participant[];
}
const ParticipantGrid: React.FC<ParticipantGridProps> = ({ participants }) => {
const videoRefs = useRef<Record<string, HTMLVideoElement | null>>({});
useEffect(() => {
participants.forEach((participant) => {
const videoEl = videoRefs.current[participant.id];
if (
participant.isVideoOn &&
videoEl &&
participant.stream &&
participant.stream.getTracks().length > 0 &&
videoEl.srcObject !== participant.stream
) {
videoEl.srcObject = null;
videoEl.srcObject = participant.stream;
videoEl
.play()
.then(() => console.log("Playing:", participant.id))
.catch((error) => {
console.error("Error playing video for", participant.id, error);
});
videoEl.onloadedmetadata = () => {
videoEl.play().catch((error) => {
console.error("Error playing video for", participant.id, error);
});
};
}
});
}, [participants]);
const getGridCols = () => {
const count = participants.length;
if (count === 1) return "grid-cols-1";
if (count === 2) return "grid-cols-2";
if (count <= 4) return "grid-cols-2";
if (count <= 6) return "grid-cols-3";
return "grid-cols-4";
};
return (
<div className={`grid ${getGridCols()} gap-4 h-full`}>
{participants.map((participant) => (
<div
key={participant.id}
className="relative bg-gray-800 rounded-lg overflow-hidden group"
>
{/* Video/Avatar */}
<div className="w-full h-full flex items-center justify-center vid_area">
{participant.isVideoOn && !participant.isHost ? (
participant.mediaStream ? (
<>
<video
key={participant.id}
ref={(el) => {
videoRefs.current[participant.id] = el;
}}
autoPlay
muted={participant.isMuted}
playsInline
className="w-full h-full object-cover transform scale-x-[-1]"
onLoadedMetadata={(e) => {
console.log("Loaded metadata for", participant.id);
}}
onError={(e) =>
console.error("Video error for", participant.id, e)
}
/>
{/* <canvas ref={canvasRef} className="..." /> */}
</>
) : (
<div className="w-full h-full bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center">
<div className="text-6xl font-bold text-white/20">
{participant.name?.charAt(0).toUpperCase()}
</div>
</div>
)
) : (
<div className="flex flex-col items-center justify-center space-y-4">
<img
src={
participant.avatar ||
`https://images.pexels.com/photos/220453/pexels-photo-220453.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=2`
}
alt={participant.name}
className="w-20 h-20 rounded-full object-cover border-4 border-gray-600"
/>
<div className="flex items-center space-x-2 px-3 py-1 bg-gray-700/80 rounded-full">
<VideoOff className="w-4 h-4 text-red-400" />
<span className="text-sm text-gray-300">Camera off</span>
</div>
</div>
)}
</div>
{/* Participant Info */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className="text-white font-medium text-sm">
{participant.name}
</span>
{participant.isHost && (
<Crown className="w-4 h-4 text-yellow-400" />
)}
</div>
<div className="flex items-center space-x-1">
<div
className={`p-1.5 rounded-full ${
participant.isMuted ? "bg-red-500" : "bg-green-500"
}`}
>
{participant.isMuted ? (
<MicOff className="w-3 h-3 text-white" />
) : (
<Mic className="w-3 h-3 text-white" />
)}
</div>
</div>
</div>
</div>
{/* Speaking Indicator */}
{!participant.isMuted && (
<div className="absolute top-4 left-4 w-3 h-3 bg-green-400 rounded-full animate-pulse" />
)}
</div>
))}
</div>
);
};
export default ParticipantGrid;
when i try to tlog the remote streams they get logged but cant be played
when i checked the readystate of the stream was 0 and the network state was 2