@hyperframes/producer 0.8.173 → 0.8.175

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.
@@ -210,11 +210,11 @@ function getHfEarlyStub() {
210
210
 
211
211
  // src/services/fileServer.ts
212
212
  var readFileAsync = promisify(readFile);
213
- function isPathInside(child, parent, options = {}) {
213
+ function isPathInside(child2, parent, options = {}) {
214
214
  const { resolveSymlinks = false, pathModule } = options;
215
215
  const resolveFn = pathModule?.resolve ?? resolve2;
216
216
  const separator = pathModule?.sep ?? sep;
217
- const resolvedChild = resolveFn(child);
217
+ const resolvedChild = resolveFn(child2);
218
218
  const resolvedParent = resolveFn(parent);
219
219
  const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
220
220
  const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
@@ -3115,7 +3115,7 @@ async function embedLocalFontFaces(html, projectDir) {
3115
3115
  embeddedPaths.add(localPath);
3116
3116
  continue;
3117
3117
  }
3118
- dataUri = await toDataUri(font.buffer, ext);
3118
+ dataUri = await toDataUri(font.buffer, ext, { compressToWoff2: false });
3119
3119
  dataUriByAbsolutePath.set(absPath, dataUri);
3120
3120
  defaultLogger.info(
3121
3121
  `[Compiler] Embedded local font file: ${localPath} (${(font.buffer.length / 1024).toFixed(0)} KB \u2192 data URI)`
@@ -4558,7 +4558,7 @@ var VideoExtractionStageError = class extends Error {
4558
4558
  affectedElementCount: failure.count
4559
4559
  }))
4560
4560
  )) {
4561
- const total = failures.reduce((sum, failure) => sum + failure.count, 0);
4561
+ const total = failures.reduce((sum2, failure) => sum2 + failure.count, 0);
4562
4562
  const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
4563
4563
  super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]`);
4564
4564
  this.code = code;
@@ -5822,6 +5822,16 @@ async function padOrTrimAudioToVideoFrameCount(input) {
5822
5822
  ...loweredDb > 0 ? { audioLoweredDb: loweredDb } : {}
5823
5823
  };
5824
5824
  }
5825
+ async function constrainAacTruePeak(input) {
5826
+ const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
5827
+ return enforceAacTruePeak({
5828
+ audioPath: input.audioPath,
5829
+ targetDurationSeconds: input.targetDurationSeconds,
5830
+ signal: input.signal,
5831
+ probeTruePeak: (audioPath, signal) => defaultProbeAudioTruePeakDbfs(audioPath, timeoutMs, signal),
5832
+ runner: (args) => defaultRunFfmpeg(args, timeoutMs, input.signal)
5833
+ });
5834
+ }
5825
5835
  async function enforceAacTruePeak(input) {
5826
5836
  let scratchDir;
5827
5837
  try {
@@ -6170,7 +6180,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
6170
6180
  return captured;
6171
6181
  }
6172
6182
  function countFrameRanges(ranges) {
6173
- return ranges.reduce((sum, range) => sum + (range.endFrame - range.startFrame), 0);
6183
+ return ranges.reduce((sum2, range) => sum2 + (range.endFrame - range.startFrame), 0);
6174
6184
  }
6175
6185
  async function executeDiskCaptureWithAdaptiveRetry(options) {
6176
6186
  const attempts = [];
@@ -7207,15 +7217,15 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
7207
7217
  function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger) {
7208
7218
  if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
7209
7219
  const { workerTimeoutSeconds, workerConcurrency } = config;
7210
- const pinsChunking = config.chunkSize !== void 0 || config.targetChunkFrames !== void 0;
7211
- if (pinsChunking || workerTimeoutSeconds === void 0 && workerConcurrency === void 0) {
7212
- return DEFAULT_MAX_PARALLEL_CHUNKS;
7213
- }
7214
- const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
7215
7220
  const wave = Math.min(
7216
7221
  workerConcurrency ?? DEFAULT_MAX_PARALLEL_CHUNKS,
7217
7222
  MAX_PARALLEL_CHUNKS_CEILING
7218
7223
  );
7224
+ const pinsChunking = config.chunkSize !== void 0 || config.targetChunkFrames !== void 0;
7225
+ if (pinsChunking || workerTimeoutSeconds === void 0 && workerConcurrency === void 0) {
7226
+ return wave;
7227
+ }
7228
+ const secondsPerFrame = ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME * megapixels;
7219
7229
  const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
7220
7230
  const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
7221
7231
  if (workerTimeoutSeconds === void 0) return wide;
@@ -7232,7 +7242,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
7232
7242
  }
7233
7243
  if (needed > wave) {
7234
7244
  log.warn(
7235
- `[plan] ${needed} chunks are needed to keep each under the ${workerTimeoutSeconds} s worker time limit, but the plan assumes ${wave} workers at once, so the render runs in ${Math.ceil(needed / wave)} waves. Raise --concurrency to ${needed} to render in one wave (Lambda: hyperframes lambda deploy --concurrency ${needed}).`,
7245
+ `[plan] ${needed} chunks are needed to keep each under the ${workerTimeoutSeconds} s worker time limit, but the plan assumes ${wave} workers at once, so the render runs in ${Math.ceil(needed / wave)} waves. Raise --concurrency to ${needed} to render in one wave (Lambda: hyperframes lambda deploy --concurrency ${needed}, or a higher account concurrency quota).`,
7236
7246
  { needed, workerConcurrency: wave, workerTimeoutSeconds }
7237
7247
  );
7238
7248
  }
@@ -7407,21 +7417,6 @@ function buildLockedRenderConfig(input) {
7407
7417
  function carriesChunkAudio(config, options) {
7408
7418
  return options.chunksCarryAudio === true && config.format === "mp4" && config.cfr !== true;
7409
7419
  }
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
- }
7425
7420
  function resolveEncoderTriple(config) {
7426
7421
  if (config.format === "mp4") {
7427
7422
  return resolveMp4EncoderTriple(config.codec);
@@ -7719,13 +7714,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7719
7714
  "utf-8"
7720
7715
  );
7721
7716
  const digests = options.digests ?? /* @__PURE__ */ new Map();
7717
+ const chunksCarryAudio2 = carriesChunkAudio(config, options);
7718
+ const audioSeconds = chunksCarryAudio2 ? totalFrames / config.fps : probeResult.duration;
7722
7719
  stageStarted = Date.now();
7723
7720
  const [audioOutcome, overlapOutcome] = await Promise.allSettled([
7724
7721
  runAudioStage({
7725
7722
  projectDir,
7726
7723
  workDir,
7727
7724
  compiledDir: finalCompiledDir,
7728
- duration: probeResult.duration,
7725
+ duration: audioSeconds,
7729
7726
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout,
7730
7727
  audioGain: cfg.audioGain,
7731
7728
  audios: composition.audios,
@@ -7755,12 +7752,16 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7755
7752
  if (audioResult.hasAudio && existsSync13(audioResult.audioOutputPath)) {
7756
7753
  renameSync3(audioResult.audioOutputPath, planAudioPath);
7757
7754
  }
7758
- const chunksCarryAudio2 = carriesChunkAudio(config, options);
7759
7755
  if (chunksCarryAudio2 && existsSync13(planAudioPath)) {
7760
- await fitPlanAudioToFrames(planAudioPath, workDir, totalFrames, config.fps, {
7756
+ stageStarted = Date.now();
7757
+ const peak = await constrainAacTruePeak({
7758
+ audioPath: planAudioPath,
7759
+ targetDurationSeconds: audioSeconds,
7761
7760
  signal: abortSignal,
7762
7761
  timeoutMs: cfg.ffmpegProcessTimeout
7763
7762
  });
7763
+ if (!peak.success) throw encoderFailureError("[plan] audio true-peak check failed", peak);
7764
+ logPlanStage(log, "audio-peak", stageStarted);
7764
7765
  }
7765
7766
  const ffmpegVersion = await readFfmpegVersion();
7766
7767
  const producerVersion = readProducerVersion();
@@ -7845,8 +7846,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
7845
7846
  producerVersion
7846
7847
  };
7847
7848
  }
7848
- async function plan(projectDir, config, planDir) {
7849
- const executionPlan = await buildLocalExecutionPlan(projectDir, config, planDir);
7849
+ async function plan(projectDir, config, planDir, options = {}) {
7850
+ const executionPlan = await buildLocalExecutionPlan(projectDir, config, planDir, options);
7850
7851
  return {
7851
7852
  planDir: executionPlan.executionPlanDir,
7852
7853
  planProtocol: PLAN_PROTOCOL_V1,
@@ -8091,7 +8092,7 @@ function resolveExtractedVideoOutputDir(planDir, videoId) {
8091
8092
  }
8092
8093
  function artifactTargets(path, videoDependencies, chunksCarryAudio2) {
8093
8094
  if (isPlanAudioArtifactPath(path)) {
8094
- return { chunks: chunksCarryAudio2 ? "all" : [], assembler: true };
8095
+ return chunksCarryAudio2 ? { chunks: "all", assembler: false } : { chunks: [], assembler: true };
8095
8096
  }
8096
8097
  if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
8097
8098
  return { chunks: "all", assembler: true };
@@ -8652,7 +8653,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir) {
8652
8653
  planHash: manifest.planHash,
8653
8654
  sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
8654
8655
  artifactCount: artifacts.length,
8655
- sizeBytes: artifacts.reduce((sum, artifact) => sum + artifact.sizeBytes, 0),
8656
+ sizeBytes: artifacts.reduce((sum2, artifact) => sum2 + artifact.sizeBytes, 0),
8656
8657
  // Join the artifact's OWN name, not the current constant: a plan written
8657
8658
  // before the container change carries the legacy name, and materializing it
8658
8659
  // under the new one would point at a file that was never written.
@@ -8689,24 +8690,24 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
8689
8690
  }
8690
8691
 
8691
8692
  // src/services/distributed/planV2Execution.ts
8692
- import { mkdtempSync as mkdtempSync6, rmSync as rmSync12 } from "node:fs";
8693
+ import { mkdtempSync as mkdtempSync7, rmSync as rmSync13 } from "node:fs";
8693
8694
  import { tmpdir as tmpdir2 } from "node:os";
8694
- import { join as join24 } from "node:path";
8695
+ import { join as join25 } from "node:path";
8695
8696
 
8696
8697
  // src/services/distributed/assemble.ts
8697
8698
  import {
8698
8699
  cpSync as cpSync3,
8699
8700
  existsSync as existsSync16,
8700
- mkdirSync as mkdirSync11,
8701
- mkdtempSync as mkdtempSync5,
8701
+ mkdirSync as mkdirSync12,
8702
+ mkdtempSync as mkdtempSync6,
8702
8703
  readFileSync as readFileSync10,
8703
8704
  readdirSync as readdirSync7,
8704
- renameSync as renameSync6,
8705
- rmSync as rmSync10,
8705
+ renameSync as renameSync7,
8706
+ rmSync as rmSync11,
8706
8707
  statSync as statSync7,
8707
8708
  writeFileSync as writeFileSync8
8708
8709
  } from "node:fs";
8709
- import { dirname as dirname11, join as join22 } from "node:path";
8710
+ import { dirname as dirname12, join as join23 } from "node:path";
8710
8711
  import {
8711
8712
  appendRenderProvenanceArgs,
8712
8713
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -8717,6 +8718,353 @@ import {
8717
8718
  runFfmpeg as runFfmpeg5
8718
8719
  } from "@hyperframes/engine";
8719
8720
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
8721
+
8722
+ // src/services/distributed/mp4Stitch.ts
8723
+ import {
8724
+ createReadStream as createReadStream4,
8725
+ createWriteStream,
8726
+ mkdirSync as mkdirSync11,
8727
+ mkdtempSync as mkdtempSync5,
8728
+ renameSync as renameSync6,
8729
+ rmSync as rmSync10
8730
+ } from "node:fs";
8731
+ import { open } from "node:fs/promises";
8732
+ import { dirname as dirname11, join as join22 } from "node:path";
8733
+ import { finished, pipeline } from "node:stream/promises";
8734
+ function boxAt(src, off, at, limit) {
8735
+ let size = src.readUInt32BE(off);
8736
+ let bodyStart = at + 8;
8737
+ if (size === 1) {
8738
+ size = Number(src.readBigUInt64BE(off + 8));
8739
+ bodyStart = at + 16;
8740
+ }
8741
+ if (size < 8 || at + size > limit) throw new Error(`[mp4Stitch] malformed box at ${at}`);
8742
+ return { type: src.toString("latin1", off + 4, off + 8), start: at, bodyStart, end: at + size };
8743
+ }
8744
+ function childBoxes(buf, parent) {
8745
+ const boxes = [];
8746
+ for (let at = parent.bodyStart; at < parent.end; at = boxes.at(-1).end) {
8747
+ boxes.push(boxAt(buf, at, at, parent.end));
8748
+ }
8749
+ return boxes;
8750
+ }
8751
+ function findChild(buf, parent, type) {
8752
+ return childBoxes(buf, parent).find((box2) => box2.type === type);
8753
+ }
8754
+ function child(buf, parent, type) {
8755
+ const found = findChild(buf, parent, type);
8756
+ if (!found) throw new Error(`[mp4Stitch] ${parent.type} has no ${type}`);
8757
+ return found;
8758
+ }
8759
+ var SCAN_BYTES = 64 * 1024;
8760
+ async function readMp4Layout(read) {
8761
+ const found = /* @__PURE__ */ new Map();
8762
+ let payload;
8763
+ let buf = Buffer.alloc(0);
8764
+ let bufAt = 0;
8765
+ const load = async (start, end) => {
8766
+ if (start >= bufAt && end <= bufAt + buf.length) return;
8767
+ buf = await read(start, Math.max(end, start + SCAN_BYTES));
8768
+ bufAt = start;
8769
+ };
8770
+ for (let at = 0; !(found.has("ftyp") && found.has("moov") && payload); ) {
8771
+ await load(at, at + 16);
8772
+ if (at + 8 > bufAt + buf.length) {
8773
+ throw new Error(`[mp4Stitch] chunk needs ftyp, moov and mdat; found ${[...found.keys()]}`);
8774
+ }
8775
+ const box2 = boxAt(buf, at - bufAt, at, Number.MAX_SAFE_INTEGER);
8776
+ if (box2.type === "mdat") payload ??= box2;
8777
+ if (box2.type === "ftyp" || box2.type === "moov") {
8778
+ await load(at, box2.end);
8779
+ found.set(box2.type, buf.subarray(at - bufAt, box2.end - bufAt));
8780
+ }
8781
+ at = box2.end;
8782
+ }
8783
+ const [ftyp, moov] = [found.get("ftyp"), found.get("moov")];
8784
+ if (moov.length !== moovSize(moov)) throw new Error("[mp4Stitch] moov is cut off");
8785
+ return { ftyp, moov, payloadStart: payload.bodyStart, payloadEnd: payload.end };
8786
+ }
8787
+ var moovSize = (moov) => boxAt(moov, 0, 0, Number.MAX_SAFE_INTEGER).end;
8788
+ function readTracks(moov) {
8789
+ const root = boxAt(moov, 0, 0, moov.length);
8790
+ return childBoxes(moov, root).filter((box2) => box2.type === "trak").map((trak) => readTrack(moov, trak));
8791
+ }
8792
+ function rows(buf, table, columns, countAt = 4) {
8793
+ const count = buf.readUInt32BE(table.bodyStart + countAt);
8794
+ const first = table.bodyStart + countAt + 4;
8795
+ return Array.from(
8796
+ { length: count },
8797
+ (_, row) => Array.from(
8798
+ { length: columns },
8799
+ (_2, col) => buf.readUInt32BE(first + (row * columns + col) * 4)
8800
+ )
8801
+ );
8802
+ }
8803
+ function readMediaTime(moov, trak) {
8804
+ const edts = findChild(moov, trak, "edts");
8805
+ if (!edts) return 0;
8806
+ const elst = child(moov, edts, "elst");
8807
+ const wide = moov[elst.bodyStart] === 1;
8808
+ const entries = moov.readUInt32BE(elst.bodyStart + 4);
8809
+ for (let i = 0, at = elst.bodyStart + 8; i < entries; i++, at += wide ? 20 : 12) {
8810
+ const offset = at + (wide ? 8 : 4);
8811
+ const mediaTime = wide ? Number(moov.readBigInt64BE(offset)) : moov.readInt32BE(offset);
8812
+ if (mediaTime !== -1) return mediaTime;
8813
+ }
8814
+ return 0;
8815
+ }
8816
+ function readSizes(moov, stsz) {
8817
+ const fixedSize = moov.readUInt32BE(stsz.bodyStart + 4);
8818
+ if (!fixedSize) return rows(moov, stsz, 1, 8).map(([size]) => size);
8819
+ return Array(moov.readUInt32BE(stsz.bodyStart + 8)).fill(fixedSize);
8820
+ }
8821
+ function readSync2(moov, stbl, sampleCount) {
8822
+ const stss = findChild(moov, stbl, "stss");
8823
+ if (!stss) return null;
8824
+ const sync = Array(sampleCount).fill(false);
8825
+ for (const [n] of rows(moov, stss, 1)) sync[n - 1] = true;
8826
+ return sync;
8827
+ }
8828
+ function readChunkOffsets(moov, stbl) {
8829
+ const stco = findChild(moov, stbl, "stco");
8830
+ if (stco) return rows(moov, stco, 1).map(([offset]) => offset);
8831
+ return rows(moov, child(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
8832
+ }
8833
+ function readSamplesPerChunk(moov, stbl, chunkCount) {
8834
+ const stsc = rows(moov, child(moov, stbl, "stsc"), 3);
8835
+ return stsc.flatMap(([firstChunk, perChunk, description], i) => {
8836
+ if (description !== 1) throw new Error("[mp4Stitch] chunk uses more than one sample entry");
8837
+ const nextFirst = stsc[i + 1]?.[0] ?? chunkCount + 1;
8838
+ return Array(nextFirst - firstChunk).fill(perChunk);
8839
+ });
8840
+ }
8841
+ function readTrack(moov, trak) {
8842
+ const mdia = child(moov, trak, "mdia");
8843
+ const hdlr = child(moov, mdia, "hdlr");
8844
+ const stbl = child(moov, child(moov, mdia, "minf"), "stbl");
8845
+ if (findChild(moov, stbl, "ctts")) throw new Error("[mp4Stitch] chunk has ctts (B-frames)");
8846
+ const stts = rows(moov, child(moov, stbl, "stts"), 2);
8847
+ const durations = stts.flatMap(([n, delta]) => Array(n).fill(delta));
8848
+ const sizes = readSizes(moov, child(moov, stbl, "stsz"));
8849
+ const chunkOffsets = readChunkOffsets(moov, stbl);
8850
+ const samplesPerChunk = readSamplesPerChunk(moov, stbl, chunkOffsets.length);
8851
+ if (durations.length !== sizes.length || sum(samplesPerChunk) !== sizes.length) {
8852
+ throw new Error("[mp4Stitch] sample tables disagree on the sample count");
8853
+ }
8854
+ const stsd = child(moov, stbl, "stsd");
8855
+ return {
8856
+ trak,
8857
+ handler: moov.toString("latin1", hdlr.bodyStart + 8, hdlr.bodyStart + 12),
8858
+ timescale: timescaleOf(moov, child(moov, mdia, "mdhd")),
8859
+ mediaTime: readMediaTime(moov, trak),
8860
+ stsd: moov.subarray(stsd.start, stsd.end),
8861
+ durations,
8862
+ sizes,
8863
+ sync: readSync2(moov, stbl, sizes.length),
8864
+ samplesPerChunk,
8865
+ chunkOffsets
8866
+ };
8867
+ }
8868
+ function sum(values) {
8869
+ return values.reduce((total, value) => total + value, 0);
8870
+ }
8871
+ function box(type, ...body) {
8872
+ const header = Buffer.alloc(8);
8873
+ header.writeUInt32BE(8 + sum(body.map((part) => part.length)));
8874
+ header.write(type, 4, "latin1");
8875
+ return Buffer.concat([header, ...body]);
8876
+ }
8877
+ function fullBox(type, version, body) {
8878
+ return box(type, Buffer.from([version, 0, 0, 0]), body);
8879
+ }
8880
+ function u32s(values) {
8881
+ const buf = Buffer.alloc(values.length * 4);
8882
+ values.forEach((value, i) => buf.writeUInt32BE(value, i * 4));
8883
+ return buf;
8884
+ }
8885
+ function runs(values) {
8886
+ const out = [];
8887
+ for (const value of values) {
8888
+ const last = out.at(-1);
8889
+ if (last && last[1] === value) last[0]++;
8890
+ else out.push([1, value]);
8891
+ }
8892
+ return out;
8893
+ }
8894
+ function rebuild(buf, parent, replace) {
8895
+ const parts = childBoxes(buf, parent).map(
8896
+ (box2) => replace[box2.type]?.(box2) ?? buf.subarray(box2.start, box2.end)
8897
+ );
8898
+ return box(parent.type, ...parts);
8899
+ }
8900
+ var DURATION_AT = {
8901
+ mvhd: [16, 24],
8902
+ mdhd: [16, 24],
8903
+ tkhd: [20, 28]
8904
+ };
8905
+ function withDuration(buf, target, duration) {
8906
+ const copy = Buffer.from(buf.subarray(target.start, target.end));
8907
+ const body = target.bodyStart - target.start;
8908
+ const wide = copy[body] === 1;
8909
+ const at = body + DURATION_AT[target.type][wide ? 1 : 0];
8910
+ if (wide) copy.writeBigUInt64BE(BigInt(duration), at);
8911
+ else if (duration <= 4294967295) copy.writeUInt32BE(duration, at);
8912
+ else throw new Error(`[mp4Stitch] ${target.type} duration ${duration} needs a version 1 box`);
8913
+ return copy;
8914
+ }
8915
+ function timescaleOf(buf, header) {
8916
+ return buf.readUInt32BE(header.bodyStart + (buf[header.bodyStart] === 1 ? 20 : 12));
8917
+ }
8918
+ function buildStbl(stsd, track) {
8919
+ const stts = runs(track.durations);
8920
+ const stsc = [];
8921
+ let firstChunk = 1;
8922
+ for (const [count, perChunk] of runs(track.samplesPerChunk)) {
8923
+ stsc.push([firstChunk, perChunk, 1]);
8924
+ firstChunk += count;
8925
+ }
8926
+ const co64 = Buffer.alloc(4 + track.chunkOffsets.length * 8);
8927
+ co64.writeUInt32BE(track.chunkOffsets.length);
8928
+ track.chunkOffsets.forEach((offset, i) => co64.writeBigUInt64BE(BigInt(offset), 4 + i * 8));
8929
+ const syncSamples = track.sync?.flatMap((isSync, i) => isSync ? [i + 1] : []) ?? null;
8930
+ return box(
8931
+ "stbl",
8932
+ stsd,
8933
+ fullBox("stts", 0, u32s([stts.length, ...stts.flat()])),
8934
+ ...syncSamples ? [fullBox("stss", 0, u32s([syncSamples.length, ...syncSamples]))] : [],
8935
+ fullBox("stsc", 0, u32s([stsc.length, ...stsc.flat()])),
8936
+ fullBox("stsz", 0, u32s([0, track.sizes.length, ...track.sizes])),
8937
+ fullBox("co64", 0, co64)
8938
+ );
8939
+ }
8940
+ var codecOf = (track) => track.stsd.toString("latin1", 20, 24);
8941
+ var sameTrack = (track, plan2) => track.handler === plan2.handler && track.timescale === plan2.timescale && codecOf(track) === codecOf(plan2);
8942
+ function planTracksOf(tracks, plan2, index) {
8943
+ let next = 0;
8944
+ const matched = plan2.map((planTrack) => {
8945
+ const track = tracks[next];
8946
+ if (track && sameTrack(track, planTrack)) return tracks[next++];
8947
+ if (planTrack.handler === "soun") return null;
8948
+ throw new Error(`[mp4Stitch] chunk ${index} tracks differ from chunk 0`);
8949
+ });
8950
+ if (next !== tracks.length)
8951
+ throw new Error(`[mp4Stitch] chunk ${index} tracks differ from chunk 0`);
8952
+ for (const track of tracks) {
8953
+ if (track.handler === "vide" && track.mediaTime !== 0) {
8954
+ throw new Error(`[mp4Stitch] chunk ${index} video starts at media time ${track.mediaTime}`);
8955
+ }
8956
+ }
8957
+ return matched;
8958
+ }
8959
+ function buildStitchedHeader(layouts) {
8960
+ const plan2 = readTracks(layouts[0].moov);
8961
+ const chunks = layouts.map((layout, i) => planTracksOf(readTracks(layout.moov), plan2, i));
8962
+ const payloadBases = [];
8963
+ let payloadBytes = 0;
8964
+ for (const layout of layouts) {
8965
+ payloadBases.push(payloadBytes);
8966
+ payloadBytes += layout.payloadEnd - layout.payloadStart;
8967
+ }
8968
+ const template = layouts[0].moov;
8969
+ const root = boxAt(template, 0, 0, template.length);
8970
+ const movieTimescale = timescaleOf(template, child(template, root, "mvhd"));
8971
+ const buildMoov = (dataStart2) => {
8972
+ const traks = plan2.map((track, t) => {
8973
+ const parts = chunks.flatMap((tracks, chunk) => {
8974
+ const part = tracks[t];
8975
+ return part ? [{ ...part, chunk }] : [];
8976
+ });
8977
+ const merged = {
8978
+ durations: parts.flatMap((part) => part.durations),
8979
+ sizes: parts.flatMap((part) => part.sizes),
8980
+ sync: parts.every((part) => !part.sync) ? null : parts.flatMap((part) => part.sync ?? part.sizes.map(() => true)),
8981
+ samplesPerChunk: parts.flatMap((part) => part.samplesPerChunk),
8982
+ chunkOffsets: parts.flatMap(
8983
+ (part) => part.chunkOffsets.map((offset) => {
8984
+ const i = part.chunk;
8985
+ const { payloadStart, payloadEnd } = layouts[i];
8986
+ if (offset < payloadStart || offset >= payloadEnd) {
8987
+ throw new Error(`[mp4Stitch] chunk ${i} has samples outside its mdat`);
8988
+ }
8989
+ return offset - payloadStart + payloadBases[i] + dataStart2;
8990
+ })
8991
+ )
8992
+ };
8993
+ const mediaDuration = sum(merged.durations);
8994
+ const shown = Math.round(
8995
+ (mediaDuration - track.mediaTime) * movieTimescale / track.timescale
8996
+ );
8997
+ const elst = Buffer.alloc(24);
8998
+ elst.writeUInt32BE(1);
8999
+ elst.writeBigUInt64BE(BigInt(shown), 4);
9000
+ elst.writeBigInt64BE(BigInt(track.mediaTime), 12);
9001
+ elst.writeUInt32BE(65536, 20);
9002
+ const edts = track.mediaTime ? box("edts", fullBox("elst", 1, elst)) : Buffer.alloc(0);
9003
+ const trak = rebuild(template, track.trak, {
9004
+ tkhd: (tkhd) => Buffer.concat([withDuration(template, tkhd, shown), edts]),
9005
+ edts: () => Buffer.alloc(0),
9006
+ mdia: (mdia) => rebuild(template, mdia, {
9007
+ mdhd: (mdhd) => withDuration(template, mdhd, mediaDuration),
9008
+ minf: (minf) => rebuild(template, minf, { stbl: () => buildStbl(track.stsd, merged) })
9009
+ })
9010
+ });
9011
+ return { trak, shown };
9012
+ });
9013
+ let next = 0;
9014
+ return rebuild(template, root, {
9015
+ mvhd: (mvhd) => withDuration(template, mvhd, Math.max(...traks.map((t) => t.shown))),
9016
+ trak: () => traks[next++].trak
9017
+ });
9018
+ };
9019
+ const ftyp = layouts[0].ftyp;
9020
+ const dataStart = ftyp.length + buildMoov(0).length + 16;
9021
+ const mdat = Buffer.alloc(16);
9022
+ mdat.writeUInt32BE(1);
9023
+ mdat.write("mdat", 4, "latin1");
9024
+ mdat.writeBigUInt64BE(BigInt(16 + payloadBytes), 8);
9025
+ return Buffer.concat([ftyp, buildMoov(dataStart), mdat]);
9026
+ }
9027
+ function fileRange(path) {
9028
+ return async (start, end) => {
9029
+ const file = await open(path, "r", 384);
9030
+ try {
9031
+ const buf = Buffer.alloc(end - start);
9032
+ const { bytesRead } = await file.read(buf, 0, buf.length, start);
9033
+ return buf.subarray(0, bytesRead);
9034
+ } finally {
9035
+ await file.close();
9036
+ }
9037
+ };
9038
+ }
9039
+ async function copyPayload(path, layout, index, out) {
9040
+ const { payloadStart, payloadEnd } = layout;
9041
+ const payload = createReadStream4(path, { start: payloadStart, end: payloadEnd - 1 });
9042
+ await pipeline(payload, out, { end: false });
9043
+ if (payload.bytesRead !== payloadEnd - payloadStart) {
9044
+ throw new Error(
9045
+ `[mp4Stitch] chunk ${index} mdat holds ${payload.bytesRead} of ${payloadEnd - payloadStart} payload bytes`
9046
+ );
9047
+ }
9048
+ }
9049
+ async function writeStitchedMp4(chunkPaths, outputPath) {
9050
+ const layouts = await Promise.all(chunkPaths.map((path) => readMp4Layout(fileRange(path))));
9051
+ const header = buildStitchedHeader(layouts);
9052
+ mkdirSync11(dirname11(outputPath), { recursive: true });
9053
+ const workDir = mkdtempSync5(`${outputPath}.stitch-`);
9054
+ const out = createWriteStream(join22(workDir, "out.mp4"));
9055
+ try {
9056
+ out.write(header);
9057
+ for (const [i, path] of chunkPaths.entries()) await copyPayload(path, layouts[i], i, out);
9058
+ out.end();
9059
+ await finished(out);
9060
+ renameSync6(join22(workDir, "out.mp4"), outputPath);
9061
+ } finally {
9062
+ out.destroy();
9063
+ rmSync10(workDir, { recursive: true, force: true });
9064
+ }
9065
+ }
9066
+
9067
+ // src/services/distributed/assemble.ts
8720
9068
  function cfrRateControlArgs(locked) {
8721
9069
  const preset = getEncoderPreset5(locked.quality ?? "standard", "mp4");
8722
9070
  if (locked.crf == null && locked.bitrate) {
@@ -8732,12 +9080,21 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
8732
9080
  if (plan2.dimensions.format === "png-sequence") {
8733
9081
  return mergePngFrameDirs(chunkPaths, outputPath, plan2.totalFrames, audioPath, start);
8734
9082
  }
9083
+ if (chunksAreStitchedMp4(planDir, cfr)) {
9084
+ await writeStitchedMp4(chunkPaths, outputPath);
9085
+ return {
9086
+ outputPath,
9087
+ durationMs: Date.now() - start,
9088
+ framesEncoded: plan2.totalFrames,
9089
+ fileSize: statSync7(outputPath).size
9090
+ };
9091
+ }
8735
9092
  const format = plan2.dimensions.format;
8736
9093
  const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
8737
- if (!existsSync16(dirname11(outputPath))) {
8738
- mkdirSync11(dirname11(outputPath), { recursive: true });
9094
+ if (!existsSync16(dirname12(outputPath))) {
9095
+ mkdirSync12(dirname12(outputPath), { recursive: true });
8739
9096
  }
8740
- const workDir = mkdtempSync5(`${outputPath}.assemble-work-`);
9097
+ const workDir = mkdtempSync6(`${outputPath}.assemble-work-`);
8741
9098
  const fps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
8742
9099
  const stageMs = {};
8743
9100
  const ctx = {
@@ -8745,7 +9102,6 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
8745
9102
  format,
8746
9103
  fps,
8747
9104
  fpsArg: fpsToFfmpegArg3(fps),
8748
- chunkSeconds: chunksCarryAudio(planDir) ? plan2.chunks.map((chunk) => (chunk.endFrame - chunk.startFrame) * fps.den / fps.num) : null,
8749
9105
  signal: options?.abortSignal,
8750
9106
  timeout: resolveConfig4().ffmpegProcessTimeout,
8751
9107
  log,
@@ -8758,10 +9114,10 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
8758
9114
  };
8759
9115
  try {
8760
9116
  const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
8761
- const finalPath = join22(workDir, `output.${format}`);
9117
+ const finalPath = join23(workDir, `output.${format}`);
8762
9118
  const audio = audioPath !== null && existsSync16(audioPath) ? audioPath : null;
8763
9119
  await writeJoined(ctx, videoSources, audio, finalPath);
8764
- renameSync6(finalPath, outputPath);
9120
+ renameSync7(finalPath, outputPath);
8765
9121
  log.info("[assemble] stage timings", {
8766
9122
  ...stageMs,
8767
9123
  chunks: chunkPaths.length,
@@ -8769,7 +9125,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
8769
9125
  });
8770
9126
  } finally {
8771
9127
  try {
8772
- rmSync10(workDir, { recursive: true, force: true });
9128
+ rmSync11(workDir, { recursive: true, force: true });
8773
9129
  } catch (err) {
8774
9130
  log.warn("[assemble] failed to remove work dir", {
8775
9131
  workDir,
@@ -8786,8 +9142,8 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
8786
9142
  };
8787
9143
  }
8788
9144
  function readAssemblePlan(planDir, chunkPaths) {
8789
- const planJsonPath = join22(planDir, "plan.json");
8790
- const chunksJsonPath = join22(planDir, "meta", "chunks.json");
9145
+ const planJsonPath = join23(planDir, "plan.json");
9146
+ const chunksJsonPath = join23(planDir, "meta", "chunks.json");
8791
9147
  if (!existsSync16(planJsonPath)) {
8792
9148
  throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
8793
9149
  }
@@ -8808,13 +9164,13 @@ function readAssemblePlan(planDir, chunkPaths) {
8808
9164
  throw new Error(`[assemble] chunk path does not exist: ${path}`);
8809
9165
  }
8810
9166
  }
8811
- return { ...plan2, chunks };
9167
+ return plan2;
8812
9168
  }
8813
9169
  function videoInputArgs(ctx, videoSources) {
8814
- return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg, ctx.chunkSeconds);
9170
+ return videoSources.length === 1 ? ["-i", videoSources[0]] : concatDemuxerInput(videoSources, ctx.workDir, ctx.fpsArg);
8815
9171
  }
8816
9172
  async function reencodeCfr(ctx, chunkPaths, encoder) {
8817
- const cfrOutputPath = join22(ctx.workDir, `cfr.${ctx.format}`);
9173
+ const cfrOutputPath = join23(ctx.workDir, `cfr.${ctx.format}`);
8818
9174
  const cfrArgs = [
8819
9175
  ...videoInputArgs(ctx, chunkPaths),
8820
9176
  "-c:v",
@@ -8852,7 +9208,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
8852
9208
  );
8853
9209
  return;
8854
9210
  }
8855
- const paddedAudioPath = join22(ctx.workDir, "audio-padded.m4a");
9211
+ const paddedAudioPath = join23(ctx.workDir, "audio-padded.m4a");
8856
9212
  const padTrimResult = await ctx.timed(
8857
9213
  "audioMs",
8858
9214
  () => padOrTrimAudioToVideoFrameCount({
@@ -8891,15 +9247,10 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
8891
9247
  throw encoderFailureError("[assemble] audio mux failed", muxResult);
8892
9248
  }
8893
9249
  }
8894
- function concatDemuxerInput(chunkPaths, workDir, fpsArg, chunkSeconds) {
8895
- const listPath = join22(workDir, "concat-list.txt");
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")}
9250
+ function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
9251
+ const listPath = join23(workDir, "concat-list.txt");
9252
+ const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
9253
+ writeFileSync8(listPath, `${body}
8903
9254
  `, "utf-8");
8904
9255
  return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
8905
9256
  }
@@ -8908,7 +9259,7 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
8908
9259
  videoPaths.map((path) => defaultProbeVideoFrameInfo(path, signal))
8909
9260
  );
8910
9261
  return {
8911
- frameCount: infos.reduce((sum, info) => sum + info.frameCount, 0),
9262
+ frameCount: infos.reduce((sum2, info) => sum2 + info.frameCount, 0),
8912
9263
  fpsNum: fps.num,
8913
9264
  fpsDen: fps.den
8914
9265
  };
@@ -8922,8 +9273,15 @@ async function runAssembleFfmpeg(args, step, signal, timeout) {
8922
9273
  });
8923
9274
  }
8924
9275
  }
9276
+ function chunksAreStitchedMp4(planDir, cfr) {
9277
+ if (!chunksCarryAudio(planDir)) return false;
9278
+ if (cfr) {
9279
+ throw new Error("[assemble] cfr=true cannot re-encode a plan whose mp4 chunks carry audio");
9280
+ }
9281
+ return true;
9282
+ }
8925
9283
  function chunksCarryAudio(planDir) {
8926
- const encoderJsonPath = join22(planDir, "meta", "encoder.json");
9284
+ const encoderJsonPath = join23(planDir, "meta", "encoder.json");
8927
9285
  if (!existsSync16(encoderJsonPath)) return false;
8928
9286
  return JSON.parse(readFileSync10(encoderJsonPath, "utf-8")).chunksCarryAudio === true;
8929
9287
  }
@@ -8933,7 +9291,7 @@ function readCfrEncoder(planDir, format) {
8933
9291
  `[assemble] cfr=true is only supported for format="mp4" (got "${format}"). Stream-copy paths for webm and mov already produce exact avg_frame_rate; cfr re-encode is not needed.`
8934
9292
  );
8935
9293
  }
8936
- const encoderJsonPath = join22(planDir, "meta", "encoder.json");
9294
+ const encoderJsonPath = join23(planDir, "meta", "encoder.json");
8937
9295
  if (!existsSync16(encoderJsonPath)) {
8938
9296
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
8939
9297
  }
@@ -8946,8 +9304,8 @@ function readCfrEncoder(planDir, format) {
8946
9304
  return encoderJson;
8947
9305
  }
8948
9306
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
8949
- if (existsSync16(outputPath)) rmSync10(outputPath, { recursive: true, force: true });
8950
- mkdirSync11(outputPath, { recursive: true });
9307
+ if (existsSync16(outputPath)) rmSync11(outputPath, { recursive: true, force: true });
9308
+ mkdirSync12(outputPath, { recursive: true });
8951
9309
  let globalIdx = 0;
8952
9310
  for (const chunkDir of chunkPaths) {
8953
9311
  if (!statSync7(chunkDir).isDirectory()) {
@@ -8960,8 +9318,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
8960
9318
  throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
8961
9319
  }
8962
9320
  for (const frame of frames) {
8963
- const dst = join22(outputPath, formatExportFrameName(globalIdx, "png"));
8964
- cpSync3(join22(chunkDir, frame), dst);
9321
+ const dst = join23(outputPath, formatExportFrameName(globalIdx, "png"));
9322
+ cpSync3(join23(chunkDir, frame), dst);
8965
9323
  globalIdx += 1;
8966
9324
  }
8967
9325
  }
@@ -8971,13 +9329,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
8971
9329
  );
8972
9330
  }
8973
9331
  if (audioPath !== null && existsSync16(audioPath)) {
8974
- const sidecar = join22(outputPath, MIXED_AUDIO_FILENAME4);
9332
+ const sidecar = join23(outputPath, MIXED_AUDIO_FILENAME4);
8975
9333
  cpSync3(audioPath, sidecar);
8976
9334
  }
8977
9335
  let fileSize = 0;
8978
9336
  for (const name of readdirSync7(outputPath)) {
8979
9337
  try {
8980
- fileSize += statSync7(join22(outputPath, name)).size;
9338
+ fileSize += statSync7(join23(outputPath, name)).size;
8981
9339
  } catch {
8982
9340
  }
8983
9341
  }
@@ -8991,8 +9349,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
8991
9349
 
8992
9350
  // src/services/distributed/renderChunk.ts
8993
9351
  import { randomBytes } from "node:crypto";
8994
- import { existsSync as existsSync17, mkdirSync as mkdirSync12, readFileSync as readFileSync11, readdirSync as readdirSync8, rmSync as rmSync11, writeFileSync as writeFileSync9 } from "node:fs";
8995
- import { extname as extname4, join as join23 } from "node:path";
9352
+ import { existsSync as existsSync17, mkdirSync as mkdirSync13, readFileSync as readFileSync11, readdirSync as readdirSync8, rmSync as rmSync12, writeFileSync as writeFileSync9 } from "node:fs";
9353
+ import { extname as extname4, join as join24 } from "node:path";
8996
9354
  import {
8997
9355
  assertSwiftShader,
8998
9356
  BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE3,
@@ -9198,7 +9556,7 @@ function frameNumberFromFileName(name) {
9198
9556
  function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
9199
9557
  const result = [];
9200
9558
  for (const v of videos) {
9201
- const outputDir = join23(planDir, "video-frames", v.videoId);
9559
+ const outputDir = join24(planDir, "video-frames", v.videoId);
9202
9560
  if (!existsSync17(outputDir)) {
9203
9561
  throw new Error(
9204
9562
  `[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
@@ -9217,7 +9575,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
9217
9575
  if (!frameName) continue;
9218
9576
  const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
9219
9577
  const frameIndex = frameNumber === null ? i : frameNumber - 1;
9220
- framePaths.set(frameIndex, join23(outputDir, frameName));
9578
+ framePaths.set(frameIndex, join24(outputDir, frameName));
9221
9579
  }
