perf(lab): stream sealed spatial playback tracks

This commit is contained in:
DCCONSTRUCTIONS
2026-08-27 15:18:45 +03:00
parent b70dc4298a
commit bf76304e00
10 changed files with 911 additions and 50 deletions
@@ -84,6 +84,48 @@ export interface M49TgsFullShadowSpatialChunk {
frames: readonly M49TgsFullShadowSpatial[];
}
export type M49TgsFullShadowPlaybackTrackId =
| "frames"
| "centers"
| "states"
| "z-bounds";
export interface M49TgsFullShadowPlaybackProgress {
phase: "manifest" | "download" | "verify" | "ready";
trackId: M49TgsFullShadowPlaybackTrackId | null;
loadedBytes: number;
totalBytes: number;
}
interface M49TgsFullShadowPlaybackTrack {
id: M49TgsFullShadowPlaybackTrackId;
url: string;
mediaType: string;
dtype: "ndjson" | "<f4" | "|u1";
shape: readonly number[];
byteLength: number;
sha256: string;
}
export interface M49TgsFullShadowPlaybackFrame {
sourceSequence: number;
sourceFrameIndex: number;
sessionSeconds: number;
sampleAvailable: boolean;
metrics: M49TgsFullShadowSpatial["metrics"];
}
export interface M49TgsFullShadowPlaybackPack {
resultId: string;
frameCount: 4489;
cellCount: 2244;
totalByteLength: number;
centersXyM: readonly (readonly [number, number])[];
states: Uint8Array;
zBoundsM: Float32Array;
frames: readonly M49TgsFullShadowPlaybackFrame[];
}
export class M49TgsFullShadowContractError extends Error {}
function objectValue(value: unknown, label: string): Record<string, unknown> {
@@ -335,3 +377,285 @@ export async function fetchM49TgsFullShadowSpatialChunk(
)),
};
}
function playbackTrackId(value: unknown, label: string): M49TgsFullShadowPlaybackTrackId {
const parsed = text(value, label);
if (parsed !== "frames" && parsed !== "centers" && parsed !== "states" && parsed !== "z-bounds") {
throw new M49TgsFullShadowContractError(`${label}: неизвестная playback-дорожка.`);
}
return parsed;
}
function playbackDtype(value: unknown, label: string): M49TgsFullShadowPlaybackTrack["dtype"] {
const parsed = text(value, label);
if (parsed !== "ndjson" && parsed !== "<f4" && parsed !== "|u1") {
throw new M49TgsFullShadowContractError(`${label}: неизвестный dtype.`);
}
return parsed;
}
function playbackShape(value: unknown, label: string): readonly number[] {
if (!Array.isArray(value) || !value.length) {
throw new M49TgsFullShadowContractError(`${label}: ожидалась размерность.`);
}
return value.map((item, index) => integer(item, `${label}.${index}`));
}
async function fetchPlaybackManifest(
id: string,
fetcher: LaboratoryFetch,
signal: AbortSignal | undefined,
): Promise<{
frameCount: 4489;
cellCount: 2244;
totalByteLength: number;
tracks: ReadonlyMap<M49TgsFullShadowPlaybackTrackId, M49TgsFullShadowPlaybackTrack>;
}> {
const response = await fetcher(
`/api/v1/laboratory/m49/tgs-full-shadow/${encodeURIComponent(id)}/playback/manifest`,
{ method: "GET", headers: { Accept: "application/json" }, signal },
);
if (!response.ok) {
throw new M49TgsFullShadowContractError(`M49 playback manifest недоступен: HTTP ${response.status}.`);
}
const payload = objectValue(await response.json(), "M49 playback manifest");
exact(payload.schema_version, "missioncore.m49-tgs-full-shadow-playback/v1", "M49 playback schema");
exact(payload.result_id, id, "M49 playback result");
exact(payload.coordinate_frame, "map-gravity-local", "M49 playback frame");
exact(payload.access, "read-only", "M49 playback access");
const frameCount = exact(integer(payload.frame_count, "M49 playback frames"), 4489, "M49 playback frames");
const cellCount = exact(integer(payload.cell_count, "M49 playback cells"), 2244, "M49 playback cells");
const totalByteLength = integer(payload.total_byte_length, "M49 playback bytes");
if (!Array.isArray(payload.tracks) || payload.tracks.length !== 4) {
throw new M49TgsFullShadowContractError("M49 playback tracks: неверная размерность.");
}
const tracks = new Map<M49TgsFullShadowPlaybackTrackId, M49TgsFullShadowPlaybackTrack>();
for (const [index, value] of payload.tracks.entries()) {
const row = objectValue(value, `M49 playback track ${index}`);
const trackId = playbackTrackId(row.id, `M49 playback track ${index}.id`);
const sha256 = text(row.sha256, `M49 playback track ${trackId}.sha256`);
if (!/^[a-f0-9]{64}$/.test(sha256) || tracks.has(trackId)) {
throw new M49TgsFullShadowContractError(`M49 playback track ${trackId}: нарушена идентичность.`);
}
tracks.set(trackId, {
id: trackId,
url: text(row.url, `M49 playback track ${trackId}.url`),
mediaType: text(row.media_type, `M49 playback track ${trackId}.media_type`),
dtype: playbackDtype(row.dtype, `M49 playback track ${trackId}.dtype`),
shape: playbackShape(row.shape, `M49 playback track ${trackId}.shape`),
byteLength: integer(row.byte_length, `M49 playback track ${trackId}.byte_length`),
sha256,
});
}
if ([...tracks.values()].reduce((sum, track) => sum + track.byteLength, 0) !== totalByteLength) {
throw new M49TgsFullShadowContractError("M49 playback bytes: нарушен accounting.");
}
return { frameCount, cellCount, totalByteLength, tracks };
}
function sha256Hex(buffer: ArrayBuffer): Promise<string> {
if (!globalThis.crypto?.subtle) {
throw new M49TgsFullShadowContractError("Браузер не поддерживает проверку playback SHA-256.");
}
return globalThis.crypto.subtle.digest("SHA-256", buffer).then((digest) => (
[...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("")
));
}
async function fetchPlaybackTrack(
track: M49TgsFullShadowPlaybackTrack,
fetcher: LaboratoryFetch,
signal: AbortSignal | undefined,
completedBytes: number,
totalBytes: number,
onProgress: ((progress: M49TgsFullShadowPlaybackProgress) => void) | undefined,
): Promise<ArrayBuffer> {
const response = await fetcher(track.url, {
method: "GET",
headers: { Accept: track.mediaType },
signal,
});
if (!response.ok) {
throw new M49TgsFullShadowContractError(`M49 playback ${track.id} недоступен: HTTP ${response.status}.`);
}
const bytes = new Uint8Array(track.byteLength);
if (response.body) {
const reader = response.body.getReader();
let offset = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (offset + value.byteLength > bytes.byteLength) {
throw new M49TgsFullShadowContractError(`M49 playback ${track.id}: превышен объявленный размер.`);
}
bytes.set(value, offset);
offset += value.byteLength;
onProgress?.({
phase: "download",
trackId: track.id,
loadedBytes: completedBytes + offset,
totalBytes,
});
}
if (offset !== bytes.byteLength) {
throw new M49TgsFullShadowContractError(`M49 playback ${track.id}: получен неполный файл.`);
}
} else {
const fallback = new Uint8Array(await response.arrayBuffer());
if (fallback.byteLength !== bytes.byteLength) {
throw new M49TgsFullShadowContractError(`M49 playback ${track.id}: получен неверный размер.`);
}
bytes.set(fallback);
}
onProgress?.({
phase: "verify",
trackId: track.id,
loadedBytes: completedBytes + bytes.byteLength,
totalBytes,
});
if (await sha256Hex(bytes.buffer) !== track.sha256) {
throw new M49TgsFullShadowContractError(`M49 playback ${track.id}: SHA-256 не совпал.`);
}
return bytes.buffer;
}
function equalShape(actual: readonly number[], expected: readonly number[]): boolean {
return actual.length === expected.length && actual.every((value, index) => value === expected[index]);
}
export function parseM49TgsNpyTrack(
buffer: ArrayBuffer,
expectedDtype: "<f4" | "|u1",
expectedShape: readonly number[],
): Float32Array | Uint8Array {
const bytes = new Uint8Array(buffer);
if (bytes.byteLength < 12
|| bytes[0] !== 0x93
|| String.fromCharCode(...bytes.subarray(1, 6)) !== "NUMPY") {
throw new M49TgsFullShadowContractError("M49 playback NPY: неверная сигнатура.");
}
const major = bytes[6];
const headerLength = major === 1
? new DataView(buffer).getUint16(8, true)
: major === 2 || major === 3
? new DataView(buffer).getUint32(8, true)
: -1;
const headerOffset = major === 1 ? 10 : 12;
if (headerLength < 0 || headerOffset + headerLength > bytes.byteLength) {
throw new M49TgsFullShadowContractError("M49 playback NPY: неверный заголовок.");
}
const header = new TextDecoder("latin1").decode(bytes.subarray(headerOffset, headerOffset + headerLength));
const dtype = header.match(/["']descr["']\s*:\s*["']([^"']+)["']/)?.[1];
const fortran = header.match(/["']fortran_order["']\s*:\s*(True|False)/)?.[1];
const rawShape = header.match(/["']shape["']\s*:\s*\(([^)]*)\)/)?.[1];
const shape = rawShape?.split(",").map((value) => value.trim()).filter(Boolean).map(Number) ?? [];
if (dtype !== expectedDtype || fortran !== "False" || !equalShape(shape, expectedShape)) {
throw new M49TgsFullShadowContractError("M49 playback NPY: dtype или shape изменились.");
}
const count = expectedShape.reduce((product, value) => product * value, 1);
const dataOffset = headerOffset + headerLength;
const bytesPerValue = expectedDtype === "<f4" ? 4 : 1;
if (dataOffset + count * bytesPerValue !== buffer.byteLength) {
throw new M49TgsFullShadowContractError("M49 playback NPY: длина payload изменилась.");
}
return expectedDtype === "<f4"
? new Float32Array(buffer, dataOffset, count)
: new Uint8Array(buffer, dataOffset, count);
}
function parsePlaybackFrames(buffer: ArrayBuffer): readonly M49TgsFullShadowPlaybackFrame[] {
const lines = new TextDecoder().decode(buffer).trim().split("\n");
if (lines.length !== 4489) {
throw new M49TgsFullShadowContractError("M49 playback frames: неверная размерность.");
}
return lines.map((line, sourceSequence) => {
const row = objectValue(JSON.parse(line), `M49 playback frame ${sourceSequence}`);
return {
sourceSequence,
sourceFrameIndex: integer(row.source_frame_index, `M49 playback frame ${sourceSequence}.source`),
sessionSeconds: numberValue(row.session_seconds, `M49 playback frame ${sourceSequence}.time`),
sampleAvailable: booleanValue(row.sample_available, `M49 playback frame ${sourceSequence}.available`),
metrics: {
eligiblePointCount: integer(row.eligible_point_count, `M49 playback frame ${sourceSequence}.eligible`),
groundPointCount: integer(row.ground_point_count, `M49 playback frame ${sourceSequence}.ground`),
nongroundPointCount: integer(row.nonground_point_count, `M49 playback frame ${sourceSequence}.nonground`),
rejectedPointCount: integer(row.rejected_point_count, `M49 playback frame ${sourceSequence}.rejected`),
occupiedCellCount: integer(row.occupied_cell_count, `M49 playback frame ${sourceSequence}.occupied`),
},
};
});
}
export async function fetchM49TgsFullShadowPlaybackPack(
id: string,
{
fetcher = fetch,
signal,
onProgress,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
onProgress?: (progress: M49TgsFullShadowPlaybackProgress) => void;
} = {},
): Promise<M49TgsFullShadowPlaybackPack> {
if (!RESULT_ID.test(id)) {
throw new M49TgsFullShadowContractError("M49 playback identity недопустима.");
}
onProgress?.({ phase: "manifest", trackId: null, loadedBytes: 0, totalBytes: 0 });
const manifest = await fetchPlaybackManifest(id, fetcher, signal);
const buffers = new Map<M49TgsFullShadowPlaybackTrackId, ArrayBuffer>();
let completedBytes = 0;
const order: readonly M49TgsFullShadowPlaybackTrackId[] = ["frames", "centers", "states", "z-bounds"];
for (const trackId of order) {
const track = manifest.tracks.get(trackId);
if (!track) throw new M49TgsFullShadowContractError(`M49 playback ${trackId}: дорожка отсутствует.`);
const buffer = await fetchPlaybackTrack(
track,
fetcher,
signal,
completedBytes,
manifest.totalByteLength,
onProgress,
);
buffers.set(trackId, buffer);
completedBytes += track.byteLength;
}
const centersTrack = manifest.tracks.get("centers")!;
const statesTrack = manifest.tracks.get("states")!;
const zBoundsTrack = manifest.tracks.get("z-bounds")!;
const centersRaw = parseM49TgsNpyTrack(
buffers.get("centers")!,
"<f4",
centersTrack.shape,
) as Float32Array;
const centersXyM = Array.from({ length: manifest.cellCount }, (_, index) => (
[centersRaw[index * 2]!, centersRaw[index * 2 + 1]!] as const
));
const states = parseM49TgsNpyTrack(
buffers.get("states")!,
"|u1",
statesTrack.shape,
) as Uint8Array;
const zBoundsM = parseM49TgsNpyTrack(
buffers.get("z-bounds")!,
"<f4",
zBoundsTrack.shape,
) as Float32Array;
const frames = parsePlaybackFrames(buffers.get("frames")!);
onProgress?.({
phase: "ready",
trackId: null,
loadedBytes: manifest.totalByteLength,
totalBytes: manifest.totalByteLength,
});
return {
resultId: id,
frameCount: manifest.frameCount,
cellCount: manifest.cellCount,
totalByteLength: manifest.totalByteLength,
centersXyM,
states,
zBoundsM,
frames,
};
}
@@ -226,6 +226,20 @@ export interface M4ThreatTimelineChunk {
frames: readonly M4ThreatTimelineFrame[];
}
export interface M4ThreatPlaybackProgress {
phase: "manifest" | "download" | "verify" | "ready";
loadedBytes: number;
totalBytes: number;
}
export interface M4ThreatPlaybackPointPack {
resultId: string;
frameCount: 4489;
pointCount: number;
pointOffsets: Uint32Array;
pointsMapXyzM: Float32Array;
}
type LaboratoryFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
export const M4_THREAT_TIMELINE_ENDPOINT_ROOT = "/api/v1/laboratory/m4-threat/results";
class M4ThreatContractError extends Error {}
@@ -803,11 +817,13 @@ export async function fetchM4ThreatTimelineChunk(
signal,
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
cameraObstacleProjectionDelivery = null,
playbackPointPack,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
endpointRoot?: string;
cameraObstacleProjectionDelivery?: M4ThreatTimeline["cameraObstacleProjectionDelivery"];
playbackPointPack?: M4ThreatPlaybackPointPack;
} = {},
): Promise<M4ThreatTimelineChunk> {
const params = new URLSearchParams({
@@ -817,6 +833,7 @@ export async function fetchM4ThreatTimelineChunk(
if (cameraObstacleProjectionDelivery !== null) {
params.set("obstacle_projection", cameraObstacleProjectionDelivery);
}
if (playbackPointPack) params.set("include_points", "false");
const response = await fetcher(
`${endpointRoot}/${result}/timeline/chunk?${params}`,
{ headers: { Accept: "application/json" }, signal },
@@ -834,8 +851,12 @@ export async function fetchM4ThreatTimelineChunk(
if (parsedStart !== startSequence) {
throw new M4ThreatContractError("M4.6 timeline chunk start: нарушен контракт.");
}
const frames = array(payload.frames, "M4.6 timeline frames").map((raw, offset) =>
parseTimelineFrame(raw, result, parsedStart + offset, endpointRoot));
const frames = array(payload.frames, "M4.6 timeline frames").map((raw, offset) => {
const parsed = parseTimelineFrame(raw, result, parsedStart + offset, endpointRoot);
return playbackPointPack
? hydrateM4ThreatTimelineFrame(parsed, playbackPointPack)
: parsed;
});
const parsedCount = integer(payload.frame_count, "M4.6 timeline chunk count");
if (parsedCount !== frames.length || parsedCount > frameCount) {
throw new M4ThreatContractError("M4.6 timeline chunk count: нарушен контракт.");
@@ -851,6 +872,159 @@ export async function fetchM4ThreatTimelineChunk(
};
}
function playbackSha256Hex(buffer: ArrayBuffer): Promise<string> {
if (!globalThis.crypto?.subtle) {
throw new M4ThreatContractError("Браузер не поддерживает проверку playback SHA-256.");
}
return globalThis.crypto.subtle.digest("SHA-256", buffer).then((digest) => (
[...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("")
));
}
function roundedBodyCoordinate(value: number): number {
return Math.round(value * 1_000_000) / 1_000_000;
}
export function hydrateM4ThreatTimelineFrame(
frame: M4ThreatTimelineFrame,
pack: M4ThreatPlaybackPointPack,
): M4ThreatTimelineFrame {
if (frame.sequence >= pack.frameCount || pack.resultId === "") {
throw new M4ThreatContractError("M4.6 binary playback frame: нарушена идентичность.");
}
if (!frame.sourceAvailable || !frame.spatialAvailable || !frame.bodyFrame) {
if (frame.pointCloudSourceCount !== 0) {
throw new M4ThreatContractError("M4.6 binary playback unavailable frame содержит точки.");
}
return { ...frame, pointCloudBodyXyzM: [], pointCloudSampleCount: 0 };
}
const start = pack.pointOffsets[frame.sequence];
const stop = pack.pointOffsets[frame.sequence + 1];
if (start === undefined || stop === undefined || stop < start || stop > pack.pointCount) {
throw new M4ThreatContractError("M4.6 binary playback offsets: нарушена размерность.");
}
const count = stop - start;
if (frame.pointCloudSourceCount !== count || frame.pointCloudSampleCount !== count) {
throw new M4ThreatContractError("M4.6 binary playback point accounting: нарушен контракт.");
}
const origin = frame.bodyFrame.originMapXyzM;
const basis = frame.bodyFrame.basisMapFromBody;
const points = new Array<M4Point3>(count);
for (let index = 0; index < count; index += 1) {
const sourceOffset = (start + index) * 3;
const dx = pack.pointsMapXyzM[sourceOffset]! - origin[0];
const dy = pack.pointsMapXyzM[sourceOffset + 1]! - origin[1];
const dz = pack.pointsMapXyzM[sourceOffset + 2]! - origin[2];
points[index] = [
roundedBodyCoordinate(dx * basis[0][0] + dy * basis[1][0] + dz * basis[2][0]),
roundedBodyCoordinate(dx * basis[0][1] + dy * basis[1][1] + dz * basis[2][1]),
roundedBodyCoordinate(dx * basis[0][2] + dy * basis[1][2] + dz * basis[2][2]),
];
}
return { ...frame, pointCloudBodyXyzM: points, pointCloudSampleCount: count };
}
export async function fetchM4ThreatPlaybackPointPack(
result: string,
{
fetcher = fetch,
signal,
endpointRoot = M4_THREAT_TIMELINE_ENDPOINT_ROOT,
onProgress,
}: {
fetcher?: LaboratoryFetch;
signal?: AbortSignal;
endpointRoot?: string;
onProgress?: (progress: M4ThreatPlaybackProgress) => void;
} = {},
): Promise<M4ThreatPlaybackPointPack> {
resultId(result);
onProgress?.({ phase: "manifest", loadedBytes: 0, totalBytes: 0 });
const manifestResponse = await fetcher(`${endpointRoot}/${result}/timeline/playback`, {
headers: { Accept: "application/json" },
signal,
});
if (!manifestResponse.ok) {
throw new M4ThreatContractError(`M4.6 playback manifest: HTTP ${manifestResponse.status}.`);
}
const manifest = object(await manifestResponse.json(), "M4.6 playback manifest");
exact(manifest.schema_version, "missioncore.recorded-spatial-playback/v1", "M4.6 playback schema");
exact(manifest.result_id, result, "M4.6 playback result");
exact(manifest.coordinate_frame, "map", "M4.6 playback coordinate frame");
exact(manifest.access, "read-only-sealed-binary-playback", "M4.6 playback access");
const frameCount = exact(integer(manifest.frame_count, "M4.6 playback frames"), 4489, "M4.6 playback frames");
const pointCount = integer(manifest.point_count, "M4.6 playback points");
const offsetsRaw = array(manifest.point_offsets, "M4.6 playback offsets");
if (offsetsRaw.length !== frameCount + 1) {
throw new M4ThreatContractError("M4.6 playback offsets: неверная размерность.");
}
const pointOffsets = Uint32Array.from(offsetsRaw, (value) => integer(value, "M4.6 playback offset"));
if (pointOffsets[0] !== 0 || pointOffsets[pointOffsets.length - 1] !== pointCount) {
throw new M4ThreatContractError("M4.6 playback offsets: нарушено замыкание.");
}
for (let index = 1; index < pointOffsets.length; index += 1) {
if (pointOffsets[index]! < pointOffsets[index - 1]!) {
throw new M4ThreatContractError("M4.6 playback offsets: нарушена монотонность.");
}
}
const track = object(manifest.track, "M4.6 playback track");
exact(track.id, "points-map-f32", "M4.6 playback track id");
exact(track.dtype, "<f4", "M4.6 playback dtype");
const shape = array(track.shape, "M4.6 playback shape").map((value) => integer(value, "M4.6 playback shape"));
if (shape.length !== 2 || shape[0] !== pointCount || shape[1] !== 3) {
throw new M4ThreatContractError("M4.6 playback shape: нарушена размерность.");
}
const byteLength = integer(track.bytes, "M4.6 playback bytes");
if (byteLength !== pointCount * 3 * Float32Array.BYTES_PER_ELEMENT) {
throw new M4ThreatContractError("M4.6 playback bytes: нарушен accounting.");
}
const sha256 = text(track.sha256, "M4.6 playback SHA-256");
if (!/^[a-f0-9]{64}$/.test(sha256)) {
throw new M4ThreatContractError("M4.6 playback SHA-256: нарушен контракт.");
}
const response = await fetcher(text(track.url, "M4.6 playback URL"), {
headers: { Accept: "application/octet-stream" },
signal,
});
if (!response.ok) throw new M4ThreatContractError(`M4.6 playback points: HTTP ${response.status}.`);
const bytes = new Uint8Array(byteLength);
if (response.body) {
const reader = response.body.getReader();
let offset = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (offset + value.byteLength > bytes.byteLength) {
throw new M4ThreatContractError("M4.6 playback points превысил объявленный размер.");
}
bytes.set(value, offset);
offset += value.byteLength;
onProgress?.({ phase: "download", loadedBytes: offset, totalBytes: byteLength });
}
if (offset !== byteLength) {
throw new M4ThreatContractError("M4.6 playback points получен не полностью.");
}
} else {
const fallback = new Uint8Array(await response.arrayBuffer());
if (fallback.byteLength !== byteLength) {
throw new M4ThreatContractError("M4.6 playback points: неверный размер.");
}
bytes.set(fallback);
}
onProgress?.({ phase: "verify", loadedBytes: byteLength, totalBytes: byteLength });
if (await playbackSha256Hex(bytes.buffer) !== sha256) {
throw new M4ThreatContractError("M4.6 playback points: SHA-256 не совпал.");
}
onProgress?.({ phase: "ready", loadedBytes: byteLength, totalBytes: byteLength });
return {
resultId: result,
frameCount,
pointCount,
pointOffsets,
pointsMapXyzM: new Float32Array(bytes.buffer),
};
}
export async function fetchM4ThreatCameraPointOverlay(
result: string,
sequence: number,