This commit is contained in:
@@ -31,28 +31,36 @@ export const useApp = createGlobalState(() => {
|
||||
|
||||
const outputMuted = ref(false)
|
||||
|
||||
const videoEnabled = computed(() => {
|
||||
return !!mediasoup.videoProducer.value
|
||||
})
|
||||
|
||||
const sharingEnabled = computed(() => {
|
||||
return !!mediasoup.shareProducer.value
|
||||
})
|
||||
|
||||
const somebodyStreamingVideo = computed(() => {
|
||||
return mediasoup.videoConsumers.value.length > 0 || mediasoup.shareConsumers.value.length > 0
|
||||
})
|
||||
|
||||
async function muteInput() {
|
||||
if (inputMuted.value)
|
||||
if (inputMuted.value || !mediasoup.micProducer.value)
|
||||
return
|
||||
|
||||
await mediasoup.pauseProducer('microphone')
|
||||
await mediasoup.pauseProducer(mediasoup.micProducer.value)
|
||||
|
||||
toast.add({ severity: 'info', summary: 'Microphone muted', closable: false, life: 1000 })
|
||||
}
|
||||
|
||||
async function unmuteInput() {
|
||||
if (!inputMuted.value)
|
||||
if (!inputMuted.value || !mediasoup.micProducer.value)
|
||||
return
|
||||
|
||||
if (outputMuted.value) {
|
||||
await unmuteOutput()
|
||||
}
|
||||
|
||||
await mediasoup.resumeProducer('microphone')
|
||||
await mediasoup.resumeProducer(mediasoup.micProducer.value)
|
||||
|
||||
toast.add({ severity: 'info', summary: 'Microphone activated', closable: false, life: 1000 })
|
||||
}
|
||||
@@ -101,12 +109,21 @@ export const useApp = createGlobalState(() => {
|
||||
await muteOutput()
|
||||
}
|
||||
|
||||
async function toggleVideo() {
|
||||
if (!mediasoup.videoProducer.value) {
|
||||
await mediasoup.enableVideo()
|
||||
}
|
||||
else {
|
||||
await mediasoup.disableProducer(mediasoup.videoProducer.value)
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleShare() {
|
||||
if (!mediasoup.shareProducer.value) {
|
||||
await mediasoup.enableShare()
|
||||
}
|
||||
else {
|
||||
await mediasoup.disableProducer('share')
|
||||
await mediasoup.disableProducer(mediasoup.shareProducer.value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,10 +138,13 @@ export const useApp = createGlobalState(() => {
|
||||
muteOutput,
|
||||
unmuteOutput,
|
||||
toggleOutput,
|
||||
toggleVideo,
|
||||
version,
|
||||
isTauri,
|
||||
commitSha,
|
||||
toggleShare,
|
||||
videoEnabled,
|
||||
sharingEnabled,
|
||||
somebodyStreamingVideo,
|
||||
}
|
||||
})
|
||||
|
||||
52
client/app/composables/use-client.ts
Normal file
52
client/app/composables/use-client.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { ChadClient } from '#shared/types'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
export function useClient(socketId: MaybeRef<ChadClient['socketId']>) {
|
||||
const mediasoup = useMediasoup()
|
||||
const { getClient } = useClients()
|
||||
|
||||
const client = computed(() => getClient(unref(socketId))!)
|
||||
|
||||
const volume = useLocalStorage<number>(computed(() => `CLIENT_VOLUME_${client.value.userId}`), 100, { writeDefaults: false })
|
||||
const premuted = useLocalStorage<boolean>(computed(() => `CLIENT_PREMUTED_${client.value.userId}`), false, { writeDefaults: false })
|
||||
|
||||
const consumers = computed(() => {
|
||||
return Object.values(mediasoup.consumers.value).filter(consumer => consumer.appData.socketId === client.value.socketId)
|
||||
})
|
||||
|
||||
const producers = computed(() => {
|
||||
return mediasoup.producers.value.values().filter(producer => producer.appData.socketId === client.value.socketId).toArray()
|
||||
})
|
||||
|
||||
const audioConsumers = computed(() => {
|
||||
return mediasoup.audioConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId)
|
||||
})
|
||||
|
||||
const videoConsumers = computed(() => {
|
||||
return mediasoup.videoConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId)
|
||||
})
|
||||
|
||||
const shareConsumers = computed(() => {
|
||||
return mediasoup.shareConsumers.value.filter(consumer => consumer.appData.socketId === client.value.socketId)
|
||||
})
|
||||
|
||||
const streaming = computed(() => {
|
||||
return videoConsumers.value.length > 0 || shareConsumers.value.length > 0
|
||||
})
|
||||
|
||||
const speaking = computed(() => {
|
||||
return mediasoup.speakingClients.value.some(speaker => speaker.clientId === client.value.socketId)
|
||||
})
|
||||
|
||||
return {
|
||||
volume,
|
||||
premuted,
|
||||
consumers,
|
||||
producers,
|
||||
audioConsumers,
|
||||
videoConsumers,
|
||||
shareConsumers,
|
||||
streaming,
|
||||
speaking,
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,16 @@ export const useDevices = createGlobalState(() => {
|
||||
})
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
if (permissionGranted.value)
|
||||
return
|
||||
|
||||
await ensurePermissions()
|
||||
})()
|
||||
|
||||
return {
|
||||
ensurePermissions,
|
||||
permissionGranted,
|
||||
videoInputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(videoInputs.value))),
|
||||
audioInputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(audioInputs.value))),
|
||||
audioOutputs: computed<MediaDeviceInfo[]>(() => JSON.parse(JSON.stringify(audioOutputs.value))),
|
||||
|
||||
5
client/app/composables/use-fullscreen-gallery.ts
Normal file
5
client/app/composables/use-fullscreen-gallery.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
|
||||
export const useFullscreenGallery = createSharedComposable(() => {
|
||||
return {}
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const usePreferences = createGlobalState(() => {
|
||||
|
||||
const inputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('INPUT_DEVICE_ID', 'default')
|
||||
const outputDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('OUTPUT_DEVICE_ID', 'default')
|
||||
const videoDeviceId = useLocalStorage<MediaDeviceInfo['deviceId']>('VIDEO_DEVICE_ID', 'default')
|
||||
|
||||
const autoGainControl = useLocalStorage('AUTO_GAIN_CONTROL', false)
|
||||
const noiseSuppression = useLocalStorage('NOISE_SUPPRESSION', true)
|
||||
@@ -32,6 +33,10 @@ export const usePreferences = createGlobalState(() => {
|
||||
return audioOutputs.value.some(device => device.deviceId === outputDeviceId.value)
|
||||
})
|
||||
|
||||
const videoDeviceExist = computed(() => {
|
||||
return videoInputs.value.some(device => device.deviceId === videoDeviceId.value)
|
||||
})
|
||||
|
||||
watchDebounced(
|
||||
[toggleInputHotkey, toggleOutputHotkey],
|
||||
async ([toggleInputHotkey, toggleOutputHotkey]) => {
|
||||
@@ -56,6 +61,7 @@ export const usePreferences = createGlobalState(() => {
|
||||
synced,
|
||||
inputDeviceId,
|
||||
outputDeviceId,
|
||||
videoDeviceId,
|
||||
autoGainControl,
|
||||
noiseSuppression,
|
||||
echoCancellation,
|
||||
@@ -64,5 +70,6 @@ export const usePreferences = createGlobalState(() => {
|
||||
toggleOutputHotkey,
|
||||
inputDeviceExist,
|
||||
outputDeviceExist,
|
||||
videoDeviceExist,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user