pa_encoder 0.2.8 → 0.3.2

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.
@@ -16,6 +16,47 @@ function post(type, data = {}) {
16
16
  } catch {}
17
17
  }
18
18
 
19
+ function toPositiveInt(v, fallback) {
20
+ const n = Number(v);
21
+ if (!Number.isInteger(n) || n <= 0) return fallback;
22
+ return n;
23
+ }
24
+
25
+ function createThrottledPoster(type, intervalMs = 160) {
26
+ let latest = null;
27
+ const timer = setInterval(() => {
28
+ if (!latest) return;
29
+ post(type, latest);
30
+ latest = null;
31
+ }, Math.max(50, intervalMs));
32
+
33
+ return {
34
+ push(data) {
35
+ latest = data;
36
+ },
37
+ flush() {
38
+ if (!latest) return;
39
+ post(type, latest);
40
+ latest = null;
41
+ },
42
+ stop() {
43
+ clearInterval(timer);
44
+ latest = null;
45
+ },
46
+ };
47
+ }
48
+
49
+ function throttle(fn, waitMs = 120) {
50
+ let timeoutId = null;
51
+ return (...args) => {
52
+ if (timeoutId) return;
53
+ timeoutId = setTimeout(() => {
54
+ timeoutId = null;
55
+ fn(...args);
56
+ }, waitMs);
57
+ };
58
+ }
59
+
19
60
  async function importEntry(entry) {
20
61
  await import(entry);
21
62
  }
@@ -49,6 +90,76 @@ function sendCanvasList() {
49
90
  post("pa_preview_canvas_list", { count: canvases.length, canvases });
50
91
  }
51
92
 
