@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.
@@ -7400,9 +7400,28 @@ function buildLockedRenderConfig(input) {
7400
7400
  chunkSize: input.effectiveChunkSize,
7401
7401
  chunkCount: input.chunkCount,
7402
7402
  runtimeEnv: input.runtimeEnv,
7403
- variables: config.variables
7403
+ variables: config.variables,
7404
+ ...input.chunksCarryAudio ? { chunksCarryAudio: true } : {}
7404
7405
  };
7405
7406
  }
7407
+ function carriesChunkAudio(config, options) {
7408
+ return options.chunksCarryAudio === true && config.format === "mp4" && config.cfr !== true;
7409
+ }
7410
+ async function fitPlanAudioToFrames(audioPath, workDir, totalFrames, fps, run) {
7411
+ const fittedPath = join18(workDir, "audio-fitted.m4a");
7412
+ const result = await padOrTrimAudioToVideoFrameCount({
7413
+ videoPath: audioPath,
7414
+ probeVideoFrameInfo: async () => ({ frameCount: totalFrames, fpsNum: fps, fpsDen: 1 }),
7415
+ audioPath,
7416
+ outputPath: fittedPath,
7417
+ signal: run.signal,
7418
+ timeoutMs: run.timeoutMs
7419
+ });
7420
+ if (!result.success) {
7421
+ throw encoderFailureError("[plan] audio pad/trim failed", result);
7422
+ }
7423
+ renameSync3(fittedPath, audioPath);
7424
+ }
7406
7425
  function resolveEncoderTriple(config) {
7407
7426
  if (config.format === "mp4") {
7408
7427
  return resolveMp4EncoderTriple(config.codec);
@@ -7456,6 +7475,13 @@ function pathInside(root, path) {
7456
7475
  const rel = relative5(root, path);
7457
7476
  return rel === "" || rel.startsWith("..") || isAbsolute3(rel) ? null : rel;
7458
7477
  }
7478
+ function stableSourcePath(roots, srcPath) {
7479
+ for (const [root, prefix] of roots) {
7480
+ const rel = pathInside(root, srcPath);
7481
+ if (rel !== null) return [prefix, ...rel.split(sep2)].filter(Boolean).join("/");
7482
+ }
7483
+ return srcPath;
7484
+ }
7459
7485
  function createVideoSourceShipper(input) {
7460
7486
  const shipped = /* @__PURE__ */ new Map();
7461
7487
  return (srcPath) => {
@@ -7668,12 +7694,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7668
7694
  workCompiledDir: compiledDir,
7669
7695
  workVideoFramesDir: stagedVideoFrames
7670
7696
  });
7697
+ const sourceRoots = [
7698
+ [stagedVideoFrames, "video-frames"],
7699
+ [compiledDir, "compiled"],
7700
+ [projectDir, ""]
7701
+ ];
7671
7702
  const planVideosJson = buildPlanVideosJson({
7672
7703
  videos: composition.videos,
7673
7704
  compositionEnd: job.duration ?? Number.NaN,
7674
7705
  extracted: (extractResult.extractionResult?.extracted ?? []).map((ext) => ({
7675
7706
  videoId: ext.videoId,
7676
- srcPath: ext.srcPath,
7707
+ srcPath: stableSourcePath(sourceRoots, ext.srcPath),
7677
7708
  framePattern: ext.framePattern,
7678
7709
  fps: ext.fps,
7679
7710
  totalFrames: ext.totalFrames,
@@ -7724,6 +7755,13 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7724
7755
  if (audioResult.hasAudio && existsSync13(audioResult.audioOutputPath)) {
7725
7756
  renameSync3(audioResult.audioOutputPath, planAudioPath);
7726
7757
  }
7758
+ const chunksCarryAudio2 = carriesChunkAudio(config, options);
7759
+ if (chunksCarryAudio2 && existsSync13(planAudioPath)) {
7760
+ await fitPlanAudioToFrames(planAudioPath, workDir, totalFrames, config.fps, {
7761
+ signal: abortSignal,
7762
+ timeoutMs: cfg.ffmpegProcessTimeout
7763
+ });
7764
+ }
7727
7765
  const ffmpegVersion = await readFfmpegVersion();
7728
7766
  const producerVersion = readProducerVersion();
7729
7767
  const fontSnapshotSha = await readFontSnapshotSha();
@@ -7736,7 +7774,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7736
7774
  engineConfig: cfg,
7737
7775
  effectiveChunkSize,
7738
7776
  chunkCount,
7739
- runtimeEnv
7777
+ runtimeEnv,
7778
+ chunksCarryAudio: chunksCarryAudio2
7740
7779
  });
7741
7780
  const compositionJson = {
7742
7781
  durationSeconds: job.duration ?? 0,
@@ -8050,8 +8089,10 @@ function resolveExtractedVideoOutputDir(planDir, videoId) {
8050
8089
  }
8051
8090
  return outputDir;
8052
8091
  }
8053
- function artifactTargets(path, videoDependencies) {
8054
- if (isPlanAudioArtifactPath(path)) return { chunks: [], assembler: true };
8092
+ function artifactTargets(path, videoDependencies, chunksCarryAudio2) {
8093
+ if (isPlanAudioArtifactPath(path)) {
8094
+ return { chunks: chunksCarryAudio2 ? "all" : [], assembler: true };
8095
+ }
8055
8096
  if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
8056
8097
  return { chunks: "all", assembler: true };
8057
8098
  }
@@ -8273,9 +8314,11 @@ function buildPlanV2Publication(executionPlanDir, digests) {
8273
8314
  }
8274
8315
  validateExtractionCacheCompleteSentinels(executionPlanDir);
8275
8316
  const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
8317
+ const encoder = readJsonFile(join21(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
8318
+ const chunksCarryAudio2 = isRecord3(encoder) && encoder.chunksCarryAudio === true;
8276
8319
  for (const file of listFiles(executionPlanDir)) {
8277
8320
  if (isExtractionCacheCompleteSentinelPath(file.path)) continue;
8278
- const targets = artifactTargets(file.path, videoDependencyPlan.dependencies);
8321
+ const targets = artifactTargets(file.path, videoDependencyPlan.dependencies, chunksCarryAudio2);
8279
8322
  if ((file.path.startsWith("video-frames/") || file.path.startsWith("video-sources/")) && targets.chunks !== "all" && targets.chunks.length === 0 && !targets.assembler) {
8280
8323
  continue;
8281
8324
  }
@@ -8378,7 +8421,8 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
8378
8421
  listFiles(join21(stagingRoot, "compiled")).map(
8379
8422
  (file) => publishBlobFor(file.absolutePath, digests)
8380
8423
  )
8381
- )
8424
+ ),
8425
+ chunksCarryAudio: options.chunksCarryAudio
8382
8426
  });
8383
8427
  return await publishExecutionPlan(stagingRoot, publisher, { digests, log: config.logger });
8384
8428
  } catch (error) {
@@ -8701,6 +8745,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
8701
8745
  format,
8702
8746
  fps,
8703
8747
  fpsArg: fpsToFfmpegArg3(fps),
8748
+ chunkSeconds: chunksCarryAudio(planDir) ? plan2.chunks.map((chunk) => (chunk.endFrame - chunk.startFrame) * fps.den / fps.num) : null,
8704
8749
  signal: options?.abortSignal,
8705
8750
  timeout: resolveConfig4().ffmpegProcessTimeout,
8706
8751
  log,
@@ -8763,10 +8808,10 @@ function readAssemblePlan(planDir, chunkPaths) {
8763
8808
  throw new Error(`[assemble] chunk path does not exist: ${path}`);
8764
8809
  }
8765
8810
  }
8766
- return plan2;
8811
+ return { ...plan2, chunks };
8767
8812
  }
8768
8813
  function videoInputArgs(ctx, videoSources) {
8769
- return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
8814
+ return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg, ctx.chunkSeconds);
8770
8815
  }
8771
8816
  async function reencodeCfr(ctx, chunkPaths, encoder) {
8772
8817
  const cfrOutputPath = join22(ctx.workDir, `cfr.${ctx.format}`);
@@ -8846,10 +8891,15 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
8846
8891
  throw encoderFailureError("[assemble] audio mux failed", muxResult);
8847
8892
  }
8848
8893
  }
8849
- function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
8894
+ function concatDemuxerInput(chunkPaths, workDir, fpsArg, chunkSeconds) {
8850
8895
  const listPath = join22(workDir, "concat-list.txt");
8851
- const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
8852
- writeFileSync8(listPath, `${body}
8896
+ const head = chunkSeconds ? ["stream", "exact_stream_id 1"] : [];
8897
+ const body = chunkPaths.map((path, i) => {
8898
+ const line = `file '${path.replace(/'/g, "'\\''")}'`;
8899
+ return chunkSeconds ? `${line}
8900
+ duration ${chunkSeconds[i].toFixed(6)}` : line;
8901
+ });
8902
+ writeFileSync8(listPath, `${[...head, ...body].join("\n")}
8853
8903
  `, "utf-8");
