Advance M8-M11 parity workflows
This commit is contained in:
201
web/app/src/sequencer/SequencerMediaProxyCache.ts
Normal file
201
web/app/src/sequencer/SequencerMediaProxyCache.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
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 waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number): 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);
|
||||
if (error) reject(error); else resolve();
|
||||
};
|
||||
const ready = (): void => finish();
|
||||
const failed = (): void => finish(new Error("decode"));
|
||||
video.addEventListener("loadeddata", ready, { once: true });
|
||||
video.addEventListener("error", failed, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateInitialSequencerMovieProxyFrame(
|
||||
sourceValue: unknown,
|
||||
capabilityValue: unknown,
|
||||
profileValue: unknown,
|
||||
sourceData: ArrayBuffer,
|
||||
): Promise<SequencerGeneratedProxyFrameIR> {
|
||||
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");
|
||||
}
|
||||
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);
|
||||
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);
|
||||
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 };
|
||||
Reference in New Issue
Block a user