Advance M8-M11 parity workflows
This commit is contained in:
@@ -67,6 +67,38 @@ export interface SequencerRuntimeCapabilityIR {
|
||||
localEncoding: "BLOCKED";
|
||||
}
|
||||
|
||||
export const SEQUENCER_CODEC_PROBE_SCHEMA = 1 as const;
|
||||
export const SEQUENCER_CODEC_PROBE_MAX_BYTES = 512 * 1024 * 1024;
|
||||
export type SequencerCodecStripType = "IMAGE" | "SOUND" | "MOVIE";
|
||||
export type SequencerCodecProbeBackend = "IMAGE_BITMAP" | "WEB_AUDIO" | "HTML_MEDIA";
|
||||
export type SequencerCodecProbeBlockReason =
|
||||
| "RUNTIME_UNAVAILABLE"
|
||||
| "MIME_UNSUPPORTED"
|
||||
| "SOURCE_IDENTITY_MISMATCH"
|
||||
| "DECODE_FAILED";
|
||||
|
||||
export interface SequencerCodecProbeRequestIR {
|
||||
schemaVersion: typeof SEQUENCER_CODEC_PROBE_SCHEMA;
|
||||
stripType: SequencerCodecStripType;
|
||||
mimeType: string;
|
||||
byteLength: number;
|
||||
sourceSha256: string;
|
||||
}
|
||||
|
||||
export interface SequencerCodecProbeResultIR extends SequencerCodecProbeRequestIR {
|
||||
status: "READY" | "BLOCKED";
|
||||
backend: SequencerCodecProbeBackend | null;
|
||||
reason: SequencerCodecProbeBlockReason | null;
|
||||
decoded: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
sampleRate?: number;
|
||||
channels?: number;
|
||||
durationFrames?: number;
|
||||
durationMicros?: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface SequencerFrameStripIR {
|
||||
stripId: string;
|
||||
channel: number;
|
||||
@@ -94,6 +126,20 @@ export class SequencerValidationError extends Error {
|
||||
|
||||
const SHA256 = /^[a-f0-9]{64}$/;
|
||||
const STRIP_TYPES = new Set<SequencerStripType>(["SCENE", "MOVIE", "IMAGE", "SOUND", "EFFECT", "META"]);
|
||||
const CODEC_STRIP_TYPES = new Set<SequencerCodecStripType>(["IMAGE", "SOUND", "MOVIE"]);
|
||||
const CODEC_BACKENDS: Readonly<Record<SequencerCodecStripType, SequencerCodecProbeBackend>> = {
|
||||
IMAGE: "IMAGE_BITMAP",
|
||||
SOUND: "WEB_AUDIO",
|
||||
MOVIE: "HTML_MEDIA",
|
||||
};
|
||||
const CODEC_MIME_PREFIX: Readonly<Record<SequencerCodecStripType, string>> = {
|
||||
IMAGE: "image/",
|
||||
SOUND: "audio/",
|
||||
MOVIE: "video/",
|
||||
};
|
||||
const CODEC_BLOCK_REASONS = new Set<SequencerCodecProbeBlockReason>([
|
||||
"RUNTIME_UNAVAILABLE", "MIME_UNSUPPORTED", "SOURCE_IDENTITY_MISMATCH", "DECODE_FAILED",
|
||||
]);
|
||||
|
||||
function record(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
@@ -308,7 +354,73 @@ export function sequencerRuntimeCapabilities(scope: typeof globalThis = globalTh
|
||||
};
|
||||
}
|
||||
|
||||
export function gateSequencerCodec(mimeType: string, verifiedMimeTypes: ReadonlySet<string>): CapabilityGateResult {
|
||||
if (verifiedMimeTypes.has(mimeType)) return readyGate("N-021", `CODEC_${mimeType}`);
|
||||
return blockedGate("N-021", `CODEC_${mimeType}`, [capabilityIssue("SEQUENCER_CODEC_UNSUPPORTED", `Codec ${mimeType} has not passed an exact seek/decode probe`)]);
|
||||
function exactCodecProbeKeys(value: Record<string, unknown>, names: readonly string[], label: string): void {
|
||||
const allowed = new Set(names);
|
||||
const unexpected = Object.keys(value).filter((key) => !allowed.has(key));
|
||||
if (unexpected.length > 0) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `${label} contains undeclared fields: ${unexpected.join(", ")}`);
|
||||
}
|
||||
|
||||
export function parseSequencerCodecProbeRequest(value: unknown): SequencerCodecProbeRequestIR {
|
||||
if (!record(value) || value.schemaVersion !== SEQUENCER_CODEC_PROBE_SCHEMA || !CODEC_STRIP_TYPES.has(value.stripType as SequencerCodecStripType)) {
|
||||
throw new SequencerValidationError("PROTOCOL_MISMATCH", "Unsupported Sequencer codec probe request");
|
||||
}
|
||||
exactCodecProbeKeys(value, ["schemaVersion", "stripType", "mimeType", "byteLength", "sourceSha256"], "Codec probe request");
|
||||
const stripType = value.stripType as SequencerCodecStripType;
|
||||
const mimeType = text(value.mimeType, "mimeType", 128);
|
||||
if (mimeType !== mimeType.toLowerCase() || !mimeType.startsWith(CODEC_MIME_PREFIX[stripType]) || !/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(mimeType)) {
|
||||
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `Codec MIME ${mimeType} does not match ${stripType}`);
|
||||
}
|
||||
const byteLength = integer(value.byteLength, "byteLength", 1, SEQUENCER_CODEC_PROBE_MAX_BYTES);
|
||||
if (typeof value.sourceSha256 !== "string" || !SHA256.test(value.sourceSha256)) {
|
||||
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "Codec probe sourceSha256 is invalid");
|
||||
}
|
||||
return { schemaVersion: SEQUENCER_CODEC_PROBE_SCHEMA, stripType, mimeType, byteLength, sourceSha256: value.sourceSha256 };
|
||||
}
|
||||
|
||||
export function parseSequencerCodecProbeResult(value: unknown): SequencerCodecProbeResultIR {
|
||||
if (!record(value)) throw new SequencerValidationError("PROTOCOL_MISMATCH", "Unsupported Sequencer codec probe result");
|
||||
exactCodecProbeKeys(value, ["schemaVersion", "stripType", "mimeType", "byteLength", "sourceSha256", "status", "backend", "reason", "decoded"], "Codec probe result");
|
||||
const request = parseSequencerCodecProbeRequest({
|
||||
schemaVersion: value.schemaVersion,
|
||||
stripType: value.stripType,
|
||||
mimeType: value.mimeType,
|
||||
byteLength: value.byteLength,
|
||||
sourceSha256: value.sourceSha256,
|
||||
});
|
||||
if (value.status !== "READY" && value.status !== "BLOCKED") throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "Codec probe status is invalid");
|
||||
if (value.status === "BLOCKED") {
|
||||
if ((value.backend !== null && value.backend !== CODEC_BACKENDS[request.stripType]) ||
|
||||
!CODEC_BLOCK_REASONS.has(value.reason as SequencerCodecProbeBlockReason) || value.decoded !== null) {
|
||||
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "Blocked codec probe result is invalid");
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (value.backend !== CODEC_BACKENDS[request.stripType] || value.reason !== null || !record(value.decoded)) {
|
||||
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "Ready codec probe result is invalid");
|
||||
}
|
||||
const decodedKeys = request.stripType === "IMAGE" ? ["width", "height"] :
|
||||
request.stripType === "SOUND" ? ["sampleRate", "channels", "durationFrames"] :
|
||||
["width", "height", "durationMicros"];
|
||||
exactCodecProbeKeys(value.decoded, decodedKeys, "Codec probe decoded result");
|
||||
for (const key of decodedKeys) integer(value.decoded[key], `decoded.${key}`, 1, Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
return value as unknown as SequencerCodecProbeResultIR;
|
||||
}
|
||||
|
||||
export function gateSequencerCodec(requestValue: unknown, resultValue: unknown): CapabilityGateResult {
|
||||
let capability = "CODEC_RUNTIME_PROBE";
|
||||
try {
|
||||
const request = parseSequencerCodecProbeRequest(requestValue);
|
||||
capability = `CODEC_${request.stripType}_${request.mimeType}`;
|
||||
const result = parseSequencerCodecProbeResult(resultValue);
|
||||
if (result.stripType !== request.stripType || result.mimeType !== request.mimeType ||
|
||||
result.byteLength !== request.byteLength || result.sourceSha256 !== request.sourceSha256) {
|
||||
return blockedGate("N-021", capability, [capabilityIssue("SEQUENCER_CODEC_UNSUPPORTED", "Codec probe result does not match the source identity")]);
|
||||
}
|
||||
if (result.status === "READY") return readyGate("N-021", capability);
|
||||
return blockedGate("N-021", capability, [capabilityIssue("SEQUENCER_CODEC_UNSUPPORTED", `Codec runtime probe blocked: ${result.reason}`)]);
|
||||
}
|
||||
catch (error) {
|
||||
return blockedGate("N-021", capability, [capabilityIssue("SEQUENCER_CODEC_UNSUPPORTED", error instanceof Error ? error.message : "Codec probe is invalid")]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user