From 3549cf737aa6f66c98ae3cc065c829f207f347f9 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:38:10 +0100 Subject: [PATCH] feat: send and receive stuff kinda done (moving to redis) --- app/components/app/CodeInput.vue | 28 +++++++ app/components/ui/pin-input/PinInput.vue | 25 ++++++ app/components/ui/pin-input/PinInputGroup.vue | 21 +++++ .../ui/pin-input/PinInputSeparator.vue | 19 +++++ app/components/ui/pin-input/PinInputSlot.vue | 21 +++++ app/components/ui/pin-input/index.ts | 4 + app/components/ui/sonner/Sonner.vue | 19 +++++ app/components/ui/sonner/index.ts | 1 + app/pages/index.vue | 77 ++++++++++++++++++- app/pages/stream.vue | 72 +++++++++++++++++ app/state/streamer.ts | 16 ++++ app/state/viewer.ts | 12 +++ nuxt.config.ts | 2 +- package.json | 6 +- pnpm-lock.yaml | 74 ++++++++++++++++++ server/routes/ws/signaling.ts | 72 ++++++++++------- 16 files changed, 440 insertions(+), 29 deletions(-) create mode 100644 app/components/app/CodeInput.vue create mode 100644 app/components/ui/pin-input/PinInput.vue create mode 100644 app/components/ui/pin-input/PinInputGroup.vue create mode 100644 app/components/ui/pin-input/PinInputSeparator.vue create mode 100644 app/components/ui/pin-input/PinInputSlot.vue create mode 100644 app/components/ui/pin-input/index.ts create mode 100644 app/components/ui/sonner/Sonner.vue create mode 100644 app/components/ui/sonner/index.ts create mode 100644 app/pages/stream.vue create mode 100644 app/state/streamer.ts create mode 100644 app/state/viewer.ts diff --git a/app/components/app/CodeInput.vue b/app/components/app/CodeInput.vue new file mode 100644 index 0000000..20173d1 --- /dev/null +++ b/app/components/app/CodeInput.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/app/components/ui/pin-input/PinInput.vue b/app/components/ui/pin-input/PinInput.vue new file mode 100644 index 0000000..95fd4d9 --- /dev/null +++ b/app/components/ui/pin-input/PinInput.vue @@ -0,0 +1,25 @@ + + + diff --git a/app/components/ui/pin-input/PinInputGroup.vue b/app/components/ui/pin-input/PinInputGroup.vue new file mode 100644 index 0000000..f5aff44 --- /dev/null +++ b/app/components/ui/pin-input/PinInputGroup.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/components/ui/pin-input/PinInputSeparator.vue b/app/components/ui/pin-input/PinInputSeparator.vue new file mode 100644 index 0000000..1d46418 --- /dev/null +++ b/app/components/ui/pin-input/PinInputSeparator.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/components/ui/pin-input/PinInputSlot.vue b/app/components/ui/pin-input/PinInputSlot.vue new file mode 100644 index 0000000..c9a3c95 --- /dev/null +++ b/app/components/ui/pin-input/PinInputSlot.vue @@ -0,0 +1,21 @@ + + + diff --git a/app/components/ui/pin-input/index.ts b/app/components/ui/pin-input/index.ts new file mode 100644 index 0000000..74faa6c --- /dev/null +++ b/app/components/ui/pin-input/index.ts @@ -0,0 +1,4 @@ +export { default as PinInput } from "./PinInput.vue" +export { default as PinInputGroup } from "./PinInputGroup.vue" +export { default as PinInputSeparator } from "./PinInputSeparator.vue" +export { default as PinInputSlot } from "./PinInputSlot.vue" diff --git a/app/components/ui/sonner/Sonner.vue b/app/components/ui/sonner/Sonner.vue new file mode 100644 index 0000000..1c70dca --- /dev/null +++ b/app/components/ui/sonner/Sonner.vue @@ -0,0 +1,19 @@ + + + diff --git a/app/components/ui/sonner/index.ts b/app/components/ui/sonner/index.ts new file mode 100644 index 0000000..6673112 --- /dev/null +++ b/app/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./Sonner.vue" diff --git a/app/pages/index.vue b/app/pages/index.vue index 53e890d..bf0ec1c 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,3 +1,78 @@ + + + \ No newline at end of file diff --git a/app/pages/stream.vue b/app/pages/stream.vue new file mode 100644 index 0000000..683e968 --- /dev/null +++ b/app/pages/stream.vue @@ -0,0 +1,72 @@ + + + \ No newline at end of file diff --git a/app/state/streamer.ts b/app/state/streamer.ts new file mode 100644 index 0000000..794abbf --- /dev/null +++ b/app/state/streamer.ts @@ -0,0 +1,16 @@ +import { defineStore} from 'pinia'; + +export const useStreamerStore = defineStore('streamer', { + state: () => ({ + code: '', + peerConnections: {} as Record, + }), + actions: { + setCode(code: string) { + this.code = code; + }, + addPeerConnection(id: string, pc: RTCPeerConnection) { + this.peerConnections[id] = pc; + }, + }, +}); \ No newline at end of file diff --git a/app/state/viewer.ts b/app/state/viewer.ts new file mode 100644 index 0000000..5be0122 --- /dev/null +++ b/app/state/viewer.ts @@ -0,0 +1,12 @@ +import { defineStore} from 'pinia'; + +export const useViewerStore = defineStore('viewer', { + state: () => ({ + code: '', + }), + actions: { + setCode(code: string) { + this.code = code; + }, + }, +}); \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index 846d7d9..3771dcb 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -10,7 +10,7 @@ export default defineNuxtConfig({ tailwindcss(), ], }, - modules: ['shadcn-nuxt', '@nuxtjs/color-mode'], + modules: ['shadcn-nuxt', '@nuxtjs/color-mode', '@pinia/nuxt'], colorMode: { classSuffix: '' }, diff --git a/package.json b/package.json index ef1535c..7340c61 100644 --- a/package.json +++ b/package.json @@ -11,24 +11,28 @@ "ui:add": "pnpm dlx shadcn-vue@latest add" }, "dependencies": { + "@pinia/nuxt": "0.11.2", "@tailwindcss/vite": "^4.1.16", "@vueuse/core": "^14.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-vue-next": "^0.548.0", "nuxt": "^4.2.0", + "pinia": "^3.0.3", "reka-ui": "^2.6.0", "shadcn-nuxt": "2.3.2", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.16", "tw-animate-css": "^1.4.0", "vue": "^3.5.22", - "vue-router": "^4.6.3" + "vue-router": "^4.6.3", + "vue-sonner": "^2.0.9" }, "devDependencies": { "@iconify-json/radix-icons": "^1.2.5", "@iconify/vue": "^5.0.0", "@nuxtjs/color-mode": "^3.5.2", + "nuxi": "^3.29.3", "typescript": "^5.9.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5938bac..7573bc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@pinia/nuxt': + specifier: 0.11.2 + 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)) @@ -26,6 +29,9 @@ importers: 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) + pinia: + specifier: ^3.0.3 + version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) reka-ui: specifier: ^2.6.0 version: 2.6.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) @@ -47,6 +53,9 @@ importers: vue-router: specifier: ^4.6.3 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)) devDependencies: '@iconify-json/radix-icons': specifier: ^1.2.5 @@ -57,6 +66,9 @@ importers: '@nuxtjs/color-mode': specifier: ^3.5.2 version: 3.5.2(magicast@0.5.0) + nuxi: + specifier: ^3.29.3 + version: 3.29.3 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -950,6 +962,11 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@pinia/nuxt@0.11.2': + resolution: {integrity: sha512-CgvSWpbktxxWBV7ModhAcsExsQZqpPq6vMYEe9DexmmY6959ev8ukL4iFhr/qov2Nb9cQAWd7niFDnaWkN+FHg==} + peerDependencies: + pinia: ^3.0.3 + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1355,6 +1372,9 @@ packages: '@vue/devtools-api@6.6.4': resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + '@vue/devtools-core@7.7.7': resolution: {integrity: sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==} peerDependencies: @@ -2519,6 +2539,11 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nuxi@3.29.3: + resolution: {integrity: sha512-+/IHFCXT2t1XO5dQdpVyV/eU12DrVOCkKKwrDv9YwHIL4pXMyTDxIGMfU995YWjLjRb7SUsfNLfGV4wWCFdg0A==} + engines: {node: ^16.10.0 || >=18.0.0} + hasBin: true + nuxt@4.2.0: resolution: {integrity: sha512-4qzf2Ymf07dMMj50TZdNZgMqCdzDch8NY3NO2ClucUaIvvsr6wd9+JrDpI1CckSTHwqU37/dIPFpvIQZoeHoYA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2650,6 +2675,15 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pinia@3.0.3: + resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -3469,6 +3503,20 @@ packages: peerDependencies: vue: ^3.5.0 + vue-sonner@2.0.9: + resolution: {integrity: sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==} + peerDependencies: + '@nuxt/kit': ^4.0.3 + '@nuxt/schema': ^4.0.3 + nuxt: ^4.0.3 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@nuxt/schema': + optional: true + nuxt: + optional: true + vue@3.5.22: resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==} peerDependencies: @@ -4540,6 +4588,13 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@pinia/nuxt@0.11.2(magicast@0.5.0)(pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)))': + dependencies: + '@nuxt/kit': 3.20.0(magicast@0.5.0) + pinia: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)) + transitivePeerDependencies: + - magicast + '@pkgjs/parseargs@0.11.0': optional: true @@ -4912,6 +4967,10 @@ snapshots: '@vue/devtools-api@6.6.4': {} + '@vue/devtools-api@7.7.7': + 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))': dependencies: '@vue/devtools-kit': 7.7.7 @@ -6143,6 +6202,8 @@ snapshots: dependencies: boolbase: 1.0.0 + 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): dependencies: '@dxup/nuxt': 0.2.0(magicast@0.5.0) @@ -6428,6 +6489,13 @@ snapshots: picomatch@4.0.3: {} + pinia@3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.7 + vue: 3.5.22(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -7229,6 +7297,12 @@ 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)): + 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) + vue@3.5.22(typescript@5.9.3): dependencies: '@vue/compiler-dom': 3.5.22 diff --git a/server/routes/ws/signaling.ts b/server/routes/ws/signaling.ts index 68b3ad8..0918a0c 100644 --- a/server/routes/ws/signaling.ts +++ b/server/routes/ws/signaling.ts @@ -1,30 +1,49 @@ // thanks nitropack smh type Peer = Parameters[0]['message']>>[0]; -const rooms: Record = {}; +// Store peer instances in memory (these can't be serialized to storage) +const activePeers = new Map(); + +async function getRooms() { + return await useStorage('rooms').getItem>('data') || {}; +} + +async function saveRooms(rooms: Record) { + await useStorage('rooms').setItem('data', rooms); +} export default defineWebSocketHandler({ - message(peer, message) { + async open(peer) { + activePeers.set(peer.id, peer); + }, + + async message(peer, message) { // TODO: proper typing const msg = message.json() as any; console.log("[ws] message", peer.id, msg); + const rooms = await getRooms(); if (msg.event === 'create-room') { const roomId = generateRoomId(); - rooms[roomId] = { broadcaster: peer, viewers: [] }; + rooms[roomId] = { broadcaster: peer.id, viewers: [] }; + await saveRooms(rooms); peer.send(JSON.stringify({ event: 'room-created', roomId })); } if (msg.event === 'join-room') { const room = rooms[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 saveRooms(rooms); + 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', @@ -34,17 +53,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', @@ -55,35 +74,36 @@ export default defineWebSocketHandler({ } }, - close(peer, event) { + async close(peer, event) { console.log("[ws] close", peer.id, event); + activePeers.delete(peer.id); + const rooms = await getRooms(); + for (const [roomId, room] of Object.entries(rooms)) { - if (room.broadcaster.id === peer.id) { + if (room.broadcaster === peer.id) { // broadcaster disconnected, close room - room.viewers.forEach(viewer => { - viewer.send(JSON.stringify({ event: 'room-closed' })); + room.viewers.forEach(viewerId => { + const viewer = activePeers.get(viewerId); + if (viewer) { + viewer.send(JSON.stringify({ event: 'room-closed' })); + } }); delete rooms[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 })); + const broadcasterPeer = activePeers.get(room.broadcaster); + if (broadcasterPeer) { + broadcasterPeer.send(JSON.stringify({ event: 'viewer-left', viewerId: peer.id })); + } } } } + await saveRooms(rooms); }, }); function generateRoomId(): string { - return Math.random().toString(36).substring(2, 8).toUpperCase(); -} - -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; + return Math.random().toString().slice(2, 8); }