8854
8904
  return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
8855
8905
  }
@@ -8872,6 +8922,11 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
8872
8922
  });
8873
8923
  }
8874
8924
  }
8925
+ function chunksCarryAudio(planDir) {
8926
+ const encoderJsonPath = join22(planDir, "meta", "encoder.json");
8927
+ if (!existsSync16(encoderJsonPath)) return false;
8928
+ return JSON.parse(readFileSync10(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
8929
+ }
8875
8930
  function readCfrEncoder(planDir, format) {
8876
8931
  if (format !== "mp4") {
8877
8932
  throw new Error(
@@ -8956,6 +9011,104 @@ import {
8956
9011
  readWebGlVendorInfoFromCanvas,
8957
9012
  resolveConfig as resolveConfig5
8958
9013
  } from "@hyperframes/engine";
9014
+
9015
+ // src/services/distributed/chunkAudio.ts
9016
+ import { appendRenderProvenanceArgs as appendRenderProvenanceArgs2, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
9017
+ function audioCutForChunk(grid, chunks, index, fps) {
9018
+ const firstPacketAt = (frame) => Math.min(
9019
+ grid.packetCount,
9020
+ Math.max(
9021
+ 0,
9022
+ Math.ceil(
9023
+ (frame * fps.den * grid.sampleRate - grid.firstStart * fps.num) / (grid.packetSamples * fps.num)
9024
+ )
9025
+ )
9026
+ );
9027
+ const first = index === 0 ? 0 : firstPacketAt(chunks[index].startFrame);
9028
+ const end = index === chunks.length - 1 ? grid.packetCount : firstPacketAt(chunks[index + 1].startFrame);
9029
+ if (end <= first) return null;
9030
+ const cutSeconds = (packet) => (grid.firstStart + packet * grid.packetSamples - grid.packetSamples / 2) / grid.sampleRate;
9031
+ const seekSeconds = index === 0 ? null : cutSeconds(first);
9032
+ const durationSeconds = end === grid.packetCount ? null : cutSeconds(end) - (seekSeconds ?? 0);
9033
+ return { seekSeconds, durationSeconds, firstPacket: first, packetCount: end - first };
9034
+ }
9035
+ async function probeAacPacketGrid(audioPath) {
9036
+ const probe = await runFfprobeJson([
9037
+ "-v",
9038
+ "error",
9039
+ "-select_streams",
9040
+ "a:0",
9041
+ "-show_entries",
9042
+ "stream=sample_rate,time_base,nb_frames:packet=pts,duration",
9043
+ "-read_intervals",
9044
+ "%+#1",
9045
+ "-of",
9046
+ "json",
9047
+ "--",
9048
+ audioPath
9049
+ ]);
9050
+ const stream = probe.streams?.[0] ?? {};
9051
+ const packet = probe.packets?.[0] ?? {};
9052
+ const sampleRate = Number(stream.sample_rate);
9053
+ const [tbNum = NaN, tbDen = NaN] = String(stream.time_base).split("/").map(Number);
9054
+ const toSamples = (ticks) => Number(ticks) * sampleRate * tbNum / tbDen;
9055
+ const grid = {
9056
+ sampleRate,
9057
+ firstStart: toSamples(packet.pts),
9058
+ packetSamples: toSamples(packet.duration),
9059
+ packetCount: Number(stream.nb_frames)
9060
+ };
9061
+ const counts = [grid.sampleRate, grid.packetSamples, grid.packetCount];
9062
+ if (!Number.isFinite(grid.firstStart) || !counts.every((n) => Number.isFinite(n) && n > 0)) {
9063
+ throw new Error(`[chunkAudio] could not read the AAC packet grid: ${JSON.stringify(probe)}`);
9064
+ }
9065
+ return grid;
9066
+ }
9067
+ function chunkMuxArgs(input, cut) {
9068
+ if (!cut) return ["-i", input.videoPath, "-map", "0:v:0"];
9069
+ const seek = cut.seekSeconds === null ? [] : ["-ss", cut.seekSeconds.toFixed(6)];
9070
+ const length = cut.durationSeconds === null ? [] : ["-t", cut.durationSeconds.toFixed(6)];
9071
+ const exact = seek.length ? ["-copypriorss", "0"] : [];
9072
+ return [
9073
+ ...["-i", input.videoPath, ...seek, ...length, "-i", input.audioPath],
9074
+ ...["-map", "0:v:0", "-map", "1:a:0", ...exact]
9075
+ ];
9076
+ }
9077
+ async function muxChunkWithAudio(input) {
9078
+ const grid = input.audioPath ? await probeAacPacketGrid(input.audioPath) : null;
9079
+ const cut = grid ? audioCutForChunk(grid, input.chunks, input.chunkIndex, input.fps) : null;
9080
+ const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
9081
+ appendRenderProvenanceArgs2(args, input.outputPath);
9082
+ args.push("-y", input.outputPath);
9083
+ const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
9084
+ if (!result.success) {
9085
+ throw new Error(
9086
+ `[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
9087
+ );
9088
+ }
9089
+ const slice = input.chunks[input.chunkIndex];
9090
+ const expected = { video: slice.endFrame - slice.startFrame };
9091
+ if (cut) expected.audio = cut.packetCount;
9092
+ const probe = await runFfprobeJson([
9093
+ "-v",
9094
+ "error",
9095
+ "-count_packets",
9096
+ "-show_entries",
9097
+ "stream=codec_type,nb_read_packets",
9098
+ "-of",
9099
+ "json",
9100
+ "--",
9101
+ input.outputPath
9102
+ ]);
9103
+ for (const [type, count] of Object.entries(expected)) {
9104
+ const packets = Number(probe.streams?.find((s) => s.codec_type === type)?.nb_read_packets ?? 0);
9105
+ if (packets !== count) {
9106
+ throw new Error(`[chunkAudio] ${type} packets in chunk ${packets}, expected ${count}`);
9107
+ }
9108
+ }
9109
+ }
9110
+
9111
+ // src/services/distributed/renderChunk.ts
8959
9112
  import { readWebGlVendorInfoFromCanvas as readWebGlVendorInfoFromCanvas2 } from "@hyperframes/engine";
8960
9113
  var FFMPEG_VERSION_MISMATCH = "FFMPEG_VERSION_MISMATCH";
8961
9114
  var PLAN_HASH_MISMATCH = "PLAN_HASH_MISMATCH";
@@ -9505,7 +9658,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9505
9658
  const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
9506
9659
  const effectiveQuality = encoder.crf ?? preset.quality;
9507
9660
  const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
9508
- const videoOnlyPath = outputChunkPath;
9661
+ const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
9509
9662
  if (isPngSequence) {
9510
9663
  if (!existsSync17(outputChunkPath)) mkdirSync12(outputChunkPath, { recursive: true });
9511
9664
  } else {
@@ -9516,16 +9669,16 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9516
9669
  await runEncodeStage({
9517
9670
  job,
9518
9671
  log,
9519
- outputPath: outputChunkPath,
9672
+ outputPath: videoOnlyPath,
9520
9673
  framesDir,
9521
9674
  videoOnlyPath,
9522
9675
  width: Math.round(plan2.dimensions.width * encoder.deviceScaleFactor),
9523
9676
  height: Math.round(plan2.dimensions.height * encoder.deviceScaleFactor),
9524
9677
  needsAlpha: plan2.dimensions.format !== "mp4",
9525
9678
  captureImageFormat: captureOptions.format ?? "jpeg",
9526
- // Each chunk produces video only — audio is muxed once at assemble
9527
- // time. Suppressing `hasAudio` skips the png-sequence audio sidecar
9528
- // AND the mp4 audio mux.
9679
+ // The encoder writes video only; audio joins at assemble time or in the
9680
+ // chunk that carries audio. Suppressing `hasAudio` skips the png-sequence audio
9681
+ // sidecar AND the mp4 audio mux.
9529
9682
  hasAudio: false,
9530
9683
  isPngSequence,
9531
9684
  // `DistributedFormat` has no "gif" member — distributed chunks are
@@ -9552,6 +9705,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9552
9705
  lockGopForChunkConcat: !isPngSequence,
9553
9706
  gopSize: framesInChunk
9554
9707
  });
9708
+ if (encoder.chunksCarryAudio) {
9709
+ await muxChunkWithAudio({
9710
+ videoPath: videoOnlyPath,
9711
+ audioPath: resolvePlanAudioPath(planDir),
9712
+ outputPath: outputChunkPath,
9713
+ chunks,
9714
+ chunkIndex,
9715
+ fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
9716
+ timeoutMs: cfg.ffmpegProcessTimeout
9717
+ });
9718
+ rmSync11(videoOnlyPath, { force: true });
9719
+ }
9555
9720
  encodeStageMs = Date.now() - encodeStarted;
9556
9721
  } finally {
9557
9722
  if (session) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "buildId": "dev",
4
- "sha256": "0a18eb36f911ced8c6c122593a67e2fbd9506c09c48001db53c85951cb440aba",
4
+ "sha256": "4a56ffcd5e2b0d5c22cce6c2201205b85cb77ad9d7e32796c2e789fa46231935",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"