import type { ChadClient, RemoteClient } from '#shared/types' import { createSharedComposable } from '@vueuse/core' import * as mediasoupClient from 'mediasoup-client' import { usePreferences } from '~/composables/use-preferences' import { useSignaling } from '~/composables/use-signaling' 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 device = shallowRef() const rtpCapabilities = shallowRef() const sendTransport = shallowRef() const recvTransport = shallowRef() const micProducer = shallowRef() const webcamProducer = shallowRef() const shareProducer = shallowRef() const consumers = shallowRef>(new Map()) const producers = shallowRef>(new Map()) watch(signaling.socket, (socket) => { if (!socket) return socket.on('connect', 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', { username: preferences.username.value, rtpCapabilities: rtpCapabilities.value, })).map(transformClient) addClient(...joinedClients) toast.add({ severity: 'success', summary: 'Joined', closable: false, life: 1000 }) await enableMic() }) socket.on('newPeer', (client) => { addClient(transformClient(client)) }) socket.on('peerClosed', (id) => { removeClient(id) }) socket.on( 'newConsumer', async ( { id, producerId, kind, rtpParameters, peerId: clientId, appData }, cb, ) => { if (!recvTransport.value) return const consumer = await recvTransport.value.consume({ id, producerId, kind, rtpParameters, streamId: `${clientId}-${appData.share ? 'share' : 'mic-webcam'}`, appData: { ...appData, clientId }, }) consumer.on('transportclose', () => { consumers.value.delete(consumer.id) triggerRef(consumers) }) consumers.value.set(consumer.id, consumer) triggerRef(consumers) cb() }, ) socket.on( 'consumerClosed', async ( { consumerId }, ) => { const consumer = consumers.value.get(consumerId) if (!consumer) return consumers.value.delete(consumer.id) triggerRef(consumers) }, ) socket.on('disconnect', () => { sendTransport.value?.close() sendTransport.value = undefined recvTransport.value?.close() recvTransport.value = undefined }) }, { immediate: true, flush: 'sync' }) function getClientConsumers(clientId: ChadClient['id']) { return consumers.value.values().filter(consumer => consumer.appData.clientId === clientId) } async function enableMic() { if (micProducer.value) return if (!device.value || !sendTransport.value) return if (!device.value.canProduce('audio')) return const stream = await navigator.mediaDevices.getUserMedia({ audio: { autoGainControl: false, noiseSuppression: true, echoCancellation: false, channelCount: 2, }, }) const track = stream.getAudioTracks()[0] if (!track) return micProducer.value = await sendTransport.value.produce({ track, codecOptions: { opusStereo: true, opusDtx: true, // Меньше пакетов летит когда тишина opusFec: false, // Фиксит пакет лос }, }) producers.value.set(micProducer.value.id, micProducer.value) triggerRef(producers) micProducer.value.on('transportclose', () => { micProducer.value = undefined }) micProducer.value.on('trackended', () => { disableMic() }) } async function disableMic() { if (!signaling.socket.value || !micProducer.value) return producers.value.delete(micProducer.value.id) triggerRef(producers) try { micProducer.value.close() await signaling.socket.value.emitWithAck('closeProducer', { producerId: micProducer.value.id, }) } catch { } micProducer.value = undefined } async function muteMic() { if (!signaling.socket.value || !micProducer.value) return try { micProducer.value.pause() await signaling.socket.value.emitWithAck('pauseProducer', { producerId: micProducer.value.id, }) } catch { } } async function unmuteMic() { if (!signaling.socket.value || !micProducer.value) return try { micProducer.value.resume() await signaling.socket.value.emitWithAck('resumeProducer', { producerId: micProducer.value.id, }) } catch { } } async function init() { signaling.connect() } function transformClient(client: RemoteClient): ChadClient { return { ...client, isMe: client.id === signaling.socket.value!.id, } } function dispose() { device.value = undefined rtpCapabilities.value = undefined sendTransport.value = undefined recvTransport.value = undefined micProducer.value = undefined webcamProducer.value = undefined shareProducer.value = undefined consumers.value = new Map() producers.value = new Map() } return { init, consumers, producers, sendTransport, recvTransport, rtpCapabilities, device, micProducer, webcamProducer, shareProducer, getClientConsumers, muteMic, unmuteMic, } })