93
+ function findCanvasDeep({
94
+ selector = "canvas",
95
+ includeShadow = true,
96
+ maxIframeDepth = 8,
97
+ doc = document,
98
+ _depth = 0,
99
+ } = {}) {
100
+ if (!doc || _depth > maxIframeDepth) return null;
101
+
102
+ try {
103
+ const el = doc.querySelector(selector);
104
+ if (el instanceof HTMLCanvasElement) return el;
105
+ } catch {}
106
+
107
+ if (includeShadow) {
108
+ let all = [];
109
+ try {
110
+ all = doc.querySelectorAll("*");
111
+ } catch {
112
+ all = [];
113
+ }
114
+ for (const host of all) {
115
+ const sr = host.shadowRoot;
116
+ if (!sr) continue;
117
+ try {
118
+ const el = sr.querySelector(selector);
119
+ if (el instanceof HTMLCanvasElement) return el;
120
+ } catch {}
121
+ try {
122
+ const fallback = sr.querySelector("canvas");
123
+ if (fallback instanceof HTMLCanvasElement) return fallback;
124
+ } catch {}
125
+ }
126
+ }
127
+
128
+ let iframes = [];
129
+ try {
130
+ iframes = doc.querySelectorAll("iframe");
131
+ } catch {
132
+ iframes = [];
133
+ }
134
+ for (const iframe of iframes) {
135
+ let childDoc = null;
136
+ try {
137
+ childDoc = iframe.contentDocument;
138
+ } catch {
139
+ childDoc = null;
140
+ }
141
+ if (!childDoc) continue;
142
+ const found = findCanvasDeep({
143
+ selector,
144
+ includeShadow,
145
+ maxIframeDepth,
146
+ doc: childDoc,
147
+ _depth: _depth + 1,
148
+ });
149
+ if (found) return found;
150
+ }
151
+
152
+ return null;
153
+ }
154
+
155
+ function resolveCanvas(selector = "canvas") {
156
+ try {
157
+ const direct = document.querySelector(selector);
158
+ if (direct instanceof HTMLCanvasElement) return direct;
159
+ } catch {}
160
+ return findCanvasDeep({ selector }) || findCanvasDeep({ selector: "canvas" });
161
+ }
162
+
52
163
  function canvasToBlob(canvas, type = "image/png") {
53
164
  return new Promise((resolve, reject) => {
54
165
  canvas.toBlob((b) => {
@@ -103,7 +214,7 @@ window.addEventListener("message", async (ev) => {
103
214
 
104
215
  if (msg.type === "pa_focus_canvas") {
105
216
  const sel = msg.payload?.canvasSelector || "canvas";
106
- const c = document.querySelector(sel) || document.querySelector("canvas");
217
+ const c = resolveCanvas(sel);
107
218
  if (c instanceof HTMLCanvasElement) focusCanvasBestEffort(c);
108
219
  return;
109
220
  }
@@ -136,6 +247,14 @@ async function runFrameAutostart(payload, entry) {
136
247
  let exporter = null;
137
248
  let didFinalize = false;
138
249
  let firstCanvasFocused = false;
250
+ const progressPoster = createThrottledPoster(
251
+ "pa_progress",
252
+ toPositiveInt(p?.statsIntervalMs, 120)
253
+ );
254
+ const statsPoster = createThrottledPoster(
255
+ "pa_stats",
256
+ toPositiveInt(p?.statsIntervalMs, 120)
257
+ );
139
258
 
140
259
  try {
141
260
  exporter = await createExporterFromPayload(p);
@@ -163,9 +282,7 @@ async function runFrameAutostart(payload, entry) {
163
282
  start: async () => {
164
283
  await importEntry(entry);
165
284
  // after import, try focus if canvas already exists
166
- const c =
167
- document.querySelector(canvasSelector) ||
168
- document.querySelector("canvas");
285
+ const c = resolveCanvas(canvasSelector);
169
286
  if (c instanceof HTMLCanvasElement) {
170
287
  focusCanvasBestEffort(c);
171
288
  firstCanvasFocused = true;
@@ -186,8 +303,8 @@ async function runFrameAutostart(payload, entry) {
186
303
  await exporter.write(frameIndex, blob);
187
304
 
188
305
  written++;
189
- post("pa_progress", { done: written, total: frames });
190
- post("pa_stats", { written, captured: i + 1 });
306
+ progressPoster.push({ done: written, total: frames });
307
+ statsPoster.push({ written, captured: i + 1 });
191
308
  },
192
309
  });
193
310
  } catch (e) {
@@ -202,6 +319,8 @@ async function runFrameAutostart(payload, entry) {
202
319
  await exporter.finalize();
203
320
  didFinalize = true;
204
321
  }
322
+ progressPoster.flush();
323
+ statsPoster.flush();
205
324
 
206
325
  post("pa_status", {
207
326
  status: "idle",
@@ -216,6 +335,8 @@ async function runFrameAutostart(payload, entry) {
216
335
  await exporter.finalize();
217
336
  } catch {}
218
337
  }
338
+ progressPoster.stop();
339
+ statsPoster.stop();
219
340
  running = false;
220
341
  stopFlag = false;
221
342
  }
@@ -230,13 +351,15 @@ async function runLive(p) {
230
351
 
231
352
  let exporter = null;
232
353
  let didFinalize = false;
354
+ const statsPoster = createThrottledPoster(
355
+ "pa_stats",
356
+ toPositiveInt(p?.statsIntervalMs, 180)
357
+ );
233
358
 
234
359
  try {
235
360
  exporter = await createExporterFromPayload(p);
236
361
 
237
- const canvas =
238
- document.querySelector(canvasSelector) ||
239
- document.querySelector("canvas");
362
+ const canvas = resolveCanvas(canvasSelector);
240
363
  if (!(canvas instanceof HTMLCanvasElement)) {
241
364
  throw new Error(`Canvas not found: ${canvasSelector}`);
242
365
  }
@@ -250,28 +373,44 @@ async function runLive(p) {
250
373
  }, 50);
251
374
 
252
375
  try {
253
- const { stop } = await startLiveCapture({
376
+ const session = await startLiveCapture({
254
377
  canvas,
255
378
  exporter,
256
379
  fps: Number(p.fps ?? 30),
257
380
  concurrency: Number(p.concurrency ?? 2),
258
381
  maxQueue: Number(p.maxQueue ?? 8),
382
+ maxPendingCaptures: toPositiveInt(p.maxPendingCaptures, undefined),
259
383
  policy: p.policy ?? "drop",
260
384
  signal: ac.signal,
261
- onProgress: (stats) => post("pa_stats", stats),
385
+ onProgress: (stats) => statsPoster.push(stats),
262
386
  });
263
387
 
264
388
  post("pa_status", { status: "running", message: "capturing..." });
265
389
 
266
- await new Promise((_, reject) => {
267
- ac.signal.addEventListener("abort", () => reject(ac.signal.reason));
268
- }).catch(async (e) => {
269
- await stop();
390
+ const result = await Promise.race([
391
+ new Promise((resolve) => {
392
+ ac.signal.addEventListener(
393
+ "abort",
394
+ () => resolve({ kind: "abort", reason: ac.signal.reason }),
395
+ { once: true }
396
+ );
397
+ }),
398
+ Promise.resolve(session.done).then((v) => ({ kind: "done", value: v })),
399
+ ]);
400
+
401
+ if (result.kind === "abort") {
402
+ await session.stop();
270
403
  didFinalize = true;
271
- throw e;
272
- });
404
+ throw result.reason ?? new Error("stopped");
405
+ }
406
+
407
+ didFinalize = true;
408
+ const err = result.value?.error;
409
+ if (err) throw err;
410
+ throw new Error("stopped");
273
411
  } finally {
274
412
  clearInterval(stopWatcher);
413
+ statsPoster.flush();
275
414
  }
276
415
  } catch (e) {
277
416
  if ((e?.message ?? "") === "stopped") {
@@ -286,6 +425,7 @@ async function runLive(p) {
286
425
  await exporter.finalize();
287
426
  } catch {}
288
427
  }
428
+ statsPoster.stop();
289
429
  running = false;
290
430
  stopFlag = false;
291
431
  }
@@ -296,7 +436,8 @@ async function runLive(p) {
296
436
  post("pa_preview_ready", { entry });
297
437
 
298
438
  sendCanvasList();
299
- const mo = new MutationObserver(() => sendCanvasList());
439
+ const sendCanvasListThrottled = throttle(sendCanvasList, 120);
440
+ const mo = new MutationObserver(() => sendCanvasListThrottled());
300
441
  mo.observe(document.documentElement, { childList: true, subtree: true });
301
442
  setTimeout(sendCanvasList, 250);
302
443
  setTimeout(sendCanvasList, 1000);
package/dist/worker.js CHANGED
@@ -24,13 +24,22 @@ self.onmessage = async (ev) => {
24
24
 
25
25
  ctx.clearRect(0, 0, width, height);
26
26
  ctx.drawImage(bitmap, 0, 0);
27
- if (typeof bitmap.close === "function") bitmap.close();
28
27
 
29
28
  const blob = await offscreen.convertToBlob({ type: "image/png" });
30
29
  const t1 = performance.now();
31
30
 
32
31
  self.postMessage({ type: "frame", frameIndex, blob, encodeMs: t1 - t0 });
33
32
  } catch (e) {
34
- self.postMessage({ type: "error", message: e?.message ?? String(e) });
33
+ self.postMessage({
34
+ type: "error",
35
+ frameIndex,
36
+ message: e?.message ?? String(e),
37
+ });
38
+ } finally {
39
+ if (typeof bitmap?.close === "function") {
40
+ try {
41
+ bitmap.close();
42
+ } catch {}
43
+ }
35
44
  }
36
45
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pa_encoder",
3
3
  "license": "MIT",
4
- "version": "0.2.8",
4
+ "version": "0.3.2",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "files": [