@hyperframes/producer 0.8.168 → 0.8.170
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 +182 -17
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +63 -62
- package/dist/index.js +189 -17
- package/dist/services/distributed/chunkAudio.d.ts +30 -0
- package/dist/services/distributed/plan.d.ts +5 -0
- package/dist/services/distributed/planV2.d.ts +1 -0
- package/dist/services/render/audioPadTrim.d.ts +1 -0
- package/dist/services/render/stages/freezePlan.d.ts +1 -0
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -13969,6 +13969,13 @@ var PLAN_AUDIO_LEGACY_RELATIVE_PATH = "audio.aac";
|
|
|
13969
13969
|
function isPlanAudioArtifactPath(path) {
|
|
13970
13970
|
return path === PLAN_AUDIO_RELATIVE_PATH || path === PLAN_AUDIO_LEGACY_RELATIVE_PATH;
|
|
13971
13971
|
}
|
|
13972
|
+
function resolvePlanAudioPath(planDir) {
|
|
13973
|
+
for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
|
|
13974
|
+
const candidate = join28(planDir, name);
|
|
13975
|
+
if (existsSync18(candidate)) return candidate;
|
|
13976
|
+
}
|
|
13977
|
+
return null;
|
|
13978
|
+
}
|
|
13972
13979
|
var INVALID_VIDEO_METADATA = "INVALID_VIDEO_METADATA";
|
|
13973
13980
|
var PlanVideosMetadataError = class extends Error {
|
|
13974
13981
|
// Read by cloud adapters across the package boundary to classify retries.
|
|
@@ -14621,9 +14628,28 @@ function buildLockedRenderConfig(input) {
|
|
|
14621
14628
|
chunkSize: input.effectiveChunkSize,
|
|
14622
14629
|
chunkCount: input.chunkCount,
|
|
14623
14630
|
runtimeEnv: input.runtimeEnv,
|
|
14624
|
-
variables: config.variables
|
|
14631
|
+
variables: config.variables,
|
|
14632
|
+
...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
|
|
14625
14633
|
};
|
|
14626
14634
|
}
|
|
14635
|
+
function carriesChunkAudio(config, options) {
|
|
14636
|
+
return options.chunksCarryAudio === true && config.format === "mp4" && config.cfr !== true;
|
|
14637
|
+
}
|
|
14638
|
+
async function fitPlanAudioToFrames(audioPath, workDir, totalFrames, fps, run) {
|
|
14639
|
+
const fittedPath = join30(workDir, "audio-fitted.m4a");
|
|
14640
|
+
const result = await padOrTrimAudioToVideoFrameCount({
|
|
14641
|
+
videoPath: audioPath,
|
|
14642
|
+
probeVideoFrameInfo: async () => ({ frameCount: totalFrames, fpsNum: fps, fpsDen: 1 }),
|
|
14643
|
+
audioPath,
|
|
14644
|
+
outputPath: fittedPath,
|
|
14645
|
+
signal: run.signal,
|
|
14646
|
+
timeoutMs: run.timeoutMs
|
|
14647
|
+
});
|
|
14648
|
+
if (!result.success) {
|
|
14649
|
+
throw encoderFailureError("[plan] audio pad/trim failed", result);
|
|
14650
|
+
}
|
|
14651
|
+
renameSync5(fittedPath, audioPath);
|
|
14652
|
+
}
|
|
14627
14653
|
function resolveEncoderTriple(config) {
|
|
14628
14654
|
if (config.format === "mp4") {
|
|
14629
14655
|
return resolveMp4EncoderTriple(config.codec);
|
|
@@ -14677,6 +14703,13 @@ function pathInside(root, path) {
|
|
|
14677
14703
|
const rel = relative5(root, path);
|
|
14678
14704
|
return rel === "" || rel.startsWith("..") || isAbsolute3(rel) ? null : rel;
|
|
14679
14705
|
}
|
|
14706
|
+
function stableSourcePath(roots, srcPath) {
|
|
14707
|
+
for (const [root, prefix] of roots) {
|
|
14708
|
+
const rel = pathInside(root, srcPath);
|
|
14709
|
+
if (rel !== null) return [prefix, ...rel.split(sep2)].filter(Boolean).join("/");
|
|
14710
|
+
}
|
|
14711
|
+
return srcPath;
|
|
14712
|
+
}
|
|
14680
14713
|
function createVideoSourceShipper(input) {
|
|
14681
14714
|
const shipped = /* @__PURE__ */ new Map();
|
|
14682
14715
|
return (srcPath) => {
|
|
@@ -14889,12 +14922,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
14889
14922
|
workCompiledDir: compiledDir,
|
|
14890
14923
|
workVideoFramesDir: stagedVideoFrames
|
|
14891
14924
|
});
|
|
14925
|
+
const sourceRoots = [
|
|
14926
|
+
[stagedVideoFrames, "video-frames"],
|
|
14927
|
+
[compiledDir, "compiled"],
|
|
14928
|
+
[projectDir, ""]
|
|
14929
|
+
];
|
|
14892
14930
|
const planVideosJson = buildPlanVideosJson({
|
|
14893
14931
|
videos: composition.videos,
|
|
14894
14932
|
compositionEnd: job.duration ?? Number.NaN,
|
|
14895
14933
|
extracted: (extractResult.extractionResult?.extracted ?? []).map((ext) => ({
|
|
14896
14934
|
videoId: ext.videoId,
|
|
14897
|
-
srcPath: ext.srcPath,
|
|
14935
|
+
srcPath: stableSourcePath(sourceRoots, ext.srcPath),
|
|
14898
14936
|
framePattern: ext.framePattern,
|
|
14899
14937
|
fps: ext.fps,
|
|
14900
14938
|
totalFrames: ext.totalFrames,
|
|
@@ -14945,6 +14983,13 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
14945
14983
|
if (audioResult.hasAudio && existsSync19(audioResult.audioOutputPath)) {
|
|
14946
14984
|
renameSync5(audioResult.audioOutputPath, planAudioPath);
|
|
14947
14985
|
}
|
|
14986
|
+
const chunksCarryAudio2 = carriesChunkAudio(config, options);
|
|
14987
|
+
if (chunksCarryAudio2 && existsSync19(planAudioPath)) {
|
|
14988
|
+
await fitPlanAudioToFrames(planAudioPath, workDir, totalFrames, config.fps, {
|
|
14989
|
+
signal: abortSignal,
|
|
14990
|
+
timeoutMs: cfg.ffmpegProcessTimeout
|
|
14991
|
+
});
|
|
14992
|
+
}
|
|
14948
14993
|
const ffmpegVersion = await readFfmpegVersion();
|
|
14949
14994
|
const producerVersion = readProducerVersion();
|
|
14950
14995
|
const fontSnapshotSha = await readFontSnapshotSha();
|
|
@@ -14957,7 +15002,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
|
|
|
14957
15002
|
engineConfig: cfg,
|
|
14958
15003
|
effectiveChunkSize,
|
|
14959
15004
|
chunkCount,
|
|
14960
|
-
runtimeEnv
|
|
15005
|
+
runtimeEnv,
|
|
15006
|
+
chunksCarryAudio: chunksCarryAudio2
|
|
14961
15007
|
});
|
|
14962
15008
|
const compositionJson = {
|
|
14963
15009
|
durationSeconds: job.duration ?? 0,
|
|
@@ -16656,8 +16702,10 @@ function resolveExtractedVideoOutputDir(planDir, videoId) {
|
|
|
16656
16702
|
}
|
|
16657
16703
|
return outputDir;
|
|
16658
16704
|
}
|
|
16659
|
-
function artifactTargets(path, videoDependencies) {
|
|
16660
|
-
if (isPlanAudioArtifactPath(path))
|
|
16705
|
+
function artifactTargets(path, videoDependencies, chunksCarryAudio2) {
|
|
16706
|
+
if (isPlanAudioArtifactPath(path)) {
|
|
16707
|
+
return { chunks: chunksCarryAudio2 ? "all" : [], assembler: true };
|
|
16708
|
+
}
|
|
16661
16709
|
if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
|
|
16662
16710
|
return { chunks: "all", assembler: true };
|
|
16663
16711
|
}
|
|
@@ -16879,9 +16927,11 @@ function buildPlanV2Publication(executionPlanDir, digests) {
|
|
|
16879
16927
|
}
|
|
16880
16928
|
validateExtractionCacheCompleteSentinels(executionPlanDir);
|
|
16881
16929
|
const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
|
|
16930
|
+
const encoder = readJsonFile(join36(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
|
|
16931
|
+
const chunksCarryAudio2 = isRecord3(encoder) && encoder.chunksCarryAudio === true;
|
|
16882
16932
|
for (const file of listFiles(executionPlanDir)) {
|
|
16883
16933
|
if (isExtractionCacheCompleteSentinelPath(file.path)) continue;
|
|
16884
|
-
const targets = artifactTargets(file.path, videoDependencyPlan.dependencies);
|
|
16934
|
+
const targets = artifactTargets(file.path, videoDependencyPlan.dependencies, chunksCarryAudio2);
|
|
16885
16935
|
if ((file.path.startsWith("video-frames/") || file.path.startsWith("video-sources/")) && targets.chunks !== "all" && targets.chunks.length === 0 && !targets.assembler) {
|
|
16886
16936
|
continue;
|
|
16887
16937
|
}
|
|
@@ -16984,7 +17034,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
|
|
|
16984
17034
|
listFiles(join36(stagingRoot, "compiled")).map(
|
|
16985
17035
|
(file) => publishBlobFor(file.absolutePath, digests)
|
|
16986
17036
|
)
|
|
16987
|
-
)
|
|
17037
|
+
),
|
|
17038
|
+
chunksCarryAudio: options.chunksCarryAudio
|
|
16988
17039
|
});
|
|
16989
17040
|
return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
|
|
16990
17041
|
} catch (error) {
|
|
@@ -17307,6 +17358,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
|
|
|
17307
17358
|
format,
|
|
17308
17359
|
fps,
|
|
17309
17360
|
fpsArg: fpsToFfmpegArg3(fps),
|
|
17361
|
+
chunkSeconds: chunksCarryAudio(planDir) ? plan2.chunks.map((chunk) => (chunk.endFrame - chunk.startFrame) * fps.den / fps.num) : null,
|
|
17310
17362
|
signal: options?.abortSignal,
|
|
17311
17363
|
timeout: resolveConfig6().ffmpegProcessTimeout,
|
|
17312
17364
|
log,
|
|
@@ -17369,10 +17421,10 @@ function readAssemblePlan(planDir, chunkPaths) {
|
|
|
17369
17421
|
throw new Error(`[assemble] chunk path does not exist: ${path}`);
|
|
17370
17422
|
}
|
|
17371
17423
|
}
|
|
17372
|
-
return plan2;
|
|
17424
|
+
return { ...plan2, chunks };
|
|
17373
17425
|
}
|
|
17374
17426
|
function videoInputArgs(ctx, videoSources) {
|
|
17375
|
-
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
|
|
17427
|
+
return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg, ctx.chunkSeconds);
|
|
17376
17428
|
}
|
|
17377
17429
|
async function reencodeCfr(ctx, chunkPaths, encoder) {
|
|
17378
17430
|
const cfrOutputPath = join37(ctx.workDir, `cfr.${ctx.format}`);
|
|
@@ -17452,10 +17504,15 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
|
|
|
17452
17504
|
throw encoderFailureError("[assemble] audio mux failed", muxResult);
|
|
17453
17505
|
}
|
|
17454
17506
|
}
|
|
17455
|
-
function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
|
|
17507
|
+
function concatDemuxerInput(chunkPaths, workDir, fpsArg, chunkSeconds) {
|
|
17456
17508
|
const listPath = join37(workDir, "concat-list.txt");
|
|
17457
|
-
const
|
|
17458
|
-
|
|
17509
|
+
const head = chunkSeconds ? ["stream", "exact_stream_id 1"] : [];
|
|
17510
|
+
const body = chunkPaths.map((path, i) => {
|
|
17511
|
+
const line = `file '${path.replace(/'/g, "'\\''")}'`;
|
|
17512
|
+
return chunkSeconds ? `${line}
|
|
17513
|
+
duration ${chunkSeconds[i].toFixed(6)}` : line;
|
|
17514
|
+
});
|
|
17515
|
+
writeFileSync11(listPath, `${[...head, ...body].join("\n")}
|
|
17459
17516
|
`, "utf-8");
|
|
17460
17517
|
return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
|
|
17461
17518
|
}
|
|
@@ -17478,6 +17535,11 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
|
|
|
17478
17535
|
});
|
|
17479
17536
|
}
|
|
17480
17537
|
}
|
|
17538
|
+
function chunksCarryAudio(planDir) {
|
|
17539
|
+
const encoderJsonPath = join37(planDir, "meta", "encoder.json");
|
|
17540
|
+
if (!existsSync25(encoderJsonPath)) return false;
|
|
17541
|
+
return JSON.parse(readFileSync13(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
|
|
17542
|
+
}
|
|
17481
17543
|
function readCfrEncoder(planDir, format) {
|
|
17482
17544
|
if (format !== "mp4") {
|
|
17483
17545
|
throw new Error(
|
|
@@ -17562,6 +17624,104 @@ import {
|
|
|
17562
17624
|
readWebGlVendorInfoFromCanvas,
|
|
17563
17625
|
resolveConfig as resolveConfig7
|
|
17564
17626
|
} from "@hyperframes/engine";
|
|
17627
|
+
|
|
17628
|
+
// src/services/distributed/chunkAudio.ts
|
|
17629
|
+
import { appendRenderProvenanceArgs as appendRenderProvenanceArgs2, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
|
|
17630
|
+
function audioCutForChunk(grid, chunks, index, fps) {
|
|
17631
|
+
const firstPacketAt = (frame) => Math.min(
|
|
17632
|
+
grid.packetCount,
|
|
17633
|
+
Math.max(
|
|
17634
|
+
0,
|
|
17635
|
+
Math.ceil(
|
|
17636
|
+
(frame * fps.den * grid.sampleRate - grid.firstStart * fps.num) / (grid.packetSamples * fps.num)
|
|
17637
|
+
)
|
|
17638
|
+
)
|
|
17639
|
+
);
|
|
17640
|
+
const first = index === 0 ? 0 : firstPacketAt(chunks[index].startFrame);
|
|
17641
|
+
const end = index === chunks.length - 1 ? grid.packetCount : firstPacketAt(chunks[index + 1].startFrame);
|
|
17642
|
+
if (end <= first) return null;
|
|
17643
|
+
const cutSeconds = (packet) => (grid.firstStart + packet * grid.packetSamples - grid.packetSamples / 2) / grid.sampleRate;
|
|
17644
|
+
const seekSeconds = index === 0 ? null : cutSeconds(first);
|
|
17645
|
+
const durationSeconds = end === grid.packetCount ? null : cutSeconds(end) - (seekSeconds ?? 0);
|
|
17646
|
+
return { seekSeconds, durationSeconds, firstPacket: first, packetCount: end - first };
|
|
17647
|
+
}
|
|
17648
|
+
async function probeAacPacketGrid(audioPath) {
|
|
17649
|
+
const probe = await runFfprobeJson([
|
|
17650
|
+
"-v",
|
|
17651
|
+
"error",
|
|
17652
|
+
"-select_streams",
|
|
17653
|
+
"a:0",
|
|
17654
|
+
"-show_entries",
|
|
17655
|
+
"stream=sample_rate,time_base,nb_frames:packet=pts,duration",
|
|
17656
|
+
"-read_intervals",
|
|
17657
|
+
"%+#1",
|
|
17658
|
+
"-of",
|
|
17659
|
+
"json",
|
|
17660
|
+
"--",
|
|
17661
|
+
audioPath
|
|
17662
|
+
]);
|
|
17663
|
+
const stream = probe.streams?.[0] ?? {};
|
|
17664
|
+
const packet = probe.packets?.[0] ?? {};
|
|
17665
|
+
const sampleRate = Number(stream.sample_rate);
|
|
17666
|
+
const [tbNum = NaN, tbDen = NaN] = String(stream.time_base).split("/").map(Number);
|
|
17667
|
+
const toSamples = (ticks) => Number(ticks) * sampleRate * tbNum / tbDen;
|
|
17668
|
+
const grid = {
|
|
17669
|
+
sampleRate,
|
|
17670
|
+
firstStart: toSamples(packet.pts),
|
|
17671
|
+
packetSamples: toSamples(packet.duration),
|
|
17672
|
+
packetCount: Number(stream.nb_frames)
|
|
17673
|
+
};
|
|
17674
|
+
const counts = [grid.sampleRate, grid.packetSamples, grid.packetCount];
|
|
17675
|
+
if (!Number.isFinite(grid.firstStart) || !counts.every((n) => Number.isFinite(n) && n > 0)) {
|
|
17676
|
+
throw new Error(`[chunkAudio] could not read the AAC packet grid: ${JSON.stringify(probe)}`);
|
|
17677
|
+
}
|
|
17678
|
+
return grid;
|
|
17679
|
+
}
|
|
17680
|
+
function chunkMuxArgs(input, cut) {
|
|
17681
|
+
if (!cut) return ["-i", input.videoPath, "-map", "0:v:0"];
|
|
17682
|
+
const seek = cut.seekSeconds === null ? [] : ["-ss", cut.seekSeconds.toFixed(6)];
|
|
17683
|
+
const length = cut.durationSeconds === null ? [] : ["-t", cut.durationSeconds.toFixed(6)];
|
|
17684
|
+
const exact = seek.length ? ["-copypriorss", "0"] : [];
|
|
17685
|
+
return [
|
|
17686
|
+
...["-i", input.videoPath, ...seek, ...length, "-i", input.audioPath],
|
|
17687
|
+
...["-map", "0:v:0", "-map", "1:a:0", ...exact]
|
|
17688
|
+
];
|
|
17689
|
+
}
|
|
17690
|
+
async function muxChunkWithAudio(input) {
|
|
17691
|
+
const grid = input.audioPath ? await probeAacPacketGrid(input.audioPath) : null;
|
|
17692
|
+
const cut = grid ? audioCutForChunk(grid, input.chunks, input.chunkIndex, input.fps) : null;
|
|
17693
|
+
const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
|
|
17694
|
+
appendRenderProvenanceArgs2(args, input.outputPath);
|
|
17695
|
+
args.push("-y", input.outputPath);
|
|
17696
|
+
const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
|
|
17697
|
+
if (!result.success) {
|
|
17698
|
+
throw new Error(
|
|
17699
|
+
`[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
|
|
17700
|
+
);
|
|
17701
|
+
}
|
|
17702
|
+
const slice = input.chunks[input.chunkIndex];
|
|
17703
|
+
const expected = { video: slice.endFrame - slice.startFrame };
|
|
17704
|
+
if (cut) expected.audio = cut.packetCount;
|
|
17705
|
+
const probe = await runFfprobeJson([
|
|
17706
|
+
"-v",
|
|
17707
|
+
"error",
|
|
17708
|
+
"-count_packets",
|
|
17709
|
+
"-show_entries",
|
|
17710
|
+
"stream=codec_type,nb_read_packets",
|
|
17711
|
+
"-of",
|
|
17712
|
+
"json",
|
|
17713
|
+
"--",
|
|
17714
|
+
input.outputPath
|
|
17715
|
+
]);
|
|
17716
|
+
for (const [type, count] of Object.entries(expected)) {
|
|
17717
|
+
const packets = Number(probe.streams?.find((s) => s.codec_type === type)?.nb_read_packets ?? 0);
|
|
17718
|
+
if (packets !== count) {
|
|
17719
|
+
throw new Error(`[chunkAudio] ${type} packets in chunk ${packets}, expected ${count}`);
|
|
17720
|
+
}
|
|
17721
|
+
}
|
|
17722
|
+
}
|
|
17723
|
+
|
|
17724
|
+
// src/services/distributed/renderChunk.ts
|
|
17565
17725
|
import { readWebGlVendorInfoFromCanvas as readWebGlVendorInfoFromCanvas2 } from "@hyperframes/engine";
|
|
17566
17726
|
var FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
|
|
17567
17727
|
var PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
|
|
@@ -18111,7 +18271,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18111
18271
|
const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
|
|
18112
18272
|
const effectiveQuality = encoder.crf ?? preset.quality;
|
|
18113
18273
|
const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
|
|
18114
|
-
const videoOnlyPath = outputChunkPath;
|
|
18274
|
+
const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
|
|
18115
18275
|
if (isPngSequence) {
|
|
18116
18276
|
if (!existsSync26(outputChunkPath)) mkdirSync16(outputChunkPath, { recursive: true });
|
|
18117
18277
|
} else {
|
|
@@ -18122,16 +18282,16 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18122
18282
|
await runEncodeStage({
|
|
18123
18283
|
job,
|
|
18124
18284
|
log,
|
|
18125
|
-
outputPath:
|
|
18285
|
+
outputPath: videoOnlyPath,
|
|
18126
18286
|
framesDir,
|
|
18127
18287
|
videoOnlyPath,
|
|
18128
18288
|
width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
|
|
18129
18289
|
height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
|
|
18130
18290
|
needsAlpha: plan2.dimensions.format !== "mp4",
|
|
18131
18291
|
captureImageFormat: captureOptions.format ?? "jpeg",
|
|
18132
|
-
//
|
|
18133
|
-
//
|
|
18134
|
-
// AND the mp4 audio mux.
|
|
18292
|
+
// The encoder writes video only; audio joins at assemble time or in the
|
|
18293
|
+
// chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
|
|
18294
|
+
// sidecar AND the mp4 audio mux.
|
|
18135
18295
|
hasAudio: false,
|
|
18136
18296
|
isPngSequence,
|
|
18137
18297
|
// `DistributedFormat` has no "gif" member — distributed chunks are
|
|
@@ -18158,6 +18318,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
|
|
|
18158
18318
|
lockGopForChunkConcat: !isPngSequence,
|
|
18159
18319
|
gopSize: framesInChunk
|
|
18160
18320
|
});
|
|
18321
|
+
if (encoder.chunksCarryAudio) {
|
|
18322
|
+
await muxChunkWithAudio({
|
|
18323
|
+
videoPath: videoOnlyPath,
|
|
18324
|
+
audioPath: resolvePlanAudioPath(planDir),
|
|
18325
|
+
outputPath: outputChunkPath,
|
|
18326
|
+
chunks,
|
|
18327
|
+
chunkIndex,
|
|
18328
|
+
fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
|
|
18329
|
+
timeoutMs: cfg.ffmpegProcessTimeout
|
|
18330
|
+
});
|
|
18331
|
+
rmSync15(videoOnlyPath, { force: true });
|
|
18332
|
+
}
|
|
18161
18333
|
encodeStageMs = Date.now() - encodeStarted;
|
|
18162
18334
|
} finally {
|
|
18163
18335
|
if (session) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Fps } from "@hyperframes/core";
|
|
2
|
+
import type { ChunkSliceJson } from "../render/stages/freezePlan.js";
|
|
3
|
+
/** Where AAC packet `j` of the plan audio starts: `firstStart + j * packetSamples`, in samples. */
|
|
4
|
+
interface AacPacketGrid {
|
|
5
|
+
sampleRate: number;
|
|
6
|
+
firstStart: number;
|
|
7
|
+
packetSamples: number;
|
|
8
|
+
packetCount: number;
|
|
9
|
+
}
|
|
10
|
+
/** Input-side `-ss` / `-t` that select exactly packets `[first, end)` for one chunk. */
|
|
11
|
+
interface AudioCut {
|
|
12
|
+
seekSeconds: number | null;
|
|
13
|
+
durationSeconds: number | null;
|
|
14
|
+
firstPacket: number;
|
|
15
|
+
packetCount: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function audioCutForChunk(grid: AacPacketGrid, chunks: readonly ChunkSliceJson[], index: number, fps: Fps): AudioCut | null;
|
|
18
|
+
interface MuxInput {
|
|
19
|
+
videoPath: string;
|
|
20
|
+
audioPath: string | null;
|
|
21
|
+
outputPath: string;
|
|
22
|
+
chunks: readonly ChunkSliceJson[];
|
|
23
|
+
chunkIndex: number;
|
|
24
|
+
fps: Fps;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
timeoutMs: number;
|
|
27
|
+
}
|
|
28
|
+
/** Mux one encoded chunk with the plan audio packets it owns into a regular mp4 at `outputPath`. */
|
|
29
|
+
export declare function muxChunkWithAudio(input: MuxInput): Promise<void>;
|
|
30
|
+
export {};
|
|
@@ -205,6 +205,11 @@ export interface BuildLocalExecutionPlanOptions {
|
|
|
205
205
|
readonly digests?: PlanFileDigests;
|
|
206
206
|
/** Runs while the audio mixes, once `compiled/` and the video trees are final. */
|
|
207
207
|
readonly whileAudioMixes?: () => Promise<void>;
|
|
208
|
+
/**
|
|
209
|
+
* mp4 chunks carry their own slice of the plan audio (renderChunk muxes it). Ignored for other
|
|
210
|
+
* formats and with `cfr`, which re-encodes.
|
|
211
|
+
*/
|
|
212
|
+
readonly chunksCarryAudio?: boolean;
|
|
208
213
|
}
|
|
209
214
|
export declare function logPlanStage(log: ProducerLogger, stage: string, startedAt: number): void;
|
|
210
215
|
/**
|
|
@@ -112,3 +112,4 @@ export declare function buildPadTrimAudioArgs(audioPath: string, outputPath: str
|
|
|
112
112
|
*/
|
|
113
113
|
export declare function padOrTrimAudioToVideoFrameCount(input: PadTrimAudioInput): Promise<PadTrimAudioResult>;
|
|
114
114
|
export declare function defaultProbeVideoFrameInfo(videoPath: string, signal?: AbortSignal): Promise<ProbeVideoFrameInfo>;
|
|
115
|
+
export declare function runFfprobeJson<T>(args: string[], signal?: AbortSignal): Promise<T>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/producer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.170",
|
|
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/core": "0.8.
|
|
39
|
-
"@hyperframes/parsers": "0.8.
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/engine": "0.8.
|
|
38
|
+
"@hyperframes/core": "0.8.170",
|
|
39
|
+
"@hyperframes/parsers": "0.8.170",
|
|
40
|
+
"@hyperframes/studio-server": "0.8.170",
|
|
41
|
+
"@hyperframes/lint": "0.8.170",
|
|
42
|
+
"@hyperframes/engine": "0.8.170"
|
|
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.170"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|