Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
398 changes: 396 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@
},
"dependencies": {
"@radix-ui/react-slot": "^1.1.0",
"@types/socket.io-client": "^1.4.36",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"mediasoup-client": "^3.7.18",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.2",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7"
},
Expand Down
274 changes: 274 additions & 0 deletions src/components/VideoCall/VideoCall.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useEffect, useRef } from "react";
import { types as mediasoupTypes, Device } from "mediasoup-client";
import { setupVideoSocket } from "@/config/videoSocket";
import { RtpCapabilities, TransportParams } from "@/types/video";

const VideoCall: React.FC = () => {
const [localStream, setLocalStream] = useState<MediaStream | null>(null);
const [isConnected, setIsConnected] = useState(false);
const localVideoRef = useRef<HTMLVideoElement>(null);
const remoteVideoRef = useRef<HTMLVideoElement>(null);

const videoSocket = setupVideoSocket();
const deviceRef = useRef<Device | null>(null);
const producerTransportRef = useRef<mediasoupTypes.Transport | null>(null);
const consumerTransportRef = useRef<mediasoupTypes.Transport | null>(null);
const producerRef = useRef<mediasoupTypes.Producer | null>(null);
const consumerRef = useRef<mediasoupTypes.Consumer | null>(null);

useEffect(() => {
setupConnection();

// Listen for new producers
videoSocket.socket.on("newProducer", () => {
console.log("New producer available");
consumeMedia();
});

return () => {
// Cleanup
localStream?.getTracks().forEach((track) => track.stop());
videoSocket.socket.off("newProducer");

// Close transports and producers/consumers
producerTransportRef.current?.close();
consumerTransportRef.current?.close();
producerRef.current?.close();
consumerRef.current?.close();
};
}, []);

const setupConnection = async () => {
try {
await getLocalStream();
await setupMediasoupDevice();
await createProducerTransport();
await produceMedia();
await createConsumerTransport();
setIsConnected(true);
} catch (error) {
console.error("Connection setup error:", error);
setIsConnected(false);
}
};

const getLocalStream = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
setLocalStream(stream);
if (localVideoRef.current) {
localVideoRef.current.srcObject = stream;
}
} catch (error) {
console.error("Error getting local stream:", error);
}
};

const setupMediasoupDevice = async () => {
deviceRef.current = new Device();
return new Promise<void>((resolve, reject) => {
videoSocket.getRouterCapabilities(
(routerRtpCapabilities: RtpCapabilities) => {
try {
if (deviceRef.current) {
deviceRef.current.load({ routerRtpCapabilities });
resolve();
} else {
reject(new Error("Device not initialized"));
}
} catch (error) {
console.error("Error loading device:", error);
reject(error);
}
}
);
});
};

const createProducerTransport = async () => {
return new Promise<void>((resolve, reject) => {
videoSocket.createProducerTransport((params: TransportParams) => {
try {
if (!deviceRef.current) throw new Error("Device not initialized");

producerTransportRef.current =
deviceRef.current.createSendTransport(params);

producerTransportRef.current.on(
"connect",
async ({ dtlsParameters }, callback) => {
try {
await videoSocket.connectProducerTransport(
dtlsParameters,
callback
);
} catch (error) {
console.error("Producer transport connect error:", error);
}
}
);

producerTransportRef.current.on(
"produce",
async ({ kind, rtpParameters }, callback) => {
try {
const result = await videoSocket.produce(
kind,
rtpParameters,
callback
);
// Notify other clients about new producer
videoSocket.socket.emit("newProducer");
return result;
} catch (error) {
console.error("Produce error:", error);
}
}
);

resolve();
} catch (error) {
console.error("Create producer transport error:", error);
reject(error);
}
});
});
};

const produceMedia = async () => {
try {
if (!producerTransportRef.current || !localStream) return;

const audioTrack = localStream.getAudioTracks()[0];
const videoTrack = localStream.getVideoTracks()[0];

if (audioTrack) {
producerRef.current = await producerTransportRef.current.produce({
track: audioTrack,
// Removed audioGain as it's not a valid option
});
}
if (videoTrack) {
producerRef.current = await producerTransportRef.current.produce({
track: videoTrack,
encodings: [
{ maxBitrate: 1000000 },
{ maxBitrate: 500000 },
{ maxBitrate: 100000 },
],
});
}
} catch (error) {
console.error("Produce media error:", error);
}
};

const createConsumerTransport = async () => {
return new Promise<void>((resolve, reject) => {
videoSocket.createConsumerTransport((params: TransportParams) => {
try {
if (!deviceRef.current) throw new Error("Device not initialized");

consumerTransportRef.current =
deviceRef.current.createRecvTransport(params);

consumerTransportRef.current.on(
"connect",
async ({ dtlsParameters }, callback) => {
try {
await videoSocket.connectConsumerTransport(
dtlsParameters,
callback
);
} catch (error) {
console.error("Consumer transport connect error:", error);
}
}
);

resolve();
} catch (error) {
console.error("Create consumer transport error:", error);
reject(error);
}
});
});
};

