import type { Socket } from 'socket.io-client' import { createSharedComposable } from '@vueuse/core' import * as mediasoupClient from 'mediasoup-client' import { io } from 'socket.io-client' 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 state = useGlobalState() const socket = shallowRef() const device = shallowRef() const rtpCapabilities = shallowRef() const sendTransport = shallowRef() const recvTransport = shallowRef() const micProducer = shallowRef() const webcamProducer = shallowRef() const shareProducer = shallowRef() const producers = shallowRef>(new Map()) const consumers = shallowRef>(new Map()) // // socket.on('producers', async (producers) => { // watch(connected, async () => { // if (!connected.value) // return // // for (const producer of producers) { // await consume(producer.producerId) // } // }, { immediate: true }) // }) // // socket.on('newProducer', async ({ producerId }) => { // await consume(producerId) // }) // // socket.on('producerClosed', async (producerId: Producer['id']) => { // delete streams.value[producerId] // // triggerRef(streams) // }) // // async function consume(producerId: number) { // const params = await socket.emitWithAck('consume', { // producerId, // transportId: recvTransport.id, // rtpCapabilities: device.rtpCapabilities, // }) // // if (params?.error) { // console.error('consume error:', params.error) // return // } // // const consumer = await recvTransport.consume({ // ...params, // id: params.consumerId, // }) // // const stream = new MediaStream([consumer.track]) // // streams.value[producerId] = stream // // triggerRef(streams) // } watch(socket, (socket, prevSocket) => { if (prevSocket) { prevSocket.close() dispose() state.reset() } if (!socket) { return } socket.onAny((event, ...args) => { console.log('[onAny]', event, args) }) socket.onAnyOutgoing((event, ...args) => { console.log('[onAnyOutgoing]', event, args) }) socket.on('connect', () => { if (!state.username.value) state.username.value = socket.id! join() }) socket.on('newPeer', (client) => { state.clients.value.push(client) triggerRef(state.clients) }) socket.on('peerClosed', (id) => { state.clients.value = state.clients.value.filter(client => client.id !== id) }) socket.on( 'newConsumer', async ( { id, producerId, kind, rtpParameters, peerId, appData, producerPaused }, cb, ) => { console.log({ id, producerId, kind, rtpParameters, peerId, appData }, cb) if (!recvTransport.value) return const consumer = await recvTransport.value.consume({ id, producerId, kind, rtpParameters, streamId: `${peerId}-${appData.share ? 'share' : 'mic-webcam'}`, appData: { ...appData, peerId }, }) consumers.value.set(consumer.id, consumer) triggerRef(consumers) consumer.on('transportclose', () => { 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' }) onBeforeUnmount(() => { socket.value?.close() }) async function join() { if (!socket.value) return device.value = new mediasoupClient.Device() rtpCapabilities.value = await socket.value.emitWithAck('getRtpCapabilities') await device.value.load({ routerRtpCapabilities: rtpCapabilities.value! }) // Send transport { const transportInfo = await 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 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 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 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 socket.value!.emitWithAck('connectTransport', { transportId: recvTransport.value!.id, dtlsParameters, }) callback() } catch (error) { if (error instanceof Error) { errback(error) } } }) } const result = await socket.value.emitWithAck('join', { username: state.username.value, rtpCapabilities: rtpCapabilities.value }) state.clients.value = result await enableMic() } 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] 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 (!micProducer.value) return producers.value.delete(micProducer.value.id) triggerRef(producers) try { micProducer.value.close() await socket.value?.emitWithAck('closeProducer', { producerId: micProducer.value.id, }) } catch { } micProducer.value = undefined } async function muteMic() { if (!micProducer.value) return try { micProducer.value.pause() await socket.value?.emitWithAck('pauseProducer', { producerId: micProducer.value.id, }) } catch { } } async function unmuteMic() { if (!micProducer.value) return try { micProducer.value?.resume() await socket.value?.emitWithAck('resumeProducer', { producerId: micProducer.value.id, }) } catch { } } function init() { if (socket.value) return socket.value = io('https://api.koptilnya.xyz/webrtc', { path: '/chad/ws', transports: ['websocket'], }) } 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() } return { init, sendTransport, recvTransport, socket, rtpCapabilities, device, producers, consumers, micProducer, webcamProducer, shareProducer, } })