From 5b0ef9c60601af2817da22675b54d0036ff047f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Tue, 13 Jan 2026 13:53:23 +0100 Subject: [PATCH] Segregate script canvas context --- src/main/resources/static/css/styles.css | 14 ++- src/main/resources/static/js/broadcast.js | 4 +- .../resources/static/js/broadcast/renderer.js | 92 ++++++++++++++----- .../static/js/broadcast/script-worker.js | 43 ++++----- src/main/resources/templates/broadcast.html | 2 +- 5 files changed, 109 insertions(+), 46 deletions(-) diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index a2c8d5d..29c82c1 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -1120,10 +1120,22 @@ button:disabled:hover { 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; } +.broadcast-script-layer canvas { + position: absolute; + top: 0; + left: 0; +} + .broadcast-body { margin: 0; overflow: hidden; diff --git a/src/main/resources/static/js/broadcast.js b/src/main/resources/static/js/broadcast.js index 8ff1d4e..332cdde 100644 --- a/src/main/resources/static/js/broadcast.js +++ b/src/main/resources/static/js/broadcast.js @@ -1,7 +1,7 @@ import { BroadcastRenderer } from "./broadcast/renderer.js"; const canvas = document.getElementById("broadcast-canvas"); -const scriptCanvas = document.getElementById("broadcast-script-canvas"); -const renderer = new BroadcastRenderer({ canvas, scriptCanvas, broadcaster, showToast }); +const scriptLayer = document.getElementById("broadcast-script-layer"); +const renderer = new BroadcastRenderer({ canvas, scriptLayer, broadcaster, showToast }); renderer.start(); diff --git a/src/main/resources/static/js/broadcast/renderer.js b/src/main/resources/static/js/broadcast/renderer.js index 6c96b0b..ac91260 100644 --- a/src/main/resources/static/js/broadcast/renderer.js +++ b/src/main/resources/static/js/broadcast/renderer.js @@ -8,10 +8,11 @@ import { createMediaManager } from "./mediaManager.js"; import { createModelManager } from "../media/modelManager.js"; export class BroadcastRenderer { - constructor({ canvas, scriptCanvas, broadcaster, showToast }) { + constructor({ canvas, scriptLayer, broadcaster, showToast }) { this.canvas = canvas; this.ctx = canvas.getContext("2d"); - this.scriptCanvas = scriptCanvas; + this.scriptLayer = scriptLayer; + this.scriptCanvases = new Map(); this.broadcaster = broadcaster; this.showToast = showToast; this.state = createBroadcastState(); @@ -146,13 +147,12 @@ 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`; + if (this.scriptLayer) { + this.scriptLayer.style.width = `${this.state.canvasSettings.width}px`; + this.scriptLayer.style.height = `${this.state.canvasSettings.height}px`; } } + this.resizeScriptCanvases(); this.updateScriptWorkerCanvas(); this.draw(); } @@ -395,40 +395,31 @@ export class BroadcastRenderer { } ensureScriptWorker() { - if (this.scriptWorker || !this.scriptCanvas) { + if (this.scriptWorker) { 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.addEventListener("message", (event) => this.handleScriptWorkerMessage(event)); this.scriptWorker.postMessage( { type: "init", payload: { - canvas: offscreen, - width: this.scriptCanvas.width, - height: this.scriptCanvas.height, channelName: this.broadcaster, }, }, - [offscreen], ); this.scriptWorkerReady = true; } updateScriptWorkerCanvas() { - if (!this.scriptWorker || !this.scriptWorkerReady || !this.scriptCanvas) { + if (!this.scriptWorker || !this.scriptWorkerReady) { return; } this.scriptWorker.postMessage({ type: "resize", payload: { - width: this.scriptCanvas.width, - height: this.scriptCanvas.height, + width: this.state.canvasSettings.width, + height: this.state.canvasSettings.height, }, }); } @@ -483,6 +474,15 @@ export class BroadcastRenderer { if (!this.scriptWorkerReady) { 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; try { const response = await fetch(asset.url); @@ -499,9 +499,12 @@ export class BroadcastRenderer { payload: { id: asset.id, source: assetSource, + canvas: offscreen, + width: scriptCanvas.width, + height: scriptCanvas.height, attachments: await this.resolveScriptAttachments(asset.scriptAttachments), }, - }); + }, [offscreen]); } async updateScriptWorkerAttachments(asset) { @@ -525,6 +528,53 @@ export class BroadcastRenderer { type: "removeScript", 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) { diff --git a/src/main/resources/static/js/broadcast/script-worker.js b/src/main/resources/static/js/broadcast/script-worker.js index 7fc89bd..2b70cfe 100644 --- a/src/main/resources/static/js/broadcast/script-worker.js +++ b/src/main/resources/static/js/broadcast/script-worker.js @@ -1,7 +1,5 @@ const scripts = new Map(); const allowedFetchUrls = new Set(); -let canvas = null; -let ctx = null; let channelName = ""; let tickIntervalId = null; let lastTick = 0; @@ -134,11 +132,11 @@ function updateScriptContexts() { if (!script.context) { return; } - script.context.canvas = canvas; - script.context.ctx = ctx; + script.context.canvas = script.canvas; + script.context.ctx = script.ctx; script.context.channelName = channelName; - script.context.width = canvas?.width ?? 0; - script.context.height = canvas?.height ?? 0; + script.context.width = script.canvas?.width ?? 0; + script.context.height = script.canvas?.height ?? 0; }); } @@ -149,7 +147,7 @@ function ensureTickLoop() { startTime = performance.now(); lastTick = startTime; tickIntervalId = setInterval(() => { - if (!ctx || scripts.size === 0) { + if (scripts.size === 0) { return; } const now = performance.now(); @@ -158,7 +156,7 @@ function ensureTickLoop() { lastTick = now; scripts.forEach((script) => { - if (!script.tick) { + if (!script.tick || !script.ctx) { return; } script.context.now = now; @@ -199,22 +197,19 @@ function createScriptHandlers(source, context, state, sourceLabel = "") { 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; - } + scripts.forEach((script) => { + if (!script.canvas) { + return; + } + script.canvas.width = payload.width || script.canvas.width; + script.canvas.height = payload.height || script.canvas.height; + }); updateScriptContexts(); return; } @@ -226,16 +221,20 @@ self.addEventListener("message", (event) => { } if (type === "addScript") { - if (!payload?.id || !payload?.source) { + if (!payload?.id || !payload?.source || !payload?.canvas) { 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 context = { canvas, ctx, channelName, - width: canvas?.width ?? 0, - height: canvas?.height ?? 0, + width: canvas.width ?? 0, + height: canvas.height ?? 0, now: 0, deltaMs: 0, elapsedMs: 0, @@ -251,6 +250,8 @@ self.addEventListener("message", (event) => { } const script = { id: payload.id, + canvas, + ctx, context, state, init: handlers.init, diff --git a/src/main/resources/templates/broadcast.html b/src/main/resources/templates/broadcast.html index 2ebd455..87e63c3 100644 --- a/src/main/resources/templates/broadcast.html +++ b/src/main/resources/templates/broadcast.html @@ -13,7 +13,7 @@ - +