mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 13:07:11 +00:00
* perf(studio): preview decodes hard-to-play videos at the size they are shown Studio's copy of an HEVC or ProRes clip was made at the source's full size, so a 4K clip was decoded at 4K inside a preview a few hundred pixels wide, and back-to-back 4K clips flashed the background at their cuts. The player now reports how large it shows the composition, the runtime asks for a copy sized to the shown video (on a ladder of sizes, never above the source), and Studio stops pre-warming source-size copies. CLI play, static serving, publish and renders keep the source-size copy. * fix(studio): sized preview copies follow zoom, hover previews and the newest size Studio reports the preview's on-screen scale when a zoom settles and for composition hover previews, which no player measures. The runtime loads only the newest size asked for, regrows a first copy made while the frame grew, and never replaces a copy with one smaller on either side. The player's scale is measured against the frame's own layout width. * fix(studio): keep comment shares and file sizes within the repo ratchets * test(studio-server): end the sizing test's ffprobe options before its input * fix(studio): hover previews report their size once their runtime is ready
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
export const RUNTIME_PROTOCOL_VERSION = 1 as const;
|
|
|
|
export const RUNTIME_FILLER = "hf-runtime-filler";
|
|
|
|
export const RUNTIME_PROTOCOL_CAPABILITIES = [
|
|
"seconds-time",
|
|
"rational-fps",
|
|
"seek-keep-playing",
|
|
"composition-manifest-v1",
|
|
"runtime-data",
|
|
"play-range",
|
|
] as const;
|
|
|
|
export type RuntimeProtocolFps = {
|
|
numerator: number;
|
|
denominator: number;
|
|
};
|
|
|
|
export type RuntimeProtocolV1 = {
|
|
protocolVersion: typeof RUNTIME_PROTOCOL_VERSION;
|
|
capabilities: readonly string[];
|
|
fps: RuntimeProtocolFps;
|
|
};
|
|
|
|
export type RuntimeProtocolInspection =
|
|
| { status: "legacy"; fps: number }
|
|
| { status: "supported"; fps: number; metadata: RuntimeProtocolV1 }
|
|
| {
|
|
status: "unsupported";
|
|
code: "unsupported_protocol_version" | "invalid_protocol_metadata";
|
|
receivedVersion: unknown;
|
|
};
|
|
|
|
function greatestCommonDivisor(a: number, b: number): number {
|
|
let left = Math.abs(a);
|
|
let right = Math.abs(b);
|
|
while (right !== 0) {
|
|
const remainder = left % right;
|
|
left = right;
|
|
right = remainder;
|
|
}
|
|
return left || 1;
|
|
}
|
|
|
|
export function runtimeProtocolFpsFromNumber(value: number): RuntimeProtocolFps {
|
|
const safe = Number.isFinite(value) && value > 0 ? value : 30;
|
|
const denominator = Number.isInteger(safe) ? 1 : 1_000_000;
|
|
const numerator = Math.round(safe * denominator);
|
|
const divisor = greatestCommonDivisor(numerator, denominator);
|
|
return { numerator: numerator / divisor, denominator: denominator / divisor };
|
|
}
|
|
|
|
export function playRangeHoldTime(start: number, end: number, fps: number): number {
|
|
return Math.max(start, (Math.ceil(end * fps - 1e-6) - 1) / fps);
|
|
}
|
|
|
|
export function runtimeProtocolFpsToNumber(value: unknown): number | null {
|
|
if (typeof value !== "object" || value === null) return null;
|
|
const fps = value as Partial<RuntimeProtocolFps>;
|
|
if (!Number.isFinite(fps.numerator) || !Number.isFinite(fps.denominator)) return null;
|
|
if ((fps.numerator ?? 0) <= 0 || (fps.denominator ?? 0) <= 0) return null;
|
|
return Number(fps.numerator) / Number(fps.denominator);
|
|
}
|
|
|
|
export function runtimeProtocolMetadata(fps: number): RuntimeProtocolV1 {
|
|
return {
|
|
protocolVersion: RUNTIME_PROTOCOL_VERSION,
|
|
capabilities: RUNTIME_PROTOCOL_CAPABILITIES,
|
|
fps: runtimeProtocolFpsFromNumber(fps),
|
|
};
|
|
}
|
|
|
|
export function frameDisplayScale(frame: HTMLElement): number | null {
|
|
const layoutWidth = frame.offsetWidth;
|
|
const shownWidth = frame.getBoundingClientRect().width;
|
|
return layoutWidth > 0 && shownWidth > 0 ? shownWidth / layoutWidth : null;
|
|
}
|
|
|
|
function hasDeclaredCapabilities(value: unknown): boolean {
|
|
return Array.isArray(value) && value.every((capability) => typeof capability === "string");
|
|
}
|
|
|
|
export function inspectRuntimeProtocol(value: unknown, legacyFps = 30): RuntimeProtocolInspection {
|
|
if (typeof value !== "object" || value === null) return { status: "legacy", fps: legacyFps };
|
|
const message = value as Record<string, unknown>;
|
|
if (message.protocolVersion === undefined) return { status: "legacy", fps: legacyFps };
|
|
if (message.protocolVersion !== RUNTIME_PROTOCOL_VERSION) {
|
|
return {
|
|
status: "unsupported",
|
|
code: "unsupported_protocol_version",
|
|
receivedVersion: message.protocolVersion,
|
|
};
|
|
}
|
|
const fps = runtimeProtocolFpsToNumber(message.fps);
|
|
if (fps === null || !hasDeclaredCapabilities(message.capabilities)) {
|
|
return {
|
|
status: "unsupported",
|
|
code: "invalid_protocol_metadata",
|
|
receivedVersion: message.protocolVersion,
|
|
};
|
|
}
|
|
return {
|
|
status: "supported",
|
|
fps,
|
|
metadata: message as RuntimeProtocolV1,
|
|
};
|
|
}
|