From cade45d16de5d968053b194a78cb6f2b8d16d64e Mon Sep 17 00:00:00 2001 From: srtk Date: Sun, 8 Feb 2026 23:26:40 +0530 Subject: [PATCH] Fix VideoTile bug and improve screen sharing --- README.md | 70 +++++++++++++++++++++++ package.json | 3 + src/main/network.ts | 38 ++++++++++-- src/renderer/src/App.tsx | 21 ++++--- src/renderer/src/components/Lobby.tsx | 4 +- src/renderer/src/components/Stage.tsx | 4 +- src/renderer/src/components/VideoTile.tsx | 4 +- 7 files changed, 124 insertions(+), 20 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3e768d --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +# Meet Client (Electron) + +The desktop client for the Meet application. Built with **Electron**, **React**, **TypeScript**, and **Vite**. + +## Features +- **High Quality Video**: 1080p @ 30 FPS video calling. +- **Screen Sharing**: Low-latency 1080p screen sharing. +- **Chat**: Real-time text chat. +- **Protocol**: Custom UDP-based media transport for minimal latency. + +## Prerequisites +- **Node.js**: v18.0.0 or higher. +- **NPM**: v9.0.0 or higher. + +## Installation + +1. **Clone the Repository**: + ```bash + git clone + cd client-electron + ``` + +2. **Install Dependencies**: + ```bash + npm install + ``` + +## Development + +Start the app in development mode with Hot Module Replacement (HMR): +```bash +npm run dev +``` + +## Building for Production + +To create a distributable installer/executable for your OS: + +### Linux +```bash +npm run build:linux +``` +Output: `dist/*.AppImage` (or configured target) + +### Windows +```bash +npm run build:win +``` +Output: `dist/*.exe` + +### macOS +```bash +npm run build:mac +``` +Output: `dist/*.dmg` + +## Configuration + +### Connection Settings +- **Server URL**: When launching the app, enter your server's domain or IP (e.g., `meet.srtk.in` or `192.168.1.8`). +- **UDP Port**: The client hardcodes the target UDP port to **4000**. Ensure your server's firewall allows inbound UDP on port 4000. + +### Troubleshooting +- **Black Video**: Ensure "NAT Loopback" is enabled if testing locally with a public domain, or use the local IP. +- **Connection Drops**: Check your router's UDP flood protection / firewall settings if video freezes after a few seconds. + +## Project Structure +- `src/main/`: Electron Main process (Window creation, UDP socket handling). +- `src/renderer/`: React frontend (UI, Video rendering). +- `src/preload/`: IPC bridge between Main and Renderer. diff --git a/package.json b/package.json index 4a55888..c096f42 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "scripts": { "dev": "electron-vite dev", "build": "electron-vite build", + "build:linux": "electron-vite build && electron-builder --linux", + "build:win": "electron-vite build && electron-builder --win", + "build:mac": "electron-vite build && electron-builder --mac", "preview": "electron-vite preview" }, "dependencies": { diff --git a/src/main/network.ts b/src/main/network.ts index ae1c8fc..1a23e7a 100644 --- a/src/main/network.ts +++ b/src/main/network.ts @@ -35,11 +35,14 @@ export class NetworkManager extends EventEmitter { this.roomCode = roomCode; // Store for UDP handshake return new Promise((resolve, reject) => { // Determine Host and Protocol - let host = serverUrl.replace(/^wss?:\/\//, '').replace(/\/$/, ''); + let host = serverUrl.trim().replace(/^wss?:\/\//, '').replace(/\/$/, ''); this.serverUdpHost = host.split(':')[0]; // Hostname only for UDP (strip port if present) // Auto-detect protocol: localhost/IP uses ws://, domains use wss:// (HTTPS) - const isLocal = host.includes('localhost') || host.includes('127.0.0.1'); + const isLocal = host.includes('localhost') || + host.includes('127.0.0.1') || + host.startsWith('192.168.') || + host.startsWith('10.'); const protocol = isLocal ? 'ws' : 'wss'; const wsUrl = `${protocol}://${host}/ws`; @@ -141,16 +144,19 @@ export class NetworkManager extends EventEmitter { this.ws.send(JSON.stringify(msg)); } + private heartbeatInterval: NodeJS.Timeout | null = null; + setupUdp() { this.udp = dgram.createSocket('udp4'); this.udp.on('listening', () => { const addr = this.udp?.address(); console.log(`UDP Listening on ${addr?.port}`); - this.sendHandshake(); + this.startHeartbeat(); }); - this.udp.on('message', (msg) => { + this.udp.on('message', (msg, rinfo) => { + console.log(`[UDP] Msg from ${rinfo.address}:${rinfo.port} - ${msg.length} bytes`); this.handleUdpMessage(msg); }); @@ -286,9 +292,25 @@ export class NetworkManager extends EventEmitter { } } + startHeartbeat() { + if (this.heartbeatInterval) clearInterval(this.heartbeatInterval); + + // Send immediately + this.sendHandshake(); + + // Send 3 bursts to ensure traversal + setTimeout(() => this.sendHandshake(), 500); + setTimeout(() => this.sendHandshake(), 1000); + + // Keep-alive every 3 seconds + this.heartbeatInterval = setInterval(() => { + this.sendHandshake(); + }, 3000); + } + sendHandshake() { if (!this.udp || !this.userId || !this.roomCode) { - console.error('[UDP] Cannot send handshake: missing udp, userId, or roomCode'); + // console.error('[UDP] Cannot send handshake: missing udp, userId, or roomCode'); return; } @@ -311,13 +333,17 @@ export class NetworkManager extends EventEmitter { const packet = Buffer.concat([header, payload]); - console.log(`[UDP] Sending Handshake: userId=${this.userId}, room=${this.roomCode}, ${packet.length} bytes`); + console.log(`[UDP] Sending Handshake: userId=${this.userId}, room=${this.roomCode}, ${packet.length} bytes to ${this.serverUdpHost}:${SERVER_UDP_PORT}`); this.udp.send(packet, SERVER_UDP_PORT, this.serverUdpHost, (err) => { if (err) console.error('UDP Handshake Send Error', err); }); } disconnect() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } if (this.ws) this.ws.close(); if (this.udp) this.udp.close(); this.ws = null; diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index ee392fd..a82f5a0 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -113,7 +113,8 @@ function App() { const removeStreamUpdate = window.electron.ipcRenderer.on("peer-stream-update", (_, data) => { const { user_id, active, media_type } = data; if (!active) { - if (media_type === 'Video') { + // Support both legacy string and numeric enum (1=Video, 2=Screen) + if (media_type === 'Video' || media_type === 1) { setPeerVideoUrls(prev => { const newState = { ...prev }; if (newState[user_id]) { @@ -122,7 +123,7 @@ function App() { } return newState; }); - } else if (media_type === 'Screen') { + } else if (media_type === 'Screen' || media_type === 2) { setPeerScreenUrls(prev => { const newState = { ...prev }; if (newState[user_id]) { @@ -369,8 +370,12 @@ function App() { const ctx = canvas.getContext('2d'); if (ctx && video.readyState === 4) { - canvas.width = 320; // Low res for MVP - canvas.height = 240; + // Use native video size, capped at 1080p for bandwidth sanity + const width = Math.min(video.videoWidth, 1920); + const height = Math.min(video.videoHeight, 1080); + + canvas.width = width; + canvas.height = height; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); canvas.toBlob(async (blob) => { @@ -384,12 +389,12 @@ function App() { // Ignore send errors } } - }, 'image/jpeg', 0.5); + }, 'image/jpeg', 0.4); // Quality 0.4 (Good balance for 1080p UDP) } } setTimeout(() => { if (isActive) animationFrameId = requestAnimationFrame(sendFrame); - }, 66); // ~15 FPS for lower latency + }, 33); // 30 FPS }; if (videoEnabled) { @@ -506,12 +511,12 @@ function App() { window.electron.ipcRenderer.send('send-screen-frame', { frame: Array.from(new Uint8Array(buf)) }); }); } - }, 'image/jpeg', 0.8); + }, 'image/jpeg', 0.4); // Quality 0.4 for speed } setTimeout(() => { if (isActive) requestAnimationFrame(sendScreenFrame); - }, 16); // ~60 FPS for screen + }, 33); // 30 FPS for Screen Share }; screenVideo.onloadeddata = () => sendScreenFrame(); diff --git a/src/renderer/src/components/Lobby.tsx b/src/renderer/src/components/Lobby.tsx index 9637e94..a272a9f 100644 --- a/src/renderer/src/components/Lobby.tsx +++ b/src/renderer/src/components/Lobby.tsx @@ -10,7 +10,7 @@ interface LobbyProps { export function Lobby({ onJoin, initialName = '', initialRoom = '' }: LobbyProps) { const [roomCode, setRoomCode] = useState(initialRoom); const [displayName, setDisplayName] = useState(initialName); - const [serverUrl, setServerUrl] = useState('meet.srtk.in'); + const [serverUrl, setServerUrl] = useState(''); const [videoEnabled, setVideoEnabled] = useState(false); const [audioEnabled, setAudioEnabled] = useState(false); const videoRef = useRef(null); @@ -99,7 +99,7 @@ export function Lobby({ onJoin, initialName = '', initialRoom = '' }: LobbyProps type="text" value={serverUrl} onChange={e => setServerUrl(e.target.value)} - placeholder="meet.srtk.in" + placeholder="Enter your server URL" className="bg-[#202124] border border-[#5f6368] rounded-md px-4 py-3 text-white placeholder-gray-500 focus:outline-none focus:border-[#8ab4f8]" /> diff --git a/src/renderer/src/components/Stage.tsx b/src/renderer/src/components/Stage.tsx index b4ca473..869b764 100644 --- a/src/renderer/src/components/Stage.tsx +++ b/src/renderer/src/components/Stage.tsx @@ -138,7 +138,7 @@ export function Stage({ > {/* Self Webcam */} {selfId && ( -
+
( -
+
)} @@ -43,7 +43,7 @@ export function VideoTile({ {displayName} )}