@hyperframes/producer 0.9.20 → 0.9.21

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
@@ -410,6 +410,68 @@ ${seededRandomBlock}
410
410
  }
411
411
  };
412
412
 
413
+ // Timers set once rendering has started run on film time: each seek fires every timer due by then, in
414
+ // due order. Until a seek after the page reports ready, timers keep the wall clock: setup code waits on
415
+ // them while the renderer may already be seeking.
416
+ var rendering = false;
417
+ var timerQueue = [];
418
+ var nextTimerId = 1000000000;
419
+ var firingDepth = 0;
420
+ function addTimer(callback, delay, args, repeats) {
421
+ // A timer set from a firing timer waits at least 1 ms, so a zero-delay chain cannot stall a seek.
422
+ var ms = Math.max(firingDepth > 0 ? 1 : 0, Number(delay) || 0);
423
+ var entry = { id: nextTimerId++, callback: callback, args: args, due: virtualNowMs + ms, every: repeats ? Math.max(1, ms) : 0, cancelled: false };
424
+ timerQueue.push(entry);
425
+ return entry.id;
426
+ }
427
+ function cancelTimer(id) {
428
+ for (var i = 0; i < timerQueue.length; i++) {
429
+ if (timerQueue[i].id === id) {
430
+ timerQueue[i].cancelled = true;
431
+ return true;
432
+ }
433
+ }
434
+ return false;
435
+ }
436
+ function fireDueTimers(untilMs) {
437
+ // Frame times are fractional, so a timer due on a frame lands a rounding error past it; it still fires on it.
438
+ untilMs += 0.000001;
439
+ while (true) {
440
+ var next = null;
441
+ for (var i = 0; i < timerQueue.length; i++) {
442
+ var candidate = timerQueue[i];
443
+ if (!candidate.cancelled && candidate.due <= untilMs && (next === null || candidate.due < next.due)) next = candidate;
444
+ }
445
+ if (next === null) break;
446
+ var dueMs = next.due;
447
+ if (next.every) next.due += next.every;
448
+ else next.cancelled = true;
449
+ virtualNowMs = dueMs;
450
+ firingDepth++;
451
+ try {
452
+ next.callback.apply(window, next.args);
453
+ } catch (err) {
454
+ originalSetTimeout(function() { throw err; }, 0);
455
+ }
456
+ firingDepth--;
457
+ }
458
+ timerQueue = timerQueue.filter(function(entry) { return !entry.cancelled; });
459
+ }
460
+ window.setTimeout = function(callback, delay) {
461
+ if (!rendering || typeof callback !== "function") return originalSetTimeout.apply(null, arguments);
462
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), false);
463
+ };
464
+ window.setInterval = function(callback, delay) {
465
+ if (!rendering || typeof callback !== "function") return originalSetInterval.apply(null, arguments);
466
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), true);
467
+ };
468
+ window.clearTimeout = function(id) {
469
+ if (!cancelTimer(id)) originalClearTimeout(id);
470
+ };
471
+ window.clearInterval = function(id) {
472
+ if (!cancelTimer(id)) originalClearInterval(id);
473
+ };
474
+
413
475
  window.__HF_VIRTUAL_TIME__ = {
414
476
  originalSetTimeout: originalSetTimeout,
415
477
  originalClearTimeout: originalClearTimeout,
@@ -419,8 +481,10 @@ ${seededRandomBlock}
419
481
  originalCancelAnimationFrame: originalCancelAnimationFrame,
420
482
  seekToTime: function(nextTimeMs) {
421
483
  var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
484
+ if (window.__renderReady === true) rendering = true;
485
+ ${seekToTimeReseedCall}fireDueTimers(safeTimeMs);
422
486
  virtualNowMs = safeTimeMs;
423
- ${seekToTimeReseedCall}flushAnimationFrame();
487
+ flushAnimationFrame();
424
488
  return virtualNowMs;
425
489
  },
426
490
  getTime: function() {
@@ -18006,7 +18070,7 @@ function assertPlanSizeWithinLimit(input) {
18006
18070
  });
18007
18071
  throw new PlanTooLargeError(breakdown.totalBytes, input.limitBytes, breakdown, input.observedAt);
18008
18072
  }
