feat: Add dynamic server URL and deployment guide
This commit is contained in:
parent
2a9e80cc5a
commit
d489873060
8 changed files with 335 additions and 95 deletions
|
|
@ -58,9 +58,9 @@ app.whenReady().then(() => {
|
|||
})
|
||||
|
||||
// IPC Handlers
|
||||
ipcMain.handle('connect', async (_, { roomCode, displayName }) => {
|
||||
ipcMain.handle('connect', async (_, { serverUrl, roomCode, displayName }) => {
|
||||
if (networkManager) {
|
||||
return await networkManager.connect(roomCode, displayName);
|
||||
return await networkManager.connect(serverUrl, roomCode, displayName);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -108,6 +108,13 @@ app.whenReady().then(() => {
|
|||
}
|
||||
});
|
||||
|
||||
// Stream Updates
|
||||
ipcMain.on('update-stream', (_, { active, mediaType }) => {
|
||||
if (networkManager) {
|
||||
networkManager.updateStream(active, mediaType);
|
||||
}
|
||||
});
|
||||
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,10 @@ import * as dgram from 'dgram';
|
|||
import WebSocket from 'ws';
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
// Constants - Configure SERVER_HOST for production
|
||||
const SERVER_HOST = process.env.MEET_SERVER_HOST || '127.0.0.1';
|
||||
const SERVER_WS_URL = `ws://${SERVER_HOST}:5000/ws`;
|
||||
const SERVER_UDP_HOST = SERVER_HOST;
|
||||
// Constants
|
||||
const SERVER_UDP_PORT = 4000;
|
||||
|
||||
// Packet Header Structure (22 bytes)
|
||||
// version: u8, media_type: u8, user_id: u32, sequence: u32, timestamp: u64, frag_idx: u8, frag_cnt: u8, flags: u16
|
||||
const HEADER_SIZE = 22;
|
||||
|
||||
export enum MediaType {
|
||||
|
|
@ -26,21 +22,33 @@ export class NetworkManager extends EventEmitter {
|
|||
private roomCode: string = '';
|
||||
private videoSeq: number = 0;
|
||||
private audioSeq: number = 0;
|
||||
private screenSeq = 0;
|
||||
private mainWindow: BrowserWindow;
|
||||
private serverUdpHost: string = '127.0.0.1';
|
||||
|
||||
constructor(mainWindow: BrowserWindow) {
|
||||
super();
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
async connect(roomCode: string, displayName: string): Promise<any> {
|
||||
async connect(serverUrl: string, roomCode: string, displayName: string): Promise<any> {
|
||||
this.roomCode = roomCode; // Store for UDP handshake
|
||||
return new Promise((resolve, reject) => {
|
||||
this.ws = new WebSocket(`${SERVER_WS_URL}?room=${roomCode}&name=${displayName}`);
|
||||
// Determine Host and Protocol
|
||||
let host = serverUrl.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 protocol = isLocal ? 'ws' : 'wss';
|
||||
const wsUrl = `${protocol}://${host}/ws`;
|
||||
|
||||
console.log(`[Network] Connecting to WS: ${wsUrl}, UDP Host: ${this.serverUdpHost}`);
|
||||
|
||||
this.ws = new WebSocket(`${wsUrl}?room=${roomCode}&name=${displayName}`);
|
||||
|
||||
this.ws.on('open', () => {
|
||||
console.log('WS Connected');
|
||||
// Send Join Message (serde adjacent tagging: type + data)
|
||||
const joinMsg = {
|
||||
type: 'Join',
|
||||
data: {
|
||||
|
|
@ -92,6 +100,9 @@ export class NetworkManager extends EventEmitter {
|
|||
case 'ChatMessage':
|
||||
this.safeSend('chat-message', msg.data);
|
||||
break;
|
||||
case 'UpdateStream':
|
||||
this.safeSend('peer-stream-update', msg.data);
|
||||
break;
|
||||
case 'Error':
|
||||
console.error('WS Error Msg:', msg.data);
|
||||
reject(msg.data);
|
||||
|
|
@ -113,13 +124,29 @@ export class NetworkManager extends EventEmitter {
|
|||
this.ws.send(JSON.stringify(chatMsg));
|
||||
}
|
||||
|
||||
updateStream(active: boolean, mediaType: MediaType) {
|
||||
if (!this.ws) return;
|
||||
const mediaTypeStr = mediaType === MediaType.Audio ? 'Audio'
|
||||
: mediaType === MediaType.Video ? 'Video'
|
||||
: 'Screen';
|
||||
const msg = {
|
||||
type: 'UpdateStream',
|
||||
data: {
|
||||
user_id: this.userId,
|
||||
stream_id: 0,
|
||||
active,
|
||||
media_type: mediaTypeStr
|
||||
}
|
||||
};
|
||||
this.ws.send(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
setupUdp() {
|
||||
this.udp = dgram.createSocket('udp4');
|
||||
|
||||
this.udp.on('listening', () => {
|
||||
const addr = this.udp?.address();
|
||||
console.log(`UDP Listening on ${addr?.port}`);
|
||||
// Send UDP handshake so server can associate our UDP address with room/user
|
||||
this.sendHandshake();
|
||||
});
|
||||
|
||||
|
|
@ -133,15 +160,8 @@ export class NetworkManager extends EventEmitter {
|
|||
handleUdpMessage(msg: Buffer) {
|
||||
if (msg.length < HEADER_SIZE) return;
|
||||
|
||||
// Parse Header (Little Endian)
|
||||
// const version = msg.readUInt8(0); // 1
|
||||
const mediaType = msg.readUInt8(1);
|
||||
const userId = msg.readUInt32LE(2);
|
||||
// const seq = msg.readUInt32LE(6);
|
||||
// const ts = msg.readBigUInt64LE(10);
|
||||
// const fidx = msg.readUInt8(18);
|
||||
// const fcnt = msg.readUInt8(19);
|
||||
// const flags = msg.readUInt16LE(20);
|
||||
|
||||
const payload = msg.subarray(HEADER_SIZE);
|
||||
const sequence = msg.readUInt32LE(6);
|
||||
|
|
@ -150,13 +170,6 @@ export class NetworkManager extends EventEmitter {
|
|||
const fragCnt = msg.readUInt8(19);
|
||||
|
||||
if (mediaType === MediaType.Audio) {
|
||||
// Forward audio? Or decode here?
|
||||
// Electron renderer can use Web Audio API?
|
||||
// Or we decode in Main and send PCM?
|
||||
// Better to send encoded Opus to Renderer and decode there with WASM (libopus)
|
||||
// OR decode here with `opus-native` binding.
|
||||
// For now, let's send payload to renderer via IPC.
|
||||
// Note: IPC with high frequency audio packets might be laggy.
|
||||
this.safeSend('audio-frame', { user_id: userId, data: payload });
|
||||
} else if (mediaType === MediaType.Video) {
|
||||
this.safeSend('video-frame', {
|
||||
|
|
@ -203,7 +216,6 @@ export class NetworkManager extends EventEmitter {
|
|||
const end = Math.min(start + MAX_PAYLOAD, buffer.length);
|
||||
const chunk = buffer.subarray(start, end);
|
||||
|
||||
// MediaType.Video = 1
|
||||
const header = Buffer.alloc(HEADER_SIZE);
|
||||
header.writeUInt8(1, 0); // Version
|
||||
header.writeUInt8(MediaType.Video, 1);
|
||||
|
|
@ -216,7 +228,7 @@ export class NetworkManager extends EventEmitter {
|
|||
|
||||
const packet = Buffer.concat([header, chunk]);
|
||||
|
||||
this.udp.send(packet, SERVER_UDP_PORT, SERVER_UDP_HOST, (err) => {
|
||||
this.udp.send(packet, SERVER_UDP_PORT, this.serverUdpHost, (err) => {
|
||||
if (err) console.error('UDP Video Send Error', err);
|
||||
});
|
||||
}
|
||||
|
|
@ -225,7 +237,6 @@ export class NetworkManager extends EventEmitter {
|
|||
sendAudioFrame(frame: Uint8Array) {
|
||||
if (!this.udp) return;
|
||||
|
||||
// Construct Header (same format as video)
|
||||
const header = Buffer.alloc(HEADER_SIZE);
|
||||
header.writeUInt8(1, 0); // Version
|
||||
header.writeUInt8(MediaType.Audio, 1);
|
||||
|
|
@ -238,18 +249,16 @@ export class NetworkManager extends EventEmitter {
|
|||
|
||||
const packet = Buffer.concat([header, Buffer.from(frame)]);
|
||||
|
||||
this.udp.send(packet, SERVER_UDP_PORT, SERVER_UDP_HOST, (err) => {
|
||||
this.udp.send(packet, SERVER_UDP_PORT, this.serverUdpHost, (err) => {
|
||||
if (err) console.error('UDP Audio Send Error', err);
|
||||
});
|
||||
}
|
||||
|
||||
private screenSeq = 0;
|
||||
|
||||
sendScreenFrame(frame: number[]) {
|
||||
if (!this.udp || !this.userId) return;
|
||||
|
||||
const buffer = Buffer.from(frame);
|
||||
const MAX_PAYLOAD = 1400; // MTU friendly (~1500 total with headers)
|
||||
const MAX_PAYLOAD = 1400;
|
||||
const fragCount = Math.ceil(buffer.length / MAX_PAYLOAD);
|
||||
const seq = this.screenSeq++;
|
||||
const ts = BigInt(Date.now());
|
||||
|
|
@ -259,7 +268,6 @@ export class NetworkManager extends EventEmitter {
|
|||
const end = Math.min(start + MAX_PAYLOAD, buffer.length);
|
||||
const chunk = buffer.subarray(start, end);
|
||||
|
||||
// MediaType.Screen = 2
|
||||
const header = Buffer.alloc(HEADER_SIZE);
|
||||
header.writeUInt8(1, 0); // Version
|
||||
header.writeUInt8(MediaType.Screen, 1);
|
||||
|
|
@ -272,7 +280,7 @@ export class NetworkManager extends EventEmitter {
|
|||
|
||||
const packet = Buffer.concat([header, chunk]);
|
||||
|
||||
this.udp.send(packet, SERVER_UDP_PORT, SERVER_UDP_HOST, (err) => {
|
||||
this.udp.send(packet, SERVER_UDP_PORT, this.serverUdpHost, (err) => {
|
||||
if (err) console.error('UDP Screen Send Error', err);
|
||||
});
|
||||
}
|
||||
|
|
@ -284,18 +292,13 @@ export class NetworkManager extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
// Server expects bincode-serialized Handshake { user_id: u32, room_code: String }
|
||||
// bincode format for String: u64 length prefix (LE) + UTF-8 bytes
|
||||
const roomCodeBytes = Buffer.from(this.roomCode, 'utf-8');
|
||||
|
||||
// Payload: | user_id (4 bytes LE) | room_code_len (8 bytes LE) | room_code (N bytes) |
|
||||
const payloadLen = 4 + 8 + roomCodeBytes.length;
|
||||
const payload = Buffer.alloc(payloadLen);
|
||||
payload.writeUInt32LE(this.userId, 0); // user_id
|
||||
payload.writeBigUInt64LE(BigInt(roomCodeBytes.length), 4); // string length
|
||||
roomCodeBytes.copy(payload, 12); // room_code
|
||||
|
||||
// Construct header with MediaType.Command (3)
|
||||
const header = Buffer.alloc(HEADER_SIZE);
|
||||
header.writeUInt8(1, 0); // Version
|
||||
header.writeUInt8(3, 1); // MediaType.Command = 3
|
||||
|
|
@ -309,7 +312,7 @@ 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`);
|
||||
this.udp.send(packet, SERVER_UDP_PORT, SERVER_UDP_HOST, (err) => {
|
||||
this.udp.send(packet, SERVER_UDP_PORT, this.serverUdpHost, (err) => {
|
||||
if (err) console.error('UDP Handshake Send Error', err);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue