@hyperframes/producer 0.8.189 → 0.8.191
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/distributed.d.ts +1 -0
- package/dist/distributed.js +75 -17
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +61 -61
- package/dist/index.js +76 -30
- package/dist/public-server.js +40 -15
- package/dist/services/distributed/audioSidecar.d.ts +10 -0
- package/dist/services/distributed/chunkFrames.d.ts +2 -2
- package/dist/services/distributed/plan.d.ts +2 -0
- package/dist/services/distributed/renderChunk.d.ts +7 -1
- package/dist/services/render/artifactTransaction.d.ts +14 -11
- package/dist/services/render/frameCacheBudget.d.ts +3 -0
- package/dist/services/render/stages/audioStage.d.ts +1 -0
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1188,6 +1188,14 @@ function materializeExtractedFramesForCompiledDir(extracted, compiledDir, option
|
|
|
1188
1188
|
}
|
|
1189
1189
|
}
|
|
1190
1190
|
|
|
1191
|
+
// src/services/render/frameCacheBudget.ts
|
|
1192
|
+
function frameCacheOptions(cfg, workers) {
|
|
1193
|
+
return {
|
|
1194
|
+
frameDataUriCacheLimit: cfg.frameDataUriCacheLimit,
|
|
1195
|
+
frameDataUriCacheBytesLimitMb: Math.floor(cfg.frameDataUriCacheBytesLimitMb / (workers || 1))
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1191
1199
|
// src/services/render/cleanup.ts
|
|
1192
1200
|
import { freemem } from "node:os";
|
|
1193
1201
|
|
|
@@ -1500,7 +1508,7 @@ async function defaultArtifactDurationProbe(path) {
|
|
|
1500
1508
|
};
|
|
1501
1509
|
}
|
|
1502
1510
|
function buildArtifactExpectation(input) {
|
|
1503
|
-
if (input.outputFormat === "
|
|
1511
|
+
if (input.outputFormat === "gif") return void 0;
|
|
1504
1512
|
if (!Number.isFinite(input.durationSeconds) || input.durationSeconds <= 0) return void 0;
|
|
1505
1513
|
const expectation = {
|
|
1506
1514
|
expectedDurationSeconds: input.durationSeconds,
|
|
@@ -1565,12 +1573,12 @@ function assertDurationWithinTolerance(expectedSeconds, probedSeconds, tolerance
|
|
|
1565
1573
|
`Render artifact is truncated: expected ${expectedSeconds.toFixed(3)}s, probed ${probedSeconds.toFixed(3)}s (deficit ${deficit.toFixed(3)}s exceeds tolerance ${toleranceSeconds.toFixed(3)}s). Artifact: ${stagingPath}`
|
|
1566
1574
|
);
|
|
1567
1575
|
}
|
|
1568
|
-
function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath) {
|
|
1576
|
+
function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath, toleranceFrames = 1) {
|
|
1569
1577
|
if (probedFrames === void 0 || !Number.isFinite(probedFrames) || probedFrames <= 0) {
|
|
1570
1578
|
return;
|
|
1571
1579
|
}
|
|
1572
1580
|
const shortfall = expectedFrames - probedFrames;
|
|
1573
|
-
if (shortfall <=
|
|
1581
|
+
if (shortfall <= toleranceFrames) return;
|
|
1574
1582
|
throw new Error(
|
|
1575
1583
|
`Render artifact is truncated: expected ${expectedFrames} frames, probed ${probedFrames} frames (shortfall ${shortfall} frames). Artifact: ${stagingPath}`
|
|
1576
1584
|
);
|
|
@@ -1607,8 +1615,8 @@ var ArtifactTransaction = class {
|
|
|
1607
1615
|
if (expected) await this.assertArtifactDuration(expected);
|
|
1608
1616
|
return;
|
|
1609
1617
|
}
|
|
1610
|
-
this.assertReadableNonEmptyDirectory();
|
|
1611
|
-
if (expected) await this.assertDirectoryExpectation(expected);
|
|
1618
|
+
const files = this.assertReadableNonEmptyDirectory();
|
|
1619
|
+
if (expected) await this.assertDirectoryExpectation(expected, files);
|
|
1612
1620
|
}
|
|
1613
1621
|
assertReadableNonEmptyDirectory() {
|
|
1614
1622
|
let files;
|
|
@@ -1623,13 +1631,14 @@ var ArtifactTransaction = class {
|
|
|
1623
1631
|
throw new Error(`Render artifact directory is empty: ${this.stagingPath}`);
|
|
1624
1632
|
}
|
|
1625
1633
|
for (const file of files) assertReadableNonEmptyFile(file);
|
|
1634
|
+
return files;
|
|
1626
1635
|
}
|
|
1627
1636
|
/**
|
|
1628
1637
|
* The named-entry and duration gates a directory artifact opts into. A
|
|
1629
|
-
* directory
|
|
1630
|
-
*
|
|
1638
|
+
* directory with a probe target checks duration; one without a probe target
|
|
1639
|
+
* checks the exported PNG frame count when the caller supplied it.
|
|
1631
1640
|
*/
|
|
1632
|
-
async assertDirectoryExpectation(expected) {
|
|
1641
|
+
async assertDirectoryExpectation(expected, files) {
|
|
1633
1642
|
for (const required of expected.requiredFiles ?? []) {
|
|
1634
1643
|
const path = join5(this.stagingPath, required);
|
|
1635
1644
|
if (!existsSync5(path)) {
|
|
@@ -1637,8 +1646,25 @@ var ArtifactTransaction = class {
|
|
|
1637
1646
|
}
|
|
1638
1647
|
assertReadableNonEmptyFile(path);
|
|
1639
1648
|
}
|
|
1640
|
-
if (expected.probeTarget
|
|
1641
|
-
|
|
1649
|
+
if (expected.probeTarget !== void 0) {
|
|
1650
|
+
await this.assertArtifactDuration(expected, join5(this.stagingPath, expected.probeTarget));
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
if (expected.expectedFrames === void 0) return;
|
|
1654
|
+
const frameCount = files.filter(
|
|
1655
|
+
(file) => dirname4(file) === this.stagingPath && /^frame_\d+\.png$/.test(basename3(file))
|
|
1656
|
+
).length;
|
|
1657
|
+
if (frameCount === 0) {
|
|
1658
|
+
throw new Error(
|
|
1659
|
+
`Render artifact is truncated: expected ${expected.expectedFrames} frames, probed 0 frames. Artifact: ${this.stagingPath}`
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
assertFrameCountWithinTolerance(
|
|
1663
|
+
expected.expectedFrames,
|
|
1664
|
+
frameCount,
|
|
1665
|
+
this.stagingPath,
|
|
1666
|
+
Math.max(1, Math.ceil(expected.expectedFrames * 2e-3))
|
|
1667
|
+
);
|
|
1642
1668
|
}
|
|
1643
1669
|
async assertArtifactDuration(expected, probePath = this.stagingPath) {
|
|
1644
1670
|
const expectedSeconds = expected.expectedDurationSeconds;
|
|
@@ -7137,7 +7163,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7137
7163
|
);
|
|
7138
7164
|
if (analysis.isProblematic) {
|
|
7139
7165
|
defaultLogger.warn(
|
|
7140
|
-
`[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s).
|
|
7166
|
+
`[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s). Long GOPs can make preview seeking slower; rendering is unaffected. If preview seeking is slow, re-encode with: ${reencode}`
|
|
7141
7167
|
);
|
|
7142
7168
|
}
|
|
7143
7169
|
if (metadata.isVFR) {
|
|
@@ -9188,6 +9214,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9188
9214
|
audios,
|
|
9189
9215
|
masterFxChain,
|
|
9190
9216
|
urlByAbsentLocalPath,
|
|
9217
|
+
audioSidecarUrlByLocalPath,
|
|
9191
9218
|
abortSignal,
|
|
9192
9219
|
assertNotAborted,
|
|
9193
9220
|
log
|
|
@@ -9211,6 +9238,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9211
9238
|
audioGain,
|
|
9212
9239
|
masterFxChain,
|
|
9213
9240
|
urlByAbsentLocalPath,
|
|
9241
|
+
audioSidecarUrlByLocalPath,
|
|
9214
9242
|
keepEncoderInputAt: pcmSourcePath
|
|
9215
9243
|
},
|
|
9216
9244
|
compiledDir
|
|
@@ -12515,10 +12543,7 @@ async function executeRenderPipeline(input) {
|
|
|
12515
12543
|
});
|
|
12516
12544
|
}
|
|
12517
12545
|
});
|
|
12518
|
-
const createRenderVideoFrameInjector = () => createVideoFrameInjector(frameLookup,
|
|
12519
|
-
frameDataUriCacheLimit: cfg.frameDataUriCacheLimit,
|
|
12520
|
-
frameDataUriCacheBytesLimitMb: cfg.frameDataUriCacheBytesLimitMb
|
|
12521
|
-
});
|
|
12546
|
+
const createRenderVideoFrameInjector = () => createVideoFrameInjector(frameLookup, frameCacheOptions(cfg, workerCount));
|
|
12522
12547
|
let captureCalibration;
|
|
12523
12548
|
const htmlInCanvasDetected = compiled.renderModeHints.reasons.some(
|
|
12524
12549
|
(r) => r.code === "htmlInCanvas"
|
|
@@ -14172,12 +14197,13 @@ var RenderChunkValidationError = class extends Error {
|
|
|
14172
14197
|
this.code = code;
|
|
14173
14198
|
}
|
|
14174
14199
|
};
|
|
14175
|
-
function createChunkVideoFrameInjectorFactory(frameLookup) {
|
|
14176
|
-
if (!(frameLookup instanceof Promise))
|
|
14200
|
+
function createChunkVideoFrameInjectorFactory(frameLookup, options) {
|
|
14201
|
+
if (!(frameLookup instanceof Promise))
|
|
14202
|
+
return () => createVideoFrameInjector2(frameLookup, options);
|
|
14177
14203
|
return () => {
|
|
14178
14204
|
let hook;
|
|
14179
14205
|
return async (page, time, heldVideoTime) => {
|
|
14180
|
-
hook ??= createVideoFrameInjector2(await frameLookup);
|
|
14206
|
+
hook ??= createVideoFrameInjector2(await frameLookup, options);
|
|
14181
14207
|
await hook?.(page, time, heldVideoTime);
|
|
14182
14208
|
};
|
|
14183
14209
|
};
|
|
@@ -15895,6 +15921,11 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
15895
15921
|
const localPathByUrl = new Map(
|
|
15896
15922
|
remoteFiles.map(([rel, file]) => [file.url, remoteLocalPath(rel)])
|
|
15897
15923
|
);
|
|
15924
|
+
const audioSidecarUrlByLocalPath = new Map(
|
|
15925
|
+
remoteFiles.flatMap(
|
|
15926
|
+
([rel, file]) => file.audioSidecarUrl ? [[remoteLocalPath(rel), file.audioSidecarUrl]] : []
|
|
15927
|
+
)
|
|
15928
|
+
);
|
|
15898
15929
|
stageStarted = Date.now();
|
|
15899
15930
|
const compileResult = await runCompileStage({
|
|
15900
15931
|
urlByAbsentLocalPath,
|
|
@@ -15979,6 +16010,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
15979
16010
|
finalCompiledDir,
|
|
15980
16011
|
remoteFiles,
|
|
15981
16012
|
urlByAbsentLocalPath,
|
|
16013
|
+
audioSidecarUrlByLocalPath,
|
|
15982
16014
|
localPathByUrl,
|
|
15983
16015
|
compileResult,
|
|
15984
16016
|
compiled,
|
|
@@ -15996,6 +16028,7 @@ function mixPlanAudio(c, compiledDir) {
|
|
|
15996
16028
|
workDir: c.workDir,
|
|
15997
16029
|
compiledDir,
|
|
15998
16030
|
urlByAbsentLocalPath: c.urlByAbsentLocalPath,
|
|
16031
|
+
audioSidecarUrlByLocalPath: c.audioSidecarUrlByLocalPath,
|
|
15999
16032
|
totalFrames: c.totalFrames,
|
|
16000
16033
|
fps: c.job.config.fps,
|
|
16001
16034
|
ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
|
|
@@ -18975,6 +19008,22 @@ async function createVerifiedDistributedCaptureSession(serverUrl, framesDir, cap
|
|
|
18975
19008
|
function isCaptureMode(value) {
|
|
18976
19009
|
return value === "beginframe" || value === "screenshot" || value === "drawelement";
|
|
18977
19010
|
}
|
|
19011
|
+
var MixedCaptureModesError = class extends Error {
|
|
19012
|
+
constructor(modes) {
|
|
19013
|
+
super(`[renderChunk] capture workers reported inconsistent modes: ${modes.join(",")}`);
|
|
19014
|
+
this.modes = modes;
|
|
19015
|
+
this.name = "MixedCaptureModesError";
|
|
19016
|
+
}
|
|
19017
|
+
};
|
|
19018
|
+
function observedChunkCaptureMode(perfs) {
|
|
19019
|
+
const modes = [...new Set(perfs.map((perf) => perf.captureMode))];
|
|
19020
|
+
if (modes.length > 1) throw new MixedCaptureModesError(modes);
|
|
19021
|
+
const [mode] = modes;
|
|
19022
|
+
if (!mode || !isCaptureMode(mode)) {
|
|
19023
|
+
throw new Error(`[renderChunk] capture workers reported an invalid mode: ${mode ?? "<none>"}`);
|
|
19024
|
+
}
|
|
19025
|
+
return mode;
|
|
19026
|
+
}
|
|
18978
19027
|
function shouldRetryChunkCaptureWithScreenshot(error) {
|
|
18979
19028
|
const failure = classifyCaptureFailure4(error);
|
|
18980
19029
|
if (failure.kind === "cancelled" || failure.kind === "memory_exhaustion") return false;
|
|
@@ -19272,7 +19321,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19272
19321
|
) : null;
|
|
19273
19322
|
videoFrameLookup?.catch(() => {
|
|
19274
19323
|
});
|
|
19275
|
-
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(
|
|
19324
|
+
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(
|
|
19325
|
+
videoFrameLookup,
|
|
19326
|
+
frameCacheOptions(cfg, chunkWorkerCount)
|
|
19327
|
+
);
|
|
19276
19328
|
const captureStarted = Date.now();
|
|
19277
19329
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
19278
19330
|
forceScreenshot: forceScreenshotForChunk,
|
|
@@ -19326,17 +19378,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19326
19378
|
},
|
|
19327
19379
|
frameRange: range
|
|
19328
19380
|
});
|
|
19329
|
-
|
|
19330
|
-
if (observedModes.size !== 1 || ![...observedModes].every(isCaptureMode)) {
|
|
19331
|
-
throw new Error(
|
|
19332
|
-
`[renderChunk] capture workers reported invalid or inconsistent modes: ${[...observedModes].join(",") || "<none>"}`
|
|
19333
|
-
);
|
|
19334
|
-
}
|
|
19335
|
-
const [observedMode] = observedModes;
|
|
19336
|
-
if (!observedMode || !isCaptureMode(observedMode)) {
|
|
19337
|
-
throw new Error("[renderChunk] capture completed without an observed mode");
|
|
19338
|
-
}
|
|
19339
|
-
return observedMode;
|
|
19381
|
+
return observedChunkCaptureMode(capturePerfs);
|
|
19340
19382
|
},
|
|
19341
19383
|
resetForScreenshotRetry: () => {
|
|
19342
19384
|
rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
@@ -19541,6 +19583,10 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
19541
19583
|
|
|
19542
19584
|
// src/services/distributed/projectHash.ts
|
|
19543
19585
|
var READ_CHUNK_BYTES = 256 * 1024;
|
|
19586
|
+
|
|
19587
|
+
// src/services/distributed/audioSidecar.ts
|
|
19588
|
+
import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as runFfmpeg8 } from "@hyperframes/engine";
|
|
19589
|
+
var AUDIO_SIDECAR_MIN_BYTES = 32 * 1024 * 1024;
|
|
19544
19590
|
export {
|
|
19545
19591
|
CURRENT_PLAN_PROTOCOL,
|
|
19546
19592
|
DEFAULT_CONFIG2 as DEFAULT_CONFIG,
|
package/dist/public-server.js
CHANGED
|
@@ -1208,6 +1208,14 @@ function materializeExtractedFramesForCompiledDir(extracted, compiledDir, option
|
|
|
1208
1208
|
}
|
|
1209
1209
|
}
|
|
1210
1210
|
|
|
1211
|
+
// src/services/render/frameCacheBudget.ts
|
|
1212
|
+
function frameCacheOptions(cfg, workers) {
|
|
1213
|
+
return {
|
|
1214
|
+
frameDataUriCacheLimit: cfg.frameDataUriCacheLimit,
|
|
1215
|
+
frameDataUriCacheBytesLimitMb: Math.floor(cfg.frameDataUriCacheBytesLimitMb / (workers || 1))
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1211
1219
|
// src/services/render/cleanup.ts
|
|
1212
1220
|
import { freemem } from "node:os";
|
|
1213
1221
|
|
|
@@ -1519,7 +1527,7 @@ async function defaultArtifactDurationProbe(path) {
|
|
|
1519
1527
|
};
|
|
1520
1528
|
}
|
|
1521
1529
|
function buildArtifactExpectation(input) {
|
|
1522
|
-
if (input.outputFormat === "
|
|
1530
|
+
if (input.outputFormat === "gif") return void 0;
|
|
1523
1531
|
if (!Number.isFinite(input.durationSeconds) || input.durationSeconds <= 0) return void 0;
|
|
1524
1532
|
const expectation = {
|
|
1525
1533
|
expectedDurationSeconds: input.durationSeconds,
|
|
@@ -1584,12 +1592,12 @@ function assertDurationWithinTolerance(expectedSeconds, probedSeconds, tolerance
|
|
|
1584
1592
|
`Render artifact is truncated: expected ${expectedSeconds.toFixed(3)}s, probed ${probedSeconds.toFixed(3)}s (deficit ${deficit.toFixed(3)}s exceeds tolerance ${toleranceSeconds.toFixed(3)}s). Artifact: ${stagingPath}`
|
|
1585
1593
|
);
|
|
1586
1594
|
}
|
|
1587
|
-
function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath) {
|
|
1595
|
+
function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath, toleranceFrames = 1) {
|
|
1588
1596
|
if (probedFrames === void 0 || !Number.isFinite(probedFrames) || probedFrames <= 0) {
|
|
1589
1597
|
return;
|
|
1590
1598
|
}
|
|
1591
1599
|
const shortfall = expectedFrames - probedFrames;
|
|
1592
|
-
if (shortfall <=
|
|
1600
|
+
if (shortfall <= toleranceFrames) return;
|
|
1593
1601
|
throw new Error(
|
|
1594
1602
|
`Render artifact is truncated: expected ${expectedFrames} frames, probed ${probedFrames} frames (shortfall ${shortfall} frames). Artifact: ${stagingPath}`
|
|
1595
1603
|
);
|
|
@@ -1626,8 +1634,8 @@ var ArtifactTransaction = class {
|
|
|
1626
1634
|
if (expected) await this.assertArtifactDuration(expected);
|
|
1627
1635
|
return;
|
|
1628
1636
|
}
|
|
1629
|
-
this.assertReadableNonEmptyDirectory();
|
|
1630
|
-
if (expected) await this.assertDirectoryExpectation(expected);
|
|
1637
|
+
const files = this.assertReadableNonEmptyDirectory();
|
|
1638
|
+
if (expected) await this.assertDirectoryExpectation(expected, files);
|
|
1631
1639
|
}
|
|
1632
1640
|
assertReadableNonEmptyDirectory() {
|
|
1633
1641
|
let files;
|
|
@@ -1642,13 +1650,14 @@ var ArtifactTransaction = class {
|
|
|
1642
1650
|
throw new Error(`Render artifact directory is empty: ${this.stagingPath}`);
|
|
1643
1651
|
}
|
|
1644
1652
|
for (const file of files) assertReadableNonEmptyFile(file);
|
|
1653
|
+
return files;
|
|
1645
1654
|
}
|
|
1646
1655
|
/**
|
|
1647
1656
|
* The named-entry and duration gates a directory artifact opts into. A
|
|
1648
|
-
* directory
|
|
1649
|
-
*
|
|
1657
|
+
* directory with a probe target checks duration; one without a probe target
|
|
1658
|
+
* checks the exported PNG frame count when the caller supplied it.
|
|
1650
1659
|
*/
|
|
1651
|
-
async assertDirectoryExpectation(expected) {
|
|
1660
|
+
async assertDirectoryExpectation(expected, files) {
|
|
1652
1661
|
for (const required of expected.requiredFiles ?? []) {
|
|
1653
1662
|
const path = join5(this.stagingPath, required);
|
|
1654
1663
|
if (!existsSync5(path)) {
|
|
@@ -1656,8 +1665,25 @@ var ArtifactTransaction = class {
|
|
|
1656
1665
|
}
|
|
1657
1666
|
assertReadableNonEmptyFile(path);
|
|
1658
1667
|
}
|
|
1659
|
-
if (expected.probeTarget
|
|
1660
|
-
|
|
1668
|
+
if (expected.probeTarget !== void 0) {
|
|
1669
|
+
await this.assertArtifactDuration(expected, join5(this.stagingPath, expected.probeTarget));
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
if (expected.expectedFrames === void 0) return;
|
|
1673
|
+
const frameCount = files.filter(
|
|
1674
|
+
(file) => dirname4(file) === this.stagingPath && /^frame_\d+\.png$/.test(basename3(file))
|
|
1675
|
+
).length;
|
|
1676
|
+
if (frameCount === 0) {
|
|
1677
|
+
throw new Error(
|
|
1678
|
+
`Render artifact is truncated: expected ${expected.expectedFrames} frames, probed 0 frames. Artifact: ${this.stagingPath}`
|
|
1679
|
+
);
|
|
1680
|
+
}
|
|
1681
|
+
assertFrameCountWithinTolerance(
|
|
1682
|
+
expected.expectedFrames,
|
|
1683
|
+
frameCount,
|
|
1684
|
+
this.stagingPath,
|
|
1685
|
+
Math.max(1, Math.ceil(expected.expectedFrames * 2e-3))
|
|
1686
|
+
);
|
|
1661
1687
|
}
|
|
1662
1688
|
async assertArtifactDuration(expected, probePath = this.stagingPath) {
|
|
1663
1689
|
const expectedSeconds = expected.expectedDurationSeconds;
|
|
@@ -7156,7 +7182,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7156
7182
|
);
|
|
7157
7183
|
if (analysis.isProblematic) {
|
|
7158
7184
|
defaultLogger.warn(
|
|
7159
|
-
`[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s).
|
|
7185
|
+
`[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s). Long GOPs can make preview seeking slower; rendering is unaffected. If preview seeking is slow, re-encode with: ${reencode}`
|
|
7160
7186
|
);
|
|
7161
7187
|
}
|
|
7162
7188
|
if (metadata.isVFR) {
|
|
@@ -9171,6 +9197,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9171
9197
|
audios,
|
|
9172
9198
|
masterFxChain,
|
|
9173
9199
|
urlByAbsentLocalPath,
|
|
9200
|
+
audioSidecarUrlByLocalPath,
|
|
9174
9201
|
abortSignal,
|
|
9175
9202
|
assertNotAborted,
|
|
9176
9203
|
log
|
|
@@ -9194,6 +9221,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9194
9221
|
audioGain,
|
|
9195
9222
|
masterFxChain,
|
|
9196
9223
|
urlByAbsentLocalPath,
|
|
9224
|
+
audioSidecarUrlByLocalPath,
|
|
9197
9225
|
keepEncoderInputAt: pcmSourcePath
|
|
9198
9226
|
},
|
|
9199
9227
|
compiledDir
|
|
@@ -12498,10 +12526,7 @@ async function executeRenderPipeline(input) {
|
|
|
12498
12526
|
});
|
|
12499
12527
|
}
|
|
12500
12528
|
});
|
|
12501
|
-
const createRenderVideoFrameInjector = () => createVideoFrameInjector(frameLookup,
|
|
12502
|
-
frameDataUriCacheLimit: cfg.frameDataUriCacheLimit,
|
|
12503
|
-
frameDataUriCacheBytesLimitMb: cfg.frameDataUriCacheBytesLimitMb
|
|
12504
|
-
});
|
|
12529
|
+
const createRenderVideoFrameInjector = () => createVideoFrameInjector(frameLookup, frameCacheOptions(cfg, workerCount));
|
|
12505
12530
|
let captureCalibration;
|
|
12506
12531
|
const htmlInCanvasDetected = compiled.renderModeHints.reasons.some(
|
|
12507
12532
|
(r) => r.code === "htmlInCanvas"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A video's audio copied out of its container, nothing re-encoded, stored beside it. Reading audio from the
|
|
3
|
+
* cloud file moves the whole interleaved file; the sidecar is a few MB.
|
|
4
|
+
*/
|
|
5
|
+
/** Smaller files are cheap to read whole. */
|
|
6
|
+
export declare const AUDIO_SIDECAR_MIN_BYTES: number;
|
|
7
|
+
export declare function wantsAudioSidecar(path: string, sizeBytes: number): boolean;
|
|
8
|
+
/** `unavailable`: no ffmpeg on this machine. `none`: no audio, or audio the sidecar would not read back the same. */
|
|
9
|
+
export type AudioSidecarOutcome = "built" | "none" | "unavailable";
|
|
10
|
+
export declare function buildAudioSidecar(inputPath: string, outputPath: string, signal?: AbortSignal): Promise<AudioSidecarOutcome>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BeforeCaptureHook, BROWSER_GPU_NOT_SOFTWARE, type CaptureOptions, type EngineConfig, type ExtractedFrames, type FrameLookupTable, type ShownBox } from "@hyperframes/engine";
|
|
1
|
+
import { type BeforeCaptureHook, BROWSER_GPU_NOT_SOFTWARE, type CaptureOptions, createVideoFrameInjector, type EngineConfig, type ExtractedFrames, type FrameLookupTable, type ShownBox } from "@hyperframes/engine";
|
|
2
2
|
import type { LockedRenderConfig } from "../render/stages/freezePlan.js";
|
|
3
3
|
import type { PlanV2RemoteSources } from "./planV2.js";
|
|
4
4
|
import { type DistributedFormat, INVALID_VIDEO_METADATA, type PlanVideosJson } from "./shared.js";
|
|
@@ -17,7 +17,7 @@ export declare class RenderChunkValidationError extends Error {
|
|
|
17
17
|
constructor(code: RenderChunkValidationCode, message: string);
|
|
18
18
|
}
|
|
19
19
|
/** One injector per browser session: a hook reused on a fresh page can skip its first injection. */
|
|
20
|
-
export declare function createChunkVideoFrameInjectorFactory(frameLookup: FrameLookupTable | Promise<FrameLookupTable> | null): () => BeforeCaptureHook | null;
|
|
20
|
+
export declare function createChunkVideoFrameInjectorFactory(frameLookup: FrameLookupTable | Promise<FrameLookupTable> | null, options?: Parameters<typeof createVideoFrameInjector>[1]): () => BeforeCaptureHook | null;
|
|
21
21
|
/**
|
|
22
22
|
* Rebuilds `ExtractedFrames[]` from `<planDir>/video-frames/<videoId>/`. `framePaths` keys are 0-based
|
|
23
23
|
* frame indexes (what `getFrameAtTime` reads); any other key drops every video's first-paint frame.
|
|
@@ -217,6 +217,8 @@ export interface BuildLocalExecutionPlanOptions {
|
|
|
217
217
|
}
|
|
218
218
|
export interface RemoteProjectFile {
|
|
219
219
|
readonly url: string;
|
|
220
|
+
/** A file of only this video's audio, when one was stored: the audio mix reads it, not the video. */
|
|
221
|
+
readonly audioSidecarUrl?: string;
|
|
220
222
|
readonly sha256: string;
|
|
221
223
|
readonly sizeBytes: number;
|
|
222
224
|
}
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* Every determinism toggle above is opt-in — only this primitive enables them.
|
|
34
34
|
* In-process renders (`executeRenderJob`) leave them off.
|
|
35
35
|
*/
|
|
36
|
-
import { assertSwiftShader, type CaptureOptions, type CaptureMode, type CaptureSession, closeCaptureSession, createCaptureSession, type EngineConfig, initializeSession, probeBeginFrameLiveness, readWebGlVendorInfoFromCanvas } from "@hyperframes/engine";
|
|
36
|
+
import { assertSwiftShader, type CaptureOptions, type CaptureMode, type CapturePerfSummary, type CaptureSession, closeCaptureSession, createCaptureSession, type EngineConfig, initializeSession, probeBeginFrameLiveness, readWebGlVendorInfoFromCanvas } from "@hyperframes/engine";
|
|
37
37
|
import { type ChunkSliceJson, type LockedRenderConfig } from "../render/stages/freezePlan.js";
|
|
38
38
|
import { type PlanVideosJson } from "./shared.js";
|
|
39
39
|
export { CHUNK_FRAME_RANGE_INVALID, CHUNK_INDEX_OUT_OF_RANGE, createChunkVideoFrameInjectorFactory, extractDeferredVideoFramesForChunk, FFMPEG_VERSION_MISMATCH, INVALID_VIDEO_METADATA, MISSING_PLAN_ARTIFACT, MISSING_RUNTIME_ENV_SNAPSHOT, PLAN_HASH_MISMATCH, rebuildExtractedFramesFromPlanDir, RenderChunkValidationError, } from "./chunkFrames.js";
|
|
@@ -136,6 +136,12 @@ export declare const distributedCaptureSessionDependencies: DistributedCaptureSe
|
|
|
136
136
|
* assertion, including fresh screenshot browsers created after a fallback.
|
|
137
137
|
*/
|
|
138
138
|
export declare function createVerifiedDistributedCaptureSession(serverUrl: string, framesDir: string, captureOptions: CaptureOptions, cfg: EngineConfig, dependencies?: DistributedCaptureSessionDependencies): Promise<CaptureSession>;
|
|
139
|
+
/** Workers of one chunk launched in different modes: one worker's BeginFrame check failed and it fell back alone. */
|
|
140
|
+
export declare class MixedCaptureModesError extends Error {
|
|
141
|
+
readonly modes: readonly string[];
|
|
142
|
+
constructor(modes: readonly string[]);
|
|
143
|
+
}
|
|
144
|
+
export declare function observedChunkCaptureMode(perfs: readonly Pick<CapturePerfSummary, "captureMode">[]): CaptureMode;
|
|
139
145
|
/**
|
|
140
146
|
* Only BeginFrame-specific failures are safe to retry in screenshot mode.
|
|
141
147
|
* Cancellation, memory exhaustion, and unrelated authoring/IO failures must
|
|
@@ -37,7 +37,7 @@ export interface ArtifactDurationProbeResult {
|
|
|
37
37
|
*/
|
|
38
38
|
export type ArtifactDurationProbe = (path: string) => Promise<ArtifactDurationProbeResult>;
|
|
39
39
|
/**
|
|
40
|
-
* Caller-supplied expectation for
|
|
40
|
+
* Caller-supplied expectation for artifact validation. The transaction
|
|
41
41
|
* still does the readable-non-empty check; this layer adds a duration /
|
|
42
42
|
* frame-count comparison against the values the pipeline already held.
|
|
43
43
|
*
|
|
@@ -48,15 +48,15 @@ export type ArtifactDurationProbe = (path: string) => Promise<ArtifactDurationPr
|
|
|
48
48
|
*
|
|
49
49
|
* `toleranceSeconds` defaults to a single frame at `fps` (or 20 ms when fps
|
|
50
50
|
* is unknown) so that a normal container-level last-frame rounding does not
|
|
51
|
-
* trip the gate. Multi-frame drops
|
|
52
|
-
* #3395) still fail.
|
|
51
|
+
* trip the gate. Multi-frame drops beyond that tolerance still fail.
|
|
53
52
|
*
|
|
54
53
|
* `probeTarget` / `requiredFiles` exist for directory artifacts. A `"file"`
|
|
55
54
|
* artifact is its own probe target; a directory has no single one, so an HLS
|
|
56
55
|
* output names its video playlist (`probeTarget: "video.m3u8"`) and the files
|
|
57
56
|
* that must be present (`requiredFiles: ["master.m3u8"]`). Without
|
|
58
|
-
* `probeTarget
|
|
59
|
-
*
|
|
57
|
+
* `probeTarget`, an `expectedFrames` value instead checks the top-level PNG
|
|
58
|
+
* frame files. Directory artifacts without either keep their historical
|
|
59
|
+
* readable-non-empty check.
|
|
60
60
|
*/
|
|
61
61
|
export interface ArtifactValidationExpectation {
|
|
62
62
|
expectedDurationSeconds: number;
|
|
@@ -75,8 +75,10 @@ export interface ArtifactValidationExpectation {
|
|
|
75
75
|
* The per-format expectation the render pipeline hands `validate()`, or
|
|
76
76
|
* `undefined` when the format has nothing probeable.
|
|
77
77
|
*
|
|
78
|
-
* - `png-sequence
|
|
79
|
-
*
|
|
78
|
+
* - `png-sequence`: count exported PNG frames without a duration probe.
|
|
79
|
+
* `expectedDurationSeconds` is carried by the shared expectation but is
|
|
80
|
+
* unused for this directory format.
|
|
81
|
+
* - `gif`: no expectation. The GIF encoder writes `outputPath` itself without a
|
|
80
82
|
* frame-accurate duration the pipeline could compare against.
|
|
81
83
|
* - `hls`: the directory's `video.m3u8` is the probe target and `master.m3u8`
|
|
82
84
|
* must exist. `expectedFrames` is passed through but is inert — ffprobe
|
|
@@ -104,8 +106,9 @@ export declare function buildArtifactExpectation(input: {
|
|
|
104
106
|
* transaction additionally probes the staged artifact's container duration
|
|
105
107
|
* (and decoded frame count when available) and rejects any artifact that is
|
|
106
108
|
* significantly shorter than what the pipeline asked for. A directory artifact
|
|
107
|
-
* is probed only when `expected.probeTarget` names a file inside it.
|
|
108
|
-
*
|
|
109
|
+
* is probed only when `expected.probeTarget` names a file inside it.
|
|
110
|
+
* Without a directory probe target, the expectation instead checks exported
|
|
111
|
+
* PNG frame files. The readable-non-empty check is a separate gate.
|
|
109
112
|
*/
|
|
110
113
|
export declare class ArtifactTransaction {
|
|
111
114
|
private readonly kind;
|
|
@@ -121,8 +124,8 @@ export declare class ArtifactTransaction {
|
|
|
121
124
|
private assertReadableNonEmptyDirectory;
|
|
122
125
|
/**
|
|
123
126
|
* The named-entry and duration gates a directory artifact opts into. A
|
|
124
|
-
* directory
|
|
125
|
-
*
|
|
127
|
+
* directory with a probe target checks duration; one without a probe target
|
|
128
|
+
* checks the exported PNG frame count when the caller supplied it.
|
|
126
129
|
*/
|
|
127
130
|
private assertDirectoryExpectation;
|
|
128
131
|
private assertArtifactDuration;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { EngineConfig } from "@hyperframes/engine";
|
|
2
|
+
/** Cache options for one of `workers` injectors that share the configured budget on the one Node heap. */
|
|
3
|
+
export declare function frameCacheOptions(cfg: Pick<EngineConfig, "frameDataUriCacheLimit" | "frameDataUriCacheBytesLimitMb">, workers: number | undefined): Pick<EngineConfig, "frameDataUriCacheLimit" | "frameDataUriCacheBytesLimitMb">;
|
|
@@ -36,6 +36,7 @@ export interface AudioStageInput {
|
|
|
36
36
|
audios: CompositionMetadata["audios"];
|
|
37
37
|
masterFxChain?: string;
|
|
38
38
|
urlByAbsentLocalPath?: ReadonlyMap<string, string>;
|
|
39
|
+
audioSidecarUrlByLocalPath?: ReadonlyMap<string, string>;
|
|
39
40
|
abortSignal: AbortSignal | undefined;
|
|
40
41
|
assertNotAborted: () => void;
|
|
41
42
|
/** Where a per-track failure's detail goes. Optional so tests need not pass one. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.191",
|
|
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.2.1",
|
|
37
37
|
"puppeteer-core": "^25.2.1",
|
|
38
|
-
"@hyperframes/
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/studio-server": "0.8.
|
|
42
|
-
"@hyperframes/
|
|
38
|
+
"@hyperframes/lint": "0.8.191",
|
|
39
|
+
"@hyperframes/core": "0.8.191",
|
|
40
|
+
"@hyperframes/parsers": "0.8.191",
|
|
41
|
+
"@hyperframes/studio-server": "0.8.191",
|
|
42
|
+
"@hyperframes/engine": "0.8.191"
|
|
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.8.
|
|
51
|
+
"@hyperframes/aws-lambda": "0.8.191"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|