85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
171 lines
6.3 KiB
TypeScript
171 lines
6.3 KiB
TypeScript
/**
|
|
* Helpers shared between the distributed activity scripts (`plan.ts`,
|
|
* `renderChunk.ts`, `assemble.ts`). Kept module-local so the public surface
|
|
* stays just the three activity functions plus their result types.
|
|
*/
|
|
|
|
import { execFile as execFileCallback } from "node:child_process";
|
|
import { dirname, join } from "node:path";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
import { type Fps } from "@hyperframes/core";
|
|
import { type VideoElement, type VideoFrameFormat, type VideoMetadata } from "@hyperframes/engine";
|
|
import { type RenderConfig, type RenderJob, createRenderJob } from "../renderOrchestrator.js";
|
|
import { defaultLogger, type ProducerLogger } from "../../logger.js";
|
|
|
|
/**
|
|
* Output container formats the distributed pipeline supports end-to-end.
|
|
* Single source of truth for the format union — `plan()`, `renderChunk()`,
|
|
* `assemble()`, the aws-lambda handler, and the harness all derive from
|
|
* this type. Adding a new format starts here.
|
|
*/
|
|
export type DistributedFormat = "mp4" | "mov" | "png-sequence" | "webm";
|
|
|
|
/**
|
|
* Filename of the per-video extraction manifest written by `plan()` into
|
|
* `<planDir>/meta/` and consumed by `renderChunk()` to rebuild the
|
|
* BeforeCaptureHook that injects pre-extracted frames into the page.
|
|
* Absence is fine — compositions with no `<video>` elements never
|
|
* produce the file.
|
|
*/
|
|
export const PLAN_VIDEOS_META_RELATIVE_PATH = "meta/videos.json";
|
|
|
|
/**
|
|
* On-disk shape of `<planDir>/meta/videos.json`. The engine's
|
|
* `ExtractedFrames` shape carries an absolute `outputDir`, a `framePaths`
|
|
* Map, and potentially an open file descriptor — none of those survive
|
|
* a serialize → re-deserialize round trip across processes. The
|
|
* serialized form keeps only what plan-time produced; `renderChunk` re-
|
|
* derives `outputDir` (always `<planDir>/video-frames/<videoId>`) and
|
|
* `framePaths` (re-listed from that directory) when reconstructing the
|
|
* `FrameLookupTable`.
|
|
*/
|
|
export interface PlanVideosJson {
|
|
videos: VideoElement[];
|
|
extracted: Array<{
|
|
videoId: string;
|
|
srcPath: string;
|
|
framePattern: string;
|
|
fps: number;
|
|
totalFrames: number;
|
|
metadata: VideoMetadata;
|
|
}>;
|
|
}
|
|
|
|
const execFile = promisify(execFileCallback);
|
|
|
|
/**
|
|
* Cached first line of `ffmpeg -version` (e.g. `"ffmpeg version 6.1.1"`).
|
|
* Cached because workers that fan out multiple `renderChunk()` calls in the
|
|
* same process (Cloud Run Jobs, Temporal activity workers) would otherwise
|
|
* spawn ffmpeg once per chunk just to read the version — ~20-50ms each.
|
|
*/
|
|
let cachedFfmpegVersion: string | null = null;
|
|
|
|
/**
|
|
* Read `ffmpeg -version` first line. The string is opaque — `planHash`
|
|
* mixes it in verbatim, so any drift across worker hosts trips a
|
|
* `FFMPEG_VERSION_MISMATCH` rather than producing pixels that subtly
|
|
* disagree with the plan's baked-in encoder args.
|
|
*/
|
|
export async function readFfmpegVersion(): Promise<string> {
|
|
if (cachedFfmpegVersion !== null) return cachedFfmpegVersion;
|
|
const { stdout } = await execFile("ffmpeg", ["-version"], { maxBuffer: 1024 * 1024 });
|
|
const firstLine = stdout.split(/\r?\n/)[0]?.trim() ?? "";
|
|
if (!firstLine) {
|
|
throw new Error("ffmpeg -version returned empty output");
|
|
}
|
|
cachedFfmpegVersion = firstLine;
|
|
return firstLine;
|
|
}
|
|
|
|
/** Test-only: clear the cached ffmpeg version so a fresh probe runs. */
|
|
function _resetFfmpegVersionCacheForTests(): void {
|
|
cachedFfmpegVersion = null;
|
|
}
|
|
|
|
/**
|
|
* Inputs for {@link buildSyntheticRenderJob}. The two distributed activity
|
|
* scripts (`plan.ts`, `renderChunk.ts`) reach for slightly different
|
|
* sources — caller config vs. frozen `LockedRenderConfig` — but the
|
|
* resulting `RenderJob` shape is identical, so the helper accepts both.
|
|
*/
|
|
export interface SyntheticRenderJobInput {
|
|
fps: Fps;
|
|
format: RenderConfig["format"];
|
|
quality: RenderConfig["quality"];
|
|
crf?: number;
|
|
bitrate?: string;
|
|
videoFrameFormat?: VideoFrameFormat;
|
|
outputResolution?: RenderConfig["outputResolution"];
|
|
hdrMode: RenderConfig["hdrMode"];
|
|
entryFile: string;
|
|
logger?: ProducerLogger;
|
|
producerConfig?: RenderConfig["producerConfig"];
|
|
}
|
|
|
|
/**
|
|
* Synthesize a `RenderJob` from a distributed-render config. The distributed
|
|
* activities operate without a full `RenderJob` (they're stateless workers),
|
|
* so we build one to feed the existing stage interfaces.
|
|
*/
|
|
export function buildSyntheticRenderJob(input: SyntheticRenderJobInput): RenderJob {
|
|
const renderConfig: RenderConfig = {
|
|
fps: input.fps,
|
|
quality: input.quality,
|
|
format: input.format,
|
|
crf: input.crf,
|
|
videoBitrate: input.bitrate,
|
|
videoFrameFormat: input.videoFrameFormat,
|
|
outputResolution: input.outputResolution,
|
|
// Distributed mode hard-pins to software GPU. The plan-time validator
|
|
// refuses to fan out otherwise.
|
|
useGpu: false,
|
|
debug: false,
|
|
entryFile: input.entryFile,
|
|
logger: input.logger ?? defaultLogger,
|
|
hdrMode: input.hdrMode,
|
|
producerConfig: input.producerConfig,
|
|
};
|
|
return createRenderJob(renderConfig);
|
|
}
|
|
|
|
/**
|
|
* Resolve the producer package version by walking up from the calling
|
|
* module until a `package.json` whose `name === "@hyperframes/producer"`
|
|
* is found. Works for both the bundled `dist/index.js` (1 level up) and
|
|
* the unbundled source tree (4 levels up).
|
|
*
|
|
* Cached at module load — the version is fixed for the life of the process,
|
|
* and reading the package.json over and over wastes per-chunk syscalls.
|
|
*/
|
|
let cachedProducerVersion: string | null = null;
|
|
export function readProducerVersion(): string {
|
|
if (cachedProducerVersion !== null) return cachedProducerVersion;
|
|
const startDir = dirname(fileURLToPath(import.meta.url));
|
|
let current = startDir;
|
|
for (let i = 0; i < 10; i++) {
|
|
const candidate = join(current, "package.json");
|
|
if (existsSync(candidate)) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(candidate, "utf-8")) as {
|
|
name?: string;
|
|
version?: string;
|
|
};
|
|
if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
|
|
cachedProducerVersion = pkg.version;
|
|
return pkg.version;
|
|
}
|
|
} catch {
|
|
// Fall through to the next ancestor.
|
|
}
|
|
}
|
|
const parent = dirname(current);
|
|
if (parent === current) break;
|
|
current = parent;
|
|
}
|
|
cachedProducerVersion = "0.0.0-unknown";
|
|
return cachedProducerVersion;
|
|
}
|