Files
workinf_Blender_Wasm/web/protocol/sequencer-media-cache.ts
mes123456 0fe8d2bb56
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

271 lines
11 KiB
TypeScript

import type { ErrorCode } from "./error";
import {
gateSequencerCodec,
parseSequencerCodecProbeRequest,
parseSequencerCodecProbeResult,
type SequencerCodecProbeRequestIR,
type SequencerCodecProbeResultIR,
} from "./sequencer";
export const SEQUENCER_MEDIA_CACHE_SCHEMA = 1 as const;
export const SEQUENCER_MEDIA_PROXY_MAX_BYTES = 64 * 1024 * 1024;
export interface SequencerMediaProxyProfileIR {
kind: "MOVIE_RGBA8_FRAME";
width: number;
height: number;
colorSpace: "SRGB8";
alphaMode: "STRAIGHT";
}
export interface SequencerMediaCacheManifestIR {
schemaVersion: typeof SEQUENCER_MEDIA_CACHE_SCHEMA;
source: SequencerCodecProbeRequestIR;
decodeCapability: SequencerCodecProbeResultIR;
profile: SequencerMediaProxyProfileIR;
sourceFrame: number;
identitySha256: string;
payloadByteLength: number;
payloadSha256: string;
}
export class SequencerMediaCacheValidationError extends Error {
readonly code: ErrorCode;
constructor(code: ErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = "SequencerMediaCacheValidationError";
this.code = code;
}
}
const SHA256 = /^[a-f0-9]{64}$/;
const MANIFEST_KEYS = new Set([
"schemaVersion", "source", "decodeCapability", "profile", "sourceFrame",
"identitySha256", "payloadByteLength", "payloadSha256",
]);
const PROFILE_KEYS = new Set(["kind", "width", "height", "colorSpace", "alphaMode"]);
function record(value: unknown, label: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw new SequencerMediaCacheValidationError("SEQUENCER_SCHEMA_INVALID", `${label} must be an object`);
}
return value as Record<string, unknown>;
}
function exactKeys(value: Record<string, unknown>, allowed: ReadonlySet<string>, label: string): void {
const unexpected = Object.keys(value).filter((key) => !allowed.has(key));
if (unexpected.length > 0) {
throw new SequencerMediaCacheValidationError("SEQUENCER_SCHEMA_INVALID", `${label} contains undeclared fields: ${unexpected.join(", ")}`);
}
}
function integer(value: unknown, label: string, minimum: number, maximum: number): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
throw new SequencerMediaCacheValidationError("SEQUENCER_SCHEMA_INVALID", `${label} is outside the bounded range`);
}
return value;
}
function digest(value: unknown, label: string): string {
if (typeof value !== "string" || !SHA256.test(value)) {
throw new SequencerMediaCacheValidationError("SEQUENCER_SCHEMA_INVALID", `${label} must be a lowercase SHA-256 digest`);
}
return value;
}
async function sha256(data: ArrayBuffer): Promise<string> {
const result = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(result), (value) => value.toString(16).padStart(2, "0")).join("");
}
function canonicalSource(source: SequencerCodecProbeRequestIR): Record<string, unknown> {
return {
schemaVersion: source.schemaVersion,
stripType: source.stripType,
mimeType: source.mimeType,
byteLength: source.byteLength,
sourceSha256: source.sourceSha256,
};
}
function canonicalCapability(capability: SequencerCodecProbeResultIR): Record<string, unknown> {
const decoded = capability.decoded === null ? null : capability.stripType === "IMAGE" ? {
width: capability.decoded.width,
height: capability.decoded.height,
} : capability.stripType === "SOUND" ? {
sampleRate: capability.decoded.sampleRate,
channels: capability.decoded.channels,
durationFrames: capability.decoded.durationFrames,
} : {
width: capability.decoded.width,
height: capability.decoded.height,
durationMicros: capability.decoded.durationMicros,
};
return {
...canonicalSource(capability),
status: capability.status,
backend: capability.backend,
reason: capability.reason,
decoded,
};
}
function canonicalProfile(profile: SequencerMediaProxyProfileIR): Record<string, unknown> {
return {
kind: profile.kind,
width: profile.width,
height: profile.height,
colorSpace: profile.colorSpace,
alphaMode: profile.alphaMode,
};
}
function sameJson(left: unknown, right: unknown): boolean {
return JSON.stringify(left) === JSON.stringify(right);
}
function parseReadyMovieCapability(
sourceValue: unknown,
capabilityValue: unknown,
): { source: SequencerCodecProbeRequestIR; capability: SequencerCodecProbeResultIR } {
const source = parseSequencerCodecProbeRequest(sourceValue);
const capability = parseSequencerCodecProbeResult(capabilityValue);
if (source.stripType !== "MOVIE" || gateSequencerCodec(source, capability).status !== "READY") {
throw new SequencerMediaCacheValidationError(
"SEQUENCER_CODEC_UNSUPPORTED",
"Movie proxy cache requires a source-bound READY runtime decode receipt",
);
}
return { source, capability };
}
export function parseSequencerMediaProxyProfile(value: unknown): SequencerMediaProxyProfileIR {
const profile = record(value, "Sequencer media proxy profile");
exactKeys(profile, PROFILE_KEYS, "Sequencer media proxy profile");
if (profile.kind !== "MOVIE_RGBA8_FRAME" || profile.colorSpace !== "SRGB8" || profile.alphaMode !== "STRAIGHT") {
throw new SequencerMediaCacheValidationError("SEQUENCER_SCHEMA_INVALID", "Sequencer media proxy profile is unsupported");
}
const width = integer(profile.width, "profile.width", 1, 16_384);
const height = integer(profile.height, "profile.height", 1, 16_384);
if (width * height * 4 > SEQUENCER_MEDIA_PROXY_MAX_BYTES) {
throw new SequencerMediaCacheValidationError("SEQUENCER_BUDGET_EXCEEDED", "Sequencer media proxy frame exceeds the RGBA8 budget");
}
return { kind: "MOVIE_RGBA8_FRAME", width, height, colorSpace: "SRGB8", alphaMode: "STRAIGHT" };
}
export async function computeSequencerMediaCacheIdentity(
sourceValue: unknown,
capabilityValue: unknown,
profileValue: unknown,
sourceFrameValue: unknown,
): Promise<string> {
const { source, capability } = parseReadyMovieCapability(sourceValue, capabilityValue);
const profile = parseSequencerMediaProxyProfile(profileValue);
const sourceFrame = integer(sourceFrameValue, "sourceFrame", 0, 1_000_000);
const identity = JSON.stringify({
schemaVersion: SEQUENCER_MEDIA_CACHE_SCHEMA,
source: canonicalSource(source),
decodeCapability: canonicalCapability(capability),
profile: canonicalProfile(profile),
sourceFrame,
});
return sha256(new TextEncoder().encode(identity).buffer as ArrayBuffer);
}
export function parseSequencerMediaCacheManifest(value: unknown): SequencerMediaCacheManifestIR {
const manifest = record(value, "Sequencer media cache manifest");
exactKeys(manifest, MANIFEST_KEYS, "Sequencer media cache manifest");
if (manifest.schemaVersion !== SEQUENCER_MEDIA_CACHE_SCHEMA) {
throw new SequencerMediaCacheValidationError("PROTOCOL_MISMATCH", "Unsupported Sequencer media cache schema");
}
const { source, capability } = parseReadyMovieCapability(manifest.source, manifest.decodeCapability);
const profile = parseSequencerMediaProxyProfile(manifest.profile);
if (capability.decoded === null || capability.decoded.width === undefined || capability.decoded.height === undefined ||
profile.width > capability.decoded.width || profile.height > capability.decoded.height) {
throw new SequencerMediaCacheValidationError("SEQUENCER_SCHEMA_INVALID", "Proxy profile exceeds the runtime decoded movie dimensions");
}
const sourceFrame = integer(manifest.sourceFrame, "sourceFrame", 0, 1_000_000);
const payloadByteLength = integer(manifest.payloadByteLength, "payloadByteLength", 1, SEQUENCER_MEDIA_PROXY_MAX_BYTES);
if (payloadByteLength !== profile.width * profile.height * 4) {
throw new SequencerMediaCacheValidationError("SEQUENCER_SCHEMA_INVALID", "Proxy payload length does not match its RGBA8 profile");
}
return {
schemaVersion: SEQUENCER_MEDIA_CACHE_SCHEMA,
source,
decodeCapability: capability,
profile,
sourceFrame,
identitySha256: digest(manifest.identitySha256, "identitySha256"),
payloadByteLength,
payloadSha256: digest(manifest.payloadSha256, "payloadSha256"),
};
}
export async function createSequencerMediaCacheManifest(
sourceValue: unknown,
capabilityValue: unknown,
profileValue: unknown,
sourceFrameValue: unknown,
payload: ArrayBuffer,
): Promise<SequencerMediaCacheManifestIR> {
if (!(payload instanceof ArrayBuffer) || payload.byteLength === 0 || payload.byteLength > SEQUENCER_MEDIA_PROXY_MAX_BYTES) {
throw new SequencerMediaCacheValidationError("SEQUENCER_BUDGET_EXCEEDED", "Sequencer media proxy payload exceeds the byte budget");
}
const { source, capability } = parseReadyMovieCapability(sourceValue, capabilityValue);
const profile = parseSequencerMediaProxyProfile(profileValue);
if (capability.decoded === null || capability.decoded.width === undefined || capability.decoded.height === undefined ||
profile.width > capability.decoded.width || profile.height > capability.decoded.height) {
throw new SequencerMediaCacheValidationError("SEQUENCER_SCHEMA_INVALID", "Proxy profile exceeds the runtime decoded movie dimensions");
}
const sourceFrame = integer(sourceFrameValue, "sourceFrame", 0, 1_000_000);
const manifest = {
schemaVersion: SEQUENCER_MEDIA_CACHE_SCHEMA,
source,
decodeCapability: capability,
profile,
sourceFrame,
identitySha256: await computeSequencerMediaCacheIdentity(source, capability, profile, sourceFrame),
payloadByteLength: payload.byteLength,
payloadSha256: await sha256(payload),
} satisfies SequencerMediaCacheManifestIR;
return parseSequencerMediaCacheManifest(manifest);
}
export async function verifySequencerMediaCacheEntry(
manifestValue: unknown,
payload: ArrayBuffer,
currentSourceValue: unknown,
currentCapabilityValue: unknown,
): Promise<SequencerMediaCacheManifestIR> {
const manifest = parseSequencerMediaCacheManifest(manifestValue);
const { source: currentSource, capability: currentCapability } = parseReadyMovieCapability(
currentSourceValue,
currentCapabilityValue,
);
if (!sameJson(canonicalSource(manifest.source), canonicalSource(currentSource))) {
throw new SequencerMediaCacheValidationError("SEQUENCER_CACHE_SOURCE_MISMATCH", "Proxy cache source identity is stale");
}
if (!sameJson(canonicalCapability(manifest.decodeCapability), canonicalCapability(currentCapability))) {
throw new SequencerMediaCacheValidationError("SEQUENCER_CACHE_CAPABILITY_MISMATCH", "Proxy cache decode capability is stale");
}
const identitySha256 = await computeSequencerMediaCacheIdentity(
manifest.source,
manifest.decodeCapability,
manifest.profile,
manifest.sourceFrame,
);
if (identitySha256 !== manifest.identitySha256) {
throw new SequencerMediaCacheValidationError("SEQUENCER_CACHE_IDENTITY_MISMATCH", "Proxy cache identity hash is invalid");
}
if (!(payload instanceof ArrayBuffer) || payload.byteLength !== manifest.payloadByteLength || await sha256(payload) !== manifest.payloadSha256) {
throw new SequencerMediaCacheValidationError("SEQUENCER_CACHE_HASH_MISMATCH", "Proxy cache payload failed SHA-256 verification");
}
return manifest;
}
export function sequencerMediaCacheKey(manifestValue: unknown): string {
const manifest = parseSequencerMediaCacheManifest(manifestValue);
return `sequencer-media-cache:v${manifest.schemaVersion}:${manifest.identitySha256}`;
}