@hyperframes/producer 0.8.190 → 0.8.192

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.
@@ -39,6 +39,7 @@ export { muxChunkWithAudio } from "./services/distributed/chunkAudio.js";
39
39
  export { buildStitchedHeader, readMp4Layout, type Mp4Layout, type ReadRange, } from "./services/distributed/mp4Stitch.js";
40
40
  export { InvalidConfigError, type SerializableDistributedRenderConfig, validateDistributedRenderConfig, validateVariablesPayload, } from "./services/distributed/renderConfigValidation.js";
41
41
  export { hashProjectDir, listSiteFiles } from "./services/distributed/projectHash.js";
42
+ export { AUDIO_SIDECAR_MIN_BYTES, buildAudioSidecar, wantsAudioSidecar, type AudioSidecarOutcome, } from "./services/distributed/audioSidecar.js";
42
43
  export { CURRENT_PLAN_PROTOCOL, DISTRIBUTED_RENDER_CAPABILITIES, getDistributedRenderCapabilities, PLAN_ARTIFACT_LAYOUT, PLAN_HASH_SCHEMA, PLAN_PROTOCOL_V1, PLAN_PROTOCOL_V2, PLAN_PROTOCOL_UNSUPPORTED, PLAN_SCHEMA_VERSION, PLAN_V2_ARTIFACT_LAYOUT, PLAN_V2_HASH_SCHEMA, PLAN_V2_SCHEMA_VERSION, PlanProtocolUnsupportedError, readPlanProtocol, readPlanProtocolV1, type DistributedRenderCapabilities, type PlanProtocolConsumerCapabilities, type PlanProtocolDescriptor, type PlanProtocolV1Descriptor, type PlanProtocolV2Descriptor, type SupportedPlanProtocolDescriptor, } from "./services/distributed/planProtocol.js";
43
44
  export { PlanVideosMetadataError, type DistributedFormat } from "./services/distributed/shared.js";
44
45
  export { isPlanAudioArtifactPath, PLAN_AUDIO_LEGACY_RELATIVE_PATH, PLAN_AUDIO_RELATIVE_PATH, resolvePlanAudioPath, } from "./services/distributed/shared.js";
@@ -18,7 +18,8 @@ import {
18
18
  import {
19
19
  getEncoderPreset as getEncoderPreset4,
20
20
  normalizeVp9CpuUsed,
21
- resolveConfig as resolveConfig3
21
+ resolveConfig as resolveConfig3,
22
+ declareContentAddressedUrl
22
23
  } from "@hyperframes/engine";
23
24
 
24
25
  // src/logger.ts
@@ -3395,7 +3396,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3395
3396
  );
3396
3397
  if (analysis.isProblematic) {
3397
3398
  defaultLogger.warn(
3398
- `[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s). This causes seek failures and frame freezing. Re-encode with: ${reencode}`
3399
+ `[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s). Long GOPs can make preview seeking slower; rendering is unaffected. If preview seeking is slow, re-encode with: ${reencode}`
3399
3400
  );
3400
3401
  }
