import type { ChadClient, Consumer, Producer } from '#shared/types' import type { MediaKind, ProducerOptions } from 'mediasoup-client/types' import { createSharedComposable } from '@vueuse/core' import * as mediasoupClient from 'mediasoup-client' import { shallowRef } from 'vue' import { useDevices } from '~/composables/use-devices' import { usePreferences } from '~/composables/use-preferences' import { useSignaling } from '~/composables/use-signaling' type ProducerType = 'microphone' | 'video' | 'share' interface SpeakingClient { clientId: ChadClient['socketId'] volume: number } 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 sfx = useSfx() 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 consumers = ref>({}) const producers = ref>({}) const consumersArray = computed(() => { return Object.values(consumers.value) }) const audioConsumers = computed(() => { return consumersArray.value.filter(consumer => consumer.raw.kind === 'audio') }) const videoConsumers = computed(() => { return consumersArray.value.filter(consumer => consumer.raw.kind === 'video' && consumer.appData.source !== 'share') }) const shareConsumers = computed(() => { return consumersArray.value.filter(consumer => consumer.raw.kind === 'video' && consumer.appData.source === 'share') }) const producersArray = computed(() => { return Object.values(producers.value) }) const micProducer = computed(() => { return producersArray.value.find(producer => producer.raw.kind === 'audio' && producer.raw.appData.source === 'mic-video') }) const videoProducer = computed(() => { return producersArray.value.find(producer => producer.raw.kind === 'video' && producer.raw.appData.source !== 'share') }) const shareProducer = computed(() => { return producersArray.value.find(producer => producer.raw.kind === 'video' && producer.raw.appData.source === 'share') }) const speakingClients = shallowRef([]) 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) => { sfx.playRandomConnectionSound(client.socketId).then() 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 || 'stream'}`, appData: { ...appData, socketId }, }) if (producerPaused) consumer.pause() consumers.value[consumer.id] = { id: consumer.id, paused: consumer.paused, appData: consumer.appData, raw: markRaw(consumer), } consumer.observer.on('resume', () => { consumers.value[consumer.id]!.paused = false }) consumer.observer.on('pause', () => { consumers.value[consumer.id]!.paused = true }) consumer.observer.on('close', () => { delete consumers.value[consumer.id] }) consumer.on('trackended', () => { consumer.close() }) cb() }, ) socket.on( 'consumerClosed', async ( { consumerId }, ) => { const consumer = consumers.value[consumerId] if (!consumer) return consumer.raw.close() }, ) socket.on('consumerPaused', ({ consumerId }) => { const consumer = consumers.value[consumerId] if (!consumer) return consumer.raw.pause() }) socket.on('consumerResumed', ({ consumerId }) => { const consumer = consumers.value[consumerId] if (!consumer) return consumer.raw.resume() }) socket.on('speakingPeers', (value: SpeakingClient[]) => { speakingClients.value = value }) socket.on('disconnect', () => { device.value = undefined rtpCapabilities.value = undefined sendTransport.value?.close() sendTransport.value = undefined recvTransport.value?.close() recvTransport.value = undefined consumers.value = {} producers.value = {} }) }, { immediate: true, flush: 'sync' }) async function createProducer(options: ProducerOptions) { if (!device.value || !sendTransport.value) return if (!options.track) return if (!device.value.canProduce(options.track.kind as MediaKind)) return const producer = await sendTransport.value.produce({ disableTrackOnPause: true, ...options }) producers.value[producer.id] = { id: producer.id, paused: producer.paused, appData: producer.appData, raw: markRaw(producer), } producer.observer.on('pause', () => { producers.value[producer.id]!.paused = true }) producer.observer.on('resume', () => { producers.value[producer.id]!.paused = false }) producer.observer.on('close', () => { delete producers.value[producer.id] }) producer.on('trackended', () => { disableProducer(producers.value[producer.id]!) }) } async function disableProducer(producer: Producer) { if (!signaling.socket.value) return try { producer.raw.close() await signaling.socket.value.emitWithAck('closeProducer', { producerId: producer.id, }) } catch { } finally { delete producers.value[producer.id] } } async function enableMic() { if (micProducer.value) return 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 createProducer({ track, streamId: 'mic-video', codecOptions: { opusStereo: true, opusDtx: true, // Меньше пакетов летит когда тишина opusFec: false, // Фиксит пакет лос }, appData: { source: 'mic-video', }, }) } async function disableMic() { if (!micProducer.value) return await disableProducer(micProducer.value) } async function enableVideo() { if (videoProducer.value) return if (!device.value) return const stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: preferences.videoDeviceId.value }, width: { ideal: 1920 }, height: { ideal: 1080 }, frameRate: { ideal: 60 }, }, }) const track = stream.getVideoTracks()[0] if (!track) return await createProducer({ track, streamId: 'mic-video', // codec: device.value.rtpCapabilities.codecs?.find( // c => c.mimeType.toLowerCase() === 'video/AV1', // ), // codecOptions: { // videoGoogleStartBitrate: 1000, // }, appData: { source: 'mic-video', }, }) } async function enableShare() { if (shareProducer.value) return if (!device.value) return const stream = await getShareStream(preferences.shareFps.value) const track = stream.getVideoTracks()[0] if (!track) return await createProducer({ track, streamId: 'share', codec: device.value.rtpCapabilities.codecs?.find( c => c.mimeType.toLowerCase() === 'video/AV1', ), codecOptions: { videoGoogleStartBitrate: 1000, }, zeroRtpOnPause: true, appData: { source: 'share', }, }) } async function pauseProducer(producer: Producer) { if (!signaling.socket.value) return if (producer.paused) return try { producer.raw.pause() await signaling.socket.value.emitWithAck('pauseProducer', { producerId: producer.id, }) } catch { producer.raw.resume() } } async function resumeProducer(producer: Producer) { if (!signaling.socket.value) return try { producer.raw.resume() await signaling.socket.value.emitWithAck('resumeProducer', { producerId: producer.id, }) } catch { producer.raw.pause() } } watch([ preferences.inputDeviceId, preferences.echoCancellation, preferences.autoGainControl, preferences.noiseSuppression, ], async ([inputDeviceId]) => { await disableMic() if (!inputDeviceId) return await enableMic() }) return { consumers, audioConsumers, videoConsumers, shareConsumers, producers, speakingClients, sendTransport, recvTransport, rtpCapabilities, device, micProducer, videoProducer, shareProducer, pauseProducer, resumeProducer, enableVideo, enableShare, disableProducer, } })