CoffeeChat/webapp/components/chat/MediaSoupWidget.tsx
2026-04-03 12:35:13 +02:00

389 lines
15 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import { useEffect, useRef, useState } from "react"
import { Device } from 'mediasoup-client'
import { Transport } from "mediasoup-client/types"
const wsURL = 'ws://localhost:4000'
let remoteStream
export default function MediaSoupWidget() {
const socket = useRef<WebSocket | undefined>(undefined)
const device = useRef<Device | undefined>(undefined)
const videoRef = useRef<HTMLVideoElement | null>(null)
const videoBRef = useRef<HTMLVideoElement | null>(null)
const consumerTransport = useRef<Transport | null>(null)
const [connected, setConnected] = useState<boolean>(false)
const [states, setStates] = useState<string[]>([])
const loadDevices = async (routerCapabilities: any) => {
try {
device.current = new Device()
await device.current!.load({ routerRtpCapabilities: routerCapabilities })
// console.log(`Supported : `, device)
setConnected(true)
} catch (e) {
if ((e as any).name === 'UnsupportedErro') {
console.log(`Not Supported`)
}
console.error(`Faield to create device:`, e)
}
}
const getUserMedia = async (transport: any, isWebCam: boolean) => {
if (!device.current?.canProduce('video')) {
console.error(`Cant stream video`)
return
}
let stream
try {
stream = isWebCam ? await navigator.mediaDevices.getUserMedia({
video: true, audio: true
}) : await navigator.mediaDevices.getDisplayMedia({ video: true })
} catch (e) {
console.error(`Unable to get device to stream`, e)
}
return stream
}
const onProducerTransportCreated = async (producerDetails: any) => {
// 5. Createe Send Transport
setStates(s => [...s, 'createSendTransport'])
const transport = device.current?.createSendTransport({ ...producerDetails })
transport?.on('connect', async ({ dtlsParameters }, callback, errback) => {
// 6. connectProducerTransport
setStates(s => [...s, 'createSendTransport'])
socket.current?.send(JSON.stringify({
type: 'connectProducerTransport',
dtlsParameters
}))
socket.current?.addEventListener('message', ev => {
// 7. producerTransportConnected
// console.log(`SEOM TO CHK `, ev.data)
if (JSON.parse(ev.data).type === 'producerTransportConnected') {
setStates(s => [...s, 'producerTransportConnected'])
// console.log(`------->producerTransportConnected:onCB`)
callback()
}
})
})
transport?.on('produce', async ({ kind, rtpParameters }, callback, errback) => {
// 8. Produce
setStates(s => [...s, 'produce'])
socket.current?.send(JSON.stringify({
type: 'produce',
transportId: transport.id,
kind,
rtpParameters
}))
socket.current?.addEventListener('message', ev => {
if (ev.data.type === 'published') {
callback(ev.data.id``)
}
})
})
transport?.on('connectionstatechange', async (state) => {
console.log(`connectionstatechange`, state)
switch (state) {
case 'connecting': break;
case 'closed': break;
case 'connected':
// Link stream here
break;
case 'disconnected': break;
case 'failed':
transport.close()
break;
case 'new': break;
}
})
transport?.on('icecandidateerror', async (error) => {
console.log('icecandidateerror', error)
})
transport?.on('icegatheringstatechange', async (error) => {
console.log('icegatheringstatechange', error)
})
transport?.on('producedata', async (error) => {
console.log('producedata', error)
})
let streamMedia
try {
streamMedia = await getUserMedia(transport, true) // is webcam
const track = streamMedia!.getVideoTracks()[0]
// const params - { track }
// videoRef.current?.h = track
console.log(`LocalStream`, streamMedia)
// videoRef.current!.srcObject = streamMedia
const producer = await transport?.produce({ track })
producer!.on("trackended", () => {
console.log("track ended");
});
producer!.on("transportclose", () => {
console.log("transport ended");
});
}
catch (e) {
console.log(`ERROR`, e)
}
}
const onSubTransportCreated = (consumerDetails: any) => {
consumerTransport.current = device.current!.createRecvTransport({ ...consumerDetails })
console.log(`onSubTransportCreated`, consumerDetails, consumerTransport.current.connectionState)
consumerTransport.current.on('connect', ({ dtlsParameters }, callback, errback) => {
//11 . Accept connect
setStates(s => [...s, 'connectConsumerTransport'])
socket.current?.send(JSON.stringify({
type: 'connectConsumerTransport',
transportId: consumerTransport.current!.id,
dtlsParameters
}))
socket.current?.addEventListener('message', ev => {
if (JSON.parse(ev.data).type === 'subConnected') {
console.log(`subConnected**`, JSON.parse(ev.data).type)
callback()
}
})
})
consumerTransport.current?.on('connectionstatechange', async (state) => {
console.log(`connectionstatechange:`, state)
switch (state) {
case 'connecting':
break;
case 'failed': console.log(`FILAED`); break;
case 'connected':
console.log(`remoteStream`, remoteStream)
// videoBRef.current!.srcObject = remoteStream
socket.current?.send(JSON.stringify({
type: 'resume',
}))
break;
default:
break;
}
})
consumerTransport.current?.on('icecandidateerror', () => {
console.log(`icecandidateerror`)
})
consumerTransport.current?.on('icegatheringstatechange', async (state) => {
console.log(`icegatheringstatechange`, state)
})
const stream = consumer(consumerTransport.current)
}
const onSubscribe = async (details: any) => {
console.log('onSubscribe', details)
const {
producerId,
id,
kind,
rtpParameters,
type,
producerPaused,
} = details
const codecOptions = {}
const consumer = await consumerTransport.current!.consume({
producerId,
id,
kind,
rtpParameters,
// type,
// producerPaused,
// codecOptions
})
const stream = new MediaStream()
stream.addTrack(consumer.track)
console.log(`TRYIN STREAM 1`,stream)
videoBRef.current!.srcObject = stream
}
const consumer = async (transport: any) => {
const rtpCapabilities = device.current?.recvRtpCapabilities
socket.current?.send(JSON.stringify({
type: 'consume',
rtpCapabilities
}))
}
// const connectSendTransport = async () => {
// const producer = await transport.produce(params);
// console.log("Producer created:", producer.id, producer.kind);
// producer.on("trackended", () => {
// console.log("track ended");
// });
// producer.on("transportclose", () => {
// console.log("transport ended");
// });
// };
const stream = () => {
// 4.1 Start creating producers steam
// 4. Producers stream
setStates(s => [...s, 'createProducerTransport'])
socket.current?.send(JSON.stringify({
type: 'createProducerTransport',
forceTcp: false,
rtpCapabilities: device.current?.sendRtpCapabilities
}))
}
const joinStream = () => {
setStates(s => [...s, 'createConsumerTransport'])
socket.current?.send(JSON.stringify({
type: 'createConsumerTransport',
forceTcp: false,
// rtpCapabilities: device.current?.sendRtpCapabilities
}))
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parseWSMessage = (ev: any) => {
const recv = JSON.parse(ev)
// console.log(`-- parseWSMessage --`, recv.data)
switch (recv.type) {
case 'routerCapabilities':
// 3. Received capabilities
setStates(s => [...s, 'routerCapabilities'])
loadDevices(recv.data);
break;
case 'producerTransportCreated':
// 4.2 Received capabilities
setStates(s => [...s, 'producerTransportCreated'])
onProducerTransportCreated(recv.data);
break;
// case 'producerTransportConnected':
// // 7. producerTransportConnected but not with callback
// setStates(s => [...s, 'producerTransportCreated'])
// // callback()
// break;
case 'newProducer':
// 9 Found new Produce contents and send to all clients
setStates(s => [...s, 'newProducer'])
break;
case 'subTransportCreated':
// 10 Consumer joined
setStates(s => [...s, 'subTransportCreated'])
onSubTransportCreated(recv.data);
break;
case 'resumed':
// 10 Consumer joined
setStates(s => [...s, 'resumed'])
console.log(`resumed`, recv.data)
break;
case 'subscribed':
// 12 Consumer joined
setStates(s => [...s, 'subscribed'])
onSubscribe(recv.data)
break;
default:
console.log(`Received Uknown`, recv.type)
break;
}
// console.log(ev)
}
return (
<div>WebRTCChat
<div>
<div className={`${states.includes('ws-connected') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Connected To Server</div>
<small>Establish connection to the WS Server</small>
</div>
<div className={`${states.includes('getRouterRtpCapabilities') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Checking Server Capabilities</div>
<small>getRouterRtpCapabilities</small>
</div>
<div className={`${states.includes('routerCapabilities') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Has Server Capabilities</div>
<small>routerCapabilities</small>
</div>
<div className={`${states.includes('createProducerTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Create Producers Transport (Waiting for user to start stream)</div>
<small>createProducerTransport</small>
</div>
<div className={`${states.includes('producerTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Created Producers Transport</div>
<small>producerTransportCreated</small>
</div>
<div className={`${states.includes('producerTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Created Producers Transport</div>
<small>producerTransportCreated</small>
</div>
<div className={`${states.includes('createSendTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Created Send Transport</div>
<small>createSendTransport</small>
</div>
<div className={`${states.includes('producerTransportConnected') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Producer Transport Connected</div>
<small>producerTransportConnected</small>
</div>
<div className={`${states.includes('producerTransportConnected') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Send Produced</div>
<small>produce</small>
</div>
<div className={`${states.includes('newProducer') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Found new Produce contents and send to all clients</div>
<small>newProducer</small>
</div>
<div className={`${states.includes('subTransportCreated') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Cunsumber connected</div>
<small>subTransportCreated</small>
</div>
<div className={`${states.includes('connectConsumerTransport') ? 'text-green-400' : 'text-gray-400'} border`}>
<div>Accept Consumer Connection</div>
<small>connectConsumerTransport</small>
</div>
</div>
<button
onClick={() => {
socket.current = new WebSocket(wsURL)
socket.current.onopen = () => {
// 1. Create websocket connection
setStates(s => [...s, 'ws-connected'])
const msg = {
type: "getRouterRtpCapabilities"
}
try {
// 2. Get Server capabilities
setStates(s => [...s, 'getRouterRtpCapabilities'])
socket.current?.send(JSON.stringify(msg))
} catch (e) {
console.log(`Failed to send capabilities`, e)
}
}
socket.current.onmessage = e => parseWSMessage((e as any).data)
}}
disabled={connected}
>{connected ? 'Connected' : 'Connect'}</button>
<button onClick={() => stream()}>
Stream
</button>
<video ref={videoRef}></video>
<button
onClick={() => {
joinStream()
}}
>Join</button>
<video ref={videoBRef}></video>
</div>
)
}