@hyperframes/producer 0.8.166 → 0.8.168
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 -1
- package/dist/distributed.js +417 -263
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +72 -70
- package/dist/index.js +2881 -2707
- package/dist/plan-parity-contract.d.ts +2 -1
- package/dist/public-server.js +114 -18
- package/dist/regression-harness-distributed.d.ts +5 -5
- package/dist/regression-harness-lambda-local-types.d.ts +2 -1
- package/dist/services/distributed/assemble.d.ts +4 -8
- package/dist/services/distributed/plan.d.ts +37 -9
- package/dist/services/distributed/planV2.d.ts +3 -2
- package/dist/services/render/audioPadTrim.d.ts +3 -2
- package/dist/services/render/outputScaleRoute.d.ts +8 -0
- package/dist/services/render/shared.d.ts +4 -6
- package/dist/services/render/stages/freezePlan.d.ts +7 -1
- package/dist/services/render/stages/planHash.d.ts +3 -0
- package/dist/services/renderOrchestrator.d.ts +1 -1
- package/package.json +7 -7
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* different artifact layouts and hash schemas, so their plan hashes are not
|
|
6
6
|
* expected to match even when they render identical output.
|
|
7
7
|
*/
|
|
8
|
+
import type { DistributedFps } from "@hyperframes/core";
|
|
8
9
|
export type PlanParityProtocol = "v1" | "v2";
|
|
9
10
|
export interface PlanParityRenderConfig {
|
|
10
|
-
fps:
|
|
11
|
+
fps: DistributedFps;
|
|
11
12
|
width: number;
|
|
12
13
|
height: number;
|
|
13
14
|
format: "mp4";
|
package/dist/public-server.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
rmSync as rmSync10,
|
|
11
11
|
createReadStream as createReadStream3
|
|
12
12
|
} from "node:fs";
|
|
13
|
-
import { resolve as resolve9, dirname as dirname12, join as
|
|
13
|
+
import { resolve as resolve9, dirname as dirname12, join as join30 } from "node:path";
|
|
14
14
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
15
15
|
import { parseArgs } from "node:util";
|
|
16
16
|
import crypto from "node:crypto";
|
|
@@ -939,7 +939,7 @@ function resolveDeviceScaleFactor(input) {
|
|
|
939
939
|
});
|
|
940
940
|
if (!compat.ok) throw new Error(compat.message);
|
|
941
941
|
const target = CANVAS_DIMENSIONS[input.outputResolution];
|
|
942
|
-
return target.width / input.compositionWidth;
|
|
942
|
+
return Math.max(target.width / input.compositionWidth, target.height / input.compositionHeight);
|
|
943
943
|
}
|
|
944
944
|
function writeCompiledArtifacts(compiled, workDir, includeSummary) {
|
|
945
945
|
const compileDir = join3(workDir, "compiled");
|
|
@@ -7593,10 +7593,10 @@ async function runCompileStage(input) {
|
|
|
7593
7593
|
hdrRequested: job.config.hdrMode === "force-hdr",
|
|
7594
7594
|
alphaRequested: needsAlpha
|
|
7595
7595
|
});
|
|
7596
|
-
const outputWidth = width * deviceScaleFactor;
|
|
7597
|
-
const outputHeight = height * deviceScaleFactor;
|
|
7598
|
-
if (deviceScaleFactor
|
|
7599
|
-
log.info("
|
|
7596
|
+
const outputWidth = Math.round(width * deviceScaleFactor);
|
|
7597
|
+
const outputHeight = Math.round(height * deviceScaleFactor);
|
|
7598
|
+
if (deviceScaleFactor !== 1) {
|
|
7599
|
+
log.info("Scaling composition via deviceScaleFactor", {
|
|
7600
7600
|
compositionWidth: width,
|
|
7601
7601
|
compositionHeight: height,
|
|
7602
7602
|
outputResolution: effectiveResolution,
|
|
@@ -10876,6 +10876,22 @@ function assertMotionBlurSupported(motionBlur, route) {
|
|
|
10876
10876
|
);
|
|
10877
10877
|
}
|
|
10878
10878
|
|
|
10879
|
+
// src/services/render/outputScaleRoute.ts
|
|
10880
|
+
var UNSUPPORTED_REASON2 = {
|
|
10881
|
+
sdr_streaming: null,
|
|
10882
|
+
sdr_disk: null,
|
|
10883
|
+
sdr_segmented: null,
|
|
10884
|
+
hdr_layered: "the layered compositor, used for HDR content and for shader transitions in webm and mov, sizes its layers and its encoder from the composition"
|
|
10885
|
+
};
|
|
10886
|
+
function assertOutputScaleSupported(deviceScaleFactor, route) {
|
|
10887
|
+
if (deviceScaleFactor === 1) return;
|
|
10888
|
+
const reason = UNSUPPORTED_REASON2[route];
|
|
10889
|
+
if (reason === null) return;
|
|
10890
|
+
throw new Error(
|
|
10891
|
+
`[Render] --resolution other than the composition's size is not supported on the "${route}" capture route: ${reason}. Render at the composition's size, or as mp4 with page-side compositing.`
|
|
10892
|
+
);
|
|
10893
|
+
}
|
|
10894
|
+
|
|
10879
10895
|
// src/services/renderOrchestrator.ts
|
|
10880
10896
|
function sampleDirectoryBytes(dir) {
|
|
10881
10897
|
let total = 0;
|
|
@@ -11663,7 +11679,7 @@ async function closeOrphanedProbeForRetry(probe, closer, log, retryContext) {
|
|
|
11663
11679
|
}
|
|
11664
11680
|
}
|
|
11665
11681
|
function resolveParallelCaptureMode(args) {
|
|
11666
|
-
const supersampling = (args.deviceScaleFactor ?? 1)
|
|
11682
|
+
const supersampling = (args.deviceScaleFactor ?? 1) !== 1;
|
|
11667
11683
|
return args.headlessShell && args.platform === "linux" && !args.forceScreenshot && !supersampling ? "beginframe" : "screenshot";
|
|
11668
11684
|
}
|
|
11669
11685
|
function isCaptureParallelStreamRouterEnabled(env, resolvedMode) {
|
|
@@ -12311,7 +12327,7 @@ async function executeRenderPipeline(input) {
|
|
|
12311
12327
|
minFrames: deSingleMinFrames,
|
|
12312
12328
|
singleWorkerStreamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
|
|
12313
12329
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12314
|
-
supersampling: deviceScaleFactor
|
|
12330
|
+
supersampling: deviceScaleFactor !== 1,
|
|
12315
12331
|
probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
|
|
12316
12332
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || // Verified parallel DE streaming (opt-in) wants its parallelism kept.
|
|
12317
12333
|
process.env.HF_DE_PARALLEL_STREAM === "true"
|
|
@@ -12347,7 +12363,7 @@ async function executeRenderPipeline(input) {
|
|
|
12347
12363
|
totalFrames,
|
|
12348
12364
|
minFrames: deParallelMinFrames,
|
|
12349
12365
|
layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
|
|
12350
|
-
supersampling: deviceScaleFactor
|
|
12366
|
+
supersampling: deviceScaleFactor !== 1,
|
|
12351
12367
|
probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
|
|
12352
12368
|
experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
|
|
12353
12369
|
routerEnabled: deParallelRouterEnabled,
|
|
@@ -12731,6 +12747,7 @@ async function executeRenderPipeline(input) {
|
|
|
12731
12747
|
});
|
|
12732
12748
|
const syncCapturePlan = () => {
|
|
12733
12749
|
assertMotionBlurSupported(job.config.motionBlur, capturePlan.kind);
|
|
12750
|
+
assertOutputScaleSupported(deviceScaleFactor, capturePlan.kind);
|
|
12734
12751
|
workerCount = capturePlan.workerCount;
|
|
12735
12752
|
captureForceScreenshot = capturePlan.forceScreenshot;
|
|
12736
12753
|
useStreamingEncode = capturePlan.kind === "sdr_streaming";
|
|
@@ -12904,8 +12921,8 @@ async function executeRenderPipeline(input) {
|
|
|
12904
12921
|
outputFormat,
|
|
12905
12922
|
streamingEncoderOptions: {
|
|
12906
12923
|
fps: job.config.fps,
|
|
12907
|
-
width,
|
|
12908
|
-
height,
|
|
12924
|
+
width: captureCompositionWidth ?? width,
|
|
12925
|
+
height: captureCompositionHeight ?? height,
|
|
12909
12926
|
codec: preset.codec,
|
|
12910
12927
|
preset: preset.preset,
|
|
12911
12928
|
quality: effectiveQuality,
|
|
@@ -12996,8 +13013,8 @@ async function executeRenderPipeline(input) {
|
|
|
12996
13013
|
outputFormat,
|
|
12997
13014
|
streamingEncoderOptions: {
|
|
12998
13015
|
fps: job.config.fps,
|
|
12999
|
-
width,
|
|
13000
|
-
height,
|
|
13016
|
+
width: captureCompositionWidth ?? width,
|
|
13017
|
+
height: captureCompositionHeight ?? height,
|
|
13001
13018
|
codec: preset.codec,
|
|
13002
13019
|
preset: preset.preset,
|
|
13003
13020
|
quality: effectiveQuality,
|
|
@@ -13727,10 +13744,13 @@ import { parseEncoderOptions } from "@hyperframes/studio-server/encoder-options"
|
|
|
13727
13744
|
// src/renderRequest.ts
|
|
13728
13745
|
import {
|
|
13729
13746
|
isVideoFrameFormat as isVideoFrameFormat2,
|
|
13730
|
-
resolveConfig as
|
|
13747
|
+
resolveConfig as resolveConfig4,
|
|
13731
13748
|
validateEngineConfigSnapshot as validateEngineConfigSnapshot2
|
|
13732
13749
|
} from "@hyperframes/engine";
|
|
13733
|
-
import {
|
|
13750
|
+
import {
|
|
13751
|
+
VALID_CANVAS_RESOLUTIONS,
|
|
13752
|
+
isDistributedFps as isDistributedFps2
|
|
13753
|
+
} from "@hyperframes/core";
|
|
13734
13754
|
|
|
13735
13755
|
// src/services/distributed/renderConfigValidation.ts
|
|
13736
13756
|
import {
|
|
@@ -13738,6 +13758,82 @@ import {
|
|
|
13738
13758
|
isVideoFrameFormat,
|
|
13739
13759
|
validateEngineConfigSnapshot
|
|
13740
13760
|
} from "@hyperframes/engine";
|
|
13761
|
+
import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
13762
|
+
|
|
13763
|
+
// src/services/distributed/plan.ts
|
|
13764
|
+
import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
|
|
13765
|
+
import { extname as extname4, isAbsolute as isAbsolute4, join as join29, relative as relative4, sep as sep3 } from "node:path";
|
|
13766
|
+
import { fpsToNumber as fpsToNumber10 } from "@hyperframes/core";
|
|
13767
|
+
import {
|
|
13768
|
+
getEncoderPreset as getEncoderPreset4,
|
|
13769
|
+
normalizeVp9CpuUsed,
|
|
13770
|
+
resolveConfig as resolveConfig3
|
|
13771
|
+
} from "@hyperframes/engine";
|
|
13772
|
+
|
|
13773
|
+
// src/services/distributed/planProtocol.ts
|
|
13774
|
+
var PLAN_SCHEMA_VERSION = 1;
|
|
13775
|
+
var PLAN_ARTIFACT_LAYOUT = "plan-dir-v1";
|
|
13776
|
+
var PLAN_HASH_SCHEMA = "hyperframes-plan-hash-v1";
|
|
13777
|
+
var PLAN_V2_SCHEMA_VERSION = 2;
|
|
13778
|
+
var PLAN_V2_ARTIFACT_LAYOUT = "content-addressed-plan-v2";
|
|
13779
|
+
var PLAN_V2_HASH_SCHEMA = "hyperframes-plan-manifest-hash-v2";
|
|
13780
|
+
var PLAN_PROTOCOL_V1 = Object.freeze({
|
|
13781
|
+
schemaVersion: PLAN_SCHEMA_VERSION,
|
|
13782
|
+
artifactLayout: PLAN_ARTIFACT_LAYOUT,
|
|
13783
|
+
hashSchema: PLAN_HASH_SCHEMA
|
|
13784
|
+
});
|
|
13785
|
+
var PLAN_PROTOCOL_V2 = Object.freeze({
|
|
13786
|
+
schemaVersion: PLAN_V2_SCHEMA_VERSION,
|
|
13787
|
+
artifactLayout: PLAN_V2_ARTIFACT_LAYOUT,
|
|
13788
|
+
hashSchema: PLAN_V2_HASH_SCHEMA
|
|
13789
|
+
});
|
|
13790
|
+
var DISTRIBUTED_RENDER_CAPABILITIES = Object.freeze({
|
|
13791
|
+
roles: Object.freeze({
|
|
13792
|
+
planner: Object.freeze({
|
|
13793
|
+
produces: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2])
|
|
13794
|
+
}),
|
|
13795
|
+
chunk: Object.freeze({
|
|
13796
|
+
accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
|
|
13797
|
+
acceptsLegacyV1WithoutDescriptor: true
|
|
13798
|
+
}),
|
|
13799
|
+
assembler: Object.freeze({
|
|
13800
|
+
accepts: Object.freeze([PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2]),
|
|
13801
|
+
acceptsLegacyV1WithoutDescriptor: true
|
|
13802
|
+
})
|
|
13803
|
+
})
|
|
13804
|
+
});
|
|
13805
|
+
|
|
13806
|
+
// src/services/render/stages/planHash.ts
|
|
13807
|
+
var FIELD_DELIMITER = Buffer.from([0]);
|
|
13808
|
+
|
|
13809
|
+
// src/services/distributed/shared.ts
|
|
13810
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
13811
|
+
import { promisify as promisify2 } from "node:util";
|
|
13812
|
+
import {
|
|
13813
|
+
getFfmpegBinary,
|
|
13814
|
+
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME3
|
|
13815
|
+
} from "@hyperframes/engine";
|
|
13816
|
+
var PLAN_AUDIO_RELATIVE_PATH = MIXED_AUDIO_FILENAME3;
|
|
13817
|
+
var execFile = promisify2(execFileCallback);
|
|
13818
|
+
|
|
13819
|
+
// src/services/distributed/planSize.ts
|
|
13820
|
+
var PLAN_ROOT_CATEGORY = {
|
|
13821
|
+
compiled: "compiled",
|
|
13822
|
+
[PLAN_AUDIO_RELATIVE_PATH]: "audio",
|
|
13823
|
+
meta: "metadata",
|
|
13824
|
+
"plan.json": "metadata"
|
|
13825
|
+
};
|
|
13826
|
+
|
|
13827
|
+
// src/services/distributed/plan.ts
|
|
13828
|
+
var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
13829
|
+
var FREEZE_OWNED_PLAN_FILES = [
|
|
13830
|
+
"plan.json",
|
|
13831
|
+
join29("meta", "composition.json"),
|
|
13832
|
+
join29("meta", "encoder.json"),
|
|
13833
|
+
join29("meta", "chunks.json")
|
|
13834
|
+
];
|
|
13835
|
+
|
|
13836
|
+
// src/services/distributed/renderConfigValidation.ts
|
|
13741
13837
|
var InvalidConfigError = class extends Error {
|
|
13742
13838
|
// Read via Error.prototype.toString; fallow can't see it.
|
|
13743
13839
|
// fallow-ignore-next-line unused-class-member
|
|
@@ -13944,7 +14040,7 @@ function createRenderRequest(input) {
|
|
|
13944
14040
|
outputPath: input.outputPath,
|
|
13945
14041
|
options: {
|
|
13946
14042
|
...omitUndefinedProperties(input.options),
|
|
13947
|
-
engineConfig: input.engineConfig ??
|
|
14043
|
+
engineConfig: input.engineConfig ?? resolveConfig4(input.engineOverrides)
|
|
13948
14044
|
}
|
|
13949
14045
|
};
|
|
13950
14046
|
return parseRenderRequest(serializeRenderRequest(request));
|
|
@@ -14161,8 +14257,8 @@ async function resolveInlineRenderHtml(body) {
|
|
|
14161
14257
|
}
|
|
14162
14258
|
function materializeInlineProject(html, options) {
|
|
14163
14259
|
const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
|
|
14164
|
-
const tempProjectDir = mkdtempSync4(
|
|
14165
|
-
writeFileSync6(
|
|
14260
|
+
const tempProjectDir = mkdtempSync4(join30(tempRoot, "producer-project-"));
|
|
14261
|
+
writeFileSync6(join30(tempProjectDir, "index.html"), html, "utf-8");
|
|
14166
14262
|
return {
|
|
14167
14263
|
prepared: {
|
|
14168
14264
|
input: { projectDir: tempProjectDir, ...options },
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
* capture jitter, so the harness can't use it as a per-test gate.
|
|
26
26
|
*
|
|
27
27
|
* Not every fixture can run in distributed-simulated mode. Distributed mode
|
|
28
|
-
* refuses HDR mp4, NTSC framerates, and
|
|
28
|
+
* refuses HDR mp4, NTSC framerates, and fps outside DISTRIBUTED_FPS at plan time.
|
|
29
29
|
* Fixtures that don't meet the constraints are skipped — the harness logs
|
|
30
30
|
* the reason and the fixture is treated as "passed (skipped)" in
|
|
31
31
|
* distributed-simulated mode.
|
|
32
32
|
*/
|
|
33
|
-
import type
|
|
33
|
+
import { type DistributedFps, type Fps } from "@hyperframes/core";
|
|
34
34
|
import { type DistributedFormat } from "./services/distributed/shared.js";
|
|
35
35
|
/**
|
|
36
36
|
* Three-mode contract that backs `--mode=<value>` on the regression
|
|
@@ -67,8 +67,8 @@ export type DistributedSupportResult = {
|
|
|
67
67
|
* Decide whether a fixture's `renderConfig` is one the distributed pipeline
|
|
68
68
|
* can actually run. Two hard gates:
|
|
69
69
|
*
|
|
70
|
-
* - fps must be `{ num:
|
|
71
|
-
* accepts only
|
|
70
|
+
* - fps must be `{ num: <DISTRIBUTED_FPS member>, den: 1 }`. `DistributedRenderConfig.fps`
|
|
71
|
+
* accepts only those integer values, and rationals like
|
|
72
72
|
* `{ num: 30000, den: 1001 }` (NTSC) trip the type system at the call
|
|
73
73
|
* site. We surface this gate in code rather than only in TS so the
|
|
74
74
|
* harness can skip the fixture cleanly instead of throwing.
|
|
@@ -95,7 +95,7 @@ export interface RunDistributedSimulatedInput {
|
|
|
95
95
|
/** Where to write the assembled final mp4 / mov / png-sequence directory. */
|
|
96
96
|
renderedOutputPath: string;
|
|
97
97
|
/** From the fixture's renderConfig — must pass `checkDistributedSupport`. */
|
|
98
|
-
fps:
|
|
98
|
+
fps: DistributedFps;
|
|
99
99
|
format: DistributedFormat;
|
|
100
100
|
/**
|
|
101
101
|
* Codec for `format: "mp4"`. Defaults to `"h264"`; pass `"h265"` to
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* adapter's shape without pulling the aws-lambda graph into producer's
|
|
9
9
|
* type-check pass.
|
|
10
10
|
*/
|
|
11
|
+
import type { DistributedFps } from "@hyperframes/core";
|
|
11
12
|
import type { DistributedFormat } from "./services/distributed/shared.js";
|
|
12
13
|
/** Inputs for {@link runLambdaLocalRender}. Same contract as `runDistributedSimulatedRender`. */
|
|
13
14
|
export interface RunLambdaLocalInput {
|
|
@@ -16,7 +17,7 @@ export interface RunLambdaLocalInput {
|
|
|
16
17
|
projectDir: string;
|
|
17
18
|
tempRoot: string;
|
|
18
19
|
renderedOutputPath: string;
|
|
19
|
-
fps:
|
|
20
|
+
fps: DistributedFps;
|
|
20
21
|
/**
|
|
21
22
|
* Width/height from the fixture's renderConfig. Forwarded directly to
|
|
22
23
|
* the Lambda event so this mode catches drift if the handler ever
|
|
@@ -10,14 +10,10 @@
|
|
|
10
10
|
* decodable). For png-sequence chunks (each chunk is a directory of
|
|
11
11
|
* frames) this is a straight directory merge with global re-numbering.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* applyFaststart copies the input verbatim.) Audio length is
|
|
18
|
-
* pad-or-trimmed to `frameCount / fps` via
|
|
19
|
-
* `padOrTrimAudioToVideoFrameCount` so the mux step doesn't introduce
|
|
20
|
-
* sub-millisecond drift at the end of long renders.
|
|
13
|
+
* Join, audio mux and mp4 faststart run as one ffmpeg pass over the concat
|
|
14
|
+
* demuxer, so disk holds the chunks plus one output copy. Audio is first
|
|
15
|
+
* pad-or-trimmed to `frameCount / fps` (frames summed over the chunks) so
|
|
16
|
+
* the mux adds no drift at the end of long renders.
|
|
21
17
|
*
|
|
22
18
|
* Pure function over local paths. No networking. The caller is responsible
|
|
23
19
|
* for moving `outputPath` to its orchestration-level storage.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type CanvasResolution } from "@hyperframes/core";
|
|
1
|
+
import { type CanvasResolution, type DistributedFps } from "@hyperframes/core";
|
|
2
2
|
import { type EngineConfig, type VideoFrameFormat, type AudioProcessingFailure } from "@hyperframes/engine";
|
|
3
3
|
import { type ProducerLogger } from "../../logger.js";
|
|
4
4
|
import { type RenderJob, type RenderStrictness } from "../renderOrchestrator.js";
|
|
5
|
-
import { type ChunkSliceJson } from "../render/stages/freezePlan.js";
|
|
5
|
+
import { type ChunkSliceJson, type PlanFileDigests } from "../render/stages/freezePlan.js";
|
|
6
6
|
import { type DistributedFormat } from "./shared.js";
|
|
7
7
|
import { type PlanProtocolV1Descriptor } from "./planProtocol.js";
|
|
8
8
|
import { type PlanSizeBreakdown } from "./planSize.js";
|
|
@@ -13,7 +13,7 @@ import { type PlanSizeBreakdown } from "./planSize.js";
|
|
|
13
13
|
*/
|
|
14
14
|
export interface DistributedRenderConfig {
|
|
15
15
|
/** Integer frame rate. Distributed renders only accept integer fps; the in-process renderer's `Fps` rational handles NTSC. */
|
|
16
|
-
fps:
|
|
16
|
+
fps: DistributedFps;
|
|
17
17
|
width: number;
|
|
18
18
|
height: number;
|
|
19
19
|
/**
|
|
@@ -79,8 +79,15 @@ export interface DistributedRenderConfig {
|
|
|
79
79
|
* old 8-second-GOP behavior.
|
|
80
80
|
*/
|
|
81
81
|
chunkSize?: number;
|
|
82
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* Caps the chunk count. When omitted, chunks are sized from `workerConcurrency` and
|
|
84
|
+
* `workerTimeoutSeconds` (see {@link resolveMaxParallelChunks}); without those, `16`.
|
|
85
|
+
*/
|
|
83
86
|
maxParallelChunks?: number;
|
|
87
|
+
/** Seconds a chunk worker may run before the platform kills it (Lambda: the function timeout). */
|
|
88
|
+
workerTimeoutSeconds?: number;
|
|
89
|
+
/** How many chunk workers can run at once (Lambda: reserved concurrency). */
|
|
90
|
+
workerConcurrency?: number;
|
|
84
91
|
/**
|
|
85
92
|
* Upper bound on frames-per-chunk, in frames. Optional; when omitted (the
|
|
86
93
|
* default) chunk sizing is unchanged. When set, chunking targets the fewest
|
|
@@ -174,7 +181,7 @@ export interface LocalExecutionPlan {
|
|
|
174
181
|
executionPlanHash: string;
|
|
175
182
|
chunkCount: number;
|
|
176
183
|
totalFrames: number;
|
|
177
|
-
fps:
|
|
184
|
+
fps: DistributedFps;
|
|
178
185
|
width: number;
|
|
179
186
|
height: number;
|
|
180
187
|
format: DistributedFormat;
|
|
@@ -194,7 +201,12 @@ export interface BuildLocalExecutionPlanOptions {
|
|
|
194
201
|
* chunks write those frames into their own work directory.
|
|
195
202
|
*/
|
|
196
203
|
readonly deferVideoExtraction?: boolean;
|
|
204
|
+
/** Shared with the publisher so each plan file is hashed once. */
|
|
205
|
+
readonly digests?: PlanFileDigests;
|
|
206
|
+
/** Runs while the audio mixes, once `compiled/` and the video trees are final. */
|
|
207
|
+
readonly whileAudioMixes?: () => Promise<void>;
|
|
197
208
|
}
|
|
209
|
+
export declare function logPlanStage(log: ProducerLogger, stage: string, startedAt: number): void;
|
|
198
210
|
/**
|
|
199
211
|
* Result of the legacy v1 {@link plan} wrapper. The `planHash` is the
|
|
200
212
|
* content-addressed identifier that adapters key replay short-circuits off of.
|
|
@@ -208,7 +220,7 @@ export interface PlanResult {
|
|
|
208
220
|
planHash: string;
|
|
209
221
|
chunkCount: number;
|
|
210
222
|
totalFrames: number;
|
|
211
|
-
fps:
|
|
223
|
+
fps: DistributedFps;
|
|
212
224
|
width: number;
|
|
213
225
|
height: number;
|
|
214
226
|
format: DistributedFormat;
|
|
@@ -228,9 +240,8 @@ export declare function applyDistributedAudioWarningPolicy(job: RenderJob, audio
|
|
|
228
240
|
*/
|
|
229
241
|
export declare const PLAN_PROJECT_DIR_SKIP_SEGMENTS: ReadonlySet<string>;
|
|
230
242
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* `undefined`, `plan()` auto-sizes from `maxParallelChunks` instead.
|
|
243
|
+
* Chunk size in frames (~8s @ 30fps) for callers that want a fixed GOP.
|
|
244
|
+
* `plan()` never applies it on its own; an undefined `chunkSize` auto-sizes.
|
|
234
245
|
*/
|
|
235
246
|
export declare const DEFAULT_CHUNK_SIZE = 240;
|
|
236
247
|
/** Default cap on parallel chunks for operational fairness across renders. */
|
|
@@ -242,6 +253,23 @@ export declare const DEFAULT_MAX_PARALLEL_CHUNKS = 16;
|
|
|
242
253
|
* outweighs the parallelism gain on tiny renders.
|
|
243
254
|
*/
|
|
244
255
|
export declare const MIN_CHUNK_SIZE = 10;
|
|
256
|
+
/** Most chunks one render may plan; `validateDistributedRenderConfig` enforces it on `maxParallelChunks`. */
|
|
257
|
+
export declare const MAX_PARALLEL_CHUNKS_CEILING = 256;
|
|
258
|
+
/** Predicted seconds for one chunk of `frames` frames at `megapixels` output pixels per frame. */
|
|
259
|
+
export declare function estimateChunkSeconds(frames: number, megapixels: number): number;
|
|
260
|
+
/** Non-retryable: even {@link MAX_PARALLEL_CHUNKS_CEILING} chunks cannot fit the worker time limit. */
|
|
261
|
+
export declare const CHUNK_EXCEEDS_WORKER_TIMEOUT = "CHUNK_EXCEEDS_WORKER_TIMEOUT";
|
|
262
|
+
export declare class ChunkExceedsWorkerTimeoutError extends Error {
|
|
263
|
+
readonly code: typeof CHUNK_EXCEEDS_WORKER_TIMEOUT;
|
|
264
|
+
constructor(predictedChunkSeconds: number, workerTimeoutSeconds: number);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The `maxParallelChunks` a plan uses: explicit chunking flags win; otherwise one wave across
|
|
268
|
+
* `workerConcurrency`, widened so each chunk fits `workerTimeoutSeconds`; with neither, 16.
|
|
269
|
+
*/
|
|
270
|
+
export declare function resolveMaxParallelChunks(config: Pick<DistributedRenderConfig, "maxParallelChunks" | "chunkSize" | "targetChunkFrames" | "workerTimeoutSeconds" | "workerConcurrency">, totalFrames: number,
|
|
271
|
+
/** Output pixels per frame, after supersampling. */
|
|
272
|
+
megapixels: number, log?: ProducerLogger): number;
|
|
245
273
|
/**
|
|
246
274
|
* Default hard ceiling on `<planDir>/` size in bytes. 2 GB fits inside
|
|
247
275
|
* AWS Lambda's 10 GB `/tmp` alongside the chunk worker's captured frames
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* FrameLookupTable at every captured global frame. If legacy video metadata
|
|
11
11
|
* is absent, v2 explicitly falls back to the full-source pack.
|
|
12
12
|
*/
|
|
13
|
+
import { type DistributedFps } from "@hyperframes/core";
|
|
13
14
|
import { type DistributedRenderConfig } from "./plan.js";
|
|
14
15
|
import { type PlanProtocolV2Descriptor } from "./planProtocol.js";
|
|
15
16
|
import { type PlanV2ArtifactPublisher } from "./planV2Publisher.js";
|
|
@@ -45,7 +46,7 @@ export interface PlanV2Manifest {
|
|
|
45
46
|
readonly sourcePlanV1Hash: string;
|
|
46
47
|
readonly chunkCount: number;
|
|
47
48
|
readonly totalFrames: number;
|
|
48
|
-
readonly fps:
|
|
49
|
+
readonly fps: DistributedFps;
|
|
49
50
|
readonly width: number;
|
|
50
51
|
readonly height: number;
|
|
51
52
|
readonly format: DistributedFormat;
|
|
@@ -71,7 +72,7 @@ export interface PlanV2Result {
|
|
|
71
72
|
readonly sourcePlanV1Hash: string;
|
|
72
73
|
readonly chunkCount: number;
|
|
73
74
|
readonly totalFrames: number;
|
|
74
|
-
readonly fps:
|
|
75
|
+
readonly fps: DistributedFps;
|
|
75
76
|
readonly width: number;
|
|
76
77
|
readonly height: number;
|
|
77
78
|
readonly format: DistributedFormat;
|
|
@@ -48,8 +48,8 @@ export interface PadTrimAudioInput {
|
|
|
48
48
|
/** Deadline for each ffmpeg process; defaults to the engine's `ffmpegProcessTimeout`. */
|
|
49
49
|
timeoutMs?: number;
|
|
50
50
|
/**
|
|
51
|
-
* Optional injectables
|
|
52
|
-
*
|
|
51
|
+
* Optional injectables. Tests stub them; the distributed assembler supplies
|
|
52
|
+
* the frame probe because its video is still separate chunks.
|
|
53
53
|
*/
|
|
54
54
|
probeVideoFrameInfo?: (videoPath: string) => Promise<ProbeVideoFrameInfo>;
|
|
55
55
|
probeAudioInfo?: (audioPath: string, signal?: AbortSignal) => Promise<AudioProbeInfo>;
|
|
@@ -111,3 +111,4 @@ export declare function buildPadTrimAudioArgs(audioPath: string, outputPath: str
|
|
|
111
111
|
* for the assembled video.
|
|
112
112
|
*/
|
|
113
113
|
export declare function padOrTrimAudioToVideoFrameCount(input: PadTrimAudioInput): Promise<PadTrimAudioResult>;
|
|
114
|
+
export declare function defaultProbeVideoFrameInfo(videoPath: string, signal?: AbortSignal): Promise<ProbeVideoFrameInfo>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One owner for "does this capture route honour an output resolution other than the composition's".
|
|
3
|
+
* The scale is Chrome's deviceScaleFactor; a route that sizes its own buffers and encoder from the
|
|
4
|
+
* composition would encode at native size while the render reports the requested one.
|
|
5
|
+
*/
|
|
6
|
+
import type { CapturePlan } from "./capturePlan.js";
|
|
7
|
+
/** Throw a named error when an output resolution other than the composition's meets a route that ignores it. */
|
|
8
|
+
export declare function assertOutputScaleSupported(deviceScaleFactor: number, route: CapturePlan["kind"]): void;
|
|
@@ -55,18 +55,16 @@ export declare function projectBrowserEndToCompositionTimeline(existingStart: nu
|
|
|
55
55
|
* `deviceScaleFactor`. The composition's intrinsic dimensions stay the
|
|
56
56
|
* page-layout viewport; the screenshot lands at output dims via DPR.
|
|
57
57
|
*
|
|
58
|
-
*
|
|
58
|
+
* Upsampling must be by a whole number — fractional DPRs above 1 introduce
|
|
59
59
|
* visible aliasing and we'd rather fail loudly than produce a blurry
|
|
60
|
-
* 4K render.
|
|
61
|
-
*
|
|
62
|
-
* we can plumb a separate flag.
|
|
60
|
+
* 4K render. A smaller preset (720p, 480p) takes a DPR under 1, so Chrome
|
|
61
|
+
* rasterizes straight at the smaller size.
|
|
63
62
|
*
|
|
64
63
|
* Throws on:
|
|
65
64
|
* - HDR + outputResolution (HDR compositor processes raw pixel buffers
|
|
66
65
|
* at composition dimensions and would need parallel scaling).
|
|
67
66
|
* - Aspect-ratio mismatch (e.g. landscape composition → portrait-4k).
|
|
68
|
-
* - Non-integer
|
|
69
|
-
* - Downsampling (output dimensions smaller than composition).
|
|
67
|
+
* - Non-integer upsampling ratio.
|
|
70
68
|
*/
|
|
71
69
|
export declare function resolveDeviceScaleFactor(input: {
|
|
72
70
|
compositionWidth: number;
|
|
@@ -107,6 +107,7 @@ export interface FreezePlanInput {
|
|
|
107
107
|
totalFrames: number;
|
|
108
108
|
/** Whether the plan's mixed-audio artifact was produced. */
|
|
109
109
|
hasAudio: boolean;
|
|
110
|
+
digests?: PlanFileDigests;
|
|
110
111
|
}
|
|
111
112
|
export interface FreezePlanResult {
|
|
112
113
|
/** Absolute path to `plan.json`. */
|
|
@@ -114,6 +115,11 @@ export interface FreezePlanResult {
|
|
|
114
115
|
/** Content-addressed planHash; see {@link computePlanHash}. */
|
|
115
116
|
planHash: string;
|
|
116
117
|
}
|
|
118
|
+
/** sha256 by absolute path. A staged plan file never changes, so one plan hashes it once. */
|
|
119
|
+
export type PlanFileDigests = Map<string, string>;
|
|
120
|
+
export declare function planFileSha256(absolutePath: string, digests?: PlanFileDigests): string;
|
|
121
|
+
/** Hash every file under `dir` into `digests` without blocking the event loop. */
|
|
122
|
+
export declare function prehashPlanFiles(dir: string, digests: PlanFileDigests): Promise<void>;
|
|
117
123
|
/**
|
|
118
124
|
* Read a frozen plan directory back from disk and recompute its
|
|
119
125
|
* content-addressed `planHash` over the actual on-disk bytes — including
|
|
@@ -130,7 +136,7 @@ export interface FreezePlanResult {
|
|
|
130
136
|
* callers should catch those as `MISSING_PLAN_ARTIFACT` rather than
|
|
131
137
|
* lumping them with hash drift.
|
|
132
138
|
*/
|
|
133
|
-
export declare function recomputePlanHashFromPlanDir(planDir: string): string;
|
|
139
|
+
export declare function recomputePlanHashFromPlanDir(planDir: string, digests?: PlanFileDigests): string;
|
|
134
140
|
/**
|
|
135
141
|
* Freeze a plan directory: write `meta/*.json` + top-level `plan.json`, then
|
|
136
142
|
* compute `planHash` over the canonicalized contents.
|
|
@@ -94,3 +94,6 @@ export declare function canonicalJsonStringify(value: unknown): string;
|
|
|
94
94
|
* by the eventual `freezePlan` to hash assets on disk.
|
|
95
95
|
*/
|
|
96
96
|
export declare function sha256Hex(bytes: Uint8Array | string): string;
|
|
97
|
+
/** Hash large plan artifacts with bounded memory (important above the legacy 2 GiB cap). */
|
|
98
|
+
export declare function sha256File(path: string): string;
|
|
99
|
+
export declare function sha256FileAsync(path: string): Promise<string>;
|
|
@@ -846,7 +846,7 @@ export declare function shouldPreferSingleWorkerDrawElement(args: {
|
|
|
846
846
|
* telemetry and keep the probe session alive through the heaviest stage.
|
|
847
847
|
*/
|
|
848
848
|
layeredOrEffectRoute: boolean;
|
|
849
|
-
/** deviceScaleFactor
|
|
849
|
+
/** deviceScaleFactor other than 1 — the engine's supersampling gate blocks DE. */
|
|
850
850
|
supersampling: boolean;
|
|
851
851
|
/**
|
|
852
852
|
* The probe session already ran the engine's init-time DE gates and DE did
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.168",
|
|
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/core": "0.8.
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/lint": "0.8.
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/
|
|
38
|
+
"@hyperframes/core": "0.8.168",
|
|
39
|
+
"@hyperframes/parsers": "0.8.168",
|
|
40
|
+
"@hyperframes/lint": "0.8.168",
|
|
41
|
+
"@hyperframes/studio-server": "0.8.168",
|
|
42
|
+
"@hyperframes/engine": "0.8.168"
|
|
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.168"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|