3401
3402
  if (metadata.isVFR) {
@@ -4281,6 +4282,10 @@ async function runProbeStage(input) {
4281
4282
  if (el.loop && !existing.loop) {
4282
4283
  existing.loop = true;
4283
4284
  }
4285
+ const audio = composition.audios.find(
4286
+ (audio2) => audio2.type === "video" && audio2.id === `${el.id}-audio`
4287
+ );
4288
+ if (audio) audio.loop = existing.loop;
4284
4289
  }
4285
4290
  } else if (!compiled.droppedMediaIds.has(el.id)) {
4286
4291
  composition.videos.push({
@@ -4318,6 +4323,7 @@ async function runProbeStage(input) {
4318
4323
  el,
4319
4324
  authoredStarts.get(el.id) ?? existing.mediaStart
4320
4325
  );
4326
+ existing.loop = el.loop;
4321
4327
  if (el.volume > 0 && Math.abs((existing.volume ?? 1) - el.volume) > BROWSER_MEDIA_EPSILON) {
4322
4328
  existing.volume = el.volume;
4323
4329
  }
@@ -4329,6 +4335,7 @@ async function runProbeStage(input) {
4329
4335
  start: el.start,
4330
4336
  end: resolveBrowserMediaEnd(el.start, el.end, el.duration),
4331
4337
  mediaStart: el.mediaStart,
4338
+ loop: el.loop,
4332
4339
  layer: 0,
4333
4340
  volume: el.volume,
4334
4341
  type: "audio"
@@ -4583,6 +4590,10 @@ import {
4583
4590
  extractMediaMetadata as extractMediaMetadata3,
4584
4591
  isVideoPastTimelineEnd,
4585
4592
  isHdrColorSpace as isHdrColorSpace2,
4593
+ rebaseVideoToWindow,
4594
+ resolveFinalFrameExtractionWindow as resolveFinalFrameExtractionWindow2,
4595
+ resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
4596
+ isHttpUrl as isHttpUrl2,
4586
4597
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
4587
4598
  runVideoExtractionWithRetry,
4588
4599
  safeVideoExtractionSourceIdentity
@@ -4948,6 +4959,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
4948
4959
  start: video.start,
4949
4960
  end: video.end,
4950
4961
  mediaStart: video.mediaStart,
4962
+ playbackRate: video.playbackRate,
4963
+ loop: video.loop,
4951
4964
  layer: 0,
4952
4965
  volume: 1,
4953
4966
  type: "video"
@@ -4955,6 +4968,33 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
4955
4968
  existingAudioSrcs.add(video.src);
4956
4969
  }
4957
4970
  }
4971
+ async function appendVideoAudioFromProbe(input) {
4972
+ const { projectDir, compiledDir, composition, urlByAbsentLocalPath } = input;
4973
+ const videos = composition.videos.filter(
4974
+ (video) => !isVideoPastTimelineEnd(video, composition.duration)
4975
+ );
4976
+ const probed = await Promise.all(
4977
+ videos.map(async (video) => {
4978
+ const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
4979
+ const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
4980
+ if (!isHttpUrl2(readFrom) && !existsSync7(readFrom)) return null;
4981
+ const metadata = await extractMediaMetadata3(readFrom);
4982
+ const window2 = await resolveFinalFrameExtractionWindow2(
4983
+ readFrom,
4984
+ video,
4985
+ metadata,
4986
+ resolveVideoExtractionWindow2(video, metadata, composition.duration)
4987
+ );
4988
+ if (window2.durationSeconds <= 0) return null;
4989
+ rebaseVideoToWindow(video, window2);
4990
+ return { videoId: video.id, metadata };
4991
+ })
4992
+ );
4993
+ appendAutoDetectedVideoAudio(
4994
+ composition,
4995
+ probed.filter((entry) => entry !== null)
4996
+ );
4997
+ }
4958
4998
 
4959
4999
  // src/services/render/stages/audioStage.ts
4960
5000
  import { join as join10 } from "node:path";
@@ -5446,6 +5486,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5446
5486
  audios,
5447
5487
  masterFxChain,
5448
5488
  urlByAbsentLocalPath,
5489
+ audioSidecarUrlByLocalPath,
5449
5490
  abortSignal,
5450
5491
  assertNotAborted,
5451
5492
  log
@@ -5469,6 +5510,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5469
5510
  audioGain,
5470
5511
  masterFxChain,
5471
5512
  urlByAbsentLocalPath,
5513
+ audioSidecarUrlByLocalPath,
5472
5514
  keepEncoderInputAt: pcmSourcePath
5473
5515
  },
5474
5516
  compiledDir
@@ -8577,12 +8619,18 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
8577
8619
  const needsAlpha = outputNeedsAlpha(config.format);
8578
8620
  const remoteFiles = Object.entries(options.remoteFiles ?? {});
8579
8621
  const remoteLocalPath = (rel) => join21(projectDir, ...rel.split("/"));
8622
+ for (const [, file] of remoteFiles) declareContentAddressedUrl(file.url, file.sha256);
8580
8623
  const urlByAbsentLocalPath = new Map(
8581
8624
  remoteFiles.map(([rel, file]) => [remoteLocalPath(rel), file.url])
8582
8625
  );
8583
8626
  const localPathByUrl = new Map(
8584
8627
  remoteFiles.map(([rel, file]) => [file.url, remoteLocalPath(rel)])
8585
8628
  );
8629
+ const audioSidecarUrlByLocalPath = new Map(
8630
+ remoteFiles.flatMap(
8631
+ ([rel, file]) => file.audioSidecarUrl ? [[remoteLocalPath(rel), file.audioSidecarUrl]] : []
8632
+ )
8633
+ );
8586
8634
  stageStarted = Date.now();
8587
8635
  const compileResult = await runCompileStage({
8588
8636
  urlByAbsentLocalPath,
@@ -8667,6 +8715,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
8667
8715
  finalCompiledDir,
8668
8716
  remoteFiles,
8669
8717
  urlByAbsentLocalPath,
8718
+ audioSidecarUrlByLocalPath,
8670
8719
  localPathByUrl,
8671
8720
  compileResult,
8672
8721
  compiled,
@@ -8684,6 +8733,7 @@ function mixPlanAudio(c, compiledDir) {
8684
8733
  workDir: c.workDir,
8685
8734
  compiledDir,
8686
8735
  urlByAbsentLocalPath: c.urlByAbsentLocalPath,
8736
+ audioSidecarUrlByLocalPath: c.audioSidecarUrlByLocalPath,
8687
8737
  totalFrames: c.totalFrames,
8688
8738
  fps: c.job.config.fps,
8689
8739
  ffmpegProcessTimeout: c.cfg.ffmpegProcessTimeout,
@@ -8968,7 +9018,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
8968
9018
  });
8969
9019
  try {
8970
9020
  let stageStarted = Date.now();
8971
- await extractPlanVideos(c, true);
9021
+ await appendVideoAudioFromProbe(c);
8972
9022
  logPlanStage(c.log, "extract", stageStarted);
8973
9023
  stageStarted = Date.now();
8974
9024
  const mixed = await mixPlanAudio(c, c.compiledDir);
@@ -10036,7 +10086,7 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
10036
10086
  "-r",
10037
10087
  fpsArg
10038
10088
  ];
10039
- appendSoftwareEncoderThreadsArg(args);
10089
+ appendSoftwareEncoderThreadsArg(args, "libx264");
10040
10090
  return args;
10041
10091
  }
10042
10092
  async function reencodeCfr(ctx, chunkPaths, encoder) {
@@ -11164,7 +11214,36 @@ function hashProjectDir(projectDir, files = listSiteFiles(projectDir)) {
11164
11214
  }
11165
11215
  return hash.digest("hex").slice(0, 16);
11166
11216
  }
11217
+
11218
+ // src/services/distributed/audioSidecar.ts
11219
+ import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as runFfmpeg8 } from "@hyperframes/engine";
11220
+ import { extname as extname5 } from "node:path";
11221
+ var AUDIO_SIDECAR_MIN_BYTES = 32 * 1024 * 1024;
11222
+ var AUDIO_SIDECAR_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v"]);
11223
+ function wantsAudioSidecar(path, sizeBytes) {
11224
+ return sizeBytes >= AUDIO_SIDECAR_MIN_BYTES && AUDIO_SIDECAR_SOURCE_EXTENSIONS.has(extname5(path).toLowerCase());
11225
+ }
11226
+ async function buildAudioSidecar(inputPath, outputPath, signal) {
11227
+ const audio = await extractAudioMetadata4(inputPath, { signal }).catch(() => null);
11228
+ if (!audio) return await ffmpegIsMissing(signal) ? "unavailable" : "none";
11229
+ if (audio.audioStreamCount !== 1 || (audio.audioStreamLeadSeconds ?? 0) > 0) return "none";
11230
+ const result = await runFfmpeg8(
11231
+ ["-v", "error", "-y", "-i", inputPath, "-map", "0:a:0", "-vn", "-c:a", "copy"].concat([
11232
+ "-movflags",
11233
+ "+faststart",
11234
+ "-f",
11235
+ "mp4",
11236
+ outputPath
11237
+ ]),
11238
+ { signal }
11239
+ );
11240
+ if (result.success) return "built";
11241
+ return isMissing(result) ? "unavailable" : "none";
11242
+ }
11243
+ var isMissing = (result) => result.error?.code === "ENOENT";
11244
+ var ffmpegIsMissing = async (signal) => isMissing(await runFfmpeg8(["-version"], { signal }));
11167
11245
  export {
11246
+ AUDIO_SIDECAR_MIN_BYTES,
11168
11247
  CHUNK_EXCEEDS_WORKER_TIMEOUT,
11169
11248
  CHUNK_FRAME_RANGE_INVALID,
11170
11249
  CURRENT_PLAN_PROTOCOL,
@@ -11207,6 +11286,7 @@ export {
11207
11286
  applyRuntimeEnvSnapshot,
11208
11287
  assemble,
11209
11288
  assembleV2,
11289
+ buildAudioSidecar,
11210
11290
  buildChunkSlices,
11211
11291
  buildPlanAudio,
11212
11292
  buildStitchedHeader,
@@ -11242,5 +11322,6 @@ export {
11242
11322
  stitchesSeparateAudio,
11243
11323
  validateDistributedRenderConfig,
11244
11324
  validatePlanV2MaterializedTarget,
11245
- validateVariablesPayload
11325
+ validateVariablesPayload,
11326
+ wantsAudioSidecar
11246
11327
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "buildId": "dev",
4
- "sha256": "54a6d80fb24c9d14b82e50000abc5ff73819d53fe7951b53299a20e941371b5b",
4
+ "sha256": "a44a8127a863733eafcb4dfa80f2517a3a74266a32bbaf072be4b7401ea46c42",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"