@hyperframes/producer 0.8.181 → 0.8.184
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 +2 -2
- package/dist/distributed.js +451 -246
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +59 -59
- package/dist/index.d.ts +1 -0
- package/dist/index.js +601 -292
- package/dist/public-server.js +311 -158
- package/dist/services/distributed/assemble.d.ts +2 -1
- package/dist/services/distributed/chunkFrames.d.ts +6 -1
- package/dist/services/distributed/mp4Stitch.d.ts +4 -4
- package/dist/services/distributed/plan.d.ts +11 -0
- package/dist/services/distributed/planV2.d.ts +1 -0
- package/dist/services/distributed/projectHash.d.ts +1 -4
- package/dist/services/distributed/renderChunk.d.ts +9 -2
- package/dist/services/render/artifactTransaction.d.ts +7 -0
- package/dist/services/render/audioPadTrim.d.ts +20 -15
- package/dist/services/render/audioTruePeak.d.ts +1 -5
- package/dist/services/render/perfSummary.d.ts +2 -0
- package/dist/services/render/stages/audioStage.d.ts +2 -0
- package/dist/services/render/stages/captureStage.d.ts +11 -4
- package/dist/services/render/stages/encodeStage.d.ts +26 -2
- package/dist/services/render/stages/freezePlan.d.ts +1 -0
- package/dist/services/renderOrchestrator.d.ts +2 -2
- package/package.json +7 -7
package/dist/distributed.js
CHANGED
|
@@ -89,6 +89,8 @@ import {
|
|
|
89
89
|
extractMediaMetadata as extractMediaMetadata4,
|
|
90
90
|
resolveConfig as resolveConfig2,
|
|
91
91
|
closeCaptureSession as closeCaptureSession8,
|
|
92
|
+
createCaptureSession as createCaptureSession8,
|
|
93
|
+
initializeSession as initializeSession8,
|
|
92
94
|
createVideoFrameInjector,
|
|
93
95
|
getEncoderPreset as getEncoderPreset3,
|
|
94
96
|
distributeFrames as distributeFrames2,
|
|
@@ -4955,6 +4957,7 @@ async function runAudioStage(input) {
|
|
|
4955
4957
|
audios,
|
|
4956
4958
|
masterFxChain,
|
|
4957
4959
|
urlByAbsentLocalPath,
|
|
4960
|
+
keepEncoderInputAt,
|
|
4958
4961
|
abortSignal,
|
|
4959
4962
|
assertNotAborted,
|
|
4960
4963
|
log
|
|
@@ -4974,7 +4977,13 @@ async function runAudioStage(input) {
|
|
|
4974
4977
|
audioOutputPath,
|
|
4975
4978
|
duration,
|
|
4976
4979
|
abortSignal,
|
|
4977
|
-
{
|
|
4980
|
+
{
|
|
4981
|
+
ffmpegProcessTimeout,
|
|
4982
|
+
audioGain,
|
|
4983
|
+
masterFxChain,
|
|
4984
|
+
urlByAbsentLocalPath,
|
|
4985
|
+
keepEncoderInputAt
|
|
4986
|
+
},
|
|
4978
4987
|
compiledDir
|
|
4979
4988
|
);
|
|
4980
4989
|
} catch (err) {
|
|
@@ -5088,13 +5097,29 @@ function inspectDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, free
|
|
|
5088
5097
|
}
|
|
5089
5098
|
return { available: false, estimatedBytes, freeBytes };
|
|
5090
5099
|
}
|
|
5091
|
-
function
|
|
5100
|
+
function diskCaptureRemedy(cfg, outputFormat, durationSeconds, workerCount) {
|
|
5101
|
+
const oneWorker = explainStreamingEncodeGate(cfg, outputFormat, 1, durationSeconds);
|
|
5102
|
+
if (workerCount > 1 && oneWorker.enabled) {
|
|
5103
|
+
return `This render stores frames because it captures with ${workerCount} workers. Free up disk space, or re-run this render with --workers 1 to stream frames into the encoder instead. One worker is slower, so keep the default worker count for renders that fit on disk.`;
|
|
5104
|
+
}
|
|
5105
|
+
switch (oneWorker.reason) {
|
|
5106
|
+
case "format_excluded":
|
|
5107
|
+
return `${outputFormat} output stores every frame at any worker count. Free up disk space or render to mp4.`;
|
|
5108
|
+
case "disabled_by_config":
|
|
5109
|
+
return "PRODUCER_ENABLE_STREAMING_ENCODE=false sends every render to disk. Free up disk space or unset it.";
|
|
5110
|
+
case "duration_cap":
|
|
5111
|
+
return "The streaming duration cap (PRODUCER_STREAMING_ENCODE_DURATION_CAP_ENABLED=true) sends renders longer than PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS to disk at any worker count. Free up disk space, unset the cap, or use --low-memory-mode, which streams past it on one worker.";
|
|
5112
|
+
default:
|
|
5113
|
+
return "Free up disk space.";
|
|
5114
|
+
}
|
|
5115
|
+
}
|
|
5116
|
+
function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remedy) {
|
|
5092
5117
|
const basisNote = basis === "measured" ? ` (measured from the first ${DISK_PROJECTION_SAMPLE_FRAMES} frames)` : "";
|
|
5093
5118
|
return new Error(
|
|
5094
|
-
`Disk capture may need ~${(needBytes / 1e6).toFixed(1)} MB of temporary frame storage${basisNote}, but only ${(freeBytes / 1e6).toFixed(1)} MB is free at ${framesDir}. Disk capture stores every frame as an image file (JPEG, or PNG for alpha).
|
|
5119
|
+
`Disk capture may need ~${(needBytes / 1e6).toFixed(1)} MB of temporary frame storage${basisNote}, but only ${(freeBytes / 1e6).toFixed(1)} MB is free at ${framesDir}. Disk capture stores every frame as an image file (JPEG, or PNG for alpha). ${remedy}`
|
|
5095
5120
|
);
|
|
5096
5121
|
}
|
|
5097
|
-
function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeDiskBytes) {
|
|
5122
|
+
function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy, freeDiskBytes) {
|
|
5098
5123
|
const headroom = inspectDiskCaptureHeadroom(
|
|
5099
5124
|
framesDir,
|
|
5100
5125
|
totalFrames,
|
|
@@ -5106,7 +5131,8 @@ function assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, freeD
|
|
|
5106
5131
|
headroom.estimatedBytes,
|
|
5107
5132
|
headroom.freeBytes,
|
|
5108
5133
|
framesDir,
|
|
5109
|
-
"estimate"
|
|
5134
|
+
"estimate",
|
|
5135
|
+
remedy
|
|
5110
5136
|
);
|
|
5111
5137
|
}
|
|
5112
5138
|
function measureCaptureFrameBytes(workDir, framesDir) {
|
|
@@ -5126,7 +5152,7 @@ function measureCaptureFrameBytes(workDir, framesDir) {
|
|
|
5126
5152
|
return total;
|
|
5127
5153
|
}
|
|
5128
5154
|
function createDiskCaptureProjection(input) {
|
|
5129
|
-
const { framesDir, totalFrames, measureBytes, freeDiskBytes = freeDiskBytesAt } = input;
|
|
5155
|
+
const { framesDir, totalFrames, measureBytes, remedy, freeDiskBytes = freeDiskBytesAt } = input;
|
|
5130
5156
|
let checked = false;
|
|
5131
5157
|
return (capturedFrames) => {
|
|
5132
5158
|
if (checked || capturedFrames < DISK_PROJECTION_SAMPLE_FRAMES) return;
|
|
@@ -5138,7 +5164,7 @@ function createDiskCaptureProjection(input) {
|
|
|
5138
5164
|
if (remaining <= 0 || freeBytes === null) return;
|
|
5139
5165
|
const needBytes = Math.ceil(measured / capturedFrames * remaining);
|
|
5140
5166
|
if (needBytes > freeBytes * 0.9) {
|
|
5141
|
-
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured");
|
|
5167
|
+
throw diskCaptureShortfallError(needBytes, freeBytes, framesDir, "measured", remedy);
|
|
5142
5168
|
}
|
|
5143
5169
|
};
|
|
5144
5170
|
}
|
|
@@ -5180,11 +5206,13 @@ async function runCaptureStage(input) {
|
|
|
5180
5206
|
}
|
|
5181
5207
|
}
|
|
5182
5208
|
const captureOptions = buildCaptureOptions();
|
|
5183
|
-
|
|
5209
|
+
const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
|
|
5210
|
+
assertDiskCaptureHeadroom(framesDir, totalFrames, captureOptions, remedy);
|
|
5184
5211
|
const checkDiskProjection = createDiskCaptureProjection({
|
|
5185
5212
|
framesDir,
|
|
5186
5213
|
totalFrames,
|
|
5187
|
-
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir)
|
|
5214
|
+
measureBytes: () => measureCaptureFrameBytes(workDir, framesDir),
|
|
5215
|
+
remedy
|
|
5188
5216
|
});
|
|
5189
5217
|
if (workerCount > 1) {
|
|
5190
5218
|
const attempts = await executeDiskCaptureWithAdaptiveRetry({
|
|
@@ -5205,6 +5233,7 @@ async function runCaptureStage(input) {
|
|
|
5205
5233
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
5206
5234
|
return;
|
|
5207
5235
|
}
|
|
5236
|
+
if (progress.landedFrame) input.onFrameLanded?.(progress.landedFrame);
|
|
5208
5237
|
job.framesRendered = progress.capturedFrames;
|
|
5209
5238
|
checkDiskProjection(progress.capturedFrames);
|
|
5210
5239
|
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
@@ -5445,7 +5474,8 @@ import {
|
|
|
5445
5474
|
formatFfmpegError,
|
|
5446
5475
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
5447
5476
|
resolveConfig,
|
|
5448
|
-
runFfmpeg as runFfmpeg3
|
|
5477
|
+
runFfmpeg as runFfmpeg3,
|
|
5478
|
+
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
5449
5479
|
} from "@hyperframes/engine";
|
|
5450
5480
|
import {
|
|
5451
5481
|
clearGifFramesBeforeNext,
|
|
@@ -5459,8 +5489,10 @@ function fpsToFfmpegArg2(fps) {
|
|
|
5459
5489
|
}
|
|
5460
5490
|
var KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA = ["-reinit_filter", "0"];
|
|
5461
5491
|
var WRITE_EVERY_FRAME_WHOLE = ["-gifflags", "0"];
|
|
5492
|
+
var FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE = ["-xerror"];
|
|
5462
5493
|
function framesInput(input, fpsArg) {
|
|
5463
5494
|
return [
|
|
5495
|
+
...FAIL_ON_UNREADABLE_FRAME_INSTEAD_OF_ENDING_THERE,
|
|
5464
5496
|
"-framerate",
|
|
5465
5497
|
fpsArg,
|
|
5466
5498
|
...KEEP_FILTER_GRAPH_WHEN_FRAMES_DROP_ALPHA,
|
|
@@ -5580,6 +5612,30 @@ function startEncodeProgress(job, stage, onProgress) {
|
|
|
5580
5612
|
});
|
|
5581
5613
|
return (frames = total) => reportEncodeProgress(job, frames, total, onProgress, 75);
|
|
5582
5614
|
}
|
|
5615
|
+
function resolveEngineConfig(input) {
|
|
5616
|
+
return input.engineConfig ?? input.job.config.producerConfig ?? resolveConfig();
|
|
5617
|
+
}
|
|
5618
|
+
function videoEncoderOptions(input, engineCfg) {
|
|
5619
|
+
const { job, preset } = input;
|
|
5620
|
+
return {
|
|
5621
|
+
fps: job.config.fps,
|
|
5622
|
+
width: input.width,
|
|
5623
|
+
height: input.height,
|
|
5624
|
+
codec: preset.codec,
|
|
5625
|
+
preset: preset.preset,
|
|
5626
|
+
quality: input.effectiveQuality,
|
|
5627
|
+
bitrate: input.effectiveBitrate,
|
|
5628
|
+
pixelFormat: preset.pixelFormat,
|
|
5629
|
+
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
5630
|
+
useGpu: job.config.useGpu,
|
|
5631
|
+
hdr: preset.hdr,
|
|
5632
|
+
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
5633
|
+
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
5634
|
+
// renders leave both undefined → preserves the existing open-GOP output.
|
|
5635
|
+
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
5636
|
+
gopSize: input.gopSize
|
|
5637
|
+
};
|
|
5638
|
+
}
|
|
5583
5639
|
async function runEncodeStage(input) {
|
|
5584
5640
|
const {
|
|
5585
5641
|
job,
|
|
@@ -5587,16 +5643,11 @@ async function runEncodeStage(input) {
|
|
|
5587
5643
|
outputPath,
|
|
5588
5644
|
framesDir,
|
|
5589
5645
|
videoOnlyPath,
|
|
5590
|
-
width,
|
|
5591
|
-
height,
|
|
5592
5646
|
needsAlpha,
|
|
5593
5647
|
hasAudio,
|
|
5594
5648
|
audioOutputPath,
|
|
5595
5649
|
isPngSequence,
|
|
5596
5650
|
isGif,
|
|
5597
|
-
preset,
|
|
5598
|
-
effectiveQuality,
|
|
5599
|
-
effectiveBitrate,
|
|
5600
5651
|
enableChunkedEncode,
|
|
5601
5652
|
chunkedEncodeSize,
|
|
5602
5653
|
abortSignal,
|
|
@@ -5626,7 +5677,7 @@ async function runEncodeStage(input) {
|
|
|
5626
5677
|
encoded();
|
|
5627
5678
|
return { encodeMs: Date.now() - stage5Start };
|
|
5628
5679
|
}
|
|
5629
|
-
const engineCfg = input
|
|
5680
|
+
const engineCfg = resolveEngineConfig(input);
|
|
5630
5681
|
if (isGif) {
|
|
5631
5682
|
const encoded = startEncodeProgress(job, "Encoding GIF", onProgress);
|
|
5632
5683
|
if (hasAudio) {
|
|
@@ -5656,24 +5707,7 @@ async function runEncodeStage(input) {
|
|
|
5656
5707
|
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
5657
5708
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
5658
5709
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
5659
|
-
const encoderOpts =
|
|
5660
|
-
fps: job.config.fps,
|
|
5661
|
-
width,
|
|
5662
|
-
height,
|
|
5663
|
-
codec: preset.codec,
|
|
5664
|
-
preset: preset.preset,
|
|
5665
|
-
quality: effectiveQuality,
|
|
5666
|
-
bitrate: effectiveBitrate,
|
|
5667
|
-
pixelFormat: preset.pixelFormat,
|
|
5668
|
-
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
5669
|
-
useGpu: job.config.useGpu,
|
|
5670
|
-
hdr: preset.hdr,
|
|
5671
|
-
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
5672
|
-
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
5673
|
-
// renders leave both undefined → preserves the existing open-GOP output.
|
|
5674
|
-
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
5675
|
-
gopSize: input.gopSize
|
|
5676
|
-
};
|
|
5710
|
+
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
5677
5711
|
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
5678
5712
|
framesDir,
|
|
5679
5713
|
framePattern,
|
|
@@ -5710,7 +5744,8 @@ import { dirname as dirname6, join as join13 } from "node:path";
|
|
|
5710
5744
|
import {
|
|
5711
5745
|
DEFAULT_CONFIG,
|
|
5712
5746
|
describeFfmpegFailure as describeFfmpegFailure2,
|
|
5713
|
-
|
|
5747
|
+
encodeAac,
|
|
5748
|
+
extractAudioMetadata as extractAudioMetadata3,
|
|
5714
5749
|
getFfprobeBinary,
|
|
5715
5750
|
ManagedChildProcess,
|
|
5716
5751
|
runFfmpeg as runFfmpeg5,
|
|
@@ -5719,7 +5754,12 @@ import {
|
|
|
5719
5754
|
import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
|
|
5720
5755
|
|
|
5721
5756
|
// src/services/render/audioTruePeak.ts
|
|
5722
|
-
import {
|
|
5757
|
+
import {
|
|
5758
|
+
aacSegmentCount,
|
|
5759
|
+
describeFfmpegFailure,
|
|
5760
|
+
extractAudioMetadata as extractAudioMetadata2,
|
|
5761
|
+
runFfmpeg as runFfmpeg4
|
|
5762
|
+
} from "@hyperframes/engine";
|
|
5723
5763
|
var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
|
|
5724
5764
|
function checkedDbfs(value) {
|
|
5725
5765
|
if (Number.isNaN(value) || value === Number.POSITIVE_INFINITY) {
|
|
@@ -5757,12 +5797,13 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
|
|
|
5757
5797
|
};
|
|
5758
5798
|
}
|
|
5759
5799
|
async function measureTruePeakDbfs(audioPath, options) {
|
|
5760
|
-
const { timeoutMs, signal
|
|
5800
|
+
const { timeoutMs, signal } = options;
|
|
5761
5801
|
const wholeFile = async () => (await scanTruePeak(audioPath, "", timeoutMs, signal)).peak;
|
|
5802
|
+
const { durationSeconds, sampleRate } = await extractAudioMetadata2(audioPath, { signal });
|
|
5762
5803
|
const block = 4800;
|
|
5763
|
-
const count =
|
|
5764
|
-
if (
|
|
5765
|
-
const cut = (k) => k * Math.floor(
|
|
5804
|
+
const count = sampleRate === 48e3 && Number.isFinite(durationSeconds) ? aacSegmentCount(durationSeconds) : 1;
|
|
5805
|
+
if (count === 1) return wholeFile();
|
|
5806
|
+
const cut = (k) => k * Math.floor(durationSeconds * 10 / count) * block;
|
|
5766
5807
|
const peers = new AbortController();
|
|
5767
5808
|
const segmentSignal = signal ? AbortSignal.any([signal, peers.signal]) : peers.signal;
|
|
5768
5809
|
const scans = await Promise.all(
|
|
@@ -5791,24 +5832,8 @@ var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
|
|
|
5791
5832
|
var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
|
|
5792
5833
|
var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
|
|
5793
5834
|
var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
|
|
5794
|
-
function
|
|
5795
|
-
return
|
|
5796
|
-
"-i",
|
|
5797
|
-
inputPath,
|
|
5798
|
-
"-map",
|
|
5799
|
-
"0:a:0",
|
|
5800
|
-
"-vn",
|
|
5801
|
-
"-af",
|
|
5802
|
-
`volume=${attenuationDb.toFixed(3)}dB`,
|
|
5803
|
-
"-t",
|
|
5804
|
-
formatSeconds(targetDurationSeconds),
|
|
5805
|
-
"-c:a",
|
|
5806
|
-
"aac",
|
|
5807
|
-
"-b:a",
|
|
5808
|
-
"192k",
|
|
5809
|
-
"-y",
|
|
5810
|
-
outputPath
|
|
5811
|
-
];
|
|
5835
|
+
function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
|
|
5836
|
+
return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
|
|
5812
5837
|
}
|
|
5813
5838
|
function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
|
|
5814
5839
|
const delta = targetDurationSeconds - sourceDurationSeconds;
|
|
@@ -5823,53 +5848,13 @@ function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, tar
|
|
|
5823
5848
|
if (delta > 0) {
|
|
5824
5849
|
return {
|
|
5825
5850
|
operation: "pad",
|
|
5826
|
-
steps: [
|
|
5827
|
-
{
|
|
5828
|
-
kind: "normalize",
|
|
5829
|
-
args: [
|
|
5830
|
-
"-i",
|
|
5831
|
-
audioPath,
|
|
5832
|
-
"-af",
|
|
5833
|
-
`apad,atrim=0:${targetSec}`,
|
|
5834
|
-
"-t",
|
|
5835
|
-
targetSec,
|
|
5836
|
-
"-c:a",
|
|
5837
|
-
"aac",
|
|
5838
|
-
"-b:a",
|
|
5839
|
-
"192k",
|
|
5840
|
-
"-y",
|
|
5841
|
-
outputPath
|
|
5842
|
-
]
|
|
5843
|
-
}
|
|
5844
|
-
],
|
|
5851
|
+
steps: [{ kind: "normalize", filter: `apad,atrim=0:${targetSec}` }],
|
|
5845
5852
|
cleanupPaths: []
|
|
5846
5853
|
};
|
|
5847
5854
|
}
|
|
5848
5855
|
return {
|
|
5849
5856
|
operation: "trim",
|
|
5850
|
-
steps: [
|
|
5851
|
-
{
|
|
5852
|
-
kind: "trim",
|
|
5853
|
-
args: [
|
|
5854
|
-
"-i",
|
|
5855
|
-
audioPath,
|
|
5856
|
-
"-af",
|
|
5857
|
-
`atrim=duration=${targetSec},asetpts=PTS-STARTPTS`,
|
|
5858
|
-
// `atrim` limits decoded samples but does not cap muxer timestamps
|
|
5859
|
-
// introduced by encoder delay/flush. The output duration contract
|
|
5860
|
-
// is enforced at the container boundary as well; without `-t`, a
|
|
5861
|
-
// long primed source can retain its original tail after re-encode.
|
|
5862
|
-
"-t",
|
|
5863
|
-
targetSec,
|
|
5864
|
-
"-c:a",
|
|
5865
|
-
"aac",
|
|
5866
|
-
"-b:a",
|
|
5867
|
-
"192k",
|
|
5868
|
-
"-y",
|
|
5869
|
-
outputPath
|
|
5870
|
-
]
|
|
5871
|
-
}
|
|
5872
|
-
],
|
|
5857
|
+
steps: [{ kind: "trim", filter: `atrim=duration=${targetSec},asetpts=PTS-STARTPTS` }],
|
|
5873
5858
|
cleanupPaths: []
|
|
5874
5859
|
};
|
|
5875
5860
|
}
|
|
@@ -5886,7 +5871,8 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5886
5871
|
const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
|
|
5887
5872
|
const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
|
|
5888
5873
|
const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
|
|
5889
|
-
const
|
|
5874
|
+
const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
|
|
5875
|
+
const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
|
|
5890
5876
|
const [videoResult, audioResult] = await Promise.allSettled([
|
|
5891
5877
|
probeVideo(input.videoPath),
|
|
5892
5878
|
probeAudio(input.audioPath, input.signal)
|
|
@@ -5933,7 +5919,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5933
5919
|
);
|
|
5934
5920
|
try {
|
|
5935
5921
|
for (const step of plan2.steps) {
|
|
5936
|
-
const ffmpegResult = await runner(step.args);
|
|
5922
|
+
const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
|
|
5937
5923
|
if (!ffmpegResult.success) {
|
|
5938
5924
|
return {
|
|
5939
5925
|
success: false,
|
|
@@ -5965,7 +5951,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
|
|
|
5965
5951
|
targetDurationSeconds,
|
|
5966
5952
|
signal: input.signal,
|
|
5967
5953
|
probeTruePeak,
|
|
5968
|
-
|
|
5954
|
+
encode
|
|
5969
5955
|
});
|
|
5970
5956
|
if (!correction.success) {
|
|
5971
5957
|
return {
|
|
@@ -5995,12 +5981,8 @@ async function constrainAacTruePeak(input) {
|
|
|
5995
5981
|
audioPath: input.audioPath,
|
|
5996
5982
|
targetDurationSeconds: input.targetDurationSeconds,
|
|
5997
5983
|
signal: input.signal,
|
|
5998
|
-
probeTruePeak:
|
|
5999
|
-
|
|
6000
|
-
const segments = { durationSeconds: input.targetDurationSeconds, sampleRate };
|
|
6001
|
-
return measureTruePeakDbfs(audioPath, { timeoutMs, signal, segments });
|
|
6002
|
-
},
|
|
6003
|
-
runner: (args) => defaultRunFfmpeg(args, timeoutMs, input.signal)
|
|
5984
|
+
probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
|
|
5985
|
+
encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
|
|
6004
5986
|
});
|
|
6005
5987
|
}
|
|
6006
5988
|
async function enforceAacTruePeak(input) {
|
|
@@ -6027,13 +6009,11 @@ async function enforceAacTruePeak(input) {
|
|
|
6027
6009
|
}
|
|
6028
6010
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
6029
6011
|
attenuationDb += correctionTargetDbfs - truePeakDbfs;
|
|
6030
|
-
const result = await input.
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
attenuationDb
|
|
6036
|
-
)
|
|
6012
|
+
const result = await input.encode(
|
|
6013
|
+
input.audioPath,
|
|
6014
|
+
correctedPath,
|
|
6015
|
+
input.targetDurationSeconds,
|
|
6016
|
+
aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
|
|
6037
6017
|
);
|
|
6038
6018
|
if (!result.success) {
|
|
6039
6019
|
return {
|
|
@@ -6128,7 +6108,7 @@ function parseFrameRate(rate) {
|
|
|
6128
6108
|
return { fpsNum, fpsDen };
|
|
6129
6109
|
}
|
|
6130
6110
|
async function defaultProbeAudioInfo(audioPath, signal) {
|
|
6131
|
-
const metadata = await
|
|
6111
|
+
const metadata = await extractAudioMetadata3(audioPath, { signal });
|
|
6132
6112
|
return {
|
|
6133
6113
|
durationSeconds: metadata.durationSeconds,
|
|
6134
6114
|
sampleRate: metadata.sampleRate,
|
|
@@ -6137,7 +6117,18 @@ async function defaultProbeAudioInfo(audioPath, signal) {
|
|
|
6137
6117
|
};
|
|
6138
6118
|
}
|
|
6139
6119
|
async function defaultRunFfmpeg(args, timeoutMs, signal) {
|
|
6140
|
-
|
|
6120
|
+
return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
|
|
6121
|
+
}
|
|
6122
|
+
function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
|
|
6123
|
+
return async (inputPath, outputPath, durationSeconds, filter) => {
|
|
6124
|
+
const options = pcmSourcePath ? { signal, timeout: timeoutMs, filter } : { signal, timeout: timeoutMs, filter, segments: 1 };
|
|
6125
|
+
return toStepResult(
|
|
6126
|
+
await encodeAac(pcmSourcePath ?? inputPath, outputPath, durationSeconds, options),
|
|
6127
|
+
timeoutMs
|
|
6128
|
+
);
|
|
6129
|
+
};
|
|
6130
|
+
}
|
|
6131
|
+
function toStepResult(result, timeoutMs) {
|
|
6141
6132
|
if (result.success) return { success: true };
|
|
6142
6133
|
return {
|
|
6143
6134
|
success: false,
|
|
@@ -6513,15 +6504,33 @@ function createRenderJob(config) {
|
|
|
6513
6504
|
warnings: []
|
|
6514
6505
|
};
|
|
6515
6506
|
}
|
|
6507
|
+
function explainStreamingEncodeGate(cfg, outputFormat, workerCount, durationSeconds, forceParallelStream = false) {
|
|
6508
|
+
if (!cfg.enableStreamingEncode) return { enabled: false, reason: "disabled_by_config" };
|
|
6509
|
+
if (outputFormat === "png-sequence" || outputFormat === "gif") {
|
|
6510
|
+
return { enabled: false, reason: "format_excluded" };
|
|
6511
|
+
}
|
|
6512
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
6513
|
+
return { enabled: false, reason: "invalid_duration" };
|
|
6514
|
+
}
|
|
6515
|
+
const overCap = cfg.streamingEncodeDurationCapEnabled === true && durationSeconds > cfg.streamingEncodeMaxDurationSeconds;
|
|
6516
|
+
if (overCap && !cfg.lowMemoryMode) return { enabled: false, reason: "duration_cap" };
|
|
6517
|
+
if (forceParallelStream || process.env.HF_DE_PARALLEL_STREAM === "true") {
|
|
6518
|
+
return { enabled: true, reason: "parallel_forced" };
|
|
6519
|
+
}
|
|
6520
|
+
if (workerCount === 1) {
|
|
6521
|
+
return { enabled: true, reason: overCap ? "low_memory_mode" : "single_worker" };
|
|
6522
|
+
}
|
|
6523
|
+
return { enabled: false, reason: "multi_worker" };
|
|
6524
|
+
}
|
|
6516
6525
|
var DEFAULT_SEGMENTED_MIN_SECONDS = Number.POSITIVE_INFINITY;
|
|
6517
6526
|
|
|
6518
6527
|
// src/services/distributed/planFrameCost.ts
|
|
6519
6528
|
import { join as join17 } from "node:path";
|
|
6520
6529
|
import {
|
|
6521
6530
|
closeCaptureSession as closeCaptureSession9,
|
|
6522
|
-
createCaptureSession as
|
|
6531
|
+
createCaptureSession as createCaptureSession9,
|
|
6523
6532
|
createFrameLookupTable as createFrameLookupTable3,
|
|
6524
|
-
initializeSession as
|
|
6533
|
+
initializeSession as initializeSession9
|
|
6525
6534
|
} from "@hyperframes/engine";
|
|
6526
6535
|
|
|
6527
6536
|
// src/services/distributed/chunkFrames.ts
|
|
@@ -6919,6 +6928,29 @@ function buildChunkCaptureOptions(input) {
|
|
|
6919
6928
|
requiresWebGpu: compositionRequiresWebGpu3(input.html)
|
|
6920
6929
|
};
|
|
6921
6930
|
}
|
|
6931
|
+
function shownDeferredFrameIndexes(planVideos, frames, fps) {
|
|
6932
|
+
const shown = /* @__PURE__ */ new Map();
|
|
6933
|
+
const deferred = planVideos.extracted.filter((video) => video.deferredRange);
|
|
6934
|
+
if (deferred.length === 0) return shown;
|
|
6935
|
+
const table = createFrameLookupTable2(
|
|
6936
|
+
planVideos.videos,
|
|
6937
|
+
deferred.map((video) => ({
|
|
6938
|
+
...video,
|
|
6939
|
+
outputDir: "",
|
|
6940
|
+
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
6941
|
+
})),
|
|
6942
|
+
resolveRenderFpsConfig(fps).value
|
|
6943
|
+
);
|
|
6944
|
+
for (const frame of frames) {
|
|
6945
|
+
const globalTime = frame * fps.den / fps.num;
|
|
6946
|
+
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
6947
|
+
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
6948
|
+
indexes.add(payload.frameIndex);
|
|
6949
|
+
shown.set(videoId, indexes);
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
return shown;
|
|
6953
|
+
}
|
|
6922
6954
|
async function extractDeferredVideoFramesForChunk(input) {
|
|
6923
6955
|
const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
|
|
6924
6956
|
const remoteSources = input.remoteSources ?? {};
|
|
@@ -6935,24 +6967,7 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
6935
6967
|
});
|
|
6936
6968
|
}
|
|
6937
6969
|
if (deferred.length === 0) return [];
|
|
6938
|
-
const
|
|
6939
|
-
input.planVideos.videos,
|
|
6940
|
-
deferred.map((video) => ({
|
|
6941
|
-
...video,
|
|
6942
|
-
outputDir: input.outputRoot,
|
|
6943
|
-
framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
|
|
6944
|
-
})),
|
|
6945
|
-
resolveRenderFpsConfig(input.fps).value
|
|
6946
|
-
);
|
|
6947
|
-
const shown = /* @__PURE__ */ new Map();
|
|
6948
|
-
for (const frame of input.frames) {
|
|
6949
|
-
const globalTime = frame * input.fps.den / input.fps.num;
|
|
6950
|
-
for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
|
|
6951
|
-
const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
|
|
6952
|
-
indexes.add(payload.frameIndex);
|
|
6953
|
-
shown.set(videoId, indexes);
|
|
6954
|
-
}
|
|
6955
|
-
}
|
|
6970
|
+
const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
|
|
6956
6971
|
return Promise.all(
|
|
6957
6972
|
deferred.map(async (video) => {
|
|
6958
6973
|
const outputDir = join16(input.outputRoot, video.videoId);
|
|
@@ -6978,7 +6993,8 @@ async function extractDeferredVideoFramesForChunk(input) {
|
|
|
6978
6993
|
fps: input.fps,
|
|
6979
6994
|
outputDir: input.outputRoot,
|
|
6980
6995
|
format: range.format,
|
|
6981
|
-
frameRanges: contiguousRuns(indexes)
|
|
6996
|
+
frameRanges: contiguousRuns(indexes),
|
|
6997
|
+
shownBox: input.shownBoxes?.get(video.videoId)
|
|
6982
6998
|
},
|
|
6983
6999
|
void 0,
|
|
6984
7000
|
input.cfg
|
|
@@ -7025,7 +7041,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7025
7041
|
});
|
|
7026
7042
|
let session = null;
|
|
7027
7043
|
try {
|
|
7028
|
-
session = await
|
|
7044
|
+
session = await createCaptureSession9(
|
|
7029
7045
|
fileServer.url,
|
|
7030
7046
|
join17(workDir, "frame-cost"),
|
|
7031
7047
|
buildChunkCaptureOptions({
|
|
@@ -7037,7 +7053,7 @@ async function measurePlanSecondsPerFrame(input) {
|
|
|
7037
7053
|
createChunkVideoFrameInjectorFactory(lookup)(),
|
|
7038
7054
|
createCaptureCalibrationConfig(input.cfg)
|
|
7039
7055
|
);
|
|
7040
|
-
await
|
|
7056
|
+
await initializeSession9(session);
|
|
7041
7057
|
const { estimate } = await measureCaptureCostFromSession(session, input.totalFrames, fps, log);
|
|
7042
7058
|
return (estimate.p95Ms ?? 0) / 1e3;
|
|
7043
7059
|
} finally {
|
|
@@ -7631,6 +7647,20 @@ function withDuration(buf, target, duration) {
|
|
|
7631
7647
|
function timescaleOf(buf, header) {
|
|
7632
7648
|
return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
|
|
7633
7649
|
}
|
|
7650
|
+
var TRACK_ID_AT = { tkhd: [12, 20], mvhd: [96, 108] };
|
|
7651
|
+
function idOf(buf, target) {
|
|
7652
|
+
return buf.readUInt32BE(
|
|
7653
|
+
target.bodyStart + TRACK_ID_AT[target.type][buf[target.bodyStart] === 1 ? 1 : 0]
|
|
7654
|
+
);
|
|
7655
|
+
}
|
|
7656
|
+
function withId(boxBytes, type, id) {
|
|
7657
|
+
const header = boxAt(boxBytes, 0, 0, boxBytes.length);
|
|
7658
|
+
boxBytes.writeUInt32BE(
|
|
7659
|
+
id,
|
|
7660
|
+
header.bodyStart + TRACK_ID_AT[type][boxBytes[header.bodyStart] === 1 ? 1 : 0]
|
|
7661
|
+
);
|
|
7662
|
+
return boxBytes;
|
|
7663
|
+
}
|
|
7634
7664
|
function buildStbl(stsd, track) {
|
|
7635
7665
|
const stts = runs(track.durations);
|
|
7636
7666
|
const stsc = [];
|
|
@@ -7672,24 +7702,48 @@ function planTracksOf(tracks, plan2, index) {
|
|
|
7672
7702
|
}
|
|
7673
7703
|
return matched;
|
|
7674
7704
|
}
|
|
7675
|
-
function
|
|
7676
|
-
const
|
|
7677
|
-
const
|
|
7705
|
+
function sourceTracks(layouts, audio) {
|
|
7706
|
+
const chunkPlan = readTracks(layouts[0].moov);
|
|
7707
|
+
const audioTracks = audio ? readTracks(audio.moov) : [];
|
|
7708
|
+
if (audio && (audioTracks.length !== 1 || audioTracks[0].handler !== "soun")) {
|
|
7709
|
+
throw new Error("[mp4Stitch] the separate audio file must hold one audio track");
|
|
7710
|
+
}
|
|
7711
|
+
const plan2 = [...chunkPlan, ...audioTracks];
|
|
7712
|
+
const chunks = layouts.map((layout, i) => {
|
|
7713
|
+
const tracks = readTracks(layout.moov);
|
|
7714
|
+
if (audio && tracks.some((track) => track.handler === "soun")) {
|
|
7715
|
+
throw new Error(`[mp4Stitch] chunk ${i} carries its own audio`);
|
|
7716
|
+
}
|
|
7717
|
+
return planTracksOf(tracks, plan2, i);
|
|
7718
|
+
});
|
|
7719
|
+
return {
|
|
7720
|
+
plan: plan2,
|
|
7721
|
+
chunkTrackCount: chunkPlan.length,
|
|
7722
|
+
sources: audio ? [[...chunkPlan.map(() => null), audioTracks[0]], ...chunks] : chunks
|
|
7723
|
+
};
|
|
7724
|
+
}
|
|
7725
|
+
function buildStitchedHeader(layouts, audio) {
|
|
7726
|
+
const { plan: plan2, chunkTrackCount, sources } = sourceTracks(layouts, audio);
|
|
7727
|
+
const sourceLayouts = audio ? [audio, ...layouts] : layouts;
|
|
7678
7728
|
const payloadBases = [];
|
|
7679
7729
|
let payloadBytes = 0;
|
|
7680
|
-
for (const layout of
|
|
7730
|
+
for (const layout of sourceLayouts) {
|
|
7681
7731
|
payloadBases.push(payloadBytes);
|
|
7682
7732
|
payloadBytes += layout.payloadEnd - layout.payloadStart;
|
|
7683
7733
|
}
|
|
7684
7734
|
const template = layouts[0].moov;
|
|
7685
7735
|
const root = boxAt(template, 0, 0, template.length);
|
|
7686
|
-
const
|
|
7736
|
+
const mvhd = child(template, root, "mvhd");
|
|
7737
|
+
const movieTimescale = timescaleOf(template, mvhd);
|
|
7738
|
+
const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template, child(template, track.trak, "tkhd")));
|
|
7739
|
+
const audioTrackId = Math.max(...chunkTrackIds) + 1;
|
|
7687
7740
|
const buildMoov = (dataStart2) => {
|
|
7688
7741
|
const traks = plan2.map((track, t) => {
|
|
7689
|
-
const parts =
|
|
7742
|
+
const parts = sources.flatMap((tracks, chunk) => {
|
|
7690
7743
|
const part = tracks[t];
|
|
7691
7744
|
return part ? [{ ...part, chunk }] : [];
|
|
7692
7745
|
});
|
|
7746
|
+
const moov = t < chunkTrackCount ? template : audio.moov;
|
|
7693
7747
|
const merged = {
|
|
7694
7748
|
durations: parts.flatMap((part) => part.durations),
|
|
7695
7749
|
sizes: parts.flatMap((part) => part.sizes),
|
|
@@ -7698,9 +7752,9 @@ function buildStitchedHeader(layouts) {
|
|
|
7698
7752
|
chunkOffsets: parts.flatMap(
|
|
7699
7753
|
(part) => part.chunkOffsets.map((offset) => {
|
|
7700
7754
|
const i = part.chunk;
|
|
7701
|
-
const { payloadStart, payloadEnd } =
|
|
7755
|
+
const { payloadStart, payloadEnd } = sourceLayouts[i];
|
|
7702
7756
|
if (offset < payloadStart || offset >= payloadEnd) {
|
|
7703
|
-
throw new Error(`[mp4Stitch]
|
|
7757
|
+
throw new Error(`[mp4Stitch] source ${i} has samples outside its mdat`);
|
|
7704
7758
|
}
|
|
7705
7759
|
return offset - payloadStart + payloadBases[i] + dataStart2;
|
|
7706
7760
|
})
|
|
@@ -7716,20 +7770,25 @@ function buildStitchedHeader(layouts) {
|
|
|
7716
7770
|
elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
|
|
7717
7771
|
elst.writeUInt32BE(65536, 20);
|
|
7718
7772
|
const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
|
|
7719
|
-
const
|
|
7720
|
-
|
|
7773
|
+
const tkhdOf = (tkhd) => {
|
|
7774
|
+
const timed = withDuration(moov, tkhd, shown);
|
|
7775
|
+
return t < chunkTrackCount ? timed : withId(timed, "tkhd", audioTrackId);
|
|
7776
|
+
};
|
|
7777
|
+
const trak = rebuild(moov, track.trak, {
|
|
7778
|
+
tkhd: (tkhd) => Buffer.concat([tkhdOf(tkhd), edts]),
|
|
7721
7779
|
edts: () => Buffer.alloc(0),
|
|
7722
|
-
mdia: (mdia) => rebuild(
|
|
7723
|
-
mdhd: (mdhd) => withDuration(
|
|
7724
|
-
minf: (minf) => rebuild(
|
|
7780
|
+
mdia: (mdia) => rebuild(moov, mdia, {
|
|
7781
|
+
mdhd: (mdhd) => withDuration(moov, mdhd, mediaDuration),
|
|
7782
|
+
minf: (minf) => rebuild(moov, minf, { stbl: () => buildStbl(track.stsd, merged) })
|
|
7725
7783
|
})
|
|
7726
7784
|
});
|
|
7727
7785
|
return { trak, shown };
|
|
7728
7786
|
});
|
|
7729
7787
|
let next = 0;
|
|
7788
|
+
const movieHeader = withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown)));
|
|
7730
7789
|
return rebuild(template, root, {
|
|
7731
|
-
mvhd: (
|
|
7732
|
-
trak: () => traks[next
|
|
7790
|
+
mvhd: () => audio ? withId(movieHeader, "mvhd", audioTrackId + 1) : movieHeader,
|
|
7791
|
+
trak: () => ++next < chunkTrackCount ? traks[next - 1].trak : Buffer.concat(traks.slice(next - 1).map((t) => t.trak))
|
|
7733
7792
|
});
|
|
7734
7793
|
};
|
|
7735
7794
|
const ftyp = layouts[0].ftyp;
|
|
@@ -7762,15 +7821,16 @@ async function copyPayload(path, layout, index, out) {
|
|
|
7762
7821
|
);
|
|
7763
7822
|
}
|
|
7764
7823
|
}
|
|
7765
|
-
async function writeStitchedMp4(chunkPaths, outputPath) {
|
|
7766
|
-
const
|
|
7767
|
-
const
|
|
7824
|
+
async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
7825
|
+
const sourcePaths = audioPath ? [audioPath, ...chunkPaths] : chunkPaths;
|
|
7826
|
+
const layouts = await Promise.all(sourcePaths.map((path) => readMp4Layout(fileRange(path))));
|
|
7827
|
+
const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
|
|
7768
7828
|
mkdirSync8(dirname9(outputPath), { recursive: true });
|
|
7769
7829
|
const workDir = mkdtempSync3(`${outputPath}.stitch-`);
|
|
7770
7830
|
const out = createWriteStream(join19(workDir, "out.mp4"));
|
|
7771
7831
|
try {
|
|
7772
7832
|
out.write(header);
|
|
7773
|
-
for (const [i, path] of
|
|
7833
|
+
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
7774
7834
|
out.end();
|
|
7775
7835
|
await finished(out);
|
|
7776
7836
|
renameSync3(join19(workDir, "out.mp4"), outputPath);
|
|
@@ -8345,7 +8405,8 @@ function buildLockedRenderConfig(input) {
|
|
|
8345
8405
|
chunkCount: input.chunkCount,
|
|
8346
8406
|
runtimeEnv: input.runtimeEnv,
|
|
8347
8407
|
variables: config.variables,
|
|
8348
|
-
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
|
|
8408
|
+
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {},
|
|
8409
|
+
...input.separateAudio ? { separateAudio: true } : {}
|
|
8349
8410
|
};
|
|
8350
8411
|
}
|
|
8351
8412
|
function carriesChunkAudio(config, options) {
|
|
@@ -8440,8 +8501,10 @@ function createVideoSourceShipper(input) {
|
|
|
8440
8501
|
return result;
|
|
8441
8502
|
};
|
|
8442
8503
|
}
|
|
8443
|
-
|
|
8444
|
-
|
|
8504
|
+
function plansSeparateAudio(config, options) {
|
|
8505
|
+
return options.deferAudio === true && config.format === "mp4" && config.cfr !== true;
|
|
8506
|
+
}
|
|
8507
|
+
async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
8445
8508
|
rejectUnsupportedDistributedFormat(config);
|
|
8446
8509
|
validateNoGpuEncode({
|
|
8447
8510
|
useGpu: false,
|
|
@@ -8577,6 +8640,100 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8577
8640
|
});
|
|
8578
8641
|
}
|
|
8579
8642
|
}
|
|
8643
|
+
return {
|
|
8644
|
+
projectDir,
|
|
8645
|
+
log,
|
|
8646
|
+
sizeLimitBytes,
|
|
8647
|
+
abortSignal,
|
|
8648
|
+
assertNotAborted,
|
|
8649
|
+
cfg,
|
|
8650
|
+
job,
|
|
8651
|
+
workDir,
|
|
8652
|
+
compiledDir,
|
|
8653
|
+
finalCompiledDir,
|
|
8654
|
+
remoteFiles,
|
|
8655
|
+
urlByAbsentLocalPath,
|
|
8656
|
+
localPathByUrl,
|
|
8657
|
+
compileResult,
|
|
8658
|
+
compiled,
|
|
8659
|
+
composition,
|
|
8660
|
+
deviceScaleFactor,
|
|
8661
|
+
forceScreenshot,
|
|
8662
|
+
width,
|
|
8663
|
+
height,
|
|
8664
|
+
probeResult,
|
|
8665
|
+
totalFrames
|
|
8666
|
+
};
|
|
8667
|
+
}
|
|
8668
|
+
function mixPlanAudio(c, compiledDir, seconds, pcmPath) {
|
|
8669
|
+
return runAudioStage({
|
|
8670
|
+
projectDir: c.projectDir,
|
|
8671
|
+
workDir: c.workDir,
|
|
8672
|
+
compiledDir,
|
|
8673
|
+
urlByAbsentLocalPath: c.urlByAbsentLocalPath,
|
|
8674
|
+
duration: seconds,
|
|
8675
|
+
ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
|
|
8676
|
+
audioGain: c.cfg.audioGain,
|
|
8677
|
+
audios: c.composition.audios,
|
|
8678
|
+
masterFxChain: c.composition.masterFxChain,
|
|
8679
|
+
...pcmPath ? { keepEncoderInputAt: pcmPath } : {},
|
|
8680
|
+
abortSignal: c.abortSignal,
|
|
8681
|
+
assertNotAborted: c.assertNotAborted
|
|
8682
|
+
});
|
|
8683
|
+
}
|
|
8684
|
+
async function extractPlanVideos(c, deferRangeExtraction) {
|
|
8685
|
+
const extractResult = await runExtractVideosStage({
|
|
8686
|
+
projectDir: c.projectDir,
|
|
8687
|
+
compiledDir: c.compiledDir,
|
|
8688
|
+
job: c.job,
|
|
8689
|
+
cfg: c.cfg,
|
|
8690
|
+
composition: c.composition,
|
|
8691
|
+
abortSignal: c.abortSignal,
|
|
8692
|
+
assertNotAborted: c.assertNotAborted,
|
|
8693
|
+
materializeSymlinks: true,
|
|
8694
|
+
deferRangeExtraction,
|
|
8695
|
+
urlByAbsentLocalPath: c.urlByAbsentLocalPath
|
|
8696
|
+
});
|
|
8697
|
+
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
8698
|
+
if (extractResult.extractionResult) {
|
|
8699
|
+
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
8700
|
+
}
|
|
8701
|
+
return extractResult;
|
|
8702
|
+
}
|
|
8703
|
+
async function holdPlanAudioPeak(c, audioPath, pcmPath, seconds) {
|
|
8704
|
+
const peak = await constrainAacTruePeak({
|
|
8705
|
+
audioPath,
|
|
8706
|
+
...existsSync15(pcmPath) ? { sourcePath: pcmPath } : {},
|
|
8707
|
+
targetDurationSeconds: seconds,
|
|
8708
|
+
signal: c.abortSignal,
|
|
8709
|
+
timeoutMs: c.cfg.ffmpegProcessTimeout
|
|
8710
|
+
}).finally(() => rmSync8(pcmPath, { force: true }));
|
|
8711
|
+
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
8712
|
+
}
|
|
8713
|
+
async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, options = {}) {
|
|
8714
|
+
const planDir = executionPlanDir;
|
|
8715
|
+
const c = await compilePlanComposition(projectDir, config, planDir, options);
|
|
8716
|
+
const {
|
|
8717
|
+
log,
|
|
8718
|
+
sizeLimitBytes,
|
|
8719
|
+
cfg,
|
|
8720
|
+
job,
|
|
8721
|
+
workDir,
|
|
8722
|
+
compiledDir,
|
|
8723
|
+
finalCompiledDir,
|
|
8724
|
+
remoteFiles,
|
|
8725
|
+
urlByAbsentLocalPath,
|
|
8726
|
+
localPathByUrl,
|
|
8727
|
+
compileResult,
|
|
8728
|
+
compiled,
|
|
8729
|
+
composition,
|
|
8730
|
+
deviceScaleFactor,
|
|
8731
|
+
forceScreenshot,
|
|
8732
|
+
width,
|
|
8733
|
+
height,
|
|
8734
|
+
probeResult,
|
|
8735
|
+
totalFrames
|
|
8736
|
+
} = c;
|
|
8580
8737
|
const megapixels = compileResult.outputWidth * compileResult.outputHeight / 1e6;
|
|
8581
8738
|
let maxParallel = resolveMaxParallelChunks(config, totalFrames, megapixels, log);
|
|
8582
8739
|
const measureFrameCost = chunkSizingUsesFrameCost(config) && planNeedsFrameCostMeasurement(compiled.html);
|
|
@@ -8587,24 +8744,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8587
8744
|
observedAt: "pre-extract",
|
|
8588
8745
|
log
|
|
8589
8746
|
});
|
|
8590
|
-
stageStarted = Date.now();
|
|
8591
|
-
const extractResult = await
|
|
8592
|
-
projectDir,
|
|
8593
|
-
compiledDir,
|
|
8594
|
-
job,
|
|
8595
|
-
cfg,
|
|
8596
|
-
composition,
|
|
8597
|
-
abortSignal,
|
|
8598
|
-
assertNotAborted,
|
|
8599
|
-
materializeSymlinks: true,
|
|
8600
|
-
deferRangeExtraction: options.deferVideoExtraction,
|
|
8601
|
-
urlByAbsentLocalPath
|
|
8602
|
-
});
|
|
8747
|
+
let stageStarted = Date.now();
|
|
8748
|
+
const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
|
|
8603
8749
|
logPlanStage(log, "extract", stageStarted);
|
|
8604
|
-
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
8605
|
-
if (extractResult.extractionResult) {
|
|
8606
|
-
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
8607
|
-
}
|
|
8608
8750
|
const stagedVideoFrames = join22(compiledDir, "__hyperframes_video_frames");
|
|
8609
8751
|
const videoFramesDst = join22(planDir, "video-frames");
|
|
8610
8752
|
if (existsSync15(videoFramesDst)) rmSync8(videoFramesDst, { recursive: true, force: true });
|
|
@@ -8660,23 +8802,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8660
8802
|
"utf-8"
|
|
8661
8803
|
);
|
|
8662
8804
|
const digests = options.digests ?? /* @__PURE__ */ new Map();
|
|
8663
|
-
const
|
|
8664
|
-
const
|
|
8805
|
+
const separateAudio = plansSeparateAudio(config, options);
|
|
8806
|
+
const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
|
|
8807
|
+
const audioSeconds = chunksCarryAudio ? totalFrames / config.fps : probeResult.duration;
|
|
8808
|
+
const planMixPcmPath = join22(workDir, "plan-mix.wav");
|
|
8665
8809
|
stageStarted = Date.now();
|
|
8666
8810
|
const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
|
|
8674
|
-
audioGain: cfg.audioGain,
|
|
8675
|
-
audios: composition.audios,
|
|
8676
|
-
masterFxChain: composition.masterFxChain,
|
|
8677
|
-
abortSignal,
|
|
8678
|
-
assertNotAborted
|
|
8679
|
-
}).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
8811
|
+
separateAudio ? null : mixPlanAudio(
|
|
8812
|
+
c,
|
|
8813
|
+
finalCompiledDir,
|
|
8814
|
+
audioSeconds,
|
|
8815
|
+
chunksCarryAudio ? planMixPcmPath : void 0
|
|
8816
|
+
).finally(() => logPlanStage(log, "audio", stageStarted)),
|
|
8680
8817
|
(async () => {
|
|
8681
8818
|
for (const dir of [finalCompiledDir, videoFramesDst, videoSourcesDir]) {
|
|
8682
8819
|
await prehashPlanFiles(dir, digests);
|
|
@@ -8726,22 +8863,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8726
8863
|
);
|
|
8727
8864
|
const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
|
|
8728
8865
|
const audioResult = audioOutcome.value;
|
|
8729
|
-
if (audioResult
|
|
8866
|
+
if (audioResult?.audioError) {
|
|
8730
8867
|
applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
|
|
8731
8868
|
}
|
|
8732
8869
|
const planAudioPath = join22(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
8733
|
-
if (audioResult
|
|
8870
|
+
if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
|
|
8734
8871
|
renameSync4(audioResult.audioOutputPath, planAudioPath);
|
|
8735
8872
|
}
|
|
8736
|
-
if (
|
|
8873
|
+
if (chunksCarryAudio && existsSync15(planAudioPath)) {
|
|
8737
8874
|
stageStarted = Date.now();
|
|
8738
|
-
|
|
8739
|
-
audioPath: planAudioPath,
|
|
8740
|
-
targetDurationSeconds: audioSeconds,
|
|
8741
|
-
signal: abortSignal,
|
|
8742
|
-
timeoutMs: cfg.ffmpegProcessTimeout
|
|
8743
|
-
});
|
|
8744
|
-
if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
|
|
8875
|
+
await holdPlanAudioPeak(c, planAudioPath, planMixPcmPath, audioSeconds);
|
|
8745
8876
|
logPlanStage(log, "audio-peak", stageStarted);
|
|
8746
8877
|
if (options.sliceChunkAudio) {
|
|
8747
8878
|
stageStarted = Date.now();
|
|
@@ -8767,7 +8898,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8767
8898
|
effectiveChunkSize,
|
|
8768
8899
|
chunkCount,
|
|
8769
8900
|
runtimeEnv,
|
|
8770
|
-
chunksCarryAudio
|
|
8901
|
+
chunksCarryAudio,
|
|
8902
|
+
separateAudio
|
|
8771
8903
|
});
|
|
8772
8904
|
const compositionJson = {
|
|
8773
8905
|
durationSeconds: job.duration ?? 0,
|
|
@@ -8812,7 +8944,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8812
8944
|
fontSnapshotSha,
|
|
8813
8945
|
durationSeconds: job.duration ?? 0,
|
|
8814
8946
|
totalFrames,
|
|
8815
|
-
hasAudio: audioResult
|
|
8947
|
+
hasAudio: audioResult?.hasAudio ?? false,
|
|
8816
8948
|
digests
|
|
8817
8949
|
});
|
|
8818
8950
|
logPlanStage(log, "freeze", stageStarted);
|
|
@@ -8837,6 +8969,33 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8837
8969
|
producerVersion
|
|
8838
8970
|
};
|
|
8839
8971
|
}
|
|
8972
|
+
async function buildPlanAudio(projectDir, config, workRoot, outputPath, options = {}) {
|
|
8973
|
+
const c = await compilePlanComposition(projectDir, config, workRoot, {
|
|
8974
|
+
...options,
|
|
8975
|
+
executionPlanSizeLimitBytes: Number.MAX_SAFE_INTEGER
|
|
8976
|
+
});
|
|
8977
|
+
try {
|
|
8978
|
+
const seconds = c.totalFrames / config.fps;
|
|
8979
|
+
const pcmPath = join22(c.workDir, "plan-mix.wav");
|
|
8980
|
+
let stageStarted = Date.now();
|
|
8981
|
+
await extractPlanVideos(c, true);
|
|
8982
|
+
logPlanStage(c.log, "extract", stageStarted);
|
|
8983
|
+
stageStarted = Date.now();
|
|
8984
|
+
const mixed = await mixPlanAudio(c, c.compiledDir, seconds, pcmPath);
|
|
8985
|
+
logPlanStage(c.log, "audio", stageStarted);
|
|
8986
|
+
if (mixed.audioError) {
|
|
8987
|
+
applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
|
|
8988
|
+
}
|
|
8989
|
+
if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
|
|
8990
|
+
renameSync4(mixed.audioOutputPath, outputPath);
|
|
8991
|
+
stageStarted = Date.now();
|
|
8992
|
+
await holdPlanAudioPeak(c, outputPath, pcmPath, seconds);
|
|
8993
|
+
logPlanStage(c.log, "audio-peak", stageStarted);
|
|
8994
|
+
return { hasAudio: true };
|
|
8995
|
+
} finally {
|
|
8996
|
+
rmSync8(c.workDir, { recursive: true, force: true });
|
|
8997
|
+
}
|
|
8998
|
+
}
|
|
8840
8999
|
async function plan(projectDir, config, planDir, options = {}) {
|
|
8841
9000
|
const executionPlan = await buildLocalExecutionPlan(projectDir, config, planDir, options);
|
|
8842
9001
|
return {
|
|
@@ -9445,7 +9604,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
9445
9604
|
)
|
|
9446
9605
|
),
|
|
9447
9606
|
chunksCarryAudio: options.chunksCarryAudio,
|
|
9448
|
-
sliceChunkAudio: true
|
|
9607
|
+
sliceChunkAudio: true,
|
|
9608
|
+
deferAudio: options.deferAudio
|
|
9449
9609
|
});
|
|
9450
9610
|
return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
|
|
9451
9611
|
} catch (error) {
|
|
@@ -9781,7 +9941,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
9781
9941
|
return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
|
|
9782
9942
|
}
|
|
9783
9943
|
if (chunksAreStitchedMp4(planDir, cfr)) {
|
|
9784
|
-
|
|
9944
|
+
const separateAudio = stitchesSeparateAudio(planDir) && audioPath !== null;
|
|
9945
|
+
await writeStitchedMp4(chunkPaths, outputPath, separateAudio ? audioPath : void 0);
|
|
9785
9946
|
return {
|
|
9786
9947
|
outputPath,
|
|
9787
9948
|
durationMs: Date.now() - start,
|
|
@@ -9979,16 +10140,20 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
|
9979
10140
|
}
|
|
9980
10141
|
}
|
|
9981
10142
|
function chunksAreStitchedMp4(planDir, cfr) {
|
|
9982
|
-
|
|
10143
|
+
const { chunksCarryAudio, separateAudio } = lockedAudio(planDir);
|
|
10144
|
+
if (!chunksCarryAudio && !separateAudio) return false;
|
|
9983
10145
|
if (cfr) {
|
|
9984
|
-
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks
|
|
10146
|
+
throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks are stitched");
|
|
9985
10147
|
}
|
|
9986
10148
|
return true;
|
|
9987
10149
|
}
|
|
9988
|
-
function
|
|
10150
|
+
function stitchesSeparateAudio(planDir) {
|
|
10151
|
+
return lockedAudio(planDir).separateAudio === true;
|
|
10152
|
+
}
|
|
10153
|
+
function lockedAudio(planDir) {
|
|
9989
10154
|
const encoderJsonPath = join26(planDir, "meta", "encoder.json");
|
|
9990
|
-
if (!existsSync18(encoderJsonPath)) return
|
|
9991
|
-
return JSON.parse(readFileSync11(encoderJsonPath, "utf-8"))
|
|
10155
|
+
if (!existsSync18(encoderJsonPath)) return {};
|
|
10156
|
+
return JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
|
|
9992
10157
|
}
|
|
9993
10158
|
function readCfrEncoder(planDir, format) {
|
|
9994
10159
|
if (format !== "mp4") {
|
|
@@ -10062,11 +10227,12 @@ import {
|
|
|
10062
10227
|
calculateOptimalWorkers as calculateOptimalWorkers2,
|
|
10063
10228
|
classifyCaptureFailure as classifyCaptureFailure4,
|
|
10064
10229
|
closeCaptureSession as closeCaptureSession10,
|
|
10065
|
-
createCaptureSession as
|
|
10230
|
+
createCaptureSession as createCaptureSession10,
|
|
10066
10231
|
createFrameLookupTable as createFrameLookupTable5,
|
|
10067
10232
|
deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
|
|
10068
10233
|
getEncoderPreset as getEncoderPreset6,
|
|
10069
|
-
initializeSession as
|
|
10234
|
+
initializeSession as initializeSession10,
|
|
10235
|
+
measureVideoShownBoxes,
|
|
10070
10236
|
probeBeginFrameLiveness as probeBeginFrameLiveness2,
|
|
10071
10237
|
readWebGlVendorInfoFromCanvas,
|
|
10072
10238
|
resolveConfig as resolveConfig5
|
|
@@ -10083,6 +10249,12 @@ function validatePlanVideosForChunk(value) {
|
|
|
10083
10249
|
);
|
|
10084
10250
|
}
|
|
10085
10251
|
}
|
|
10252
|
+
var MIN_FRAMES_PER_CHUNK_BROWSER = 20;
|
|
10253
|
+
function resolveChunkCaptureWorkers(framesInChunk, machineSized, maxCaptureWorkers) {
|
|
10254
|
+
if (maxCaptureWorkers === void 0) return machineSized;
|
|
10255
|
+
const byFrames = Math.floor(framesInChunk / MIN_FRAMES_PER_CHUNK_BROWSER);
|
|
10256
|
+
return Math.max(machineSized, Math.floor(Math.min(maxCaptureWorkers, byFrames)));
|
|
10257
|
+
}
|
|
10086
10258
|
function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
|
|
10087
10259
|
if (frameRange === void 0) return { startFrame: slice.startFrame, endFrame: slice.endFrame };
|
|
10088
10260
|
const { startFrame, endFrame } = frameRange;
|
|
@@ -10095,9 +10267,9 @@ function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
|
|
|
10095
10267
|
return { startFrame, endFrame };
|
|
10096
10268
|
}
|
|
10097
10269
|
var distributedCaptureSessionDependencies = {
|
|
10098
|
-
createCaptureSession:
|
|
10270
|
+
createCaptureSession: createCaptureSession10,
|
|
10099
10271
|
assertSwiftShader,
|
|
10100
|
-
initializeSession:
|
|
10272
|
+
initializeSession: initializeSession10,
|
|
10101
10273
|
closeCaptureSession: closeCaptureSession10,
|
|
10102
10274
|
readWebGlVendorInfo: readWebGlVendorInfoFromCanvas
|
|
10103
10275
|
};
|
|
@@ -10288,34 +10460,15 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10288
10460
|
const framesDir = join27(workDir, "captured-frames");
|
|
10289
10461
|
mkdirSync14(framesDir, { recursive: true });
|
|
10290
10462
|
const videoFramesDir = `${workDir}.video-frames`;
|
|
10291
|
-
const
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
10296
|
-
outputRoot: videoFramesDir,
|
|
10297
|
-
cfg,
|
|
10298
|
-
remoteSources
|
|
10299
|
-
}) : [];
|
|
10463
|
+
const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
|
|
10464
|
+
const chunkFps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
|
|
10465
|
+
const deferredVideos = planVideos?.extracted.filter((video) => video.deferredRange) ?? [];
|
|
10466
|
+
const shownFrames = planVideos ? shownDeferredFrameIndexes(planVideos, chunkFrameNumbers, chunkFps) : /* @__PURE__ */ new Map();
|
|
10300
10467
|
const remoteVideoIds = new Set(
|
|
10301
10468
|
planVideos?.extracted.filter(
|
|
10302
10469
|
(video) => video.deferredRange && Object.hasOwn(remoteSources, video.deferredRange.sourcePath)
|
|
10303
10470
|
).map((video) => video.videoId)
|
|
10304
10471
|
);
|
|
10305
|
-
const remoteVideos = deferredVideoFrames.filter((video) => remoteVideoIds.has(video.videoId));
|
|
10306
|
-
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
10307
|
-
planVideos.videos,
|
|
10308
|
-
[
|
|
10309
|
-
...rebuildExtractedFramesFromPlanDir(
|
|
10310
|
-
planDir,
|
|
10311
|
-
planVideos.extracted.filter((video) => !video.deferredRange),
|
|
10312
|
-
v2Manifest === null ? "dense-v1" : "sparse-v2"
|
|
10313
|
-
),
|
|
10314
|
-
...deferredVideoFrames
|
|
10315
|
-
],
|
|
10316
|
-
resolveRenderFpsConfig(job.config.fps).value
|
|
10317
|
-
) : null;
|
|
10318
|
-
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
|
|
10319
10472
|
const fileServer = await createFileServer({
|
|
10320
10473
|
projectDir: compiledDir,
|
|
10321
10474
|
compiledDir,
|
|
@@ -10331,10 +10484,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10331
10484
|
videoCount: planVideos?.videos.length ?? 0,
|
|
10332
10485
|
html: readFileSync12(join27(compiledDir, "index.html"), "utf-8"),
|
|
10333
10486
|
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
|
|
10334
|
-
skipReadinessVideoIds:
|
|
10335
|
-
|
|
10487
|
+
skipReadinessVideoIds: deferredVideos.filter(
|
|
10488
|
+
(video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
|
|
10489
|
+
).map((video) => video.videoId),
|
|
10490
|
+
videoMetadataHints: collectVideoMetadataHints(
|
|
10491
|
+
deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
|
|
10492
|
+
)
|
|
10336
10493
|
});
|
|
10337
|
-
const chunkWorkerCount =
|
|
10494
|
+
const chunkWorkerCount = resolveChunkCaptureWorkers(
|
|
10495
|
+
framesInChunk,
|
|
10496
|
+
calculateOptimalWorkers2(framesInChunk, void 0, cfg),
|
|
10497
|
+
options?.maxCaptureWorkers
|
|
10498
|
+
);
|
|
10338
10499
|
let session = null;
|
|
10339
10500
|
let outputKind;
|
|
10340
10501
|
let framesEncoded = 0;
|
|
@@ -10345,6 +10506,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10345
10506
|
const capturePerfs = [];
|
|
10346
10507
|
const captureAttempts = [];
|
|
10347
10508
|
let forceScreenshotForChunk = encoder.forceScreenshot;
|
|
10509
|
+
let shownBoxes;
|
|
10348
10510
|
try {
|
|
10349
10511
|
if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
|
|
10350
10512
|
const bootStarted = Date.now();
|
|
@@ -10355,6 +10517,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10355
10517
|
cfg
|
|
10356
10518
|
);
|
|
10357
10519
|
sessionBootMs = Date.now() - bootStarted;
|
|
10520
|
+
shownBoxes = await measureVideoShownBoxes(
|
|
10521
|
+
session.page,
|
|
10522
|
+
[...shownFrames.keys()],
|
|
10523
|
+
chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
|
|
10524
|
+
);
|
|
10358
10525
|
const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
|
|
10359
10526
|
const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
|
|
10360
10527
|
if (browserSelectedScreenshot) {
|
|
@@ -10396,6 +10563,29 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10396
10563
|
session = null;
|
|
10397
10564
|
}
|
|
10398
10565
|
}
|
|
10566
|
+
const deferredVideoFrames = planVideos ? await extractDeferredVideoFramesForChunk({
|
|
10567
|
+
planDir,
|
|
10568
|
+
planVideos,
|
|
10569
|
+
frames: chunkFrameNumbers,
|
|
10570
|
+
fps: chunkFps,
|
|
10571
|
+
outputRoot: videoFramesDir,
|
|
10572
|
+
cfg,
|
|
10573
|
+
remoteSources,
|
|
10574
|
+
shownBoxes
|
|
10575
|
+
}) : [];
|
|
10576
|
+
const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable5(
|
|
10577
|
+
planVideos.videos,
|
|
10578
|
+
[
|
|
10579
|
+
...rebuildExtractedFramesFromPlanDir(
|
|
10580
|
+
planDir,
|
|
10581
|
+
planVideos.extracted.filter((video) => !video.deferredRange),
|
|
10582
|
+
v2Manifest === null ? "dense-v1" : "sparse-v2"
|
|
10583
|
+
),
|
|
10584
|
+
...deferredVideoFrames
|
|
10585
|
+
],
|
|
10586
|
+
resolveRenderFpsConfig(job.config.fps).value
|
|
10587
|
+
) : null;
|
|
10588
|
+
const createChunkVideoFrameInjector = createChunkVideoFrameInjectorFactory(videoFrameLookup);
|
|
10399
10589
|
const captureStarted = Date.now();
|
|
10400
10590
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
10401
10591
|
forceScreenshot: forceScreenshotForChunk,
|
|
@@ -10586,6 +10776,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10586
10776
|
encodeStageMs,
|
|
10587
10777
|
workers: chunkWorkerCount,
|
|
10588
10778
|
captureMode,
|
|
10779
|
+
// Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
|
|
10780
|
+
videoShownBoxes: Object.fromEntries(shownBoxes ?? []),
|
|
10589
10781
|
sha256,
|
|
10590
10782
|
outputKind,
|
|
10591
10783
|
producerVersion: plan2.producerVersion,
|
|
@@ -10897,10 +11089,11 @@ function validateIntDimension(field, value) {
|
|
|
10897
11089
|
}
|
|
10898
11090
|
|
|
10899
11091
|
// src/services/distributed/projectHash.ts
|
|
10900
|
-
import { readdirSync as readdirSync10,
|
|
11092
|
+
import { closeSync as closeSync5, openSync as openSync5, readdirSync as readdirSync10, readSync as readSync3, realpathSync as realpathSync2, statSync as statSync8 } from "node:fs";
|
|
10901
11093
|
import { createHash as createHash6 } from "node:crypto";
|
|
10902
11094
|
import { join as join29 } from "node:path";
|
|
10903
11095
|
var SITE_ID_SCHEME = "site-v2\0";
|
|
11096
|
+
var READ_CHUNK_BYTES = 256 * 1024;
|
|
10904
11097
|
function listSiteFiles(projectDir) {
|
|
10905
11098
|
const files = [];
|
|
10906
11099
|
const ancestors = /* @__PURE__ */ new Set([realpathSync2(projectDir)]);
|
|
@@ -10933,10 +11126,19 @@ function listSiteFiles(projectDir) {
|
|
|
10933
11126
|
}
|
|
10934
11127
|
function hashProjectDir(projectDir, files = listSiteFiles(projectDir)) {
|
|
10935
11128
|
const hash = createHash6("sha256").update(SITE_ID_SCHEME);
|
|
11129
|
+
const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
10936
11130
|
for (const rel of files) {
|
|
10937
11131
|
hash.update(rel);
|
|
10938
11132
|
hash.update("\0");
|
|
10939
|
-
|
|
11133
|
+
const fd = openSync5(join29(projectDir, rel), "r");
|
|
11134
|
+
try {
|
|
11135
|
+
let read;
|
|
11136
|
+
while ((read = readSync3(fd, chunk, 0, chunk.length, null)) > 0) {
|
|
11137
|
+
hash.update(chunk.subarray(0, read));
|
|
11138
|
+
}
|
|
11139
|
+
} finally {
|
|
11140
|
+
closeSync5(fd);
|
|
11141
|
+
}
|
|
10940
11142
|
}
|
|
10941
11143
|
return hash.digest("hex").slice(0, 16);
|
|
10942
11144
|
}
|
|
@@ -10984,6 +11186,7 @@ export {
|
|
|
10984
11186
|
assemble,
|
|
10985
11187
|
assembleV2,
|
|
10986
11188
|
buildChunkSlices,
|
|
11189
|
+
buildPlanAudio,
|
|
10987
11190
|
buildStitchedHeader,
|
|
10988
11191
|
chunksAreStitchedMp4,
|
|
10989
11192
|
createPlanV2FromExecutionPlan,
|
|
@@ -11001,6 +11204,7 @@ export {
|
|
|
11001
11204
|
plan,
|
|
11002
11205
|
planV2,
|
|
11003
11206
|
planV2WithPublisher,
|
|
11207
|
+
plansSeparateAudio,
|
|
11004
11208
|
publishPlanV2FromExecutionPlan,
|
|
11005
11209
|
publishPlanV2FromV1,
|
|
11006
11210
|
readMp4Layout,
|
|
@@ -11013,6 +11217,7 @@ export {
|
|
|
11013
11217
|
renderChunkV2,
|
|
11014
11218
|
resolveChunkPlan,
|
|
11015
11219
|
resolvePlanAudioPath,
|
|
11220
|
+
stitchesSeparateAudio,
|
|
11016
11221
|
validateDistributedRenderConfig,
|
|
11017
11222
|
validatePlanV2MaterializedTarget,
|
|
11018
11223
|
validateVariablesPayload
|