mirror of
https://github.com/SrIzan10/helium.git
synced 2026-06-06 00:56:58 +00:00
feat: prototype done
This commit is contained in:
@@ -120,4 +120,7 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
h1 {
|
||||
@apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,10 @@ const viewerStore = useViewerStore()
|
||||
const { code: codeRef } = storeToRefs(viewerStore)
|
||||
const { send } = useWebSocket('ws://localhost:3000/ws/signaling', {
|
||||
autoReconnect: true,
|
||||
//heartbeat: true,
|
||||
heartbeat: {
|
||||
message: JSON.stringify({ event: 'ping' }),
|
||||
interval: 15000,
|
||||
},
|
||||
onMessage: async (ws, ev) => {
|
||||
const message = JSON.parse(ev.data)
|
||||
if (message.event === 'joined') {
|
||||
@@ -18,7 +21,9 @@ const { send } = useWebSocket('ws://localhost:3000/ws/signaling', {
|
||||
const peerConnection = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
],
|
||||
iceCandidatePoolSize: 10
|
||||
});
|
||||
viewerStore.setPeerConnection(peerConnection);
|
||||
|
||||
@@ -76,13 +81,14 @@ watch(codeRef, (newCode) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
||||
<h1>helium</h1>
|
||||
<p>effortless screensharing powered by webrtc</p>
|
||||
<p>code is {{ viewerStore.code }}</p>
|
||||
<app-code-input />
|
||||
|
||||
<video ref="videofeedRef" autoplay playsinline style="width: 100%; max-width: 1200px; background: black;"></video>
|
||||
<video ref="videofeedRef" autoplay playsinline controls style="width: 100%; max-width: 1200px; background: black;"></video>
|
||||
|
||||
<NuxtLink to="/stream"><Button>host instead?</Button></NuxtLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,6 +9,10 @@ const localStream = ref<MediaStream|null>(null);
|
||||
|
||||
const { send } = useWebSocket('ws://localhost:3000/ws/signaling', {
|
||||
autoReconnect: true,
|
||||
heartbeat: {
|
||||
message: JSON.stringify({ event: 'ping' }),
|
||||
interval: 15000,
|
||||
},
|
||||
onMessage: async (ws, ev) => {
|
||||
const message = JSON.parse(ev.data)
|
||||
|
||||
@@ -18,7 +22,11 @@ const { send } = useWebSocket('ws://localhost:3000/ws/signaling', {
|
||||
|
||||
if (message.event === 'viewer-joined') {
|
||||
const peerConnection = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
{ urls: 'stun:stun1.l.google.com:19302' },
|
||||
],
|
||||
iceCandidatePoolSize: 10
|
||||
});
|
||||
streamerStore.addPeerConnection(message.viewerId, peerConnection)
|
||||
|
||||
@@ -85,9 +93,11 @@ async function startScreenShare() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center gap-6 mt-10 px-4">
|
||||
<Button @click="startScreenShare">
|
||||
screenshare
|
||||
screenshare
|
||||
</Button>
|
||||
<p>Your stream code: {{ streamerStore.code }}</p>
|
||||
<p v-if="streamerStore.code" class="font-mono">{{ streamerStore.code }}</p>
|
||||
<video ref="videofeedRef" autoplay playsinline muted></video>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default defineNuxtConfig({
|
||||
tailwindcss(),
|
||||
],
|
||||
},
|
||||
modules: ['shadcn-nuxt', '@nuxtjs/color-mode', '@pinia/nuxt'],
|
||||
modules: ['shadcn-nuxt', '@nuxtjs/color-mode', '@pinia/nuxt', 'nuxt-cron'],
|
||||
colorMode: {
|
||||
classSuffix: ''
|
||||
},
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@types/node": "^24.9.2",
|
||||
"nuxi": "^3.29.3",
|
||||
"nuxt-cron": "^1.8.0",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@@ -75,6 +75,9 @@ importers:
|
||||
nuxi:
|
||||
specifier: ^3.29.3
|
||||
version: 3.29.3
|
||||
nuxt-cron:
|
||||
specifier: ^1.8.0
|
||||
version: 1.8.0(magicast@0.5.0)
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
@@ -1298,6 +1301,9 @@ packages:
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/luxon@3.4.2':
|
||||
resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==}
|
||||
|
||||
'@types/node@24.9.2':
|
||||
resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==}
|
||||
|
||||
@@ -1703,6 +1709,9 @@ packages:
|
||||
resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
cron@3.5.0:
|
||||
resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==}
|
||||
|
||||
croner@9.1.0:
|
||||
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
|
||||
engines: {node: '>=18.0'}
|
||||
@@ -2382,6 +2391,10 @@ packages:
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
|
||||
luxon@3.5.0:
|
||||
resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
magic-regexp@0.10.0:
|
||||
resolution: {integrity: sha512-Uly1Bu4lO1hwHUW0CQeSWuRtzCMNO00CmXtS8N6fyvB3B979GOEEeAkiTUDsmbYLAbvpUS/Kt5c4ibosAzVyVg==}
|
||||
|
||||
@@ -2553,6 +2566,9 @@ packages:
|
||||
engines: {node: ^16.10.0 || >=18.0.0}
|
||||
hasBin: true
|
||||
|
||||
nuxt-cron@1.8.0:
|
||||
resolution: {integrity: sha512-XgNdounfdXNHOVlYTFDhc28yerwBP7+MP4Tk8GmC6GkxWo3nCWZ/pmqpan+tCmVVhUn6ltpe5KSCChWgl5P3kg==}
|
||||
|
||||
nuxt@4.2.0:
|
||||
resolution: {integrity: sha512-4qzf2Ymf07dMMj50TZdNZgMqCdzDch8NY3NO2ClucUaIvvsr6wd9+JrDpI1CckSTHwqU37/dIPFpvIQZoeHoYA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -4851,6 +4867,8 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/luxon@3.4.2': {}
|
||||
|
||||
'@types/node@24.9.2':
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
@@ -5343,6 +5361,11 @@ snapshots:
|
||||
crc-32: 1.2.2
|
||||
readable-stream: 4.7.0
|
||||
|
||||
cron@3.5.0:
|
||||
dependencies:
|
||||
'@types/luxon': 3.4.2
|
||||
luxon: 3.5.0
|
||||
|
||||
croner@9.1.0: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
@@ -5987,6 +6010,8 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.22(typescript@5.9.3)
|
||||
|
||||
luxon@3.5.0: {}
|
||||
|
||||
magic-regexp@0.10.0:
|
||||
dependencies:
|
||||
estree-walker: 3.0.3
|
||||
@@ -6220,6 +6245,14 @@ snapshots:
|
||||
|
||||
nuxi@3.29.3: {}
|
||||
|
||||
nuxt-cron@1.8.0(magicast@0.5.0):
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.20.0(magicast@0.5.0)
|
||||
cron: 3.5.0
|
||||
fast-glob: 3.3.3
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
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)
|
||||
|
||||
43
server/cron/clean.ts
Normal file
43
server/cron/clean.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { defineCronHandler } from '#nuxt/cron'
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV === 'development';
|
||||
|
||||
export default defineCronHandler('everySecond', async () => {
|
||||
const client = new Redis(process.env.REDIS_URL!);
|
||||
|
||||
const activePeers = await client.hgetall('peers');
|
||||
const now = Date.now();
|
||||
const inactivePeerIds: string[] = [];
|
||||
|
||||
// ident inactive peers
|
||||
for (const [peerId, timestamp] of Object.entries(activePeers)) {
|
||||
if (now - parseInt(timestamp) > 30000) {
|
||||
inactivePeerIds.push(peerId);
|
||||
}
|
||||
}
|
||||
|
||||
if (inactivePeerIds.length === 0) {
|
||||
await client.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
IS_DEV && console.log('[cron] cleaning up inactive peers:', inactivePeerIds);
|
||||
|
||||
// delete rooms where broadcaster inactive
|
||||
const roomKeys = await client.keys('room:*');
|
||||
for (const key of roomKeys) {
|
||||
const roomData = await client.get(key);
|
||||
if (!roomData) continue;
|
||||
|
||||
const room = JSON.parse(roomData);
|
||||
if (inactivePeerIds.includes(room.broadcaster)) {
|
||||
await client.del(key);
|
||||
IS_DEV && console.log(`[cron] deleted room ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// remove inactive peers
|
||||
await client.hdel('peers', ...inactivePeerIds);
|
||||
await client.quit();
|
||||
})
|
||||
@@ -24,16 +24,23 @@ async function getAllRoomIds() {
|
||||
}
|
||||
|
||||
export default defineWebSocketHandler({
|
||||
open(peer) {
|
||||
async open(peer) {
|
||||
activePeers.set(peer.id, peer);
|
||||
await client.hset('peers', peer.id, Date.now().toString());
|
||||
console.log('[ws] peer connected', peer.id);
|
||||
},
|
||||
|
||||
async message(peer, message) {
|
||||
await client.hset('peers', peer.id, Date.now().toString());
|
||||
|
||||
// TODO: proper typing
|
||||
const msg = message.json() as any;
|
||||
console.log("[ws] message", peer.id, msg);
|
||||
|
||||
if (msg.event === 'ping') {
|
||||
peer.send(JSON.stringify({ event: 'pong' }));
|
||||
return;
|
||||
}
|
||||
if (msg.event === 'create-room') {
|
||||
const roomId = generateRoomId();
|
||||
await saveRoom(roomId, { broadcaster: peer.id, viewers: [] });
|
||||
@@ -44,9 +51,7 @@ export default defineWebSocketHandler({
|
||||
if (room) {
|
||||
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 }));
|
||||
@@ -90,6 +95,7 @@ export default defineWebSocketHandler({
|
||||
async close(peer, event) {
|
||||
console.log("[ws] close", peer.id, event);
|
||||
activePeers.delete(peer.id);
|
||||
await client.hdel('peers', peer.id);
|
||||
|
||||
const roomKeys = await getAllRoomIds();
|
||||
for (const key of roomKeys) {
|
||||
|
||||
Reference in New Issue
Block a user