feat: screensharing working on chrome

This commit is contained in:
2025-10-29 21:15:21 +01:00
parent 4e6633c04b
commit f58f4533a2
6 changed files with 209 additions and 115 deletions

View File

@@ -6,28 +6,22 @@ import { toast } from 'vue-sonner';
const viewerStore = useViewerStore()
const { code: codeRef } = storeToRefs(viewerStore)
const { send, data } = useWebSocket('ws://localhost:3000/ws/signaling', {
const { send } = useWebSocket('ws://localhost:3000/ws/signaling', {
autoReconnect: true,
//heartbeat: true,
onMessage: async (ws, ev) => {
const message = JSON.parse(ev.data)
if (message.event === 'joined') {
toast.success('stweam joined successfullay')
toast.success('stream joined successfully')
}
if (message.event === 'offer') {
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
],
});
await peerConnection.setRemoteDescription(message.sdp);
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
send(JSON.stringify({
event: 'answer',
targetId: message.data.streamerId,
sdp: answer,
}))
viewerStore.setPeerConnection(peerConnection);
peerConnection.ontrack = (event) => {
if (event.streams && event.streams[0] && videofeedRef.value) {
videofeedRef.value.srcObject = event.streams[0];
@@ -38,22 +32,38 @@ const { send, data } = useWebSocket('ws://localhost:3000/ws/signaling', {
if (event.candidate) {
send(JSON.stringify({
event: 'ice-candidate',
targetId: message.data.streamerId,
targetId: message.senderId,
candidate: event.candidate,
}))
}
};
await peerConnection.setRemoteDescription(new RTCSessionDescription(message.sdp));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
send(JSON.stringify({
event: 'answer',
targetId: message.senderId,
sdp: answer,
}))
}
if (message.event === 'ice-candidate') {
if (viewerStore.peerConnection && viewerStore.peerConnection.remoteDescription) {
await viewerStore.peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate));
}
}
},
});
const videofeedRef = ref<HTMLVideoElement|null>(null);
const startWebRTCConnection = async () => {
send(JSON.stringify({
event: 'join-room',
data: {
roomId: viewerStore.code,
}
roomId: viewerStore.code,
}))
}
@@ -71,7 +81,7 @@ watch(codeRef, (newCode) => {
<p>code is {{ viewerStore.code }}</p>
<app-code-input />
<video ref="videofeedRef" autoplay playsinline muted></video>
<video ref="videofeedRef" autoplay playsinline style="width: 100%; max-width: 1200px; background: black;"></video>
<NuxtLink to="/stream"><Button>host instead?</Button></NuxtLink>
</template>

View File

@@ -5,44 +5,63 @@ import { useStreamerStore } from '~/state/streamer';
const streamerStore = useStreamerStore()
const videofeedRef = ref<HTMLVideoElement|null>(null);
const { send, data, ws } = useWebSocket('ws://localhost:3000/ws/signaling', {
const localStream = ref<MediaStream|null>(null);
const { send } = useWebSocket('ws://localhost:3000/ws/signaling', {
autoReconnect: true,
//heartbeat: true,
onMessage: async (ws, ev) => {
const message = JSON.parse(ev.data)
if (message.event === 'room-created') {
const roomId = message.roomId
streamerStore.setCode(roomId)
streamerStore.setCode(message.roomId)
}
if (message.event === 'viewer-joined') {
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
streamerStore.addPeerConnection(message.data.viewerId, peerConnection)
streamerStore.addPeerConnection(message.viewerId, peerConnection)
// Add media tracks to peer connection
if (localStream.value) {
localStream.value.getTracks().forEach(track => {
peerConnection.addTrack(track, localStream.value!);
});
}
// Handle ICE candidates
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
send(JSON.stringify({
event: 'ice-candidate',
targetId: message.viewerId,
candidate: event.candidate,
}))
}
};
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
send(JSON.stringify({
event: 'offer',
targetId: message.data.viewerId,
targetId: message.viewerId,
sdp: offer,
}))
}
if (message.event === 'ice-candidate') {
ws.send(JSON.stringify({
event: 'ice-candidate',
targetId: message.data.senderId,
candidate: message.data.candidate,
}))
const pc = streamerStore.peerConnections[message.from];
if (pc) {
await pc.addIceCandidate(new RTCIceCandidate(message.candidate));
}
}
if (message.event === 'answer') {
const pc = streamerStore.peerConnections[message.data.viewerId];
if (!pc) {
console.error('peerconnection not found for peerid: ', message.data.viewerId);
return;
};
const remoteDesc = new RTCSessionDescription(message.data.sdp);
await pc.setRemoteDescription(remoteDesc);
const pc = streamerStore.peerConnections[message.from];
if (pc) {
await pc.setRemoteDescription(new RTCSessionDescription(message.sdp));
}
}
},
});
@@ -53,6 +72,8 @@ async function startScreenShare() {
audio: false,
});
localStream.value = stream;
if (videofeedRef.value) {
videofeedRef.value.srcObject = stream;
}
@@ -69,4 +90,4 @@ async function startScreenShare() {
</Button>
<p>Your stream code: {{ streamerStore.code }}</p>
<video ref="videofeedRef" autoplay playsinline muted></video>
</template>
</template>

View File

@@ -3,10 +3,14 @@ import { defineStore} from 'pinia';
export const useViewerStore = defineStore('viewer', {
state: () => ({
code: '',
peerConnection: null as RTCPeerConnection | null,
}),
actions: {
setCode(code: string) {
this.code = code;
},
setPeerConnection(pc: RTCPeerConnection) {
this.peerConnection = pc;
},
},
});

View File

@@ -16,6 +16,7 @@
"@vueuse/core": "^14.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"ioredis": "^5.8.2",
"lucide-vue-next": "^0.548.0",
"nuxt": "^4.2.0",
"pinia": "^3.0.3",
@@ -32,6 +33,7 @@
"@iconify-json/radix-icons": "^1.2.5",
"@iconify/vue": "^5.0.0",
"@nuxtjs/color-mode": "^3.5.2",
"@types/node": "^24.9.2",
"nuxi": "^3.29.3",
"typescript": "^5.9.3"
}

118
pnpm-lock.yaml generated
View File

@@ -13,7 +13,7 @@ importers:
version: 0.11.2(magicast@0.5.0)(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))
'@tailwindcss/vite':
specifier: ^4.1.16
version: 4.1.16(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
version: 4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
'@vueuse/core':
specifier: ^14.0.0
version: 14.0.0(vue@3.5.22(typescript@5.9.3))
@@ -23,12 +23,15 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
ioredis:
specifier: ^5.8.2
version: 5.8.2
lucide-vue-next:
specifier: ^0.548.0
version: 0.548.0(vue@3.5.22(typescript@5.9.3))
nuxt:
specifier: ^4.2.0
version: 4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
version: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
pinia:
specifier: ^3.0.3
version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
@@ -55,7 +58,7 @@ importers:
version: 4.6.3(vue@3.5.22(typescript@5.9.3))
vue-sonner:
specifier: ^2.0.9
version: 2.0.9(@nuxt/kit@4.2.0(magicast@0.5.0))(@nuxt/schema@4.2.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))
version: 2.0.9(@nuxt/kit@4.2.0(magicast@0.5.0))(@nuxt/schema@4.2.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))
devDependencies:
'@iconify-json/radix-icons':
specifier: ^1.2.5
@@ -66,6 +69,9 @@ importers:
'@nuxtjs/color-mode':
specifier: ^3.5.2
version: 3.5.2(magicast@0.5.0)
'@types/node':
specifier: ^24.9.2
version: 24.9.2
nuxi:
specifier: ^3.29.3
version: 3.29.3
@@ -1292,6 +1298,9 @@ packages:
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@24.9.2':
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
'@types/parse-path@7.1.0':
resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==}
deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed.
@@ -3246,6 +3255,9 @@ packages:
unctx@2.4.1:
resolution: {integrity: sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
undici@7.16.0:
resolution: {integrity: sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==}
engines: {node: '>=20.18.1'}
@@ -4037,11 +4049,11 @@ snapshots:
'@nuxt/devalue@2.0.2': {}
'@nuxt/devtools-kit@2.7.0(magicast@0.3.5)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))':
'@nuxt/devtools-kit@2.7.0(magicast@0.3.5)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@nuxt/kit': 3.20.0(magicast@0.3.5)
execa: 8.0.1
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- magicast
@@ -4056,12 +4068,12 @@ snapshots:
prompts: 2.4.2
semver: 7.7.3
'@nuxt/devtools@2.7.0(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
'@nuxt/devtools@2.7.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
dependencies:
'@nuxt/devtools-kit': 2.7.0(magicast@0.3.5)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
'@nuxt/devtools-kit': 2.7.0(magicast@0.3.5)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
'@nuxt/devtools-wizard': 2.7.0
'@nuxt/kit': 3.20.0(magicast@0.3.5)
'@vue/devtools-core': 7.7.7(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
'@vue/devtools-core': 7.7.7(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
'@vue/devtools-kit': 7.7.7
birpc: 2.6.1
consola: 3.4.2
@@ -4086,9 +4098,9 @@ snapshots:
sirv: 3.0.2
structured-clone-es: 1.0.0
tinyglobby: 0.2.15
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-plugin-inspect: 11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vite-plugin-vue-tracer: 1.0.1(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-plugin-inspect: 11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vite-plugin-vue-tracer: 1.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
which: 5.0.0
ws: 8.18.3
transitivePeerDependencies:
@@ -4174,7 +4186,7 @@ snapshots:
transitivePeerDependencies:
- magicast
'@nuxt/nitro-server@4.2.0(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3)':
'@nuxt/nitro-server@4.2.0(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3)':
dependencies:
'@nuxt/devalue': 2.0.2
'@nuxt/kit': 4.2.0(magicast@0.5.0)
@@ -4192,7 +4204,7 @@ snapshots:
klona: 2.0.6
mocked-exports: 0.1.1
nitropack: 2.12.9
nuxt: 4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
pathe: 2.0.3
pkg-types: 2.3.0
radix3: 1.1.2
@@ -4263,12 +4275,12 @@ snapshots:
transitivePeerDependencies:
- magicast
'@nuxt/vite-builder@4.2.0(lightningcss@1.30.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)':
'@nuxt/vite-builder@4.2.0(@types/node@24.9.2)(lightningcss@1.30.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)':
dependencies:
'@nuxt/kit': 4.2.0(magicast@0.5.0)
'@rollup/plugin-replace': 6.0.2(rollup@4.52.5)
'@vitejs/plugin-vue': 6.0.1(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
'@vitejs/plugin-vue-jsx': 5.1.1(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
'@vitejs/plugin-vue': 6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
'@vitejs/plugin-vue-jsx': 5.1.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
autoprefixer: 10.4.21(postcss@8.5.6)
consola: 3.4.2
cssnano: 7.1.1(postcss@8.5.6)
@@ -4283,7 +4295,7 @@ snapshots:
magic-string: 0.30.21
mlly: 1.8.0
mocked-exports: 0.1.1
nuxt: 4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
pathe: 2.0.3
pkg-types: 2.3.0
postcss: 8.5.6
@@ -4292,9 +4304,9 @@ snapshots:
std-env: 3.10.0
ufo: 1.6.1
unenv: 2.0.0-rc.23
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-node: 3.2.4(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-plugin-checker: 0.11.0(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-node: 3.2.4(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-plugin-checker: 0.11.0(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vue: 3.5.22(typescript@5.9.3)
vue-bundle-renderer: 2.2.0
transitivePeerDependencies:
@@ -4818,12 +4830,12 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.16
'@tailwindcss/oxide-win32-x64-msvc': 4.1.16
'@tailwindcss/vite@4.1.16(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))':
'@tailwindcss/vite@4.1.16(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))':
dependencies:
'@tailwindcss/node': 4.1.16
'@tailwindcss/oxide': 4.1.16
tailwindcss: 4.1.16
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
'@tanstack/virtual-core@3.13.12': {}
@@ -4839,6 +4851,10 @@ snapshots:
'@types/estree@1.0.8': {}
'@types/node@24.9.2':
dependencies:
undici-types: 7.16.0
'@types/parse-path@7.1.0':
dependencies:
parse-path: 7.1.0
@@ -4872,22 +4888,22 @@ snapshots:
- rollup
- supports-color
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
'@vitejs/plugin-vue-jsx@5.1.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5)
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
'@rolldown/pluginutils': 1.0.0-beta.45
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5)
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vue: 3.5.22(typescript@5.9.3)
transitivePeerDependencies:
- supports-color
'@vitejs/plugin-vue@6.0.1(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
'@vitejs/plugin-vue@6.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.29
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vue: 3.5.22(typescript@5.9.3)
'@volar/language-core@2.4.23':
@@ -4971,14 +4987,14 @@ snapshots:
dependencies:
'@vue/devtools-kit': 7.7.7
'@vue/devtools-core@7.7.7(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
'@vue/devtools-core@7.7.7(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))':
dependencies:
'@vue/devtools-kit': 7.7.7
'@vue/devtools-shared': 7.7.7
mitt: 3.0.1
nanoid: 5.1.6
pathe: 2.0.3
vite-hot-client: 2.1.0(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vite-hot-client: 2.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vue: 3.5.22(typescript@5.9.3)
transitivePeerDependencies:
- vite
@@ -6204,16 +6220,16 @@ snapshots:
nuxi@3.29.3: {}
nuxt@4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1):
nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1):
dependencies:
'@dxup/nuxt': 0.2.0(magicast@0.5.0)
'@nuxt/cli': 3.29.3(magicast@0.5.0)
'@nuxt/devtools': 2.7.0(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
'@nuxt/devtools': 2.7.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3))
'@nuxt/kit': 4.2.0(magicast@0.5.0)
'@nuxt/nitro-server': 4.2.0(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3)
'@nuxt/nitro-server': 4.2.0(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.3)
'@nuxt/schema': 4.2.0
'@nuxt/telemetry': 2.6.6(magicast@0.5.0)
'@nuxt/vite-builder': 4.2.0(lightningcss@1.30.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)
'@nuxt/vite-builder': 4.2.0(@types/node@24.9.2)(lightningcss@1.30.2)(magicast@0.5.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1))(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))(yaml@2.8.1)
'@unhead/vue': 2.0.19(vue@3.5.22(typescript@5.9.3))
'@vue/shared': 3.5.22
c12: 3.3.1(magicast@0.5.0)
@@ -6265,6 +6281,7 @@ snapshots:
vue-router: 4.6.3(vue@3.5.22(typescript@5.9.3))
optionalDependencies:
'@parcel/watcher': 2.5.1
'@types/node': 24.9.2
transitivePeerDependencies:
- '@azure/app-configuration'
- '@azure/cosmos'
@@ -7074,6 +7091,8 @@ snapshots:
magic-string: 0.30.21
unplugin: 2.3.10
undici-types@7.16.0: {}
undici@7.16.0: {}
unenv@2.0.0-rc.23:
@@ -7193,23 +7212,23 @@ snapshots:
util-deprecate@1.0.2: {}
vite-dev-rpc@1.1.0(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)):
vite-dev-rpc@1.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
birpc: 2.6.1
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-hot-client: 2.1.0(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-hot-client: 2.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vite-hot-client@2.1.0(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)):
vite-hot-client@2.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-node@3.2.4(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1):
vite-node@3.2.4(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -7224,7 +7243,7 @@ snapshots:
- tsx
- yaml
vite-plugin-checker@0.11.0(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)):
vite-plugin-checker@0.11.0(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
'@babel/code-frame': 7.27.1
chokidar: 4.0.3
@@ -7233,12 +7252,12 @@ snapshots:
picomatch: 4.0.3
tiny-invariant: 1.3.3
tinyglobby: 0.2.15
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vscode-uri: 3.1.0
optionalDependencies:
typescript: 5.9.3
vite-plugin-inspect@11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)):
vite-plugin-inspect@11.3.3(@nuxt/kit@3.20.0(magicast@0.3.5))(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)):
dependencies:
ansis: 4.2.0
debug: 4.4.3
@@ -7248,24 +7267,24 @@ snapshots:
perfect-debounce: 2.0.0
sirv: 3.0.2
unplugin-utils: 0.3.1
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-dev-rpc: 1.1.0(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite-dev-rpc: 1.1.0(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))
optionalDependencies:
'@nuxt/kit': 3.20.0(magicast@0.3.5)
transitivePeerDependencies:
- supports-color
vite-plugin-vue-tracer@1.0.1(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)):
vite-plugin-vue-tracer@1.0.1(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)):
dependencies:
estree-walker: 3.0.3
exsolve: 1.0.7
magic-string: 0.30.21
pathe: 2.0.3
source-map-js: 1.2.1
vite: 7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vite: 7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)
vue: 3.5.22(typescript@5.9.3)
vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1):
vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.11
fdir: 6.5.0(picomatch@4.0.3)
@@ -7274,6 +7293,7 @@ snapshots:
rollup: 4.52.5
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.9.2
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.2
@@ -7297,11 +7317,11 @@ snapshots:
'@vue/devtools-api': 6.6.4
vue: 3.5.22(typescript@5.9.3)
vue-sonner@2.0.9(@nuxt/kit@4.2.0(magicast@0.5.0))(@nuxt/schema@4.2.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)):
vue-sonner@2.0.9(@nuxt/kit@4.2.0(magicast@0.5.0))(@nuxt/schema@4.2.0)(nuxt@4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)):
optionalDependencies:
'@nuxt/kit': 4.2.0(magicast@0.5.0)
'@nuxt/schema': 4.2.0
nuxt: 4.2.0(@parcel/watcher@2.5.1)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
nuxt: 4.2.0(@parcel/watcher@2.5.1)(@types/node@24.9.2)(@vue/compiler-sfc@3.5.22)(db0@0.3.4)(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.0)(rollup@4.52.5)(terser@5.44.0)(typescript@5.9.3)(vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))(yaml@2.8.1)
vue@3.5.22(typescript@5.9.3):
dependencies:

