Files
workinf_Blender_Wasm/web/app/src/sequencer/SequencerMediaProxyCache.ts
mes123456 5a11045ca5
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 Blender 5.2 web parity through M12-03D
2026-08-17 17:30:27 -04:00

217 lines
8.1 KiB
TypeScript

import {
SEQUENCER_MEDIA_PROXY_MAX_BYTES,
createSequencerMediaCacheManifest,
computeSequencerMediaCacheIdentity,
parseSequencerMediaProxyProfile,
sequencerMediaCacheKey,
verifySequencerMediaCacheEntry,
SequencerMediaCacheValidationError,
type SequencerMediaCacheManifestIR,
type SequencerMediaProxyProfileIR,
} from "../../../protocol/sequencer-media-cache";
import {
gateSequencerCodec,
parseSequencerCodecProbeRequest,
parseSequencerCodecProbeResult,
type SequencerCodecProbeRequestIR,
type SequencerCodecProbeResultIR,
} from "../../../protocol/sequencer";
export interface SequencerMediaProxyCacheStatsIR {
entries: number;
bytes: number;
maxBytes: number;
hits: number;
misses: number;
evictions: number;
keys: string[];
}
export interface SequencerGeneratedProxyFrameIR {
manifest: SequencerMediaCacheManifestIR;
data: ArrayBuffer;
}
interface CacheEntry {
manifest: SequencerMediaCacheManifestIR;
data: ArrayBuffer;
}
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 cancelled(signal: AbortSignal | undefined): void {
if (signal?.aborted) {
throw new SequencerMediaCacheValidationError("SEQUENCER_CANCELLED", "Movie proxy generation was cancelled");
}
}
function waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number, signal?: AbortSignal): Promise<void> {
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve();
return new Promise((resolve, reject) => {
const timeout = window.setTimeout(() => finish(new Error("timeout")), timeoutMs);
const finish = (error?: Error): void => {
window.clearTimeout(timeout);
video.removeEventListener("loadeddata", ready);
video.removeEventListener("error", failed);
signal?.removeEventListener("abort", aborted);
if (error) reject(error); else resolve();
};
const ready = (): void => finish();
const failed = (): void => finish(new Error("decode"));
const aborted = (): void => finish(new SequencerMediaCacheValidationError("SEQUENCER_CANCELLED", "Movie proxy generation was cancelled"));
video.addEventListener("loadeddata", ready, { once: true });
video.addEventListener("error", failed, { once: true });
signal?.addEventListener("abort", aborted, { once: true });
if (signal?.aborted) aborted();
});
}
export async function generateInitialSequencerMovieProxyFrame(
sourceValue: unknown,
capabilityValue: unknown,
profileValue: unknown,
sourceData: ArrayBuffer,
signal?: AbortSignal,
): Promise<SequencerGeneratedProxyFrameIR> {
cancelled(signal);
const source = parseSequencerCodecProbeRequest(sourceValue);
const capability = parseSequencerCodecProbeResult(capabilityValue);
const profile = parseSequencerMediaProxyProfile(profileValue);
if (source.stripType !== "MOVIE" || capability.status !== "READY" || capability.stripType !== "MOVIE" ||
gateSequencerCodec(source, capability).status !== "READY") {
throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "Movie proxy generation requires a READY movie receipt");
}
if (!(sourceData instanceof ArrayBuffer) || sourceData.byteLength !== source.byteLength || await sha256(sourceData) !== source.sourceSha256) {
throw new SequencerMediaCacheValidationError("SEQUENCER_CACHE_SOURCE_MISMATCH", "Movie proxy source bytes failed identity verification");
}
cancelled(signal);
if (typeof document === "undefined" || typeof HTMLMediaElement === "undefined") {
throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "HTML media proxy generation is unavailable");
}
const video = document.createElement("video");
video.muted = true;
video.playsInline = true;
video.preload = "auto";
const url = URL.createObjectURL(new Blob([sourceData], { type: source.mimeType }));
try {
video.src = url;
video.load();
await waitForLoadedFrame(video, 10_000, signal);
cancelled(signal);
const canvas = document.createElement("canvas");
canvas.width = profile.width;
canvas.height = profile.height;
const context = canvas.getContext("2d", { alpha: true, willReadFrequently: true });
if (!context || video.videoWidth < 1 || video.videoHeight < 1) {
throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "Movie proxy frame could not be decoded");
}
context.clearRect(0, 0, profile.width, profile.height);
context.drawImage(video, 0, 0, profile.width, profile.height);
cancelled(signal);
const pixels = context.getImageData(0, 0, profile.width, profile.height).data;
const data = pixels.buffer.slice(pixels.byteOffset, pixels.byteOffset + pixels.byteLength);
return {
manifest: await createSequencerMediaCacheManifest(source, capability, profile, 0, data),
data,
};
}
catch (error) {
if (error instanceof SequencerMediaCacheValidationError) throw error;
throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "Movie proxy frame decode failed");
}
finally {
video.pause();
video.removeAttribute("src");
video.load();
URL.revokeObjectURL(url);
}
}
export class SequencerMediaProxyCache {
readonly maxBytes: number;
private readonly entries = new Map<string, CacheEntry>();
private currentBytes = 0;
private hitCount = 0;
private missCount = 0;
private evictionCount = 0;
constructor(maxBytes: number) {
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > SEQUENCER_MEDIA_PROXY_MAX_BYTES) {
throw new SequencerMediaCacheValidationError("SEQUENCER_BUDGET_EXCEEDED", "Sequencer proxy cache budget is invalid");
}
this.maxBytes = maxBytes;
}
async put(
manifestValue: unknown,
data: ArrayBuffer,
currentSource: SequencerCodecProbeRequestIR,
currentCapability: SequencerCodecProbeResultIR,
): Promise<string> {
const manifest = await verifySequencerMediaCacheEntry(manifestValue, data, currentSource, currentCapability);
if (data.byteLength > this.maxBytes) {
throw new SequencerMediaCacheValidationError("SEQUENCER_BUDGET_EXCEEDED", "Sequencer proxy entry exceeds the cache budget");
}
const key = sequencerMediaCacheKey(manifest);
const previous = this.entries.get(key);
if (previous) {
this.currentBytes -= previous.data.byteLength;
this.entries.delete(key);
}
while (this.currentBytes + data.byteLength > this.maxBytes) {
const oldest = this.entries.entries().next().value as [string, CacheEntry] | undefined;
if (!oldest) break;
this.entries.delete(oldest[0]);
this.currentBytes -= oldest[1].data.byteLength;
this.evictionCount++;
}
this.entries.set(key, { manifest, data: data.slice(0) });
this.currentBytes += data.byteLength;
return key;
}
async get(
sourceValue: unknown,
capabilityValue: unknown,
profileValue: unknown,
sourceFrame: number,
): Promise<SequencerGeneratedProxyFrameIR | undefined> {
const source = parseSequencerCodecProbeRequest(sourceValue);
const capability = parseSequencerCodecProbeResult(capabilityValue);
const profile = parseSequencerMediaProxyProfile(profileValue);
const identitySha256 = await computeSequencerMediaCacheIdentity(source, capability, profile, sourceFrame);
const key = `sequencer-media-cache:v1:${identitySha256}`;
const entry = this.entries.get(key);
if (!entry) { this.missCount++; return undefined; }
this.hitCount++;
this.entries.delete(key);
this.entries.set(key, entry);
return { manifest: entry.manifest, data: entry.data.slice(0) };
}
clear(): number {
const released = this.currentBytes;
this.entries.clear();
this.currentBytes = 0;
return released;
}
stats(): SequencerMediaProxyCacheStatsIR {
return {
entries: this.entries.size,
bytes: this.currentBytes,
maxBytes: this.maxBytes,
hits: this.hitCount,
misses: this.missCount,
evictions: this.evictionCount,
keys: [...this.entries.keys()],
};
}
}
export type { SequencerMediaCacheManifestIR, SequencerMediaProxyProfileIR };