abstract jpeg encoding away from "WSServer"

Additionally make thumbnail encoding threadpooled as well, just so it (probably) doesn't block as much.
This commit is contained in:
modeco80
2024-04-24 04:18:05 -04:00
parent ddae307874
commit a904f26961
5 changed files with 102 additions and 51 deletions

59
cvmts/src/JPEGEncoder.ts Normal file
View File

@@ -0,0 +1,59 @@
import path from 'node:path';
import Piscina from 'piscina';
import { Size, Rect } from '@cvmts/shared';
const kMaxJpegThreads = 4;
const kIdleTimeout = 25000;
// Thread pool for doing JPEG encoding for rects.
const TheJpegEncoderPool = new Piscina({
filename: path.join(import.meta.dirname + '/JPEGEncoderWorker.js'),
idleTimeout: kIdleTimeout,
maxThreads: kMaxJpegThreads
});
const TheThumbnailEncoderPool = new Piscina({
filename: path.join(import.meta.dirname + '/ThumbnailJPEGEncoderWorker.js'),
idleTimeout: kIdleTimeout,
maxThreads: kMaxJpegThreads
});
// A good balance. TODO: Configurable?
let gJpegQuality = 35;
export class JPEGEncoder {
static SetQuality(quality: number) {
gJpegQuality = quality;
}
static async EncodeJpeg(canvas: Buffer, displaySize: Size, rect: Rect): Promise<Buffer> {
let offset = (rect.y * displaySize.width + rect.x) * 4;
let res = await TheJpegEncoderPool.run({
buffer: canvas.subarray(offset),
width: rect.width,
height: rect.height,
stride: displaySize.width,
quality: gJpegQuality
});
// TODO: There's probably (definitely) a better way to fix this
if (res == undefined) return Buffer.from([]);
// have to manually turn it back into a buffer because
// Piscina for some reason turns it into a Uint8Array
return Buffer.from(res);
}
static async EncodeThumbnail(buffer: Buffer, size: Size) : Promise<Buffer> {
let res = await TheThumbnailEncoderPool.run({
buffer: buffer,
size: size,
quality: gJpegQuality
});
return Buffer.from(res)
}
}

View File

@@ -9,7 +9,7 @@ export default async (opts: any) => {
height: opts.height, height: opts.height,
subsampling: jpegTurbo.SAMP_422, subsampling: jpegTurbo.SAMP_422,
stride: opts.stride, stride: opts.stride,
quality: opts.quality quality: opts.quality || 75
}); });
return Piscina.move(res); return Piscina.move(res);

View File

@@ -0,0 +1,31 @@
import { Size } from '@cvmts/shared';
import Piscina from 'piscina';
import sharp from 'sharp';
const kThumbnailSize: Size = {
width: 400,
height: 300
};
// this returns appropiate Sharp options to deal with CVMTS raw framebuffers
// (which are RGBA bitmaps, essentially. We probably should abstract that out but
// that'd mean having to introduce that to rfb and oihwekjtgferklds;./tghnredsltg;erhds)
function GetRawSharpOptions(size: Size): sharp.CreateRaw {
return {
width: size.width,
height: size.height,
channels: 4
};
}
export default async (opts: any) => {
let out = await sharp(opts.buffer, { raw: GetRawSharpOptions(opts.size) })
.resize(kThumbnailSize.width, kThumbnailSize.height, { fit: 'fill' })
.jpeg({
quality: opts.quality || 75
})
.toFormat('jpeg')
.toBuffer();
return Piscina.move(out);
};

View File

@@ -17,8 +17,7 @@ import path from 'node:path';
import AuthManager from './AuthManager.js'; import AuthManager from './AuthManager.js';
import { Size, Rect, Logger } from '@cvmts/shared'; import { Size, Rect, Logger } from '@cvmts/shared';
import sharp from 'sharp'; import { JPEGEncoder } from './JPEGEncoder.js';
import Piscina from 'piscina';
// Instead of strange hacks we can just use nodejs provided // Instead of strange hacks we can just use nodejs provided
// import.meta properties, which have existed since LTS if not before // import.meta properties, which have existed since LTS if not before
@@ -37,43 +36,8 @@ type VoteTally = {
no: number; no: number;
}; };
// A good balance. TODO: Configurable?
const kJpegQuality = 35;
// this returns appropiate Sharp options to deal with the framebuffer
function GetRawSharpOptions(size: Size): sharp.CreateRaw {
return {
width: size.width,
height: size.height,
channels: 4
};
}
// Thread pool for doing JPEG encoding for rects.
const TheJpegEncoderPool = new Piscina({
filename: path.join(import.meta.dirname + '/JPEGEncoderWorker.js'),
minThreads: 4,
maxThreads: 4
});
async function EncodeJpeg(canvas: Buffer, displaySize: Size, rect: Rect): Promise<Buffer> {
let offset = (rect.y * displaySize.width + rect.x) * 4;
let res = await TheJpegEncoderPool.run({
buffer: canvas.subarray(offset),
width: rect.width,
height: rect.height,
stride: displaySize.width,
quality: kJpegQuality
});
// TODO: There's probably (definitely) a better way to fix this
if (res == undefined) return Buffer.from([]);
// have to manually turn it back into a buffer because
// Piscina for some reason turns it into a Uint8Array
return Buffer.from(res);
}
export default class WSServer { export default class WSServer {
private Config: IConfig; private Config: IConfig;
@@ -930,24 +894,17 @@ export default class WSServer {
let display = this.VM.GetDisplay(); let display = this.VM.GetDisplay();
let displaySize = display.Size(); let displaySize = display.Size();
let encoded = await EncodeJpeg(display.Buffer(), displaySize, rect); let encoded = await JPEGEncoder.EncodeJpeg(display.Buffer(), displaySize, rect);
return encoded.toString('base64'); return encoded.toString('base64');
} }
getThumbnail(): Promise<string> { async getThumbnail(): Promise<string> {
return new Promise(async (res, rej) => {
let display = this.VM.GetDisplay(); let display = this.VM.GetDisplay();
if (display == null) return; if (!display.Connected()) throw new Error('VM display is not connected');
// TODO: pass custom options to Sharp.resize() probably let buf = await JPEGEncoder.EncodeThumbnail(display.Buffer(), display.Size());
let out = await sharp(display.Buffer(), { raw: GetRawSharpOptions(display.Size()) }) return buf.toString('base64');
.resize(400, 300, { fit: 'fill' })
.toFormat('jpeg')
.toBuffer();
res(out.toString('base64'));
});
} }
startVote() { startVote() {

View File

@@ -116,6 +116,10 @@ export class QemuDisplay extends EventEmitter {
this.displayVnc.disconnect(); this.displayVnc.disconnect();
} }
Connected() {
return this.displayVnc.connected;
}
Buffer(): Buffer { Buffer(): Buffer {
return this.displayVnc.fb; return this.displayVnc.fb;
} }