@hyperframes/producer 0.8.154 → 0.8.156

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/index.js CHANGED
@@ -36,7 +36,7 @@ import {
36
36
  LOW_MEMORY_TOTAL_MB_THRESHOLD,
37
37
  assertConfiguredFfmpegBinariesExist,
38
38
  resolveBrowserGpuMode,
39
- resolveHeadlessShellPath as resolveHeadlessShellPath2,
39
+ resolveHeadlessShellPath,
40
40
  compositionRequiresWebGpu as compositionRequiresWebGpu2,
41
41
  applyConcreteGpuScreenshotClamp,
42
42
  explainDrawElementDisabled,
@@ -1837,7 +1837,6 @@ import {
1837
1837
  getCapturePerfSummary as getCapturePerfSummary2,
1838
1838
  initializeSession as initializeSession2,
1839
1839
  prepareCaptureSessionForReuse as prepareCaptureSessionForReuse2,
1840
- resolveHeadlessShellPath,
1841
1840
  shouldDisableBrowserPoolForParallelWorker,
1842
1841
  spawnStreamingEncoder as spawnStreamingEncoder2
1843
1842
  } from "@hyperframes/engine";
@@ -4081,10 +4080,7 @@ async function openSegmentedSession(input) {
4081
4080
  const videoInjector = input.createRenderVideoFrameInjector();
4082
4081
  const captureOptions = input.buildCaptureOptions();
4083
4082
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4084
- parallel: (input.workerCount ?? 1) > 1,
4085
- platform: process.platform,
4086
- deviceScaleFactor: captureOptions.deviceScaleFactor,
4087
- headlessShellPath: resolveHeadlessShellPath(captureCfg)
4083
+ parallel: (input.workerCount ?? 1) > 1
4088
4084
  });
4089
4085
  return createCaptureSession2(
4090
4086
  input.fileServer.url,
@@ -4128,6 +4124,14 @@ async function createSegmentWorker(run, id) {
4128
4124
  }
4129
4125
  };
4130
4126
  }
