206 lines
6.5 KiB
JavaScript
206 lines
6.5 KiB
JavaScript
/** WebRTC/Mediasoup client utilities. */
|
|
import { logError, logWarn } from '../utils/logger.js'
|
|
|
|
const FETCH_OPTS = { credentials: 'include' }
|
|
|
|
export async function createMediasoupDevice(rtpCapabilities) {
|
|
if (typeof window === 'undefined') throw new TypeError('Mediasoup device can only be created in browser')
|
|
const { Device } = await import('mediasoup-client')
|
|
const device = new Device()
|
|
await device.load({ routerRtpCapabilities: rtpCapabilities })
|
|
return device
|
|
}
|
|
|
|
export function createWebSocketConnection(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
const wsUrl = url.startsWith('ws') ? url : `${protocol}//${window.location.host}/ws`
|
|
const ws = new WebSocket(wsUrl)
|
|
ws.onopen = () => resolve(ws)
|
|
ws.onerror = () => reject(new Error('WebSocket connection failed'))
|
|
})
|
|
}
|
|
|
|
export function sendWebSocketMessage(ws, sessionId, type, data = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
if (ws.readyState !== WebSocket.OPEN) {
|
|
reject(new Error('WebSocket not open'))
|
|
return
|
|
}
|
|
|
|
const messageId = `${Date.now()}-${Math.random()}`
|
|
const message = { sessionId, type, data, messageId }
|
|
|
|
const timeout = setTimeout(() => {
|
|
ws.removeEventListener('message', handler)
|
|
reject(new Error('WebSocket message timeout'))
|
|
}, 10000)
|
|
|
|
const handler = (event) => {
|
|
try {
|
|
const response = JSON.parse(event.data)
|
|
if (response.messageId === messageId || response.type) {
|
|
clearTimeout(timeout)
|
|
ws.removeEventListener('message', handler)
|
|
if (response.error) {
|
|
reject(new Error(response.error))
|
|
}
|
|
else {
|
|
resolve(response)
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
// Not our message, continue waiting
|
|
}
|
|
}
|
|
|
|
ws.addEventListener('message', handler)
|
|
ws.send(JSON.stringify(message))
|
|
})
|
|
}
|
|
|
|
function attachTransportHandlers(transport, transportParams, sessionId, label, { onConnectSuccess, onConnectFailure } = {}) {
|
|
transport.on('connect', async ({ dtlsParameters }, callback, errback) => {
|
|
try {
|
|
await $fetch('/api/live/webrtc/connect-transport', {
|
|
method: 'POST',
|
|
body: { sessionId, transportId: transportParams.id, dtlsParameters },
|
|
...FETCH_OPTS,
|
|
})
|
|
onConnectSuccess?.()
|
|
callback()
|
|
}
|
|
catch (err) {
|
|
logError(`useWebRTC: ${label} transport connect failed`, {
|
|
err: err?.message ?? String(err),
|
|
transportId: transportParams.id,
|
|
connectionState: transport.connectionState,
|
|
sessionId,
|
|
})
|
|
onConnectFailure?.(err)
|
|
errback(err)
|
|
}
|
|
})
|
|
transport.on('connectionstatechange', () => {
|
|
const state = transport.connectionState
|
|
if (['failed', 'disconnected', 'closed'].includes(state)) {
|
|
logWarn(`useWebRTC: ${label} transport connection state changed`, { state, transportId: transportParams.id, sessionId })
|
|
}
|
|
})
|
|
}
|
|
|
|
export async function createSendTransport(device, sessionId, options = {}) {
|
|
const transportParams = await $fetch('/api/live/webrtc/create-transport', {
|
|
method: 'POST',
|
|
body: { sessionId, isProducer: true },
|
|
...FETCH_OPTS,
|
|
})
|
|
const transport = device.createSendTransport({
|
|
id: transportParams.id,
|
|
iceParameters: transportParams.iceParameters,
|
|
iceCandidates: transportParams.iceCandidates,
|
|
dtlsParameters: transportParams.dtlsParameters,
|
|
})
|
|
attachTransportHandlers(transport, transportParams, sessionId, 'Send', options)
|
|
|
|
transport.on('produce', async ({ kind, rtpParameters }, callback, errback) => {
|
|
try {
|
|
const { id } = await $fetch('/api/live/webrtc/create-producer', {
|
|
method: 'POST',
|
|
body: { sessionId, transportId: transportParams.id, kind, rtpParameters },
|
|
...FETCH_OPTS,
|
|
})
|
|
callback({ id })
|
|
}
|
|
catch (err) {
|
|
logError('useWebRTC: Producer creation failed', { err: err?.message ?? String(err) })
|
|
errback(err)
|
|
}
|
|
})
|
|
return transport
|
|
}
|
|
|
|
export async function createRecvTransport(device, sessionId) {
|
|
const transportParams = await $fetch('/api/live/webrtc/create-transport', {
|
|
method: 'POST',
|
|
body: { sessionId, isProducer: false },
|
|
...FETCH_OPTS,
|
|
})
|
|
const transport = device.createRecvTransport({
|
|
id: transportParams.id,
|
|
iceParameters: transportParams.iceParameters,
|
|
iceCandidates: transportParams.iceCandidates,
|
|
dtlsParameters: transportParams.dtlsParameters,
|
|
})
|
|
attachTransportHandlers(transport, transportParams, sessionId, 'Recv')
|
|
return transport
|
|
}
|
|
|
|
export async function consumeProducer(transport, device, sessionId) {
|
|
const consumerParams = await $fetch('/api/live/webrtc/create-consumer', {
|
|
method: 'POST',
|
|
body: { sessionId, transportId: transport.id, rtpCapabilities: device.rtpCapabilities },
|
|
...FETCH_OPTS,
|
|
})
|
|
|
|
const consumer = await transport.consume({
|
|
id: consumerParams.id,
|
|
producerId: consumerParams.producerId,
|
|
kind: consumerParams.kind,
|
|
rtpParameters: consumerParams.rtpParameters,
|
|
})
|
|
|
|
if (!consumer.track) {
|
|
logWarn('useWebRTC: Consumer created but no track immediately', { consumerId: consumer.id })
|
|
await waitForCondition(() => consumer.track, 3000, 100)
|
|
if (!consumer.track) {
|
|
logError('useWebRTC: Track did not become available after 3s', { consumerId: consumer.id })
|
|
}
|
|
}
|
|
|
|
return consumer
|
|
}
|
|
|
|
function waitForCondition(condition, timeoutMs = 3000, intervalMs = 100) {
|
|
return new Promise((resolve) => {
|
|
const timeoutId = setTimeout(() => {
|
|
clearInterval(intervalId)
|
|
resolve()
|
|
}, timeoutMs)
|
|
const intervalId = setInterval(() => {
|
|
if (condition()) {
|
|
clearTimeout(timeoutId)
|
|
clearInterval(intervalId)
|
|
resolve()
|
|
}
|
|
}, intervalMs)
|
|
if (condition()) {
|
|
clearTimeout(timeoutId)
|
|
clearInterval(intervalId)
|
|
resolve()
|
|
}
|
|
})
|
|
}
|
|
|
|
export function waitForConnectionState(transport, timeoutMs = 10000) {
|
|
const terminal = ['connected', 'failed', 'disconnected', 'closed']
|
|
return new Promise((resolve) => {
|
|
const tid = ref(null)
|
|
const handler = () => {
|
|
const state = transport.connectionState
|
|
if (terminal.includes(state)) {
|
|
transport.off('connectionstatechange', handler)
|
|
if (tid.value) clearTimeout(tid.value)
|
|
resolve(state)
|
|
}
|
|
}
|
|
transport.on('connectionstatechange', handler)
|
|
handler()
|
|
tid.value = setTimeout(() => {
|
|
transport.off('connectionstatechange', handler)
|
|
resolve(transport.connectionState)
|
|
}, timeoutMs)
|
|
})
|
|
}
|