Segregate script canvas context

This commit is contained in:
2026-01-13 13:53:23 +01:00
parent f215ef9aba
commit 5b0ef9c606
5 changed files with 109 additions and 46 deletions

View File

@@ -1120,10 +1120,22 @@ button:disabled:hover {
z-index: 1; z-index: 1;
} }
#broadcast-script-canvas { .broadcast-script-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 2; z-index: 2;
} }
.broadcast-script-layer canvas {
position: absolute;
top: 0;
left: 0;
}
.broadcast-body { .broadcast-body {
margin: 0; margin: 0;
overflow: hidden; overflow: hidden;

View File

@@ -1,7 +1,7 @@
import { BroadcastRenderer } from "./broadcast/renderer.js"; import { BroadcastRenderer } from "./broadcast/renderer.js";
const canvas = document.getElementById("broadcast-canvas"); const canvas = document.getElementById("broadcast-canvas");
const scriptCanvas = document.getElementById("broadcast-script-canvas"); const scriptLayer = document.getElementById("broadcast-script-layer");
const renderer = new BroadcastRenderer({ canvas, scriptCanvas, broadcaster, showToast }); const renderer = new BroadcastRenderer({ canvas, scriptLayer, broadcaster, showToast });
renderer.start(); renderer.start();

View File

@@ -8,10 +8,11 @@ import { createMediaManager } from "./mediaManager.js";
import { createModelManager } from "../media/modelManager.js"; import { createModelManager } from "../media/modelManager.js";
export class BroadcastRenderer { export class BroadcastRenderer {
constructor({ canvas, scriptCanvas, broadcaster, showToast }) { constructor({ canvas, scriptLayer, broadcaster, showToast }) {
this.canvas = canvas; this.canvas = canvas;
this.ctx = canvas.getContext("2d"); this.ctx = canvas.getContext("2d");
this.scriptCanvas = scriptCanvas; this.scriptLayer = scriptLayer;
this.scriptCanvases = new Map();
this.broadcaster = broadcaster; this.broadcaster = broadcaster;
this.showToast = showToast; this.showToast = showToast;
this.state = createBroadcastState(); this.state = createBroadcastState();
@@ -146,13 +147,12 @@ export class BroadcastRenderer {
this.canvas.height = this.state.canvasSettings.height; this.canvas.height = this.state.canvasSettings.height;
this.canvas.style.width = `${this.state.canvasSettings.width}px`; this.canvas.style.width = `${this.state.canvasSettings.width}px`;
this.canvas.style.height = `${this.state.canvasSettings.height}px`; this.canvas.style.height = `${this.state.canvasSettings.height}px`;
if (this.scriptCanvas) { if (this.scriptLayer) {
this.scriptCanvas.width = this.state.canvasSettings.width; this.scriptLayer.style.width = `${this.state.canvasSettings.width}px`;
this.scriptCanvas.height = this.state.canvasSettings.height; this.scriptLayer.style.height = `${this.state.canvasSettings.height}px`;
this.scriptCanvas.style.width = `${this.state.canvasSettings.width}px`;
this.scriptCanvas.style.height = `${this.state.canvasSettings.height}px`;
} }
} }
this.resizeScriptCanvases();
this.updateScriptWorkerCanvas(); this.updateScriptWorkerCanvas();
this.draw(); this.draw();
} }
@@ -395,40 +395,31 @@ export class BroadcastRenderer {
} }
ensureScriptWorker() { ensureScriptWorker() {
if (this.scriptWorker || !this.scriptCanvas) { if (this.scriptWorker) {
return; 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 = new Worker("/js/broadcast/script-worker.js");
this.scriptWorker.addEventListener("message", (event) => this.handleScriptWorkerMessage(event)); this.scriptWorker.addEventListener("message", (event) => this.handleScriptWorkerMessage(event));
this.scriptWorker.postMessage( this.scriptWorker.postMessage(
{ {
type: "init", type: "init",
payload: { payload: {
canvas: offscreen,
width: this.scriptCanvas.width,
height: this.scriptCanvas.height,
channelName: this.broadcaster, channelName: this.broadcaster,
}, },
}, },
[offscreen],
); );
this.scriptWorkerReady = true; this.scriptWorkerReady = true;
} }
updateScriptWorkerCanvas() { updateScriptWorkerCanvas() {
if (!this.scriptWorker || !this.scriptWorkerReady || !this.scriptCanvas) { if (!this.scriptWorker || !this.scriptWorkerReady) {
return; return;
} }
this.scriptWorker.postMessage({ this.scriptWorker.postMessage({
type: "resize", type: "resize",
payload: { payload: {
width: this.scriptCanvas.width, width: this.state.canvasSettings.width,
height: this.scriptCanvas.height, height: this.state.canvasSettings.height,
}, },
}); });
} }
@@ -483,6 +474,15 @@ export class BroadcastRenderer {
if (!this.scriptWorkerReady) { if (!this.scriptWorkerReady) {
return; return;
} }
const scriptCanvas = this.ensureScriptCanvas(asset.id);
if (!scriptCanvas) {
return;
}
if (typeof scriptCanvas.transferControlToOffscreen !== "function") {
console.warn("OffscreenCanvas is not supported in this environment.");
return;
}
const offscreen = scriptCanvas.transferControlToOffscreen();
let assetSource; let assetSource;
try { try {
const response = await fetch(asset.url); const response = await fetch(asset.url);
@@ -499,9 +499,12 @@ export class BroadcastRenderer {
payload: { payload: {
id: asset.id, id: asset.id,
source: assetSource, source: assetSource,
canvas: offscreen,
width: scriptCanvas.width,
height: scriptCanvas.height,
attachments: await this.resolveScriptAttachments(asset.scriptAttachments), attachments: await this.resolveScriptAttachments(asset.scriptAttachments),
}, },
}); }, [offscreen]);
} }
async updateScriptWorkerAttachments(asset) { async updateScriptWorkerAttachments(asset) {
@@ -525,6 +528,53 @@ export class BroadcastRenderer {
type: "removeScript", type: "removeScript",
payload: { id: assetId }, payload: { id: assetId },
}); });
this.removeScriptCanvas(assetId);
}
ensureScriptCanvas(assetId) {
if (!assetId || !this.scriptLayer) {
return null;
}
const existing = this.scriptCanvases.get(assetId);
if (existing) {
return existing;
}
const canvas = document.createElement("canvas");
canvas.className = "broadcast-script-canvas";
canvas.dataset.scriptId = assetId;
this.applyScriptCanvasSize(canvas);
this.scriptLayer.appendChild(canvas);
this.scriptCanvases.set(assetId, canvas);
return canvas;
}
removeScriptCanvas(assetId) {
const canvas = this.scriptCanvases.get(assetId);
if (!canvas) {
return;
}
canvas.remove();
this.scriptCanvases.delete(assetId);
}
resizeScriptCanvases() {
this.scriptCanvases.forEach((canvas) => {
this.applyScriptCanvasSize(canvas);
});
}
applyScriptCanvasSize(canvas) {
if (!canvas) {
return;
}
if (Number.isFinite(this.state.canvasSettings.width)) {
canvas.width = this.state.canvasSettings.width;
canvas.style.width = `${this.state.canvasSettings.width}px`;
}
if (Number.isFinite(this.state.canvasSettings.height)) {
canvas.height = this.state.canvasSettings.height;
canvas.style.height = `${this.state.canvasSettings.height}px`;
}
} }
async resolveScriptAttachments(attachments) { async resolveScriptAttachments(attachments) {

View File

@@ -1,7 +1,5 @@
const scripts = new Map(); const scripts = new Map();
const allowedFetchUrls = new Set(); const allowedFetchUrls = new Set();
let canvas = null;
let ctx = null;
let channelName = ""; let channelName = "";
let tickIntervalId = null; let tickIntervalId = null;
let lastTick = 0; let lastTick = 0;
@@ -134,11 +132,11 @@ function updateScriptContexts() {
if (!script.context) { if (!script.context) {
return; return;
} }
script.context.canvas = canvas; script.context.canvas = script.canvas;
script.context.ctx = ctx; script.context.ctx = script.ctx;
script.context.channelName = channelName; script.context.channelName = channelName;
script.context.width = canvas?.width ?? 0; script.context.width = script.canvas?.width ?? 0;
script.context.height = canvas?.height ?? 0; script.context.height = script.canvas?.height ?? 0;
}); });
} }
@@ -149,7 +147,7 @@ function ensureTickLoop() {
startTime = performance.now(); startTime = performance.now();
lastTick = startTime; lastTick = startTime;
tickIntervalId = setInterval(() => { tickIntervalId = setInterval(() => {
if (!ctx || scripts.size === 0) { if (scripts.size === 0) {
return; return;
} }
const now = performance.now(); const now = performance.now();
@@ -158,7 +156,7 @@ function ensureTickLoop() {
lastTick = now; lastTick = now;
scripts.forEach((script) => { scripts.forEach((script) => {
if (!script.tick) { if (!script.tick || !script.ctx) {
return; return;
} }
script.context.now = now; script.context.now = now;
@@ -199,22 +197,19 @@ function createScriptHandlers(source, context, state, sourceLabel = "") {
self.addEventListener("message", (event) => { self.addEventListener("message", (event) => {
const { type, payload } = event.data || {}; const { type, payload } = event.data || {};
if (type === "init") { if (type === "init") {
canvas = payload.canvas;
channelName = payload.channelName || ""; channelName = payload.channelName || "";
if (canvas) {
canvas.width = payload.width || canvas.width;
canvas.height = payload.height || canvas.height;
ctx = canvas.getContext("2d");
}
updateScriptContexts(); updateScriptContexts();
return; return;
} }
if (type === "resize") { if (type === "resize") {
if (canvas) { scripts.forEach((script) => {
canvas.width = payload.width || canvas.width; if (!script.canvas) {
canvas.height = payload.height || canvas.height; return;
} }
script.canvas.width = payload.width || script.canvas.width;
script.canvas.height = payload.height || script.canvas.height;
});
updateScriptContexts(); updateScriptContexts();
return; return;
} }
@@ -226,16 +221,20 @@ self.addEventListener("message", (event) => {
} }
if (type === "addScript") { if (type === "addScript") {
if (!payload?.id || !payload?.source) { if (!payload?.id || !payload?.source || !payload?.canvas) {
return; return;
} }
const canvas = payload.canvas;
canvas.width = payload.width || canvas.width;
canvas.height = payload.height || canvas.height;
const ctx = canvas.getContext("2d");
const state = {}; const state = {};
const context = { const context = {
canvas, canvas,
ctx, ctx,
channelName, channelName,
width: canvas?.width ?? 0, width: canvas.width ?? 0,
height: canvas?.height ?? 0, height: canvas.height ?? 0,
now: 0, now: 0,
deltaMs: 0, deltaMs: 0,
elapsedMs: 0, elapsedMs: 0,
@@ -251,6 +250,8 @@ self.addEventListener("message", (event) => {
} }
const script = { const script = {
id: payload.id, id: payload.id,
canvas,
ctx,
context, context,
state, state,
init: handlers.init, init: handlers.init,

View File

@@ -13,7 +13,7 @@
</head> </head>
<body class="broadcast-body"> <body class="broadcast-body">
<canvas id="broadcast-canvas"></canvas> <canvas id="broadcast-canvas"></canvas>
<canvas id="broadcast-script-canvas"></canvas> <div id="broadcast-script-layer" class="broadcast-script-layer"></div>
<script th:inline="javascript"> <script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ ""; const broadcaster = /*[[${broadcaster}]]*/ "";
</script> </script>