diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 51d2eef1..fb122dd4 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -16,11 +16,13 @@ "@radix-ui/react-slot": "^1.0.2", "@repo/store": "*", "@repo/ui": "*", + "@repo/common" : "*", "chess.js": "^1.0.0-beta.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "history": "^5.3.0", "lucide-react": "^0.372.0", + "mediasoup-client": "^3.7.8", "react": "^18.2.0", "react-confetti": "^6.1.0", "react-dom": "^18.2.0", diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx index e0a1abfa..b28e2658 100644 --- a/apps/frontend/src/App.tsx +++ b/apps/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { RecoilRoot } from 'recoil'; import { useUser } from '@repo/store/useUser'; import { Loader } from './components/Loader'; import { Layout } from './layout'; +import Meet from './screens/Meet'; function App() { return ( @@ -35,6 +36,9 @@ function AuthApp() { path="/game/:gameId" element={} />} /> + }>} + /> ); diff --git a/apps/frontend/src/hooks/useSfu.ts b/apps/frontend/src/hooks/useSfu.ts new file mode 100644 index 00000000..0bee5ce8 --- /dev/null +++ b/apps/frontend/src/hooks/useSfu.ts @@ -0,0 +1,93 @@ +import { useEffect, useRef, useState } from 'react'; +import { SfuService, UserConsumer } from '../utils/SfuService'; +import { Producer } from 'mediasoup-client/lib/types'; +import { User } from '@repo/store/user'; +import { CustomAppData } from '@repo/common/sfu'; + +export const useSfu = ( + roomId: string, + user: User | null, +) => { + + const sfuService = useRef(null); + const [videoProducer, setVideoProducer] = useState>(); + const [audioProducer, setAudioProducer] = useState>(); + const [consumers, setConsumers] = useState([]); + + const handleNewConsumer = (consumer: UserConsumer) => { + setConsumers((prevConsumers) => [ + ...prevConsumers.filter((c) => c.id !== consumer.id), + consumer, + ]); + }; + + const handleConsumerClosed = (userId: string) => { + setConsumers((prevConsumers) => { + const new_consumers = prevConsumers.filter((consumer) => consumer.id !== userId); + return new_consumers; + }); + }; + + const handleTransportConnected = () => { + startConsuming(); + startProducing(); + } + + useEffect(() => { + + if(!roomId || !user) { + console.error('RoomId or User not provided'); + return; + } + + if (!sfuService.current) { + sfuService.current = new SfuService(roomId, user); + } + + sfuService.current.on(SfuService.NEW_CONSUMER_EVENT, handleNewConsumer); + sfuService.current.on(SfuService.CONSUMER_CLOSED_EVENT, handleConsumerClosed); + sfuService.current.on(SfuService.TRANSPORT_CONNECTED_EVENT, handleTransportConnected); + + return () => { + if (sfuService.current) { + sfuService.current.cleanUp(); + sfuService.current.off(SfuService.NEW_CONSUMER_EVENT, handleNewConsumer); + sfuService.current.off(SfuService.CONSUMER_CLOSED_EVENT, handleConsumerClosed); + sfuService.current.off(SfuService.TRANSPORT_CONNECTED_EVENT,handleTransportConnected); + sfuService.current = null; + } + }; + }, [roomId, user ? user.token : undefined]); + + + const startProducing = async () => { + if (!sfuService.current) { + console.error('Sfu Service not initialized'); + return; + } + try { + const { audioProducer, videoProducer } = + await sfuService.current.createProducer(); + if (audioProducer) { + setAudioProducer(audioProducer); + } + if (videoProducer) { + setVideoProducer(videoProducer); + } + } catch (error) { + console.error('Error creating producer', error); + } + }; + + const startConsuming = async () => { + if (!sfuService.current) return; + const consumer = await sfuService.current.createConsumer(); + setConsumers([...consumer]); + }; + + return { + videoProducer, + audioProducer, + consumers, + }; +}; diff --git a/apps/frontend/src/screens/Meet.tsx b/apps/frontend/src/screens/Meet.tsx new file mode 100644 index 00000000..4ae8b519 --- /dev/null +++ b/apps/frontend/src/screens/Meet.tsx @@ -0,0 +1,115 @@ +import { useUser } from '@repo/store/useUser'; +import { useSfu } from '../hooks/useSfu'; +import { useEffect, useRef } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; + +const Meet = () => { + const user = useUser(); + + const videoRef = useRef(null); + const navigate = useNavigate(); + // consumers videos ref + + const { roomId } = useParams(); + + useEffect(() => { + if (!user) { + navigate('/login'); + } + }, [user]); + + const { videoProducer, audioProducer, consumers } = useSfu(roomId!, user); + + useEffect(() => { + if (!videoRef.current) return; + const stream = new MediaStream(); + if (videoProducer && videoProducer.track) { + stream.addTrack(videoProducer.track); + } + if (audioProducer && audioProducer.track) { + stream.addTrack(audioProducer.track); + } + videoRef.current.srcObject = stream; + }, [videoProducer, audioProducer]); + + if (!user) { + return
Redirecting to Login ...
; + } + + return ( +
+
+

Meeting

+
+
+ {[ + { type: 'Producer', track: videoProducer, audio: audioProducer }, + ...consumers.map((consumer, index) => ({ + type: `Consumer ${index + 1}`, + track: consumer.videoConsumer, + audio: consumer.audioConsumer, + })), + ].map((user, index) => ( +
+ { + user.audio?.pause(); + user.track?.pause(); + }} + onPlay={() => { + user.track?.resume(); + user.audio?.resume(); + }} + /> +

{user.type}

+
+ ))} +
+
+ ); +}; + +const VideoPlayer: React.FC<{ + videoTrack: MediaStreamTrack | null; + audioTrack: MediaStreamTrack | null; + onPlay: () => void; + onPause: () => void; +}> = (props) => { + const videoRef = useRef(null); + + useEffect(() => { + if (videoRef.current) { + const stream = new MediaStream(); + if (props.audioTrack) { + stream.addTrack(props.audioTrack); + } + if (props.videoTrack) { + stream.addTrack(props.videoTrack); + } + videoRef.current.srcObject = stream; + videoRef.current.onplay = props.onPlay; + videoRef.current.onpause = props.onPause; + } + }, [props.audioTrack, props.videoTrack, props.onPlay, props.onPause]); + + return ( +
+ +
+ ); +}; + + +export default Meet; diff --git a/apps/frontend/src/utils/SfuMessageDispatcher.ts b/apps/frontend/src/utils/SfuMessageDispatcher.ts new file mode 100644 index 00000000..e2978f90 --- /dev/null +++ b/apps/frontend/src/utils/SfuMessageDispatcher.ts @@ -0,0 +1,26 @@ +import {ClientMessage,ClientMessageType} from "@repo/common/sfu"; + +type PayloadType = Extract['payload']; + +class SfuMessageDispatcher { + + private handlers: Map void)[]> = new Map(); + + registerHandler(type: T, handler: (payload: PayloadType) => Promise): () => void { + const existing = this.handlers.get(type) || []; + existing.push(handler); + this.handlers.set(type, existing); + // Return a function to deregister the handler + return () => this.handlers.set(type, existing.filter((h) => h !== handler)); + } + + dispatch(event: MessageEvent) { + const message = JSON.parse(event.data) as ClientMessage; + console.log('Received message:', message); + const handler = this.handlers.get(message.type); + if (handler) { + handler.forEach((h) => h(message.payload)); + } + } + } + export default SfuMessageDispatcher; \ No newline at end of file diff --git a/apps/frontend/src/utils/SfuService.ts b/apps/frontend/src/utils/SfuService.ts new file mode 100644 index 00000000..40c0f806 --- /dev/null +++ b/apps/frontend/src/utils/SfuService.ts @@ -0,0 +1,506 @@ +import { + Consumer, + Device, + Producer, + Transport, +} from 'mediasoup-client/lib/types'; +import SfuMessageDispatcher from './SfuMessageDispatcher'; +import { EventEmitter } from 'events'; +import { User } from "@repo/store/user"; +import { ClientMessageType, CustomAppData, RoomErrorPayload, ServerMessage, ServerMessageType, WebRtcConnectResponsePayload, WebRtcConsumerResponsePayload, WebRtcNewProducerPayload, WebRtcProducerResponsePayload, WebRtcTransportPayload, WebRtcUserDisconnectedPayload } from '@repo/common/sfu'; + +const SFU_URL = import.meta.env.VITE_APP_SFU_URL ?? 'ws://localhost:8081'; + + +export interface UserConsumer { + id: string; + videoConsumer: Consumer | null; + audioConsumer: Consumer | null; +} + +export class SfuService extends EventEmitter { + roomId: string; + webSocket: WebSocket; + device: Device; + senderTransport: Transport | null; + receiverTransport: Transport | null; + videoProducer: Producer | null; + audioProducer: Producer | null; + producersToConsume: {id: string, userId: string}[]; + consumers: Map; + user: { id: string; token: string; name: string }; + sfuMessageDispatcher: SfuMessageDispatcher; + + + constructor( + roomId: string, + user: User, + ) { + super(); + this.roomId = roomId; + this.webSocket = new WebSocket(`${SFU_URL}?token=${user.token}`); + this.device = new Device(); + this.senderTransport = null; + this.receiverTransport = null; + this.videoProducer = null; + this.audioProducer = null; + this.consumers = new Map(); + this.user = user; + this.sfuMessageDispatcher = new SfuMessageDispatcher(); + this.producersToConsume = []; + this.addHandlers(); + } + + static NEW_CONSUMER_EVENT = 'newConsumer'; + static CONSUMER_CLOSED_EVENT = 'consumerClosed'; + static TRANSPORT_CONNECTED_EVENT = 'transportConnected'; + + + addHandlers() { + this.webSocket.onmessage = (event) => { + this.sfuMessageDispatcher.dispatch(event); + }; + this.webSocket.onopen = () => { + console.log('Connected to the server'); + sendMessage(this.webSocket, { + type: ServerMessageType.JOIN_ROOM, + payload: { + roomId: this.roomId, + }, + }); + }; + + this.sfuMessageDispatcher.registerHandler( + ClientMessageType.WEBRTC_TRANSPORT_INITIALIZE, + async (payload: WebRtcTransportPayload) => { + const { id, iceParameters, iceCandidates, dtlsParameters } = + payload.sender; + + // Load this device + await this.device.load({ + routerRtpCapabilities: payload.routerRtpCapabilities, + }); + + const senderTransport = this.device.createSendTransport({ + id, + iceParameters, + iceCandidates, + dtlsParameters, + appData: { + userId: this.user.id, + }, + }); + + const receiverTransPort = this.device.createRecvTransport({ + id: payload.receiver.id, + iceParameters: payload.receiver.iceParameters, + iceCandidates: payload.receiver.iceCandidates, + dtlsParameters: payload.receiver.dtlsParameters, + appData: { + userId: this.user.id, + }, + }); + this.producersToConsume = payload.producers; // producers to server as consumers in client + this.senderTransport = senderTransport; + this.receiverTransport = receiverTransPort; + this.setUpSenderTransport(); + this.setUpReceiverTransport(); + this.emit(SfuService.TRANSPORT_CONNECTED_EVENT); + }, + ); + + this.sfuMessageDispatcher.registerHandler(ClientMessageType.ROOM_ERROR, async (payload: RoomErrorPayload) => { + console.error("Room error", payload.message); + }); + + + + this.sfuMessageDispatcher.registerHandler( + ClientMessageType.WEBRTC_NEW_PRODUCER, + async (payload: WebRtcNewProducerPayload) => { + try { + this.producersToConsume.push({ + id: payload.producerId, + userId: payload.userId, + }); + const consumer = await this.createSingleConsumer({ + producerId: payload.producerId, + userId: payload.userId, + }); + const consumerUserId = consumer.appData.userId as string; + const userConsumer = this.consumers.get(consumerUserId) ?? { + id: consumerUserId, + audioConsumer: null, + videoConsumer: null, + }; + + if (consumer.kind === 'audio') { + userConsumer.audioConsumer = consumer; + } else { + userConsumer.videoConsumer = consumer; + } + + this.consumers.set(consumerUserId, userConsumer); + this.emit(SfuService.NEW_CONSUMER_EVENT, userConsumer); + } catch (error) { + console.error('Failed to create consumer:', error); + } + }, + ); + + this.sfuMessageDispatcher.registerHandler( + ClientMessageType.WEBRTC_USER_DISCONNECTED, + async (payload: WebRtcUserDisconnectedPayload) => { + const userId = payload.userId as string; + const userConsumer = this.consumers.get(userId); + + console.log('User disconnected', userId, userConsumer); + + // Close both consumers + if (userConsumer?.audioConsumer) { + userConsumer.audioConsumer.close(); + } + if (userConsumer?.videoConsumer) { + userConsumer.videoConsumer.close(); + } + this.consumers.delete(userId); + this.emit(SfuService.CONSUMER_CLOSED_EVENT, userId); + }, + ); + + this.webSocket.onclose = () => { + this.cleanUp(); + }; + } + + cleanUp = () => { + // Close WebSocket connection + if (this.webSocket.readyState === WebSocket.OPEN) { + this.webSocket.close(); + } + + // Stop producer if it exists + if (this.videoProducer) { + this.videoProducer.close(); + this.videoProducer = null; + } + + if (this.audioProducer) { + this.audioProducer.close(); + this.audioProducer = null; + } + + // Stop all consumers + this.consumers.forEach((consumer) => { + if (consumer.audioConsumer) { + consumer.audioConsumer.close(); + } + if (consumer.videoConsumer) { + consumer.videoConsumer.close(); + } + }); + this.consumers = new Map(); + + // Close sender transport if it exists + if (this.senderTransport) { + this.senderTransport.close(); + this.senderTransport = null; + } + + // Close receiver transport if it exists + if (this.receiverTransport) { + this.receiverTransport.close(); + this.receiverTransport = null; + } + }; + + setUpSenderTransport = async () => { + if (!this.senderTransport) { + throw new Error('Sender transport is not created'); + } + + this.senderTransport!.on( + 'connect', + async ({ dtlsParameters }, callback, errback) => { + // Here we must communicate our local parameters to our remote transport. + try { + sendMessage(this.webSocket, { + type: ServerMessageType.WEBRTC_CONNECT, + payload: { + transportId: this.senderTransport!.id, + dtlsParameters, + }, + }); + // Done in the server, tell our transport. + const deregisterHandler = this.sfuMessageDispatcher.registerHandler( + ClientMessageType.WEBRTC_TRANSPORT_CONNECT_RESPONSE, + async (payload: WebRtcConnectResponsePayload) => { + if (payload.transportId === this.senderTransport!.id) { + if(payload.success === true) { + callback(); + } else { + errback(new Error('Failed to connect transport')); + } + deregisterHandler(); + } + }, + ); + } catch (error) { + // Something was wrong in server side. + errback(error as Error); + } + }, + ); + + this.senderTransport!.on( + 'produce', + async ({ kind, rtpParameters, appData }, callback, errback) => { + try { + sendMessage(this.webSocket, { + type: ServerMessageType.WEBRTC_PRODUCER, + payload: { + transportId: this.senderTransport!.id, + kind, + rtpParameters, + appData: { + ...appData, + userId: this.user.id, + }, + }, + }); + const deregisterHandler = this.sfuMessageDispatcher.registerHandler( + ClientMessageType.WEBRTC_PRODUCER_RESPONSE, + async (payload: WebRtcProducerResponsePayload) => { + if (payload.transportId === this.senderTransport!.id) { + if(payload.success) { + callback({ id: payload.producerId! }); + } else { + errback(new Error('Failed to create producer')); + } + deregisterHandler(); + } + }, + ); + } catch (error) { + errback(error as Error); + } + }, + ); + }; + + setUpReceiverTransport = async () => { + if (!this.receiverTransport) { + throw new Error('Receiver transport is not created'); + } + + this.receiverTransport!.on( + 'connect', + async ({ dtlsParameters }, callback, errback) => { + // Here we must communicate our local parameters to our remote transport + try { + sendMessage(this.webSocket, { + type: ServerMessageType.WEBRTC_CONNECT, + payload: { + transportId: this.receiverTransport!.id, + dtlsParameters, + }, + }); + // Done in the server, tell our transport. + const deregisterHandler = this.sfuMessageDispatcher.registerHandler( + ClientMessageType.WEBRTC_TRANSPORT_CONNECT_RESPONSE, + async (payload: WebRtcConnectResponsePayload) => { + if (payload.transportId === this.receiverTransport!.id) { + if(payload.success === true) { + callback(); + } else { + errback(new Error('Failed to connect transport')); + } + deregisterHandler(); + } + }, + ); + } catch (error) { + // Something was wrong in server side. + errback(error as Error); + } + }, + ); + }; + + createProducer = async () => { + try { + if (!this.senderTransport) { + throw new Error('Sender transport is not created'); + } + + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true, + }); + + const videoTrack = stream.getVideoTracks()[0]; + const audioTrack = stream.getAudioTracks()[0]; + + const params = { + encodings: [ + { + scalabilityMode: 'L1T3' + } + ], + codecOptions: { + videoGoogleStartBitrate: 1000, + }, + }; + try { + const videoProducer = await this.senderTransport.produce({ + ...params, + track: videoTrack, + appData: { + userId: this.user.id, + }, + }); + this.videoProducer = videoProducer; + } catch(err) { + console.error('Error creating video producer', err); + } + + try { + const audioProducer = await this.senderTransport.produce({ + track: audioTrack, + appData: { + userId: this.user.id, + }, + + }); + this.audioProducer = audioProducer; + } catch (error) { + console.error('Error creating audio producer', error); + } + + return { + videoProducer: this.videoProducer, + audioProducer: this.audioProducer, + }; + } catch (error) { + console.error('Error creating producer', error); + return { + videoProducer: this.videoProducer, + audioProducer: this.audioProducer, + } + } + }; + + createConsumer = async () => { + if (!this.receiverTransport) { + throw new Error('Receiver transport is not created'); + } + const consumersPromises = this.producersToConsume.map( + async (producerInfo) => { + try { + const consumer = await this.createSingleConsumer({ + producerId: producerInfo.id, + userId: producerInfo.userId, + }); + return { + userId: producerInfo.userId, + consumer, + }; + } catch(error) { + console.error(`Error creating consumer for ${producerInfo.userId}`, error); + return { + userId: producerInfo.userId, + consumer: null, + }; + } + }, + ); + + const allConsumers = await Promise.all(consumersPromises); + + allConsumers.forEach((consumerInfo) => { + + const {userId, consumer} = consumerInfo; + + if(!consumer) { + console.error(`Consumer for ${userId} is Not created`); + } + + const consumerUserId = userId; + + const userConsumer = this.consumers.get(consumerUserId) ?? { + id: consumerUserId, + videoConsumer: null, + audioConsumer: null, + }; + + if(consumer) { + if (consumer.kind == 'audio') { + userConsumer.audioConsumer = consumer; + } else { + userConsumer.videoConsumer = consumer; + } + } + + this.consumers.set(consumerUserId, userConsumer); + }); + + return this.consumers.values(); + }; + + createSingleConsumer = async ({producerId, userId} : {producerId: string,userId:string}) : Promise> => { + const consumer = new Promise>((resolve, reject) => { + sendMessage(this.webSocket, { + type: ServerMessageType.WEBRTC_CONSUMER, + payload: { + transportId: this.receiverTransport!.id, + producerId: producerId, + rtpCapabilities: this.device.rtpCapabilities, + }, + }); + + const deregisterHandler = this.sfuMessageDispatcher.registerHandler( + ClientMessageType.WEBRTC_CONSUMER_RESPONSE, + async (payload: WebRtcConsumerResponsePayload) => { + try { + if (payload.transportId === this.receiverTransport!.id && payload.producerId === producerId) { + if(!payload.success || !payload.consumerId || !payload.rtpParameters || !payload.kind) { + throw new Error('Failed to create consumer'); + } + const consumer = await this.receiverTransport!.consume({ + id: payload.consumerId, + producerId: payload.producerId, + rtpParameters: payload.rtpParameters, + kind: payload.kind, + appData: { + userId: userId, + }, + }); + // send ack to sever for resuming consumer on server side + sendMessage(this.webSocket, { + type: ServerMessageType.WEBRTC_RESUME_CONSUMER, + payload: { + consumerId: consumer.id, + }, + }); + deregisterHandler(); + resolve(consumer); + } + } catch (error) { + reject(error); + deregisterHandler(); + } + }, + ); + + setTimeout(() => { + reject(new Error('Consumer creation timeout for ' + producerId + 'for ' + userId)); + deregisterHandler(); + },10000); + }); + return consumer; + }; +} + +function sendMessage(ws: WebSocket, message: ServerMessage) { + ws.send(JSON.stringify(message)); +} \ No newline at end of file diff --git a/apps/sfu/package.json b/apps/sfu/package.json new file mode 100644 index 00000000..c297e543 --- /dev/null +++ b/apps/sfu/package.json @@ -0,0 +1,23 @@ +{ + "name": "sfu", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "npx esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js", + "start": "MEDIASOUP_WORKER_BIN=\"$(node -p 'path.resolve(__dirname, \"../../node_modules/mediasoup/worker/out/Release/mediasoup-worker\")')\" node dist/index.js", + "dev": "npm run build && npm run start" + }, + "author": "", + "license": "ISC", + "dependencies": { + "crypto": "^1.0.1", + "jsonwebtoken": "^9.0.2", + "mediasoup": "^3.14.6", + "ws": "^8.17.0", + "@repo/common" : "*" + }, + "devDependencies": { + "cross-env": "^7.0.3" + } +} diff --git a/apps/sfu/src/Room.ts b/apps/sfu/src/Room.ts new file mode 100644 index 00000000..ea446517 --- /dev/null +++ b/apps/sfu/src/Room.ts @@ -0,0 +1,386 @@ +import { + AppData, + Consumer, + Producer, + Router, + WebRtcTransport, + MediaKind +} from 'mediasoup/node/lib/types'; +import User from './User'; +import RoomManager from './RoomManager'; +import { ClientMessageType, ServerMessageType, ClientMessage, ServerMessage, WebRtcJoinRoom, WebRtcJoinRoomPayload, WebRtcConnectPayload, WebRtcProducerPayload, CustomAppData, WebRtcConsumerPayload, WebRtcResumeConsumerPayload} from '@repo/common/sfu'; +import WebSocket from 'ws'; + +class Room { + + router: Router; + + id: string; + + users: User[]; + + transports: WebRtcTransport[]; + producers: Producer[]; + consumers: Consumer[]; + + constructor(router: Router, id: string) { + this.router = router; + this.users = []; + this.transports = []; + this.producers = []; + this.consumers = []; + this.id = id; + } + + async addPeer(user: User) { + if(this.users.find(u => u.id === user.id)) { + console.log('User already exists in the room'); + sendMessage(user.ws,{ + type: ClientMessageType.ROOM_ERROR, + payload: { + message: 'User already exists in the room', + } + }); + return; + } + this.addRoomEventHandlers(user); + // create a transport for each user + let senderTransport = await this.createWebRtcTransport(user.id); + let receiverTransport = await this.createWebRtcTransport(user.id); + const { + id: senderId, + iceParameters: senderIceParameters, + iceCandidates: senderIceCandidates, + dtlsParameters: senderDtlsParameters, + } = senderTransport; + const { + id: receiverId, + iceParameters: receiverIceParameters, + iceCandidates: receiverIceCandidates, + dtlsParameters: receiverDtlsParameters, + } = receiverTransport; + // Send the transport information to the client + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_TRANSPORT_INITIALIZE, + payload: { + sender: { + id: senderId, + iceParameters: senderIceParameters, + iceCandidates: senderIceCandidates, + dtlsParameters: senderDtlsParameters, + }, + receiver: { + id: receiverId, + iceParameters: receiverIceParameters, + iceCandidates: receiverIceCandidates, + dtlsParameters: receiverDtlsParameters, + }, + routerRtpCapabilities: this.router.rtpCapabilities, + producers: this.producers.map((producer) => { + return { + id: producer.id, + userId: producer.appData.userId, + }; + }), + }, + }) + this.transports.push(senderTransport); + this.transports.push(receiverTransport); + this.users.push(user); + } + + addRoomEventHandlers(user: User) { + console.log('Adding event handlers for user', user.id); + user.ws.on('message', (data) => { + const message = JSON.parse(data.toString()) as ServerMessage; + switch (message.type) { + case ServerMessageType.WEBRTC_CONNECT: + this.connectPeer(user, message.payload); + break; + case ServerMessageType.WEBRTC_PRODUCER: + this.initiliseProducer(user, message.payload); + break; + case ServerMessageType.WEBRTC_CONSUMER: + this.initiliseConsumer(user, message.payload); + break; + case ServerMessageType.WEBRTC_RESUME_CONSUMER: + this.resumeConsumer(user, message.payload); + break; + } + }); + + user.ws.on('close', () => { + this.removeUser(user); + }); + } + resumeConsumer(user: User, payload: WebRtcResumeConsumerPayload) { + const consumer = this.consumers.find((c) => c.id === payload.consumerId); + if (!consumer) { + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_RESUME_CONSUMER_RESPONSE, + payload: { + message: 'Consumer not found', + success: false, + }, + }); + return; + } + consumer.resume(); + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_RESUME_CONSUMER_RESPONSE, + payload: { + message: 'Consumer resumed', + success: true, + }, + }); + } + + + async initiliseConsumer(user: User, payload: WebRtcConsumerPayload) { + try { + const { transportId, producerId, rtpCapabilities } = payload; + const consumerTransport = this.transports.find(transport => transport.id === transportId); + if (!consumerTransport) { + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_CONSUMER_RESPONSE, + payload: { + message: 'Transport not found', + success: false + }, + }); + return; + } + + const producer = this.producers.find((p) => p.id === producerId); + + if (!producer) { + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_CONSUMER_RESPONSE, + payload: { + message: 'Producer not found', + success: false + }, + }); + return; + } + + const consumer = await consumerTransport.consume({ + producerId, + rtpCapabilities, + paused: true, + appData: { + userId: producer.appData.userId, + }, + }); + + this.consumers.push(consumer); + + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_CONSUMER_RESPONSE, + payload: { + consumerId: consumer.id, + producerId, + kind: consumer.kind, + rtpParameters: consumer.rtpParameters, + transportId: consumerTransport.id, + appData: consumer.appData, + success: true, + }, + }); + } catch (error) { + console.error(error); + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_CONSUMER_RESPONSE, + payload: { + message: 'Failed to create consumer', + success: false + }, + }); + } + } + + async initiliseProducer(user: User, payload: WebRtcProducerPayload) { + try { + const { transportId, kind, rtpParameters, appData } = payload; + const transport = this.transports.find(transport => transport.id === transportId); + if (!transport) { + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_PRODUCER_RESPONSE, + payload: { + message: 'Transport not found', + success: false, + transportId, + }, + }); + return; + } + + if (appData && appData.userId !== user.id) { + console.log('Invalid user id', appData.userId, user.id); + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_PRODUCER_RESPONSE, + payload: { + message: 'Invalid user id', + success: false, + transportId, + }, + }); + return; + } + + // create producer + const producer = await transport.produce({ + kind: kind as MediaKind, + rtpParameters, + appData, + }); + // send producer id to the client + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_PRODUCER_RESPONSE, + payload: { + transportId, + producerId: producer.id, + success: true, + }, + }); + this.producers.push(producer); + + // notify all the clients about the new producer + this.users.forEach((u) => { + if (u.id !== user.id) { + sendMessage(u.ws,{ + type: ClientMessageType.WEBRTC_NEW_PRODUCER, + payload: { + userId: user.id, + producerId: producer.id, + }, + }); + } + }); + } catch (error) { + console.error(error); + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_PRODUCER_RESPONSE, + payload: { + message: 'Failed to create producer', + success: false, + transportId: payload.transportId, + }, + }); + } + } + + connectPeer(user: User, payload: WebRtcConnectPayload) { + const { transportId, dtlsParameters } = payload; + const transport = this.transports.find(transport => transport.id === transportId); + // establish connection between the client and the server + try { + if (!transport) { + throw new Error('Transport not found'); + } + transport.connect({ dtlsParameters }).then(() => { + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_TRANSPORT_CONNECT_RESPONSE, + payload: { + transportId: transport.id, + success: true, + }, + }); + }); + } catch (error) { + console.error(error); + sendMessage(user.ws,{ + type: ClientMessageType.WEBRTC_TRANSPORT_CONNECT_RESPONSE, + payload: { + transportId: transportId, + success: false, + }, + }); + } + } + + async createWebRtcTransport(userId: string) { + // Server will establish peer to peer connection with each client + // and hence we need to create a transport for each client + const transport = await this.router.createWebRtcTransport({ + preferUdp: true, + listenIps: [ + { + ip: '127.0.0.1', + announcedIp: undefined, + }, + ], + appData: { + userId: userId + } + }); + return transport; + } + + removeUser(user: User) { + this.users = this.users.filter((u) => u.id !== user.id); + + const transportsToRemove = Array.from(this.transports.values()).filter((transport) => { + return transport.appData.userId === user.id; + }); + + const producersToRemove = this.producers.filter( + (p) => p.appData.userId === user.id, + ); + + const consumersToRemove = this.consumers.filter( + (c) => c.appData.userId === user.id, + ); + + // close the producers + producersToRemove.forEach((prd) => { + prd.close(); + }); + + // close the consumers + consumersToRemove.forEach((c) => { + c.close(); + }); + + // close the transports + transportsToRemove.forEach((t) => { + // Close the transport and remove it from the map + t.close(); + }); + + // remove the producer from the list + this.producers = this.producers.filter((p) => p.appData.userId !== user.id); + + // remove the consumers for the user + this.consumers = this.consumers.filter((c) => c.appData.userId !== user.id); + + // remove the transports for the user + this.transports = this.transports.filter((t) => t.appData.userId !== user.id); + + + // notify all the clients about the removed producer + this.users.forEach((u) => { + if (u.id !== user.id) { + sendMessage(u.ws,{ + type: ClientMessageType.WEBRTC_USER_DISCONNECTED, + payload: { + userId: user.id, + }, + }); + } + }); + + if(this.users.length === 0) { + console.log('No users left in the room. Closing the room'); + this.router.close(); + RoomManager.getInstance().deleteRoom(this.id); + } + + } +} + +function sendMessage(ws: WebSocket, message: ClientMessage) { + ws.send(JSON.stringify(message)); +} + +export default Room; diff --git a/apps/sfu/src/RoomManager.ts b/apps/sfu/src/RoomManager.ts new file mode 100644 index 00000000..aa95dc03 --- /dev/null +++ b/apps/sfu/src/RoomManager.ts @@ -0,0 +1,48 @@ +import { createRouter } from "."; +import Room from "./Room"; + +class RoomManager { + + static instance: RoomManager; + + static getInstance() { + if (!RoomManager.instance) { + console.log('Creating new RoomManager instance'); + RoomManager.instance = new RoomManager(); + } + return RoomManager.instance; + } + + rooms: Map; + + constructor() { + this.rooms = new Map(); + } + + async createRoom(roomId: string) { + const router = await createRouter(); + const room = new Room(router, roomId); + this.rooms.set(roomId, room); + return room; + } + + async getOrCreateRoom(roomId: string) { + let room = this.rooms.get(roomId); + if (!room) { + console.log(`Creating new room ${roomId}`); + room = await this.createRoom(roomId); + } + return room; + } + + getRoom(roomId: string) { + return this.rooms.get(roomId); + } + + deleteRoom(roomId: string) { + console.log(`Deleting room ${roomId}`); + this.rooms.delete(roomId); + } +} + +export default RoomManager; \ No newline at end of file diff --git a/apps/sfu/src/User.ts b/apps/sfu/src/User.ts new file mode 100644 index 00000000..e3a44fee --- /dev/null +++ b/apps/sfu/src/User.ts @@ -0,0 +1,40 @@ +import { createRouter } from "./index"; +import Room from "./Room"; +import { WebSocket } from "ws"; +import { randomUUID } from 'crypto'; +import RoomManager from "./RoomManager"; +import { ServerMessageType } from "@repo/common/sfu"; + + +class User { + id: string; + ws: WebSocket; + sid: string; + constructor(id: string, ws: WebSocket) { + this.id = id; + this.ws = ws; + this.sid = randomUUID(); + this.handler(); + } + + + + handler() { + this.ws.on('message', async (data) => { + const message = JSON.parse(data.toString()); + switch (message.type) { + case ServerMessageType.JOIN_ROOM: + const roomId = message.payload.roomId; + const roomManager = RoomManager.getInstance(); + const room = await roomManager.getOrCreateRoom(roomId); + room.addPeer(this); + break; + } + }); + + } + + } + + export default User; + diff --git a/apps/sfu/src/index.ts b/apps/sfu/src/index.ts new file mode 100644 index 00000000..d29ce6bb --- /dev/null +++ b/apps/sfu/src/index.ts @@ -0,0 +1,112 @@ +import { WebSocketServer, WebSocket} from 'ws'; +import url from 'url'; +import jwt from 'jsonwebtoken'; +import {createWorker} from 'mediasoup'; +import { MediaKind } from 'mediasoup/node/lib/RtpParameters'; +import User from './User'; +import { cpus } from 'os'; +import { Worker } from 'mediasoup/node/lib/Worker'; + + +const wss = new WebSocketServer({ port: 8081 }); + +const JWT_SECRET = process.env.JWT_SECRET || 'your_secret_key'; + +export const extractUserId = (token: string) => { + const decoded = jwt.verify(token, JWT_SECRET) as { userId: string }; + return decoded.userId; +}; + +const mediaCodecs = [ + { + kind: 'audio' as MediaKind, + mimeType: 'audio/opus', + clockRate: 48000, + channels: 2, + parameters : { + 'usedtx': 1, // Use discontinuous transmission + } + }, + { + kind: 'video' as MediaKind, + mimeType: 'video/H264', + clockRate: 90000, + parameters: { + 'packetization-mode': 1, + 'profile-level-id': '42e01f', + 'level-asymmetry-allowed': 1, + }, + } +]; + +const numCPUs = cpus().length; +const workers: Worker[] = []; + + + +async function createWorkers() { + // Each worker runs as a separate process on a single cpu + let minPort = 40000; + let maxPort = 49999; + let portsPerWorker = Math.floor((maxPort - minPort) / numCPUs); + for (let i = 0; i < numCPUs; i++) { + let rtcMinPort = minPort + i * portsPerWorker; + let rtcMaxPort = rtcMinPort + portsPerWorker - 1; + try { + let worker = await createWorker({ + rtcMinPort: rtcMinPort, + rtcMaxPort: rtcMaxPort, + logLevel: 'warn', + }); + workers.push(worker); + } catch (err) { + console.error('Error creating worker', err); + } + } +} + +createWorkers().then(() => { + if(workers.length === 0) { + console.error('No workers created'); + process.exit(1); + } + console.log(`${workers.length} Workers created`); +}).catch((err) => { + console.error('Error creating workers', err); + process.exit(1); +});; + +let workerIndex = 0; + +export async function createRouter() { + const worker = workers[workerIndex]; + if(!worker) { + throw new Error('No workers available'); + } + const router = await worker.createRouter({ mediaCodecs }); + workerIndex = (workerIndex + 1) % workers.length; + return router; +} + +const users: { [key: string]: User } = {}; + +wss.on('connection', async function connection(ws, req) { + //@ts-ignore + const token: string = url.parse(req.url, true).query.token; + + console.log(token); + + const userId = extractUserId(token); + + let user = new User(userId, ws); + + users[user.sid] = user; + + user.ws.on('close', () => { + delete users[user.sid]; + console.log(`User ${userId} for ${user.sid} disconnected`); + }); +}); + +console.log('done'); + diff --git a/apps/sfu/tsconfig.json b/apps/sfu/tsconfig.json new file mode 100644 index 00000000..925b3500 --- /dev/null +++ b/apps/sfu/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends":"@repo/typescript-config/base.json", +} diff --git a/packages/common/package.json b/packages/common/package.json new file mode 100644 index 00000000..34722708 --- /dev/null +++ b/packages/common/package.json @@ -0,0 +1,15 @@ +{ + "name": "@repo/common", + "version": "1.0.0", + "description": "", + "main": "index.js", + "exports": { + "./sfu" : "./sfu/index.ts" + }, + "author": "", + "license": "ISC", + "dependencies": { + "typescript": "^5.2.2", + "mediasoup": "^3.9.0" + } +} diff --git a/packages/common/sfu/ClientMessageTypes.ts b/packages/common/sfu/ClientMessageTypes.ts new file mode 100644 index 00000000..9e4ee8e4 --- /dev/null +++ b/packages/common/sfu/ClientMessageTypes.ts @@ -0,0 +1,136 @@ +import {MediaKind, RtpParameters, AppData, IceParameters, DtlsParameters, IceCandidate, RtpCapabilities} from "mediasoup/node/lib/types"; +import { CustomAppData } from "./ServerMessageTypes"; + +export enum ClientMessageType { + WEBRTC_RESUME_CONSUMER_RESPONSE = 'WEBRTC_RESUME_CONSUMER_RESPONSE', + WEBRTC_PRODUCERS = 'WEBRTC_PRODUCERS', + WEBRTC_CONSUMER_RESPONSE = 'WEBRTC_CONSUMER_RESPONSE', + WEBRTC_PRODUCER_RESPONSE = 'WEBRTC_PRODUCER_RESPONSE', + WEBRTC_NEW_PRODUCER = 'WEBRTC_NEW_PRODUCER', + WEBRTC_USER_DISCONNECTED = 'WEBRTC_USER_DISCONNECTED', + WEBRTC_TRANSPORT_CONNECT_RESPONSE = 'WEBRTC_TRANSPORT_CONNECT_RESPONSE', + WEBRTC_TRANSPORT_INITIALIZE = 'WEBRTC_TRANSPORT_INITIALIZE', + ROOM_ERROR = 'ROOM_ERROR', +} + + +export interface WebRtcResumeConsumerResponsePayload { + message: string; + success: boolean; +} + +export interface WebRtcResumeConsumerResponse { + type: ClientMessageType.WEBRTC_RESUME_CONSUMER_RESPONSE; + payload: WebRtcResumeConsumerResponsePayload; +} + +export interface WebRtcProducersPayload { + producers: { + id: string; + userId: string; + }[]; +} + +export interface WebRtcProducers { + type: ClientMessageType.WEBRTC_PRODUCERS; + payload: WebRtcProducersPayload; +} + +export interface WebRtcConsumerResponsePayload { + message?: string; + success: boolean; + consumerId?: string; + producerId?: string; + kind?: MediaKind; + rtpParameters?: RtpParameters; + transportId?: string; + appData?: CustomAppData; + +} + +export interface WebRtcConsumerResponse { + type: ClientMessageType.WEBRTC_CONSUMER_RESPONSE; + payload: WebRtcConsumerResponsePayload +} + +export interface WebRtcProducerResponsePayload { + message?: string; + success: boolean; + transportId: string; + producerId?: string; + +} + +export interface WebRtcProducerResponse { + type: ClientMessageType.WEBRTC_PRODUCER_RESPONSE; + payload: WebRtcProducerResponsePayload; +} + +export interface WebRtcNewProducerPayload { + userId: string; + producerId: string; +} + +export interface WebRtcNewProducer { + type: ClientMessageType.WEBRTC_NEW_PRODUCER; + payload: WebRtcNewProducerPayload; +} + +export interface WebRtcUserDisconnectedPayload { + userId: string; + +} + +export interface WebRtcUserDisconnected { + type: ClientMessageType.WEBRTC_USER_DISCONNECTED; + payload: WebRtcUserDisconnectedPayload; +} + +export interface WebRtcConnectResponsePayload { + transportId: string; + success: boolean; +} + +export interface WebRtcConnectResponse { + type: ClientMessageType.WEBRTC_TRANSPORT_CONNECT_RESPONSE; + payload: WebRtcConnectResponsePayload; +} + +export interface WebRtcTransportPayload { + sender: { + id: string; + iceParameters: IceParameters, + iceCandidates: IceCandidate[], + dtlsParameters: DtlsParameters, + }, + receiver: { + id: string; + iceParameters: IceParameters, + iceCandidates: IceCandidate[], + dtlsParameters: DtlsParameters, + }, + routerRtpCapabilities: RtpCapabilities, + producers: { + id: string; + userId: string; + }[], +} + +export interface WebRtcTransport { + type: ClientMessageType.WEBRTC_TRANSPORT_INITIALIZE; + payload: WebRtcTransportPayload; +} + +export interface RoomErrorPayload { + message: string; + +} + +export interface RoomError { + type: ClientMessageType.ROOM_ERROR; + payload: RoomErrorPayload +} + +export type ClientMessagePayLoad = WebRtcResumeConsumerResponsePayload | WebRtcProducersPayload | WebRtcConsumerResponsePayload | WebRtcProducerResponsePayload | WebRtcNewProducerPayload | WebRtcUserDisconnectedPayload | WebRtcConnectResponsePayload | WebRtcTransportPayload | RoomErrorPayload; + +export type ClientMessage = WebRtcResumeConsumerResponse | WebRtcProducers | WebRtcConsumerResponse | WebRtcProducerResponse | WebRtcNewProducer | WebRtcUserDisconnected | WebRtcConnectResponse | WebRtcTransport | RoomError; \ No newline at end of file diff --git a/packages/common/sfu/ServerMessageTypes.ts b/packages/common/sfu/ServerMessageTypes.ts new file mode 100644 index 00000000..c87c6464 --- /dev/null +++ b/packages/common/sfu/ServerMessageTypes.ts @@ -0,0 +1,69 @@ +import {DtlsParameters,RtpParameters,AppData,RtpCapabilities} from "mediasoup/node/lib/types"; + +export interface CustomAppData extends AppData { + userId: string; +} + +export enum ServerMessageType { + WEBRTC_CONNECT = 'WEBRTC_CONNECT', + WEBRTC_PRODUCER = 'WEBRTC_PRODUCER', + WEBRTC_CONSUMER = 'WEBRTC_CONSUMER', + WEBRTC_RESUME_CONSUMER = 'WEBRTC_RESUME_CONSUMER', + JOIN_ROOM = 'JOIN_ROOM', +} + +export interface WebRtcJoinRoomPayload { + roomId: string; +} + +export interface WebRtcJoinRoom { + type: ServerMessageType.JOIN_ROOM; + payload: WebRtcJoinRoomPayload +} + +export interface WebRtcConnectPayload { + transportId: string; + dtlsParameters: DtlsParameters; +} + +export interface WebRtcConnect { + type: ServerMessageType.WEBRTC_CONNECT; + payload: WebRtcConnectPayload; + +} + +export interface WebRtcProducerPayload { + transportId: string; + kind: string; + rtpParameters: RtpParameters; + appData: CustomAppData; + +} + +export interface WebRtcProducer { + type: ServerMessageType.WEBRTC_PRODUCER; + payload: WebRtcProducerPayload +} + +export interface WebRtcConsumerPayload { + transportId: string; + producerId: string; + rtpCapabilities: RtpCapabilities; + +} + +export interface WebRtcConsumer { + type: ServerMessageType.WEBRTC_CONSUMER; + payload: WebRtcConsumerPayload +} + +export interface WebRtcResumeConsumerPayload { + consumerId: string; +} + +export interface WebRtcResumeConsumer { + type: ServerMessageType.WEBRTC_RESUME_CONSUMER; + payload: WebRtcResumeConsumerPayload +} + +export type ServerMessage = WebRtcJoinRoom | WebRtcConnect | WebRtcProducer | WebRtcConsumer | WebRtcResumeConsumer; \ No newline at end of file diff --git a/packages/common/sfu/index.ts b/packages/common/sfu/index.ts new file mode 100644 index 00000000..4184adf7 --- /dev/null +++ b/packages/common/sfu/index.ts @@ -0,0 +1,2 @@ +export * from './ClientMessageTypes' +export * from './ServerMessageTypes' \ No newline at end of file diff --git a/packages/common/tsconfig.json b/packages/common/tsconfig.json new file mode 100644 index 00000000..925b3500 --- /dev/null +++ b/packages/common/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends":"@repo/typescript-config/base.json", +} diff --git a/packages/db/package.json b/packages/db/package.json index 61dbe026..2d35dd94 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -17,7 +17,7 @@ "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "5.12.1", + "@prisma/client": "^5.12.1", "prisma": "^5.12.0" }, "exports": { diff --git a/packages/store/package.json b/packages/store/package.json index 0e75441a..2cdd8a7a 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -5,7 +5,8 @@ "main": "index.js", "exports": { "./useUser": "./src/hooks/useUser.ts", - "./chessBoard":"./src/atoms/chessBoard.ts" + "./chessBoard":"./src/atoms/chessBoard.ts", + "./user": "./src/atoms/user.ts" }, "keywords": [], "author": "", diff --git a/yarn.lock b/yarn.lock index f8d2b8f3..6a5f576b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1843,6 +1843,13 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + "@isaacs/ttlcache@^1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" @@ -2024,7 +2031,7 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== -"@prisma/client@5.12.1", "@prisma/client@^5.12.1": +"@prisma/client@^5.12.1": version "5.12.1" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.12.1.tgz#c26a674fea76754b3a9e8b90a11e617f90212f76" integrity sha512-6/JnizEdlSBxDIdiLbrBdMW5NqDxOmhXAJaNXiPpgzAPr/nLZResT6MMpbOHLo5yAbQ1Vv5UU8PTPRzb0WIxdA== @@ -2970,6 +2977,13 @@ dependencies: "@types/node" "*" +"@types/debug@^4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/eslint@^8.56.5": version "8.56.10" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" @@ -3028,6 +3042,11 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== +"@types/ini@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@types/ini/-/ini-4.1.0.tgz#20e7327b3133627f84304210670d6406cceaba25" + integrity sha512-mTehMtc+xtnWBBvqizcqYCktKDBH2WChvx1GU3Sfe4PysFDXiNe+1YwtpVX1MDtCa4NQrSPw2+3HmvXHY3gt1w== + "@types/inquirer@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be" @@ -3087,6 +3106,11 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + "@types/node@*", "@types/node@^20.11.24", "@types/node@^20.12.7": version "20.12.7" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.7.tgz#04080362fa3dd6c5822061aa3124f5c152cff384" @@ -3888,6 +3912,13 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" +awaitqueue@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/awaitqueue/-/awaitqueue-3.0.2.tgz#a37a212b137b784dc6bd701d1ecfa4a07ec89625" + integrity sha512-AVAtRwmf0DNSesMdyanFKKejTrOnjdKtz5LIDQFu2OTUgXvB/CRTYMrkPAF/2GCF9XBtYVxSwxDORlD41S+RyQ== + dependencies: + debug "^4.3.4" + axe-core@=4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" @@ -4362,6 +4393,11 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + chrome-launcher@^0.15.2: version "0.15.2" resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" @@ -4719,6 +4755,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + cross-fetch@^3.1.5: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" @@ -4737,7 +4780,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -4761,6 +4804,11 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +crypto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037" + integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -4786,6 +4834,11 @@ data-uri-to-buffer@^3.0.1: resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + data-uri-to-buffer@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz#8a58bb67384b261a38ef18bea1810cb01badd28b" @@ -5644,6 +5697,16 @@ event-target-shim@^5.0.0, event-target-shim@^5.0.1: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +event-target-shim@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-6.0.2.tgz#ea5348c3618ee8b62ff1d344f01908ee2b8a2b71" + integrity sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + exec-async@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/exec-async/-/exec-async-2.2.0.tgz#c7c5ad2eef3478d38390c6dd3acfe8af0efc8301" @@ -5848,6 +5911,14 @@ external-editor@^3.0.3: iconv-lite "^0.4.24" tmp "^0.0.33" +fake-mediastreamtrack@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fake-mediastreamtrack/-/fake-mediastreamtrack-1.2.0.tgz#11e6e0c50d36d3bc988461c034beb81debee548b" + integrity sha512-AxHtlEmka1sqNoe3Ej1H1hJc9gjjO/6vCbCPm4D4QeEXvzhjYumA+iZ7wOi2WrmkAhGElHhBgWoNgJhFccectA== + dependencies: + event-target-shim "^6.0.2" + uuid "^9.0.0" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -5920,6 +5991,14 @@ fbjs@^3.0.0: setimmediate "^1.0.5" ua-parser-js "^1.0.35" +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + fetch-retry@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/fetch-retry/-/fetch-retry-4.1.1.tgz#fafe0bb22b54f4d0a9c788dff6dd7f8673ca63f3" @@ -6025,6 +6104,11 @@ flat-cache@^3.0.4: keyv "^4.5.3" rimraf "^3.0.2" +flatbuffers@^24.3.25: + version "24.3.25" + resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-24.3.25.tgz#e2f92259ba8aa53acd0af7844afb7c7eb95e7089" + integrity sha512-3HDgPbgiwWMI9zVB7VYBHaMrbOO7Gm0v+yD2FV/sCKj+9NDeVL7BOBYUuhWAQGKWOzBo8S9WdMvV0eixO233XQ== + flatted@^3.2.9: version "3.3.1" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" @@ -6074,6 +6158,13 @@ form-data@^3.0.1: combined-stream "^1.0.8" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -6299,6 +6390,17 @@ glob@^10.3.10: minipass "^7.0.4" path-scurry "^1.10.2" +glob@^10.3.7: + version "10.4.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" + integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + path-scurry "^1.11.1" + glob@^6.0.1: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" @@ -6415,6 +6517,14 @@ graphql@15.8.0: resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== +h264-profile-level-id@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/h264-profile-level-id/-/h264-profile-level-id-2.0.0.tgz#b7ea45badbac8f5dbb9583d34b06db09764f2535" + integrity sha512-X4CLryVbVA0CtjTExS4G5U1gb2Z4wa32AF8ukVmFuLdw2JRq2aHisor7SY5SYTUUrUSqq0KdPIO18sql6IWIQw== + dependencies: + "@types/debug" "^4.1.12" + debug "^4.3.4" + hamt_plus@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601" @@ -6656,6 +6766,11 @@ inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ini/-/ini-4.1.3.tgz#4c359675a6071a46985eb39b14e4a2c0ec98a795" + integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg== + ini@~1.3.0: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -7104,6 +7219,15 @@ jackspeak@^2.3.6: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" +jackspeak@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.1.2.tgz#eada67ea949c6b71de50f1b09c92a961897b90ab" + integrity sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jest-environment-node@^29.6.3: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" @@ -7749,6 +7873,36 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== +mediasoup-client@^3.7.8: + version "3.7.8" + resolved "https://registry.yarnpkg.com/mediasoup-client/-/mediasoup-client-3.7.8.tgz#fec9fe9f9248220949fd558d0f5d4dcd4d8f65e3" + integrity sha512-an/2vQfMpHNtCMkrujJ65KsnvGebJO8QNpUOK+y7rvkd1fkRRAq9zN3DenDd7zXMSev2eD1AkFIYX/susrLNSg== + dependencies: + "@types/debug" "^4.1.12" + awaitqueue "^3.0.2" + debug "^4.3.4" + events "^3.3.0" + fake-mediastreamtrack "^1.2.0" + h264-profile-level-id "^2.0.0" + queue-microtask "^1.2.3" + sdp-transform "^2.14.2" + supports-color "^9.4.0" + ua-parser-js "^1.0.37" + +mediasoup@^3.14.6, mediasoup@^3.9.0: + version "3.14.7" + resolved "https://registry.yarnpkg.com/mediasoup/-/mediasoup-3.14.7.tgz#ab717f503ad2ad257a623af5c8c40e2366ec1868" + integrity sha512-Byq5ZwLYa2sjtTUoN3f7rbEDKT4gJ/NZTMNsWzbKLBI6n+szPigQXo8+39DDOnPXeShzX5ciEY7mktwMy6IZLw== + dependencies: + "@types/ini" "^4.1.0" + debug "^4.3.4" + flatbuffers "^24.3.25" + h264-profile-level-id "^2.0.0" + ini "^4.1.2" + node-fetch "^3.3.2" + supports-color "^9.4.0" + tar "^7.1.0" + memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -8072,6 +8226,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +minipass@^7.1.0, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -8080,6 +8239,14 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +minizlib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.1.tgz#46d5329d1eb3c83924eff1d3b858ca0a31581012" + integrity sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg== + dependencies: + minipass "^7.0.4" + rimraf "^5.0.5" + mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -8092,6 +8259,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + mrmime@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" @@ -8199,6 +8371,11 @@ node-dir@^0.1.17: dependencies: minimatch "^3.0.2" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" @@ -8206,6 +8383,15 @@ node-fetch@^2.2.0, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.12, nod dependencies: whatwg-url "^5.0.0" +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + node-forge@^1.2.1, node-forge@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -8768,6 +8954,14 @@ path-scurry@^1.10.2: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -9059,7 +9253,7 @@ query-string@^7.1.3: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -queue-microtask@^1.2.2: +queue-microtask@^1.2.2, queue-microtask@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== @@ -9606,6 +9800,13 @@ rimraf@^3.0.0, rimraf@^3.0.2: dependencies: glob "^7.1.3" +rimraf@^5.0.5: + version "5.0.7" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.7.tgz#27bddf202e7d89cb2e0381656380d1734a854a74" + integrity sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg== + dependencies: + glob "^10.3.7" + rimraf@~2.4.0: version "2.4.5" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" @@ -9739,6 +9940,11 @@ schema-utils@^4.0.1: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +sdp-transform@^2.14.2: + version "2.14.2" + resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.14.2.tgz#d2cee6a1f7abe44e6332ac6cbb94e8600f32d813" + integrity sha512-icY6jVao7MfKCieyo1AyxFYm1baiM+fA00qW/KrNNVlkxHAd34riEKuEkUe4bBb3gJwLJZM+xT60Yj1QL8rHiA== + "semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -10343,6 +10549,11 @@ supports-color@^8.0.0: dependencies: has-flag "^4.0.0" +supports-color@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.4.0.tgz#17bfcf686288f531db3dea3215510621ccb55954" + integrity sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw== + supports-hyperlinks@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" @@ -10429,6 +10640,18 @@ tar@^6.0.2, tar@^6.0.5: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.1.0.tgz#c6d4ec5b10ccdffe8bc412b206eaeaf5181f3098" + integrity sha512-ENhg4W6BmjYxl8GTaE7/h99f0aXiSWv4kikRZ9n2/JRxypZniE84ILZqimAhxxX7Zb8Px6pFdheW3EeHfhnXQQ== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.0" + minizlib "^3.0.1" + mkdirp "^3.0.1" + yallist "^5.0.0" + temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" @@ -10816,7 +11039,7 @@ typescript@^5.2.2, typescript@^5.3.0, typescript@^5.3.3, typescript@^5.4.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== -ua-parser-js@^1.0.35: +ua-parser-js@^1.0.35, ua-parser-js@^1.0.37: version "1.0.37" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.37.tgz#b5dc7b163a5c1f0c510b08446aed4da92c46373f" integrity sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ== @@ -11025,6 +11248,11 @@ uuid@^8.0.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -11106,7 +11334,7 @@ web-encoding@1.1.5: optionalDependencies: "@zxing/text-encoding" "0.9.0" -web-streams-polyfill@^3.1.1: +web-streams-polyfill@^3.0.3, web-streams-polyfill@^3.1.1: version "3.3.3" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== @@ -11285,6 +11513,11 @@ ws@^8.12.1, ws@^8.16.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== +ws@^8.17.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" + integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow== + xcode@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/xcode/-/xcode-3.0.1.tgz#3efb62aac641ab2c702458f9a0302696146aa53c" @@ -11341,6 +11574,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + yaml@^2.2.1, yaml@^2.3.4: version "2.4.1" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" @@ -11404,4 +11642,4 @@ zustand@^4.5.2: resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848" integrity sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g== dependencies: - use-sync-external-store "1.2.0" \ No newline at end of file + use-sync-external-store "1.2.0"