import type { MediaKind, ProducerOptions } from 'mediasoup-client/types' import { createSharedComposable } from '@vueuse/core' import * as mediasoupClient from 'mediasoup-client' import { useDevices } from '~/composables/use-devices' import { usePreferences } from '~/composables/use-preferences' import { useSignaling } from '~/composables/use-signaling' type ProducerType = 'microphone' | 'camera' | 'share' const ICE_SERVERS: RTCIceServer[] = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.l.google.com:5349' }, { urls: 'stun:stun1.l.google.com:3478' }, { urls: 'stun:stun1.l.google.com:5349' }, { urls: 'stun:stun2.l.google.com:19302' }, { urls: 'stun:stun2.l.google.com:5349' }, { urls: 'stun:stun3.l.google.com:3478' }, { urls: 'stun:stun3.l.google.com:5349' }, { urls: 'stun:stun4.l.google.com:19302' }, { urls: 'stun:stun4.l.google.com:5349' }, ] export const useMediasoup = createSharedComposable(() => { const toast = useToast() const signaling = useSignaling() const { addClient, removeClient } = useClients() const preferences = usePreferences() const { getShareStream } = useDevices() const device = shallowRef() const rtpCapabilities = shallowRef() const sendTransport = shallowRef() const recvTransport = shallowRef() const micProducer = shallowRef() const cameraProducer = shallowRef() const shareProducer = shallowRef() const consumers = shallowRef>(new Map()) const producers = shallowRef>(new Map()) watch(signaling.socket, (socket) => { if (!socket) return socket.on('authenticated', async () => { if (!signaling.socket.value) return device.value = new mediasoupClient.Device() rtpCapabilities.value = await signaling.socket.value.emitWithAck('getRtpCapabilities') await device.value.load({ routerRtpCapabilities: rtpCapabilities.value! }) // Send transport { const transportInfo = await signaling.socket.value.emitWithAck('createTransport', { producing: true, consuming: false }) sendTransport.value = device.value.createSendTransport({ ...transportInfo, iceServers: [ ...ICE_SERVERS, ...(transportInfo.iceServers ?? []), ], }) sendTransport.value.on('connect', async ({ dtlsParameters }, callback, errback) => { try { await signaling.socket.value!.emitWithAck('connectTransport', { transportId: sendTransport.value!.id, dtlsParameters, }) callback() } catch (error) { if (error instanceof Error) { errback(error) } } }) sendTransport.value.on('produce', async ({ kind, rtpParameters, appData }, callback, errback) => { try { const { id } = await signaling.socket.value!.emitWithAck('produce', { transportId: sendTransport.value!.id, kind, rtpParameters, appData, }) callback({ id }) } catch (error) { if (error instanceof Error) { errback(error) } } }) } // Recv Transport { const transportInfo = await signaling.socket.value.emitWithAck('createTransport', { producing: false, consuming: true }) recvTransport.value = device.value.createRecvTransport({ ...transportInfo, iceServers: [ ...ICE_SERVERS, ...(transportInfo.iceServers ?? []), ], }) recvTransport.value.on('connect', async ({ dtlsParameters }, callback, errback) => { try { await signaling.socket.value!.emitWithAck('connectTransport', { transportId: recvTransport.value!.id, dtlsParameters, }) callback() } catch (error) { if (error instanceof Error) { errback(error) } } }) } const joinedClients = (await signaling.socket.value.emitWithAck('join', { rtpCapabilities: rtpCapabilities.value, })) addClient(...joinedClients) toast.add({ severity: 'success', summary: 'Joined', closable: false, life: 1000 }) await enableMic() }) socket.on('newPeer', (client) => { addClient(client) }) socket.on('peerClosed', (id) => { removeClient(id) }) socket.on( 'newConsumer', async ( { id, producerId, kind, rtpParameters, socketId, appData, producerPaused }, cb, ) => { if (!recvTransport.value) return const consumer = await recvTransport.value.consume({ id, producerId, kind, rtpParameters, streamId: `${socketId}-${appData.source === 'share' ? 'share' : 'mic-webcam'}`, appData: { ...appData, socketId }, }) if (producerPaused) consumer.pause() consumer.on('transportclose', () => { if (consumers.value.delete(consumer.id)) triggerRef(consumers) }) consumers.value.set(consumer.id, consumer) triggerRef(consumers) cb() }, ) socket.on( 'consumerClosed', async ( { consumerId }, ) => { if (consumers.value.delete(consumerId)) triggerRef(consumers) }, ) socket.on('disconnect', () => { device.value = undefined rtpCapabilities.value = undefined sendTransport.value?.close() sendTransport.value = undefined recvTransport.value?.close() recvTransport.value = undefined micProducer.value = undefined cameraProducer.value = undefined shareProducer.value = undefined consumers.value = new Map() producers.value = new Map() }) socket.on('consumerPaused', ({ consumerId }) => { const consumer = consumers.value.get(consumerId) if (!consumer) return consumer.pause() triggerRef(consumers) }) socket.on('consumerResumed', ({ consumerId }) => { const consumer = consumers.value.get(consumerId) if (!consumer) return consumer.resume() triggerRef(consumers) }) }, { immediate: true, flush: 'sync' }) async function enableProducer(type: ProducerType, options: ProducerOptions) { const producer = getProducerByType(type) if (producer.value) return if (!device.value || !sendTransport.value) return if (!options.track) return if (!device.value.canProduce(options.track.kind as MediaKind)) return producer.value = await sendTransport.value.produce(options) producers.value.set(producer.value.id, producer.value) triggerRef(producers) triggerRef(producer) producer.value.on('transportclose', () => { micProducer.value = undefined }) producer.value.on('trackended', () => { disableProducer(type) }) } async function disableProducer(type: ProducerType) { const producer = getProducerByType(type) if (!signaling.socket.value || !producer.value) return producers.value.delete(producer.value.id) try { producer.value.close() await signaling.socket.value.emitWithAck('closeProducer', { producerId: producer.value.id, }) } catch { } finally { triggerRef(producers) triggerRef(producer) } producer.value = undefined } async function enableMic() { const stream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: { exact: preferences.inputDeviceId.value }, autoGainControl: { exact: preferences.autoGainControl.value }, echoCancellation: { exact: preferences.echoCancellation.value }, noiseSuppression: { exact: preferences.noiseSuppression.value }, }, }) const track = stream.getAudioTracks()[0] if (!track) return await enableProducer('microphone', { track, codecOptions: { opusStereo: true, opusDtx: true, // Меньше пакетов летит когда тишина opusFec: false, // Фиксит пакет лос }, }) } async function disableMic() { await disableProducer('microphone') } async function enableShare() { if (!device.value) return const stream = await getShareStream(preferences.shareFps.value) const track = stream.getVideoTracks()[0] if (!track) return await enableProducer('share', { track, codec: device.value.rtpCapabilities.codecs?.find( c => c.mimeType.toLowerCase() === 'video/h264', ), codecOptions: { videoGoogleStartBitrate: 1000, }, appData: { source: 'share', }, }) } async function pauseProducer(type: ProducerType) { if (!signaling.socket.value) return const producer = getProducerByType(type) if (!producer.value) return if (producer.value.paused) return try { producer.value.pause() await signaling.socket.value.emitWithAck('pauseProducer', { producerId: producer.value.id, }) } catch { producer.value.resume() } finally { triggerRef(producers) triggerRef(producer) } } async function resumeProducer(type: ProducerType) { if (!signaling.socket.value) return const producer = getProducerByType(type) if (!producer.value) return try { producer.value.resume() await signaling.socket.value.emitWithAck('resumeProducer', { producerId: producer.value.id, }) } catch { producer.value.pause() } finally { triggerRef(producers) triggerRef(producer) } } async function init() { signaling.connect() } function getProducerByType(type: ProducerType) { switch (type) { case 'microphone': return micProducer case 'camera': return cameraProducer case 'share': return shareProducer } } watch([ preferences.inputDeviceId, preferences.echoCancellation, preferences.autoGainControl, preferences.noiseSuppression, ], async ([inputDeviceId]) => { await disableMic() if (!inputDeviceId) return await enableMic() }) return { init, consumers, producers, sendTransport, recvTransport, rtpCapabilities, device, micProducer, cameraProducer, shareProducer, pauseProducer, resumeProducer, enableShare, disableProducer, } })