18009
- function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targetChunkFrames) {
18073
+ function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targetChunkFrames, minParallelChunks = 1) {
18010
18074
  assertPositiveInteger2("totalFrames", totalFrames);
18011
18075
  assertPositiveInteger2("maxParallelChunks", maxParallelChunks);
18012
18076
  if (configChunkSize !== void 0) {
@@ -18015,7 +18079,11 @@ function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targe
18015
18079
  if (targetChunkFrames !== void 0) {
18016
18080
  assertPositiveInteger2("targetChunkFrames", targetChunkFrames);
18017
18081
  }
18018
- const autoSizeParallel = targetChunkFrames === void 0 ? maxParallelChunks : Math.min(maxParallelChunks, Math.max(1, Math.ceil(totalFrames / targetChunkFrames)));
18082
+ assertPositiveInteger2("minParallelChunks", minParallelChunks);
18083
+ const autoSizeParallel = targetChunkFrames === void 0 ? maxParallelChunks : Math.min(
18084
+ maxParallelChunks,
18085
+ Math.max(minParallelChunks, Math.ceil(totalFrames / targetChunkFrames))
18086
+ );
18019
18087
  const resolvedChunkSize = configChunkSize ?? Math.max(MIN_CHUNK_SIZE, Math.ceil(totalFrames / autoSizeParallel));
18020
18088
  const naiveCount = Math.ceil(totalFrames / resolvedChunkSize);
18021
18089
  const chunkCount = Math.min(maxParallelChunks, Math.max(1, naiveCount));
@@ -18594,7 +18662,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
18594
18662
  totalFrames,
18595
18663
  config.chunkSize,
18596
18664
  maxParallel,
18597
- config.targetChunkFrames
18665
+ config.targetChunkFrames,
18666
+ config.minParallelChunks
18598
18667
  );
18599
18668
  const chunks = buildPlanChunkSlices(
18600
18669
  frameWeights,
@@ -20598,6 +20667,20 @@ function validateDistributedRenderConfig(config) {
20598
20667
  );
20599
20668
  }
20600
20669
  }
