@hyperframes/producer 0.8.191 → 0.8.193
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 +300 -109
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +62 -62
- package/dist/index.js +503 -456
- package/dist/public-server.js +333 -299
- package/dist/services/distributed/plan.d.ts +2 -0
- package/dist/services/distributed/probeFacts.d.ts +4 -0
- package/dist/services/render/perfSummary.d.ts +6 -0
- package/dist/services/render/stages/audioStage.d.ts +3 -1
- package/dist/services/render/stages/extractVideosStage.d.ts +17 -2
- package/dist/services/renderOrchestrator.d.ts +2 -0
- package/package.json +7 -7
package/dist/distributed.js
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
existsSync as existsSync15,
|
|
7
7
|
linkSync,
|
|
8
8
|
mkdirSync as mkdirSync10,
|
|
9
|
-
renameSync as renameSync4,
|
|
10
9
|
rmSync as rmSync9,
|
|
11
10
|
writeFileSync as writeFileSync6
|
|
12
11
|
} from "node:fs";
|
|
@@ -18,7 +17,10 @@ import {
|
|
|
18
17
|
import {
|
|
19
18
|
getEncoderPreset as getEncoderPreset4,
|
|
20
19
|
normalizeVp9CpuUsed,
|
|
21
|
-
resolveConfig as resolveConfig3
|
|
20
|
+
resolveConfig as resolveConfig3,
|
|
21
|
+
declareContentAddressedUrl,
|
|
22
|
+
renameWithRetry as renameWithRetry6,
|
|
23
|
+
declareProbeFactsUrl
|
|
22
24
|
} from "@hyperframes/engine";
|
|
23
25
|
|
|
24
26
|
// src/logger.ts
|
|
@@ -1149,7 +1151,7 @@ function encoderFailureError(prefix, result) {
|
|
|
1149
1151
|
}
|
|
1150
1152
|
|
|
1151
1153
|
// src/services/render/artifactTransaction.ts
|
|
1152
|
-
import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST } from "@hyperframes/engine";
|
|
1154
|
+
import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, renameWithRetry } from "@hyperframes/engine";
|
|
1153
1155
|
|
|
1154
1156
|
// src/utils/ffprobe.ts
|
|
1155
1157
|
import {
|
|
@@ -1226,6 +1228,24 @@ import {
|
|
|
1226
1228
|
|
|
1227
1229
|
// src/services/render/captureStageError.ts
|
|
1228
1230
|
import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
|
|
1231
|
+
|
|
1232
|
+
// src/services/render/perfSummary.ts
|
|
1233
|
+
import { fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
|
|
1234
|
+
function pushWorkerDedupPerfs(results, sink) {
|
|
1235
|
+
for (const r of results) {
|
|
1236
|
+
if (r.perf) sink.push(r.perf);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function pushWorkerPageErrors(results, sink) {
|
|
1240
|
+
for (const result of results) {
|
|
1241
|
+
for (const line of result.pageErrorLines ?? []) if (!sink.includes(line)) sink.push(line);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
function withWorkerPageErrors(consoleLines, workerLines) {
|
|
1245
|
+
return [...consoleLines, ...workerLines.filter((line) => !consoleLines.includes(line))];
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// src/services/render/captureStageError.ts
|
|
1229
1249
|
var CaptureStageError = class extends CaptureFailure {
|
|
1230
1250
|
browserConsole;
|
|
1231
1251
|
constructor(input) {
|
|
@@ -1234,10 +1254,13 @@ var CaptureStageError = class extends CaptureFailure {
|
|
|
1234
1254
|
kind: classified.kind,
|
|
1235
1255
|
message: normalizeErrorMessage(input.cause),
|
|
1236
1256
|
cause: input.cause,
|
|
1237
|
-
workerDiagnostics: classified.workerDiagnostics
|
|
1257
|
+
workerDiagnostics: classified.workerDiagnostics,
|
|
1258
|
+
pageErrorLines: classified.pageErrorLines
|
|
1238
1259
|
});
|
|
1239
1260
|
this.name = "CaptureStageError";
|
|
1240
|
-
this.browserConsole = input.browserConsole
|
|
1261
|
+
this.browserConsole = withWorkerPageErrors(input.browserConsole, [
|
|
1262
|
+
...classified.pageErrorLines
|
|
1263
|
+
]);
|
|
1241
1264
|
if (input.cause instanceof Error && input.cause.stack) {
|
|
1242
1265
|
this.stack = input.cause.stack;
|
|
1243
1266
|
}
|
|
@@ -1297,7 +1320,7 @@ import {
|
|
|
1297
1320
|
resolveVideoExtractionWindow,
|
|
1298
1321
|
runFfmpeg
|
|
1299
1322
|
} from "@hyperframes/engine";
|
|
1300
|
-
import { fpsToFfmpegArg, fpsToNumber as
|
|
1323
|
+
import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
|
|
1301
1324
|
var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
|
|
1302
1325
|
var BYTES_PER_MIB = 1024 * 1024;
|
|
1303
1326
|
|
|
@@ -1323,13 +1346,8 @@ import {
|
|
|
1323
1346
|
spawnStreamingEncoder
|
|
1324
1347
|
} from "@hyperframes/engine";
|
|
1325
1348
|
|
|
1326
|
-
// src/services/render/
|
|
1327
|
-
import {
|
|
1328
|
-
function pushWorkerDedupPerfs(results, sink) {
|
|
1329
|
-
for (const r of results) {
|
|
1330
|
-
if (r.perf) sink.push(r.perf);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1349
|
+
// src/services/render/segmentManifest.ts
|
|
1350
|
+
import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
|
|
1333
1351
|
|
|
1334
1352
|
// src/services/render/hdrMode.ts
|
|
1335
1353
|
import {
|
|
@@ -1683,19 +1701,11 @@ import {
|
|
|
1683
1701
|
|
|
1684
1702
|
// src/services/animatedGifPrep.ts
|
|
1685
1703
|
import { createHash as createHash2 } from "node:crypto";
|
|
1686
|
-
import {
|
|
1687
|
-
copyFileSync as copyFileSync2,
|
|
1688
|
-
existsSync as existsSync4,
|
|
1689
|
-
mkdirSync as mkdirSync3,
|
|
1690
|
-
readFileSync as readFileSync3,
|
|
1691
|
-
renameSync,
|
|
1692
|
-
rmSync as rmSync3,
|
|
1693
|
-
statSync as statSync2
|
|
1694
|
-
} from "node:fs";
|
|
1704
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
|
|
1695
1705
|
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, resolve as resolve4 } from "node:path";
|
|
1696
1706
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
1697
1707
|
import { parseAnimatedGifMetadata } from "@hyperframes/core";
|
|
1698
|
-
import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2 } from "@hyperframes/engine";
|
|
1708
|
+
import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
|
|
1699
1709
|
var PREPARED_GIF_SUBDIR = "_animated_gif";
|
|
1700
1710
|
var CACHE_SCHEMA = "hfgif-v1";
|
|
1701
1711
|
function splitUrlSuffix(src) {
|
|
@@ -1874,7 +1884,7 @@ async function ensurePreparedWebm(input) {
|
|
|
1874
1884
|
throw new Error("Animated GIF transcode produced an empty output");
|
|
1875
1885
|
}
|
|
1876
1886
|
if (!isUsableFile(input.cachePath)) {
|
|
1877
|
-
|
|
1887
|
+
renameWithRetry3(tmpPath, input.cachePath);
|
|
1878
1888
|
} else {
|
|
1879
1889
|
rmSync3(tmpPath, { force: true });
|
|
1880
1890
|
}
|
|
@@ -4281,6 +4291,10 @@ async function runProbeStage(input) {
|
|
|
4281
4291
|
if (el.loop && !existing.loop) {
|
|
4282
4292
|
existing.loop = true;
|
|
4283
4293
|
}
|
|
4294
|
+
const audio = composition.audios.find(
|
|
4295
|
+
(audio2) => audio2.type === "video" && audio2.id === `${el.id}-audio`
|
|
4296
|
+
);
|
|
4297
|
+
if (audio) audio.loop = existing.loop;
|
|
4284
4298
|
}
|
|
4285
4299
|
} else if (!compiled.droppedMediaIds.has(el.id)) {
|
|
4286
4300
|
composition.videos.push({
|
|
@@ -4318,6 +4332,7 @@ async function runProbeStage(input) {
|
|
|
4318
4332
|
el,
|
|
4319
4333
|
authoredStarts.get(el.id) ?? existing.mediaStart
|
|
4320
4334
|
);
|
|
4335
|
+
existing.loop = el.loop;
|
|
4321
4336
|
if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
|
|
4322
4337
|
existing.volume = el.volume;
|
|
4323
4338
|
}
|
|
@@ -4329,6 +4344,7 @@ async function runProbeStage(input) {
|
|
|
4329
4344
|
start: el.start,
|
|
4330
4345
|
end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
|
|
4331
4346
|
mediaStart: el.mediaStart,
|
|
4347
|
+
loop: el.loop,
|
|
4332
4348
|
layer: 0,
|
|
4333
4349
|
volume: el.volume,
|
|
4334
4350
|
type: "audio"
|
|
@@ -4583,6 +4599,10 @@ import {
|
|
|
4583
4599
|
extractMediaMetadata as extractMediaMetadata3,
|
|
4584
4600
|
isVideoPastTimelineEnd,
|
|
4585
4601
|
isHdrColorSpace as isHdrColorSpace2,
|
|
4602
|
+
rebaseVideoToWindow,
|
|
4603
|
+
resolveFinalFrameExtractionWindow as resolveFinalFrameExtractionWindow2,
|
|
4604
|
+
resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
|
|
4605
|
+
isHttpUrl as isHttpUrl2,
|
|
4586
4606
|
resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
|
|
4587
4607
|
runVideoExtractionWithRetry,
|
|
4588
4608
|
safeVideoExtractionSourceIdentity
|
|
@@ -4948,6 +4968,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
4948
4968
|
start: video.start,
|
|
4949
4969
|
end: video.end,
|
|
4950
4970
|
mediaStart: video.mediaStart,
|
|
4971
|
+
playbackRate: video.playbackRate,
|
|
4972
|
+
loop: video.loop,
|
|
4951
4973
|
layer: 0,
|
|
4952
4974
|
volume: 1,
|
|
4953
4975
|
type: "video"
|
|
@@ -4955,6 +4977,33 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
|
|
|
4955
4977
|
existingAudioSrcs.add(video.src);
|
|
4956
4978
|
}
|
|
4957
4979
|
}
|
|
4980
|
+
async function appendVideoAudioFromProbe(input) {
|
|
4981
|
+
const { projectDir, compiledDir, composition, urlByAbsentLocalPath } = input;
|
|
4982
|
+
const videos = composition.videos.filter(
|
|
4983
|
+
(video) => !isVideoPastTimelineEnd(video, composition.duration)
|
|
4984
|
+
);
|
|
4985
|
+
const probed = await Promise.all(
|
|
4986
|
+
videos.map(async (video) => {
|
|
4987
|
+
const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
|
|
4988
|
+
const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
|
|
4989
|
+
if (!isHttpUrl2(readFrom) && !existsSync7(readFrom)) return null;
|
|
4990
|
+
const metadata = await extractMediaMetadata3(readFrom);
|
|
4991
|
+
const window2 = await resolveFinalFrameExtractionWindow2(
|
|
4992
|
+
readFrom,
|
|
4993
|
+
video,
|
|
4994
|
+
metadata,
|
|
4995
|
+
resolveVideoExtractionWindow2(video, metadata, composition.duration)
|
|
4996
|
+
);
|
|
4997
|
+
if (window2.durationSeconds <= 0) return null;
|
|
4998
|
+
rebaseVideoToWindow(video, window2);
|
|
4999
|
+
return { videoId: video.id, metadata };
|
|
5000
|
+
})
|
|
5001
|
+
);
|
|
5002
|
+
appendAutoDetectedVideoAudio(
|
|
5003
|
+
composition,
|
|
5004
|
+
probed.filter((entry) => entry !== null)
|
|
5005
|
+
);
|
|
5006
|
+
}
|
|
4958
5007
|
|
|
4959
5008
|
// src/services/render/stages/audioStage.ts
|
|
4960
5009
|
import { join as join10 } from "node:path";
|
|
@@ -4967,7 +5016,7 @@ import {
|
|
|
4967
5016
|
|
|
4968
5017
|
// src/services/render/audioPadTrim.ts
|
|
4969
5018
|
import { spawn } from "node:child_process";
|
|
4970
|
-
import { mkdtempSync as mkdtempSync2,
|
|
5019
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync4 } from "node:fs";
|
|
4971
5020
|
import { dirname as dirname5, join as join9 } from "node:path";
|
|
4972
5021
|
import {
|
|
4973
5022
|
DEFAULT_CONFIG,
|
|
@@ -4976,6 +5025,7 @@ import {
|
|
|
4976
5025
|
extractAudioMetadata as extractAudioMetadata3,
|
|
4977
5026
|
getFfprobeBinary,
|
|
4978
5027
|
ManagedChildProcess,
|
|
5028
|
+
renameWithRetry as renameWithRetry4,
|
|
4979
5029
|
runFfmpeg as runFfmpeg4,
|
|
4980
5030
|
trackChildProcess
|
|
4981
5031
|
} from "@hyperframes/engine";
|
|
@@ -5231,7 +5281,7 @@ async function enforceAacTruePeak(input) {
|
|
|
5231
5281
|
}
|
|
5232
5282
|
if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
|
|
5233
5283
|
if (measuredPath !== correctedPath) return { success: true };
|
|
5234
|
-
|
|
5284
|
+
renameWithRetry4(correctedPath, input.audioPath);
|
|
5235
5285
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
5236
5286
|
}
|
|
5237
5287
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
@@ -5421,6 +5471,7 @@ async function runAudioStage(input) {
|
|
|
5421
5471
|
const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
|
|
5422
5472
|
let audioLoweredDb;
|
|
5423
5473
|
if (result.hasAudio) {
|
|
5474
|
+
const guardStart = Date.now();
|
|
5424
5475
|
const peak = await constrainAacTruePeak({
|
|
5425
5476
|
audioPath: result.audioOutputPath,
|
|
5426
5477
|
sourcePath: pcmSourcePath,
|
|
@@ -5430,6 +5481,7 @@ async function runAudioStage(input) {
|
|
|
5430
5481
|
});
|
|
5431
5482
|
if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
|
|
5432
5483
|
audioLoweredDb = peak.loweredDb;
|
|
5484
|
+
input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
|
|
5433
5485
|
}
|
|
5434
5486
|
return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
|
|
5435
5487
|
} finally {
|
|
@@ -5455,6 +5507,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
5455
5507
|
let hasAudio = false;
|
|
5456
5508
|
let audioError;
|
|
5457
5509
|
let audioFailures;
|
|
5510
|
+
let audioPhaseMs;
|
|
5458
5511
|
if (audios.length > 0) {
|
|
5459
5512
|
let audioResult;
|
|
5460
5513
|
try {
|
|
@@ -5504,6 +5557,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
5504
5557
|
assertAudioNotInterrupted(audioResult.failures);
|
|
5505
5558
|
hasAudio = audioResult.success;
|
|
5506
5559
|
audioFailures = audioResult.failures;
|
|
5560
|
+
audioPhaseMs = audioResult.phaseMs;
|
|
5507
5561
|
if (!hasAudio) {
|
|
5508
5562
|
audioError = audioResult.error ?? "audio mix failed for an unknown reason";
|
|
5509
5563
|
for (const failure of audioResult.failures ?? []) {
|
|
@@ -5522,7 +5576,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
|
|
|
5522
5576
|
audioOutputPath,
|
|
5523
5577
|
hasAudio,
|
|
5524
5578
|
audioError,
|
|
5525
|
-
audioFailures
|
|
5579
|
+
audioFailures,
|
|
5580
|
+
audioPhaseMs
|
|
5526
5581
|
};
|
|
5527
5582
|
}
|
|
5528
5583
|
|
|
@@ -5632,6 +5687,7 @@ async function runCaptureStage(input) {
|
|
|
5632
5687
|
let { workerCount } = plan2;
|
|
5633
5688
|
const { forceScreenshot } = plan2;
|
|
5634
5689
|
let lastBrowserConsole = [];
|
|
5690
|
+
const workerPageErrors = [];
|
|
5635
5691
|
let captureBeyondViewport = probeSession?.options.captureBeyondViewport;
|
|
5636
5692
|
const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : { ...cfg, forceScreenshot };
|
|
5637
5693
|
if (frameRange !== void 0) {
|
|
@@ -5669,6 +5725,7 @@ async function runCaptureStage(input) {
|
|
|
5669
5725
|
abortSignal,
|
|
5670
5726
|
frameRangeStart: frameRange?.startFrame,
|
|
5671
5727
|
dedupPerfs,
|
|
5728
|
+
pageErrorLines: workerPageErrors,
|
|
5672
5729
|
onProgress: (progress) => {
|
|
5673
5730
|
if (progress.latestWorkerPhase) {
|
|
5674
5731
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
@@ -5689,6 +5746,11 @@ async function runCaptureStage(input) {
|
|
|
5689
5746
|
},
|
|
5690
5747
|
cfg: captureCfg,
|
|
5691
5748
|
log
|
|
5749
|
+
}).catch((error) => {
|
|
5750
|
+
throw wrapCaptureStageError(
|
|
5751
|
+
error,
|
|
5752
|
+
withWorkerPageErrors(probeSession?.browserConsoleBuffer ?? [], workerPageErrors)
|
|
5753
|
+
);
|
|
5692
5754
|
});
|
|
5693
5755
|
captureAttempts.push(...attempts);
|
|
5694
5756
|
const lastAttempt = attempts[attempts.length - 1];
|
|
@@ -5701,6 +5763,7 @@ async function runCaptureStage(input) {
|
|
|
5701
5763
|
await closeCaptureSession5(probeSession);
|
|
5702
5764
|
probeSession = null;
|
|
5703
5765
|
}
|
|
5766
|
+
lastBrowserConsole = withWorkerPageErrors(lastBrowserConsole, workerPageErrors);
|
|
5704
5767
|
} else {
|
|
5705
5768
|
const sequential = await runSequentialDiskCapture(
|
|
5706
5769
|
input,
|
|
@@ -5908,6 +5971,7 @@ import {
|
|
|
5908
5971
|
statSync as statSync3,
|
|
5909
5972
|
writeFileSync as writeFileSync2
|
|
5910
5973
|
} from "node:fs";
|
|
5974
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
5911
5975
|
import { dirname as dirname6, join as join12 } from "node:path";
|
|
5912
5976
|
import {
|
|
5913
5977
|
encodeFramesChunkedConcat,
|
|
@@ -6077,6 +6141,100 @@ function videoEncoderOptions(input, engineCfg) {
|
|
|
6077
6141
|
gopSize: input.gopSize
|
|
6078
6142
|
};
|
|
6079
6143
|
}
|
|
6144
|
+
function canEncodeWhileCapturing(input, workerCount, cpuCount) {
|
|
6145
|
+
return workerCount > 1 && workerCount < cpuCount && !input.isPngSequence && !input.isGif && !input.enableChunkedEncode && input.preset.codec === "h264" && !input.preset.hdr && input.job.config.useGpu !== true && input.captureImageFormat === "jpeg";
|
|
6146
|
+
}
|
|
6147
|
+
async function readLandedFrame(workerPath, mergedPath) {
|
|
6148
|
+
if (workerPath !== void 0) {
|
|
6149
|
+
try {
|
|
6150
|
+
return await readFile2(workerPath);
|
|
6151
|
+
} catch (error) {
|
|
6152
|
+
if (error.code !== "ENOENT") throw error;
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
return readFile2(mergedPath);
|
|
6156
|
+
}
|
|
6157
|
+
function encodeWhileCapturing(input, totalFrames) {
|
|
6158
|
+
const engineCfg = resolveEngineConfig(input);
|
|
6159
|
+
const stop = new AbortController();
|
|
6160
|
+
const signal = input.abortSignal ? AbortSignal.any([input.abortSignal, stop.signal]) : stop.signal;
|
|
6161
|
+
const landed = /* @__PURE__ */ new Map();
|
|
6162
|
+
let captureDone = false;
|
|
6163
|
+
let framesFed = 0;
|
|
6164
|
+
let wake = null;
|
|
6165
|
+
const nudge = () => {
|
|
6166
|
+
wake?.();
|
|
6167
|
+
wake = null;
|
|
6168
|
+
};
|
|
6169
|
+
signal.addEventListener("abort", nudge, { once: true });
|
|
6170
|
+
let reportFed = null;
|
|
6171
|
+
const encoder = spawnStreamingEncoder4(
|
|
6172
|
+
input.videoOnlyPath,
|
|
6173
|
+
{ ...videoEncoderOptions(input, engineCfg), imageFormat: "jpeg" },
|
|
6174
|
+
signal,
|
|
6175
|
+
engineCfg
|
|
6176
|
+
);
|
|
6177
|
+
const fed = (async () => {
|
|
6178
|
+
const stream = await encoder;
|
|
6179
|
+
for (let next = 0; next < totalFrames; next++) {
|
|
6180
|
+
while (!landed.has(next) && !captureDone && !signal.aborted) {
|
|
6181
|
+
await new Promise((resolve9) => wake = resolve9);
|
|
6182
|
+
}
|
|
6183
|
+
if (signal.aborted) return;
|
|
6184
|
+
const mergedPath = join12(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
6185
|
+
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
6186
|
+
landed.delete(next);
|
|
6187
|
+
if (!await stream.writeFrame(frame)) {
|
|
6188
|
+
throw new Error(stream.getExitError() ?? "encoder exited before the last frame");
|
|
6189
|
+
}
|
|
6190
|
+
framesFed++;
|
|
6191
|
+
reportFed?.(framesFed);
|
|
6192
|
+
}
|
|
6193
|
+
})().then(
|
|
6194
|
+
() => void 0,
|
|
6195
|
+
(error) => error ?? new Error("encode failed")
|
|
6196
|
+
);
|
|
6197
|
+
const closeEncoder = async () => (await encoder.catch(() => null))?.close();
|
|
6198
|
+
return {
|
|
6199
|
+
onFrameLanded: (frame) => {
|
|
6200
|
+
landed.set(frame.index, frame.path);
|
|
6201
|
+
nudge();
|
|
6202
|
+
},
|
|
6203
|
+
finish: async () => {
|
|
6204
|
+
const start = Date.now();
|
|
6205
|
+
captureDone = true;
|
|
6206
|
+
input.log.info("[Render] Encoded frames while capturing", {
|
|
6207
|
+
framesFed,
|
|
6208
|
+
totalFrames
|
|
6209
|
+
});
|
|
6210
|
+
const encoded = startEncodeProgress(input.job, "Encoding video", input.onProgress);
|
|
6211
|
+
reportFed = encoded;
|
|
6212
|
+
nudge();
|
|
6213
|
+
const failure = await fed;
|
|
6214
|
+
const result = await closeEncoder();
|
|
6215
|
+
input.assertNotAborted();
|
|
6216
|
+
if (failure === void 0 && result?.success) {
|
|
6217
|
+
encoded();
|
|
6218
|
+
return { encodeMs: Date.now() - start };
|
|
6219
|
+
}
|
|
6220
|
+
input.log.warn(
|
|
6221
|
+
"[Render] Encoding while capturing failed; encoding the frames after capture",
|
|
6222
|
+
{
|
|
6223
|
+
error: String(failure ?? result?.error)
|
|
6224
|
+
}
|
|
6225
|
+
);
|
|
6226
|
+
return runEncodeStage(input);
|
|
6227
|
+
},
|
|
6228
|
+
abort: async () => {
|
|
6229
|
+
stop.abort();
|
|
6230
|
+
await fed;
|
|
6231
|
+
await closeEncoder();
|
|
6232
|
+
},
|
|
6233
|
+
get framesFed() {
|
|
6234
|
+
return framesFed;
|
|
6235
|
+
}
|
|
6236
|
+
};
|
|
6237
|
+
}
|
|
6080
6238
|
async function runEncodeStage(input) {
|
|
6081
6239
|
const {
|
|
6082
6240
|
job,
|
|
@@ -6390,6 +6548,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
|
|
|
6390
6548
|
options.cfg
|
|
6391
6549
|
);
|
|
6392
6550
|
pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
|
|
6551
|
+
if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
|
|
6393
6552
|
} finally {
|
|
6394
6553
|
await mergeWorkerFrames(attemptWorkDir, tasks, options.framesDir);
|
|
6395
6554
|
}
|
|
@@ -7468,15 +7627,9 @@ import { join as join19 } from "node:path";
|
|
|
7468
7627
|
import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
|
|
7469
7628
|
|
|
7470
7629
|
// src/services/distributed/mp4Stitch.ts
|
|
7471
|
-
import {
|
|
7472
|
-
createReadStream as createReadStream4,
|
|
7473
|
-
createWriteStream,
|
|
7474
|
-
mkdirSync as mkdirSync8,
|
|
7475
|
-
mkdtempSync as mkdtempSync3,
|
|
7476
|
-
renameSync as renameSync3,
|
|
7477
|
-
rmSync as rmSync8
|
|
7478
|
-
} from "node:fs";
|
|
7630
|
+
import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
|
|
7479
7631
|
import { open } from "node:fs/promises";
|
|
7632
|
+
import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
|
|
7480
7633
|
import { dirname as dirname9, join as join18 } from "node:path";
|
|
7481
7634
|
import { finished, pipeline } from "node:stream/promises";
|
|
7482
7635
|
function boxAt(src, off, at, limit) {
|
|
@@ -7849,7 +8002,7 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
|
|
|
7849
8002
|
for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
|
|
7850
8003
|
out.end();
|
|
7851
8004
|
await finished(out);
|
|
7852
|
-
|
|
8005
|
+
renameWithRetry5(join18(workDir, "out.mp4"), outputPath);
|
|
7853
8006
|
} finally {
|
|
7854
8007
|
out.destroy();
|
|
7855
8008
|
rmSync8(workDir, { recursive: true, force: true });
|
|
@@ -8579,6 +8732,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
|
|
|
8579
8732
|
const needsAlpha = outputNeedsAlpha(config.format);
|
|
8580
8733
|
const remoteFiles = Object.entries(options.remoteFiles ?? {});
|
|
8581
8734
|
const remoteLocalPath = (rel) => join21(projectDir, ...rel.split("/"));
|
|
8735
|
+
for (const [, file] of remoteFiles) {
|
|
8736
|
+
declareContentAddressedUrl(file.url, file.sha256);
|
|
8737
|
+
if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
|
|
8738
|
+
}
|
|
8582
8739
|
const urlByAbsentLocalPath = new Map(
|
|
8583
8740
|
remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
|
|
8584
8741
|
);
|
|
@@ -8700,7 +8857,8 @@ function mixPlanAudio(c, compiledDir) {
|
|
|
8700
8857
|
audios: c.composition.audios,
|
|
8701
8858
|
masterFxChain: c.composition.masterFxChain,
|
|
8702
8859
|
abortSignal: c.abortSignal,
|
|
8703
|
-
assertNotAborted: c.assertNotAborted
|
|
8860
|
+
assertNotAborted: c.assertNotAborted,
|
|
8861
|
+
log: c.log
|
|
8704
8862
|
});
|
|
8705
8863
|
}
|
|
8706
8864
|
async function extractPlanVideos(c, deferRangeExtraction) {
|
|
@@ -8719,6 +8877,7 @@ async function extractPlanVideos(c, deferRangeExtraction) {
|
|
|
8719
8877
|
if (extractResult.failureToEnforce) throw extractResult.failureToEnforce;
|
|
8720
8878
|
if (extractResult.extractionResult) {
|
|
8721
8879
|
assertVideoExtractionSucceeded(extractResult.extractionResult);
|
|
8880
|
+
c.log.info("[plan] extract phases", { ...extractResult.extractionResult.phaseBreakdown });
|
|
8722
8881
|
}
|
|
8723
8882
|
return extractResult;
|
|
8724
8883
|
}
|
|
@@ -8764,12 +8923,12 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8764
8923
|
const videoSourcesDir = join21(planDir, "video-sources");
|
|
8765
8924
|
rmSync9(videoSourcesDir, { recursive: true, force: true });
|
|
8766
8925
|
if (existsSync15(stagedVideoFrames)) {
|
|
8767
|
-
|
|
8926
|
+
renameWithRetry6(stagedVideoFrames, videoFramesDst);
|
|
8768
8927
|
} else {
|
|
8769
8928
|
mkdirSync10(videoFramesDst, { recursive: true });
|
|
8770
8929
|
}
|
|
8771
8930
|
if (existsSync15(finalCompiledDir)) rmSync9(finalCompiledDir, { recursive: true, force: true });
|
|
8772
|
-
|
|
8931
|
+
renameWithRetry6(compiledDir, finalCompiledDir);
|
|
8773
8932
|
const shipVideoSource = createVideoSourceShipper({
|
|
8774
8933
|
planDir,
|
|
8775
8934
|
projectDir,
|
|
@@ -8872,7 +9031,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
8872
9031
|
}
|
|
8873
9032
|
const planAudioPath = join21(planDir, PLAN_AUDIO_RELATIVE_PATH);
|
|
8874
9033
|
if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
|
|
8875
|
-
|
|
9034
|
+
renameWithRetry6(audioResult.audioOutputPath, planAudioPath);
|
|
8876
9035
|
}
|
|
8877
9036
|
if (chunksCarryAudio && existsSync15(planAudioPath)) {
|
|
8878
9037
|
if (options.sliceChunkAudio) {
|
|
@@ -8977,7 +9136,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
|
|
|
8977
9136
|
});
|
|
8978
9137
|
try {
|
|
8979
9138
|
let stageStarted = Date.now();
|
|
8980
|
-
await
|
|
9139
|
+
await appendVideoAudioFromProbe(c);
|
|
8981
9140
|
logPlanStage(c.log, "extract", stageStarted);
|
|
8982
9141
|
stageStarted = Date.now();
|
|
8983
9142
|
const mixed = await mixPlanAudio(c, c.compiledDir);
|
|
@@ -8986,7 +9145,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
|
|
|
8986
9145
|
applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
|
|
8987
9146
|
}
|
|
8988
9147
|
if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
|
|
8989
|
-
|
|
9148
|
+
renameWithRetry6(mixed.audioOutputPath, outputPath);
|
|
8990
9149
|
return { hasAudio: true };
|
|
8991
9150
|
} finally {
|
|
8992
9151
|
rmSync9(c.workDir, { recursive: true, force: true });
|
|
@@ -9018,7 +9177,6 @@ import {
|
|
|
9018
9177
|
mkdtempSync as mkdtempSync5,
|
|
9019
9178
|
readFileSync as readFileSync10,
|
|
9020
9179
|
readdirSync as readdirSync7,
|
|
9021
|
-
renameSync as renameSync6,
|
|
9022
9180
|
rmSync as rmSync11,
|
|
9023
9181
|
statSync as statSync6,
|
|
9024
9182
|
writeFileSync as writeFileSync8
|
|
@@ -9026,7 +9184,7 @@ import {
|
|
|
9026
9184
|
import { tmpdir } from "node:os";
|
|
9027
9185
|
import { dirname as dirname11, isAbsolute as isAbsolute4, join as join24, relative as relative6, resolve as resolve8, sep as sep3 } from "node:path";
|
|
9028
9186
|
import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
|
|
9029
|
-
import { createFrameLookupTable as createFrameLookupTable4 } from "@hyperframes/engine";
|
|
9187
|
+
import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
|
|
9030
9188
|
|
|
9031
9189
|
// src/services/distributed/planV2Publisher.ts
|
|
9032
9190
|
import {
|
|
@@ -9035,11 +9193,11 @@ import {
|
|
|
9035
9193
|
linkSync as linkSync2,
|
|
9036
9194
|
mkdirSync as mkdirSync11,
|
|
9037
9195
|
mkdtempSync as mkdtempSync4,
|
|
9038
|
-
renameSync as renameSync5,
|
|
9039
9196
|
rmSync as rmSync10,
|
|
9040
9197
|
statSync as statSync5,
|
|
9041
9198
|
writeFileSync as writeFileSync7
|
|
9042
9199
|
} from "node:fs";
|
|
9200
|
+
import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
|
|
9043
9201
|
import { dirname as dirname10, join as join23 } from "node:path";
|
|
9044
9202
|
|
|
9045
9203
|
// src/services/distributed/planV2Errors.ts
|
|
@@ -9128,7 +9286,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9128
9286
|
if (!canFallbackToCopy(error)) throw error;
|
|
9129
9287
|
copyFileSync6(blob.sourcePath, temporaryPath);
|
|
9130
9288
|
}
|
|
9131
|
-
|
|
9289
|
+
renameWithRetry7(temporaryPath, destinationPath);
|
|
9132
9290
|
} finally {
|
|
9133
9291
|
rmSync10(stagingDir, { recursive: true, force: true });
|
|
9134
9292
|
}
|
|
@@ -9142,7 +9300,7 @@ var LocalPlanV2ArtifactPublisher = class {
|
|
|
9142
9300
|
}
|
|
9143
9301
|
}
|
|
9144
9302
|
writeFileSync7(join23(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
|
|
9145
|
-
|
|
9303
|
+
renameWithRetry7(this.temporaryDir, this.destinationDir);
|
|
9146
9304
|
this.#committed = true;
|
|
9147
9305
|
}
|
|
9148
9306
|
async abort() {
|
|
@@ -9409,7 +9567,7 @@ function writeBlob(sourcePath, destinationPath) {
|
|
|
9409
9567
|
const temporaryPath = join24(temporaryDir, "blob");
|
|
9410
9568
|
try {
|
|
9411
9569
|
copyFileSync7(sourcePath, temporaryPath);
|
|
9412
|
-
|
|
9570
|
+
renameWithRetry8(temporaryPath, destinationPath);
|
|
9413
9571
|
} finally {
|
|
9414
9572
|
rmSync11(temporaryDir, { recursive: true, force: true });
|
|
9415
9573
|
}
|
|
@@ -9541,7 +9699,7 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
|
|
|
9541
9699
|
canonicalJsonStringify(publication.manifest),
|
|
9542
9700
|
"utf-8"
|
|
9543
9701
|
);
|
|
9544
|
-
|
|
9702
|
+
renameWithRetry8(tempDir, planV2Dir);
|
|
9545
9703
|
return resultFromManifest(planV2Dir, publication.manifest);
|
|
9546
9704
|
} catch (error) {
|
|
9547
9705
|
rmSync11(tempDir, { recursive: true, force: true });
|
|
@@ -9826,7 +9984,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
|
|
|
9826
9984
|
}),
|
|
9827
9985
|
"utf-8"
|
|
9828
9986
|
);
|
|
9829
|
-
|
|
9987
|
+
renameWithRetry8(tempDir, destinationDir);
|
|
9830
9988
|
} catch (error) {
|
|
9831
9989
|
rmSync11(tempDir, { recursive: true, force: true });
|
|
9832
9990
|
throw error;
|
|
@@ -9904,7 +10062,6 @@ import {
|
|
|
9904
10062
|
mkdtempSync as mkdtempSync6,
|
|
9905
10063
|
readFileSync as readFileSync11,
|
|
9906
10064
|
readdirSync as readdirSync8,
|
|
9907
|
-
renameSync as renameSync7,
|
|
9908
10065
|
rmSync as rmSync12,
|
|
9909
10066
|
statSync as statSync7,
|
|
9910
10067
|
writeFileSync as writeFileSync9
|
|
@@ -9918,7 +10075,8 @@ import {
|
|
|
9918
10075
|
muxVideoWithAudio as muxVideoWithAudio2,
|
|
9919
10076
|
resolveConfig as resolveConfig4,
|
|
9920
10077
|
runFfmpeg as runFfmpeg7,
|
|
9921
|
-
appendSoftwareEncoderThreadsArg
|
|
10078
|
+
appendSoftwareEncoderThreadsArg,
|
|
10079
|
+
renameWithRetry as renameWithRetry9
|
|
9922
10080
|
} from "@hyperframes/engine";
|
|
9923
10081
|
import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
|
|
9924
10082
|
function cfrRateControlArgs(locked) {
|
|
@@ -9975,7 +10133,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
|
|
|
9975
10133
|
const finalPath = join25(workDir, `output.${format}`);
|
|
9976
10134
|
const audio = audioPath !== null && existsSync18(audioPath) ? audioPath : null;
|
|
9977
10135
|
await writeJoined(ctx, videoSources, audio, finalPath);
|
|
9978
|
-
|
|
10136
|
+
renameWithRetry9(finalPath, outputPath);
|
|
9979
10137
|
log.info("[assemble] stage timings", {
|
|
9980
10138
|
...stageMs,
|
|
9981
10139
|
chunks: chunkPaths.length,
|
|
@@ -10045,7 +10203,7 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
|
|
|
10045
10203
|
"-r",
|
|
10046
10204
|
fpsArg
|
|
10047
10205
|
];
|
|
10048
|
-
appendSoftwareEncoderThreadsArg(args);
|
|
10206
|
+
appendSoftwareEncoderThreadsArg(args, "libx264");
|
|
10049
10207
|
return args;
|
|
10050
10208
|
}
|
|
10051
10209
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
@@ -10227,6 +10385,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
|
|
|
10227
10385
|
// src/services/distributed/renderChunk.ts
|
|
10228
10386
|
import { randomBytes } from "node:crypto";
|
|
10229
10387
|
import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync13, writeFileSync as writeFileSync10 } from "node:fs";
|
|
10388
|
+
import { cpus } from "node:os";
|
|
10230
10389
|
import { join as join26 } from "node:path";
|
|
10231
10390
|
import {
|
|
10232
10391
|
assertSwiftShader,
|
|
@@ -10531,6 +10690,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10531
10690
|
let forceScreenshotForChunk = encoder.forceScreenshot;
|
|
10532
10691
|
let shownBoxes;
|
|
10533
10692
|
let extraction;
|
|
10693
|
+
const overlap = { current: null };
|
|
10534
10694
|
try {
|
|
10535
10695
|
if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
|
|
10536
10696
|
const bootStarted = Date.now();
|
|
@@ -10618,6 +10778,59 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10618
10778
|
videoFrameLookup,
|
|
10619
10779
|
frameCacheOptions(cfg, chunkWorkerCount)
|
|
10620
10780
|
);
|
|
10781
|
+
const isPngSequence = plan2.dimensions.format === "png-sequence";
|
|
10782
|
+
outputKind = isPngSequence ? "frame-dir" : "file";
|
|
10783
|
+
const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
|
|
10784
|
+
const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
|
|
10785
|
+
const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
|
|
10786
|
+
const effectiveQuality = encoder.crf ?? preset.quality;
|
|
10787
|
+
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
10788
|
+
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
10789
|
+
if (isPngSequence) {
|
|
10790
|
+
if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
|
|
10791
|
+
} else {
|
|
10792
|
+
const outDir = join26(outputChunkPath, "..");
|
|
10793
|
+
if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
|
|
10794
|
+
}
|
|
10795
|
+
const encodeInput = {
|
|
10796
|
+
job,
|
|
10797
|
+
log,
|
|
10798
|
+
outputPath: videoOnlyPath,
|
|
10799
|
+
framesDir,
|
|
10800
|
+
videoOnlyPath,
|
|
10801
|
+
width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
|
|
10802
|
+
height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
|
|
10803
|
+
needsAlpha: plan2.dimensions.format !== "mp4",
|
|
10804
|
+
captureImageFormat: captureOptions.format ?? "jpeg",
|
|
10805
|
+
// The encoder writes video only; audio joins at assemble time or in the
|
|
10806
|
+
// chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
|
|
10807
|
+
// sidecar AND the mp4 audio mux.
|
|
10808
|
+
hasAudio: false,
|
|
10809
|
+
isPngSequence,
|
|
10810
|
+
// `DistributedFormat` has no "gif" member — distributed chunks are
|
|
10811
|
+
// always video segments (gif renders in-process only).
|
|
10812
|
+
isGif: false,
|
|
10813
|
+
preset,
|
|
10814
|
+
effectiveQuality,
|
|
10815
|
+
effectiveBitrate,
|
|
10816
|
+
engineConfig: {
|
|
10817
|
+
ffmpegEncodeTimeout: cfg.ffmpegEncodeTimeout,
|
|
10818
|
+
vp9CpuUsed: resolveLockedVp9CpuUsed(encoder) ?? cfg.vp9CpuUsed
|
|
10819
|
+
},
|
|
10820
|
+
// Distributed chunks emit a single ffmpeg call per chunk; the
|
|
10821
|
+
// in-process per-chunk-within-chunk path would re-split our
|
|
10822
|
+
// already-chunked work.
|
|
10823
|
+
enableChunkedEncode: false,
|
|
10824
|
+
chunkedEncodeSize: framesInChunk,
|
|
10825
|
+
abortSignal: void 0,
|
|
10826
|
+
assertNotAborted: () => {
|
|
10827
|
+
},
|
|
10828
|
+
// GOP === framesInChunk + force-keyframe at frame 0 → the chunk's
|
|
10829
|
+
// first frame is an IDR keyframe and concat-copy at assemble time
|
|
10830
|
+
// round-trips losslessly.
|
|
10831
|
+
lockGopForChunkConcat: !isPngSequence,
|
|
10832
|
+
gopSize: framesInChunk
|
|
10833
|
+
};
|
|
10621
10834
|
const captureStarted = Date.now();
|
|
10622
10835
|
captureMode = await runCaptureWithScreenshotFallback({
|
|
10623
10836
|
forceScreenshot: forceScreenshotForChunk,
|
|
@@ -10650,6 +10863,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10650
10863
|
}
|
|
10651
10864
|
const captureSession = session;
|
|
10652
10865
|
session = null;
|
|
10866
|
+
overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
|
|
10653
10867
|
await runCaptureStage({
|
|
10654
10868
|
fileServer,
|
|
10655
10869
|
workDir,
|
|
@@ -10669,7 +10883,12 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10669
10883
|
abortSignal: void 0,
|
|
10670
10884
|
assertNotAborted: () => {
|
|
10671
10885
|
},
|
|
10672
|
-
frameRange: range
|
|
10886
|
+
frameRange: range,
|
|
10887
|
+
onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame)
|
|
10888
|
+
}).catch(async (captureError) => {
|
|
10889
|
+
await overlap.current?.abort();
|
|
10890
|
+
overlap.current = null;
|
|
10891
|
+
throw captureError;
|
|
10673
10892
|
});
|
|
10674
10893
|
return observedChunkCaptureMode(capturePerfs);
|
|
10675
10894
|
},
|
|
@@ -10701,60 +10920,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10701
10920
|
log
|
|
10702
10921
|
);
|
|
10703
10922
|
framesEncoded = framesInChunk;
|
|
10704
|
-
const isPngSequence = plan2.dimensions.format === "png-sequence";
|
|
10705
|
-
outputKind = isPngSequence ? "frame-dir" : "file";
|
|
10706
|
-
const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
|
|
10707
|
-
const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
|
|
10708
|
-
const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
|
|
10709
|
-
const effectiveQuality = encoder.crf ?? preset.quality;
|
|
10710
|
-
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
10711
|
-
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
10712
|
-
if (isPngSequence) {
|
|
10713
|
-
if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
|
|
10714
|
-
} else {
|
|
10715
|
-
const outDir = join26(outputChunkPath, "..");
|
|
10716
|
-
if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
|
|
10717
|
-
}
|
|
10718
10923
|
const encodeStarted = Date.now();
|
|
10719
|
-
|
|
10720
|
-
|
|
10721
|
-
|
|
10722
|
-
outputPath: videoOnlyPath,
|
|
10723
|
-
framesDir,
|
|
10724
|
-
videoOnlyPath,
|
|
10725
|
-
width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
|
|
10726
|
-
height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
|
|
10727
|
-
needsAlpha: plan2.dimensions.format !== "mp4",
|
|
10728
|
-
captureImageFormat: captureOptions.format ?? "jpeg",
|
|
10729
|
-
// The encoder writes video only; audio joins at assemble time or in the
|
|
10730
|
-
// chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
|
|
10731
|
-
// sidecar AND the mp4 audio mux.
|
|
10732
|
-
hasAudio: false,
|
|
10733
|
-
isPngSequence,
|
|
10734
|
-
// `DistributedFormat` has no "gif" member — distributed chunks are
|
|
10735
|
-
// always video segments (gif renders in-process only).
|
|
10736
|
-
isGif: false,
|
|
10737
|
-
preset,
|
|
10738
|
-
effectiveQuality,
|
|
10739
|
-
effectiveBitrate,
|
|
10740
|
-
engineConfig: {
|
|
10741
|
-
ffmpegEncodeTimeout: cfg.ffmpegEncodeTimeout,
|
|
10742
|
-
vp9CpuUsed: resolveLockedVp9CpuUsed(encoder) ?? cfg.vp9CpuUsed
|
|
10743
|
-
},
|
|
10744
|
-
// Distributed chunks emit a single ffmpeg call per chunk; the
|
|
10745
|
-
// in-process per-chunk-within-chunk path would re-split our
|
|
10746
|
-
// already-chunked work.
|
|
10747
|
-
enableChunkedEncode: false,
|
|
10748
|
-
chunkedEncodeSize: framesInChunk,
|
|
10749
|
-
abortSignal: void 0,
|
|
10750
|
-
assertNotAborted: () => {
|
|
10751
|
-
},
|
|
10752
|
-
// GOP === framesInChunk + force-keyframe at frame 0 → the chunk's
|
|
10753
|
-
// first frame is an IDR keyframe and concat-copy at assemble time
|
|
10754
|
-
// round-trips losslessly.
|
|
10755
|
-
lockGopForChunkConcat: !isPngSequence,
|
|
10756
|
-
gopSize: framesInChunk
|
|
10757
|
-
});
|
|
10924
|
+
const overlapped = overlap.current;
|
|
10925
|
+
overlap.current = null;
|
|
10926
|
+
await (overlapped ? overlapped.finish() : runEncodeStage(encodeInput));
|
|
10758
10927
|
if (encoder.chunksCarryAudio) {
|
|
10759
10928
|
const audioScratchPath = `${outputChunkPath}.audio.m4a`;
|
|
10760
10929
|
const audioChunks = options?.frameRange ? chunksWithSubRange(chunks, chunkIndex, range) : { chunks, index: chunkIndex };
|
|
@@ -10772,6 +10941,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
10772
10941
|
}
|
|
10773
10942
|
encodeStageMs = Date.now() - encodeStarted;
|
|
10774
10943
|
} finally {
|
|
10944
|
+
await overlap.current?.abort().catch(() => {
|
|
10945
|
+
});
|
|
10775
10946
|
await extraction?.catch(() => {
|
|
10776
10947
|
});
|
|
10777
10948
|
if (session) {
|
|
@@ -11201,6 +11372,24 @@ async function buildAudioSidecar(inputPath, outputPath, signal) {
|
|
|
11201
11372
|
}
|
|
11202
11373
|
var isMissing = (result) => result.error?.code === "ENOENT";
|
|
11203
11374
|
var ffmpegIsMissing = async (signal) => isMissing(await runFfmpeg8(["-version"], { signal }));
|
|
11375
|
+
|
|
11376
|
+
// src/services/distributed/probeFacts.ts
|
|
11377
|
+
import { writeFileSync as writeFileSync11 } from "node:fs";
|
|
11378
|
+
import { extname as extname6 } from "node:path";
|
|
11379
|
+
import { buildProbeFactsText } from "@hyperframes/engine";
|
|
11380
|
+
var PROBE_FACTS_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v", ".webm", ".mkv"]);
|
|
11381
|
+
function wantsProbeFacts(path) {
|
|
11382
|
+
return PROBE_FACTS_EXTENSIONS.has(extname6(path).toLowerCase());
|
|
11383
|
+
}
|
|
11384
|
+
async function buildProbeFacts(inputPath, outputPath, signal) {
|
|
11385
|
+
try {
|
|
11386
|
+
writeFileSync11(outputPath, await buildProbeFactsText(inputPath, signal));
|
|
11387
|
+
return "built";
|
|
11388
|
+
} catch (error) {
|
|
11389
|
+
if (signal?.aborted) throw error;
|
|
11390
|
+
return /ffprobe not found/i.test(String(error.message)) ? "unavailable" : "none";
|
|
11391
|
+
}
|
|
11392
|
+
}
|
|
11204
11393
|
export {
|
|
11205
11394
|
AUDIO_SIDECAR_MIN_BYTES,
|
|
11206
11395
|
CHUNK_EXCEEDS_WORKER_TIMEOUT,
|
|
@@ -11248,6 +11437,7 @@ export {
|
|
|
11248
11437
|
buildAudioSidecar,
|
|
11249
11438
|
buildChunkSlices,
|
|
11250
11439
|
buildPlanAudio,
|
|
11440
|
+
buildProbeFacts,
|
|
11251
11441
|
buildStitchedHeader,
|
|
11252
11442
|
chunksAreStitchedMp4,
|
|
11253
11443
|
createPlanV2FromExecutionPlan,
|
|
@@ -11282,5 +11472,6 @@ export {
|
|
|
11282
11472
|
validateDistributedRenderConfig,
|
|
11283
11473
|
validatePlanV2MaterializedTarget,
|
|
11284
11474
|
validateVariablesPayload,
|
|
11285
|
-
wantsAudioSidecar
|
|
11475
|
+
wantsAudioSidecar,
|
|
11476
|
+
wantsProbeFacts
|
|
11286
11477
|
};
|