better display/vm stuff
- moved superqemu's "QemuDisplay" here; the VNC VM and Qemu both share it (and it has been renamed to a less goofy dumb name) - VNC VM has been heavily refactored to just use the VNC display we have (this means only one source of truth, less bugs, and it's generally just Better to share the code imho). this means that future plans to abstract this further (or implement the client in cvm-rs in general) won't cause any explosions, or require duplicate effort - vms are now in src/vm/... just better organization - superqemu doesn't manage a display anymore (or care about it, other than making sure the socket is unlinked on stop). Instead now it provides info for us to setup our own VNC client. This is also why we provide our own shim interface This currently relies on a alpha version of superqemu. Before this is merged into cvmts main I will publish a stable tag and point cvmts to that new version
This commit is contained in:
41
cvmts/src/display/batch.ts
Normal file
41
cvmts/src/display/batch.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Size, Rect } from "../Utilities";
|
||||
|
||||
export function BatchRects(size: Size, rects: Array<Rect>): Rect {
|
||||
var mergedX = size.width;
|
||||
var mergedY = size.height;
|
||||
var mergedHeight = 0;
|
||||
var mergedWidth = 0;
|
||||
|
||||
// can't batch these
|
||||
if (rects.length == 0) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: size.width,
|
||||
height: size.height
|
||||
};
|
||||
}
|
||||
|
||||
if (rects.length == 1) {
|
||||
if (rects[0].width == size.width && rects[0].height == size.height) {
|
||||
return rects[0];
|
||||
}
|
||||
}
|
||||
|
||||
rects.forEach((r) => {
|
||||
if (r.x < mergedX) mergedX = r.x;
|
||||
if (r.y < mergedY) mergedY = r.y;
|
||||
});
|
||||
|
||||
rects.forEach((r) => {
|
||||
if (r.height + r.y - mergedY > mergedHeight) mergedHeight = r.height + r.y - mergedY;
|
||||
if (r.width + r.x - mergedX > mergedWidth) mergedWidth = r.width + r.x - mergedX;
|
||||
});
|
||||
|
||||
return {
|
||||
x: mergedX,
|
||||
y: mergedY,
|
||||
width: mergedWidth,
|
||||
height: mergedHeight
|
||||
};
|
||||
}
|
||||
12
cvmts/src/display/interface.ts
Normal file
12
cvmts/src/display/interface.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import EventEmitter from 'node:events';
|
||||
import { Size, Rect } from '../Utilities';
|
||||
|
||||
export interface VMDisplay extends EventEmitter {
|
||||
Connect(): void;
|
||||
Disconnect(): void;
|
||||
Connected(): boolean;
|
||||
Buffer(): Buffer;
|
||||
Size(): Size;
|
||||
MouseEvent(x: number, y: number, buttons: number): void;
|
||||
KeyboardEvent(keysym: number, pressed: boolean): void;
|
||||
}
|
||||
153
cvmts/src/display/vnc.ts
Normal file
153
cvmts/src/display/vnc.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import { VncClient } from '@computernewb/nodejs-rfb';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Clamp } from '../Utilities.js';
|
||||
import { BatchRects } from './batch.js';
|
||||
import { VMDisplay } from './interface.js';
|
||||
|
||||
import { Size, Rect } from '../Utilities.js';
|
||||
|
||||
const kQemuFps = 60;
|
||||
|
||||
export type VncRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
// events:
|
||||
//
|
||||
// 'resize' -> (w, h) -> done when resize occurs
|
||||
// 'rect' -> (x, y, ImageData) -> framebuffer
|
||||
// 'frame' -> () -> done at end of frame
|
||||
|
||||
// TODO: replace with a non-asshole VNC client
|
||||
export class VncDisplay extends EventEmitter implements VMDisplay {
|
||||
private displayVnc = new VncClient({
|
||||
debug: false,
|
||||
fps: kQemuFps,
|
||||
|
||||
encodings: [
|
||||
VncClient.consts.encodings.raw,
|
||||
|
||||
//VncClient.consts.encodings.pseudoQemuAudio,
|
||||
VncClient.consts.encodings.pseudoDesktopSize
|
||||
// For now?
|
||||
//VncClient.consts.encodings.pseudoCursor
|
||||
]
|
||||
});
|
||||
|
||||
private vncShouldReconnect: boolean = false;
|
||||
private vncConnectOpts: any;
|
||||
|
||||
constructor(vncConnectOpts: any) {
|
||||
super();
|
||||
|
||||
this.vncConnectOpts = vncConnectOpts;
|
||||
|
||||
this.displayVnc.on('connectTimeout', () => {
|
||||
this.Reconnect();
|
||||
});
|
||||
|
||||
this.displayVnc.on('authError', () => {
|
||||
this.Reconnect();
|
||||
});
|
||||
|
||||
this.displayVnc.on('disconnect', () => {
|
||||
this.Reconnect();
|
||||
});
|
||||
|
||||
this.displayVnc.on('closed', () => {
|
||||
this.Reconnect();
|
||||
});
|
||||
|
||||
this.displayVnc.on('firstFrameUpdate', () => {
|
||||
// apparently this library is this good.
|
||||
// at least it's better than the two others which exist.
|
||||
this.displayVnc.changeFps(kQemuFps);
|
||||
this.emit('connected');
|
||||
|
||||
this.emit('resize', { width: this.displayVnc.clientWidth, height: this.displayVnc.clientHeight });
|
||||
//this.emit('rect', { x: 0, y: 0, width: this.displayVnc.clientWidth, height: this.displayVnc.clientHeight });
|
||||
this.emit('frame');
|
||||
});
|
||||
|
||||
this.displayVnc.on('desktopSizeChanged', (size: Size) => {
|
||||
this.emit('resize', size);
|
||||
});
|
||||
|
||||
let rects: Rect[] = [];
|
||||
|
||||
this.displayVnc.on('rectUpdateProcessed', (rect: Rect) => {
|
||||
rects.push(rect);
|
||||
});
|
||||
|
||||
this.displayVnc.on('frameUpdated', (fb: Buffer) => {
|
||||
// use the cvmts batcher
|
||||
let batched = BatchRects(this.Size(), rects);
|
||||
this.emit('rect', batched);
|
||||
|
||||
// unbatched (watch the performace go now)
|
||||
//for(let rect of rects)
|
||||
// this.emit('rect', rect);
|
||||
|
||||
rects = [];
|
||||
|
||||
this.emit('frame');
|
||||
});
|
||||
}
|
||||
|
||||
private Reconnect() {
|
||||
if (this.displayVnc.connected) return;
|
||||
|
||||
if (!this.vncShouldReconnect) return;
|
||||
|
||||
// TODO: this should also give up after a max tries count
|
||||
// if we fail after max tries, emit a event
|
||||
|
||||
this.displayVnc.connect(this.vncConnectOpts);
|
||||
}
|
||||
|
||||
Connect() {
|
||||
this.vncShouldReconnect = true;
|
||||
this.Reconnect();
|
||||
}
|
||||
|
||||
Disconnect() {
|
||||
this.vncShouldReconnect = false;
|
||||
this.displayVnc.disconnect();
|
||||
|
||||
// bye bye!
|
||||
this.displayVnc.removeAllListeners();
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
Connected() {
|
||||
return this.displayVnc.connected;
|
||||
}
|
||||
|
||||
Buffer(): Buffer {
|
||||
return this.displayVnc.fb;
|
||||
}
|
||||
|
||||
Size(): Size {
|
||||
if (!this.displayVnc.connected)
|
||||
return {
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
|
||||
return {
|
||||
width: this.displayVnc.clientWidth,
|
||||
height: this.displayVnc.clientHeight
|
||||
};
|
||||
}
|
||||
|
||||
MouseEvent(x: number, y: number, buttons: number) {
|
||||
if (this.displayVnc.connected) this.displayVnc.sendPointerEvent(Clamp(x, 0, this.displayVnc.clientWidth), Clamp(y, 0, this.displayVnc.clientHeight), buttons);
|
||||
}
|
||||
|
||||
KeyboardEvent(keysym: number, pressed: boolean) {
|
||||
if (this.displayVnc.connected) this.displayVnc.sendKeyEvent(keysym, pressed);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user