Files
mes123456 0fe8d2bb56
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
Advance M8-M11 parity workflows
2026-08-17 04:37:07 -04:00

427 lines
24 KiB
TypeScript

import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "./capability-gates";
import type { ErrorCode } from "./error";
import { normalizeProjectAssetPath } from "./asset-path";
export const SEQUENCER_SCHEMA = 1 as const;
export const SEQUENCER_BUDGET = {
maxStrips: 100_000,
maxChannels: 128,
maxFrames: 1_000_000,
maxDependencies: 64,
maxImageElements: 100_000,
} as const;
export type SequencerStripType = "SCENE" | "MOVIE" | "IMAGE" | "SOUND" | "EFFECT" | "META";
export interface SequencerProxyIR {
status: "MISSING" | "AVAILABLE" | "STALE";
assetId?: string;
width?: number;
height?: number;
quality?: number;
sha256?: string;
}
export interface SequencerStripIR {
id: string;
name: string;
type: SequencerStripType;
channel: number;
frameStart: number;
frameEnd: number;
sourceStart: number;
sourceEnd: number;
speed: number;
muted: boolean;
locked: boolean;
sourceId?: string;
sourcePath?: string;
mimeType?: string;
imageAssetIds?: string[];
effectType?: "CROSS" | "GAMMA_CROSS" | "ADD" | "MULTIPLY" | "TRANSFORM" | "COLOR";
inputStripIds?: string[];
childStripIds?: string[];
proxy?: SequencerProxyIR;
}
export interface SequencerTimelineIR {
schemaVersion: typeof SEQUENCER_SCHEMA;
id: string;
revision: number;
frameStart: number;
frameEnd: number;
fpsNumerator: number;
fpsDenominator: number;
strips: SequencerStripIR[];
}
export type SequencerEditIR =
| { type: "MOVE"; revision: number; stripId: string; frameDelta: number; channel?: number }
| { type: "TRIM"; revision: number; stripId: string; frameStart?: number; frameEnd?: number }
| { type: "SPLIT"; revision: number; stripId: string; frame: number; rightStripId: string };
export interface SequencerRuntimeCapabilityIR {
webCodecsVideo: "PROBE_REQUIRED" | "UNAVAILABLE";
webCodecsAudio: "PROBE_REQUIRED" | "UNAVAILABLE";
htmlMedia: "PROBE_REQUIRED" | "UNAVAILABLE";
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;
sourceFrame: number;
dependencyStripIds: string[];
}
export interface SequencerTransitionFrameIR {
effectStripId: string;
effectType: "CROSS" | "GAMMA_CROSS";
factor: number;
from: { stripId: string; sourceFrame: number };
to: { stripId: string; sourceFrame: number };
}
export class SequencerValidationError extends Error {
readonly code: ErrorCode;
constructor(code: ErrorCode, message: string) {
super(`${code}: ${message}`);
this.name = "SequencerValidationError";
this.code = code;
}
}
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);
}
function text(value: unknown, name: string, maximum = 256): string {
if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `${name} is invalid`);
return value;
}
function integer(value: unknown, name: string, minimum: number, maximum: number): number {
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `${name} is outside the bounded range`);
return value;
}
function parseProxy(value: unknown, index: number): SequencerProxyIR | undefined {
if (value === undefined) return undefined;
if (!record(value) || !["MISSING", "AVAILABLE", "STALE"].includes(value.status as string)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}].proxy is invalid`);
const proxy: SequencerProxyIR = { status: value.status as SequencerProxyIR["status"] };
if (value.assetId !== undefined) proxy.assetId = text(value.assetId, `strips[${index}].proxy.assetId`);
if (value.width !== undefined) proxy.width = integer(value.width, `strips[${index}].proxy.width`, 1, 16_384);
if (value.height !== undefined) proxy.height = integer(value.height, `strips[${index}].proxy.height`, 1, 16_384);
if (value.quality !== undefined) proxy.quality = integer(value.quality, `strips[${index}].proxy.quality`, 0, 100);
if (value.sha256 !== undefined) {
if (typeof value.sha256 !== "string" || !SHA256.test(value.sha256)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}].proxy.sha256 is invalid`);
proxy.sha256 = value.sha256;
}
if (proxy.status === "AVAILABLE" && (!proxy.assetId || !proxy.sha256)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}] available proxy requires assetId and sha256`);
return proxy;
}
export function parseSequencerTimeline(value: unknown): SequencerTimelineIR {
if (!record(value) || value.schemaVersion !== SEQUENCER_SCHEMA || !Array.isArray(value.strips)) throw new SequencerValidationError("PROTOCOL_MISMATCH", "Unsupported Sequencer timeline schema");
if (value.strips.length > SEQUENCER_BUDGET.maxStrips) throw new SequencerValidationError("SEQUENCER_BUDGET_EXCEEDED", "Sequencer strip count exceeds the budget");
const frameStart = integer(value.frameStart, "frameStart", -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
const frameEnd = integer(value.frameEnd, "frameEnd", -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
if (frameEnd < frameStart) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "Timeline frame range is invalid");
const ids = new Set<string>();
const strips = value.strips.map((item, index): SequencerStripIR => {
if (!record(item) || !STRIP_TYPES.has(item.type as SequencerStripType)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}] is invalid`);
const id = text(item.id, `strips[${index}].id`);
if (ids.has(id)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `Duplicate strip ${id}`);
ids.add(id);
const start = integer(item.frameStart, `strips[${index}].frameStart`, -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
const end = integer(item.frameEnd, `strips[${index}].frameEnd`, -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
const sourceStart = integer(item.sourceStart, `strips[${index}].sourceStart`, -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
const sourceEnd = integer(item.sourceEnd, `strips[${index}].sourceEnd`, -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
if (end <= start || sourceEnd < sourceStart || end - start > SEQUENCER_BUDGET.maxFrames) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}] frame range is invalid`);
const speed = typeof item.speed === "number" && Number.isFinite(item.speed) && item.speed > 0 && item.speed <= 1_000 ? item.speed : NaN;
if (!Number.isFinite(speed)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}].speed is invalid`);
const strip: SequencerStripIR = {
id,
name: text(item.name, `strips[${index}].name`),
type: item.type as SequencerStripType,
channel: integer(item.channel, `strips[${index}].channel`, 1, SEQUENCER_BUDGET.maxChannels),
frameStart: start,
frameEnd: end,
sourceStart,
sourceEnd,
speed,
muted: typeof item.muted === "boolean" ? item.muted : false,
locked: typeof item.locked === "boolean" ? item.locked : false,
proxy: parseProxy(item.proxy, index),
};
if (item.sourceId !== undefined) strip.sourceId = text(item.sourceId, `strips[${index}].sourceId`);
if (item.sourcePath !== undefined) {
try { strip.sourcePath = normalizeProjectAssetPath(text(item.sourcePath, `strips[${index}].sourcePath`, 2048)); }
catch { throw new SequencerValidationError("SEQUENCER_RESOURCE_OUTSIDE_PROJECT", `strips[${index}].sourcePath is outside the project`); }
}
if (item.mimeType !== undefined) strip.mimeType = text(item.mimeType, `strips[${index}].mimeType`, 128);
if (item.imageAssetIds !== undefined) {
if (!Array.isArray(item.imageAssetIds) || item.imageAssetIds.length > SEQUENCER_BUDGET.maxImageElements || item.imageAssetIds.some((asset) => typeof asset !== "string" || asset.length === 0)) throw new SequencerValidationError("SEQUENCER_BUDGET_EXCEEDED", `strips[${index}].imageAssetIds exceeds the budget`);
strip.imageAssetIds = [...item.imageAssetIds] as string[];
}
for (const field of ["inputStripIds", "childStripIds"] as const) if (item[field] !== undefined) {
if (!Array.isArray(item[field]) || item[field].length > SEQUENCER_BUDGET.maxDependencies || item[field].some((dependency) => typeof dependency !== "string" || dependency.length === 0)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}].${field} is invalid`);
strip[field] = [...item[field]] as string[];
}
if (strip.type === "EFFECT") {
if (!["CROSS", "GAMMA_CROSS", "ADD", "MULTIPLY", "TRANSFORM", "COLOR"].includes(item.effectType as string) || !strip.inputStripIds?.length) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}] effect dependencies are invalid`);
strip.effectType = item.effectType as SequencerStripIR["effectType"];
}
if (strip.type === "META" && !strip.childStripIds) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `strips[${index}] META requires childStripIds`);
if (["MOVIE", "IMAGE", "SOUND"].includes(strip.type) && !strip.sourceId && !strip.sourcePath && !strip.imageAssetIds?.length) throw new SequencerValidationError("SEQUENCER_RESOURCE_MISSING", `strips[${index}] has no source resource`);
return strip;
});
const byId = new Map(strips.map((strip) => [strip.id, strip]));
const active = new Set<string>();
const complete = new Set<string>();
const visit = (id: string): void => {
if (active.has(id)) throw new SequencerValidationError("SEQUENCER_DEPENDENCY_CYCLE", `Sequencer dependency cycle includes ${id}`);
if (complete.has(id)) return;
active.add(id);
const strip = byId.get(id);
for (const dependency of [...(strip?.inputStripIds ?? []), ...(strip?.childStripIds ?? [])]) {
if (!byId.has(dependency)) throw new SequencerValidationError("SEQUENCER_RESOURCE_MISSING", `${id} references missing strip ${dependency}`);
visit(dependency);
}
active.delete(id);
complete.add(id);
};
strips.forEach((strip) => visit(strip.id));
return {
schemaVersion: SEQUENCER_SCHEMA,
id: text(value.id, "id"),
revision: integer(value.revision, "revision", 0, Number.MAX_SAFE_INTEGER),
frameStart,
frameEnd,
fpsNumerator: integer(value.fpsNumerator, "fpsNumerator", 1, 1_000_000),
fpsDenominator: integer(value.fpsDenominator, "fpsDenominator", 1, 1_000_000),
strips,
};
}
export function applySequencerEdit(value: unknown, edit: SequencerEditIR): SequencerTimelineIR {
const timeline = parseSequencerTimeline(value);
if (edit.revision !== timeline.revision) throw new SequencerValidationError("REVISION_CONFLICT", "Sequencer edit revision is stale");
const strips = timeline.strips.map((strip) => ({ ...strip, inputStripIds: strip.inputStripIds && [...strip.inputStripIds], childStripIds: strip.childStripIds && [...strip.childStripIds] }));
const strip = strips.find((candidate) => candidate.id === edit.stripId);
if (!strip) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `Unknown strip ${edit.stripId}`);
if (strip.locked) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `${edit.stripId} is locked`);
if (edit.type === "MOVE") {
integer(edit.frameDelta, "frameDelta", -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
strip.frameStart += edit.frameDelta;
strip.frameEnd += edit.frameDelta;
if (edit.channel !== undefined) strip.channel = integer(edit.channel, "channel", 1, SEQUENCER_BUDGET.maxChannels);
}
else if (edit.type === "TRIM") {
if (edit.frameStart === undefined && edit.frameEnd === undefined) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "TRIM requires a boundary");
const nextStart = edit.frameStart === undefined ? strip.frameStart : integer(edit.frameStart, "frameStart", -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
const nextEnd = edit.frameEnd === undefined ? strip.frameEnd : integer(edit.frameEnd, "frameEnd", -SEQUENCER_BUDGET.maxFrames, SEQUENCER_BUDGET.maxFrames);
if (nextStart >= nextEnd) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", "TRIM would create an empty strip");
const originalFrameStart = strip.frameStart;
const originalSourceStart = strip.sourceStart;
strip.sourceStart = originalSourceStart + Math.round((nextStart - originalFrameStart) * strip.speed);
strip.sourceEnd = Math.min(strip.sourceEnd, originalSourceStart + Math.round((nextEnd - originalFrameStart) * strip.speed));
strip.frameStart = nextStart;
strip.frameEnd = nextEnd;
}
else {
const splitFrame = integer(edit.frame, "frame", strip.frameStart + 1, strip.frameEnd - 1);
if (strips.some((candidate) => candidate.id === edit.rightStripId)) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `Duplicate split strip ${edit.rightStripId}`);
const originalEnd = strip.frameEnd;
const originalSourceEnd = strip.sourceEnd;
const rightSourceStart = strip.sourceStart + Math.round((splitFrame - strip.frameStart) * strip.speed);
strip.frameEnd = splitFrame;
strip.sourceEnd = rightSourceStart;
strips.push({ ...strip, id: text(edit.rightStripId, "rightStripId"), name: `${strip.name} Right`, frameStart: splitFrame, frameEnd: originalEnd, sourceStart: rightSourceStart, sourceEnd: originalSourceEnd });
}
return parseSequencerTimeline({ ...timeline, revision: timeline.revision + 1, strips });
}
export function sequencerSourceFrame(strip: SequencerStripIR, timelineFrame: number): number {
if (!Number.isFinite(timelineFrame) || timelineFrame < strip.frameStart || timelineFrame >= strip.frameEnd) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `${strip.id} is inactive at frame ${timelineFrame}`);
return Math.min(strip.sourceEnd, Math.max(strip.sourceStart, strip.sourceStart + (timelineFrame - strip.frameStart) * strip.speed));
}
export function resolveSequencerFrame(value: unknown, timelineFrame: number): SequencerFrameStripIR[] {
const timeline = parseSequencerTimeline(value);
if (!Number.isFinite(timelineFrame) || timelineFrame < timeline.frameStart || timelineFrame > timeline.frameEnd) throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `Timeline frame ${timelineFrame} is outside the scene range`);
const active = timeline.strips.filter((strip) => !strip.muted && timelineFrame >= strip.frameStart && timelineFrame < strip.frameEnd);
const activeIds = new Set(active.map((strip) => strip.id));
const hiddenByMeta = new Set(active.filter((strip) => strip.type === "META").flatMap((strip) => strip.childStripIds ?? []));
const result = active.filter((strip) => !hiddenByMeta.has(strip.id)).map((strip): SequencerFrameStripIR => {
const dependencies = [...(strip.inputStripIds ?? []), ...(strip.childStripIds ?? [])];
if (dependencies.some((id) => !activeIds.has(id))) throw new SequencerValidationError("SEQUENCER_RESOURCE_MISSING", `${strip.id} has an inactive frame dependency`);
return { stripId: strip.id, channel: strip.channel, sourceFrame: sequencerSourceFrame(strip, timelineFrame), dependencyStripIds: [...dependencies] };
});
result.sort((left, right) => left.channel - right.channel || left.stripId.localeCompare(right.stripId));
return result;
}
/** Resolves the ordered inputs and bounded progress for the verified cross-transition subset. */
export function resolveSequencerTransitionFrame(
value: unknown,
effectStripId: string,
timelineFrame: number,
): SequencerTransitionFrameIR {
const timeline = parseSequencerTimeline(value);
const effect = timeline.strips.find((strip) => strip.id === effectStripId);
if (!effect || effect.type !== "EFFECT" ||
(effect.effectType !== "CROSS" && effect.effectType !== "GAMMA_CROSS") ||
effect.inputStripIds?.length !== 2) {
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `${effectStripId} is not a supported two-input cross transition`);
}
if (!Number.isFinite(timelineFrame) || timelineFrame < effect.frameStart || timelineFrame >= effect.frameEnd) {
throw new SequencerValidationError("SEQUENCER_SCHEMA_INVALID", `${effectStripId} is inactive at frame ${timelineFrame}`);
}
const [fromId, toId] = effect.inputStripIds;
const from = timeline.strips.find((strip) => strip.id === fromId);
const to = timeline.strips.find((strip) => strip.id === toId);
if (!from || !to || from.muted || to.muted || timelineFrame < from.frameStart || timelineFrame >= from.frameEnd ||
timelineFrame < to.frameStart || timelineFrame >= to.frameEnd) {
throw new SequencerValidationError("SEQUENCER_RESOURCE_MISSING", `${effectStripId} has an inactive transition input`);
}
const factor = (timelineFrame - effect.frameStart) / (effect.frameEnd - effect.frameStart);
return {
effectStripId,
effectType: effect.effectType,
factor,
from: { stripId: from.id, sourceFrame: sequencerSourceFrame(from, timelineFrame) },
to: { stripId: to.id, sourceFrame: sequencerSourceFrame(to, timelineFrame) },
};
}
export function sequencerRuntimeCapabilities(scope: typeof globalThis = globalThis): SequencerRuntimeCapabilityIR {
return {
webCodecsVideo: "VideoDecoder" in scope ? "PROBE_REQUIRED" : "UNAVAILABLE",
webCodecsAudio: "AudioDecoder" in scope ? "PROBE_REQUIRED" : "UNAVAILABLE",
htmlMedia: "HTMLMediaElement" in scope ? "PROBE_REQUIRED" : "UNAVAILABLE",
localEncoding: "BLOCKED",
};
}
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")]);
}
}