fix(lab): сжать и адресовать RAV004 overlay
This commit is contained in:
@@ -152,6 +152,7 @@ interface RerunNativeReceiver {
|
||||
|
||||
interface LoadedNativePerceptionSource {
|
||||
receiver: RerunNativeReceiver;
|
||||
descriptorUrl: string;
|
||||
sourceUrl: string;
|
||||
byteLength: number;
|
||||
}
|
||||
@@ -596,7 +597,7 @@ export function resolveRecordedPerceptionViewerSourceUrl(
|
||||
return endpoint.href;
|
||||
}
|
||||
|
||||
/** Verify the immutable URL with a four-byte range request before Rerun opens it. */
|
||||
/** Resolve the immutable overlay generation before Rerun opens its native URL. */
|
||||
export async function probeRecordedPerceptionViewerSource(
|
||||
sourceUrl: string,
|
||||
{
|
||||
@@ -608,7 +609,7 @@ export async function probeRecordedPerceptionViewerSource(
|
||||
signal?: AbortSignal;
|
||||
fetcher?: typeof globalThis.fetch;
|
||||
},
|
||||
): Promise<number> {
|
||||
): Promise<{ sourceUrl: string; byteLength: number }> {
|
||||
const base = new URL(origin);
|
||||
const endpoint = new URL(sourceUrl, base.origin);
|
||||
const allowedParameters = ["application_id", "generation", "recording_id"];
|
||||
@@ -626,57 +627,29 @@ export async function probeRecordedPerceptionViewerSource(
|
||||
throw new Error("Unsafe recorded perception viewer source");
|
||||
}
|
||||
const response = await fetcher(endpoint.href, {
|
||||
method: "GET",
|
||||
method: "HEAD",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/vnd.rerun.rrd",
|
||||
Range: "bytes=0-3",
|
||||
},
|
||||
signal,
|
||||
});
|
||||
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
|
||||
const contentRange = response.headers.get("Content-Range");
|
||||
const rangedLength = contentRange?.match(/^bytes 0-3\/(\d+)$/)?.[1];
|
||||
const declaredLength = Number(
|
||||
rangedLength ?? response.headers.get("Content-Length"),
|
||||
);
|
||||
const declaredLength = Number(response.headers.get("Content-Length"));
|
||||
const etag = response.headers.get("ETag")?.match(/^"([a-f0-9]{64})"$/)?.[1];
|
||||
if (
|
||||
![200, 206].includes(response.status) ||
|
||||
response.status !== 200 ||
|
||||
contentType !== "application/vnd.rerun.rrd" ||
|
||||
response.headers.get("X-Rerun-Format") !== "RRF2" ||
|
||||
!etag ||
|
||||
!Number.isSafeInteger(declaredLength) ||
|
||||
declaredLength < 4 ||
|
||||
declaredLength > MAX_PERCEPTION_BYTES
|
||||
) {
|
||||
throw new Error("Invalid recorded perception viewer response");
|
||||
}
|
||||
const prefix = new Uint8Array(4);
|
||||
let receivedBytes = 0;
|
||||
const reader = response.body?.getReader();
|
||||
if (reader) {
|
||||
while (receivedBytes < prefix.byteLength) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const count = Math.min(value.byteLength, prefix.byteLength - receivedBytes);
|
||||
prefix.set(value.subarray(0, count), receivedBytes);
|
||||
receivedBytes += count;
|
||||
}
|
||||
await reader.cancel();
|
||||
} else {
|
||||
const payload = new Uint8Array(await response.arrayBuffer());
|
||||
const count = Math.min(payload.byteLength, prefix.byteLength);
|
||||
prefix.set(payload.subarray(0, count));
|
||||
receivedBytes = count;
|
||||
}
|
||||
if (
|
||||
receivedBytes !== prefix.byteLength ||
|
||||
prefix[0] !== 0x52 ||
|
||||
prefix[1] !== 0x52 ||
|
||||
prefix[2] !== 0x46 ||
|
||||
prefix[3] !== 0x32
|
||||
) {
|
||||
throw new Error("Invalid recorded perception viewer RRD");
|
||||
}
|
||||
return declaredLength;
|
||||
endpoint.searchParams.set("overlay_generation", etag);
|
||||
return { sourceUrl: endpoint.href, byteLength: declaredLength };
|
||||
}
|
||||
|
||||
export function RerunViewport({
|
||||
@@ -1636,7 +1609,7 @@ export function RerunViewport({
|
||||
return;
|
||||
}
|
||||
const loaded = loadedNativePerceptionSourceRef.current;
|
||||
if (loaded?.receiver === receiver && loaded.sourceUrl === sourceUrl) {
|
||||
if (loaded?.receiver === receiver && loaded.descriptorUrl === sourceUrl) {
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "ready",
|
||||
receivedBytes: loaded.byteLength,
|
||||
@@ -1657,17 +1630,18 @@ export function RerunViewport({
|
||||
void probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
origin: window.location.origin,
|
||||
signal: abort.signal,
|
||||
}).then((byteLength) => {
|
||||
}).then(({ sourceUrl: immutableSourceUrl, byteLength }) => {
|
||||
if (
|
||||
abort.signal.aborted ||
|
||||
perceptionReceiverRef.current !== receiver ||
|
||||
recordedIdentityRef.current !== identity ||
|
||||
!receiver.ready()
|
||||
) return;
|
||||
receiver.open(sourceUrl);
|
||||
receiver.open(immutableSourceUrl);
|
||||
loadedNativePerceptionSourceRef.current = {
|
||||
receiver,
|
||||
sourceUrl,
|
||||
descriptorUrl: sourceUrl,
|
||||
sourceUrl: immutableSourceUrl,
|
||||
byteLength,
|
||||
};
|
||||
onPerceptionLoadChange?.({
|
||||
|
||||
@@ -686,23 +686,56 @@ test("LAB perception sidecar is streamed by native Rerun from one generation-bou
|
||||
`&recording_id=recording-001&generation=${"b".repeat(64)}`,
|
||||
);
|
||||
|
||||
const byteLength = await probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
const overlayGeneration = "c".repeat(64);
|
||||
const probe = await probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
origin: "http://127.0.0.1:5174",
|
||||
fetcher: async (input, init) => {
|
||||
assert.equal(String(input), sourceUrl);
|
||||
assert.equal(init.method, "GET");
|
||||
assert.equal(new Headers(init.headers).get("Range"), "bytes=0-3");
|
||||
return new Response(Uint8Array.from([0x52, 0x52, 0x46, 0x32]), {
|
||||
status: 206,
|
||||
assert.equal(init.method, "HEAD");
|
||||
assert.equal(new Headers(init.headers).get("Range"), null);
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.rerun.rrd",
|
||||
"Content-Length": "4",
|
||||
"Content-Range": "bytes 0-3/393203594",
|
||||
"Content-Length": "186058411",
|
||||
"ETag": `"${overlayGeneration}"`,
|
||||
"X-Rerun-Format": "RRF2",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.equal(byteLength, 393_203_594);
|
||||
assert.deepEqual(probe, {
|
||||
sourceUrl: `${sourceUrl}&overlay_generation=${overlayGeneration}`,
|
||||
byteLength: 186_058_411,
|
||||
});
|
||||
});
|
||||
|
||||
test("LAB native source rejects an unsealed overlay descriptor", async () => {
|
||||
const endpoint =
|
||||
`http://127.0.0.1:5174/api/v1/laboratory/vegetation-shadow/` +
|
||||
`lab-v1-vegetation-shadow-${"a".repeat(64)}/canonical-overlay.rrd`;
|
||||
const sourceUrl = resolveRecordedPerceptionViewerSourceUrl(
|
||||
endpoint,
|
||||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||||
"b".repeat(64),
|
||||
"http://127.0.0.1:5174",
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
origin: "http://127.0.0.1:5174",
|
||||
fetcher: async () => new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.rerun.rrd",
|
||||
"Content-Length": "186058411",
|
||||
"ETag": `"${"c".repeat(64)}"`,
|
||||
"X-Rerun-Format": "RRF1",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
/Invalid recorded perception viewer response/,
|
||||
);
|
||||
});
|
||||
|
||||
test("recorded replay creates an isolated source catalog without live device bindings", () => {
|
||||
|
||||
Reference in New Issue
Block a user