const consumeMedia = async () => {
try {
if (!deviceRef.current || !consumerTransportRef.current) {
throw new Error("Transport or device not initialized");
}

const consumerParams = await new Promise<any>((resolve, reject) => {
if (!deviceRef.current) {
throw new Error("Device not initialized");
}
videoSocket.consume(deviceRef.current.rtpCapabilities, (params) => {
if (params.error) {
reject(new Error(params.error));
} else {
resolve(params);
}
});
});

// Consume the media track
consumerRef.current = await consumerTransportRef.current.consume({
id: consumerParams.id,
producerId: consumerParams.producerId,
kind: consumerParams.kind,
rtpParameters: consumerParams.rtpParameters,
appData: consumerParams.appData,
});

// Update remote video source
if (remoteVideoRef.current) {
const newRemoteStream = new MediaStream([consumerRef.current.track]);
remoteVideoRef.current.srcObject = newRemoteStream;
}

// Resume consumer if paused
if (consumerRef.current.paused) {
await consumerRef.current.resume();
}
} catch (error) {
console.error("Consume media error:", error);
}
};

return (
<div className="video-container flex space-x-4 p-4">
<div className="local-video-container">
<h2 className="text-lg font-semibold mb-2">Local Video</h2>
<video
ref={localVideoRef}
autoPlay
muted
className="w-96 border border-gray-300 rounded-lg"
/>
</div>
<div className="remote-video-container">
<h2 className="text-lg font-semibold mb-2">Remote Video</h2>
{isConnected ? (
<video
ref={remoteVideoRef}
autoPlay
className="w-96 border border-gray-300 rounded-lg"
/>
) : (
<div className="w-96 h-64 border border-gray-300 rounded-lg flex items-center justify-center">
<p className="text-gray-500">Connecting...</p>
</div>
)}
</div>
</div>
);
};

export default VideoCall;
70 changes: 70 additions & 0 deletions src/config/videoSocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// src/config/videoSocket.ts
import io, {Socket } from 'socket.io-client';
import { types as mediasoupTypes, } from 'mediasoup-client';
import { RtpCapabilities, TransportParams } from '@/types/video';

const SOCKET_URL = 'http://localhost:8080';

export const setupVideoSocket = () => {
const socket: typeof Socket = io(SOCKET_URL, { path: '/video' });

return {
socket,
getRouterCapabilities: (callback: (caps: RtpCapabilities) => void) => {
socket.emit('getRouterRtpCapabilities', null, callback);
},
createProducerTransport: (callback: (params: TransportParams) => void) => {
socket.emit('createProducerTransport', null, callback);
},
createConsumerTransport: (callback: (params: TransportParams) => void) => {
socket.emit('createConsumerTransport', null, callback);
},
connectProducerTransport: (dtlsParameters: mediasoupTypes.DtlsParameters, callback: () => void) => {
socket.emit('connectProducerTransport', { dtlsParameters }, callback);
},
connectConsumerTransport: (dtlsParameters: mediasoupTypes.DtlsParameters, callback: () => void) => {
socket.emit('connectConsumerTransport', { dtlsParameters }, callback);
},
// produce: (kind: mediasoupTypes.MediaKind, rtpParameters: mediasoupTypes.RtpParameters, callback: (id: { id: string }) => void) => {
// socket.emit('produce', { kind, rtpParameters }, callback);
// },
// consume: (rtpCapabilities: mediasoupTypes.RtpCapabilities, callback: (consumerParams: any) => void) => {
// socket.emit('consume', { rtpCapabilities }, callback);
// },
resume: (callback: () => void) => {
socket.emit('resume', null, callback);
},
produce: (
kind: mediasoupTypes.MediaKind,
rtpParameters: mediasoupTypes.RtpParameters,
callback: (id: { id: string }) => void
) => {
return new Promise((resolve, reject) => {
socket.emit('produce', { kind, rtpParameters }, (response:any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
callback(response);
}
});
});
},
consume: (
rtpCapabilities: mediasoupTypes.RtpCapabilities,
callback: (consumerParams: any) => void
) => {
return new Promise((resolve, reject) => {
socket.emit('consume', { rtpCapabilities }, (response:any) => {
if (response.error) {
reject(new Error(response.error));
} else {
resolve(response);
callback(response);
}
});
});
},
};
};
22 changes: 13 additions & 9 deletions src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import VideoCallPage from "./pages/VideoCallPage.tsx";

createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/videocall" element={<VideoCallPage />} />
</Routes>
</BrowserRouter>
);
Loading