@hyperframes/producer 0.2.3 → 0.2.4

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
@@ -99285,7 +99285,8 @@ function truncateSnippet(value, maxLength = 220) {
99285
99285
 
99286
99286
  // ../core/src/lint/context.ts
99287
99287
  function buildLintContext(html, options = {}) {
99288
- let source2 = html || "";
99288
+ const rawSource = html || "";
99289
+ let source2 = rawSource;
99289
99290
  const templateMatch = source2.match(/<template[^>]*>([\s\S]*)<\/template>/i);
99290
99291
  if (templateMatch?.[1]) source2 = templateMatch[1];
99291
99292
  const tags = extractOpenTags(source2);
@@ -99296,6 +99297,7 @@ function buildLintContext(html, options = {}) {
99296
99297
  const rootCompositionId = readAttr(rootTag?.raw || "", "data-composition-id");
99297
99298
  return {
99298
99299
  source: source2,
99300
+ rawSource,
99299
99301
  tags,
99300
99302
  styles,
99301
99303
  scripts,
@@ -100469,6 +100471,56 @@ var compositionRules = [
100469
100471
  }
100470
100472
  return findings;
100471
100473
  },
100474
+ // root_composition_missing_data_start
100475
+ ({ rootTag }) => {
100476
+ const findings = [];
100477
+ if (!rootTag) return findings;
100478
+ const compId = readAttr(rootTag.raw, "data-composition-id");
100479
+ if (!compId) return findings;
100480
+ const hasStart = readAttr(rootTag.raw, "data-start") !== null;
100481
+ if (!hasStart) {
100482
+ findings.push({
100483
+ code: "root_composition_missing_data_start",
100484
+ severity: "warning",
100485
+ message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
100486
+ fixHint: 'Add data-start="0" to the root composition element.',
100487
+ snippet: truncateSnippet(rootTag.raw)
100488
+ });
100489
+ }
100490
+ return findings;
100491
+ },
100492
+ // standalone_composition_wrapped_in_template
100493
+ ({ rawSource, options }) => {
100494
+ const findings = [];
100495
+ if (options.isSubComposition) return findings;
100496
+ const trimmed = rawSource.trimStart().toLowerCase();
100497
+ if (trimmed.startsWith("<template")) {
100498
+ findings.push({
100499
+ code: "standalone_composition_wrapped_in_template",
100500
+ severity: "warning",
100501
+ message: "Root index.html is wrapped in a <template> tag. Only sub-compositions loaded via data-composition-src should use <template> wrappers. The runtime cannot play a standalone composition inside a template.",
100502
+ fixHint: "Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead."
100503
+ });
100504
+ }
100505
+ return findings;
100506
+ },
100507
+ // root_composition_missing_html_wrapper
100508
+ ({ rawSource, options }) => {
100509
+ const findings = [];
100510
+ if (options.isSubComposition) return findings;
100511
+ const trimmed = rawSource.trimStart().toLowerCase();
100512
+ const hasDoctype = trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
100513
+ const hasComposition = rawSource.includes("data-composition-id");
100514
+ if (hasComposition && !hasDoctype) {
100515
+ findings.push({
100516
+ code: "root_composition_missing_html_wrapper",
100517
+ severity: "warning",
100518
+ message: "Composition is missing <!DOCTYPE html> and <html> wrapper. The bundler and preview expect a complete HTML document for index.html files.",
100519
+ fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.'
100520
+ });
100521
+ }
100522
+ return findings;
100523
+ },
100472
100524
  // requestanimationframe_in_composition
100473
100525
  ({ scripts }) => {
100474
100526
  const findings = [];
@@ -100669,18 +100721,36 @@ async function getCdpSession(page) {
100669
100721
  return client;
100670
100722
  }
100671
100723
  var lastFrameCache = /* @__PURE__ */ new WeakMap();
100724
+ var PENDING_FRAME_RETRIES = 5;
100725
+ async function sendBeginFrame(client, params) {
100726
+ for (let attempt = 0; ; attempt++) {
100727
+ try {
100728
+ return await client.send("HeadlessExperimental.beginFrame", params);
100729
+ } catch (err) {
100730
+ const msg = err instanceof Error ? err.message : String(err);
100731
+ const isPending = msg.includes("Another frame is pending");
100732
+ if (isPending && attempt < PENDING_FRAME_RETRIES) {
100733
+ await new Promise((r) => setTimeout(r, 50 * 2 ** attempt));
100734
+ continue;
100735
+ }
100736
+ if (isPending) {
100737
+ throw new Error(
100738
+ `[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries \u2014 CPU overloaded by parallel renders. Reduce concurrent renders or use --docker for isolation.`
100739
+ );
100740
+ }
100741
+ throw err;
100742
+ }
100743
+ }
100744
+ }
100672
100745
  async function beginFrameCapture(page, options, frameTimeTicks, interval) {
100673
100746
  const client = await getCdpSession(page);
100674
- const format3 = options.format === "png" ? "png" : "jpeg";
100675
- const result = await client.send("HeadlessExperimental.beginFrame", {
100676
- frameTimeTicks,
100677
- interval,
100678
- screenshot: {
100679
- format: format3,
100680
- quality: format3 === "jpeg" ? options.quality ?? 80 : void 0,
100681
- optimizeForSpeed: true
100682
- }
100683
- });
100747
+ const isPng = options.format === "png";
100748
+ const screenshot = {
100749
+ format: isPng ? "png" : "jpeg",
100750
+ quality: isPng ? void 0 : options.quality ?? 80,
100751
+ optimizeForSpeed: true
100752
+ };
100753
+ const result = await sendBeginFrame(client, { frameTimeTicks, interval, screenshot });
100684
100754
  let buffer;
100685
100755
  if (result.screenshotData) {
100686
100756
  buffer = Buffer.from(result.screenshotData, "base64");
@@ -100690,16 +100760,12 @@ async function beginFrameCapture(page, options, frameTimeTicks, interval) {
100690
100760
  if (cached) {
100691
100761
  buffer = cached;
100692
100762
  } else {
100693
- const retry2 = await client.send("HeadlessExperimental.beginFrame", {
100763
+ const fallback = await sendBeginFrame(client, {
100694
100764
  frameTimeTicks: frameTimeTicks + 1e-3,
100695
100765
  interval,
100696
- screenshot: {
100697
- format: format3,
100698
- quality: format3 === "jpeg" ? options.quality ?? 80 : void 0,
100699
- optimizeForSpeed: true
100700
- }
100766
+ screenshot
100701
100767
  });
100702
- buffer = retry2.screenshotData ? Buffer.from(retry2.screenshotData, "base64") : Buffer.alloc(0);
100768
+ buffer = fallback.screenshotData ? Buffer.from(fallback.screenshotData, "base64") : Buffer.alloc(0);
100703
100769
  if (buffer.length > 0) lastFrameCache.set(page, buffer);
100704
100770
  }
100705
100771
  }
@@ -102202,7 +102268,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
102202
102268
  });
102203
102269
  });
102204
102270
  }
102205
- async function extractAllVideoFrames(videos, baseDir, options, signal, config2) {
102271
+ async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
102206
102272
  const startTime = Date.now();
102207
102273
  const extracted = [];
102208
102274
  const errors = [];
@@ -102215,7 +102281,8 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2)
102215
102281
  try {
102216
102282
  let videoPath = video.src;
102217
102283
  if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
102218
- videoPath = join8(baseDir, videoPath);
102284
+ const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
102285
+ videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
102219
102286
  }
102220
102287
  if (isHttpUrl(videoPath)) {
102221
102288
  const downloadDir = join8(options.outputDir, "_downloads");
@@ -102676,7 +102743,7 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config2
102676
102743
  tracksProcessed: tracks.length
102677
102744
  };
102678
102745
  }
102679
- async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config2) {
102746
+ async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config2, compiledDir) {
102680
102747
  const startMs = Date.now();
102681
102748
  const tracks = [];
102682
102749
  const errors = [];
@@ -102690,7 +102757,8 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
102690
102757
  try {
102691
102758
  let srcPath = element.src;
102692
102759
  if (!srcPath.startsWith("/") && !isHttpUrl(srcPath)) {
102693
- srcPath = join9(baseDir, srcPath);
102760
+ const fromCompiled = compiledDir ? join9(compiledDir, srcPath) : null;
102761
+ srcPath = fromCompiled && existsSync9(fromCompiled) ? fromCompiled : join9(baseDir, srcPath);
102694
102762
  }
102695
102763
  if (isHttpUrl(srcPath)) {
102696
102764
  try {
@@ -102703,7 +102771,7 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
102703
102771
  }
102704
102772
  }
102705
102773
  if (!existsSync9(srcPath)) {
102706
- errors.push(`Source not found: ${element.id}`);
102774
+ errors.push(`Source not found: ${element.id} (${element.src})`);
102707
102775
  return;
102708
102776
  }
102709
102777
  if (element.end - element.start <= 0) {
@@ -107405,12 +107473,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107405
107473
  const stage2Start = Date.now();
107406
107474
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
107407
107475
  let frameLookup = null;
107476
+ const compiledDir = join13(workDir, "compiled");
107408
107477
  if (composition.videos.length > 0) {
107409
107478
  const extractionResult = await extractAllVideoFrames(
107410
107479
  composition.videos,
107411
107480
  projectDir,
107412
107481
  { fps: job.config.fps, outputDir: join13(workDir, "video-frames") },
107413
- abortSignal
107482
+ abortSignal,
107483
+ void 0,
107484
+ compiledDir
107414
107485
  );
107415
107486
  assertNotAborted();
107416
107487
  if (extractionResult.extracted.length > 0) {
@@ -107450,7 +107521,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107450
107521
  join13(workDir, "audio-work"),
107451
107522
  audioOutputPath,
107452
107523
  job.duration,
107453
- abortSignal
107524
+ abortSignal,
107525
+ void 0,
107526
+ compiledDir
107454
107527
  );
107455
107528
  assertNotAborted();
107456
107529
  hasAudio = audioResult.success;
@@ -108117,6 +108190,40 @@ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS
108117
108190
  return { absoluteProjectDir, absoluteOutputPath };
108118
108191
  }
108119
108192
 
108193
+ // src/utils/semaphore.ts
108194
+ var Semaphore = class {
108195
+ constructor(maxConcurrent) {
108196
+ this.maxConcurrent = maxConcurrent;
108197
+ }
108198
+ queue = [];
108199
+ active = 0;
108200
+ async acquire() {
108201
+ if (this.active < this.maxConcurrent) {
108202
+ this.active++;
108203
+ return () => this.release();
108204
+ }
108205
+ return new Promise((resolve13) => {
108206
+ this.queue.push(() => {
108207
+ this.active++;
108208
+ resolve13(() => this.release());
108209
+ });
108210
+ });
108211
+ }
108212
+ release() {
108213
+ this.active--;
108214
+ const next = this.queue.shift();
108215
+ if (next) next();
108216
+ }
108217
+ /** Current number of active slots. */
108218
+ get activeCount() {
108219
+ return this.active;
108220
+ }
108221
+ /** Number of waiters in the queue. */
108222
+ get waitingCount() {
108223
+ return this.queue.length;
108224
+ }
108225
+ };
108226
+
108120
108227
  // src/server.ts
108121
108228
  function parseRenderOptions(body) {
108122
108229
  const fps = [24, 30, 60].includes(body.fps) ? body.fps : 30;
@@ -108230,6 +108337,8 @@ function createRenderHandlers(options = {}) {
108230
108337
  const rendersDir = options.rendersDir ?? process.env.PRODUCER_RENDERS_DIR ?? "/tmp";
108231
108338
  const artifactTtlMs = options.artifactTtlMs ?? Number(process.env.PRODUCER_OUTPUT_ARTIFACT_TTL_MS || 15 * 60 * 1e3);
108232
108339
  const store = createArtifactStore(artifactTtlMs);
108340
+ const maxConcurrentRenders = options.maxConcurrentRenders ?? Number(process.env.PRODUCER_MAX_CONCURRENT_RENDERS || 2);
108341
+ const renderSemaphore = new Semaphore(maxConcurrentRenders);
108233
108342
  const startTime = Date.now();
108234
108343
  const health = (c) => c.json({
108235
108344
  status: "ok",
@@ -108286,6 +108395,7 @@ function createRenderHandlers(options = {}) {
108286
108395
  );
108287
108396
  const outputDir = dirname11(absoluteOutputPath);
108288
108397
  if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
108398
+ const release = await renderSemaphore.acquire();
108289
108399
  log.info("render started", {
108290
108400
  requestId,
108291
108401
  projectDir: input2.projectDir,
@@ -108353,6 +108463,7 @@ function createRenderHandlers(options = {}) {
108353
108463
  500
108354
108464
  );
108355
108465
  } finally {
108466
+ release();
108356
108467
  cleanupTempDir(cleanupProjectDir, log);
108357
108468
  }
108358
108469
  };
@@ -108409,6 +108520,16 @@ function createRenderHandlers(options = {}) {
108409
108520
  const abortController = new AbortController();
108410
108521
  const onRequestAbort = () => abortController.abort(new RenderCancelledError("request_aborted"));
108411
108522
  c.req.raw.signal.addEventListener("abort", onRequestAbort, { once: true });
108523
+ if (renderSemaphore.activeCount >= maxConcurrentRenders) {
108524
+ await stream2.writeSSE({
108525
+ data: JSON.stringify({
108526
+ type: "queued",
108527
+ requestId,
108528
+ position: renderSemaphore.waitingCount
108529
+ })
108530
+ });
108531
+ }
108532
+ const release = await renderSemaphore.acquire();
108412
108533
  try {
108413
108534
  await executeRenderJob(
108414
108535
  job,
@@ -108476,6 +108597,7 @@ function createRenderHandlers(options = {}) {
108476
108597
  })
108477
108598
  });
108478
108599
  } finally {
108600
+ release();
108479
108601
  c.req.raw.signal.removeEventListener("abort", onRequestAbort);
108480
108602
  cleanupTempDir(cleanupProjectDir, log);
108481
108603
  }
@@ -108500,7 +108622,12 @@ function createRenderHandlers(options = {}) {
108500
108622
  }
108501
108623
  });
108502
108624
  };
108503
- return { render: render2, renderStream, lint, health, outputs };
108625
+ const queue = (c) => c.json({
108626
+ maxConcurrentRenders,
108627
+ activeRenders: renderSemaphore.activeCount,
108628
+ queuedRenders: renderSemaphore.waitingCount
108629
+ });
108630
+ return { render: render2, renderStream, lint, health, outputs, queue };
108504
108631
  }
108505
108632
  function createProducerApp(options = {}) {
108506
108633
  const app = new Hono2();
@@ -108508,6 +108635,7 @@ function createProducerApp(options = {}) {
108508
108635
  app.get("/health", handlers.health);
108509
108636
  app.post("/render", handlers.render);
108510
108637
  app.post("/render/stream", handlers.renderStream);
108638
+ app.get("/render/queue", handlers.queue);
108511
108639
  app.post("/lint", handlers.lint);
108512
108640
  app.get("/outputs/:token", handlers.outputs);
108513
108641
  return app;