@hyperframes/producer 0.8.183 → 0.8.185
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 +75 -42
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +65 -65
- package/dist/index.d.ts +1 -0
- package/dist/index.js +203 -73
- package/dist/public-server.js +158 -58
- package/dist/services/distributed/chunkFrames.d.ts +1 -1
- 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/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,
|
|
@@ -10645,7 +10744,6 @@ async function enforceAacTruePeak(input) {
|
|
|
10645
10744
|
try {
|
|
10646
10745
|
scratchDir = mkdtempSync3(join25(dirname9(input.audioPath), ".true-peak-"));
|
|
10647
10746
|
const correctedPath = join25(scratchDir, "audio.m4a");
|
|
10648
|
-
const correctionTargetDbfs = AAC_DELIVERY_TRUE_PEAK_DBFS - AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB;
|
|
10649
10747
|
const measurements = [];
|
|
10650
10748
|
let attenuationDb = 0;
|
|
10651
10749
|
let measuredPath = input.audioPath;
|
|
@@ -10663,7 +10761,8 @@ async function enforceAacTruePeak(input) {
|
|
|
10663
10761
|
return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
|
|
10664
10762
|
}
|
|
10665
10763
|
if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
|
|
10666
|
-
|
|
10764
|
+
const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
|
|
10765
|
+
attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
|
|
10667
10766
|
const result = await input.encode(
|
|
10668
10767
|
input.audioPath,
|
|
10669
10768
|
correctedPath,
|
|
@@ -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
|
}
|
|
@@ -14370,9 +14473,6 @@ function validateOutputResolutionOverride(body) {
|
|
|
14370
14473
|
if (body.outputResolution.trim().length > 0 && normalized === void 0) {
|
|
14371
14474
|
return `Invalid outputResolution "${body.outputResolution}". Must be one of: landscape, portrait, landscape-4k, portrait-4k, square, square-4k (aliases: 1080p, 4k, \u2026).`;
|
|
14372
14475
|
}
|
|
14373
|
-
if (normalized !== void 0 && (body.format === "webm" || body.format === "mov")) {
|
|
14374
|
-
return `outputResolution is not supported with format "${body.format}": the alpha (webm/mov) capture path can't supersample. Use format "mp4", or omit outputResolution to render at the composition's native dimensions.`;
|
|
14375
|
-
}
|
|
14376
14476
|
return void 0;
|
|
14377
14477
|
}
|
|
14378
14478
|
function prepareProjectDirectory(projectDir, options) {
|
|
@@ -17,7 +17,7 @@ export declare class RenderChunkValidationError extends Error {
|
|
|
17
17
|
constructor(code: RenderChunkValidationCode, message: string);
|
|
18
18
|
}
|
|
19
19
|
/** One injector per browser session: a hook reused on a fresh page can skip its first injection. */
|
|
20
|
-
export declare function createChunkVideoFrameInjectorFactory(frameLookup: FrameLookupTable | null): () => BeforeCaptureHook | null;
|
|
20
|
+
export declare function createChunkVideoFrameInjectorFactory(frameLookup: FrameLookupTable | Promise<FrameLookupTable> | null): () => BeforeCaptureHook | null;
|
|
21
21
|
/**
|
|
22
22
|
* Rebuilds `ExtractedFrames[]` from `<planDir>/video-frames/<videoId>/`. `framePaths` keys are 0-based
|
|
23
23
|
* frame indexes (what `getFrameAtTime` reads); any other key drops every video's first-paint frame.
|
|
@@ -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. */
|
|
@@ -26,8 +26,12 @@
|
|
|
26
26
|
* branch + same args.
|
|
27
27
|
* - `Encoding failed: <err>` throws on the encoder's
|
|
28
28
|
* `success: false`.
|
|
29
|
+
*
|
|
30
|
+
* `encodeWhileCapturing` runs the same mp4 encode during multi-worker disk
|
|
31
|
+
* capture, feeding frames in order as they land, and falls back to this
|
|
32
|
+
* stage's encode if it fails.
|
|
29
33
|
*/
|
|
30
|
-
import { getEncoderPreset, type EngineConfig } from "@hyperframes/engine";
|
|
34
|
+
import { getEncoderPreset, type EngineConfig, type LandedFrame } from "@hyperframes/engine";
|
|
31
35
|
import type { ProducerLogger } from "../../../logger.js";
|
|
32
36
|
import type { ProgressCallback, RenderJob } from "../../renderOrchestrator.js";
|
|
33
37
|
export interface EncodeStageInput {
|
|
@@ -71,7 +75,7 @@ export interface EncodeStageInput {
|
|
|
71
75
|
enableChunkedEncode: boolean;
|
|
72
76
|
chunkedEncodeSize: number;
|
|
73
77
|
/** Already-resolved engine config from the orchestrator; direct callers fall back below. */
|
|
74
|
-
engineConfig?: Pick<EngineConfig, "ffmpegEncodeTimeout" | "vp9CpuUsed"
|
|
78
|
+
engineConfig?: Pick<EngineConfig, "ffmpegEncodeTimeout" | "vp9CpuUsed"> & Partial<Pick<EngineConfig, "ffmpegStreamingTimeout">>;
|
|
75
79
|
abortSignal: AbortSignal | undefined;
|
|
76
80
|
assertNotAborted: () => void;
|
|
77
81
|
onProgress?: ProgressCallback;
|
|
@@ -97,4 +101,24 @@ export interface EncodeStageResult {
|
|
|
97
101
|
/** Wall-clock ms for the encode (or png-copy) phase. */
|
|
98
102
|
encodeMs: number;
|
|
99
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Software libx264 from JPEG only: the streaming encoder then runs the disk encoder's
|
|
106
|
+
* arguments (same bytes). Needs a core the capture workers leave free.
|
|
107
|
+
*/
|
|
108
|
+
export declare function canEncodeWhileCapturing(input: EncodeStageInput, workerCount: number, cpuCount: number): boolean;
|
|
109
|
+
export interface EncodeWhileCapturing {
|
|
110
|
+
/** A worker finished writing this frame. */
|
|
111
|
+
onFrameLanded: (frame: LandedFrame) => void;
|
|
112
|
+
/** Capture succeeded and every frame is in `framesDir`: encode the rest. */
|
|
113
|
+
finish: () => Promise<EncodeStageResult>;
|
|
114
|
+
/** Capture failed: stop the encoder and wait for it to exit. */
|
|
115
|
+
abort: () => Promise<void>;
|
|
116
|
+
/** Frames handed to the encoder so far. */
|
|
117
|
+
readonly framesFed: number;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Feeds frames 0, 1, 2… to the encoder as each lands; capture never waits on it.
|
|
121
|
+
* On failure `finish` re-runs `runEncodeStage` on the same frames.
|
|
122
|
+
*/
|
|
123
|
+
export declare function encodeWhileCapturing(input: EncodeStageInput, totalFrames: number): EncodeWhileCapturing;
|
|
100
124
|
export declare function runEncodeStage(input: EncodeStageInput): Promise<EncodeStageResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.185",
|
|
4
4
|
"description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
"postcss": "^8.4.0",
|
|
36
36
|
"puppeteer": "^25.2.1",
|
|
37
37
|
"puppeteer-core": "^25.2.1",
|
|
38
|
-
"@hyperframes/
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/
|
|
38
|
+
"@hyperframes/lint": "0.8.185",
|
|
39
|
+
"@hyperframes/parsers": "0.8.185",
|
|
40
|
+
"@hyperframes/studio-server": "0.8.185",
|
|
41
|
+
"@hyperframes/engine": "0.8.185",
|
|
42
|
+
"@hyperframes/core": "0.8.185"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^25.0.10",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"tsx": "^4.21.0",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vitest": "^4.1.11",
|
|
51
|
-
"@hyperframes/aws-lambda": "0.8.
|
|
51
|
+
"@hyperframes/aws-lambda": "0.8.185"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|