Implement 1080p 60fps capture and WebSocket backpressure
This commit is contained in:
parent
b3f5902c44
commit
0167aeb7b9
3 changed files with 24 additions and 16 deletions
|
|
@ -17,7 +17,6 @@ export enum MediaType {
|
||||||
export class NetworkManager extends EventEmitter {
|
export class NetworkManager extends EventEmitter {
|
||||||
private ws: WebSocket | null = null;
|
private ws: WebSocket | null = null;
|
||||||
private userId: number = 0;
|
private userId: number = 0;
|
||||||
private roomCode: string = '';
|
|
||||||
private videoSeq: number = 0;
|
private videoSeq: number = 0;
|
||||||
private audioSeq: number = 0;
|
private audioSeq: number = 0;
|
||||||
private screenSeq = 0;
|
private screenSeq = 0;
|
||||||
|
|
@ -25,6 +24,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
private pingInterval: NodeJS.Timeout | null = null;
|
private pingInterval: NodeJS.Timeout | null = null;
|
||||||
private lastBinaryLog: number = 0;
|
private lastBinaryLog: number = 0;
|
||||||
private binaryCount: number = 0;
|
private binaryCount: number = 0;
|
||||||
|
private dropCount: number = 0;
|
||||||
|
|
||||||
constructor(mainWindow: BrowserWindow) {
|
constructor(mainWindow: BrowserWindow) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -32,7 +32,6 @@ export class NetworkManager extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect(serverUrl: string, roomCode: string, displayName: string): Promise<any> {
|
async connect(serverUrl: string, roomCode: string, displayName: string): Promise<any> {
|
||||||
this.roomCode = roomCode;
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Determine Host and Protocol
|
// Determine Host and Protocol
|
||||||
let host = serverUrl.trim().replace(/^wss?:\/\//, '').replace(/\/$/, '');
|
let host = serverUrl.trim().replace(/^wss?:\/\//, '').replace(/\/$/, '');
|
||||||
|
|
@ -127,11 +126,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
this.safeSend('chat-message', msg.data);
|
this.safeSend('chat-message', msg.data);
|
||||||
break;
|
break;
|
||||||
case 'UpdateStream':
|
case 'UpdateStream':
|
||||||
// Ignore stream updates for self (we manage local state directly)
|
this.safeSend('peer-stream-update', msg.data);
|
||||||
if (msg.data.user_id !== this.userId) {
|
|
||||||
console.log(`[Network] Peer Stream Update: User=${msg.data.user_id} Type=${msg.data.media_type} Active=${msg.data.active}`);
|
|
||||||
this.safeSend('peer-stream-update', msg.data);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'Error':
|
case 'Error':
|
||||||
console.error('WS Error Msg:', msg.data);
|
console.error('WS Error Msg:', msg.data);
|
||||||
|
|
@ -226,7 +221,19 @@ export class NetworkManager extends EventEmitter {
|
||||||
// --- New Encode Methods ---
|
// --- New Encode Methods ---
|
||||||
|
|
||||||
sendEncodedVideoChunk(chunk: any, isKeyFrame: boolean, timestamp: number, streamType: 'video' | 'screen' = 'video') {
|
sendEncodedVideoChunk(chunk: any, isKeyFrame: boolean, timestamp: number, streamType: 'video' | 'screen' = 'video') {
|
||||||
const MAX_PAYLOAD = 1400; // WS can handle larger but keeping small for consistency
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
// Backpressure check: If we have > 2.5MB buffered, drop this frame.
|
||||||
|
// For video, it's better to drop than to lag.
|
||||||
|
if (this.ws.bufferedAmount > 2.5 * 1024 * 1024) {
|
||||||
|
this.dropCount++;
|
||||||
|
if (this.dropCount % 60 === 0) {
|
||||||
|
console.warn(`[Network] Backpressure! Dropped ${this.dropCount} video frames. Buffered: ${this.ws.bufferedAmount} bytes`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_PAYLOAD = 16384;
|
||||||
const totalSize = chunk.length;
|
const totalSize = chunk.length;
|
||||||
|
|
||||||
const seq = streamType === 'screen' ? this.screenSeq++ : this.videoSeq++;
|
const seq = streamType === 'screen' ? this.screenSeq++ : this.videoSeq++;
|
||||||
|
|
@ -237,7 +244,7 @@ export class NetworkManager extends EventEmitter {
|
||||||
const end = Math.min(start + MAX_PAYLOAD, totalSize);
|
const end = Math.min(start + MAX_PAYLOAD, totalSize);
|
||||||
const slice = chunk.slice(start, end);
|
const slice = chunk.slice(start, end);
|
||||||
|
|
||||||
// Header (22 bytes)
|
// Header (24 bytes)
|
||||||
const header = Buffer.alloc(HEADER_SIZE);
|
const header = Buffer.alloc(HEADER_SIZE);
|
||||||
header.writeUInt8(1, 0); // Version
|
header.writeUInt8(1, 0); // Version
|
||||||
const mType = streamType === 'screen' ? MediaType.Screen : MediaType.Video;
|
const mType = streamType === 'screen' ? MediaType.Screen : MediaType.Video;
|
||||||
|
|
|
||||||
|
|
@ -482,7 +482,7 @@ function App() {
|
||||||
video: {
|
video: {
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
frameRate: 30,
|
frameRate: 60,
|
||||||
deviceId: selectedVideoDevice ? { exact: selectedVideoDevice } : undefined
|
deviceId: selectedVideoDevice ? { exact: selectedVideoDevice } : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -566,7 +566,8 @@ function App() {
|
||||||
chromeMediaSource: 'desktop',
|
chromeMediaSource: 'desktop',
|
||||||
chromeMediaSourceId: sourceId,
|
chromeMediaSourceId: sourceId,
|
||||||
maxWidth: 1920,
|
maxWidth: 1920,
|
||||||
maxHeight: 1080
|
maxHeight: 1080,
|
||||||
|
maxFrameRate: 60
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as any);
|
} as any);
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,8 @@ export class MediaEngine extends SimpleEventEmitter {
|
||||||
codec: 'avc1.42001f', // H.264 Baseline Profile Level 3.1 (720p safe)
|
codec: 'avc1.42001f', // H.264 Baseline Profile Level 3.1 (720p safe)
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
bitrate: 2_000_000,
|
bitrate: 4_000_000,
|
||||||
framerate: 30,
|
framerate: 60,
|
||||||
latencyMode: 'realtime',
|
latencyMode: 'realtime',
|
||||||
avc: { format: 'annexb' }
|
avc: { format: 'annexb' }
|
||||||
};
|
};
|
||||||
|
|
@ -62,8 +62,8 @@ export class MediaEngine extends SimpleEventEmitter {
|
||||||
codec: 'avc1.64002a',
|
codec: 'avc1.64002a',
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
bitrate: 2_000_000, // Reduced to 2 Mbps for better stability/FPS
|
bitrate: 8_000_000, // Reduced to 2 Mbps for better stability/FPS
|
||||||
framerate: 30,
|
framerate: 60,
|
||||||
latencyMode: 'realtime', // Changed from 'quality' to 'realtime' for lower latency
|
latencyMode: 'realtime', // Changed from 'quality' to 'realtime' for lower latency
|
||||||
avc: { format: 'annexb' }
|
avc: { format: 'annexb' }
|
||||||
};
|
};
|
||||||
|
|
@ -193,7 +193,7 @@ export class MediaEngine extends SimpleEventEmitter {
|
||||||
// Note: Decoders are usually more flexible, but giving a hint helps.
|
// Note: Decoders are usually more flexible, but giving a hint helps.
|
||||||
// Screen share uses High Profile, Video uses Baseline.
|
// Screen share uses High Profile, Video uses Baseline.
|
||||||
const config: VideoDecoderConfig = streamType === 'screen'
|
const config: VideoDecoderConfig = streamType === 'screen'
|
||||||
? { codec: 'avc1.64002a', optimizeForLatency: false }
|
? { codec: 'avc1.64002a', optimizeForLatency: true }
|
||||||
: { codec: 'avc1.42001f', optimizeForLatency: true };
|
: { codec: 'avc1.42001f', optimizeForLatency: true };
|
||||||
|
|
||||||
decoder.configure(config);
|
decoder.configure(config);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue