@czap/worker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +19 -0
- package/dist/compositor-script.d.ts +19 -0
- package/dist/compositor-script.d.ts.map +1 -0
- package/dist/compositor-script.js +374 -0
- package/dist/compositor-script.js.map +1 -0
- package/dist/compositor-startup.d.ts +200 -0
- package/dist/compositor-startup.d.ts.map +1 -0
- package/dist/compositor-startup.js +490 -0
- package/dist/compositor-startup.js.map +1 -0
- package/dist/compositor-types.d.ts +135 -0
- package/dist/compositor-types.d.ts.map +1 -0
- package/dist/compositor-types.js +7 -0
- package/dist/compositor-types.js.map +1 -0
- package/dist/compositor-worker.d.ts +65 -0
- package/dist/compositor-worker.d.ts.map +1 -0
- package/dist/compositor-worker.js +454 -0
- package/dist/compositor-worker.js.map +1 -0
- package/dist/evaluate-inline.d.ts +22 -0
- package/dist/evaluate-inline.d.ts.map +1 -0
- package/dist/evaluate-inline.js +42 -0
- package/dist/evaluate-inline.js.map +1 -0
- package/dist/host.d.ts +97 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +115 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +254 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +55 -0
- package/dist/messages.js.map +1 -0
- package/dist/render-worker.d.ts +77 -0
- package/dist/render-worker.d.ts.map +1 -0
- package/dist/render-worker.js +396 -0
- package/dist/render-worker.js.map +1 -0
- package/dist/spsc-ring.d.ts +171 -0
- package/dist/spsc-ring.d.ts.map +1 -0
- package/dist/spsc-ring.js +240 -0
- package/dist/spsc-ring.js.map +1 -0
- package/package.json +51 -0
- package/src/compositor-script.ts +374 -0
- package/src/compositor-startup.ts +666 -0
- package/src/compositor-types.ts +175 -0
- package/src/compositor-worker.ts +613 -0
- package/src/evaluate-inline.ts +42 -0
- package/src/host.ts +189 -0
- package/src/index.ts +49 -0
- package/src/messages.ts +343 -0
- package/src/render-worker.ts +454 -0
- package/src/spsc-ring.ts +309 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RenderWorker -- off-main-thread video renderer using OffscreenCanvas.
|
|
3
|
+
*
|
|
4
|
+
* The render worker:
|
|
5
|
+
* 1. Receives an OffscreenCanvas via postMessage transfer
|
|
6
|
+
* 2. On `start-render`, iterates frames at the configured fps
|
|
7
|
+
* 3. For each frame, computes state (simplified inline compositor)
|
|
8
|
+
* and draws to the OffscreenCanvas 2d context
|
|
9
|
+
* 4. Posts `frame` messages back with VideoFrameOutput data
|
|
10
|
+
* 5. Posts `render-complete` when done
|
|
11
|
+
*
|
|
12
|
+
* The worker script is inlined as a Blob URL (no separate file needed).
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { type VideoConfig, type VideoFrameOutput } from '@czap/core';
|
|
17
|
+
/**
|
|
18
|
+
* Host-facing surface of a render worker. Owns the underlying `Worker`
|
|
19
|
+
* and `OffscreenCanvas` once transferred; created by
|
|
20
|
+
* {@link RenderWorker.create}.
|
|
21
|
+
*/
|
|
22
|
+
export interface RenderWorkerShape {
|
|
23
|
+
/** The underlying Worker instance. */
|
|
24
|
+
readonly worker: Worker;
|
|
25
|
+
/**
|
|
26
|
+
* Transfer an OffscreenCanvas to the worker.
|
|
27
|
+
* The canvas must have been obtained via `canvas.transferControlToOffscreen()`.
|
|
28
|
+
*/
|
|
29
|
+
transferCanvas(canvas: OffscreenCanvas): void;
|
|
30
|
+
/** Start rendering frames with the given video configuration. */
|
|
31
|
+
startRender(config: VideoConfig): void;
|
|
32
|
+
/** Stop an in-progress render. */
|
|
33
|
+
stopRender(): void;
|
|
34
|
+
/** Subscribe to per-frame output. Returns an unsubscribe function. */
|
|
35
|
+
onFrame(callback: (output: VideoFrameOutput) => void): () => void;
|
|
36
|
+
/** Subscribe to render completion. Returns an unsubscribe function. */
|
|
37
|
+
onComplete(callback: (totalFrames: number) => void): () => void;
|
|
38
|
+
/** Terminate the worker and clean up resources. */
|
|
39
|
+
dispose(): void;
|
|
40
|
+
}
|
|
41
|
+
declare function _createRenderWorker(): RenderWorkerShape;
|
|
42
|
+
/**
|
|
43
|
+
* Factory namespace for the render worker.
|
|
44
|
+
*
|
|
45
|
+
* Call {@link RenderWorker.create} on the main thread to mint a worker
|
|
46
|
+
* that owns an `OffscreenCanvas` and renders `VideoFrameOutput` frames
|
|
47
|
+
* off the main thread. Transfer control via
|
|
48
|
+
* {@link RenderWorkerShape.transferCanvas} before calling `startRender`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { RenderWorker } from '@czap/worker';
|
|
53
|
+
*
|
|
54
|
+
* const renderer = RenderWorker.create();
|
|
55
|
+
* const offscreen = canvas.transferControlToOffscreen();
|
|
56
|
+
* renderer.transferCanvas(offscreen);
|
|
57
|
+
* renderer.onFrame((frame) => {
|
|
58
|
+
* // stream frame.image / frame.timestampMs somewhere
|
|
59
|
+
* });
|
|
60
|
+
* renderer.startRender({ durationMs: 4000, fps: 30, width: 640, height: 360 });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare const RenderWorker: {
|
|
64
|
+
/**
|
|
65
|
+
* Spin up a render worker. The worker starts idle; transfer an
|
|
66
|
+
* `OffscreenCanvas` via
|
|
67
|
+
* {@link RenderWorkerShape.transferCanvas} before calling
|
|
68
|
+
* `startRender`.
|
|
69
|
+
*/
|
|
70
|
+
readonly create: typeof _createRenderWorker;
|
|
71
|
+
};
|
|
72
|
+
export declare namespace RenderWorker {
|
|
73
|
+
/** Public host-side surface returned by {@link RenderWorker.create}. */
|
|
74
|
+
type Shape = RenderWorkerShape;
|
|
75
|
+
}
|
|
76
|
+
export {};
|
|
77
|
+
//# sourceMappingURL=render-worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-worker.d.ts","sourceRoot":"","sources":["../src/render-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAe,KAAK,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAQlF;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAExB;;;OAGG;IACH,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAE9C,iEAAiE;IACjE,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAEvC,kCAAkC;IAClC,UAAU,IAAI,IAAI,CAAC;IAEnB,sEAAsE;IACtE,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAElE,uEAAuE;IACvE,UAAU,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAEhE,mDAAmD;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB;AAqRD,iBAAS,mBAAmB,IAAI,iBAAiB,CAmFhD;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,YAAY;IACvB;;;;;OAKG;;CAEK,CAAC;AAEX,MAAM,CAAC,OAAO,WAAW,YAAY,CAAC;IACpC,wEAAwE;IACxE,KAAY,KAAK,GAAG,iBAAiB,CAAC;CACvC"}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RenderWorker -- off-main-thread video renderer using OffscreenCanvas.
|
|
3
|
+
*
|
|
4
|
+
* The render worker:
|
|
5
|
+
* 1. Receives an OffscreenCanvas via postMessage transfer
|
|
6
|
+
* 2. On `start-render`, iterates frames at the configured fps
|
|
7
|
+
* 3. For each frame, computes state (simplified inline compositor)
|
|
8
|
+
* and draws to the OffscreenCanvas 2d context
|
|
9
|
+
* 4. Posts `frame` messages back with VideoFrameOutput data
|
|
10
|
+
* 5. Posts `render-complete` when done
|
|
11
|
+
*
|
|
12
|
+
* The worker script is inlined as a Blob URL (no separate file needed).
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import { Diagnostics } from '@czap/core';
|
|
17
|
+
import { EVALUATE_THRESHOLDS_SOURCE } from './evaluate-inline.js';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Inline worker script
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/**
|
|
22
|
+
* Self-contained render worker script.
|
|
23
|
+
*
|
|
24
|
+
* Contains a minimal compositor and frame iterator that draws state
|
|
25
|
+
* visualization to an OffscreenCanvas.
|
|
26
|
+
*/
|
|
27
|
+
const RENDER_WORKER_SCRIPT = /* js */ `
|
|
28
|
+
"use strict";
|
|
29
|
+
|
|
30
|
+
/** @type {OffscreenCanvas | null} */
|
|
31
|
+
let canvas = null;
|
|
32
|
+
|
|
33
|
+
/** @type {OffscreenCanvasRenderingContext2D | null} */
|
|
34
|
+
let ctx = null;
|
|
35
|
+
|
|
36
|
+
/** @type {boolean} */
|
|
37
|
+
let rendering = false;
|
|
38
|
+
|
|
39
|
+
/** @type {boolean} */
|
|
40
|
+
let stopRequested = false;
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Simplified inline compositor (mirrors compositor-worker / Boundary.evaluate)
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** @type {Map<string, { id: string; states: string[]; thresholds: number[]; currentState: string }>} */
|
|
47
|
+
const quantizers = new Map();
|
|
48
|
+
|
|
49
|
+
/** @type {Map<string, Record<string, number>>} */
|
|
50
|
+
const blendOverrides = new Map();
|
|
51
|
+
|
|
52
|
+
${EVALUATE_THRESHOLDS_SOURCE}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Compute a CompositeState from the current quantizer state.
|
|
56
|
+
*/
|
|
57
|
+
function computeState() {
|
|
58
|
+
const discrete = {};
|
|
59
|
+
const blend = {};
|
|
60
|
+
const css = {};
|
|
61
|
+
const glsl = {};
|
|
62
|
+
const aria = {};
|
|
63
|
+
|
|
64
|
+
for (const [name, q] of quantizers) {
|
|
65
|
+
const stateStr = q.currentState;
|
|
66
|
+
discrete[name] = stateStr;
|
|
67
|
+
|
|
68
|
+
const override = blendOverrides.get(name);
|
|
69
|
+
if (override !== undefined) {
|
|
70
|
+
blend[name] = override;
|
|
71
|
+
} else {
|
|
72
|
+
const weights = {};
|
|
73
|
+
for (const s of q.states) {
|
|
74
|
+
weights[s] = s === stateStr ? 1 : 0;
|
|
75
|
+
}
|
|
76
|
+
blend[name] = weights;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
css["--czap-" + name] = stateStr;
|
|
80
|
+
|
|
81
|
+
let stateIndex = 0;
|
|
82
|
+
for (let i = 0; i < q.states.length; i++) {
|
|
83
|
+
if (q.states[i] === stateStr) {
|
|
84
|
+
stateIndex = i;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
glsl["u_" + name] = stateIndex;
|
|
89
|
+
aria["data-czap-" + name] = stateStr;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { discrete, blend, outputs: { css, glsl, aria } };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Canvas rendering
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Draw the current CompositeState to the OffscreenCanvas.
|
|
101
|
+
* This is a diagnostic visualization; real applications would
|
|
102
|
+
* implement domain-specific rendering.
|
|
103
|
+
*
|
|
104
|
+
* @param {{ discrete: Record<string, string>; blend: Record<string, Record<string, number>>; outputs: { css: Record<string, number|string>; glsl: Record<string, number>; aria: Record<string, string> } }} state
|
|
105
|
+
* @param {number} frame
|
|
106
|
+
* @param {number} progress
|
|
107
|
+
*/
|
|
108
|
+
function drawState(state, frame, progress) {
|
|
109
|
+
if (!ctx || !canvas) return;
|
|
110
|
+
|
|
111
|
+
const w = canvas.width;
|
|
112
|
+
const h = canvas.height;
|
|
113
|
+
|
|
114
|
+
// Clear
|
|
115
|
+
ctx.clearRect(0, 0, w, h);
|
|
116
|
+
|
|
117
|
+
// Background: gradient based on progress
|
|
118
|
+
const gray = Math.round(32 + progress * 32);
|
|
119
|
+
ctx.fillStyle = "rgb(" + gray + "," + gray + "," + gray + ")";
|
|
120
|
+
ctx.fillRect(0, 0, w, h);
|
|
121
|
+
|
|
122
|
+
// Draw discrete state labels
|
|
123
|
+
ctx.fillStyle = "#ffffff";
|
|
124
|
+
ctx.font = "14px monospace";
|
|
125
|
+
ctx.textBaseline = "top";
|
|
126
|
+
|
|
127
|
+
let y = 16;
|
|
128
|
+
const keys = Object.keys(state.discrete);
|
|
129
|
+
for (let i = 0; i < keys.length; i++) {
|
|
130
|
+
const name = keys[i];
|
|
131
|
+
const value = state.discrete[name];
|
|
132
|
+
ctx.fillText(name + ": " + value, 16, y);
|
|
133
|
+
y += 20;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Draw progress bar
|
|
137
|
+
const barY = h - 24;
|
|
138
|
+
const barH = 8;
|
|
139
|
+
ctx.fillStyle = "#333333";
|
|
140
|
+
ctx.fillRect(16, barY, w - 32, barH);
|
|
141
|
+
ctx.fillStyle = "#4488ff";
|
|
142
|
+
ctx.fillRect(16, barY, (w - 32) * progress, barH);
|
|
143
|
+
|
|
144
|
+
// Frame counter
|
|
145
|
+
ctx.fillStyle = "#aaaaaa";
|
|
146
|
+
ctx.font = "12px monospace";
|
|
147
|
+
ctx.textBaseline = "bottom";
|
|
148
|
+
ctx.fillText("frame " + frame, 16, barY - 4);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Render loop
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Run the fixed-step render loop.
|
|
157
|
+
* @param {{ fps: number; width: number; height: number; durationMs: number }} config
|
|
158
|
+
*/
|
|
159
|
+
async function runRender(config) {
|
|
160
|
+
if (rendering) return;
|
|
161
|
+
rendering = true;
|
|
162
|
+
stopRequested = false;
|
|
163
|
+
|
|
164
|
+
const totalFrames = Math.ceil((config.durationMs / 1000) * config.fps);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
for (let i = 0; i < totalFrames; i++) {
|
|
168
|
+
if (stopRequested) break;
|
|
169
|
+
|
|
170
|
+
const timestamp = (i * 1000) / config.fps;
|
|
171
|
+
const progress = totalFrames > 1 ? i / (totalFrames - 1) : 1;
|
|
172
|
+
const state = computeState();
|
|
173
|
+
|
|
174
|
+
// Draw to canvas
|
|
175
|
+
drawState(state, i, progress);
|
|
176
|
+
|
|
177
|
+
/** @type {import('./messages.js').VideoFrameOutput} */
|
|
178
|
+
const output = { frame: i, timestamp, progress, state };
|
|
179
|
+
|
|
180
|
+
self.postMessage({ type: "frame", output: output });
|
|
181
|
+
|
|
182
|
+
// Yield to allow stop messages to be processed.
|
|
183
|
+
// In a real scenario, the frame rate would be controlled by
|
|
184
|
+
// the encoding pipeline; here we use a minimal yield.
|
|
185
|
+
if (i % 10 === 9) {
|
|
186
|
+
await new Promise(function (r) { setTimeout(r, 0); });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
self.postMessage({ type: "render-complete", totalFrames: totalFrames });
|
|
191
|
+
} catch (err) {
|
|
192
|
+
self.postMessage({
|
|
193
|
+
type: "error",
|
|
194
|
+
message: err instanceof Error ? err.message : String(err),
|
|
195
|
+
});
|
|
196
|
+
} finally {
|
|
197
|
+
rendering = false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Message handler
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
self.addEventListener("message", function (e) {
|
|
206
|
+
const msg = e.data;
|
|
207
|
+
if (!msg || typeof msg.type !== "string") return;
|
|
208
|
+
|
|
209
|
+
switch (msg.type) {
|
|
210
|
+
case "init": {
|
|
211
|
+
quantizers.clear();
|
|
212
|
+
blendOverrides.clear();
|
|
213
|
+
self.postMessage({ type: "ready" });
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
case "transfer-canvas": {
|
|
218
|
+
canvas = msg.canvas;
|
|
219
|
+
ctx = canvas.getContext("2d");
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "add-quantizer": {
|
|
224
|
+
const initialState = msg.states[0] || "";
|
|
225
|
+
quantizers.set(msg.name, {
|
|
226
|
+
id: msg.boundaryId,
|
|
227
|
+
states: Array.from(msg.states),
|
|
228
|
+
thresholds: Array.from(msg.thresholds),
|
|
229
|
+
currentState: initialState,
|
|
230
|
+
});
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "remove-quantizer": {
|
|
235
|
+
quantizers.delete(msg.name);
|
|
236
|
+
blendOverrides.delete(msg.name);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case "evaluate": {
|
|
241
|
+
const q = quantizers.get(msg.name);
|
|
242
|
+
if (q) {
|
|
243
|
+
q.currentState = evaluateThresholds(q.thresholds, q.states, msg.value);
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case "set-blend": {
|
|
249
|
+
blendOverrides.set(msg.name, msg.weights);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case "start-render": {
|
|
254
|
+
runRender(msg.config);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case "stop-render": {
|
|
259
|
+
stopRequested = true;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case "dispose": {
|
|
264
|
+
stopRequested = true;
|
|
265
|
+
quantizers.clear();
|
|
266
|
+
blendOverrides.clear();
|
|
267
|
+
canvas = null;
|
|
268
|
+
ctx = null;
|
|
269
|
+
self.close();
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
`;
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Factory helpers
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
function _send(worker, msg, transfer) {
|
|
279
|
+
if (transfer && transfer.length > 0) {
|
|
280
|
+
worker.postMessage(msg, transfer);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
worker.postMessage(msg);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
// Factory
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
function _createRenderWorker() {
|
|
290
|
+
const blob = new Blob([RENDER_WORKER_SCRIPT], { type: 'application/javascript' });
|
|
291
|
+
const url = URL.createObjectURL(blob);
|
|
292
|
+
const worker = new Worker(url, { type: 'classic', name: 'czap-renderer' });
|
|
293
|
+
URL.revokeObjectURL(url);
|
|
294
|
+
const frameListeners = new Set();
|
|
295
|
+
const completeListeners = new Set();
|
|
296
|
+
worker.addEventListener('message', (e) => {
|
|
297
|
+
const msg = e.data;
|
|
298
|
+
if (!msg || typeof msg.type !== 'string')
|
|
299
|
+
return;
|
|
300
|
+
switch (msg.type) {
|
|
301
|
+
case 'frame':
|
|
302
|
+
for (const cb of frameListeners)
|
|
303
|
+
cb(msg.output);
|
|
304
|
+
break;
|
|
305
|
+
case 'render-complete':
|
|
306
|
+
for (const cb of completeListeners)
|
|
307
|
+
cb(msg.totalFrames);
|
|
308
|
+
break;
|
|
309
|
+
case 'error':
|
|
310
|
+
Diagnostics.error({
|
|
311
|
+
source: 'czap/worker.render-worker',
|
|
312
|
+
code: 'worker-message-error',
|
|
313
|
+
message: 'Render worker reported an error.',
|
|
314
|
+
detail: msg.message,
|
|
315
|
+
});
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
worker.addEventListener('error', (e) => {
|
|
320
|
+
Diagnostics.error({
|
|
321
|
+
source: 'czap/worker.render-worker',
|
|
322
|
+
code: 'worker-unhandled-error',
|
|
323
|
+
message: 'Render worker raised an unhandled error.',
|
|
324
|
+
detail: e.message,
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
// Initialize
|
|
328
|
+
_send(worker, { type: 'init' });
|
|
329
|
+
return {
|
|
330
|
+
get worker() {
|
|
331
|
+
return worker;
|
|
332
|
+
},
|
|
333
|
+
transferCanvas(canvas) {
|
|
334
|
+
// The canvas is Transferable -- it must be in the transfer list
|
|
335
|
+
_send(worker, { type: 'transfer-canvas', canvas }, [canvas]);
|
|
336
|
+
},
|
|
337
|
+
startRender(config) {
|
|
338
|
+
_send(worker, { type: 'start-render', config });
|
|
339
|
+
},
|
|
340
|
+
stopRender() {
|
|
341
|
+
_send(worker, { type: 'stop-render' });
|
|
342
|
+
},
|
|
343
|
+
onFrame(callback) {
|
|
344
|
+
frameListeners.add(callback);
|
|
345
|
+
return () => {
|
|
346
|
+
frameListeners.delete(callback);
|
|
347
|
+
};
|
|
348
|
+
},
|
|
349
|
+
onComplete(callback) {
|
|
350
|
+
completeListeners.add(callback);
|
|
351
|
+
return () => {
|
|
352
|
+
completeListeners.delete(callback);
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
dispose() {
|
|
356
|
+
_send(worker, { type: 'dispose' });
|
|
357
|
+
frameListeners.clear();
|
|
358
|
+
completeListeners.clear();
|
|
359
|
+
worker.terminate();
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// Export
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
/**
|
|
367
|
+
* Factory namespace for the render worker.
|
|
368
|
+
*
|
|
369
|
+
* Call {@link RenderWorker.create} on the main thread to mint a worker
|
|
370
|
+
* that owns an `OffscreenCanvas` and renders `VideoFrameOutput` frames
|
|
371
|
+
* off the main thread. Transfer control via
|
|
372
|
+
* {@link RenderWorkerShape.transferCanvas} before calling `startRender`.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* import { RenderWorker } from '@czap/worker';
|
|
377
|
+
*
|
|
378
|
+
* const renderer = RenderWorker.create();
|
|
379
|
+
* const offscreen = canvas.transferControlToOffscreen();
|
|
380
|
+
* renderer.transferCanvas(offscreen);
|
|
381
|
+
* renderer.onFrame((frame) => {
|
|
382
|
+
* // stream frame.image / frame.timestampMs somewhere
|
|
383
|
+
* });
|
|
384
|
+
* renderer.startRender({ durationMs: 4000, fps: 30, width: 640, height: 360 });
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
export const RenderWorker = {
|
|
388
|
+
/**
|
|
389
|
+
* Spin up a render worker. The worker starts idle; transfer an
|
|
390
|
+
* `OffscreenCanvas` via
|
|
391
|
+
* {@link RenderWorkerShape.transferCanvas} before calling
|
|
392
|
+
* `startRender`.
|
|
393
|
+
*/
|
|
394
|
+
create: _createRenderWorker,
|
|
395
|
+
};
|
|
396
|
+
//# sourceMappingURL=render-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-worker.js","sourceRoot":"","sources":["../src/render-worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPSCRing -- lock-free single-producer single-consumer ring buffer
|
|
3
|
+
* backed by SharedArrayBuffer.
|
|
4
|
+
*
|
|
5
|
+
* Designed for real-time compositor state streaming between a Worker
|
|
6
|
+
* (producer) and the main thread (consumer) without blocking either side.
|
|
7
|
+
*
|
|
8
|
+
* ## SharedArrayBuffer requirements
|
|
9
|
+
*
|
|
10
|
+
* SharedArrayBuffer requires the page to be served with the following
|
|
11
|
+
* HTTP headers (COOP/COEP):
|
|
12
|
+
*
|
|
13
|
+
* Cross-Origin-Opener-Policy: same-origin
|
|
14
|
+
* Cross-Origin-Embedder-Policy: require-corp
|
|
15
|
+
*
|
|
16
|
+
* Without these headers, `new SharedArrayBuffer(...)` will throw.
|
|
17
|
+
*
|
|
18
|
+
* ## Memory layout
|
|
19
|
+
*
|
|
20
|
+
* ```
|
|
21
|
+
* Int32Array view (control region):
|
|
22
|
+
* [0]: write cursor (atomically incremented by producer)
|
|
23
|
+
* [1]: read cursor (atomically incremented by consumer)
|
|
24
|
+
*
|
|
25
|
+
* Float64Array view (data region):
|
|
26
|
+
* Offset = 8 bytes (aligned after two Int32 control slots)
|
|
27
|
+
* [0 .. slotCount * slotSize - 1]: ring buffer data slots
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* The producer writes at `writeCursor % slotCount`, the consumer reads
|
|
31
|
+
* at `readCursor % slotCount`. The buffer is full when
|
|
32
|
+
* `write - read === slotCount`, empty when `write === read`.
|
|
33
|
+
*
|
|
34
|
+
* Only `Atomics.load` and `Atomics.store` are used -- no `Atomics.wait`
|
|
35
|
+
* or `Atomics.notify` -- keeping this fully lock-free and non-blocking.
|
|
36
|
+
*
|
|
37
|
+
* @module
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Producer- or consumer-side handle to a single-producer/single-consumer
|
|
41
|
+
* ring buffer backed by `SharedArrayBuffer`. Created by
|
|
42
|
+
* {@link SPSCRing.attachProducer} or {@link SPSCRing.attachConsumer}.
|
|
43
|
+
*/
|
|
44
|
+
export interface SPSCRingBufferShape {
|
|
45
|
+
/**
|
|
46
|
+
* Push a data slot into the ring buffer.
|
|
47
|
+
* Returns `false` if the buffer is full (non-blocking).
|
|
48
|
+
*/
|
|
49
|
+
push(data: Float64Array): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Pop a data slot from the ring buffer into the provided output array.
|
|
52
|
+
* Returns `false` if the buffer is empty (non-blocking).
|
|
53
|
+
*/
|
|
54
|
+
pop(out: Float64Array): boolean;
|
|
55
|
+
/** Number of slots in the ring buffer. */
|
|
56
|
+
readonly capacity: number;
|
|
57
|
+
/** Current number of occupied slots. */
|
|
58
|
+
readonly count: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Create a matched producer/consumer pair sharing the same SharedArrayBuffer.
|
|
62
|
+
*
|
|
63
|
+
* Typically called on the main thread; the `buffer` (SharedArrayBuffer) is
|
|
64
|
+
* then transferred to the Worker via `postMessage`, and the Worker calls
|
|
65
|
+
* `SPSCRing.attachProducer` to get its side of the ring.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { SPSCRing } from '@czap/worker';
|
|
70
|
+
*
|
|
71
|
+
* const { buffer, producer, consumer } = SPSCRing.createPair(64, 4);
|
|
72
|
+
* // producer.push(new Float64Array([1, 2, 3, 4])); // true
|
|
73
|
+
* // consumer.pop(new Float64Array(4)); // true
|
|
74
|
+
* // Transfer buffer to a Worker via postMessage
|
|
75
|
+
* worker.postMessage({ buffer, slotCount: 64, slotSize: 4 });
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @param slotCount - Number of slots in the ring (power of 2 recommended)
|
|
79
|
+
* @param slotSize - Number of Float64 values per slot
|
|
80
|
+
* @returns An object with the shared buffer and producer/consumer ring handles
|
|
81
|
+
*/
|
|
82
|
+
declare function _createPair(slotCount: number, slotSize: number): {
|
|
83
|
+
buffer: SharedArrayBuffer;
|
|
84
|
+
producer: SPSCRingBufferShape;
|
|
85
|
+
consumer: SPSCRingBufferShape;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Attach as producer to an existing SharedArrayBuffer.
|
|
89
|
+
* Call this inside the Worker that produces data.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* import { SPSCRing } from '@czap/worker';
|
|
94
|
+
*
|
|
95
|
+
* // Inside a Worker's message handler:
|
|
96
|
+
* self.onmessage = (e) => {
|
|
97
|
+
* const { buffer, slotCount, slotSize } = e.data;
|
|
98
|
+
* const producer = SPSCRing.attachProducer(buffer, slotCount, slotSize);
|
|
99
|
+
* const data = new Float64Array([1.0, 2.0, 3.0, 4.0]);
|
|
100
|
+
* producer.push(data); // true if buffer not full
|
|
101
|
+
* };
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @param sab - The SharedArrayBuffer from the main thread
|
|
105
|
+
* @param slotCount - Number of slots (must match createPair)
|
|
106
|
+
* @param slotSize - Float64 values per slot (must match createPair)
|
|
107
|
+
* @returns A producer-side {@link SPSCRingBufferShape}
|
|
108
|
+
*/
|
|
109
|
+
declare function _attachProducer(sab: SharedArrayBuffer, slotCount: number, slotSize: number): SPSCRingBufferShape;
|
|
110
|
+
/**
|
|
111
|
+
* Attach as consumer to an existing SharedArrayBuffer.
|
|
112
|
+
* Call this on the main thread that consumes data.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { SPSCRing } from '@czap/worker';
|
|
117
|
+
*
|
|
118
|
+
* // On the main thread after receiving buffer from Worker:
|
|
119
|
+
* const consumer = SPSCRing.attachConsumer(sharedBuffer, 64, 4);
|
|
120
|
+
* const out = new Float64Array(4);
|
|
121
|
+
* if (consumer.pop(out)) {
|
|
122
|
+
* console.log('Received:', out); // Float64Array [1.0, 2.0, 3.0, 4.0]
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @param sab - The SharedArrayBuffer shared with the producer
|
|
127
|
+
* @param slotCount - Number of slots (must match createPair)
|
|
128
|
+
* @param slotSize - Float64 values per slot (must match createPair)
|
|
129
|
+
* @returns A consumer-side {@link SPSCRingBufferShape}
|
|
130
|
+
*/
|
|
131
|
+
declare function _attachConsumer(sab: SharedArrayBuffer, slotCount: number, slotSize: number): SPSCRingBufferShape;
|
|
132
|
+
/**
|
|
133
|
+
* SPSC ring buffer namespace.
|
|
134
|
+
*
|
|
135
|
+
* Lock-free single-producer single-consumer ring buffer backed by
|
|
136
|
+
* `SharedArrayBuffer`. Designed for real-time compositor state streaming
|
|
137
|
+
* between a Worker (producer) and the main thread (consumer) without
|
|
138
|
+
* blocking either side. Uses only `Atomics.load`/`Atomics.store` --
|
|
139
|
+
* fully non-blocking.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```ts
|
|
143
|
+
* import { SPSCRing } from '@czap/worker';
|
|
144
|
+
*
|
|
145
|
+
* // Main thread: create pair and send buffer to Worker
|
|
146
|
+
* const { buffer, producer, consumer } = SPSCRing.createPair(128, 8);
|
|
147
|
+
* worker.postMessage({ buffer, slotCount: 128, slotSize: 8 });
|
|
148
|
+
*
|
|
149
|
+
* // In Worker: attach as producer
|
|
150
|
+
* // const producer = SPSCRing.attachProducer(buffer, 128, 8);
|
|
151
|
+
* // producer.push(new Float64Array(8));
|
|
152
|
+
*
|
|
153
|
+
* // Main thread: consume in animation loop
|
|
154
|
+
* const out = new Float64Array(8);
|
|
155
|
+
* function frame() {
|
|
156
|
+
* while (consumer.pop(out)) { /* process out *\/ }
|
|
157
|
+
* requestAnimationFrame(frame);
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export declare const SPSCRing: {
|
|
162
|
+
readonly createPair: typeof _createPair;
|
|
163
|
+
readonly attachProducer: typeof _attachProducer;
|
|
164
|
+
readonly attachConsumer: typeof _attachConsumer;
|
|
165
|
+
};
|
|
166
|
+
export declare namespace SPSCRing {
|
|
167
|
+
/** Producer- or consumer-facing view of a SPSC ring buffer. */
|
|
168
|
+
type Shape = SPSCRingBufferShape;
|
|
169
|
+
}
|
|
170
|
+
export {};
|
|
171
|
+
//# sourceMappingURL=spsc-ring.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spsc-ring.d.ts","sourceRoot":"","sources":["../src/spsc-ring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAoBH;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,IAAI,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC;IAElC;;;OAGG;IACH,GAAG,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC;IAEhC,0CAA0C;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAiGD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,iBAAS,WAAW,CAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf;IACD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,mBAAmB,CAAC;CAC/B,CAOA;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,iBAAS,eAAe,CAAC,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAEzG;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,iBAAS,eAAe,CAAC,GAAG,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAEzG;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,QAAQ;;;;CAIX,CAAC;AAEX,MAAM,CAAC,OAAO,WAAW,QAAQ,CAAC;IAChC,+DAA+D;IAC/D,KAAY,KAAK,GAAG,mBAAmB,CAAC;CACzC"}
|