diff --git a/client/.yarn/install-state.gz b/client/.yarn/install-state.gz index 5cb82cb..291cca4 100644 Binary files a/client/.yarn/install-state.gz and b/client/.yarn/install-state.gz differ diff --git a/client/app/components/ClientRow.vue b/client/app/components/ClientRow.vue index 174031b..13debd1 100644 --- a/client/app/components/ClientRow.vue +++ b/client/app/components/ClientRow.vue @@ -28,7 +28,7 @@
- + @@ -60,7 +60,6 @@ diff --git a/client/app/components/Debug/Consumer.vue b/client/app/components/Debug/Consumer.vue new file mode 100644 index 0000000..c22f065 --- /dev/null +++ b/client/app/components/Debug/Consumer.vue @@ -0,0 +1,25 @@ + + + diff --git a/client/app/components/FullscreenGallery.vue b/client/app/components/FullscreenGallery.vue new file mode 100644 index 0000000..2f620d0 --- /dev/null +++ b/client/app/components/FullscreenGallery.vue @@ -0,0 +1,20 @@ + + + diff --git a/client/app/components/FullscreenGallery/Card.vue b/client/app/components/FullscreenGallery/Card.vue new file mode 100644 index 0000000..b5514cb --- /dev/null +++ b/client/app/components/FullscreenGallery/Card.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/client/app/components/Gallery/Card.vue b/client/app/components/Gallery/Card.vue new file mode 100644 index 0000000..871a491 --- /dev/null +++ b/client/app/components/Gallery/Card.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/client/app/composables/use-app.ts b/client/app/composables/use-app.ts index 3e0a901..6499ba7 100644 --- a/client/app/composables/use-app.ts +++ b/client/app/composables/use-app.ts @@ -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, } }) diff --git a/client/app/composables/use-client.ts b/client/app/composables/use-client.ts new file mode 100644 index 0000000..1e5c90d --- /dev/null +++ b/client/app/composables/use-client.ts @@ -0,0 +1,52 @@ +import type { ChadClient } from '#shared/types' +import { useLocalStorage } from '@vueuse/core' + +export function useClient(socketId: MaybeRef) { + const mediasoup = useMediasoup() + const { getClient } = useClients() + + const client = computed(() => getClient(unref(socketId))!) + + const volume = useLocalStorage(computed(() => `CLIENT_VOLUME_${client.value.userId}`), 100, { writeDefaults: false }) + const premuted = useLocalStorage(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, + } +} diff --git a/client/app/composables/use-devices.ts b/client/app/composables/use-devices.ts index 85f265b..41470a9 100644 --- a/client/app/composables/use-devices.ts +++ b/client/app/composables/use-devices.ts @@ -19,7 +19,16 @@ export const useDevices = createGlobalState(() => { }) } + ;(async () => { + if (permissionGranted.value) + return + + await ensurePermissions() + })() + return { + ensurePermissions, + permissionGranted, videoInputs: computed(() => JSON.parse(JSON.stringify(videoInputs.value))), audioInputs: computed(() => JSON.parse(JSON.stringify(audioInputs.value))), audioOutputs: computed(() => JSON.parse(JSON.stringify(audioOutputs.value))), diff --git a/client/app/composables/use-fullscreen-gallery.ts b/client/app/composables/use-fullscreen-gallery.ts new file mode 100644 index 0000000..18f49da --- /dev/null +++ b/client/app/composables/use-fullscreen-gallery.ts @@ -0,0 +1,5 @@ +import { createSharedComposable } from '@vueuse/core' + +export const useFullscreenGallery = createSharedComposable(() => { + return {} +}) diff --git a/client/app/composables/use-mediasoup.ts b/client/app/composables/use-mediasoup.ts index 6e3ac3d..4754def 100644 --- a/client/app/composables/use-mediasoup.ts +++ b/client/app/composables/use-mediasoup.ts @@ -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() const recvTransport = shallowRef() - const micProducer = shallowRef() - const cameraProducer = shallowRef() - const shareProducer = shallowRef() + const consumers = ref>({}) + const producers = ref>({}) - const consumers = shallowRef>(new Map()) - const producers = shallowRef>(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([]) @@ -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, } diff --git a/client/app/composables/use-preferences.ts b/client/app/composables/use-preferences.ts index 75ba3f7..f8d7c51 100644 --- a/client/app/composables/use-preferences.ts +++ b/client/app/composables/use-preferences.ts @@ -14,6 +14,7 @@ export const usePreferences = createGlobalState(() => { const inputDeviceId = useLocalStorage('INPUT_DEVICE_ID', 'default') const outputDeviceId = useLocalStorage('OUTPUT_DEVICE_ID', 'default') + const videoDeviceId = useLocalStorage('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, } }) diff --git a/client/app/layouts/default.vue b/client/app/layouts/default.vue index be52c2e..425dd65 100644 --- a/client/app/layouts/default.vue +++ b/client/app/layouts/default.vue @@ -21,6 +21,12 @@ + + + + diff --git a/client/app/pages/preferences.vue b/client/app/pages/preferences.vue index 3ab2638..c8926cb 100644 --- a/client/app/pages/preferences.vue +++ b/client/app/pages/preferences.vue @@ -11,6 +11,7 @@ option-label="label" option-value="deviceId" input-id="inputDevice" + placeholder="No input device" fluid :invalid="!inputDeviceExist" /> @@ -51,11 +52,28 @@ Video -
-
- Share FPS -
+ + + + + + Screen sharing + + +
+

+ FPS +

@@ -118,10 +136,11 @@ definePageMeta({ }) const { isTauri, version, commitSha } = useApp() const { checking, checkForUpdates, lastUpdate } = useUpdater() -const { audioInputs, audioOutputs } = useDevices() +const { audioInputs, audioOutputs, videoInputs } = useDevices() const { inputDeviceId, outputDeviceId, + videoDeviceId, autoGainControl, noiseSuppression, echoCancellation, @@ -129,6 +148,7 @@ const { toggleOutputHotkey, inputDeviceExist, outputDeviceExist, + videoDeviceExist, shareFps, } = usePreferences() diff --git a/client/nuxt.config.ts b/client/nuxt.config.ts index b13acdc..7e9777f 100644 --- a/client/nuxt.config.ts +++ b/client/nuxt.config.ts @@ -86,8 +86,8 @@ export default defineNuxtConfig({ strictPort: true, proxy: { '/api': { - target: 'http://localhost:4000/chad', - // target: 'https://api.koptilnya.xyz/chad', + // target: 'http://localhost:4000/chad', + target: 'https://api.koptilnya.xyz/chad', ws: true, changeOrigin: true, rewrite: (path) => { diff --git a/client/package.json b/client/package.json index e54c225..eb6e11c 100644 --- a/client/package.json +++ b/client/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "build": "nuxt build", - "dev": "nuxt dev", + "dev": "nuxt dev --host", "generate": "nuxt generate", "preview": "nuxt preview", "postinstall": "nuxt prepare" @@ -20,7 +20,7 @@ "@vueuse/core": "^13.9.0", "hotkeys-js": "^4.0.0", "lucide-vue-next": "^0.562.0", - "mediasoup-client": "^3.16.7", + "mediasoup-client": "^3.18.6", "nuxt": "^4.2.2", "postcss": "^8.5.6", "primeicons": "^7.0.0", diff --git a/client/shared/types.ts b/client/shared/types.ts index 6aec3e8..0aaa22a 100644 --- a/client/shared/types.ts +++ b/client/shared/types.ts @@ -1,3 +1,5 @@ +import type { Consumer as MediasoupConsumer, Producer as MediasoupProducer } from 'mediasoup-client/types' + export interface ChadClient { socketId: string userId: string @@ -5,11 +7,30 @@ export interface ChadClient { displayName: string inputMuted?: boolean outputMuted?: boolean + + consumers: unknown[] + producers: unknown[] + volume: number + isDominant: boolean +} + +export interface AppData { + socketId?: ChadClient['socketId'] + source?: 'share' | 'mic-video' +} + +export interface Consumer { + id: MediasoupConsumer['id'] + paused: MediasoupConsumer['paused'] + appData: AppData + raw: MediasoupConsumer +} + +export interface Producer { + id: MediasoupProducer['id'] + paused: MediasoupProducer['paused'] + appData: AppData + raw: MediasoupProducer } export type UpdatedClient = Omit - -export interface SpeakingClient { - clientId: ChadClient['socketId'] - volume: number -} diff --git a/client/src-tauri/tauri.conf.json b/client/src-tauri/tauri.conf.json index c3decf7..4d9725f 100644 --- a/client/src-tauri/tauri.conf.json +++ b/client/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "chad", - "version": "0.2.22", + "version": "0.2.23", "identifier": "xyz.koptilnya.chad", "build": { "frontendDist": "../.output/public", diff --git a/client/yarn.lock b/client/yarn.lock index 962821f..2fd2c39 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -4060,7 +4060,7 @@ __metadata: eslint-plugin-format: "npm:^1.0.2" hotkeys-js: "npm:^4.0.0" lucide-vue-next: "npm:^0.562.0" - mediasoup-client: "npm:^3.16.7" + mediasoup-client: "npm:^3.18.6" nuxt: "npm:^4.2.2" postcss: "npm:^8.5.6" primeicons: "npm:^7.0.0" @@ -6176,12 +6176,12 @@ __metadata: languageName: node linkType: hard -"h264-profile-level-id@npm:^2.3.1": - version: 2.3.1 - resolution: "h264-profile-level-id@npm:2.3.1" +"h264-profile-level-id@npm:^2.3.2": + version: 2.3.2 + resolution: "h264-profile-level-id@npm:2.3.2" dependencies: debug: "npm:^4.4.3" - checksum: 10c0/c3459549bb28e456db62428c79885cffd4958ce282099c4181b09576f8e5ad90b42395a77209fff4f20a7cb920aaeb660f73902f08343daead0f5527faeb4015 + checksum: 10c0/75bd12ff36707ffacf379c31c403d4508f3116ef2065e375deadcfafd4f7d163521cf0c70ae5385ebac970fa0acc07f9dd497c4248cfc1ee5623b4533707731d languageName: node linkType: hard @@ -7302,9 +7302,9 @@ __metadata: languageName: node linkType: hard -"mediasoup-client@npm:^3.16.7": - version: 3.16.7 - resolution: "mediasoup-client@npm:3.16.7" +"mediasoup-client@npm:^3.18.6": + version: 3.18.6 + resolution: "mediasoup-client@npm:3.18.6" dependencies: "@types/debug": "npm:^4.1.12" "@types/events-alias": "npm:@types/events@^3.0.3" @@ -7312,10 +7312,10 @@ __metadata: debug: "npm:^4.4.3" events-alias: "npm:events@^3.3.0" fake-mediastreamtrack: "npm:^2.2.1" - h264-profile-level-id: "npm:^2.3.1" - sdp-transform: "npm:^2.15.0" + h264-profile-level-id: "npm:^2.3.2" + sdp-transform: "npm:^3.0.0" supports-color: "npm:^10.2.2" - checksum: 10c0/da44c6de8889963192c5b0b7907ed628e04d48be73b7bbfbf18012d66b07ede9d7367c0723466e496a87c7002c07f1af432d854c4c5e16cbd0887013870d8abe + checksum: 10c0/f5baff9139afccf88de5db767c1139efa5cdd68f4871e2fa9d6ff94d2e71d2365dc40e9ba6e903cde5fbb51a2d82972e738da656be9f6fc7006640fdd82dd5da languageName: node linkType: hard @@ -9829,12 +9829,12 @@ __metadata: languageName: node linkType: hard -"sdp-transform@npm:^2.15.0": - version: 2.15.0 - resolution: "sdp-transform@npm:2.15.0" +"sdp-transform@npm:^3.0.0": + version: 3.0.0 + resolution: "sdp-transform@npm:3.0.0" bin: sdp-verify: checker.js - checksum: 10c0/96c060f113a3d5418defa168db609f7e23e5bd7954fa1cf7784f103dbe702e24d667e5310d2ac6d88abdb32322af83d6ebd0df08e07f4f172d5ed5888f921386 + checksum: 10c0/828a4595041ba64c86b29075aa4007ab384519b1fa29882db59ccb83b54b2b2a33b60848293f8da537fe151c52f5844fc17c8325396cac309fb19e2e81ec5bf4 languageName: node linkType: hard