mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(producer): keep clip playback rate in distributed video metadata (#4736)
parsePlanVideosJson rebuilt each video from a fixed field list that had no playbackRate, so chunk workers indexed extracted frames at rate 1 while the planner mixed audio at the authored rate. A data-playback-rate="1.5" clip showed its picture at 1x in planV2/renderChunkV2/assembleV2 output, while the single-process render was correct. Carry playbackRate through the parser, accepting a finite number or a rate lane, and pin it with a JSON round-trip test that checks the chunk-side frame lookup for 1.5x, 0.5x and a rate lane.
This commit is contained in:
@@ -9,7 +9,7 @@ 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 Fps, type RateSpec } from "@hyperframes/core";
|
||||
import {
|
||||
getFfmpegBinary,
|
||||
MIXED_AUDIO_FILENAME,
|
||||
@@ -173,6 +173,17 @@ function readNonNegativeInteger(value: unknown, field: string): number {
|
||||
return value;
|
||||
}
|
||||
|
||||
function readRateSpec(value: unknown, field: string): RateSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (typeof value === "number") return readFiniteNumber(value, field);
|
||||
const lane = readRecord(value, field);
|
||||
if (!Array.isArray(lane.points)) {
|
||||
metadataError(`${field}.points`, "must be an array");
|
||||
}
|
||||
// The planner serialized this lane from the clip's parsed `data-automation` rate lane.
|
||||
return lane as unknown as Exclude<RateSpec, number>;
|
||||
}
|
||||
|
||||
function readVideoMetadata(
|
||||
value: unknown,
|
||||
field: string,
|
||||
@@ -233,6 +244,7 @@ export function parsePlanVideosJson(value: unknown): PlanVideosJson {
|
||||
start: readFiniteNumber(video.start, `${field}.start`),
|
||||
end: readFiniteNumber(video.end, `${field}.end`),
|
||||
mediaStart: readFiniteNumber(video.mediaStart, `${field}.mediaStart`),
|
||||
playbackRate: readRateSpec(video.playbackRate, `${field}.playbackRate`),
|
||||
loop: readBoolean(video.loop, `${field}.loop`),
|
||||
hasAudio: readBoolean(video.hasAudio, `${field}.hasAudio`),
|
||||
};
|
||||
|
||||
@@ -145,6 +145,55 @@ describe("distributed video metadata", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "1.5x", playbackRate: 1.5, expectedFrame: 3 },
|
||||
{ name: "0.5x", playbackRate: 0.5, expectedFrame: 1 },
|
||||
{
|
||||
name: "rate lane",
|
||||
playbackRate: {
|
||||
target: "rate",
|
||||
points: [
|
||||
{ t: 0, v: 1.5 },
|
||||
{ t: 4, v: 1.5 },
|
||||
],
|
||||
},
|
||||
expectedFrame: 3,
|
||||
},
|
||||
])(
|
||||
"keeps a $name clip's playback rate across the plan boundary",
|
||||
({ playbackRate, expectedFrame }) => {
|
||||
const metadata = extractedMetadata();
|
||||
const written = buildPlanVideosJson({
|
||||
videos: [video({ playbackRate })],
|
||||
extracted: [metadata],
|
||||
compositionEnd: 8,
|
||||
});
|
||||
const read = parsePlanVideosJson(JSON.parse(JSON.stringify(written)));
|
||||
const lookup = createFrameLookupTable(read.videos, [extractedFrames(metadata)]);
|
||||
|
||||
expect(read.videos[0]?.playbackRate).toEqual(playbackRate);
|
||||
expect(lookup.getActiveFramePayloads(3).get("hero")?.frameIndex).toBe(expectedFrame);
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a malformed playback rate", () => {
|
||||
const valid = buildPlanVideosJson({
|
||||
videos: [video()],
|
||||
extracted: [extractedMetadata()],
|
||||
compositionEnd: 8,
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
parsePlanVideosJson({ ...valid, videos: [{ ...valid.videos[0], playbackRate: "1.5" }] }),
|
||||
).toThrow(/playbackRate must be an object/);
|
||||
expect(() =>
|
||||
parsePlanVideosJson({
|
||||
...valid,
|
||||
videos: [{ ...valid.videos[0], playbackRate: { target: "rate" } }],
|
||||
}),
|
||||
).toThrow(/playbackRate.points must be an array/);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ loop: false, expectedFrame: 3 },
|
||||
{ loop: true, expectedFrame: 2 },
|
||||
|
||||
Reference in New Issue
Block a user