@hyperframes/producer 0.2.3-alpha.2 → 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/public-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
103464
|
-
const
|
|
103465
|
-
|
|
103466
|
-
|
|
103467
|
-
|
|
103468
|
-
|
|
103469
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|