mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
Revert "feat(producer): renderStretch to re-time short compositions across longer scenes (#2676)" (#2730)
This reverts commit e786b78b33.
This commit is contained in:
@@ -38,18 +38,6 @@ export function fpsToNumber(fps: Fps): number {
|
|||||||
return fps.num / fps.den;
|
return fps.num / fps.den;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Timeline seek time for an output frame. `renderStretch` (=intrinsic/target,
|
|
||||||
* default 1 = no-op) scales the mapping so a short comp spans a longer output.
|
|
||||||
*/
|
|
||||||
export function outputFrameToTimelineSeconds(
|
|
||||||
frameIndex: number,
|
|
||||||
fps: Fps,
|
|
||||||
renderStretch = 1,
|
|
||||||
): number {
|
|
||||||
return ((frameIndex * fps.den) / fps.num) * renderStretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FFmpeg-style fps argument. Returns `"30"` for integer fps and `"30000/1001"`
|
* FFmpeg-style fps argument. Returns `"30"` for integer fps and `"30000/1001"`
|
||||||
* for rationals — both forms are accepted verbatim by FFmpeg's `-r` and
|
* for rationals — both forms are accepted verbatim by FFmpeg's `-r` and
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ export {
|
|||||||
parseFpsWithDefault,
|
parseFpsWithDefault,
|
||||||
toFps,
|
toFps,
|
||||||
fpsToNumber,
|
fpsToNumber,
|
||||||
outputFrameToTimelineSeconds,
|
|
||||||
fpsToFfmpegArg,
|
fpsToFfmpegArg,
|
||||||
TIMELINE_COLORS,
|
TIMELINE_COLORS,
|
||||||
DEFAULT_DURATIONS,
|
DEFAULT_DURATIONS,
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { outputFrameToTimelineSeconds } from "./core.types.js";
|
|
||||||
|
|
||||||
describe("outputFrameToTimelineSeconds", () => {
|
|
||||||
const fps = { num: 30, den: 1 };
|
|
||||||
|
|
||||||
it("no-op when renderStretch is omitted (defaults to 1)", () => {
|
|
||||||
for (const i of [0, 1, 29, 143]) {
|
|
||||||
expect(outputFrameToTimelineSeconds(i, fps)).toBe((i * fps.den) / fps.num);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renderStretch=1 is byte-identical to the raw frame time", () => {
|
|
||||||
expect(outputFrameToTimelineSeconds(143, fps, 1)).toBe(143 / 30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stretches a 1s comp across a 4.8s output (renderStretch = intrinsic/target)", () => {
|
|
||||||
const rs = 1 / 4.8;
|
|
||||||
expect(outputFrameToTimelineSeconds(0, fps, rs)).toBe(0);
|
|
||||||
// last of 144 output frames lands just under intrinsic 1.0s — never past it
|
|
||||||
const last = outputFrameToTimelineSeconds(143, fps, rs);
|
|
||||||
expect(last).toBeGreaterThan(0.98);
|
|
||||||
expect(last).toBeLessThan(1.0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("honors an exact rational fps (NTSC 30000/1001)", () => {
|
|
||||||
const ntsc = { num: 30000, den: 1001 };
|
|
||||||
expect(outputFrameToTimelineSeconds(30, ntsc, 1)).toBe((30 * 1001) / 30000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -2709,9 +2709,8 @@ export async function verifyStaticFramesSafe(
|
|||||||
if (last && f === last.b + 1) last.b = f;
|
if (last && f === last.b + 1) last.b = f;
|
||||||
else runs.push({ a: f, b: f });
|
else runs.push({ a: f, b: f });
|
||||||
}
|
}
|
||||||
const renderStretch = session.options.renderStretch ?? 1;
|
|
||||||
const seekToFrame = async (frameIdx: number): Promise<void> => {
|
const seekToFrame = async (frameIdx: number): Promise<void> => {
|
||||||
const t = quantizeTimeToFrame((frameIdx / fps) * renderStretch, fps);
|
const t = quantizeTimeToFrame(frameIdx / fps, fps);
|
||||||
await page.evaluate((tt: number) => {
|
await page.evaluate((tt: number) => {
|
||||||
const hf = (
|
const hf = (
|
||||||
window as unknown as {
|
window as unknown as {
|
||||||
@@ -3649,14 +3648,11 @@ async function captureDeVerificationFrames(
|
|||||||
// their data-duration, and infinite-repeat GSAP reports a huge sentinel —
|
// their data-duration, and infinite-repeat GSAP reports a huge sentinel —
|
||||||
// and indices derived from it would never be drained, silently disarming
|
// and indices derived from it would never be drained, silently disarming
|
||||||
// verification for exactly the comps that need it.
|
// verification for exactly the comps that need it.
|
||||||
// compositionDurationSeconds is already the output (drained) duration; the raw
|
|
||||||
// page fallback is intrinsic — divide it by renderStretch to match (1 = no-op).
|
|
||||||
const renderStretch = session.options.renderStretch ?? 1;
|
|
||||||
const duration =
|
const duration =
|
||||||
session.options.compositionDurationSeconds ??
|
session.options.compositionDurationSeconds ??
|
||||||
(await page.evaluate(
|
(await page.evaluate(
|
||||||
() => (window as unknown as { __hf?: { duration?: number } }).__hf?.duration ?? 0,
|
() => (window as unknown as { __hf?: { duration?: number } }).__hf?.duration ?? 0,
|
||||||
)) / renderStretch;
|
));
|
||||||
const totalFrames = Math.floor(duration * fps);
|
const totalFrames = Math.floor(duration * fps);
|
||||||
if (totalFrames < 10) return;
|
if (totalFrames < 10) return;
|
||||||
if (duration > 3600) {
|
if (duration > 3600) {
|
||||||
@@ -3706,8 +3702,7 @@ async function captureDeVerificationFrames(
|
|||||||
while (boundary.has(idx) && guard++ < 6) idx = Math.min(totalFrames - 1, idx + 2);
|
while (boundary.has(idx) && guard++ < 6) idx = Math.min(totalFrames - 1, idx + 2);
|
||||||
if (boundary.has(idx)) continue;
|
if (boundary.has(idx)) continue;
|
||||||
if (frames.has(idx)) continue;
|
if (frames.has(idx)) continue;
|
||||||
// Seek truth with the same ×renderStretch mapping the real capture uses for output frame idx.
|
const t = quantizeTimeToFrame(idx / fps, fps);
|
||||||
const t = quantizeTimeToFrame((idx / fps) * renderStretch, fps);
|
|
||||||
await seekTo(t);
|
await seekTo(t);
|
||||||
// Video frame injection (same hook the real capture paths run) — without
|
// Video frame injection (same hook the real capture paths run) — without
|
||||||
// it, <video> elements screenshot black and every video comp would
|
// it, <video> elements screenshot black and every video comp would
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
type CapturePerfSummary,
|
type CapturePerfSummary,
|
||||||
type BeforeCaptureHook,
|
type BeforeCaptureHook,
|
||||||
} from "./frameCapture.js";
|
} from "./frameCapture.js";
|
||||||
import { outputFrameToTimelineSeconds } from "@hyperframes/core";
|
|
||||||
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
|
import { DEFAULT_CONFIG, type EngineConfig } from "../config.js";
|
||||||
import { assertSwiftShader } from "../utils/assertSwiftShader.js";
|
import { assertSwiftShader } from "../utils/assertSwiftShader.js";
|
||||||
import { readWebGlVendorInfoFromCanvas } from "../utils/readWebGlVendorInfoFromCanvas.js";
|
import { readWebGlVendorInfoFromCanvas } from "../utils/readWebGlVendorInfoFromCanvas.js";
|
||||||
@@ -369,10 +368,6 @@ async function captureFrameRange(
|
|||||||
let framesCaptured = 0;
|
let framesCaptured = 0;
|
||||||
const outputOffset = task.outputFrameOffset ?? 0;
|
const outputOffset = task.outputFrameOffset ?? 0;
|
||||||
const stride = task.frameStride ?? 1;
|
const stride = task.frameStride ?? 1;
|
||||||
// Per-frame seek ×renderStretch maps frames across [0, intrinsic] (1 = no-op).
|
|
||||||
const renderStretch = captureOptions.renderStretch ?? 1;
|
|
||||||
const seekTime = (i: number): number =>
|
|
||||||
outputFrameToTimelineSeconds(i, captureOptions.fps, renderStretch);
|
|
||||||
// Depth-2 pipelined drawElement produce (HF_DE_PARALLEL_STREAM spike): frame
|
// Depth-2 pipelined drawElement produce (HF_DE_PARALLEL_STREAM spike): frame
|
||||||
// k's in-page worker encode overlaps frame k+stride's produce phase — the
|
// k's in-page worker encode overlaps frame k+stride's produce phase — the
|
||||||
// same shape as the sequential worker-encode loop. Only engaged when the
|
// same shape as the sequential worker-encode loop. Only engaged when the
|
||||||
@@ -392,7 +387,7 @@ async function captureFrameRange(
|
|||||||
let prev: { idx: number; encodeResult: Promise<Buffer> } | null = null;
|
let prev: { idx: number; encodeResult: Promise<Buffer> } | null = null;
|
||||||
for (let i = task.startFrame; i < task.endFrame; i += stride) {
|
for (let i = task.startFrame; i < task.endFrame; i += stride) {
|
||||||
if (signal?.aborted) throw new Error("Parallel worker cancelled");
|
if (signal?.aborted) throw new Error("Parallel worker cancelled");
|
||||||
const time = seekTime(i);
|
const time = (i * captureOptions.fps.den) / captureOptions.fps.num;
|
||||||
if (dbg && i < task.startFrame + dbgWin) {
|
if (dbg && i < task.startFrame + dbgWin) {
|
||||||
console.log(`[par:w${task.workerId}] +${Date.now() - dbgT0}ms produce ${i} start`);
|
console.log(`[par:w${task.workerId}] +${Date.now() - dbgT0}ms produce ${i} start`);
|
||||||
}
|
}
|
||||||
@@ -436,7 +431,7 @@ async function captureFrameRange(
|
|||||||
}
|
}
|
||||||
for (let i = task.startFrame; i < task.endFrame; i += stride) {
|
for (let i = task.startFrame; i < task.endFrame; i += stride) {
|
||||||
if (signal?.aborted) throw new Error("Parallel worker cancelled");
|
if (signal?.aborted) throw new Error("Parallel worker cancelled");
|
||||||
const time = seekTime(i);
|
const time = (i * captureOptions.fps.den) / captureOptions.fps.num;
|
||||||
const fileFrameIdx = i - outputOffset;
|
const fileFrameIdx = i - outputOffset;
|
||||||
|
|
||||||
if (onFrameBuffer) {
|
if (onFrameBuffer) {
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ export interface CaptureOptions {
|
|||||||
* timelines can outrun their declared duration). Consumers that derive
|
* timelines can outrun their declared duration). Consumers that derive
|
||||||
* frame indices meant to be drained by the producer (drawElement
|
* frame indices meant to be drained by the producer (drawElement
|
||||||
* self-verification) MUST prefer this over `__hf.duration`.
|
* self-verification) MUST prefer this over `__hf.duration`.
|
||||||
* When renderStretch != 1 this is the OUTPUT (stretched) duration, not intrinsic.
|
|
||||||
*/
|
*/
|
||||||
compositionDurationSeconds?: number;
|
compositionDurationSeconds?: number;
|
||||||
/**
|
/**
|
||||||
@@ -125,7 +124,6 @@ export interface CaptureOptions {
|
|||||||
* rational form verbatim — see `fpsToFfmpegArg`.
|
* rational form verbatim — see `fpsToFfmpegArg`.
|
||||||
*/
|
*/
|
||||||
fps: Fps;
|
fps: Fps;
|
||||||
renderStretch?: number;
|
|
||||||
format?: "jpeg" | "png";
|
format?: "jpeg" | "png";
|
||||||
quality?: number;
|
quality?: number;
|
||||||
deviceScaleFactor?: number;
|
deviceScaleFactor?: number;
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ export interface RenderRequestOptions {
|
|||||||
variables?: Record<string, unknown>;
|
variables?: Record<string, unknown>;
|
||||||
outputResolution?: CanvasResolution;
|
outputResolution?: CanvasResolution;
|
||||||
outputResolutionAspectAgnostic?: boolean;
|
outputResolutionAspectAgnostic?: boolean;
|
||||||
/** intrinsic/target timeline re-time; 1 = no-op. Audio is silence-padded to output length, not time-stretched. */
|
|
||||||
renderStretch?: number;
|
|
||||||
engineConfig: EngineConfig;
|
engineConfig: EngineConfig;
|
||||||
distributed?: DistributedRenderOptions;
|
distributed?: DistributedRenderOptions;
|
||||||
}
|
}
|
||||||
@@ -107,13 +105,6 @@ function assertOptionalInteger(options: Record<string, unknown>, field: string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertOptionalPositiveNumber(options: Record<string, unknown>, field: string): void {
|
|
||||||
const value = options[field];
|
|
||||||
if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || value <= 0)) {
|
|
||||||
throw new Error(`Render request ${field} must be a positive number`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertOptionalString(options: Record<string, unknown>, field: string): void {
|
function assertOptionalString(options: Record<string, unknown>, field: string): void {
|
||||||
if (options[field] !== undefined && typeof options[field] !== "string") {
|
if (options[field] !== undefined && typeof options[field] !== "string") {
|
||||||
throw new Error(`Render request ${field} must be a string`);
|
throw new Error(`Render request ${field} must be a string`);
|
||||||
@@ -169,7 +160,6 @@ function assertRequestOptionScalars(options: Record<string, unknown>): void {
|
|||||||
assertOptionalInteger(options, "gifLoop");
|
assertOptionalInteger(options, "gifLoop");
|
||||||
assertOptionalInteger(options, "workers", 1);
|
assertOptionalInteger(options, "workers", 1);
|
||||||
assertOptionalInteger(options, "crf");
|
assertOptionalInteger(options, "crf");
|
||||||
assertOptionalPositiveNumber(options, "renderStretch");
|
|
||||||
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"] as const) {
|
for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"] as const) {
|
||||||
assertOptionalBoolean(options, field);
|
assertOptionalBoolean(options, field);
|
||||||
}
|
}
|
||||||
@@ -294,7 +284,6 @@ export function distributedConfigFromRequest(
|
|||||||
videoFrameFormat: options.videoFrameFormat,
|
videoFrameFormat: options.videoFrameFormat,
|
||||||
outputResolution: options.outputResolution,
|
outputResolution: options.outputResolution,
|
||||||
outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
|
outputResolutionAspectAgnostic: options.outputResolutionAspectAgnostic,
|
||||||
renderStretch: options.renderStretch,
|
|
||||||
chunkSize: distributed.chunkSize,
|
chunkSize: distributed.chunkSize,
|
||||||
maxParallelChunks: distributed.maxParallelChunks,
|
maxParallelChunks: distributed.maxParallelChunks,
|
||||||
targetChunkFrames: distributed.targetChunkFrames,
|
targetChunkFrames: distributed.targetChunkFrames,
|
||||||
@@ -350,7 +339,6 @@ export function renderRequestFromDistributedConfig(input: {
|
|||||||
...optionalProperty("videoFrameFormat", config.videoFrameFormat),
|
...optionalProperty("videoFrameFormat", config.videoFrameFormat),
|
||||||
...optionalProperty("outputResolution", config.outputResolution),
|
...optionalProperty("outputResolution", config.outputResolution),
|
||||||
...optionalProperty("outputResolutionAspectAgnostic", config.outputResolutionAspectAgnostic),
|
...optionalProperty("outputResolutionAspectAgnostic", config.outputResolutionAspectAgnostic),
|
||||||
...optionalProperty("renderStretch", config.renderStretch),
|
|
||||||
...optionalProperty("hdrMode", config.hdrMode),
|
...optionalProperty("hdrMode", config.hdrMode),
|
||||||
...optionalProperty("strictness", config.strictness),
|
...optionalProperty("strictness", config.strictness),
|
||||||
...optionalProperty("entryFile", config.entryFile),
|
...optionalProperty("entryFile", config.entryFile),
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ interface RenderInput {
|
|||||||
* compositions as an aspect-ratio mismatch.
|
* compositions as an aspect-ratio mismatch.
|
||||||
*/
|
*/
|
||||||
outputResolutionAspectAgnostic?: boolean;
|
outputResolutionAspectAgnostic?: boolean;
|
||||||
renderStretch?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PreparedRenderInput {
|
interface PreparedRenderInput {
|
||||||
@@ -168,10 +167,6 @@ export function parseRenderOptions(body: Record<string, unknown>): Omit<RenderIn
|
|||||||
|
|
||||||
const { variables, outputResolution, outputResolutionAspectAgnostic } =
|
const { variables, outputResolution, outputResolutionAspectAgnostic } =
|
||||||
parseRenderOverrides(body);
|
parseRenderOverrides(body);
|
||||||
const renderStretch =
|
|
||||||
typeof body.renderStretch === "number" && body.renderStretch > 0
|
|
||||||
? body.renderStretch
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
outputPath,
|
outputPath,
|
||||||
@@ -187,7 +182,6 @@ export function parseRenderOptions(body: Record<string, unknown>): Omit<RenderIn
|
|||||||
outputResolution,
|
outputResolution,
|
||||||
outputResolutionAspectAgnostic,
|
outputResolutionAspectAgnostic,
|
||||||
videoFrameFormat,
|
videoFrameFormat,
|
||||||
renderStretch,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +239,6 @@ function buildRenderJobConfig(input: RenderInput, outputPath: string, log: Produ
|
|||||||
outputResolution: input.outputResolution,
|
outputResolution: input.outputResolution,
|
||||||
outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
|
outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
|
||||||
videoFrameFormat: input.videoFrameFormat,
|
videoFrameFormat: input.videoFrameFormat,
|
||||||
renderStretch: input.renderStretch,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return renderConfigFromRequest(request, { logger: log });
|
return renderConfigFromRequest(request, { logger: log });
|
||||||
|
|||||||
@@ -135,9 +135,6 @@ export interface DistributedRenderConfig {
|
|||||||
*/
|
*/
|
||||||
outputResolutionAspectAgnostic?: boolean;
|
outputResolutionAspectAgnostic?: boolean;
|
||||||
|
|
||||||
/** intrinsic/target timeline re-time; 1 (or omitted) = no-op. Folded into planHash. */
|
|
||||||
renderStretch?: number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frames per chunk. When explicitly set, that value is used and
|
* Frames per chunk. When explicitly set, that value is used and
|
||||||
* `chunkCount = min(maxParallelChunks, ceil(totalFrames / chunkSize))`
|
* `chunkCount = min(maxParallelChunks, ceil(totalFrames / chunkSize))`
|
||||||
@@ -774,7 +771,6 @@ export async function plan(
|
|||||||
videoFrameFormat: config.videoFrameFormat,
|
videoFrameFormat: config.videoFrameFormat,
|
||||||
outputResolution: config.outputResolution,
|
outputResolution: config.outputResolution,
|
||||||
outputResolutionAspectAgnostic: config.outputResolutionAspectAgnostic,
|
outputResolutionAspectAgnostic: config.outputResolutionAspectAgnostic,
|
||||||
renderStretch: config.renderStretch,
|
|
||||||
// HDR is banned in distributed mode. force-sdr keeps the
|
// HDR is banned in distributed mode. force-sdr keeps the
|
||||||
// extract / encoder paths off the HDR branches entirely.
|
// extract / encoder paths off the HDR branches entirely.
|
||||||
hdrMode: config.hdrMode ?? "force-sdr",
|
hdrMode: config.hdrMode ?? "force-sdr",
|
||||||
@@ -1041,7 +1037,6 @@ export async function plan(
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
format: config.format,
|
format: config.format,
|
||||||
renderStretch: config.renderStretch,
|
|
||||||
};
|
};
|
||||||
// Clean up the temp work tree BEFORE freezePlan. `.plan-work/` holds
|
// Clean up the temp work tree BEFORE freezePlan. `.plan-work/` holds
|
||||||
// intermediate compileStage + audio-mix artifacts (downloaded source
|
// intermediate compileStage + audio-mix artifacts (downloaded source
|
||||||
|
|||||||
@@ -239,7 +239,6 @@ interface PlanJson {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
format: DistributedFormat;
|
format: DistributedFormat;
|
||||||
renderStretch?: number;
|
|
||||||
};
|
};
|
||||||
chunkCount: number;
|
chunkCount: number;
|
||||||
totalFrames: number;
|
totalFrames: number;
|
||||||
@@ -516,8 +515,6 @@ export async function renderChunk(
|
|||||||
width: plan.dimensions.width,
|
width: plan.dimensions.width,
|
||||||
height: plan.dimensions.height,
|
height: plan.dimensions.height,
|
||||||
fps: { num: plan.dimensions.fpsNum, den: plan.dimensions.fpsDen },
|
fps: { num: plan.dimensions.fpsNum, den: plan.dimensions.fpsDen },
|
||||||
// Per-frame seek ×renderStretch maps output frames across [0, intrinsic] (1 = no-op).
|
|
||||||
renderStretch: plan.dimensions.renderStretch ?? 1,
|
|
||||||
format: plan.dimensions.format === "mp4" ? "jpeg" : "png",
|
format: plan.dimensions.format === "mp4" ? "jpeg" : "png",
|
||||||
quality: plan.dimensions.format === "mp4" ? 80 : undefined,
|
quality: plan.dimensions.format === "mp4" ? 80 : undefined,
|
||||||
deviceScaleFactor: encoder.deviceScaleFactor,
|
deviceScaleFactor: encoder.deviceScaleFactor,
|
||||||
|
|||||||
@@ -187,18 +187,6 @@ export function validateDistributedRenderConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
config.renderStretch !== undefined &&
|
|
||||||
(typeof config.renderStretch !== "number" ||
|
|
||||||
!Number.isFinite(config.renderStretch) ||
|
|
||||||
config.renderStretch <= 0)
|
|
||||||
) {
|
|
||||||
throw new InvalidConfigError(
|
|
||||||
"config.renderStretch",
|
|
||||||
`must be a positive number; got ${String(config.renderStretch)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.runtimeCap !== undefined && !ALLOWED_RUNTIME_CAPS.includes(config.runtimeCap)) {
|
if (config.runtimeCap !== undefined && !ALLOWED_RUNTIME_CAPS.includes(config.runtimeCap)) {
|
||||||
throw new InvalidConfigError(
|
throw new InvalidConfigError(
|
||||||
"config.runtimeCap",
|
"config.runtimeCap",
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ export interface SyntheticRenderJobInput {
|
|||||||
videoFrameFormat?: VideoFrameFormat;
|
videoFrameFormat?: VideoFrameFormat;
|
||||||
outputResolution?: RenderConfig["outputResolution"];
|
outputResolution?: RenderConfig["outputResolution"];
|
||||||
outputResolutionAspectAgnostic?: RenderConfig["outputResolutionAspectAgnostic"];
|
outputResolutionAspectAgnostic?: RenderConfig["outputResolutionAspectAgnostic"];
|
||||||
renderStretch?: number;
|
|
||||||
hdrMode: RenderConfig["hdrMode"];
|
hdrMode: RenderConfig["hdrMode"];
|
||||||
strictness?: RenderConfig["strictness"];
|
strictness?: RenderConfig["strictness"];
|
||||||
entryFile: string;
|
entryFile: string;
|
||||||
@@ -125,7 +124,6 @@ export function buildSyntheticRenderJob(input: SyntheticRenderJobInput): RenderJ
|
|||||||
videoFrameFormat: input.videoFrameFormat,
|
videoFrameFormat: input.videoFrameFormat,
|
||||||
outputResolution: input.outputResolution,
|
outputResolution: input.outputResolution,
|
||||||
outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
|
outputResolutionAspectAgnostic: input.outputResolutionAspectAgnostic,
|
||||||
renderStretch: input.renderStretch,
|
|
||||||
// Distributed mode hard-pins to software GPU. The plan-time validator
|
// Distributed mode hard-pins to software GPU. The plan-time validator
|
||||||
// refuses to fan out otherwise.
|
// refuses to fan out otherwise.
|
||||||
useGpu: false,
|
useGpu: false,
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
initTransparentBackground,
|
initTransparentBackground,
|
||||||
initializeSession,
|
initializeSession,
|
||||||
} from "@hyperframes/engine";
|
} from "@hyperframes/engine";
|
||||||
import { outputFrameToTimelineSeconds } from "@hyperframes/core";
|
|
||||||
import type { FileServerHandle } from "../../fileServer.js";
|
import type { FileServerHandle } from "../../fileServer.js";
|
||||||
import type { ProducerLogger } from "../../../logger.js";
|
import type { ProducerLogger } from "../../../logger.js";
|
||||||
import {
|
import {
|
||||||
@@ -217,7 +216,7 @@ export async function runHybridLayeredFrameLoop(input: HybridLoopInput): Promise
|
|||||||
let nextRingIdx = 0;
|
let nextRingIdx = 0;
|
||||||
for (let i = range.start; i < range.end; i++) {
|
for (let i = range.start; i < range.end; i++) {
|
||||||
assertNotAborted();
|
assertNotAborted();
|
||||||
const time = outputFrameToTimelineSeconds(i, job.config.fps, job.config.renderStretch ?? 1);
|
const time = (i * job.config.fps.den) / job.config.fps.num;
|
||||||
const activeTransition = transitionFramesSet.has(i)
|
const activeTransition = transitionFramesSet.has(i)
|
||||||
? transitionRanges.find((t) => i >= t.startFrame && i <= t.endFrame)
|
? transitionRanges.find((t) => i >= t.startFrame && i <= t.endFrame)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
TRANSITIONS,
|
TRANSITIONS,
|
||||||
crossfade,
|
crossfade,
|
||||||
} from "@hyperframes/engine";
|
} from "@hyperframes/engine";
|
||||||
import { outputFrameToTimelineSeconds } from "@hyperframes/core";
|
|
||||||
import type { ProducerLogger } from "../../../logger.js";
|
import type { ProducerLogger } from "../../../logger.js";
|
||||||
import {
|
import {
|
||||||
type HdrCompositeContext,
|
type HdrCompositeContext,
|
||||||
@@ -107,7 +106,7 @@ export async function runSequentialLayeredFrameLoop(input: SequentialLoopInput):
|
|||||||
|
|
||||||
for (let i = 0; i < totalFrames; i++) {
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
assertNotAborted();
|
assertNotAborted();
|
||||||
const time = outputFrameToTimelineSeconds(i, job.config.fps, job.config.renderStretch ?? 1);
|
const time = (i * job.config.fps.den) / job.config.fps.num;
|
||||||
if (hdrPerf) hdrPerf.frames += 1;
|
if (hdrPerf) hdrPerf.frames += 1;
|
||||||
|
|
||||||
const stackingInfo = await seekInjectAndQueryStacking(
|
const stackingInfo = await seekInjectAndQueryStacking(
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ import {
|
|||||||
initializeSession,
|
initializeSession,
|
||||||
prepareCaptureSessionForReuse,
|
prepareCaptureSessionForReuse,
|
||||||
} from "@hyperframes/engine";
|
} from "@hyperframes/engine";
|
||||||
import { outputFrameToTimelineSeconds } from "@hyperframes/core";
|
|
||||||
import type { FileServerHandle } from "../../fileServer.js";
|
import type { FileServerHandle } from "../../fileServer.js";
|
||||||
import type { ProducerLogger } from "../../../logger.js";
|
import type { ProducerLogger } from "../../../logger.js";
|
||||||
import {
|
import {
|
||||||
@@ -317,11 +316,7 @@ export async function runCaptureStage(input: CaptureStageInput): Promise<Capture
|
|||||||
for (let i = 0; i < rangeFrames; i++) {
|
for (let i = 0; i < rangeFrames; i++) {
|
||||||
assertNotAborted();
|
assertNotAborted();
|
||||||
const absoluteIdx = rangeStart + i;
|
const absoluteIdx = rangeStart + i;
|
||||||
const time = outputFrameToTimelineSeconds(
|
const time = (absoluteIdx * job.config.fps.den) / job.config.fps.num;
|
||||||
absoluteIdx,
|
|
||||||
job.config.fps,
|
|
||||||
job.config.renderStretch ?? 1,
|
|
||||||
);
|
|
||||||
const { encodeResult } = await captureFrameToBufferPipelined(session, i, time);
|
const { encodeResult } = await captureFrameToBufferPipelined(session, i, time);
|
||||||
await drainPrev();
|
await drainPrev();
|
||||||
prev = { fileIndex: i, encodeResult };
|
prev = { fileIndex: i, encodeResult };
|
||||||
@@ -331,11 +326,7 @@ export async function runCaptureStage(input: CaptureStageInput): Promise<Capture
|
|||||||
for (let i = 0; i < rangeFrames; i++) {
|
for (let i = 0; i < rangeFrames; i++) {
|
||||||
assertNotAborted();
|
assertNotAborted();
|
||||||
const absoluteIdx = rangeStart + i;
|
const absoluteIdx = rangeStart + i;
|
||||||
const time = outputFrameToTimelineSeconds(
|
const time = (absoluteIdx * job.config.fps.den) / job.config.fps.num;
|
||||||
absoluteIdx,
|
|
||||||
job.config.fps,
|
|
||||||
job.config.renderStretch ?? 1,
|
|
||||||
);
|
|
||||||
await captureFrame(session, i, time);
|
await captureFrame(session, i, time);
|
||||||
reportFrame(i);
|
reportFrame(i);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ import {
|
|||||||
prepareCaptureSessionForReuse,
|
prepareCaptureSessionForReuse,
|
||||||
spawnStreamingEncoder,
|
spawnStreamingEncoder,
|
||||||
} from "@hyperframes/engine";
|
} from "@hyperframes/engine";
|
||||||
import { outputFrameToTimelineSeconds } from "@hyperframes/core";
|
|
||||||
import type { FileServerHandle } from "../../fileServer.js";
|
import type { FileServerHandle } from "../../fileServer.js";
|
||||||
import type { ProducerLogger } from "../../../logger.js";
|
import type { ProducerLogger } from "../../../logger.js";
|
||||||
import type { ProgressCallback, RenderJob } from "../../renderOrchestrator.js";
|
import type { ProgressCallback, RenderJob } from "../../renderOrchestrator.js";
|
||||||
@@ -406,8 +405,7 @@ async function runWorkerEncodePipelineLoop(
|
|||||||
abortSignal: AbortSignal | undefined,
|
abortSignal: AbortSignal | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let prev: { idx: number; encodeResult: Promise<Buffer> } | null = null;
|
let prev: { idx: number; encodeResult: Promise<Buffer> } | null = null;
|
||||||
const frameTime = (i: number) =>
|
const frameTime = (i: number) => (i * job.config.fps.den) / job.config.fps.num;
|
||||||
outputFrameToTimelineSeconds(i, job.config.fps, job.config.renderStretch ?? 1);
|
|
||||||
const guard = createDrainFrameGuard({ log, stats, frameTime });
|
const guard = createDrainFrameGuard({ log, stats, frameTime });
|
||||||
const guardFrame = (idx: number, buf: Buffer): Promise<Buffer> => guard(session, idx, buf);
|
const guardFrame = (idx: number, buf: Buffer): Promise<Buffer> => guard(session, idx, buf);
|
||||||
|
|
||||||
@@ -642,8 +640,7 @@ export async function runCaptureStreamingStage(
|
|||||||
const parallelGuard = createDrainFrameGuard({
|
const parallelGuard = createDrainFrameGuard({
|
||||||
log,
|
log,
|
||||||
stats: parallelStats,
|
stats: parallelStats,
|
||||||
frameTime: (i: number) =>
|
frameTime: (i: number) => (i * job.config.fps.den) / job.config.fps.num,
|
||||||
outputFrameToTimelineSeconds(i, job.config.fps, job.config.renderStretch ?? 1),
|
|
||||||
});
|
});
|
||||||
let parallelGuardRan = false;
|
let parallelGuardRan = false;
|
||||||
// First guard/write failure aborts the reorder buffer so peer workers
|
// First guard/write failure aborts the reorder buffer so peer workers
|
||||||
@@ -838,11 +835,7 @@ export async function runCaptureStreamingStage(
|
|||||||
let lastProgressAt = Date.now();
|
let lastProgressAt = Date.now();
|
||||||
for (let i = 0; i < totalFrames; i++) {
|
for (let i = 0; i < totalFrames; i++) {
|
||||||
assertNotAborted();
|
assertNotAborted();
|
||||||
const time = outputFrameToTimelineSeconds(
|
const time = (i * job.config.fps.den) / job.config.fps.num;
|
||||||
i,
|
|
||||||
job.config.fps,
|
|
||||||
job.config.renderStretch ?? 1,
|
|
||||||
);
|
|
||||||
const { buffer } = await raceAgainstStall(
|
const { buffer } = await raceAgainstStall(
|
||||||
captureFrameToBuffer(session, i, time),
|
captureFrameToBuffer(session, i, time),
|
||||||
stallTimeoutMs - (Date.now() - lastProgressAt),
|
stallTimeoutMs - (Date.now() - lastProgressAt),
|
||||||
|
|||||||
@@ -251,23 +251,3 @@ describe("sha256Hex", () => {
|
|||||||
expect(sha256Hex(s)).toBe(sha256Hex(new TextEncoder().encode(s)));
|
expect(sha256Hex(s)).toBe(sha256Hex(new TextEncoder().encode(s)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("computePlanHash — renderStretch byte-identity", () => {
|
|
||||||
const baseDims = { fpsNum: 30, fpsDen: 1, width: 1920, height: 1080, format: "mp4" as const };
|
|
||||||
|
|
||||||
it("renderStretch=1 and undefined hash identically to a pre-renderStretch plan", () => {
|
|
||||||
const omitted = computePlanHash(makeInput({ dimensions: { ...baseDims } }));
|
|
||||||
const one = computePlanHash(makeInput({ dimensions: { ...baseDims, renderStretch: 1 } }));
|
|
||||||
const undef = computePlanHash(
|
|
||||||
makeInput({ dimensions: { ...baseDims, renderStretch: undefined } }),
|
|
||||||
);
|
|
||||||
expect(one).toBe(omitted);
|
|
||||||
expect(undef).toBe(omitted);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("a stretched plan (renderStretch=2) gets a distinct hash", () => {
|
|
||||||
const base = computePlanHash(makeInput({ dimensions: { ...baseDims } }));
|
|
||||||
const stretched = computePlanHash(makeInput({ dimensions: { ...baseDims, renderStretch: 2 } }));
|
|
||||||
expect(stretched).not.toBe(base);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ export interface PlanDimensions {
|
|||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
format: DistributedFormat;
|
format: DistributedFormat;
|
||||||
/** intrinsic/target timeline re-time; 1 (or omitted) = no-op, hashed identically to today. */
|
|
||||||
renderStretch?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanHashInput {
|
export interface PlanHashInput {
|
||||||
@@ -129,10 +127,7 @@ export function computePlanHash(input: PlanHashInput): string {
|
|||||||
hash.update(FIELD_DELIMITER);
|
hash.update(FIELD_DELIMITER);
|
||||||
|
|
||||||
const d = input.dimensions;
|
const d = input.dimensions;
|
||||||
// Append renderStretch only when it re-times (!= 1) so unstretched plans hash identically to before.
|
hash.update(`${d.fpsNum}/${d.fpsDen}x${d.width}x${d.height}x${d.format}`, "utf8");
|
||||||
const stretchSuffix =
|
|
||||||
d.renderStretch !== undefined && d.renderStretch !== 1 ? `x${d.renderStretch}` : "";
|
|
||||||
hash.update(`${d.fpsNum}/${d.fpsDen}x${d.width}x${d.height}x${d.format}${stretchSuffix}`, "utf8");
|
|
||||||
|
|
||||||
return hash.digest("hex");
|
return hash.digest("hex");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -592,10 +592,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
|||||||
const browserProbeMs = Date.now() - probeStart;
|
const browserProbeMs = Date.now() - probeStart;
|
||||||
|
|
||||||
const duration = composition.duration;
|
const duration = composition.duration;
|
||||||
// Output length = intrinsic / renderStretch (1 = no-op); composition.duration stays intrinsic.
|
const totalFrames = durationToFrameCount(duration, fpsToNumber(job.config.fps));
|
||||||
const renderStretch = job.config.renderStretch ?? 1;
|
|
||||||
const outputDuration = duration / renderStretch;
|
|
||||||
const totalFrames = durationToFrameCount(outputDuration, fpsToNumber(job.config.fps));
|
|
||||||
|
|
||||||
if (duration <= 0) {
|
if (duration <= 0) {
|
||||||
// Gather diagnostics to help users understand why the render would produce a black video.
|
// Gather diagnostics to help users understand why the render would produce a black video.
|
||||||
@@ -664,7 +661,7 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
|
|||||||
fileServer,
|
fileServer,
|
||||||
probeSession,
|
probeSession,
|
||||||
lastBrowserConsole,
|
lastBrowserConsole,
|
||||||
duration: outputDuration,
|
duration,
|
||||||
totalFrames,
|
totalFrames,
|
||||||
browserProbeMs,
|
browserProbeMs,
|
||||||
beginFrameStalled,
|
beginFrameStalled,
|
||||||
|
|||||||
@@ -364,7 +364,6 @@ export interface RenderConfig {
|
|||||||
* alias (`1080p-portrait`). Explicit orientation presets stay strict.
|
* alias (`1080p-portrait`). Explicit orientation presets stay strict.
|
||||||
*/
|
*/
|
||||||
outputResolutionAspectAgnostic?: boolean;
|
outputResolutionAspectAgnostic?: boolean;
|
||||||
renderStretch?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RenderPerfSummary {
|
export interface RenderPerfSummary {
|
||||||
@@ -2190,7 +2189,6 @@ async function executeRenderPipeline(input: {
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fps: job.config.fps,
|
fps: job.config.fps,
|
||||||
renderStretch: job.config.renderStretch ?? 1,
|
|
||||||
format: needsAlpha ? "png" : "jpeg",
|
format: needsAlpha ? "png" : "jpeg",
|
||||||
quality: needsAlpha ? undefined : job.config.quality === "draft" ? 80 : 95,
|
quality: needsAlpha ? undefined : job.config.quality === "draft" ? 80 : 95,
|
||||||
variables: job.config.variables,
|
variables: job.config.variables,
|
||||||
|
|||||||
Reference in New Issue
Block a user