4127
+ async function openSegmentWorkersTogether(run, workersToCloseLater) {
4128
+ const opened = await Promise.allSettled(
4129
+ Array.from({ length: run.workerCount }, (_, id) => createSegmentWorker(run, id))
4130
+ );
4131
+ for (const o of opened) if (o.status === "fulfilled") workersToCloseLater.push(o.value);
4132
+ const failure = opened.find((o) => o.status === "rejected");
4133
+ if (failure && failure.status === "rejected") throw failure.reason;
4134
+ }
4131
4135
  async function recycleWorkerSession(run, worker, why) {
4132
4136
  run.input.dedupPerfs.push(getCapturePerfSummary2(worker.ctx.session));
4133
4137
  const memory = worker.ctx.session.chromeMemory?.stats();
@@ -4250,7 +4254,7 @@ async function runCaptureSegmentedStage(input) {
4250
4254
  };
4251
4255
  try {
4252
4256
  assertNotAborted();
4253
- for (let id = 0; id < run.workerCount; id++) workers.push(await createSegmentWorker(run, id));
4257
+ await openSegmentWorkersTogether(run, workers);
4254
4258
  const outcomes = await Promise.allSettled(workers.map((w) => runWorkerLoop(w)));
4255
4259
  for (const worker of workers) dedupPerfs.push(getCapturePerfSummary2(worker.ctx.session));
4256
4260
  const failure = outcomes.find((o) => o.status === "rejected");
@@ -12209,7 +12213,7 @@ async function executeRenderPipeline(input) {
12209
12213
  code: "start_browsers"
12210
12214
  });
12211
12215
  const resolvedBrowserGpuMode = await resolveBrowserGpuMode(cfg.browserGpuMode, {
12212
- chromePath: resolveHeadlessShellPath2(cfg),
12216
+ chromePath: resolveHeadlessShellPath(cfg),
12213
12217
  browserTimeout: cfg.browserTimeout
12214
12218
  });
12215
12219
  captureForceScreenshot = applyConcreteGpuScreenshotClamp(
@@ -12554,7 +12558,7 @@ async function executeRenderPipeline(input) {
12554
12558
  }
12555
12559
  const parallelCaptureMode = resolveParallelCaptureMode({
12556
12560
  platform: process.platform,
12557
- headlessShell: resolveHeadlessShellPath2(cfg) !== null,
12561
+ headlessShell: resolveHeadlessShellPath(cfg) !== null,
12558
12562
  forceScreenshot: captureForceScreenshot,
12559
12563
  deviceScaleFactor: captureOptions.deviceScaleFactor
12560
12564
  });
@@ -14241,12 +14245,12 @@ function resolveWorkerEntry2() {
14241
14245
  }
14242
14246
 
14243
14247
  // src/server.ts
14244
- import { isVideoFrameFormat as isVideoFrameFormat3 } from "@hyperframes/engine";
14245
14248
  import {
14246
14249
  parseFps,
14247
14250
  normalizeResolutionFlag,
14248
14251
  isAspectAgnosticResolutionAlias
14249
14252
  } from "@hyperframes/core";
14253
+ import { parseEncoderOptions } from "@hyperframes/studio-server/encoder-options";
14250
14254
  var DEFAULT_SERVER_FPS = { num: 30, den: 1 };
14251
14255
  var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
14252
14256
  "ASSET_MEDIA_TYPE_MISMATCH",
@@ -14320,13 +14324,16 @@ function parseRenderOptions(body) {
14320
14324
  const entryFile = nonEmptyString(body.entryFile);
14321
14325
  const format = parseServerFormat(body.format);
14322
14326
  const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
14323
- const videoFrameFormat = isVideoFrameFormat3(body.videoFrameFormat) ? body.videoFrameFormat : void 0;
14327
+ const encoder = parseEncoderOptions(body, format ?? "mp4");
14328
+ const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
14324
14329
  const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
14325
14330
  return {
14326
14331
  outputPath,
14327
14332
  fps,
14328
14333
  quality,
14329
14334
  workers,
14335
+ crf,
14336
+ videoBitrate,
14330
14337
  useGpu,
14331
14338
  debug,
14332
14339
  strictness,
@@ -14358,6 +14365,8 @@ function buildRenderJobConfig(input, outputPath, log) {
14358
14365
  quality: input.quality,
14359
14366
  format: input.format ?? "mp4",
14360
14367
  workers: input.workers,
14368
+ crf: input.crf,
14369
+ videoBitrate: input.videoBitrate,
14361
14370
  useGpu: input.useGpu,
14362
14371
  debug: input.debug,
14363
14372
  strictness: input.strictness,
@@ -14379,6 +14388,8 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
14379
14388
  return { input, cleanupProjectDir, absoluteOutputPath };
14380
14389
  }
14381
14390
  function validateRenderOverrides(body) {
14391
+ const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
14392
+ if (!encoder.ok) return encoder.error;
14382
14393
  if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
14383
14394
  return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
14384
14395
  }
@@ -17083,12 +17094,20 @@ import {
17083
17094
  appendRenderProvenanceArgs,
17084
17095
  applyFaststart as applyFaststart2,
17085
17096
  describeFfmpegFailure as describeFfmpegFailure2,
17097
+ getEncoderPreset as getEncoderPreset5,
17086
17098
  MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
17087
17099
  muxVideoWithAudio as muxVideoWithAudio2,
17088
17100
  resolveConfig as resolveConfig6,
17089
17101
  runFfmpeg as runFfmpeg5
17090
17102
  } from "@hyperframes/engine";
17091
17103
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
17104
+ function cfrRateControlArgs(locked) {
17105
+ const preset = getEncoderPreset5(locked.quality ?? "standard", "mp4");
17106
+ if (locked.crf == null && locked.bitrate) {
17107
+ return ["-preset", preset.preset, "-b:v", locked.bitrate];
17108
+ }
17109
+ return ["-preset", preset.preset, "-crf", String(locked.crf ?? preset.quality)];
17110
+ }
17092
17111
  async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
17093
17112
  const start = Date.now();
17094
17113
  const log = options?.logger ?? defaultLogger;
@@ -17185,7 +17204,9 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
17185
17204
  if (!existsSync25(encoderJsonPath)) {
17186
17205
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
17187
17206
  }
17188
- const encoderJson = JSON.parse(readFileSync13(encoderJsonPath, "utf-8"));
17207
+ const encoderJson = JSON.parse(
17208
+ readFileSync13(encoderJsonPath, "utf-8")
17209
+ );
17189
17210
  if (encoderJson.encoder === "libx265-software") {
17190
17211
  throw new Error(
17191
17212
  `[assemble] cfr=true is not yet supported with codec: "h265". The cfr re-encode pass uses libx264 and would silently transcode the h265 chunks. Either disable cfr or render with codec: "h264".`
@@ -17197,10 +17218,7 @@ async function assemble(planDir, chunkPaths, audioPath, outputPath, options) {
17197
17218
  concatOutputPath,
17198
17219
  "-c:v",
17199
17220
  "libx264",
17200
- "-preset",
17201
- "medium",
17202
- "-crf",
17203
- "18",
17221
+ ...cfrRateControlArgs(encoderJson),
17204
17222
  "-pix_fmt",
17205
17223
  "yuv420p",
17206
17224
  "-fps_mode",
@@ -17352,7 +17370,7 @@ import {
17352
17370
  createVideoFrameInjector as createVideoFrameInjector2,
17353
17371
  deriveBeginFrameProbeTimeTicks as deriveBeginFrameProbeTimeTicks2,
17354
17372
  extractVideoFramesRange,
17355
- getEncoderPreset as getEncoderPreset5,
17373
+ getEncoderPreset as getEncoderPreset6,
17356
17374
  initializeSession as initializeSession9,
17357
17375
  probeBeginFrameLiveness as probeBeginFrameLiveness2,
17358
17376
  readWebGlVendorInfoFromCanvas,
@@ -17903,7 +17921,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath) {
17903
17921
  const isPngSequence = plan2.dimensions.format === "png-sequence";
17904
17922
  outputKind = isPngSequence ? "frame-dir" : "file";
17905
17923
  const presetFormat = isPngSequence ? "mp4" : plan2.dimensions.format;
17906
- const basePreset = getEncoderPreset5(job.config.quality, presetFormat, void 0);
17924
+ const basePreset = getEncoderPreset6(job.config.quality, presetFormat, void 0);
17907
17925
  const preset = resolvePresetForLockedEncoder(basePreset, encoder.encoder);
17908
17926
  const effectiveQuality = encoder.crf ?? preset.quality;
17909
17927
  const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
@@ -56,7 +56,7 @@ import {
56
56
  LOW_MEMORY_TOTAL_MB_THRESHOLD,
57
57
  assertConfiguredFfmpegBinariesExist,
58
58
  resolveBrowserGpuMode,
59
- resolveHeadlessShellPath as resolveHeadlessShellPath2,
59
+ resolveHeadlessShellPath,
60
60
  compositionRequiresWebGpu as compositionRequiresWebGpu2,
61
61
  applyConcreteGpuScreenshotClamp,
62
62
  explainDrawElementDisabled,
@@ -1856,7 +1856,6 @@ import {
1856
1856
  getCapturePerfSummary as getCapturePerfSummary2,
1857
1857
  initializeSession as initializeSession2,
1858
1858
  prepareCaptureSessionForReuse as prepareCaptureSessionForReuse2,
1859
- resolveHeadlessShellPath,
1860
1859
  shouldDisableBrowserPoolForParallelWorker,
1861
1860
  spawnStreamingEncoder as spawnStreamingEncoder2
1862
1861
  } from "@hyperframes/engine";
@@ -4100,10 +4099,7 @@ async function openSegmentedSession(input) {
4100
4099
  const videoInjector = input.createRenderVideoFrameInjector();
4101
4100
  const captureOptions = input.buildCaptureOptions();
4102
4101
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4103
- parallel: (input.workerCount ?? 1) > 1,
4104
- platform: process.platform,
4105
- deviceScaleFactor: captureOptions.deviceScaleFactor,
4106
- headlessShellPath: resolveHeadlessShellPath(captureCfg)
4102
+ parallel: (input.workerCount ?? 1) > 1
4107
4103
  });
4108
4104
  return createCaptureSession2(
4109
4105
  input.fileServer.url,
@@ -4147,6 +4143,14 @@ async function createSegmentWorker(run, id) {
4147
4143
  }
4148
4144
  };
4149
4145
  }
4146
+ async function openSegmentWorkersTogether(run, workersToCloseLater) {
4147
+ const opened = await Promise.allSettled(
4148
+ Array.from({ length: run.workerCount }, (_, id) => createSegmentWorker(run, id))
4149
+ );
4150
+ for (const o of opened) if (o.status === "fulfilled") workersToCloseLater.push(o.value);
4151
+ const failure = opened.find((o) => o.status === "rejected");
4152
+ if (failure && failure.status === "rejected") throw failure.reason;
4153
+ }
4150
4154
  async function recycleWorkerSession(run, worker, why) {
4151
4155
  run.input.dedupPerfs.push(getCapturePerfSummary2(worker.ctx.session));
4152
4156
  const memory = worker.ctx.session.chromeMemory?.stats();
@@ -4269,7 +4273,7 @@ async function runCaptureSegmentedStage(input) {
4269
4273
  };
4270
4274
  try {
4271
4275
  assertNotAborted();
4272
- for (let id = 0; id < run.workerCount; id++) workers.push(await createSegmentWorker(run, id));
4276
+ await openSegmentWorkersTogether(run, workers);
4273
4277
  const outcomes = await Promise.allSettled(workers.map((w) => runWorkerLoop(w)));
4274
4278
  for (const worker of workers) dedupPerfs.push(getCapturePerfSummary2(worker.ctx.session));
4275
4279
  const failure = outcomes.find((o) => o.status === "rejected");
@@ -12192,7 +12196,7 @@ async function executeRenderPipeline(input) {
12192
12196
  code: "start_browsers"
12193
12197
  });
12194
12198
  const resolvedBrowserGpuMode = await resolveBrowserGpuMode(cfg.browserGpuMode, {
12195
- chromePath: resolveHeadlessShellPath2(cfg),
12199
+ chromePath: resolveHeadlessShellPath(cfg),
12196
12200
  browserTimeout: cfg.browserTimeout
12197
12201
  });
12198
12202
  captureForceScreenshot = applyConcreteGpuScreenshotClamp(
@@ -12537,7 +12541,7 @@ async function executeRenderPipeline(input) {
12537
12541
  }
12538
12542
  const parallelCaptureMode = resolveParallelCaptureMode({
12539
12543
  platform: process.platform,
12540
- headlessShell: resolveHeadlessShellPath2(cfg) !== null,
12544
+ headlessShell: resolveHeadlessShellPath(cfg) !== null,
12541
12545
  forceScreenshot: captureForceScreenshot,
12542
12546
  deviceScaleFactor: captureOptions.deviceScaleFactor
12543
12547
  });
@@ -13703,12 +13707,12 @@ function resolveWorkerEntry2() {
13703
13707
  }
13704
13708
 
13705
13709
  // src/server.ts
13706
- import { isVideoFrameFormat as isVideoFrameFormat3 } from "@hyperframes/engine";
13707
13710
  import {
13708
13711
  parseFps,
13709
13712
  normalizeResolutionFlag,
13710
13713
  isAspectAgnosticResolutionAlias
13711
13714
  } from "@hyperframes/core";
13715
+ import { parseEncoderOptions } from "@hyperframes/studio-server/encoder-options";
13712
13716
 
13713
13717
  // src/renderRequest.ts
13714
13718
  import {
@@ -14018,13 +14022,16 @@ function parseRenderOptions(body) {
14018
14022
  const entryFile = nonEmptyString(body.entryFile);
14019
14023
  const format = parseServerFormat(body.format);
14020
14024
  const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
14021
- const videoFrameFormat = isVideoFrameFormat3(body.videoFrameFormat) ? body.videoFrameFormat : void 0;
14025
+ const encoder = parseEncoderOptions(body, format ?? "mp4");
14026
+ const { crf, videoBitrate, videoFrameFormat } = encoder.ok ? encoder.options : {};
14022
14027
  const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
14023
14028
  return {
14024
14029
  outputPath,
14025
14030
  fps,
14026
14031
  quality,
14027
14032
  workers,
14033
+ crf,
14034
+ videoBitrate,
14028
14035
  useGpu,
14029
14036
  debug,
14030
14037
  strictness,
@@ -14056,6 +14063,8 @@ function buildRenderJobConfig(input, outputPath, log) {
14056
14063
  quality: input.quality,
14057
14064
  format: input.format ?? "mp4",
14058
14065
  workers: input.workers,
14066
+ crf: input.crf,
14067
+ videoBitrate: input.videoBitrate,
14059
14068
  useGpu: input.useGpu,
14060
14069
  debug: input.debug,
14061
14070
  strictness: input.strictness,
@@ -14077,6 +14086,8 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
14077
14086
  return { input, cleanupProjectDir, absoluteOutputPath };
14078
14087
  }
14079
14088
  function validateRenderOverrides(body) {
14089
+ const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
14090
+ if (!encoder.ok) return encoder.error;
14080
14091
  if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
14081
14092
  return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
14082
14093
  }
package/dist/server.d.ts CHANGED
@@ -43,6 +43,8 @@ interface RenderInput {
43
43
  videoFrameFormat?: RenderConfig["videoFrameFormat"];
44
44
  outputDynamicRange?: "auto" | "hdr" | "sdr";
45
45
  workers?: number;
46
+ crf?: number;
47
+ videoBitrate?: string;
46
48
  useGpu: boolean;
47
49
  debug: boolean;
48
50
  strictness: RenderConfig["strictness"];
@@ -23,6 +23,7 @@
23
23
  * for moving `outputPath` to its orchestration-level storage.
24
24
  */
25
25
  import { type ProducerLogger } from "../../logger.js";
26
+ import type { LockedRenderConfig } from "../render/stages/freezePlan.js";
26
27
  /**
27
28
  * Result of {@link assemble}. `fileSize` reflects the final file on disk
28
29
  * (mp4/mov) or the cumulative byte total of the frame directory
@@ -34,6 +35,8 @@ export interface AssembleResult {
34
35
  framesEncoded: number;
35
36
  fileSize: number;
36
37
  }
38
+ type CfrRateControlSource = Partial<Pick<LockedRenderConfig, "quality" | "crf" | "bitrate">>;
39
+ export declare function cfrRateControlArgs(locked: CfrRateControlSource): string[];
37
40
  /**
38
41
  * Assemble the chunk outputs into a single deliverable.
39
42
  *
@@ -60,10 +63,11 @@ export declare function assemble(planDir: string, chunkPaths: readonly string[],
60
63
  * duration lands on the requested `frameCount / fps` to ms
61
64
  * precision, with no PTS-derived drift). Trade-off: ~2-5x the
62
65
  * stitch time for a 60s 1080p clip plus second-generation H.264
63
- * quality loss (negligible at `-crf 18` but non-zero). Default
66
+ * quality loss (at the chunks' own CRF or bitrate, non-zero). Default
64
67
  * `false` preserves the existing `-c copy` behavior. mp4 only
65
68
  * (libx264); webm / mov pass through unchanged because their
66
69
  * stream-copy paths don't exhibit the same avg-frame-rate drift.
67
70
  */
68
71
  cfr?: boolean;
69
72
  }): Promise<AssembleResult>;
73
+ export {};
@@ -84,9 +84,9 @@ export declare function isVideoFrameCoverageError(err: unknown): err is VideoFra
84
84
  */
85
85
  export declare function resolveVideoCoverageThreshold(envValue?: string | undefined): number | null;
86
86
  /**
87
- * Count frames in a clip's authored `[start,end)` window. CFR extraction uses
88
- * FFmpeg's `-vf fps=<fps>` and rounds to the nearest output-frame boundary;
89
- * VFR extraction uses `-fps_mode cfr -r <fps>` and rounds up. Missing entries
87
+ * Count frames in a clip's authored `[start,end)` window. CFR extraction
88
+ * rounds to the nearest output-frame boundary and VFR extraction rounds up,
89
+ * as `extractionFrameCountForDuration` does. Missing entries
90
90
  * retain the fail-closed `ceil` default. Positive sub-frame clips emit one.
91
91
  */
92
92
  export declare function expectedFramesForClip(start: number, end: number, fps: FpsInput, rounding?: "ceil" | "nearest"): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.154",
3
+ "version": "0.8.156",
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/engine": "0.8.154",
39
- "@hyperframes/studio-server": "0.8.154",
40
- "@hyperframes/lint": "0.8.154",
41
- "@hyperframes/parsers": "0.8.154",
42
- "@hyperframes/core": "0.8.154"
38
+ "@hyperframes/engine": "0.8.156",
39
+ "@hyperframes/parsers": "0.8.156",
40
+ "@hyperframes/core": "0.8.156",
41
+ "@hyperframes/lint": "0.8.156",
42
+ "@hyperframes/studio-server": "0.8.156"
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.154"
51
+ "@hyperframes/aws-lambda": "0.8.156"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"