fix: remote display now uses per-instance WebSocket (was shared singleton — broke multi-session)
This commit is contained in:
parent
b7d6c950cf
commit
ef1b8dfa4e
@ -1,18 +1,48 @@
|
||||
import {
|
||||
Component,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
signal,
|
||||
computed,
|
||||
AfterViewInit,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { WebSocketService } from '../../services/websocket.service';
|
||||
import { Subject, filter, takeUntil } from 'rxjs';
|
||||
|
||||
// ── Types matching the Rust WsMessage enum ─────────────────────────────────
|
||||
|
||||
interface FrameBroadcast {
|
||||
msg_type: 'frame_broadcast';
|
||||
data: string;
|
||||
content_type: string;
|
||||
}
|
||||
|
||||
interface AudioBroadcast {
|
||||
msg_type: 'audio_broadcast';
|
||||
data: string;
|
||||
content_type: string;
|
||||
}
|
||||
|
||||
interface SessionUpdate {
|
||||
msg_type: 'session_update';
|
||||
session_id: string;
|
||||
status: 'waiting' | 'active' | 'disconnected';
|
||||
resolution: string | null;
|
||||
}
|
||||
|
||||
interface WsError {
|
||||
msg_type: 'error';
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface WsAck {
|
||||
msg_type: 'ack';
|
||||
message: string;
|
||||
}
|
||||
|
||||
type ServerMessage = FrameBroadcast | AudioBroadcast | SessionUpdate | WsError | WsAck;
|
||||
|
||||
@Component({
|
||||
selector: 'app-remote-display',
|
||||
@ -24,8 +54,6 @@ import { WebSocketService } from '../../services/websocket.service';
|
||||
export class RemoteDisplayComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@Input({ required: true }) sessionId!: string;
|
||||
|
||||
@Output() statusChange = new EventEmitter<'connecting' | 'connected' | 'disconnected' | 'error'>();
|
||||
|
||||
@ViewChild('canvas') canvasRef!: ElementRef<HTMLCanvasElement>;
|
||||
@ViewChild('displayContainer') containerRef!: ElementRef<HTMLDivElement>;
|
||||
|
||||
@ -49,73 +77,145 @@ export class RemoteDisplayComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
/** Session status from server. */
|
||||
readonly sessionStatus = signal<string>('waiting');
|
||||
|
||||
constructor(private ws: WebSocketService) {}
|
||||
/** Per-instance WebSocket connection (not the shared singleton). */
|
||||
private socket: WebSocket | null = null;
|
||||
private messagesSubject = new Subject<ServerMessage>();
|
||||
private destroy$ = new Subject<void>();
|
||||
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
// Connect to the session via WebSocket.
|
||||
this.ws.connect(this.sessionId);
|
||||
|
||||
// Subscribe to display frames.
|
||||
this.ws.displayFrames.subscribe({
|
||||
next: (frame) => {
|
||||
if (!this.canvas || !this.ctx) return;
|
||||
this.img.onload = () => {
|
||||
this.ctx!.drawImage(this.img, 0, 0, this.canvas!.width, this.canvas!.height);
|
||||
this.hasFrame = true;
|
||||
};
|
||||
this.img.src = 'data:' + frame.content_type + ';base64,' + frame.data;
|
||||
// FPS counter.
|
||||
this.frameCount++;
|
||||
const now = performance.now();
|
||||
if (now - this.lastFpsTime >= 1000) {
|
||||
this.fps.set(this.frameCount);
|
||||
this.frameCount = 0;
|
||||
this.lastFpsTime = now;
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.status.set('error');
|
||||
this.statusChange.emit('error');
|
||||
},
|
||||
});
|
||||
|
||||
// Subscribe to session state updates.
|
||||
this.ws.sessionUpdates.subscribe((update) => {
|
||||
if (update.session_id === this.sessionId) {
|
||||
this.sessionStatus.set(update.status);
|
||||
this.resolution.set(update.resolution);
|
||||
if (update.status === 'active') {
|
||||
this.status.set('connected');
|
||||
this.statusChange.emit('connected');
|
||||
} else if (update.status === 'disconnected') {
|
||||
this.status.set('disconnected');
|
||||
this.statusChange.emit('disconnected');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Connection state.
|
||||
this.ws.connected.subscribe((connected) => {
|
||||
if (connected) {
|
||||
this.status.set('connecting');
|
||||
} else {
|
||||
this.status.set('disconnected');
|
||||
this.statusChange.emit('disconnected');
|
||||
}
|
||||
});
|
||||
this.connectWebSocket();
|
||||
this.setupSubscriptions();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.canvas = this.canvasRef.nativeElement;
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.resizeCanvas();
|
||||
// Start render loop.
|
||||
this.renderLoop();
|
||||
|
||||
// Watch for container size changes (window resize, maximize, etc.).
|
||||
this.resizeObserver = new ResizeObserver(() => this.resizeCanvas());
|
||||
this.resizeObserver.observe(this.containerRef.nativeElement);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.ws.disconnect();
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
this.disconnectWebSocket();
|
||||
if (this.animFrameId) cancelAnimationFrame(this.animFrameId);
|
||||
this.resizeObserver?.disconnect();
|
||||
}
|
||||
|
||||
// ── WebSocket connection (per-instance) ─────────────────────────────────
|
||||
|
||||
private connectWebSocket(): void {
|
||||
const wsUrl =
|
||||
window.location.origin.replace(/^http/, 'ws') +
|
||||
`/ws/${this.sessionId}?client_type=viewer`;
|
||||
|
||||
this.socket = new WebSocket(wsUrl);
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log('[RemoteDisplay] connected to session', this.sessionId);
|
||||
this.status.set('connecting');
|
||||
this.heartbeatTimer = setInterval(() => this.sendRaw({ msg_type: 'heartbeat' }), 15000);
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
try {
|
||||
const msg: ServerMessage = JSON.parse(event.data);
|
||||
this.messagesSubject.next(msg);
|
||||
} catch (e) {
|
||||
console.warn('[RemoteDisplay] failed to parse message', event.data, e);
|
||||
}
|
||||
};
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
console.log('[RemoteDisplay] disconnected', event.code, event.reason);
|
||||
this.status.set('disconnected');
|
||||
this.stopHeartbeat();
|
||||
};
|
||||
|
||||
this.socket.onerror = () => {
|
||||
this.status.set('error');
|
||||
};
|
||||
}
|
||||
|
||||
private disconnectWebSocket(): void {
|
||||
this.stopHeartbeat();
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
private setupSubscriptions(): void {
|
||||
// Display frames
|
||||
this.messagesSubject.pipe(
|
||||
filter((m): m is FrameBroadcast => m.msg_type === 'frame_broadcast'),
|
||||
takeUntil(this.destroy$),
|
||||
).subscribe((frame) => {
|
||||
if (!this.canvas || !this.ctx) return;
|
||||
this.img.onload = () => {
|
||||
this.ctx!.drawImage(this.img, 0, 0, this.canvas!.width, this.canvas!.height);
|
||||
this.hasFrame = true;
|
||||
};
|
||||
this.img.src = 'data:' + frame.content_type + ';base64,' + frame.data;
|
||||
this.frameCount++;
|
||||
const now = performance.now();
|
||||
if (now - this.lastFpsTime >= 1000) {
|
||||
this.fps.set(this.frameCount);
|
||||
this.frameCount = 0;
|
||||
this.lastFpsTime = now;
|
||||
}
|
||||
});
|
||||
|
||||
// Session state updates
|
||||
this.messagesSubject.pipe(
|
||||
filter((m): m is SessionUpdate => m.msg_type === 'session_update'),
|
||||
takeUntil(this.destroy$),
|
||||
).subscribe((update) => {
|
||||
if (update.session_id === this.sessionId) {
|
||||
this.sessionStatus.set(update.status);
|
||||
this.resolution.set(update.resolution);
|
||||
if (update.status === 'active') {
|
||||
this.status.set('connected');
|
||||
} else if (update.status === 'disconnected') {
|
||||
this.status.set('disconnected');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Error messages
|
||||
this.messagesSubject.pipe(
|
||||
filter((m): m is WsError => m.msg_type === 'error'),
|
||||
takeUntil(this.destroy$),
|
||||
).subscribe((err) => {
|
||||
console.warn('[RemoteDisplay] server error:', err.message);
|
||||
});
|
||||
}
|
||||
|
||||
private sendRaw(msg: Record<string, unknown>): void {
|
||||
if (this.socket?.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify(msg));
|
||||
}
|
||||
}
|
||||
|
||||
private sendHudCommand(command: string, params: Record<string, unknown> = {}): void {
|
||||
this.sendRaw({
|
||||
msg_type: 'hud_command',
|
||||
session_id: this.sessionId,
|
||||
command,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
private stopHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private resizeCanvas(): void {
|
||||
@ -125,15 +225,11 @@ export class RemoteDisplayComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
this.canvas.height = rect.height;
|
||||
}
|
||||
|
||||
private renderLoop = (): void => {
|
||||
this.animFrameId = requestAnimationFrame(this.renderLoop);
|
||||
};
|
||||
|
||||
// ── Mouse events (HUD forwarding) ───────────────────────────────────────
|
||||
|
||||
onMouseDown(event: MouseEvent): void {
|
||||
const pos = this.getCanvasPos(event);
|
||||
this.ws.sendHudCommand('mouse_down', {
|
||||
this.sendHudCommand('mouse_down', {
|
||||
button: event.button,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
@ -143,7 +239,7 @@ export class RemoteDisplayComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
|
||||
onMouseUp(event: MouseEvent): void {
|
||||
const pos = this.getCanvasPos(event);
|
||||
this.ws.sendHudCommand('mouse_up', {
|
||||
this.sendHudCommand('mouse_up', {
|
||||
button: event.button,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
@ -152,11 +248,11 @@ export class RemoteDisplayComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
|
||||
onMouseMove(event: MouseEvent): void {
|
||||
const pos = this.getCanvasPos(event);
|
||||
this.ws.sendHudCommand('mouse_move', { x: pos.x, y: pos.y });
|
||||
this.sendHudCommand('mouse_move', { x: pos.x, y: pos.y });
|
||||
}
|
||||
|
||||
onWheel(event: WheelEvent): void {
|
||||
this.ws.sendHudCommand('scroll', {
|
||||
this.sendHudCommand('scroll', {
|
||||
deltaX: event.deltaX,
|
||||
deltaY: event.deltaY,
|
||||
});
|
||||
@ -167,7 +263,7 @@ export class RemoteDisplayComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
}
|
||||
|
||||
onKeyDown(event: KeyboardEvent): void {
|
||||
this.ws.sendHudCommand('key_down', {
|
||||
this.sendHudCommand('key_down', {
|
||||
key: event.key,
|
||||
code: event.code,
|
||||
ctrl: event.ctrlKey,
|
||||
@ -182,7 +278,7 @@ export class RemoteDisplayComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
}
|
||||
|
||||
onKeyUp(event: KeyboardEvent): void {
|
||||
this.ws.sendHudCommand('key_up', {
|
||||
this.sendHudCommand('key_up', {
|
||||
key: event.key,
|
||||
code: event.code,
|
||||
ctrl: event.ctrlKey,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user