@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.
@@ -102074,7 +102074,8 @@ function truncateSnippet(value, maxLength = 220) {
102074
102074
 
102075
102075
  // ../core/src/lint/context.ts
102076
102076
  function buildLintContext(html, options = {}) {
102077
- let source2 = html || "";
102077
+ const rawSource = html || "";
102078
+ let source2 = rawSource;
102078
102079
  const templateMatch = source2.match(/<template[^>]*>([\s\S]*)<\/template>/i);
102079
102080
  if (templateMatch?.[1]) source2 = templateMatch[1];
102080
102081
  const tags = extractOpenTags(source2);
@@ -102085,6 +102086,7 @@ function buildLintContext(html, options = {}) {
102085
102086
  const rootCompositionId = readAttr(rootTag?.raw || "", "data-composition-id");
102086
102087
  return {
102087
102088
  source: source2,
102089
+ rawSource,
102088
102090
  tags,
102089
102091
  styles,
102090
102092
  scripts,
@@ -103258,6 +103260,56 @@ var compositionRules = [
103258
103260
  }
103259
103261
  return findings;
103260
103262
  },
103263
+ // root_composition_missing_data_start
103264
+ ({ rootTag }) => {
103265
+ const findings = [];
103266
+ if (!rootTag) return findings;
103267
+ const compId = readAttr(rootTag.raw, "data-composition-id");
103268
+ if (!compId) return findings;
103269
+ const hasStart = readAttr(rootTag.raw, "data-start") !== null;
103270
+ if (!hasStart) {
103271
+ findings.push({
103272
+ code: "root_composition_missing_data_start",
103273
+ severity: "warning",
103274
+ message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
103275
+ fixHint: 'Add data-start="0" to the root composition element.',
103276
+ snippet: truncateSnippet(rootTag.raw)
103277
+ });
103278
+ }
103279
+ return findings;
103280
+ },
103281
+ // standalone_composition_wrapped_in_template
103282
+ ({ rawSource, options }) => {
103283
+ const findings = [];
103284
+ if (options.isSubComposition) return findings;
103285
+ const trimmed = rawSource.trimStart().toLowerCase();
103286
+ if (trimmed.startsWith("<template")) {
103287
+ findings.push({
103288
+ code: "standalone_composition_wrapped_in_template",
103289
+ severity: "warning",
103290
+ 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.",
103291
+ fixHint: "Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead."
103292
+ });
103293
+ }
103294
+ return findings;
103295
+ },
103296
+ // root_composition_missing_html_wrapper
103297
+ ({ rawSource, options }) => {
103298
+ const findings = [];
103299
+ if (options.isSubComposition) return findings;
103300
+ const trimmed = rawSource.trimStart().toLowerCase();
103301
+ const hasDoctype = trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
103302
+ const hasComposition = rawSource.includes("data-composition-id");
103303
+ if (hasComposition && !hasDoctype) {
103304
+ findings.push({
103305
+ code: "root_composition_missing_html_wrapper",
103306
+ severity: "warning",
103307
+ message: "Composition is missing <!DOCTYPE html> and <html> wrapper. The bundler and preview expect a complete HTML document for index.html files.",
103308
+ fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.'
103309
+ });
103310
+ }
103311
+ return findings;
103312
+ },
103261
103313
  // requestanimationframe_in_composition
103262
103314
  ({ scripts }) => {
103263
103315
  const findings = [];
@@ -103458,18 +103510,36 @@ async function getCdpSession(page) {
103458
103510
  return client;
103459
103511
  }
103460
103512
  var lastFrameCache = /* @__PURE__ */ new WeakMap();
103513
+ var PENDING_FRAME_RETRIES = 5;
103514
+ async function sendBeginFrame(client, params) {
103515
+ for (let attempt = 0; ; attempt++) {
103516
+ try {
103517
+ return await client.send("HeadlessExperimental.beginFrame", params);
103518
+ } catch (err) {
103519
+ const msg = err instanceof Error ? err.message : String(err);
103520
+ const isPending = msg.includes("Another frame is pending");
103521
+ if (isPending && attempt < PENDING_FRAME_RETRIES) {
103522
+ await new Promise((r) => setTimeout(r, 50 * 2 ** attempt));
103523
+ continue;
103524
+ }
103525
+ if (isPending) {
103526
+ throw new Error(
103527
+ `[BeginFrame] Frame still pending after ${PENDING_FRAME_RETRIES} retries \u2014 CPU overloaded by parallel renders. Reduce concurrent renders or use --docker for isolation.`
103528
+ );
103529
+ }
103530
+ throw err;
103531
+ }
103532
+ }
103533
+ }
103461
103534
  async function beginFrameCapture(page, options, frameTimeTicks, interval) {
103462
103535
  const client = await getCdpSession(page);
103463
- const format3 = options.format === "png" ? "png" : "jpeg";
103464
- const result = await client.send("HeadlessExperimental.beginFrame", {
103465
- frameTimeTicks,
103466
- interval,
103467
- screenshot: {
103468
- format: format3,
103469
- quality: format3 === "jpeg" ? options.quality ?? 80 : void 0,
103470
- optimizeForSpeed: true
103471
- }
103472
- });
103536
+ const isPng = options.format === "png";
103537
+ const screenshot = {
103538
+ format: isPng ? "png" : "jpeg",
103539
+ quality: isPng ? void 0 : options.quality ?? 80,
103540
+ optimizeForSpeed: true
103541
+ };
103542
+ const result = await sendBeginFrame(client, { frameTimeTicks, interval, screenshot });
103473
103543
  let buffer;
103474
103544
  if (result.screenshotData) {
103475
103545
  buffer = Buffer.from(result.screenshotData, "base64");
@@ -103479,16 +103549,12 @@ async function beginFrameCapture(page, options, frameTimeTicks, interval) {
103479
103549
  if (cached) {
103480
103550
  buffer = cached;
103481
103551
  } else {
103482
- const retry2 = await client.send("HeadlessExperimental.beginFrame", {
103552
+ const fallback = await sendBeginFrame(client, {
103483
103553
  frameTimeTicks: frameTimeTicks + 1e-3,
103484
103554
  interval,
103485
- screenshot: {
103486
- format: format3,
103487
- quality: format3 === "jpeg" ? options.quality ?? 80 : void 0,
103488
- optimizeForSpeed: true
103489
- }
103555
+ screenshot
103490
103556
  });
103491
- buffer = retry2.screenshotData ? Buffer.from(retry2.screenshotData, "base64") : Buffer.alloc(0);
103557
+ buffer = fallback.screenshotData ? Buffer.from(fallback.screenshotData, "base64") : Buffer.alloc(0);
103492
103558
  if (buffer.length > 0) lastFrameCache.set(page, buffer);
103493
103559
  }
103494
103560
  }
@@ -104991,7 +105057,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
104991
105057
  });
104992
105058
  });
104993
105059
  }
104994
- async function extractAllVideoFrames(videos, baseDir, options, signal, config2) {
105060
+ async function extractAllVideoFrames(videos, baseDir, options, signal, config2, compiledDir) {
104995
105061
  const startTime = Date.now();
104996
105062
  const extracted = [];
104997
105063
  const errors = [];
@@ -105004,7 +105070,8 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2)
105004
105070
  try {
105005
105071
  let videoPath = video.src;
105006
105072
  if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
105007
- videoPath = join8(baseDir, videoPath);
105073
+ const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
105074
+ videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
105008
105075
  }
105009
105076
  if (isHttpUrl(videoPath)) {
105010
105077
  const downloadDir = join8(options.outputDir, "_downloads");
@@ -105465,7 +105532,7 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config2
105465
105532
  tracksProcessed: tracks.length
105466
105533
  };
105467
105534
  }
105468
- async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config2) {
105535
+ async function processCompositionAudio(elements, baseDir, workDir, outputPath, totalDuration, signal, config2, compiledDir) {
105469
105536
  const startMs = Date.now();
105470
105537
  const tracks = [];
105471
105538
  const errors = [];
@@ -105479,7 +105546,8 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
105479
105546
  try {
105480
105547
  let srcPath = element.src;
105481
105548
  if (!srcPath.startsWith("/") && !isHttpUrl(srcPath)) {
105482
- srcPath = join9(baseDir, srcPath);
105549
+ const fromCompiled = compiledDir ? join9(compiledDir, srcPath) : null;
105550
+ srcPath = fromCompiled && existsSync9(fromCompiled) ? fromCompiled : join9(baseDir, srcPath);
105483
105551
  }
105484
105552
  if (isHttpUrl(srcPath)) {
105485
105553
  try {
@@ -105492,7 +105560,7 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
105492
105560
  }
105493
105561
  }
105494
105562
  if (!existsSync9(srcPath)) {
105495
- errors.push(`Source not found: ${element.id}`);
105563
+ errors.push(`Source not found: ${element.id} (${element.src})`);
105496
105564
  return;
105497
105565
  }
105498
105566
  if (element.end - element.start <= 0) {
@@ -107570,12 +107638,15 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107570
107638
  const stage2Start = Date.now();
107571
107639
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
107572
107640
  let frameLookup = null;
107641
+ const compiledDir = join13(workDir, "compiled");
107573
107642
  if (composition.videos.length > 0) {
107574
107643
  const extractionResult = await extractAllVideoFrames(
107575
107644
  composition.videos,
107576
107645
  projectDir,
107577
107646
  { fps: job.config.fps, outputDir: join13(workDir, "video-frames") },
107578
- abortSignal
107647
+ abortSignal,
107648
+ void 0,
107649
+ compiledDir
107579
107650
  );
107580
107651
  assertNotAborted();
107581
107652
  if (extractionResult.extracted.length > 0) {
@@ -107615,7 +107686,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107615
107686
  join13(workDir, "audio-work"),
107616
107687
  audioOutputPath,
107617
107688
  job.duration,
107618
- abortSignal
107689
+ abortSignal,
107690
+ void 0,
107691
+ compiledDir
107619
107692
  );
107620
107693
  assertNotAborted();
107621
107694
  hasAudio = audioResult.success;
@@ -108118,6 +108191,40 @@ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS
108118
108191
  return { absoluteProjectDir, absoluteOutputPath };
108119
108192
  }
108120
108193
 
108194
+ // src/utils/semaphore.ts
108195
+ var Semaphore = class {
108196
+ constructor(maxConcurrent) {
108197
+ this.maxConcurrent = maxConcurrent;
108198
+ }
108199
+ queue = [];
108200
+ active = 0;
108201
+ async acquire() {
108202
+ if (this.active < this.maxConcurrent) {
108203
+ this.active++;
108204
+ return () => this.release();
108205
+ }
108206
+ return new Promise((resolve13) => {
108207
+ this.queue.push(() => {
108208
+ this.active++;
108209
+ resolve13(() => this.release());
108210
+ });
108211
+ });
108212
+ }
108213
+ release() {
108214
+ this.active--;
108215
+ const next = this.queue.shift();
108216
+ if (next) next();
108217
+ }
108218
+ /** Current number of active slots. */
108219
+ get activeCount() {
108220
+ return this.active;
108221
+ }
108222
+ /** Number of waiters in the queue. */
108223
+ get waitingCount() {
108224
+ return this.queue.length;
108225
+ }
108226
+ };
108227
+
108121
108228
  // src/server.ts
108122
108229
  function parseRenderOptions(body) {
108123
108230
  const fps = [24, 30, 60].includes(body.fps) ? body.fps : 30;
@@ -108231,6 +108338,8 @@ function createRenderHandlers(options = {}) {
108231
108338
  const rendersDir = options.rendersDir ?? process.env.PRODUCER_RENDERS_DIR ?? "/tmp";
108232
108339
  const artifactTtlMs = options.artifactTtlMs ?? Number(process.env.PRODUCER_OUTPUT_ARTIFACT_TTL_MS || 15 * 60 * 1e3);
108233
108340
  const store = createArtifactStore(artifactTtlMs);
108341
+ const maxConcurrentRenders = options.maxConcurrentRenders ?? Number(process.env.PRODUCER_MAX_CONCURRENT_RENDERS || 2);
108342
+ const renderSemaphore = new Semaphore(maxConcurrentRenders);
108234
108343
  const startTime = Date.now();
108235
108344
  const health = (c) => c.json({
108236
108345
  status: "ok",
@@ -108287,6 +108396,7 @@ function createRenderHandlers(options = {}) {
108287
108396
  );
108288
108397
  const outputDir = dirname11(absoluteOutputPath);
108289
108398
  if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
108399
+ const release = await renderSemaphore.acquire();
108290
108400
  log.info("render started", {
108291
108401
  requestId,
108292
108402
  projectDir: input2.projectDir,
@@ -108354,6 +108464,7 @@ function createRenderHandlers(options = {}) {
108354
108464
  500
108355
108465
  );
108356
108466
  } finally {
108467
+ release();
108357
108468
  cleanupTempDir(cleanupProjectDir, log);
108358
108469
  }
108359
108470
  };
@@ -108410,6 +108521,16 @@ function createRenderHandlers(options = {}) {
108410
108521
  const abortController = new AbortController();
108411
108522
  const onRequestAbort = () => abortController.abort(new RenderCancelledError("request_aborted"));
108412
108523
  c.req.raw.signal.addEventListener("abort", onRequestAbort, { once: true });
108524
+ if (renderSemaphore.activeCount >= maxConcurrentRenders) {
108525
+ await stream2.writeSSE({
108526
+ data: JSON.stringify({
108527
+ type: "queued",
108528
+ requestId,
108529
+ position: renderSemaphore.waitingCount
108530
+ })
108531
+ });
108532
+ }
108533
+ const release = await renderSemaphore.acquire();
108413
108534
  try {
108414
108535
  await executeRenderJob(
108415
108536
  job,
@@ -108477,6 +108598,7 @@ function createRenderHandlers(options = {}) {
108477
108598
  })
108478
108599
  });
108479
108600
  } finally {
108601
+ release();
108480
108602
  c.req.raw.signal.removeEventListener("abort", onRequestAbort);
108481
108603
  cleanupTempDir(cleanupProjectDir, log);
108482
108604
  }
@@ -108501,7 +108623,12 @@ function createRenderHandlers(options = {}) {
108501
108623
  }
108502
108624
  });
108503
108625
  };
108504
- return { render: render2, renderStream, lint, health, outputs };
108626
+ const queue = (c) => c.json({
108627
+ maxConcurrentRenders,
108628
+ activeRenders: renderSemaphore.activeCount,
108629
+ queuedRenders: renderSemaphore.waitingCount
108630
+ });
108631
+ return { render: render2, renderStream, lint, health, outputs, queue };
108505
108632
  }
108506
108633
  function createProducerApp(options = {}) {
108507
108634
  const app = new Hono2();
@@ -108509,6 +108636,7 @@ function createProducerApp(options = {}) {
108509
108636
  app.get("/health", handlers.health);
108510
108637
  app.post("/render", handlers.render);
108511
108638
  app.post("/render/stream", handlers.renderStream);
108639
+ app.get("/render/queue", handlers.queue);
108512
108640
  app.post("/lint", handlers.lint);
108513
108641
  app.get("/outputs/:token", handlers.outputs);
108514
108642
  return app;