9222
9580
  result.push({
9223
9581
  videoId: v.videoId,
@@ -9239,13 +9597,13 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
9239
9597
  return result;
9240
9598
  }
9241
9599
  function contiguousRuns(sortedIndexes) {
9242
- const runs = [];
9600
+ const runs2 = [];
9243
9601
  for (const index of sortedIndexes) {
9244
- const last = runs[runs.length - 1];
9602
+ const last = runs2[runs2.length - 1];
9245
9603
  if (last && last.firstFrame + last.frames === index) last.frames++;
9246
- else runs.push({ firstFrame: index, frames: 1 });
9604
+ else runs2.push({ firstFrame: index, frames: 1 });
9247
9605
  }
9248
- return runs;
9606
+ return runs2;
9249
9607
  }
9250
9608
  async function extractDeferredVideoFramesForChunk(input) {
9251
9609
  const deferred = input.planVideos.extracted.filter((video) => video.deferredRange);
@@ -9270,13 +9628,13 @@ async function extractDeferredVideoFramesForChunk(input) {
9270
9628
  }
9271
9629
  return Promise.all(
9272
9630
  deferred.map(async (video) => {
9273
- const outputDir = join23(input.outputRoot, video.videoId);
9631
+ const outputDir = join24(input.outputRoot, video.videoId);
9274
9632
  const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
9275
9633
  if (indexes.length === 0) {
9276
9634
  return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
9277
9635
  }
9278
9636
  const range = video.deferredRange;
9279
- const sourcePath = join23(input.planDir, ...range.sourcePath.split("/"));
9637
+ const sourcePath = join24(input.planDir, ...range.sourcePath.split("/"));
9280
9638
  if (!existsSync17(sourcePath)) {
9281
9639
  throw new RenderChunkValidationError(
9282
9640
  MISSING_PLAN_ARTIFACT,
@@ -9312,7 +9670,7 @@ function hashChunkOutput(outputPath, kind) {
9312
9670
  if (kind === "file") return sha256Hex(readFileSync11(outputPath));
9313
9671
  const entries = readdirSync8(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
9314
9672
  const lines = entries.map(
9315
- (name) => `${name}\0${sha256Hex(readFileSync11(join23(outputPath, name)))}`
9673
+ (name) => `${name}\0${sha256Hex(readFileSync11(join24(outputPath, name)))}`
9316
9674
  );
9317
9675
  return sha256Hex(lines.join("\0"));
9318
9676
  }
@@ -9329,9 +9687,9 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
9329
9687
  async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9330
9688
  const start = Date.now();
9331
9689
  const log = defaultLogger;
9332
- const planJsonPath = join23(planDir, "plan.json");
9333
- const encoderJsonPath = join23(planDir, "meta", "encoder.json");
9334
- const chunksJsonPath = join23(planDir, "meta", "chunks.json");
9690
+ const planJsonPath = join24(planDir, "plan.json");
9691
+ const encoderJsonPath = join24(planDir, "meta", "encoder.json");
9692
+ const chunksJsonPath = join24(planDir, "meta", "chunks.json");
9335
9693
  if (!existsSync17(planJsonPath)) {
9336
9694
  throw new RenderChunkValidationError(
9337
9695
  MISSING_PLAN_ARTIFACT,
@@ -9355,7 +9713,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9355
9713
  }
9356
9714
  const encoder = JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
9357
9715
  const chunks = JSON.parse(readFileSync11(chunksJsonPath, "utf-8"));
9358
- const videosJsonPath = join23(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
9716
+ const videosJsonPath = join24(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
9359
9717
  let planVideos = null;
9360
9718
  if (existsSync17(videosJsonPath)) {
9361
9719
  try {
@@ -9385,7 +9743,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9385
9743
  `[renderChunk] chunk ${chunkIndex} has non-positive frame count: ${framesInChunk}`
9386
9744
  );
9387
9745
  }
9388
- const compiledDir = join23(planDir, "compiled");
9746
+ const compiledDir = join24(planDir, "compiled");
9389
9747
  if (!existsSync17(compiledDir)) {
9390
9748
  throw new RenderChunkValidationError(
9391
9749
  MISSING_PLAN_ARTIFACT,
@@ -9442,9 +9800,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9442
9800
  forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot
9443
9801
  };
9444
9802
  const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
9445
- mkdirSync12(workDir, { recursive: true });
9446
- const framesDir = join23(workDir, "captured-frames");
9447
- mkdirSync12(framesDir, { recursive: true });
9803
+ mkdirSync13(workDir, { recursive: true });
9804
+ const framesDir = join24(workDir, "captured-frames");
9805
+ mkdirSync13(framesDir, { recursive: true });
9448
9806
  const videoFramesDir = `${workDir}.video-frames`;
9449
9807
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? createFrameLookupTable3(
9450
9808
  planVideos.videos,
@@ -9497,7 +9855,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9497
9855
  // `beginFrameTimeTicks` is host-independent. Only chunks ever set this.
9498
9856
  lockWarmupTicks: true,
9499
9857
  requiresWebGpu: compositionRequiresWebGpu3(
9500
- readFileSync11(join23(compiledDir, "index.html"), "utf-8")
9858
+ readFileSync11(join24(compiledDir, "index.html"), "utf-8")
9501
9859
  )
9502
9860
  };
9503
9861
  const chunkWorkerCount = calculateOptimalWorkers2(framesInChunk, void 0, cfg);
@@ -9628,8 +9986,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9628
9986
  return observedMode;
9629
9987
  },
9630
9988
  resetForScreenshotRetry: () => {
9631
- rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
9632
- mkdirSync12(framesDir, { recursive: true });
9989
+ rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
9990
+ mkdirSync13(framesDir, { recursive: true });
9633
9991
  capturePerfs.length = 0;
9634
9992
  captureAttempts.length = 0;
9635
9993
  job.framesRendered = 0;
@@ -9660,10 +10018,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9660
10018
  const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
9661
10019
  const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
9662
10020
  if (isPngSequence) {
9663
- if (!existsSync17(outputChunkPath)) mkdirSync12(outputChunkPath, { recursive: true });
10021
+ if (!existsSync17(outputChunkPath)) mkdirSync13(outputChunkPath, { recursive: true });
9664
10022
  } else {
9665
- const outDir = join23(outputChunkPath, "..");
9666
- if (!existsSync17(outDir)) mkdirSync12(outDir, { recursive: true });
10023
+ const outDir = join24(outputChunkPath, "..");
10024
+ if (!existsSync17(outDir)) mkdirSync13(outDir, { recursive: true });
9667
10025
  }
9668
10026
  const encodeStarted = Date.now();
9669
10027
  await runEncodeStage({
@@ -9715,7 +10073,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9715
10073
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
9716
10074
  timeoutMs: cfg.ffmpegProcessTimeout
9717
10075
  });
9718
- rmSync11(videoOnlyPath, { force: true });
10076
+ rmSync12(videoOnlyPath, { force: true });
9719
10077
  }
9720
10078
  encodeStageMs = Date.now() - encodeStarted;
9721
10079
  } finally {
@@ -9757,8 +10115,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9757
10115
  writeFileSync9(perfPath, `${JSON.stringify(perfPayload, null, 2)}
9758
10116
  `, "utf-8");
9759
10117
  try {
9760
- rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
9761
- rmSync11(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
10118
+ rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
10119
+ rmSync12(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
9762
10120
  } catch (err) {
9763
10121
  log.warn("[renderChunk] failed to remove work dir", {
9764
10122
  workDir,
@@ -9786,18 +10144,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
9786
10144
 
9787
10145
  // src/services/distributed/planV2Execution.ts
9788
10146
  async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath) {
9789
- const workRoot = mkdtempSync6(join24(tmpdir2(), "hf-plan-v2-chunk-"));
9790
- const materializedPlanDir = join24(workRoot, "plan");
10147
+ const workRoot = mkdtempSync7(join25(tmpdir2(), "hf-plan-v2-chunk-"));
10148
+ const materializedPlanDir = join25(workRoot, "plan");
9791
10149
  try {
9792
10150
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
9793
10151
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath);
9794
10152
  } finally {
9795
- rmSync12(workRoot, { recursive: true, force: true });
10153
+ rmSync13(workRoot, { recursive: true, force: true });
9796
10154
  }
9797
10155
  }
9798
10156
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
9799
- const workRoot = mkdtempSync6(join24(tmpdir2(), "hf-plan-v2-assembler-"));
9800
- const materializedPlanDir = join24(workRoot, "plan");
10157
+ const workRoot = mkdtempSync7(join25(tmpdir2(), "hf-plan-v2-assembler-"));
10158
+ const materializedPlanDir = join25(workRoot, "plan");
9801
10159
  try {
9802
10160
  const materialized = materializePlanV2Target(
9803
10161
  planV2Dir,
@@ -9812,7 +10170,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
9812
10170
  options
9813
10171
  );
9814
10172
  } finally {
9815
- rmSync12(workRoot, { recursive: true, force: true });
10173
+ rmSync13(workRoot, { recursive: true, force: true });
9816
10174
  }
9817
10175
  }
9818
10176
 
@@ -10061,7 +10419,7 @@ function validateIntDimension(field, value) {
10061
10419
  // src/services/distributed/projectHash.ts
10062
10420
  import { readdirSync as readdirSync9, readFileSync as readFileSync12 } from "node:fs";
10063
10421
  import { createHash as createHash6 } from "node:crypto";
10064
- import { join as join25, relative as relative7 } from "node:path";
10422
+ import { join as join26, relative as relative7 } from "node:path";
10065
10423
  function hashProjectDir(projectDir) {
10066
10424
  const hash = createHash6("sha256");
10067
10425
  const files = [];
@@ -10070,7 +10428,7 @@ function hashProjectDir(projectDir) {
10070
10428
  (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
10071
10429
  )) {
10072
10430
  if (isRoot && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(entry.name)) continue;
10073
- const full = join25(dir, entry.name);
10431
+ const full = join26(dir, entry.name);
10074
10432
  if (entry.isDirectory()) walk(full, false);
10075
10433
  else if (entry.isFile()) files.push(full);
10076
10434
  }
@@ -10127,6 +10485,8 @@ export {
10127
10485
  assemble,
10128
10486
  assembleV2,
10129
10487
  buildChunkSlices,
10488
+ buildStitchedHeader,
10489
+ chunksAreStitchedMp4,
10130
10490
  createPlanV2FromExecutionPlan,
10131
10491
  createPlanV2FromV1,
10132
10492
  getDistributedRenderCapabilities,
@@ -10136,11 +10496,13 @@ export {
10136
10496
  listPlanV2ArtifactsForTarget,
10137
10497
  materializePlanV2Target,
10138
10498
  measurePlanDirBytes,
10499
+ muxChunkWithAudio,
10139
10500
  plan,
10140
10501
  planV2,
10141
10502
  planV2WithPublisher,
10142
10503
  publishPlanV2FromExecutionPlan,
10143
10504
  publishPlanV2FromV1,
10505
+ readMp4Layout,
10144
10506
  readPlanProtocol,
10145
10507
  readPlanProtocolV1,
10146
10508
  readPlanV2Manifest,