Fix VideoTile bug and improve screen sharing

This commit is contained in:
srtk 2026-02-08 23:26:40 +05:30
parent d489873060
commit cade45d16d
7 changed files with 124 additions and 20 deletions

70
README.md Normal file
View file

@ -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 <your-client-repo-url>
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.

View file

@ -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": {

View file

@ -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;

View file

@ -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();

View file

@ -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<HTMLVideoElement>(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]"
/>
</div>

View file

@ -138,7 +138,7 @@ export function Stage({
>
{/* Self Webcam */}
{selfId && (
<div className="aspect-video min-h-0">
<div className="aspect-video min-h-0 min-w-0">
<VideoTile
displayName={displayName}
isSelf
@ -151,7 +151,7 @@ export function Stage({
{/* Remote Webcam Peers */}
{allParticipants.map(peer => (
<div key={peer.user_id} className="aspect-video min-h-0">
<div key={peer.user_id} className="aspect-video min-h-0 min-w-0">
<VideoTile
displayName={peer.display_name}
audioEnabled={true}

View file

@ -34,7 +34,7 @@ export function VideoTile({
autoPlay
playsInline
muted={true}
className={`w-full h-full object-cover ${!isScreenShare ? 'scale-x-[-1]' : ''}`}
className={`w-full h-full object-contain bg-black ${!isScreenShare ? 'scale-x-[-1]' : ''}`}
/>
)}
@ -43,7 +43,7 @@ export function VideoTile({
<img
src={videoSrc}
alt={displayName}
className={`w-full h-full ${isScreenShare ? 'object-contain bg-black' : 'object-cover'}`}
className={`w-full h-full object-contain bg-black`}
/>
)}