вебкамера там, туда-сюда
All checks were successful
Deploy / publish-web (push) Successful in 1m16s

This commit is contained in:
2026-02-02 14:39:16 +06:00
parent 0922fc4f41
commit 269b19a5be
20 changed files with 546 additions and 211 deletions

View File

@@ -1,4 +1,4 @@
import type { SpeakingClient } from '#shared/types'
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'
@@ -7,7 +7,12 @@ import { useDevices } from '~/composables/use-devices'
import { usePreferences } from '~/composables/use-preferences'
import { useSignaling } from '~/composables/use-signaling'
type ProducerType = 'microphone' | 'camera' | 'share'
type ProducerType = 'microphone' | 'video' | 'share'
interface SpeakingClient {
clientId: ChadClient['socketId']
volume: number
}
const ICE_SERVERS: RTCIceServer[] = [
{ urls: 'stun:stun.l.google.com:19302' },
@@ -35,12 +40,40 @@ export const useMediasoup = createSharedComposable(() => {
const sendTransport = shallowRef<mediasoupClient.types.Transport>()
const recvTransport = shallowRef<mediasoupClient.types.Transport>()
const micProducer = shallowRef<mediasoupClient.types.Producer>()
const cameraProducer = shallowRef<mediasoupClient.types.Producer>()
const shareProducer = shallowRef<mediasoupClient.types.Producer>()
const consumers = ref<Record<Consumer['id'], Consumer>>({})
const producers = ref<Record<Producer['id'], Producer>>({})
const consumers = shallowRef<Map<string, mediasoupClient.types.Consumer>>(new Map())
const producers = shallowRef<Map<string, mediasoupClient.types.Producer>>(new Map())
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<SpeakingClient[]>([])
@@ -163,20 +196,35 @@ export const useMediasoup = createSharedComposable(() => {
producerId,
kind,
rtpParameters,
streamId: `${socketId}-${appData.source === 'share' ? 'share' : 'mic-webcam'}`,
streamId: `${socketId}-${appData.source || 'stream'}`,
appData: { ...appData, socketId },
})
if (producerPaused)
consumer.pause()
consumer.on('transportclose', () => {
if (consumers.value.delete(consumer.id))
triggerRef(consumers)
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
})
consumers.value.set(consumer.id, consumer)
triggerRef(consumers)
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()
},
@@ -187,11 +235,37 @@ export const useMediasoup = createSharedComposable(() => {
async (
{ consumerId },
) => {
if (consumers.value.delete(consumerId))
triggerRef(consumers)
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
@@ -202,47 +276,12 @@ export const useMediasoup = createSharedComposable(() => {
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)
})
socket.on('speakingPeers', (value: SpeakingClient[]) => {
speakingClients.value = value
consumers.value = {}
producers.value = {}
})
}, { immediate: true, flush: 'sync' })
async function enableProducer(type: ProducerType, options: ProducerOptions) {
const producer = getProducerByType(type)
if (producer.value)
return
async function createProducer(options: ProducerOptions) {
if (!device.value || !sendTransport.value)
return
@@ -252,47 +291,54 @@ export const useMediasoup = createSharedComposable(() => {
if (!device.value.canProduce(options.track.kind as MediaKind))
return
producer.value = await sendTransport.value.produce(options)
const producer = await sendTransport.value.produce({ disableTrackOnPause: true, ...options })
producers.value.set(producer.value.id, producer.value)
triggerRef(producers)
triggerRef(producer)
producers.value[producer.id] = {
id: producer.id,
paused: producer.paused,
appData: producer.appData,
raw: markRaw(producer),
}
producer.value.on('transportclose', () => {
micProducer.value = undefined
producer.observer.on('pause', () => {
producers.value[producer.id]!.paused = true
})
producer.value.on('trackended', () => {
disableProducer(type)
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(type: ProducerType) {
const producer = getProducerByType(type)
if (!signaling.socket.value || !producer.value)
async function disableProducer(producer: Producer) {
if (!signaling.socket.value)
return
producers.value.delete(producer.value.id)
try {
producer.value.close()
producer.raw.close()
await signaling.socket.value.emitWithAck('closeProducer', {
producerId: producer.value.id,
producerId: producer.id,
})
}
catch {
}
finally {
triggerRef(producers)
triggerRef(producer)
delete producers.value[producer.id]
}
producer.value = undefined
}
async function enableMic() {
if (micProducer.value)
return
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
deviceId: { exact: preferences.inputDeviceId.value },
@@ -307,21 +353,64 @@ export const useMediasoup = createSharedComposable(() => {
if (!track)
return
await enableProducer('microphone', {
await createProducer({
track,
streamId: 'mic-video',
codecOptions: {
opusStereo: true,
opusDtx: true, // Меньше пакетов летит когда тишина
opusFec: false, // Фиксит пакет лос
},
appData: {
source: 'mic-video',
},
})
}
async function disableMic() {
await disableProducer('microphone')
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 },
},
})
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
@@ -332,85 +421,54 @@ export const useMediasoup = createSharedComposable(() => {
if (!track)
return
await enableProducer('share', {
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(type: ProducerType) {
async function pauseProducer(producer: Producer) {
if (!signaling.socket.value)
return
const producer = getProducerByType(type)
if (!producer.value)
return
if (producer.value.paused)
if (producer.paused)
return
try {
producer.value.pause()
producer.raw.pause()
await signaling.socket.value.emitWithAck('pauseProducer', {
producerId: producer.value.id,
producerId: producer.id,
})
}
catch {
producer.value.resume()
}
finally {
triggerRef(producers)
triggerRef(producer)
producer.raw.resume()
}
}
async function resumeProducer(type: ProducerType) {
async function resumeProducer(producer: Producer) {
if (!signaling.socket.value)
return
const producer = getProducerByType(type)
if (!producer.value)
return
try {
producer.value.resume()
producer.raw.resume()
await signaling.socket.value.emitWithAck('resumeProducer', {
producerId: producer.value.id,
producerId: producer.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
producer.raw.pause()
}
}
@@ -429,8 +487,10 @@ export const useMediasoup = createSharedComposable(() => {
})
return {
init,
consumers,
audioConsumers,
videoConsumers,
shareConsumers,
producers,
speakingClients,
sendTransport,
@@ -438,10 +498,11 @@ export const useMediasoup = createSharedComposable(() => {
rtpCapabilities,
device,
micProducer,
cameraProducer,
videoProducer,
shareProducer,
pauseProducer,
resumeProducer,
enableVideo,
enableShare,
disableProducer,
}