@hyperframes/producer 0.8.190 → 0.8.192
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 +86 -5
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +64 -64
- package/dist/index.js +59 -13
- package/dist/public-server.js +46 -12
- package/dist/services/distributed/audioSidecar.d.ts +10 -0
- package/dist/services/distributed/plan.d.ts +2 -0
- package/dist/services/render/artifactTransaction.d.ts +14 -11
- package/dist/services/render/stages/audioStage.d.ts +1 -0
- package/dist/services/render/stages/extractVideosStage.d.ts +17 -2
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1508,7 +1508,7 @@ async function defaultArtifactDurationProbe(path) {
|
|
|
1508
1508
|
};
|
|
1509
1509
|
}
|
|
1510
1510
|
function buildArtifactExpectation(input) {
|
|
1511
|
-
if (input.outputFormat === "
|
|
1511
|
+
if (input.outputFormat === "gif") return void 0;
|
|
1512
1512
|
if (!Number.isFinite(input.durationSeconds) || input.durationSeconds <= 0) return void 0;
|
|
1513
1513
|
const expectation = {
|
|
1514
1514
|
expectedDurationSeconds: input.durationSeconds,
|
|
@@ -1573,12 +1573,12 @@ function assertDurationWithinTolerance(expectedSeconds, probedSeconds, tolerance
|
|
|
1573
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}`
|
|
1574
1574
|
);
|
|
1575
1575
|
}
|
|
1576
|
-
function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath) {
|
|
1576
|
+
function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath, toleranceFrames = 1) {
|
|
1577
1577
|
if (probedFrames === void 0 || !Number.isFinite(probedFrames) || probedFrames <= 0) {
|
|
1578
1578
|
return;
|
|
1579
1579
|
}
|
|
1580
1580
|
const shortfall = expectedFrames - probedFrames;
|
|
1581
|
-
if (shortfall <=
|
|
1581
|
+
if (shortfall <= toleranceFrames) return;
|
|
1582
1582
|
throw new Error(
|
|
1583
1583
|
`Render artifact is truncated: expected ${expectedFrames} frames, probed ${probedFrames} frames (shortfall ${shortfall} frames). Artifact: ${stagingPath}`
|
|
1584
1584
|
);
|
|
@@ -1615,8 +1615,8 @@ var ArtifactTransaction = class {
|
|
|
1615
1615
|
if (expected) await this.assertArtifactDuration(expected);
|
|
1616
1616
|
return;
|
|
1617
1617
|
}
|
|
1618
|
-
this.assertReadableNonEmptyDirectory();
|
|
1619
|
-
if (expected) await this.assertDirectoryExpectation(expected);
|
|
1618
|
+
const files = this.assertReadableNonEmptyDirectory();
|
|
1619
|
+
if (expected) await this.assertDirectoryExpectation(expected, files);
|
|
1620
1620
|
}
|
|
1621
1621
|
assertReadableNonEmptyDirectory() {
|
|
1622
1622
|
let files;
|
|
@@ -1631,13 +1631,14 @@ var ArtifactTransaction = class {
|
|
|
1631
1631
|
throw new Error(`Render artifact directory is empty: ${this.stagingPath}`);
|
|
1632
1632
|
}
|
|
1633
1633
|
for (const file of files) assertReadableNonEmptyFile(file);
|
|
1634
|
+
return files;
|
|
1634
1635
|
}
|
|
1635
1636
|
/**
|
|
1636
1637
|
* The named-entry and duration gates a directory artifact opts into. A
|
|
1637
|
-
* directory
|
|
1638
|
-
*
|
|
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.
|
|
1639
1640
|
*/
|
|
1640
|
-
async assertDirectoryExpectation(expected) {
|
|
1641
|
+
async assertDirectoryExpectation(expected, files) {
|
|
1641
1642
|
for (const required of expected.requiredFiles ?? []) {
|
|
1642
1643
|
const path = join5(this.stagingPath, required);
|
|
1643
1644
|
if (!existsSync5(path)) {
|
|
@@ -1645,8 +1646,25 @@ var ArtifactTransaction = class {
|
|
|
1645
1646
|
}
|
|
1646
1647
|
assertReadableNonEmptyFile(path);
|
|
1647
1648
|
}
|
|
1648
|
-
if (expected.probeTarget
|
|
1649
|
-
|
|
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
|
+
);
|
|
1650
1668
|
}
|
|
1651
1669
|
async assertArtifactDuration(expected, probePath = this.stagingPath) {
|
|
1652
1670
|
const expectedSeconds = expected.expectedDurationSeconds;
|
|
@@ -7145,7 +7163,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7145
7163
|
);
|
|
7146
7164
|
if (analysis.isProblematic) {
|
|
7147
7165
|
defaultLogger.warn(
|
|
7148
|
-
`[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}`
|
|
7149
7167
|
);
|
|
7150
7168
|
}
|
|
7151
7169
|
if (metadata.isVFR) {
|
|
@@ -8031,6 +8049,10 @@ async function runProbeStage(input) {
|
|
|
8031
8049
|
if (el.loop && !existing.loop) {
|
|
8032
8050
|
existing.loop = true;
|
|
8033
8051
|
}
|
|
8052
|
+
const audio = composition.audios.find(
|
|
8053
|
+
(audio2) => audio2.type === "video" && audio2.id === `${el.id}-audio`
|
|
8054
|
+
);
|
|
8055
|
+
if (audio) audio.loop = existing.loop;
|
|
8034
8056
|
}
|
|
8035
8057
|
} else if (!compiled.droppedMediaIds.has(el.id)) {
|
|
8036
8058
|
composition.videos.push({
|
|
@@ -8068,6 +8090,7 @@ async function runProbeStage(input) {
|
|
|
8068
8090
|
el,
|
|
8069
8091
|
authoredStarts.get(el.id) ?? existing.mediaStart
|
|
8070
8092
|
);
|
|
8093
|
+
existing.loop = el.loop;
|
|
8071
8094
|
if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
|
|
8072
8095
|
existing.volume = el.volume;
|
|
8073
8096
|
}
|
|
@@ -8079,6 +8102,7 @@ async function runProbeStage(input) {
|
|
|
8079
8102
|
start: el.start,
|
|
8080
8103
|
end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
|
8081
8104
|
mediaStart: el.mediaStart,
|
|
8105
|
+
loop: el.loop,
|
|
8082
8106
|
layer: 0,
|
|
8083
8107
|
volume: el.volume,
|
|
8084
8108
|
type: "audio"
|
|
@@ -8333,6 +8357,10 @@ import {
|
|
|
8333
8357
|
extractMediaMetadata as extractMediaMetadata3,
|
|
8334
8358
|
isVideoPastTimelineEnd,
|
|
8335
8359
|
isHdrColorSpace as isHdrColorSpace2,
|
|
8360
|
+
rebaseVideoToWindow,
|
|
8361
|
+
resolveFinalFrameExtractionWindow as resolveFinalFrameExtractionWindow2,
|
|
8362
|
+
resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
|
|
8363
|
+
isHttpUrl as isHttpUrl2,
|
|
8336
8364
|
resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
|
|
8337
8365
|
runVideoExtractionWithRetry,
|
|
8338
8366
|
safeVideoExtractionSourceIdentity
|
|
@@ -8698,6 +8726,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
8698
8726
|
start: video.start,
|
|
8699
8727
|
end: video.end,
|
|
8700
8728
|
mediaStart: video.mediaStart,
|
|
8729
|
+
playbackRate: video.playbackRate,
|
|
8730
|
+
loop: video.loop,
|
|
8701
8731
|
layer: 0,
|
|
8702
8732
|
volume: 1,
|
|
8703
8733
|
type: "video"
|
|
@@ -9196,6 +9226,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9196
9226
|
audios,
|
|
9197
9227
|
masterFxChain,
|
|
9198
9228
|
urlByAbsentLocalPath,
|
|
9229
|
+
audioSidecarUrlByLocalPath,
|
|
9199
9230
|
abortSignal,
|
|
9200
9231
|
assertNotAborted,
|
|
9201
9232
|
log
|
|
@@ -9219,6 +9250,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9219
9250
|
audioGain,
|
|
9220
9251
|
masterFxChain,
|
|
9221
9252
|
urlByAbsentLocalPath,
|
|
9253
|
+
audioSidecarUrlByLocalPath,
|
|
9222
9254
|
keepEncoderInputAt: pcmSourcePath
|
|
9223
9255
|
},
|
|
9224
9256
|
compiledDir
|
|
@@ -10402,6 +10434,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10402
10434
|
width,
|
|
10403
10435
|
height,
|
|
10404
10436
|
codec: preset.codec,
|
|
10437
|
+
useGpu: job.config.useGpu,
|
|
10405
10438
|
preset: preset.preset,
|
|
10406
10439
|
quality: effectiveQuality,
|
|
10407
10440
|
bitrate: effectiveBitrate,
|
|
@@ -13847,7 +13880,8 @@ import {
|
|
|
13847
13880
|
import {
|
|
13848
13881
|
getEncoderPreset as getEncoderPreset4,
|
|
13849
13882
|
normalizeVp9CpuUsed,
|
|
13850
|
-
resolveConfig as resolveConfig3
|
|
13883
|
+
resolveConfig as resolveConfig3,
|
|
13884
|
+
declareContentAddressedUrl
|
|
13851
13885
|
} from "@hyperframes/engine";
|
|
13852
13886
|
|
|
13853
13887
|
// src/services/distributed/planFrameCost.ts
|
|
@@ -15895,12 +15929,18 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
15895
15929
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
15896
15930
|
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
15897
15931
|
const remoteLocalPath = (rel) => join33(projectDir, ...rel.split("/"));
|
|
15932
|
+
for (const [, file] of remoteFiles) declareContentAddressedUrl(file.url, file.sha256);
|
|
15898
15933
|
const urlByAbsentLocalPath = new Map(
|
|
15899
15934
|
remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
|
|
15900
15935
|
);
|
|
15901
15936
|
const localPathByUrl = new Map(
|
|
15902
15937
|
remoteFiles.map(([rel, file]) => [file.url, remoteLocalPath(rel)])
|
|
15903
15938
|
);
|
|
15939
|
+
const audioSidecarUrlByLocalPath = new Map(
|
|
15940
|
+
remoteFiles.flatMap(
|
|
15941
|
+
([rel, file]) => file.audioSidecarUrl ? [[remoteLocalPath(rel), file.audioSidecarUrl]] : []
|
|
15942
|
+
)
|
|
15943
|
+
);
|
|
15904
15944
|
stageStarted = Date.now();
|
|
15905
15945
|
const compileResult = await runCompileStage({
|
|
15906
15946
|
urlByAbsentLocalPath,
|
|
@@ -15985,6 +16025,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
15985
16025
|
finalCompiledDir,
|
|
15986
16026
|
remoteFiles,
|
|
15987
16027
|
urlByAbsentLocalPath,
|
|
16028
|
+
audioSidecarUrlByLocalPath,
|
|
15988
16029
|
localPathByUrl,
|
|
15989
16030
|
compileResult,
|
|
15990
16031
|
compiled,
|
|
@@ -16002,6 +16043,7 @@ function mixPlanAudio(c, compiledDir) {
|
|
|
16002
16043
|
workDir: c.workDir,
|
|
16003
16044
|
compiledDir,
|
|
16004
16045
|
urlByAbsentLocalPath: c.urlByAbsentLocalPath,
|
|
16046
|
+
audioSidecarUrlByLocalPath: c.audioSidecarUrlByLocalPath,
|
|
16005
16047
|
totalFrames: c.totalFrames,
|
|
16006
16048
|
fps: c.job.config.fps,
|
|
16007
16049
|
ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
|
|
@@ -18725,7 +18767,7 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
|
|
|
18725
18767
|
"-r",
|
|
18726
18768
|
fpsArg
|
|
18727
18769
|
];
|
|
18728
|
-
appendSoftwareEncoderThreadsArg(args);
|
|
18770
|
+
appendSoftwareEncoderThreadsArg(args, "libx264");
|
|
18729
18771
|
return args;
|
|
18730
18772
|
}
|
|
18731
18773
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
@@ -19556,6 +19598,10 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
|
|
|
19556
19598
|
|
|
19557
19599
|
// src/services/distributed/projectHash.ts
|
|
19558
19600
|
var READ_CHUNK_BYTES = 256 * 1024;
|
|
19601
|
+
|
|
19602
|
+
// src/services/distributed/audioSidecar.ts
|
|
19603
|
+
import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as runFfmpeg8 } from "@hyperframes/engine";
|
|
19604
|
+
var AUDIO_SIDECAR_MIN_BYTES = 32 * 1024 * 1024;
|
|
19559
19605
|
export {
|
|
19560
19606
|
CURRENT_PLAN_PROTOCOL,
|
|
19561
19607
|
DEFAULT_CONFIG2 as DEFAULT_CONFIG,
|
package/dist/public-server.js
CHANGED
|
@@ -1527,7 +1527,7 @@ async function defaultArtifactDurationProbe(path) {
|
|
|
1527
1527
|
};
|
|
1528
1528
|
}
|
|
1529
1529
|
function buildArtifactExpectation(input) {
|
|
1530
|
-
if (input.outputFormat === "
|
|
1530
|
+
if (input.outputFormat === "gif") return void 0;
|
|
1531
1531
|
if (!Number.isFinite(input.durationSeconds) || input.durationSeconds <= 0) return void 0;
|
|
1532
1532
|
const expectation = {
|
|
1533
1533
|
expectedDurationSeconds: input.durationSeconds,
|
|
@@ -1592,12 +1592,12 @@ function assertDurationWithinTolerance(expectedSeconds, probedSeconds, tolerance
|
|
|
1592
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}`
|
|
1593
1593
|
);
|
|
1594
1594
|
}
|
|
1595
|
-
function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath) {
|
|
1595
|
+
function assertFrameCountWithinTolerance(expectedFrames, probedFrames, stagingPath, toleranceFrames = 1) {
|
|
1596
1596
|
if (probedFrames === void 0 || !Number.isFinite(probedFrames) || probedFrames <= 0) {
|
|
1597
1597
|
return;
|
|
1598
1598
|
}
|
|
1599
1599
|
const shortfall = expectedFrames - probedFrames;
|
|
1600
|
-
if (shortfall <=
|
|
1600
|
+
if (shortfall <= toleranceFrames) return;
|
|
1601
1601
|
throw new Error(
|
|
1602
1602
|
`Render artifact is truncated: expected ${expectedFrames} frames, probed ${probedFrames} frames (shortfall ${shortfall} frames). Artifact: ${stagingPath}`
|
|
1603
1603
|
);
|
|
@@ -1634,8 +1634,8 @@ var ArtifactTransaction = class {
|
|
|
1634
1634
|
if (expected) await this.assertArtifactDuration(expected);
|
|
1635
1635
|
return;
|
|
1636
1636
|
}
|
|
1637
|
-
this.assertReadableNonEmptyDirectory();
|
|
1638
|
-
if (expected) await this.assertDirectoryExpectation(expected);
|
|
1637
|
+
const files = this.assertReadableNonEmptyDirectory();
|
|
1638
|
+
if (expected) await this.assertDirectoryExpectation(expected, files);
|
|
1639
1639
|
}
|
|
1640
1640
|
assertReadableNonEmptyDirectory() {
|
|
1641
1641
|
let files;
|
|
@@ -1650,13 +1650,14 @@ var ArtifactTransaction = class {
|
|
|
1650
1650
|
throw new Error(`Render artifact directory is empty: ${this.stagingPath}`);
|
|
1651
1651
|
}
|
|
1652
1652
|
for (const file of files) assertReadableNonEmptyFile(file);
|
|
1653
|
+
return files;
|
|
1653
1654
|
}
|
|
1654
1655
|
/**
|
|
1655
1656
|
* The named-entry and duration gates a directory artifact opts into. A
|
|
1656
|
-
* directory
|
|
1657
|
-
*
|
|
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.
|
|
1658
1659
|
*/
|
|
1659
|
-
async assertDirectoryExpectation(expected) {
|
|
1660
|
+
async assertDirectoryExpectation(expected, files) {
|
|
1660
1661
|
for (const required of expected.requiredFiles ?? []) {
|
|
1661
1662
|
const path = join5(this.stagingPath, required);
|
|
1662
1663
|
if (!existsSync5(path)) {
|
|
@@ -1664,8 +1665,25 @@ var ArtifactTransaction = class {
|
|
|
1664
1665
|
}
|
|
1665
1666
|
assertReadableNonEmptyFile(path);
|
|
1666
1667
|
}
|
|
1667
|
-
if (expected.probeTarget
|
|
1668
|
-
|
|
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
|
+
);
|
|
1669
1687
|
}
|
|
1670
1688
|
async assertArtifactDuration(expected, probePath = this.stagingPath) {
|
|
1671
1689
|
const expectedSeconds = expected.expectedDurationSeconds;
|
|
@@ -7164,7 +7182,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
|
|
|
7164
7182
|
);
|
|
7165
7183
|
if (analysis.isProblematic) {
|
|
7166
7184
|
defaultLogger.warn(
|
|
7167
|
-
`[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}`
|
|
7168
7186
|
);
|
|
7169
7187
|
}
|
|
7170
7188
|
if (metadata.isVFR) {
|
|
@@ -8050,6 +8068,10 @@ async function runProbeStage(input) {
|
|
|
8050
8068
|
if (el.loop && !existing.loop) {
|
|
8051
8069
|
existing.loop = true;
|
|
8052
8070
|
}
|
|
8071
|
+
const audio = composition.audios.find(
|
|
8072
|
+
(audio2) => audio2.type === "video" && audio2.id === `${el.id}-audio`
|
|
8073
|
+
);
|
|
8074
|
+
if (audio) audio.loop = existing.loop;
|
|
8053
8075
|
}
|
|
8054
8076
|
} else if (!compiled.droppedMediaIds.has(el.id)) {
|
|
8055
8077
|
composition.videos.push({
|
|
@@ -8087,6 +8109,7 @@ async function runProbeStage(input) {
|
|
|
8087
8109
|
el,
|
|
8088
8110
|
authoredStarts.get(el.id) ?? existing.mediaStart
|
|
8089
8111
|
);
|
|
8112
|
+
existing.loop = el.loop;
|
|
8090
8113
|
if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
|
|
8091
8114
|
existing.volume = el.volume;
|
|
8092
8115
|
}
|
|
@@ -8098,6 +8121,7 @@ async function runProbeStage(input) {
|
|
|
8098
8121
|
start: el.start,
|
|
8099
8122
|
end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
|
8100
8123
|
mediaStart: el.mediaStart,
|
|
8124
|
+
loop: el.loop,
|
|
8101
8125
|
layer: 0,
|
|
8102
8126
|
volume: el.volume,
|
|
8103
8127
|
type: "audio"
|
|
@@ -8321,6 +8345,10 @@ import {
|
|
|
8321
8345
|
extractMediaMetadata as extractMediaMetadata3,
|
|
8322
8346
|
isVideoPastTimelineEnd,
|
|
8323
8347
|
isHdrColorSpace as isHdrColorSpace2,
|
|
8348
|
+
rebaseVideoToWindow,
|
|
8349
|
+
resolveFinalFrameExtractionWindow as resolveFinalFrameExtractionWindow2,
|
|
8350
|
+
resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
|
|
8351
|
+
isHttpUrl as isHttpUrl2,
|
|
8324
8352
|
resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
|
|
8325
8353
|
runVideoExtractionWithRetry,
|
|
8326
8354
|
safeVideoExtractionSourceIdentity
|
|
@@ -8681,6 +8709,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
8681
8709
|
start: video.start,
|
|
8682
8710
|
end: video.end,
|
|
8683
8711
|
mediaStart: video.mediaStart,
|
|
8712
|
+
playbackRate: video.playbackRate,
|
|
8713
|
+
loop: video.loop,
|
|
8684
8714
|
layer: 0,
|
|
8685
8715
|
volume: 1,
|
|
8686
8716
|
type: "video"
|
|
@@ -9179,6 +9209,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9179
9209
|
audios,
|
|
9180
9210
|
masterFxChain,
|
|
9181
9211
|
urlByAbsentLocalPath,
|
|
9212
|
+
audioSidecarUrlByLocalPath,
|
|
9182
9213
|
abortSignal,
|
|
9183
9214
|
assertNotAborted,
|
|
9184
9215
|
log
|
|
@@ -9202,6 +9233,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
9202
9233
|
audioGain,
|
|
9203
9234
|
masterFxChain,
|
|
9204
9235
|
urlByAbsentLocalPath,
|
|
9236
|
+
audioSidecarUrlByLocalPath,
|
|
9205
9237
|
keepEncoderInputAt: pcmSourcePath
|
|
9206
9238
|
},
|
|
9207
9239
|
compiledDir
|
|
@@ -10385,6 +10417,7 @@ async function runCaptureHdrStage(input) {
|
|
|
10385
10417
|
width,
|
|
10386
10418
|
height,
|
|
10387
10419
|
codec: preset.codec,
|
|
10420
|
+
useGpu: job.config.useGpu,
|
|
10388
10421
|
preset: preset.preset,
|
|
10389
10422
|
quality: effectiveQuality,
|
|
10390
10423
|
bitrate: effectiveBitrate,
|
|
@@ -14035,7 +14068,8 @@ import {
|
|
|
14035
14068
|
import {
|
|
14036
14069
|
getEncoderPreset as getEncoderPreset4,
|
|
14037
14070
|
normalizeVp9CpuUsed,
|
|
14038
|
-
resolveConfig as resolveConfig3
|
|
14071
|
+
resolveConfig as resolveConfig3,
|
|
14072
|
+
declareContentAddressedUrl
|
|
14039
14073
|
} from "@hyperframes/engine";
|
|
14040
14074
|
|
|
14041
14075
|
// src/services/distributed/planFrameCost.ts
|
|
@@ -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>;
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -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. */
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* machines (symlinks don't survive S3 / GCS round-trips). Default
|
|
28
28
|
* `false` preserves the in-process renderer's symlink behavior.
|
|
29
29
|
*/
|
|
30
|
-
import { type CaptureVideoMetadataHint, type EngineConfig, type
|
|
30
|
+
import { type CaptureVideoMetadataHint, type EngineConfig, type ExtractionResult, type FrameLookupTable, type HdrTransfer, type VideoExtractionFailureKind, type VideoColorSpace, classifyVideoExtractionError, extractAllVideoFrames } from "@hyperframes/engine";
|
|
31
31
|
import { type RenderJob } from "../../renderOrchestrator.js";
|
|
32
32
|
import { type CompositionMetadata } from "../shared.js";
|
|
33
33
|
import type { ProducerLogger } from "../../../logger.js";
|
|
@@ -127,4 +127,19 @@ export declare function runExtractVideosStage(input: ExtractVideosStageInput): P
|
|
|
127
127
|
* to composition.audios. Both the file AND the element must declare audio —
|
|
128
128
|
* a muted <video> whose source contains audio should not leak into the render.
|
|
129
129
|
*/
|
|
130
|
-
export declare function appendAutoDetectedVideoAudio(composition: Pick<CompositionMetadata, "videos" | "audios">, extracted:
|
|
130
|
+
export declare function appendAutoDetectedVideoAudio(composition: Pick<CompositionMetadata, "videos" | "audios">, extracted: ReadonlyArray<{
|
|
131
|
+
videoId: string;
|
|
132
|
+
metadata: {
|
|
133
|
+
hasAudio: boolean;
|
|
134
|
+
};
|
|
135
|
+
}>): void;
|
|
136
|
+
/**
|
|
137
|
+
* The audio-only form of the extract stage: which videos carry sound takes a probe per source, not their frames.
|
|
138
|
+
* Each video is moved to its extraction window first, as extraction does, so its sound lands where its picture does.
|
|
139
|
+
*/
|
|
140
|
+
export declare function appendVideoAudioFromProbe(input: {
|
|
141
|
+
projectDir: string;
|
|
142
|
+
compiledDir: string;
|
|
143
|
+
composition: CompositionMetadata;
|
|
144
|
+
urlByAbsentLocalPath?: ReadonlyMap<string, string>;
|
|
145
|
+
}): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.192",
|
|
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/parsers": "0.8.
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/
|
|
38
|
+
"@hyperframes/lint": "0.8.192",
|
|
39
|
+
"@hyperframes/core": "0.8.192",
|
|
40
|
+
"@hyperframes/parsers": "0.8.192",
|
|
41
|
+
"@hyperframes/studio-server": "0.8.192",
|
|
42
|
+
"@hyperframes/engine": "0.8.192"
|
|
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.192"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|