20670
+ if (config.minParallelChunks !== void 0) {
20671
+ if (!Number.isInteger(config.minParallelChunks) || config.minParallelChunks < 1) {
20672
+ throw new InvalidConfigError(
20673
+ "config.minParallelChunks",
20674
+ `must be a positive integer; got ${config.minParallelChunks}`
20675
+ );
20676
+ }
20677
+ if (config.minParallelChunks > MAX_PARALLEL_CHUNKS_CEILING) {
20678
+ throw new InvalidConfigError(
20679
+ "config.minParallelChunks",
20680
+ `must be <= ${MAX_PARALLEL_CHUNKS_CEILING}; got ${config.minParallelChunks}`
20681
+ );
20682
+ }
20683
+ }
20601
20684
  for (const field of ["workerTimeoutSeconds", "workerConcurrency"]) {
20602
20685
  const value = config[field];
20603
20686
  if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {
@@ -20767,6 +20850,7 @@ function assertDistributedOptions(value) {
20767
20850
  "chunkSize",
20768
20851
  "maxParallelChunks",
20769
20852
  "targetChunkFrames",
20853
+ "minParallelChunks",
20770
20854
  "planDirSizeLimitBytes"
20771
20855
  ]) {
20772
20856
  assertOptionalInteger(value, field, 1);
@@ -20906,6 +20990,7 @@ function distributedConfigFromRequest(request, runtime = {}) {
20906
20990
  chunkSize: distributed.chunkSize,
20907
20991
  maxParallelChunks: distributed.maxParallelChunks,
20908
20992
  targetChunkFrames: distributed.targetChunkFrames,
20993
+ minParallelChunks: distributed.minParallelChunks,
20909
20994
  runtimeCap: distributed.runtimeCap,
20910
20995
  rejectOnSystemFonts: distributed.rejectOnSystemFonts,
20911
20996
  failClosedFontFetch: distributed.failClosedFontFetch,
@@ -20933,6 +21018,7 @@ function renderRequestFromDistributedConfig(input) {
20933
21018
  ...optionalProperty("chunkSize", config.chunkSize),
20934
21019
  ...optionalProperty("maxParallelChunks", config.maxParallelChunks),
20935
21020
  ...optionalProperty("targetChunkFrames", config.targetChunkFrames),
21021
+ ...optionalProperty("minParallelChunks", config.minParallelChunks),
20936
21022
  ...optionalProperty("runtimeCap", config.runtimeCap),
20937
21023
  ...optionalProperty("rejectOnSystemFonts", config.rejectOnSystemFonts),
20938
21024
  ...optionalProperty("failClosedFontFetch", config.failClosedFontFetch),
@@ -427,6 +427,68 @@ ${seededRandomBlock}
427
427
  }
428
428
  };
429
429
 
430
+ // Timers set once rendering has started run on film time: each seek fires every timer due by then, in
431
+ // due order. Until a seek after the page reports ready, timers keep the wall clock: setup code waits on
432
+ // them while the renderer may already be seeking.
433
+ var rendering = false;
434
+ var timerQueue = [];
435
+ var nextTimerId = 1000000000;
436
+ var firingDepth = 0;
437
+ function addTimer(callback, delay, args, repeats) {
438
+ // A timer set from a firing timer waits at least 1 ms, so a zero-delay chain cannot stall a seek.
439
+ var ms = Math.max(firingDepth > 0 ? 1 : 0, Number(delay) || 0);
440
+ var entry = { id: nextTimerId++, callback: callback, args: args, due: virtualNowMs + ms, every: repeats ? Math.max(1, ms) : 0, cancelled: false };
441
+ timerQueue.push(entry);
442
+ return entry.id;
443
+ }
444
+ function cancelTimer(id) {
445
+ for (var i = 0; i < timerQueue.length; i++) {
446
+ if (timerQueue[i].id === id) {
447
+ timerQueue[i].cancelled = true;
448
+ return true;
449
+ }
450
+ }
451
+ return false;
452
+ }
453
+ function fireDueTimers(untilMs) {
454
+ // Frame times are fractional, so a timer due on a frame lands a rounding error past it; it still fires on it.
455
+ untilMs += 0.000001;
456
+ while (true) {
457
+ var next = null;
458
+ for (var i = 0; i < timerQueue.length; i++) {
459
+ var candidate = timerQueue[i];
460
+ if (!candidate.cancelled && candidate.due <= untilMs && (next === null || candidate.due < next.due)) next = candidate;
461
+ }
462
+ if (next === null) break;
463
+ var dueMs = next.due;
464
+ if (next.every) next.due += next.every;
465
+ else next.cancelled = true;
466
+ virtualNowMs = dueMs;
467
+ firingDepth++;
468
+ try {
469
+ next.callback.apply(window, next.args);
470
+ } catch (err) {
471
+ originalSetTimeout(function() { throw err; }, 0);
472
+ }
473
+ firingDepth--;
474
+ }
475
+ timerQueue = timerQueue.filter(function(entry) { return !entry.cancelled; });
476
+ }
477
+ window.setTimeout = function(callback, delay) {
478
+ if (!rendering || typeof callback !== "function") return originalSetTimeout.apply(null, arguments);
479
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), false);
480
+ };
481
+ window.setInterval = function(callback, delay) {
482
+ if (!rendering || typeof callback !== "function") return originalSetInterval.apply(null, arguments);
483
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), true);
484
+ };
485
+ window.clearTimeout = function(id) {
486
+ if (!cancelTimer(id)) originalClearTimeout(id);
487
+ };
488
+ window.clearInterval = function(id) {
489
+ if (!cancelTimer(id)) originalClearInterval(id);
490
+ };
491
+
430
492
  window.__HF_VIRTUAL_TIME__ = {
431
493
  originalSetTimeout: originalSetTimeout,
432
494
  originalClearTimeout: originalClearTimeout,
@@ -436,8 +498,10 @@ ${seededRandomBlock}
436
498
  originalCancelAnimationFrame: originalCancelAnimationFrame,
437
499
  seekToTime: function(nextTimeMs) {
438
500
  var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
501
+ if (window.__renderReady === true) rendering = true;
502
+ ${seekToTimeReseedCall}fireDueTimers(safeTimeMs);
439
503
  virtualNowMs = safeTimeMs;
440
- ${seekToTimeReseedCall}flushAnimationFrame();
504
+ flushAnimationFrame();
441
505
  return virtualNowMs;
442
506
  },
443
507
  getTime: function() {
@@ -16537,6 +16601,7 @@ function assertDistributedOptions(value) {
16537
16601
  "chunkSize",
16538
16602
  "maxParallelChunks",
16539
16603
  "targetChunkFrames",
16604
+ "minParallelChunks",
16540
16605
  "planDirSizeLimitBytes"
16541
16606
  ]) {
16542
16607
  assertOptionalInteger(value, field, 1);
@@ -12,6 +12,7 @@ export interface DistributedRenderOptions {
12
12
  chunkSize?: number;
13
13
  maxParallelChunks?: number;
14
14
  targetChunkFrames?: number;
15
+ minParallelChunks?: number;
15
16
  runtimeCap?: DistributedRenderConfig["runtimeCap"];
16
17
  rejectOnSystemFonts?: boolean;
17
18
  failClosedFontFetch?: boolean;
@@ -104,17 +104,19 @@ export interface DistributedRenderConfig extends Pick<RenderRequestOptions, "pag
104
104
  * chunks whose per-chunk frame count stays at or below this bound, still
105
105
  * capped by `maxParallelChunks`:
106
106
  *
107
- * chunkCount = clamp(ceil(totalFrames / targetChunkFrames), 1, maxParallelChunks)
107
+ * chunkCount = clamp(ceil(totalFrames / targetChunkFrames), minParallelChunks, maxParallelChunks)
108
108
  *
109
109
  * This bounds per-chunk render *time* (which scales with frames-per-chunk) so
110
110
  * a single chunk can't exceed a downstream per-chunk timeout on a long video,
111
- * while short videos still collapse to few chunks. It is a ceiling, not a
111
+ * while short videos drop to `minParallelChunks` (default 1). It is a ceiling, not a
112
112
  * fixed size: a video short enough to fit in fewer chunks gets fewer. Ignored
113
113
  * when `chunkSize` is set (an explicit fixed size already pins per-chunk
114
114
  * frames). Mutually exclusive with `chunkSize` in intent; if both are passed,
115
115
  * `chunkSize` wins and `targetChunkFrames` is a no-op.
116
116
  */
117
117
  targetChunkFrames?: number;
118
+ /** Fewest chunks a `targetChunkFrames`-sized plan uses, so a short video keeps this fan-out. */
119
+ minParallelChunks?: number;
118
120
  /** Runtime hint; consumed by future per-runtime budget checks. The current implementation records the value but does not enforce. */
119
121
  runtimeCap?: "lambda" | "temporal" | "cloud-run-job" | "k8s-job" | "none";
120
122
  /**
@@ -397,12 +399,12 @@ export declare function measurePlanDirBytes(planDir: string): number;
397
399
  * on tiny renders). Explicit numbers, including `240`, take precedence.
398
400
  *
399
401
  * Optional `targetChunkFrames` caps per-chunk frames in the auto-sized path:
400
- * the auto-sizer then targets `clamp(ceil(totalFrames / targetChunkFrames), 1,
401
- * maxParallelChunks)` chunks, so short videos collapse to fewer chunks and long
402
+ * the auto-sizer then targets `clamp(ceil(totalFrames / targetChunkFrames), minParallelChunks,
403
+ * maxParallelChunks)` chunks, so short videos drop to `minParallelChunks` and long
402
404
  * videos add chunks (up to the cap) to keep each one under the bound. It is a
403
405
  * no-op when omitted, and ignored when `configChunkSize` is set.
404
406
  */
405
- export declare function resolveChunkPlan(totalFrames: number, configChunkSize: number | undefined, maxParallelChunks: number, targetChunkFrames?: number): {
407
+ export declare function resolveChunkPlan(totalFrames: number, configChunkSize: number | undefined, maxParallelChunks: number, targetChunkFrames?: number, minParallelChunks?: number): {
406
408
  chunkCount: number;
407
409
  effectiveChunkSize: number;
408
410
  };
@@ -95,9 +95,9 @@ export interface VirtualTimeShimOptions {
95
95
  /**
96
96
  * Build the page-side virtual-time shim script.
97
97
  *
98
- * The shim freezes `Date.now`, `performance.now`, and the rAF/setTimeout
99
- * pipeline so a render seek can deterministically advance the page's
100
- * notion of "now". The renderer issues `__HF_VIRTUAL_TIME__.seekToTime(ms)`
98
+ * The shim freezes `Date.now`, `performance.now`, rAF, and the timers set
99
+ * after the first seek, so a render seek can deterministically advance the
100
+ * page's notion of "now". The renderer issues `__HF_VIRTUAL_TIME__.seekToTime(ms)`
101
101
  * before every frame capture; everything timing-related on the page sees
102
102
  * exactly `ms` until the next seek.
103
103
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.9.20",
3
+ "version": "0.9.21",
4
4
  "description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,11 +35,11 @@
35
35
  "postcss": "^8.4.0",
36
36
  "puppeteer": "~25.11.0",
37
37
  "puppeteer-core": "^25.11.0",
38
- "@hyperframes/lint": "0.9.20",
39
- "@hyperframes/studio-server": "0.9.20",
40
- "@hyperframes/core": "0.9.20",
41
- "@hyperframes/parsers": "0.9.20",
42
- "@hyperframes/engine": "0.9.20"
38
+ "@hyperframes/engine": "0.9.21",
39
+ "@hyperframes/parsers": "0.9.21",
40
+ "@hyperframes/studio-server": "0.9.21",
41
+ "@hyperframes/lint": "0.9.21",
42
+ "@hyperframes/core": "0.9.21"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.10",
@@ -48,7 +48,7 @@
48
48
  "tsx": "^4.21.0",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^4.1.11",
51
- "@hyperframes/aws-lambda": "0.9.20"
51
+ "@hyperframes/aws-lambda": "0.9.21"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"