mirror of
https://github.com/imgfloat/server.git
synced 2026-02-05 11:49:25 +00:00
Start custom script assets
This commit is contained in:
@@ -7,9 +7,10 @@ import { createAudioManager } from "./audioManager.js";
|
||||
import { createMediaManager } from "./mediaManager.js";
|
||||
|
||||
export class BroadcastRenderer {
|
||||
constructor({ canvas, broadcaster, showToast }) {
|
||||
constructor({ canvas, scriptCanvas, broadcaster, showToast }) {
|
||||
this.canvas = canvas;
|
||||
this.ctx = canvas.getContext("2d");
|
||||
this.scriptCanvas = scriptCanvas;
|
||||
this.broadcaster = broadcaster;
|
||||
this.showToast = showToast;
|
||||
this.state = createBroadcastState();
|
||||
@@ -17,6 +18,8 @@ export class BroadcastRenderer {
|
||||
this.frameScheduled = false;
|
||||
this.pendingDraw = false;
|
||||
this.renderIntervalId = null;
|
||||
this.scriptWorker = null;
|
||||
this.scriptWorkerReady = false;
|
||||
|
||||
this.obsBrowser = !!globalThis.obsstudio;
|
||||
this.supportsAnimatedDecode =
|
||||
@@ -135,7 +138,14 @@ export class BroadcastRenderer {
|
||||
this.canvas.height = this.state.canvasSettings.height;
|
||||
this.canvas.style.width = `${this.state.canvasSettings.width}px`;
|
||||
this.canvas.style.height = `${this.state.canvasSettings.height}px`;
|
||||
if (this.scriptCanvas) {
|
||||
this.scriptCanvas.width = this.state.canvasSettings.width;
|
||||
this.scriptCanvas.height = this.state.canvasSettings.height;
|
||||
this.scriptCanvas.style.width = `${this.state.canvasSettings.width}px`;
|
||||
this.scriptCanvas.style.height = `${this.state.canvasSettings.height}px`;
|
||||
}
|
||||
}
|
||||
this.updateScriptWorkerCanvas();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
@@ -368,7 +378,79 @@ export class BroadcastRenderer {
|
||||
}, MIN_FRAME_TIME);
|
||||
}
|
||||
|
||||
spawnUserJavaScriptWorker() {}
|
||||
ensureScriptWorker() {
|
||||
if (this.scriptWorker || !this.scriptCanvas) {
|
||||
return;
|
||||
}
|
||||
if (typeof this.scriptCanvas.transferControlToOffscreen !== "function") {
|
||||
console.warn("OffscreenCanvas is not supported in this environment.");
|
||||
return;
|
||||
}
|
||||
const offscreen = this.scriptCanvas.transferControlToOffscreen();
|
||||
this.scriptWorker = new Worker("/js/broadcast/script-worker.js");
|
||||
this.scriptWorker.postMessage(
|
||||
{
|
||||
type: "init",
|
||||
payload: {
|
||||
canvas: offscreen,
|
||||
width: this.scriptCanvas.width,
|
||||
height: this.scriptCanvas.height,
|
||||
channelName: this.broadcaster,
|
||||
},
|
||||
},
|
||||
[offscreen],
|
||||
);
|
||||
this.scriptWorkerReady = true;
|
||||
}
|
||||
|
||||
stopUserJavaScriptWorker() {}
|
||||
updateScriptWorkerCanvas() {
|
||||
if (!this.scriptWorker || !this.scriptWorkerReady || !this.scriptCanvas) {
|
||||
return;
|
||||
}
|
||||
this.scriptWorker.postMessage({
|
||||
type: "resize",
|
||||
payload: {
|
||||
width: this.scriptCanvas.width,
|
||||
height: this.scriptCanvas.height,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async spawnUserJavaScriptWorker(asset) {
|
||||
if (!asset?.id || !asset?.url) {
|
||||
return;
|
||||
}
|
||||
this.ensureScriptWorker();
|
||||
if (!this.scriptWorkerReady) {
|
||||
return;
|
||||
}
|
||||
let assetSource;
|
||||
try {
|
||||
const response = await fetch(asset.url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load script asset ${asset.id}`);
|
||||
}
|
||||
assetSource = await response.text();
|
||||
} catch (error) {
|
||||
console.error(`Unable to fetch asset ${asset.id} from ${asset.url}`, error);
|
||||
return;
|
||||
}
|
||||
this.scriptWorker.postMessage({
|
||||
type: "addScript",
|
||||
payload: {
|
||||
id: asset.id,
|
||||
source: assetSource,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
stopUserJavaScriptWorker(assetId) {
|
||||
if (!this.scriptWorker || !assetId) {
|
||||
return;
|
||||
}
|
||||
this.scriptWorker.postMessage({
|
||||
type: "removeScript",
|
||||
payload: { id: assetId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
150
src/main/resources/static/js/broadcast/script-worker.js
Normal file
150
src/main/resources/static/js/broadcast/script-worker.js
Normal file
@@ -0,0 +1,150 @@
|
||||
const scripts = new Map();
|
||||
let canvas = null;
|
||||
let ctx = null;
|
||||
let channelName = "";
|
||||
let tickIntervalId = null;
|
||||
let lastTick = 0;
|
||||
let startTime = 0;
|
||||
|
||||
function updateScriptContexts() {
|
||||
scripts.forEach((script) => {
|
||||
if (!script.context) {
|
||||
return;
|
||||
}
|
||||
script.context.canvas = canvas;
|
||||
script.context.ctx = ctx;
|
||||
script.context.channelName = channelName;
|
||||
script.context.width = canvas?.width ?? 0;
|
||||
script.context.height = canvas?.height ?? 0;
|
||||
});
|
||||
}
|
||||
|
||||
function ensureTickLoop() {
|
||||
if (tickIntervalId) {
|
||||
return;
|
||||
}
|
||||
startTime = performance.now();
|
||||
lastTick = startTime;
|
||||
tickIntervalId = setInterval(() => {
|
||||
if (!ctx || scripts.size === 0) {
|
||||
return;
|
||||
}
|
||||
const now = performance.now();
|
||||
const deltaMs = now - lastTick;
|
||||
const elapsedMs = now - startTime;
|
||||
lastTick = now;
|
||||
|
||||
scripts.forEach((script) => {
|
||||
if (!script.tick) {
|
||||
return;
|
||||
}
|
||||
script.context.now = now;
|
||||
script.context.deltaMs = deltaMs;
|
||||
script.context.elapsedMs = elapsedMs;
|
||||
try {
|
||||
script.tick(script.context, script.state);
|
||||
} catch (error) {
|
||||
console.error(`Script ${script.id} tick failed`, error);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function stopTickLoopIfIdle() {
|
||||
if (scripts.size === 0 && tickIntervalId) {
|
||||
clearInterval(tickIntervalId);
|
||||
tickIntervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function createScriptHandlers(source, context, state) {
|
||||
const factory = new Function(
|
||||
"context",
|
||||
"state",
|
||||
"module",
|
||||
"exports",
|
||||
`${source}\nconst resolved = (module && module.exports) || exports || {};\nreturn {\n init: typeof resolved.init === "function" ? resolved.init : typeof init === "function" ? init : null,\n tick: typeof resolved.tick === "function" ? resolved.tick : typeof tick === "function" ? tick : null,\n};`,
|
||||
);
|
||||
const module = { exports: {} };
|
||||
const exports = module.exports;
|
||||
return factory(context, state, module, exports);
|
||||
}
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
const { type, payload } = event.data || {};
|
||||
if (type === "init") {
|
||||
canvas = payload.canvas;
|
||||
channelName = payload.channelName || "";
|
||||
if (canvas) {
|
||||
canvas.width = payload.width || canvas.width;
|
||||
canvas.height = payload.height || canvas.height;
|
||||
ctx = canvas.getContext("2d");
|
||||
}
|
||||
updateScriptContexts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "resize") {
|
||||
if (canvas) {
|
||||
canvas.width = payload.width || canvas.width;
|
||||
canvas.height = payload.height || canvas.height;
|
||||
}
|
||||
updateScriptContexts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "channel") {
|
||||
channelName = payload.channelName || channelName;
|
||||
updateScriptContexts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "addScript") {
|
||||
if (!payload?.id || !payload?.source) {
|
||||
return;
|
||||
}
|
||||
const state = {};
|
||||
const context = {
|
||||
canvas,
|
||||
ctx,
|
||||
channelName,
|
||||
width: canvas?.width ?? 0,
|
||||
height: canvas?.height ?? 0,
|
||||
now: 0,
|
||||
deltaMs: 0,
|
||||
elapsedMs: 0,
|
||||
};
|
||||
let handlers = {};
|
||||
try {
|
||||
handlers = createScriptHandlers(payload.source, context, state);
|
||||
} catch (error) {
|
||||
console.error(`Script ${payload.id} failed to initialize`, error);
|
||||
return;
|
||||
}
|
||||
const script = {
|
||||
id: payload.id,
|
||||
context,
|
||||
state,
|
||||
init: handlers.init,
|
||||
tick: handlers.tick,
|
||||
};
|
||||
scripts.set(payload.id, script);
|
||||
if (script.init) {
|
||||
try {
|
||||
script.init(script.context, script.state);
|
||||
} catch (error) {
|
||||
console.error(`Script ${payload.id} init failed`, error);
|
||||
}
|
||||
}
|
||||
ensureTickLoop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "removeScript") {
|
||||
if (!payload?.id) {
|
||||
return;
|
||||
}
|
||||
scripts.delete(payload.id);
|
||||
stopTickLoopIfIdle();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user