@hyperframes/producer 0.8.183 → 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.js +40 -26
- 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 +168 -54
- package/dist/public-server.js +156 -53
- package/dist/services/distributed/renderChunk.d.ts +9 -2
- package/dist/services/render/stages/captureStage.d.ts +2 -1
- package/dist/services/render/stages/encodeStage.d.ts +26 -2
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { type BrowserDiagnosticSummary, type RenderCaptureObservability, type Re
|
|
|
12
12
|
export { localizeRemoteMediaSources, localizeRemoteImageSources, localizeRemoteFontFaces, } from "./services/htmlCompiler.js";
|
|
13
13
|
export { createCaptureSession, initializeSession, closeCaptureSession, captureFrame, captureFrameToBuffer, getCompositionDuration, getCapturePerfSummary, isTransientBrowserError, prepareCaptureSessionForReuse, type CaptureOptions, type CaptureSession, type CaptureResult, type CapturePerfSummary, type BeforeCaptureHook, } from "./services/frameCapture.js";
|
|
14
14
|
export { createFileServer, type FileServerOptions, type FileServerHandle, } from "./services/fileServer.js";
|
|
15
|
+
export { prepareAnimatedGifInputs } from "./services/animatedGifPrep.js";
|
|
15
16
|
export { createVideoFrameInjector } from "@hyperframes/engine";
|
|
16
17
|
export { resolveConfig, DEFAULT_CONFIG, type ProducerConfig } from "./config.js";
|
|
17
18
|
export { type ProducerLogger, type LogLevel, createConsoleLogger, defaultLogger, } from "./logger.js";
|
package/dist/index.js
CHANGED
|
@@ -8982,6 +8982,7 @@ async function runCaptureStage(input) {
|
|
|
8982
8982
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
8983
8983
|
return;
|
|
8984
8984
|
}
|
|
8985
|
+
if (progress.landedFrame) input.onFrameLanded?.(progress.landedFrame);
|
|
8985
8986
|
job.framesRendered = progress.capturedFrames;
|
|
8986
8987
|
checkDiskProjection(progress.capturedFrames);
|
|
8987
8988
|
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
@@ -10151,6 +10152,7 @@ import {
|
|
|
10151
10152
|
statSync as statSync5,
|
|
10152
10153
|
writeFileSync as writeFileSync4
|
|
10153
10154
|
} from "node:fs";
|
|
10155
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
10154
10156
|
import { dirname as dirname8, join as join24 } from "node:path";
|
|
10155
10157
|
import {
|
|
10156
10158
|
encodeFramesChunkedConcat,
|
|
@@ -10158,7 +10160,8 @@ import {
|
|
|
10158
10160
|
formatFfmpegError,
|
|
10159
10161
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
10160
10162
|
resolveConfig,
|
|
10161
|
-
runFfmpeg as runFfmpeg3
|
|
10163
|
+
runFfmpeg as runFfmpeg3,
|
|
10164
|
+
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
10162
10165
|
} from "@hyperframes/engine";
|
|
10163
10166
|
import {
|
|
10164
10167
|
clearGifFramesBeforeNext,
|
|
@@ -10295,6 +10298,124 @@ function startEncodeProgress(job, stage, onProgress) {
|
|
|
10295
10298
|
});
|
|
10296
10299
|
return (frames = total) => reportEncodeProgress(job, frames, total, onProgress, 75);
|
|
10297
10300
|
}
|
|
10301
|
+
function resolveEngineConfig(input) {
|
|
10302
|
+
return input.engineConfig ?? input.job.config.producerConfig ?? resolveConfig();
|
|
10303
|
+
}
|
|
10304
|
+
function videoEncoderOptions(input, engineCfg) {
|
|
10305
|
+
const { job, preset } = input;
|
|
10306
|
+
return {
|
|
10307
|
+
fps: job.config.fps,
|
|
10308
|
+
width: input.width,
|
|
10309
|
+
height: input.height,
|
|
10310
|
+
codec: preset.codec,
|
|
10311
|
+
preset: preset.preset,
|
|
10312
|
+
quality: input.effectiveQuality,
|
|
10313
|
+
bitrate: input.effectiveBitrate,
|
|
10314
|
+
pixelFormat: preset.pixelFormat,
|
|
10315
|
+
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
10316
|
+
useGpu: job.config.useGpu,
|
|
10317
|
+
hdr: preset.hdr,
|
|
10318
|
+
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
10319
|
+
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
10320
|
+
// renders leave both undefined → preserves the existing open-GOP output.
|
|
10321
|
+
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
10322
|
+
gopSize: input.gopSize
|
|
10323
|
+
};
|
|
10324
|
+
}
|
|
10325
|
+
function canEncodeWhileCapturing(input, workerCount, cpuCount) {
|
|
10326
|
+
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" && input.lockGopForChunkConcat !== true;
|
|
10327
|
+
}
|
|
10328
|
+
async function readLandedFrame(workerPath, mergedPath) {
|
|
10329
|
+
if (workerPath !== void 0) {
|
|
10330
|
+
try {
|
|
10331
|
+
return await readFile2(workerPath);
|
|
10332
|
+
} catch (error) {
|
|
10333
|
+
if (error.code !== "ENOENT") throw error;
|
|
10334
|
+
}
|
|
10335
|
+
}
|
|
10336
|
+
return readFile2(mergedPath);
|
|
10337
|
+
}
|
|
10338
|
+
function encodeWhileCapturing(input, totalFrames) {
|
|
10339
|
+
const engineCfg = resolveEngineConfig(input);
|
|
10340
|
+
const stop = new AbortController();
|
|
10341
|
+
const signal = input.abortSignal ? AbortSignal.any([input.abortSignal, stop.signal]) : stop.signal;
|
|
10342
|
+
const landed = /* @__PURE__ */ new Map();
|
|
10343
|
+
let captureDone = false;
|
|
10344
|
+
let framesFed = 0;
|
|
10345
|
+
let wake = null;
|
|
10346
|
+
const nudge = () => {
|
|
10347
|
+
wake?.();
|
|
10348
|
+
wake = null;
|
|
10349
|
+
};
|
|
10350
|
+
signal.addEventListener("abort", nudge, { once: true });
|
|
10351
|
+
let reportFed = null;
|
|
10352
|
+
const encoder = spawnStreamingEncoder4(
|
|
10353
|
+
input.videoOnlyPath,
|
|
10354
|
+
{ ...videoEncoderOptions(input, engineCfg), imageFormat: "jpeg" },
|
|
10355
|
+
signal,
|
|
10356
|
+
engineCfg
|
|
10357
|
+
);
|
|
10358
|
+
const fed = (async () => {
|
|
10359
|
+
const stream = await encoder;
|
|
10360
|
+
for (let next = 0; next < totalFrames; next++) {
|
|
10361
|
+
while (!landed.has(next) && !captureDone && !signal.aborted) {
|
|
10362
|
+
await new Promise((resolve12) => wake = resolve12);
|
|
10363
|
+
}
|
|
10364
|
+
if (signal.aborted) return;
|
|
10365
|
+
const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
10366
|
+
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
10367
|
+
landed.delete(next);
|
|
10368
|
+
if (!await stream.writeFrame(frame)) {
|
|
10369
|
+
throw new Error(stream.getExitError() ?? "encoder exited before the last frame");
|
|
10370
|
+
}
|
|
10371
|
+
framesFed++;
|
|
10372
|
+
reportFed?.(framesFed);
|
|
10373
|
+
}
|
|
10374
|
+
})().then(
|
|
10375
|
+
() => void 0,
|
|
10376
|
+
(error) => error ?? new Error("encode failed")
|
|
10377
|
+
);
|
|
10378
|
+
const closeEncoder = async () => (await encoder.catch(() => null))?.close();
|
|
10379
|
+
return {
|
|
10380
|
+
onFrameLanded: (frame) => {
|
|
10381
|
+
landed.set(frame.index, frame.path);
|
|
10382
|
+
nudge();
|
|
10383
|
+
},
|
|
10384
|
+
finish: async () => {
|
|
10385
|
+
const start = Date.now();
|
|
10386
|
+
captureDone = true;
|
|
10387
|
+
input.log.info("[Render] Encoded frames while capturing", {
|
|
10388
|
+
framesFed,
|
|
10389
|
+
totalFrames
|
|
10390
|
+
});
|
|
10391
|
+
const encoded = startEncodeProgress(input.job, "Encoding video", input.onProgress);
|
|
10392
|
+
reportFed = encoded;
|
|
10393
|
+
nudge();
|
|
10394
|
+
const failure = await fed;
|
|
10395
|
+
const result = await closeEncoder();
|
|
10396
|
+
input.assertNotAborted();
|
|
10397
|
+
if (failure === void 0 && result?.success) {
|
|
10398
|
+
encoded();
|
|
10399
|
+
return { encodeMs: Date.now() - start };
|
|
10400
|
+
}
|
|
10401
|
+
input.log.warn(
|
|
10402
|
+
"[Render] Encoding while capturing failed; encoding the frames after capture",
|
|
10403
|
+
{
|
|
10404
|
+
error: String(failure ?? result?.error)
|
|
10405
|
+
}
|
|
10406
|
+
);
|
|
10407
|
+
return runEncodeStage(input);
|
|
10408
|
+
},
|
|
10409
|
+
abort: async () => {
|
|
10410
|
+
stop.abort();
|
|
10411
|
+
await fed;
|
|
10412
|
+
await closeEncoder();
|
|
10413
|
+
},
|
|
10414
|
+
get framesFed() {
|
|
10415
|
+
return framesFed;
|
|
10416
|
+
}
|
|
10417
|
+
};
|
|
10418
|
+
}
|
|
10298
10419
|
async function runEncodeStage(input) {
|
|
10299
10420
|
const {
|
|
10300
10421
|
job,
|
|
@@ -10302,16 +10423,11 @@ async function runEncodeStage(input) {
|
|
|
10302
10423
|
outputPath,
|
|
10303
10424
|
framesDir,
|
|
10304
10425
|
videoOnlyPath,
|
|
10305
|
-
width,
|
|
10306
|
-
height,
|
|
10307
10426
|
needsAlpha,
|
|
10308
10427
|
hasAudio,
|
|
10309
10428
|
audioOutputPath,
|
|
10310
10429
|
isPngSequence,
|
|
10311
10430
|
isGif,
|
|
10312
|
-
preset,
|
|
10313
|
-
effectiveQuality,
|
|
10314
|
-
effectiveBitrate,
|
|
10315
10431
|
enableChunkedEncode,
|
|
10316
10432
|
chunkedEncodeSize,
|
|
10317
10433
|
abortSignal,
|
|
@@ -10341,7 +10457,7 @@ async function runEncodeStage(input) {
|
|
|
10341
10457
|
encoded();
|
|
10342
10458
|
return { encodeMs: Date.now() - stage5Start };
|
|
10343
10459
|
}
|
|
10344
|
-
const engineCfg = input
|
|
10460
|
+
const engineCfg = resolveEngineConfig(input);
|
|
10345
10461
|
if (isGif) {
|
|
10346
10462
|
const encoded = startEncodeProgress(job, "Encoding GIF", onProgress);
|
|
10347
10463
|
if (hasAudio) {
|
|
@@ -10371,24 +10487,7 @@ async function runEncodeStage(input) {
|
|
|
10371
10487
|
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
10372
10488
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
10373
10489
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
10374
|
-
const encoderOpts =
|
|
10375
|
-
fps: job.config.fps,
|
|
10376
|
-
width,
|
|
10377
|
-
height,
|
|
10378
|
-
codec: preset.codec,
|
|
10379
|
-
preset: preset.preset,
|
|
10380
|
-
quality: effectiveQuality,
|
|
10381
|
-
bitrate: effectiveBitrate,
|
|
10382
|
-
pixelFormat: preset.pixelFormat,
|
|
10383
|
-
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
10384
|
-
useGpu: job.config.useGpu,
|
|
10385
|
-
hdr: preset.hdr,
|
|
10386
|
-
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
10387
|
-
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
10388
|
-
// renders leave both undefined → preserves the existing open-GOP output.
|
|
10389
|
-
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
10390
|
-
gopSize: input.gopSize
|
|
10391
|
-
};
|
|
10490
|
+
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
10392
10491
|
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
10393
10492
|
framesDir,
|
|
10394
10493
|
framePattern,
|
|
@@ -13313,6 +13412,34 @@ async function executeRenderPipeline(input) {
|
|
|
13313
13412
|
throw new Error(`Disk capture requires sdr_disk plan; got ${capturePlan.kind}`);
|
|
13314
13413
|
}
|
|
13315
13414
|
resetCaptureAttemptProgress(job);
|
|
13415
|
+
const encodeInput = {
|
|
13416
|
+
job,
|
|
13417
|
+
log,
|
|
13418
|
+
outputPath: stagedOutputPath,
|
|
13419
|
+
framesDir,
|
|
13420
|
+
videoOnlyPath,
|
|
13421
|
+
width,
|
|
13422
|
+
height,
|
|
13423
|
+
needsAlpha,
|
|
13424
|
+
captureImageFormat: captureOptions.format ?? "jpeg",
|
|
13425
|
+
hasAudio,
|
|
13426
|
+
audioOutputPath,
|
|
13427
|
+
isPngSequence,
|
|
13428
|
+
isGif,
|
|
13429
|
+
preset,
|
|
13430
|
+
effectiveQuality,
|
|
13431
|
+
effectiveBitrate,
|
|
13432
|
+
enableChunkedEncode,
|
|
13433
|
+
chunkedEncodeSize,
|
|
13434
|
+
engineConfig: cfg,
|
|
13435
|
+
// Same value the streaming encoder above spreads — the disk and
|
|
13436
|
+
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
13437
|
+
...hlsEncoderGopLock,
|
|
13438
|
+
abortSignal: executionSignal,
|
|
13439
|
+
assertNotAborted,
|
|
13440
|
+
onProgress
|
|
13441
|
+
};
|
|
13442
|
+
const overlap = !cfg.useDrawElement && workerSizing !== void 0 && canEncodeWhileCapturing(encodeInput, capturePlan.workerCount, workerSizing.cpuCount) ? encodeWhileCapturing(encodeInput, totalFrames) : null;
|
|
13316
13443
|
const captureFrameStart = Date.now();
|
|
13317
13444
|
const invokeDiskCapture = (diskPlan) => observeRenderStage(
|
|
13318
13445
|
observability,
|
|
@@ -13334,13 +13461,15 @@ async function executeRenderPipeline(input) {
|
|
|
13334
13461
|
createRenderVideoFrameInjector,
|
|
13335
13462
|
abortSignal: executionSignal,
|
|
13336
13463
|
assertNotAborted,
|
|
13337
|
-
onProgress
|
|
13464
|
+
onProgress,
|
|
13465
|
+
onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
|
|
13338
13466
|
})
|
|
13339
13467
|
);
|
|
13340
13468
|
let captureRes;
|
|
13341
13469
|
try {
|
|
13342
13470
|
captureRes = await invokeDiskCapture(capturePlan);
|
|
13343
13471
|
} catch (err) {
|
|
13472
|
+
await overlap?.abort();
|
|
13344
13473
|
if (!isDrawElementVerificationError(err) && !isDrawElementCaptureError(err) || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
|
|
13345
13474
|
throw err;
|
|
13346
13475
|
}
|
|
@@ -13409,33 +13538,7 @@ async function executeRenderPipeline(input) {
|
|
|
13409
13538
|
isGif,
|
|
13410
13539
|
chunkedEncode: enableChunkedEncode
|
|
13411
13540
|
}),
|
|
13412
|
-
() => runEncodeStage(
|
|
13413
|
-
job,
|
|
13414
|
-
log,
|
|
13415
|
-
outputPath: stagedOutputPath,
|
|
13416
|
-
framesDir,
|
|
13417
|
-
videoOnlyPath,
|
|
13418
|
-
width,
|
|
13419
|
-
height,
|
|
13420
|
-
needsAlpha,
|
|
13421
|
-
captureImageFormat: captureOptions.format ?? "jpeg",
|
|
13422
|
-
hasAudio,
|
|
13423
|
-
audioOutputPath,
|
|
13424
|
-
isPngSequence,
|
|
13425
|
-
isGif,
|
|
13426
|
-
preset,
|
|
13427
|
-
effectiveQuality,
|
|
13428
|
-
effectiveBitrate,
|
|
13429
|
-
enableChunkedEncode,
|
|
13430
|
-
chunkedEncodeSize,
|
|
13431
|
-
engineConfig: cfg,
|
|
13432
|
-
// Same value the streaming encoder above spreads — the disk and
|
|
13433
|
-
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
13434
|
-
...hlsEncoderGopLock,
|
|
13435
|
-
abortSignal: executionSignal,
|
|
13436
|
-
assertNotAborted,
|
|
13437
|
-
onProgress
|
|
13438
|
-
})
|
|
13541
|
+
() => overlap ? overlap.finish() : runEncodeStage(encodeInput)
|
|
13439
13542
|
);
|
|
13440
13543
|
perfStages.encodeMs = encodeRes.encodeMs;
|
|
13441
13544
|
}
|
|
@@ -18794,6 +18897,12 @@ function validatePlanVideosForChunk(value) {
|
|
|
18794
18897
|
);
|
|
18795
18898
|
}
|
|
18796
18899
|
}
|
|
18900
|
+
var MIN_FRAMES_PER_CHUNK_BROWSER = 20;
|
|
18901
|
+
function resolveChunkCaptureWorkers(framesInChunk, machineSized, maxCaptureWorkers) {
|
|
18902
|
+
if (maxCaptureWorkers === void 0) return machineSized;
|
|
18903
|
+
const byFrames = Math.floor(framesInChunk / MIN_FRAMES_PER_CHUNK_BROWSER);
|
|
18904
|
+
return Math.max(machineSized, Math.floor(Math.min(maxCaptureWorkers, byFrames)));
|
|
18905
|
+
}
|
|
18797
18906
|
function resolveChunkFrameRange(slice, chunkIndex, frameRange) {
|
|
18798
18907
|
if (frameRange === void 0) return { startFrame: slice.startFrame, endFrame: slice.endFrame };
|
|
18799
18908
|
const { startFrame, endFrame } = frameRange;
|
|
@@ -19030,7 +19139,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
|
|
|
19030
19139
|
deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
|
|
19031
19140
|
)
|
|
19032
19141
|
});
|
|
19033
|
-
const chunkWorkerCount =
|
|
19142
|
+
const chunkWorkerCount = resolveChunkCaptureWorkers(
|
|
19143
|
+
framesInChunk,
|
|
19144
|
+
calculateOptimalWorkers2(framesInChunk, void 0, cfg),
|
|
19145
|
+
options?.maxCaptureWorkers
|
|
19146
|
+
);
|
|
19034
19147
|
let session = null;
|
|
19035
19148
|
let outputKind;
|
|
19036
19149
|
let framesEncoded = 0;
|
|
@@ -19447,6 +19560,7 @@ export {
|
|
|
19447
19560
|
plan,
|
|
19448
19561
|
planV2,
|
|
19449
19562
|
planV2WithPublisher,
|
|
19563
|
+
prepareAnimatedGifInputs,
|
|
19450
19564
|
prepareCaptureSessionForReuse4 as prepareCaptureSessionForReuse,
|
|
19451
19565
|
prepareHyperframeLintBody,
|
|
19452
19566
|
publishPlanV2FromExecutionPlan,
|
package/dist/public-server.js
CHANGED
|
@@ -8965,6 +8965,7 @@ async function runCaptureStage(input) {
|
|
|
8965
8965
|
if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
|
|
8966
8966
|
return;
|
|
8967
8967
|
}
|
|
8968
|
+
if (progress.landedFrame) input.onFrameLanded?.(progress.landedFrame);
|
|
8968
8969
|
job.framesRendered = progress.capturedFrames;
|
|
8969
8970
|
checkDiskProjection(progress.capturedFrames);
|
|
8970
8971
|
const frameProgress = progress.capturedFrames / progress.totalFrames;
|
|
@@ -10134,6 +10135,7 @@ import {
|
|
|
10134
10135
|
statSync as statSync5,
|
|
10135
10136
|
writeFileSync as writeFileSync4
|
|
10136
10137
|
} from "node:fs";
|
|
10138
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
10137
10139
|
import { dirname as dirname8, join as join24 } from "node:path";
|
|
10138
10140
|
import {
|
|
10139
10141
|
encodeFramesChunkedConcat,
|
|
@@ -10141,7 +10143,8 @@ import {
|
|
|
10141
10143
|
formatFfmpegError,
|
|
10142
10144
|
MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
|
|
10143
10145
|
resolveConfig,
|
|
10144
|
-
runFfmpeg as runFfmpeg3
|
|
10146
|
+
runFfmpeg as runFfmpeg3,
|
|
10147
|
+
spawnStreamingEncoder as spawnStreamingEncoder4
|
|
10145
10148
|
} from "@hyperframes/engine";
|
|
10146
10149
|
import {
|
|
10147
10150
|
clearGifFramesBeforeNext,
|
|
@@ -10278,6 +10281,124 @@ function startEncodeProgress(job, stage, onProgress) {
|
|
|
10278
10281
|
});
|
|
10279
10282
|
return (frames = total) => reportEncodeProgress(job, frames, total, onProgress, 75);
|
|
10280
10283
|
}
|
|
10284
|
+
function resolveEngineConfig(input) {
|
|
10285
|
+
return input.engineConfig ?? input.job.config.producerConfig ?? resolveConfig();
|
|
10286
|
+
}
|
|
10287
|
+
function videoEncoderOptions(input, engineCfg) {
|
|
10288
|
+
const { job, preset } = input;
|
|
10289
|
+
return {
|
|
10290
|
+
fps: job.config.fps,
|
|
10291
|
+
width: input.width,
|
|
10292
|
+
height: input.height,
|
|
10293
|
+
codec: preset.codec,
|
|
10294
|
+
preset: preset.preset,
|
|
10295
|
+
quality: input.effectiveQuality,
|
|
10296
|
+
bitrate: input.effectiveBitrate,
|
|
10297
|
+
pixelFormat: preset.pixelFormat,
|
|
10298
|
+
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
10299
|
+
useGpu: job.config.useGpu,
|
|
10300
|
+
hdr: preset.hdr,
|
|
10301
|
+
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
10302
|
+
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
10303
|
+
// renders leave both undefined → preserves the existing open-GOP output.
|
|
10304
|
+
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
10305
|
+
gopSize: input.gopSize
|
|
10306
|
+
};
|
|
10307
|
+
}
|
|
10308
|
+
function canEncodeWhileCapturing(input, workerCount, cpuCount) {
|
|
10309
|
+
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" && input.lockGopForChunkConcat !== true;
|
|
10310
|
+
}
|
|
10311
|
+
async function readLandedFrame(workerPath, mergedPath) {
|
|
10312
|
+
if (workerPath !== void 0) {
|
|
10313
|
+
try {
|
|
10314
|
+
return await readFile2(workerPath);
|
|
10315
|
+
} catch (error) {
|
|
10316
|
+
if (error.code !== "ENOENT") throw error;
|
|
10317
|
+
}
|
|
10318
|
+
}
|
|
10319
|
+
return readFile2(mergedPath);
|
|
10320
|
+
}
|
|
10321
|
+
function encodeWhileCapturing(input, totalFrames) {
|
|
10322
|
+
const engineCfg = resolveEngineConfig(input);
|
|
10323
|
+
const stop = new AbortController();
|
|
10324
|
+
const signal = input.abortSignal ? AbortSignal.any([input.abortSignal, stop.signal]) : stop.signal;
|
|
10325
|
+
const landed = /* @__PURE__ */ new Map();
|
|
10326
|
+
let captureDone = false;
|
|
10327
|
+
let framesFed = 0;
|
|
10328
|
+
let wake = null;
|
|
10329
|
+
const nudge = () => {
|
|
10330
|
+
wake?.();
|
|
10331
|
+
wake = null;
|
|
10332
|
+
};
|
|
10333
|
+
signal.addEventListener("abort", nudge, { once: true });
|
|
10334
|
+
let reportFed = null;
|
|
10335
|
+
const encoder = spawnStreamingEncoder4(
|
|
10336
|
+
input.videoOnlyPath,
|
|
10337
|
+
{ ...videoEncoderOptions(input, engineCfg), imageFormat: "jpeg" },
|
|
10338
|
+
signal,
|
|
10339
|
+
engineCfg
|
|
10340
|
+
);
|
|
10341
|
+
const fed = (async () => {
|
|
10342
|
+
const stream = await encoder;
|
|
10343
|
+
for (let next = 0; next < totalFrames; next++) {
|
|
10344
|
+
while (!landed.has(next) && !captureDone && !signal.aborted) {
|
|
10345
|
+
await new Promise((resolve10) => wake = resolve10);
|
|
10346
|
+
}
|
|
10347
|
+
if (signal.aborted) return;
|
|
10348
|
+
const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
|
|
10349
|
+
const frame = await readLandedFrame(landed.get(next), mergedPath);
|
|
10350
|
+
landed.delete(next);
|
|
10351
|
+
if (!await stream.writeFrame(frame)) {
|
|
10352
|
+
throw new Error(stream.getExitError() ?? "encoder exited before the last frame");
|
|
10353
|
+
}
|
|
10354
|
+
framesFed++;
|
|
10355
|
+
reportFed?.(framesFed);
|
|
10356
|
+
}
|
|
10357
|
+
})().then(
|
|
10358
|
+
() => void 0,
|
|
10359
|
+
(error) => error ?? new Error("encode failed")
|
|
10360
|
+
);
|
|
10361
|
+
const closeEncoder = async () => (await encoder.catch(() => null))?.close();
|
|
10362
|
+
return {
|
|
10363
|
+
onFrameLanded: (frame) => {
|
|
10364
|
+
landed.set(frame.index, frame.path);
|
|
10365
|
+
nudge();
|
|
10366
|
+
},
|
|
10367
|
+
finish: async () => {
|
|
10368
|
+
const start = Date.now();
|
|
10369
|
+
captureDone = true;
|
|
10370
|
+
input.log.info("[Render] Encoded frames while capturing", {
|
|
10371
|
+
framesFed,
|
|
10372
|
+
totalFrames
|
|
10373
|
+
});
|
|
10374
|
+
const encoded = startEncodeProgress(input.job, "Encoding video", input.onProgress);
|
|
10375
|
+
reportFed = encoded;
|
|
10376
|
+
nudge();
|
|
10377
|
+
const failure = await fed;
|
|
10378
|
+
const result = await closeEncoder();
|
|
10379
|
+
input.assertNotAborted();
|
|
10380
|
+
if (failure === void 0 && result?.success) {
|
|
10381
|
+
encoded();
|
|
10382
|
+
return { encodeMs: Date.now() - start };
|
|
10383
|
+
}
|
|
10384
|
+
input.log.warn(
|
|
10385
|
+
"[Render] Encoding while capturing failed; encoding the frames after capture",
|
|
10386
|
+
{
|
|
10387
|
+
error: String(failure ?? result?.error)
|
|
10388
|
+
}
|
|
10389
|
+
);
|
|
10390
|
+
return runEncodeStage(input);
|
|
10391
|
+
},
|
|
10392
|
+
abort: async () => {
|
|
10393
|
+
stop.abort();
|
|
10394
|
+
await fed;
|
|
10395
|
+
await closeEncoder();
|
|
10396
|
+
},
|
|
10397
|
+
get framesFed() {
|
|
10398
|
+
return framesFed;
|
|
10399
|
+
}
|
|
10400
|
+
};
|
|
10401
|
+
}
|
|
10281
10402
|
async function runEncodeStage(input) {
|
|
10282
10403
|
const {
|
|
10283
10404
|
job,
|
|
@@ -10285,16 +10406,11 @@ async function runEncodeStage(input) {
|
|
|
10285
10406
|
outputPath,
|
|
10286
10407
|
framesDir,
|
|
10287
10408
|
videoOnlyPath,
|
|
10288
|
-
width,
|
|
10289
|
-
height,
|
|
10290
10409
|
needsAlpha,
|
|
10291
10410
|
hasAudio,
|
|
10292
10411
|
audioOutputPath,
|
|
10293
10412
|
isPngSequence,
|
|
10294
10413
|
isGif,
|
|
10295
|
-
preset,
|
|
10296
|
-
effectiveQuality,
|
|
10297
|
-
effectiveBitrate,
|
|
10298
10414
|
enableChunkedEncode,
|
|
10299
10415
|
chunkedEncodeSize,
|
|
10300
10416
|
abortSignal,
|
|
@@ -10324,7 +10440,7 @@ async function runEncodeStage(input) {
|
|
|
10324
10440
|
encoded();
|
|
10325
10441
|
return { encodeMs: Date.now() - stage5Start };
|
|
10326
10442
|
}
|
|
10327
|
-
const engineCfg = input
|
|
10443
|
+
const engineCfg = resolveEngineConfig(input);
|
|
10328
10444
|
if (isGif) {
|
|
10329
10445
|
const encoded = startEncodeProgress(job, "Encoding GIF", onProgress);
|
|
10330
10446
|
if (hasAudio) {
|
|
@@ -10354,24 +10470,7 @@ async function runEncodeStage(input) {
|
|
|
10354
10470
|
const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
|
|
10355
10471
|
const frameExt = frameFileExtension2(input.captureImageFormat);
|
|
10356
10472
|
const framePattern = `frame_%06d.${frameExt}`;
|
|
10357
|
-
const encoderOpts =
|
|
10358
|
-
fps: job.config.fps,
|
|
10359
|
-
width,
|
|
10360
|
-
height,
|
|
10361
|
-
codec: preset.codec,
|
|
10362
|
-
preset: preset.preset,
|
|
10363
|
-
quality: effectiveQuality,
|
|
10364
|
-
bitrate: effectiveBitrate,
|
|
10365
|
-
pixelFormat: preset.pixelFormat,
|
|
10366
|
-
vp9CpuUsed: engineCfg.vp9CpuUsed,
|
|
10367
|
-
useGpu: job.config.useGpu,
|
|
10368
|
-
hdr: preset.hdr,
|
|
10369
|
-
// Distributed chunk renders pass these so the encoder writes closed-GOP
|
|
10370
|
-
// keyframes that survive `-f concat -c copy` at assemble time. In-process
|
|
10371
|
-
// renders leave both undefined → preserves the existing open-GOP output.
|
|
10372
|
-
lockGopForChunkConcat: input.lockGopForChunkConcat === true,
|
|
10373
|
-
gopSize: input.gopSize
|
|
10374
|
-
};
|
|
10473
|
+
const encoderOpts = videoEncoderOptions(input, engineCfg);
|
|
10375
10474
|
const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
|
|
10376
10475
|
framesDir,
|
|
10377
10476
|
framePattern,
|
|
@@ -13286,6 +13385,34 @@ async function executeRenderPipeline(input) {
|
|
|
13286
13385
|
throw new Error(`Disk capture requires sdr_disk plan; got ${capturePlan.kind}`);
|
|
13287
13386
|
}
|
|
13288
13387
|
resetCaptureAttemptProgress(job);
|
|
13388
|
+
const encodeInput = {
|
|
13389
|
+
job,
|
|
13390
|
+
log,
|
|
13391
|
+
outputPath: stagedOutputPath,
|
|
13392
|
+
framesDir,
|
|
13393
|
+
videoOnlyPath,
|
|
13394
|
+
width,
|
|
13395
|
+
height,
|
|
13396
|
+
needsAlpha,
|
|
13397
|
+
captureImageFormat: captureOptions.format ?? "jpeg",
|
|
13398
|
+
hasAudio,
|
|
13399
|
+
audioOutputPath,
|
|
13400
|
+
isPngSequence,
|
|
13401
|
+
isGif,
|
|
13402
|
+
preset,
|
|
13403
|
+
effectiveQuality,
|
|
13404
|
+
effectiveBitrate,
|
|
13405
|
+
enableChunkedEncode,
|
|
13406
|
+
chunkedEncodeSize,
|
|
13407
|
+
engineConfig: cfg,
|
|
13408
|
+
// Same value the streaming encoder above spreads — the disk and
|
|
13409
|
+
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
13410
|
+
...hlsEncoderGopLock,
|
|
13411
|
+
abortSignal: executionSignal,
|
|
13412
|
+
assertNotAborted,
|
|
13413
|
+
onProgress
|
|
13414
|
+
};
|
|
13415
|
+
const overlap = !cfg.useDrawElement && workerSizing !== void 0 && canEncodeWhileCapturing(encodeInput, capturePlan.workerCount, workerSizing.cpuCount) ? encodeWhileCapturing(encodeInput, totalFrames) : null;
|
|
13289
13416
|
const captureFrameStart = Date.now();
|
|
13290
13417
|
const invokeDiskCapture = (diskPlan) => observeRenderStage(
|
|
13291
13418
|
observability,
|
|
@@ -13307,13 +13434,15 @@ async function executeRenderPipeline(input) {
|
|
|
13307
13434
|
createRenderVideoFrameInjector,
|
|
13308
13435
|
abortSignal: executionSignal,
|
|
13309
13436
|
assertNotAborted,
|
|
13310
|
-
onProgress
|
|
13437
|
+
onProgress,
|
|
13438
|
+
onFrameLanded: (frame) => overlap?.onFrameLanded(frame)
|
|
13311
13439
|
})
|
|
13312
13440
|
);
|
|
13313
13441
|
let captureRes;
|
|
13314
13442
|
try {
|
|
13315
13443
|
captureRes = await invokeDiskCapture(capturePlan);
|
|
13316
13444
|
} catch (err) {
|
|
13445
|
+
await overlap?.abort();
|
|
13317
13446
|
if (!isDrawElementVerificationError(err) && !isDrawElementCaptureError(err) || err instanceof RenderCancelledError || executionSignal?.aborted === true) {
|
|
13318
13447
|
throw err;
|
|
13319
13448
|
}
|
|
@@ -13382,33 +13511,7 @@ async function executeRenderPipeline(input) {
|
|
|
13382
13511
|
isGif,
|
|
13383
13512
|
chunkedEncode: enableChunkedEncode
|
|
13384
13513
|
}),
|
|
13385
|
-
() => runEncodeStage(
|
|
13386
|
-
job,
|
|
13387
|
-
log,
|
|
13388
|
-
outputPath: stagedOutputPath,
|
|
13389
|
-
framesDir,
|
|
13390
|
-
videoOnlyPath,
|
|
13391
|
-
width,
|
|
13392
|
-
height,
|
|
13393
|
-
needsAlpha,
|
|
13394
|
-
captureImageFormat: captureOptions.format ?? "jpeg",
|
|
13395
|
-
hasAudio,
|
|
13396
|
-
audioOutputPath,
|
|
13397
|
-
isPngSequence,
|
|
13398
|
-
isGif,
|
|
13399
|
-
preset,
|
|
13400
|
-
effectiveQuality,
|
|
13401
|
-
effectiveBitrate,
|
|
13402
|
-
enableChunkedEncode,
|
|
13403
|
-
chunkedEncodeSize,
|
|
13404
|
-
engineConfig: cfg,
|
|
13405
|
-
// Same value the streaming encoder above spreads — the disk and
|
|
13406
|
-
// chunked-concat encoders need an identical GOP lock for HLS.
|
|
13407
|
-
...hlsEncoderGopLock,
|
|
13408
|
-
abortSignal: executionSignal,
|
|
13409
|
-
assertNotAborted,
|
|
13410
|
-
onProgress
|
|
13411
|
-
})
|
|
13514
|
+
() => overlap ? overlap.finish() : runEncodeStage(encodeInput)
|
|
13412
13515
|
);
|
|
13413
13516
|
perfStages.encodeMs = encodeRes.encodeMs;
|
|
13414
13517
|
}
|
|
@@ -73,7 +73,7 @@ export interface ChunkResult {
|
|
|
73
73
|
sessionBootMs: number;
|
|
74
74
|
captureStageMs: number;
|
|
75
75
|
encodeStageMs: number;
|
|
76
|
-
/** Capture workers used for this chunk (`
|
|
76
|
+
/** Capture workers used for this chunk (`resolveChunkCaptureWorkers` result). */
|
|
77
77
|
workers: number;
|
|
78
78
|
/**
|
|
79
79
|
* Effective engine mode used by every worker, after any browser fallback.
|
|
@@ -110,11 +110,18 @@ export interface RenderChunkOptions {
|
|
|
110
110
|
* whole slice.
|
|
111
111
|
*/
|
|
112
112
|
frameRange?: ChunkFrameRange;
|
|
113
|
+
/**
|
|
114
|
+
* Capture browsers the runtime can host. Raises the machine-wide sizing up to
|
|
115
|
+
* this many, one browser per 20 frames; never lowers it.
|
|
116
|
+
*/
|
|
117
|
+
maxCaptureWorkers?: number;
|
|
113
118
|
}
|
|
119
|
+
/** Capture browsers for a chunk: the machine-wide sizing, raised by the runtime's budget. */
|
|
120
|
+
export declare function resolveChunkCaptureWorkers(framesInChunk: number, machineSized: number, maxCaptureWorkers: number | undefined): number;
|
|
114
121
|
/** The frames a chunk renders: its slice, or a validated sub-range of it. */
|
|
115
122
|
export declare function resolveChunkFrameRange(slice: ChunkSliceJson, chunkIndex: number, frameRange: ChunkFrameRange | undefined): ChunkFrameRange;
|
|
116
123
|
/** Compatibility-safe adapter seam for built-in or injected chunk renderers. */
|
|
117
|
-
export type ChunkRenderer = (planDir: string, chunkIndex: number, outputChunkPath: string) => Promise<ChunkResult>;
|
|
124
|
+
export type ChunkRenderer = (planDir: string, chunkIndex: number, outputChunkPath: string, options?: RenderChunkOptions) => Promise<ChunkResult>;
|
|
118
125
|
interface DistributedCaptureSessionDependencies {
|
|
119
126
|
createCaptureSession: typeof createCaptureSession;
|
|
120
127
|
assertSwiftShader: typeof assertSwiftShader;
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* `safeCleanup`, `sampleDirectoryBytes`, etc.) into a shared module so
|
|
35
35
|
* the stages can import them without reaching back into the orchestrator.
|
|
36
36
|
*/
|
|
37
|
-
import { type BeforeCaptureHook, type CaptureOptions, type CapturePerfSummary, type CaptureSession, type EngineConfig } from "@hyperframes/engine";
|
|
37
|
+
import { type BeforeCaptureHook, type CaptureOptions, type CapturePerfSummary, type CaptureSession, type EngineConfig, type LandedFrame } from "@hyperframes/engine";
|
|
38
38
|
import type { FileServerHandle } from "../../fileServer.js";
|
|
39
39
|
import type { ProducerLogger } from "../../../logger.js";
|
|
40
40
|
import { type CaptureAttemptSummary, type ProgressCallback, type RenderConfig, type RenderJob } from "../../renderOrchestrator.js";
|
|
@@ -87,6 +87,7 @@ export interface CaptureStageInput {
|
|
|
87
87
|
startFrame: number;
|
|
88
88
|
endFrame: number;
|
|
89
89
|
};
|
|
90
|
+
onFrameLanded?: (frame: LandedFrame) => void;
|
|
90
91
|
}
|
|
91
92
|
export interface CaptureStageResult {
|
|
92
93
|
/** Final worker count after any adaptive retry. */
|