View File

@@ -1,31 +1,62 @@
import Redis from "ioredis";
// thanks nitropack smh
type Peer = Parameters<NonNullable<Parameters<typeof defineWebSocketHandler>[0]['message']>>[0];
const rooms: Record<string, { broadcaster: Peer, viewers: Peer[] }> = {};
const client = new Redis(process.env.REDIS_URL!);
const activePeers = new Map<string, Peer>();
async function getRoom(roomId: string) {
const data = await client.get(`room:${roomId}`);
return data ? JSON.parse(data) : null;
}
async function saveRoom(roomId: string, data: { broadcaster: string, viewers: string[] }) {
await client.set(`room:${roomId}`, JSON.stringify(data));
}
async function deleteRoom(roomId: string) {
await client.del(`room:${roomId}`);
}
async function getAllRoomIds() {
return await client.keys('room:*');
}
export default defineWebSocketHandler({
message(peer, message) {
open(peer) {
activePeers.set(peer.id, peer);
console.log('[ws] peer connected', peer.id);
},
async message(peer, message) {
// TODO: proper typing
//if (message.text() === 'ping') return;
const msg = message.json() as any;
console.log("[ws] message", peer.id, msg);
if (msg.event === 'create-room') {
const roomId = generateRoomId();
rooms[roomId] = { broadcaster: peer, viewers: [] };
await saveRoom(roomId, { broadcaster: peer.id, viewers: [] });
peer.send(JSON.stringify({ event: 'room-created', roomId }));
}
if (msg.event === 'join-room') {
const room = rooms[msg.roomId];
const room = await getRoom(msg.roomId);
if (room) {
room.viewers.push(peer);
room.broadcaster.send(JSON.stringify({ event: 'viewer-joined', viewerId: peer.id }));
room.viewers.push(peer.id);
await saveRoom(msg.roomId, room);
// Notify viewer they joined
peer.send(JSON.stringify({ event: 'joined', roomId: msg.roomId }));
// Notify broadcaster
const broadcasterPeer = activePeers.get(room.broadcaster);
if (broadcasterPeer) {
broadcasterPeer.send(JSON.stringify({ event: 'viewer-joined', viewerId: peer.id }));
}
} else {
peer.send(JSON.stringify({ event: 'error', message: 'Room not found' }));
}
}
if (msg.event === 'offer') {
const viewerSocket = findSocketById(msg.targetId);
const viewerSocket = activePeers.get(msg.targetId);
if (viewerSocket) {
viewerSocket.send(JSON.stringify({
event: 'offer',
@@ -35,17 +66,17 @@ export default defineWebSocketHandler({
}
}
if (msg.event === 'answer') {
const broadcasterSocket = findSocketById(msg.targetId)
const broadcasterSocket = activePeers.get(msg.targetId);
if (broadcasterSocket) {
broadcasterSocket.send({
broadcasterSocket.send(JSON.stringify({
event: 'answer',
sdp: msg.sdp,
from: peer.id,
})
}));
}
}
if (msg.event === 'ice-candidate') {
const targetSocket = findSocketById(msg.targetId);
const targetSocket = activePeers.get(msg.targetId);
if (targetSocket) {
targetSocket.send(JSON.stringify({
event: 'ice-candidate',
@@ -56,20 +87,35 @@ export default defineWebSocketHandler({
}
},
close(peer, event) {
async close(peer, event) {
console.log("[ws] close", peer.id, event);
for (const [roomId, room] of Object.entries(rooms)) {
if (room.broadcaster.id === peer.id) {
activePeers.delete(peer.id);
const roomKeys = await getAllRoomIds();
for (const key of roomKeys) {
const roomId = key.replace('room:', '');
const room = await getRoom(roomId);
if (!room) continue;
if (room.broadcaster === peer.id) {
// broadcaster disconnected, close room
room.viewers.forEach(viewer => {
viewer.send(JSON.stringify({ event: 'room-closed' }));
room.viewers.forEach((viewerId: string) => {
const viewer = activePeers.get(viewerId);
if (viewer) {
viewer.send(JSON.stringify({ event: 'room-closed' }));
}
});
delete rooms[roomId];
await deleteRoom(roomId);
} else {
const viewerIndex = room.viewers.findIndex(v => v.id === peer.id);
const viewerIndex = room.viewers.indexOf(peer.id);
if (viewerIndex !== -1) {
room.viewers.splice(viewerIndex, 1);
room.broadcaster.send(JSON.stringify({ event: 'viewer-left', viewerId: peer.id }));
await saveRoom(roomId, room);
const broadcasterPeer = activePeers.get(room.broadcaster);
if (broadcasterPeer) {
broadcasterPeer.send(JSON.stringify({ event: 'viewer-left', viewerId: peer.id }));
}
}
}
}
@@ -79,12 +125,3 @@ export default defineWebSocketHandler({
function generateRoomId(): string {
return Math.random().toString().slice(2, 8);
}
function findSocketById(id: string): Peer | null {
for (const room of Object.values(rooms)) {
if (room.broadcaster.id === id) return room.broadcaster;
const viewer = room.viewers.find(v => v.id === id);
if (viewer) return viewer;
}
return null;
}