389 lines
15 KiB
TypeScript
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>
|
||
|
|
)
|
||
|
|
}
|