@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/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +5 -5
- package/dist/index.js +154 -26
- package/dist/index.js.map +4 -4
- package/dist/public-server.js +154 -26
- package/dist/public-server.js.map +4 -4
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/services/renderOrchestrator.d.ts.map +1 -1
- package/dist/utils/semaphore.d.ts +16 -0
- package/dist/utils/semaphore.d.ts.map +1 -0
- package/package.json +3 -3
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
|
-
|
|
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
|
|
100675
|
-
const
|
|
100676
|
-
|
|
100677
|
-
|
|
100678
|
-
|
|
100679
|
-
|
|
100680
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|