pa_encoder 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -206,63 +206,168 @@ async function startLiveCapture({
206
206
  function installVirtualTime({
207
207
  fps = 60,
208
208
  hookDateNow = true,
209
- hookPerformanceNow = true
209
+ hookPerformanceNow = true,
210
+ hookTimers = true,
211
+ maxTimerCallbacksPerStep = 1e4
210
212
  } = {}) {
211
- if (!Number.isFinite(fps) || fps <= 0)
212
- throw new TypeError("fps must be positive");
213
+ var _a, _b, _c, _d, _e, _f;
214
+ if (!Number.isFinite(fps) || fps <= 0) {
215
+ throw new TypeError("installVirtualTime: fps must be a positive number");
216
+ }
213
217
  const dtMs = 1e3 / fps;
214
- const _requestAnimationFrame = window.requestAnimationFrame.bind(window);
215
- const _cancelAnimationFrame = window.cancelAnimationFrame.bind(window);
216
- const _dateNow = Date.now.bind(Date);
217
- const _perfNow = performance.now.bind(performance);
218
- let vNowMs = 0;
218
+ const orig = {
219
+ requestAnimationFrame: (_a = window.requestAnimationFrame) == null ? void 0 : _a.bind(window),
220
+ cancelAnimationFrame: (_b = window.cancelAnimationFrame) == null ? void 0 : _b.bind(window),
221
+ setTimeout: (_c = window.setTimeout) == null ? void 0 : _c.bind(window),
222
+ clearTimeout: (_d = window.clearTimeout) == null ? void 0 : _d.bind(window),
223
+ setInterval: (_e = window.setInterval) == null ? void 0 : _e.bind(window),
224
+ clearInterval: (_f = window.clearInterval) == null ? void 0 : _f.bind(window),
225
+ dateNow: Date.now.bind(Date),
226
+ perfNow: performance.now.bind(performance)
227
+ };
228
+ if (typeof orig.requestAnimationFrame !== "function") {
229
+ throw new Error("installVirtualTime: requestAnimationFrame not available");
230
+ }
231
+ if (typeof orig.cancelAnimationFrame !== "function") {
232
+ throw new Error("installVirtualTime: cancelAnimationFrame not available");
233
+ }
219
234
  let running = true;
220
- let nextId = 1;
221
- const pending = /* @__PURE__ */ new Map();
222
- const scheduled = [];
235
+ let vNowMs = 0;
236
+ let nextRafId = 1;
237
+ const rafCallbacks = /* @__PURE__ */ new Map();
238
+ const rafQueue = [];
223
239
  function hookedRAF(cb) {
224
- if (!running) return _requestAnimationFrame(cb);
225
- const id = nextId++;
226
- pending.set(id, cb);
227
- scheduled.push(id);
240
+ if (!running) return orig.requestAnimationFrame(cb);
241
+ if (typeof cb !== "function") return orig.requestAnimationFrame(cb);
242
+ const id = nextRafId++;
243
+ rafCallbacks.set(id, cb);
244
+ rafQueue.push(id);
228
245
  return id;
229
246
  }
230
247
  function hookedCancelRAF(id) {
231
- if (!running) return _cancelAnimationFrame(id);
232
- pending.delete(id);
248
+ if (!running) return orig.cancelAnimationFrame(id);
249
+ rafCallbacks.delete(id);
250
+ }
251
+ let nextTimerId = 1;
252
+ const timers = /* @__PURE__ */ new Map();
253
+ function normalizeDelay(ms) {
254
+ const n = Number(ms);
255
+ if (!Number.isFinite(n) || n < 0) return 0;
256
+ return n;
257
+ }
258
+ function hookedSetTimeout(fn, delay, ...args) {
259
+ if (!running) return orig.setTimeout(fn, delay, ...args);
260
+ if (typeof fn !== "function") return orig.setTimeout(fn, delay, ...args);
261
+ const id = nextTimerId++;
262
+ const d = normalizeDelay(delay);
263
+ timers.set(id, {
264
+ type: "timeout",
265
+ dueMs: vNowMs + d,
266
+ intervalMs: 0,
267
+ fn: () => fn(...args)
268
+ });
269
+ return id;
270
+ }
271
+ function hookedClearTimeout(id) {
272
+ if (!running) return orig.clearTimeout(id);
273
+ timers.delete(id);
274
+ }
275
+ function hookedSetInterval(fn, delay, ...args) {
276
+ if (!running) return orig.setInterval(fn, delay, ...args);
277
+ if (typeof fn !== "function") return orig.setInterval(fn, delay, ...args);
278
+ const id = nextTimerId++;
279
+ const d = normalizeDelay(delay);
280
+ timers.set(id, {
281
+ type: "interval",
282
+ dueMs: vNowMs + d,
283
+ intervalMs: d,
284
+ fn: () => fn(...args)
285
+ });
286
+ return id;
287
+ }
288
+ function hookedClearInterval(id) {
289
+ if (!running) return orig.clearInterval(id);
290
+ timers.delete(id);
233
291
  }
234
292
  window.requestAnimationFrame = hookedRAF;
235
293
  window.cancelAnimationFrame = hookedCancelRAF;
294
+ if (hookTimers) {
295
+ if (typeof orig.setTimeout !== "function" || typeof orig.clearTimeout !== "function") {
296
+ throw new Error(
297
+ "installVirtualTime: setTimeout/clearTimeout not available"
298
+ );
299
+ }
300
+ if (typeof orig.setInterval !== "function" || typeof orig.clearInterval !== "function") {
301
+ throw new Error(
302
+ "installVirtualTime: setInterval/clearInterval not available"
303
+ );
304
+ }
305
+ window.setTimeout = hookedSetTimeout;
306
+ window.clearTimeout = hookedClearTimeout;
307
+ window.setInterval = hookedSetInterval;
308
+ window.clearInterval = hookedClearInterval;
309
+ }
236
310
  if (hookDateNow) {
237
311
  Date.now = function() {
238
- return running ? vNowMs : _dateNow();
312
+ return running ? vNowMs : orig.dateNow();
239
313
  };
240
314
  }
241
315
  if (hookPerformanceNow) {
242
316
  performance.now = function() {
243
- return running ? vNowMs : _perfNow();
317
+ return running ? vNowMs : orig.perfNow();
244
318
  };
245
319
  }
320
+ function flushDueTimers() {
321
+ let executed = 0;
322
+ while (true) {
323
+ if (executed > maxTimerCallbacksPerStep) {
324
+ throw new Error(
325
+ `VirtualTime: too many timer callbacks in a single step (>${maxTimerCallbacksPerStep}).`
326
+ );
327
+ }
328
+ let nextDue = Infinity;
329
+ for (const t of timers.values()) {
330
+ if (t.dueMs < nextDue) nextDue = t.dueMs;
331
+ }
332
+ if (!(nextDue <= vNowMs)) break;
333
+ const due = [];
334
+ for (const [id, t] of timers.entries()) {
335
+ if (t.dueMs <= vNowMs) due.push([id, t]);
336
+ }
337
+ due.sort((a, b) => a[1].dueMs - b[1].dueMs || a[0] - b[0]);
338
+ for (const [id, t] of due) {
339
+ if (!timers.has(id)) continue;
340
+ executed++;
341
+ t.fn();
342
+ if (t.type === "timeout") {
343
+ timers.delete(id);
344
+ } else {
345
+ const interval = t.intervalMs;
346
+ let next = t.dueMs + interval;
347
+ if (next <= vNowMs) next = vNowMs;
348
+ t.dueMs = next;
349
+ timers.set(id, t);
350
+ }
351
+ }
352
+ }
353
+ }
246
354
  function flushRAFFrame() {
247
- const ids = scheduled.splice(0, scheduled.length);
355
+ const ids = rafQueue.splice(0, rafQueue.length);
248
356
  for (const id of ids) {
249
- const cb = pending.get(id);
357
+ const cb = rafCallbacks.get(id);
250
358
  if (!cb) continue;
251
- pending.delete(id);
252
- try {
253
- cb(vNowMs);
254
- } catch (e) {
255
- console.error("Error in rAF callback:", e);
256
- throw e;
257
- }
359
+ rafCallbacks.delete(id);
360
+ cb(vNowMs);
258
361
  }
259
362
  }
260
363
  function step(frames = 1) {
261
- if (!running) throw new Error("Virtual time is not running");
262
- if (!Number.isInteger(frames) || frames < 1)
263
- throw new TypeError("frames must be >= 1");
364
+ if (!running) throw new Error("VirtualTime: not running");
365
+ if (!Number.isInteger(frames) || frames < 1) {
366
+ throw new TypeError("VirtualTime.step: frames must be an integer >= 1");
367
+ }
264
368
  for (let i = 0; i < frames; i++) {
265
369
  vNowMs += dtMs;
370
+ if (hookTimers) flushDueTimers();
266
371
  flushRAFFrame();
267
372
  }
268
373
  }
@@ -270,161 +375,297 @@ function installVirtualTime({
270
375
  running = false;
271
376
  }
272
377
  function restore() {
273
- window.requestAnimationFrame = _requestAnimationFrame;
274
- window.cancelAnimationFrame = _cancelAnimationFrame;
275
- if (hookDateNow) Date.now = _dateNow;
276
- if (hookPerformanceNow) performance.now = _perfNow;
378
+ window.requestAnimationFrame = orig.requestAnimationFrame;
379
+ window.cancelAnimationFrame = orig.cancelAnimationFrame;
380
+ if (hookTimers) {
381
+ window.setTimeout = orig.setTimeout;
382
+ window.clearTimeout = orig.clearTimeout;
383
+ window.setInterval = orig.setInterval;
384
+ window.clearInterval = orig.clearInterval;
385
+ }
386
+ if (hookDateNow) Date.now = orig.dateNow;
387
+ if (hookPerformanceNow) performance.now = orig.perfNow;
277
388
  running = false;
278
- pending.clear();
279
- scheduled.length = 0;
389
+ rafCallbacks.clear();
390
+ rafQueue.length = 0;
391
+ timers.clear();
280
392
  }
281
393
  return {
394
+ step,
395
+ restore,
396
+ stopVirtualTime,
282
397
  get nowMs() {
283
398
  return vNowMs;
284
399
  },
285
- dtMs,
286
- step,
287
- stopVirtualTime,
288
- restore
400
+ get dtMs() {
401
+ return dtMs;
402
+ }
289
403
  };
290
404
  }
291
405
 
292
406
  // src/virtual_capture.js
293
- function resolveCanvas(canvasOrSelector) {
294
- if (canvasOrSelector instanceof HTMLCanvasElement) return canvasOrSelector;
295
- if (typeof canvasOrSelector === "string") {
296
- const el = document.querySelector(canvasOrSelector);
297
- if (el instanceof HTMLCanvasElement) return el;
298
- throw new Error(`No canvas found for selector: ${canvasOrSelector}`);
299
- }
300
- const first = document.querySelector("canvas");
301
- if (first instanceof HTMLCanvasElement) return first;
302
- throw new Error("No canvas found on the page");
303
- }
304
- async function virtualTimeCapture({
305
- // IMPORTANT: loadSketch must be called AFTER hooks are installed
306
- loadSketch,
307
- // () => import("...") (or any function that executes the sketch module)
308
- canvas: canvasOrSelector,
309
- // canvas element OR selector OR omit to use first canvas
310
- fps = 60,
311
- frames = 300,
312
- warmupFrames = 0,
313
- exporter,
314
- concurrency = 2,
315
- onProgress,
316
- hookDateNow = true,
317
- hookPerformanceNow = true,
318
- signal
407
+ function findElementDeep({
408
+ doc,
409
+ selector,
410
+ includeShadow = true,
411
+ maxIframeDepth = 8,
412
+ _depth = 0
319
413
  } = {}) {
320
- if (typeof loadSketch !== "function") {
321
- throw new TypeError(
322
- "loadSketch must be a function returning a Promise (dynamic import)"
323
- );
324
- }
325
- if (!exporter || typeof exporter.write !== "function" || typeof exporter.finalize !== "function") {
326
- throw new TypeError("exporter must have write() and finalize()");
414
+ if (!doc || _depth > maxIframeDepth) return null;
415
+ try {
416
+ const el = doc.querySelector(selector);
417
+ if (el) return { el, doc };
418
+ } catch {
327
419
  }
328
- if (!Number.isFinite(fps) || fps <= 0)
329
- throw new TypeError("fps must be positive");
330
- if (!Number.isInteger(frames) || frames < 1)
331
- throw new TypeError("frames must be >= 1");
332
- if (!Number.isInteger(warmupFrames) || warmupFrames < 0)
333
- throw new TypeError("warmupFrames must be >= 0");
334
- if (!Number.isInteger(concurrency) || concurrency < 1)
335
- throw new TypeError("concurrency must be >= 1");
336
- const vt = installVirtualTime({ fps, hookDateNow, hookPerformanceNow });
337
- const worker = new Worker(new URL("./worker.js", import.meta.url), {
338
- type: "module"
339
- });
340
- const ready = new Promise((resolve, reject) => {
341
- const onMsg = (ev) => {
342
- var _a, _b;
343
- if (((_a = ev.data) == null ? void 0 : _a.type) === "ready") {
344
- worker.removeEventListener("message", onMsg);
345
- resolve();
346
- } else if (((_b = ev.data) == null ? void 0 : _b.type) === "error") {
347
- worker.removeEventListener("message", onMsg);
348
- reject(new Error(ev.data.message || "Worker error"));
420
+ if (includeShadow) {
421
+ let all = [];
422
+ try {
423
+ all = doc.querySelectorAll("*");
424
+ } catch {
425
+ all = [];
426
+ }
427
+ for (const host of all) {
428
+ const sr = host.shadowRoot;
429
+ if (!sr) continue;
430
+ try {
431
+ const el = sr.querySelector(selector);
432
+ if (el) return { el, doc };
433
+ } catch {
349
434
  }
350
- };
351
- worker.addEventListener("message", onMsg);
352
- worker.addEventListener("error", reject);
353
- });
354
- function throwIfAborted() {
355
- if (signal == null ? void 0 : signal.aborted) {
356
- throw signal.reason ?? new DOMException("Aborted", "AbortError");
357
435
  }
358
436
  }
359
- await ready;
360
- await loadSketch();
361
- const canvas = resolveCanvas(canvasOrSelector);
362
- for (let i = 0; i < warmupFrames; i++) {
363
- throwIfAborted();
364
- vt.step(1);
365
- }
366
- let inFlight = 0;
367
- const waiters = [];
368
- const ack = /* @__PURE__ */ new Map();
369
- function acquireSlot() {
370
- if (inFlight < concurrency) return Promise.resolve();
371
- return new Promise((r) => waiters.push(r));
437
+ let iframes = [];
438
+ try {
439
+ iframes = doc.querySelectorAll("iframe");
440
+ } catch {
441
+ iframes = [];
372
442
  }
373
- function releaseSlot() {
374
- inFlight--;
375
- const w = waiters.shift();
376
- if (w) w();
443
+ for (const f of iframes) {
444
+ let childDoc = null;
445
+ try {
446
+ childDoc = f.contentDocument;
447
+ } catch {
448
+ childDoc = null;
449
+ }
450
+ if (!childDoc) continue;
451
+ const found = findElementDeep({
452
+ doc: childDoc,
453
+ selector,
454
+ includeShadow,
455
+ maxIframeDepth,
456
+ _depth: _depth + 1
457
+ });
458
+ if (found) return found;
377
459
  }
378
- worker.addEventListener("message", async (ev) => {
379
- const msg = ev.data;
380
- if (!msg || typeof msg !== "object") return;
381
- if (msg.type === "frame") {
382
- const { frameIndex, blob } = msg;
460
+ return null;
461
+ }
462
+ function debugSnapshot(tag = "snapshot") {
463
+ const info = {
464
+ tag,
465
+ href: (() => {
383
466
  try {
384
- await exporter.write(frameIndex, blob);
385
- const entry = ack.get(frameIndex);
386
- if (entry) entry.resolve();
387
- } catch (e) {
388
- const entry = ack.get(frameIndex);
389
- if (entry) entry.reject(e);
390
- } finally {
391
- ack.delete(frameIndex);
392
- releaseSlot();
467
+ return location.href;
468
+ } catch {
469
+ return "(no location)";
393
470
  }
394
- } else if (msg.type === "error") {
395
- console.error("Worker error:", msg.message);
471
+ })(),
472
+ readyState: (() => {
473
+ try {
474
+ return document.readyState;
475
+ } catch {
476
+ return "(no document)";
477
+ }
478
+ })(),
479
+ canvasCount: (() => {
480
+ try {
481
+ return document.querySelectorAll("canvas").length;
482
+ } catch {
483
+ return -1;
484
+ }
485
+ })(),
486
+ iframeCount: (() => {
487
+ try {
488
+ return document.querySelectorAll("iframe").length;
489
+ } catch {
490
+ return -1;
491
+ }
492
+ })()
493
+ };
494
+ console.warn("[pa_encoder]", info);
495
+ }
496
+ function waitForCanvasLiveDeep({
497
+ selector = "canvas",
498
+ timeoutMs = 15e3,
499
+ pollMs = 50,
500
+ includeShadow = true
501
+ } = {}) {
502
+ const immediate = findElementDeep({ doc: document, selector, includeShadow });
503
+ if (immediate == null ? void 0 : immediate.el) return Promise.resolve(immediate);
504
+ return new Promise((resolve, reject) => {
505
+ const t0 = performance.now();
506
+ let done = false;
507
+ let intervalId = null;
508
+ const cleanup = (observer2) => {
509
+ if (done) return;
510
+ done = true;
511
+ try {
512
+ observer2 == null ? void 0 : observer2.disconnect();
513
+ } catch {
514
+ }
515
+ if (intervalId) clearInterval(intervalId);
516
+ };
517
+ const check = (observer2) => {
518
+ const found = findElementDeep({ doc: document, selector, includeShadow });
519
+ if (found == null ? void 0 : found.el) {
520
+ cleanup(observer2);
521
+ resolve(found);
522
+ return;
523
+ }
524
+ if (performance.now() - t0 > timeoutMs) {
525
+ cleanup(observer2);
526
+ debugSnapshot("waitForCanvasLiveDeep timeout");
527
+ reject(new Error(`Timed out waiting for canvas selector: ${selector}`));
528
+ }
529
+ };
530
+ const observer = new MutationObserver(() => check(observer));
531
+ observer.observe(document.documentElement, {
532
+ childList: true,
533
+ subtree: true
534
+ });
535
+ intervalId = setInterval(() => check(observer), pollMs);
536
+ });
537
+ }
538
+ function resolveCanvasFromFound(found) {
539
+ const el = found == null ? void 0 : found.el;
540
+ if (el instanceof HTMLCanvasElement) return el;
541
+ throw new Error("Found element is not a canvas");
542
+ }
543
+ async function resolveCanvasWithWaitDeep({
544
+ vt,
545
+ selector = "canvas",
546
+ canvasWaitFrames = 240,
547
+ includeShadow = true
548
+ } = {}) {
549
+ const tryResolve = () => {
550
+ const found = findElementDeep({ doc: document, selector, includeShadow });
551
+ if (!(found == null ? void 0 : found.el)) throw new Error("No canvas found on the page");
552
+ return resolveCanvasFromFound(found);
553
+ };
554
+ try {
555
+ return tryResolve();
556
+ } catch (e) {
557
+ console.warn("[pa_encoder] resolveCanvas failed:", (e == null ? void 0 : e.message) ?? e);
558
+ }
559
+ for (let i = 0; i < canvasWaitFrames; i++) {
560
+ vt.step(1);
561
+ try {
562
+ return tryResolve();
563
+ } catch (e) {
564
+ console.warn("[pa_encoder] resolveCanvas failed:", (e == null ? void 0 : e.message) ?? e);
396
565
  }
566
+ }
567
+ throw new Error("No canvas found on the page");
568
+ }
569
+ async function virtualTimeCapture({
570
+ fps = 60,
571
+ canvasSelector = "canvas",
572
+ preWaitTimeoutMs = 15e3,
573
+ includeShadow = true,
574
+ canvasWaitFrames = 240,
575
+ hookDateNow = true,
576
+ hookPerformanceNow = true,
577
+ hookTimers = true,
578
+ frameCount = 60,
579
+ onFrame = null
580
+ } = {}) {
581
+ debugSnapshot("virtualTimeCapture start");
582
+ const foundLive = await waitForCanvasLiveDeep({
583
+ selector: canvasSelector || "canvas",
584
+ timeoutMs: preWaitTimeoutMs,
585
+ includeShadow
586
+ });
587
+ resolveCanvasFromFound(foundLive);
588
+ const vt = installVirtualTime({
589
+ fps,
590
+ hookDateNow,
591
+ hookPerformanceNow,
592
+ hookTimers
397
593
  });
398
594
  try {
399
- for (let i = 0; i < frames; i++) {
400
- throwIfAborted();
595
+ let canvas = await resolveCanvasWithWaitDeep({
596
+ vt,
597
+ selector: canvasSelector || "canvas",
598
+ canvasWaitFrames,
599
+ includeShadow
600
+ });
601
+ for (let i = 0; i < frameCount; i++) {
401
602
  vt.step(1);
402
- const bitmap = await createImageBitmap(canvas);
403
- await acquireSlot();
404
- inFlight++;
405
- const done = new Promise(
406
- (resolve, reject) => ack.set(i, { resolve, reject })
407
- );
408
- worker.postMessage(
409
- {
410
- type: "encode",
411
- frameIndex: i,
412
- bitmap,
413
- width: canvas.width,
414
- height: canvas.height
415
- },
416
- [bitmap]
417
- );
418
- await done;
419
- onProgress == null ? void 0 : onProgress(i + 1, frames, { nowMs: vt.nowMs });
603
+ if (!(canvas instanceof HTMLCanvasElement) || !canvas.isConnected) {
604
+ canvas = await resolveCanvasWithWaitDeep({
605
+ vt,
606
+ selector: canvasSelector || "canvas",
607
+ canvasWaitFrames: 30,
608
+ includeShadow
609
+ });
610
+ }
611
+ if (typeof onFrame === "function") {
612
+ await onFrame(canvas, i);
613
+ }
420
614
  }
421
- while (inFlight > 0) {
422
- await new Promise((r) => setTimeout(r, 20));
615
+ return { ok: true };
616
+ } finally {
617
+ vt.restore();
618
+ debugSnapshot("virtualTimeCapture end");
619
+ }
620
+ }
621
+ async function virtualTimeCaptureFromStart({
622
+ fps = 60,
623
+ // caller provides a start hook (e.g. import(entry))
624
+ start = null,
625
+ canvasSelector = "canvas",
626
+ includeShadow = true,
627
+ canvasWaitFrames = 600,
628
+ hookDateNow = true,
629
+ hookPerformanceNow = true,
630
+ hookTimers = true,
631
+ frameCount = 60,
632
+ onFrame = null
633
+ } = {}) {
634
+ debugSnapshot("virtualTimeCaptureFromStart begin");
635
+ const vt = installVirtualTime({
636
+ fps,
637
+ hookDateNow,
638
+ hookPerformanceNow,
639
+ hookTimers
640
+ });
641
+ try {
642
+ if (typeof start === "function") {
643
+ await start();
423
644
  }
424
- await exporter.finalize();
645
+ let canvas = await resolveCanvasWithWaitDeep({
646
+ vt,
647
+ selector: canvasSelector || "canvas",
648
+ canvasWaitFrames,
649
+ includeShadow
650
+ });
651
+ for (let i = 0; i < frameCount; i++) {
652
+ vt.step(1);
653
+ if (!(canvas instanceof HTMLCanvasElement) || !canvas.isConnected) {
654
+ canvas = await resolveCanvasWithWaitDeep({
655
+ vt,
656
+ selector: canvasSelector || "canvas",
657
+ canvasWaitFrames: 30,
658
+ includeShadow
659
+ });
660
+ }
661
+ if (typeof onFrame === "function") {
662
+ await onFrame(canvas, i);
663
+ }
664
+ }
665
+ return { ok: true };
425
666
  } finally {
426
- worker.terminate();
427
667
  vt.restore();
668
+ debugSnapshot("virtualTimeCaptureFromStart end");
428
669
  }
429
670
  }
430
671
 
@@ -510,5 +751,6 @@ export {
510
751
  createFsExporter,
511
752
  createZipExporter,
512
753
  startLiveCapture,
513
- virtualTimeCapture
754
+ virtualTimeCapture,
755
+ virtualTimeCaptureFromStart
514
756
  };