fix(observation): admit bounded LAB chunk coverage
This commit is contained in:
parent
082b7057da
commit
5a29a536a2
|
|
@ -46,6 +46,8 @@ export interface RerunPlaybackState {
|
||||||
playing: boolean;
|
playing: boolean;
|
||||||
/** Latest session-time value currently available to the browser receiver. */
|
/** Latest session-time value currently available to the browser receiver. */
|
||||||
bufferedEndNs: number | null;
|
bufferedEndNs: number | null;
|
||||||
|
/** Declared first session-time value, when the archive descriptor provides it. */
|
||||||
|
expectedStartNs: number | null;
|
||||||
/** Declared final session-time value, when the archive descriptor provides it. */
|
/** Declared final session-time value, when the archive descriptor provides it. */
|
||||||
expectedEndNs: number | null;
|
expectedEndNs: number | null;
|
||||||
/** Download progress in the closed interval 0..1, or null without a valid expectation. */
|
/** Download progress in the closed interval 0..1, or null without a valid expectation. */
|
||||||
|
|
@ -182,6 +184,7 @@ export function createRecordedOpenWatchdog<T>({
|
||||||
|
|
||||||
export interface RecordedPlaybackBufferState {
|
export interface RecordedPlaybackBufferState {
|
||||||
bufferedEndNs: number | null;
|
bufferedEndNs: number | null;
|
||||||
|
expectedStartNs: number | null;
|
||||||
expectedEndNs: number | null;
|
expectedEndNs: number | null;
|
||||||
bufferProgress: number | null;
|
bufferProgress: number | null;
|
||||||
fullyBuffered: boolean;
|
fullyBuffered: boolean;
|
||||||
|
|
@ -214,6 +217,7 @@ export function recordedPlaybackBufferState(
|
||||||
if (expectedTimelineEndSeconds === undefined) {
|
if (expectedTimelineEndSeconds === undefined) {
|
||||||
return {
|
return {
|
||||||
bufferedEndNs,
|
bufferedEndNs,
|
||||||
|
expectedStartNs: null,
|
||||||
expectedEndNs: null,
|
expectedEndNs: null,
|
||||||
bufferProgress: null,
|
bufferProgress: null,
|
||||||
fullyBuffered: usableRange !== null,
|
fullyBuffered: usableRange !== null,
|
||||||
|
|
@ -222,6 +226,7 @@ export function recordedPlaybackBufferState(
|
||||||
if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) {
|
if (!Number.isFinite(expectedTimelineEndSeconds) || expectedTimelineEndSeconds < 0) {
|
||||||
return {
|
return {
|
||||||
bufferedEndNs,
|
bufferedEndNs,
|
||||||
|
expectedStartNs: null,
|
||||||
expectedEndNs: null,
|
expectedEndNs: null,
|
||||||
bufferProgress: null,
|
bufferProgress: null,
|
||||||
fullyBuffered: false,
|
fullyBuffered: false,
|
||||||
|
|
@ -240,21 +245,32 @@ export function recordedPlaybackBufferState(
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
bufferedEndNs,
|
bufferedEndNs,
|
||||||
|
expectedStartNs: null,
|
||||||
expectedEndNs: null,
|
expectedEndNs: null,
|
||||||
bufferProgress: null,
|
bufferProgress: null,
|
||||||
fullyBuffered: false,
|
fullyBuffered: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// Rerun split preserves whole boundary chunks so that all generated splits
|
||||||
|
// still sum exactly to the source recording. A verified bounded artifact can
|
||||||
|
// therefore begin slightly before its declared operator window. Admission
|
||||||
|
// requires coverage of the declared window, not byte-chunk boundary equality.
|
||||||
const fullyBuffered = usableRange !== null &&
|
const fullyBuffered = usableRange !== null &&
|
||||||
Math.abs(usableRange.min - expectedStartNs) <= BUFFER_END_TOLERANCE_NS &&
|
usableRange.min <= expectedStartNs + BUFFER_END_TOLERANCE_NS &&
|
||||||
Math.abs(usableRange.max - expectedEndNs) <= BUFFER_END_TOLERANCE_NS;
|
usableRange.max >= expectedEndNs - BUFFER_END_TOLERANCE_NS;
|
||||||
const expectedDurationNs = expectedEndNs - expectedStartNs;
|
const expectedDurationNs = expectedEndNs - expectedStartNs;
|
||||||
const bufferProgress = bufferedEndNs === null
|
const bufferProgress = bufferedEndNs === null
|
||||||
? 0
|
? 0
|
||||||
: expectedDurationNs <= 0
|
: expectedDurationNs <= 0
|
||||||
? (fullyBuffered ? 1 : 0)
|
? (fullyBuffered ? 1 : 0)
|
||||||
: Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs));
|
: Math.min(1, Math.max(0, (bufferedEndNs - expectedStartNs) / expectedDurationNs));
|
||||||
return { bufferedEndNs, expectedEndNs, bufferProgress, fullyBuffered };
|
return {
|
||||||
|
bufferedEndNs,
|
||||||
|
expectedStartNs,
|
||||||
|
expectedEndNs,
|
||||||
|
bufferProgress,
|
||||||
|
fullyBuffered,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A recorded viewport is publishable only after its declared range is complete. */
|
/** A recorded viewport is publishable only after its declared range is complete. */
|
||||||
|
|
@ -272,9 +288,19 @@ export function recordedPlaybackRangeWhenReady(
|
||||||
buffer: RecordedPlaybackBufferState,
|
buffer: RecordedPlaybackBufferState,
|
||||||
artifactVerified: boolean,
|
artifactVerified: boolean,
|
||||||
): { min: number; max: number } | null {
|
): { min: number; max: number } | null {
|
||||||
return artifactVerified && buffer.fullyBuffered && isUsableRecordedPlaybackRange(rangeNs)
|
if (
|
||||||
? rangeNs
|
!artifactVerified ||
|
||||||
: null;
|
!buffer.fullyBuffered ||
|
||||||
|
!isUsableRecordedPlaybackRange(rangeNs)
|
||||||
|
) return null;
|
||||||
|
return {
|
||||||
|
min: buffer.expectedStartNs === null
|
||||||
|
? rangeNs.min
|
||||||
|
: Math.max(rangeNs.min, buffer.expectedStartNs),
|
||||||
|
max: buffer.expectedEndNs === null
|
||||||
|
? rangeNs.max
|
||||||
|
: Math.min(rangeNs.max, buffer.expectedEndNs),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Do not mount host timeline controls while a recorded generation is partial. */
|
/** Do not mount host timeline controls while a recorded generation is partial. */
|
||||||
|
|
@ -1076,6 +1102,7 @@ export function RerunViewport({
|
||||||
...(followLive
|
...(followLive
|
||||||
? {
|
? {
|
||||||
bufferedEndNs: rangeNs?.max ?? null,
|
bufferedEndNs: rangeNs?.max ?? null,
|
||||||
|
expectedStartNs: null,
|
||||||
expectedEndNs: null,
|
expectedEndNs: null,
|
||||||
bufferProgress: null,
|
bufferProgress: null,
|
||||||
fullyBuffered: true,
|
fullyBuffered: true,
|
||||||
|
|
@ -1096,7 +1123,7 @@ export function RerunViewport({
|
||||||
emitPlayback({ playing });
|
emitPlayback({ playing });
|
||||||
},
|
},
|
||||||
jumpToEnd: () => {
|
jumpToEnd: () => {
|
||||||
const range = viewer.get_time_range(event.recording_id, timeline);
|
const range = playbackState?.rangeNs;
|
||||||
if (!range) return;
|
if (!range) return;
|
||||||
viewer.set_current_time(event.recording_id, timeline, range.max);
|
viewer.set_current_time(event.recording_id, timeline, range.max);
|
||||||
emitPlayback({ rangeNs: range, currentNs: range.max, playing: false });
|
emitPlayback({ rangeNs: range, currentNs: range.max, playing: false });
|
||||||
|
|
@ -1151,7 +1178,9 @@ export function RerunViewport({
|
||||||
viewer.set_playing(event.recording_id, true);
|
viewer.set_playing(event.recording_id, true);
|
||||||
},
|
},
|
||||||
initialPlaybackStartSeconds === undefined
|
initialPlaybackStartSeconds === undefined
|
||||||
? undefined
|
? expectedTimelineStartSeconds === undefined
|
||||||
|
? undefined
|
||||||
|
: expectedTimelineStartSeconds * 1_000_000_000
|
||||||
: initialPlaybackStartSeconds * 1_000_000_000,
|
: initialPlaybackStartSeconds * 1_000_000_000,
|
||||||
);
|
);
|
||||||
if (autoplayStarted) {
|
if (autoplayStarted) {
|
||||||
|
|
@ -1167,6 +1196,7 @@ export function RerunViewport({
|
||||||
...(followLive
|
...(followLive
|
||||||
? {
|
? {
|
||||||
bufferedEndNs: rangeNs?.max ?? null,
|
bufferedEndNs: rangeNs?.max ?? null,
|
||||||
|
expectedStartNs: null,
|
||||||
expectedEndNs: null,
|
expectedEndNs: null,
|
||||||
bufferProgress: null,
|
bufferProgress: null,
|
||||||
fullyBuffered: true,
|
fullyBuffered: true,
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo
|
||||||
durationSeconds: 1_450.744,
|
durationSeconds: 1_450.744,
|
||||||
replayable: true,
|
replayable: true,
|
||||||
preparation: null,
|
preparation: null,
|
||||||
|
lab: null,
|
||||||
}]);
|
}]);
|
||||||
assert.equal("raw_capture" in catalog.items[0], false);
|
assert.equal("raw_capture" in catalog.items[0], false);
|
||||||
assert.equal("path" in catalog.items[0], false);
|
assert.equal("path" in catalog.items[0], false);
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ test("a first frame reports buffer telemetry but is not ready for presentation",
|
||||||
const firstFrame = recordedPlaybackBufferState({ min: 0, max: 0 }, 20);
|
const firstFrame = recordedPlaybackBufferState({ min: 0, max: 0 }, 20);
|
||||||
assert.deepEqual(firstFrame, {
|
assert.deepEqual(firstFrame, {
|
||||||
bufferedEndNs: 0,
|
bufferedEndNs: 0,
|
||||||
|
expectedStartNs: 0,
|
||||||
expectedEndNs: 20_000_000_000,
|
expectedEndNs: 20_000_000_000,
|
||||||
bufferProgress: 0,
|
bufferProgress: 0,
|
||||||
fullyBuffered: false,
|
fullyBuffered: false,
|
||||||
|
|
@ -58,6 +59,7 @@ test("host timeline remains unmounted until the verified recording is fully read
|
||||||
currentNs: 0,
|
currentNs: 0,
|
||||||
playing: false,
|
playing: false,
|
||||||
bufferedEndNs: 5,
|
bufferedEndNs: 5,
|
||||||
|
expectedStartNs: 0,
|
||||||
expectedEndNs: 10,
|
expectedEndNs: 10,
|
||||||
bufferProgress: 0.5,
|
bufferProgress: 0.5,
|
||||||
fullyBuffered: false,
|
fullyBuffered: false,
|
||||||
|
|
@ -77,6 +79,7 @@ test("host timeline remains unmounted until the verified recording is fully read
|
||||||
test("buffer progress grows independently and preserves the full-buffer tolerance", () => {
|
test("buffer progress grows independently and preserves the full-buffer tolerance", () => {
|
||||||
assert.deepEqual(recordedPlaybackBufferState({ min: 0, max: 5_000_000_000 }, 20), {
|
assert.deepEqual(recordedPlaybackBufferState({ min: 0, max: 5_000_000_000 }, 20), {
|
||||||
bufferedEndNs: 5_000_000_000,
|
bufferedEndNs: 5_000_000_000,
|
||||||
|
expectedStartNs: 0,
|
||||||
expectedEndNs: 20_000_000_000,
|
expectedEndNs: 20_000_000_000,
|
||||||
bufferProgress: 0.25,
|
bufferProgress: 0.25,
|
||||||
fullyBuffered: false,
|
fullyBuffered: false,
|
||||||
|
|
@ -97,14 +100,37 @@ test("buffer progress grows independently and preserves the full-buffer toleranc
|
||||||
{ min: 0, max: 19_999_500_000 },
|
{ min: 0, max: 19_999_500_000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
const wrongDeclaredStart = recordedPlaybackBufferState(
|
const missingDeclaredStart = recordedPlaybackBufferState(
|
||||||
{ min: 5_000_000_000, max: 20_000_000_000 },
|
{ min: 5_000_000_000, max: 20_000_000_000 },
|
||||||
20,
|
20,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
assert.equal(wrongDeclaredStart.bufferProgress, 1);
|
assert.equal(missingDeclaredStart.bufferProgress, 1);
|
||||||
assert.equal(wrongDeclaredStart.fullyBuffered, false);
|
assert.equal(missingDeclaredStart.fullyBuffered, false);
|
||||||
assert.equal(isRecordedPlaybackReady(true, true, wrongDeclaredStart), false);
|
assert.equal(isRecordedPlaybackReady(true, true, missingDeclaredStart), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a verified split boundary spill covers and clamps the declared LAB window", () => {
|
||||||
|
const actualRange = {
|
||||||
|
min: 34_756_228_209,
|
||||||
|
max: 95_383_857_292,
|
||||||
|
};
|
||||||
|
const buffer = recordedPlaybackBufferState(
|
||||||
|
actualRange,
|
||||||
|
95.383857292,
|
||||||
|
35.421857292,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(buffer.bufferProgress, 1);
|
||||||
|
assert.equal(buffer.fullyBuffered, true);
|
||||||
|
assert.equal(isRecordedPlaybackReady(true, true, buffer), true);
|
||||||
|
assert.deepEqual(
|
||||||
|
recordedPlaybackRangeWhenReady(actualRange, buffer, true),
|
||||||
|
{
|
||||||
|
min: 35_421_857_292,
|
||||||
|
max: 95_383_857_292,
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("recorded autoplay waits for the full range and then runs exactly once", () => {
|
test("recorded autoplay waits for the full range and then runs exactly once", () => {
|
||||||
|
|
|
||||||
|
|
@ -222,8 +222,41 @@ The 144.54-second step is viewer-container publication from already computed
|
||||||
results, not detector, semantic or fusion inference. It is completed before
|
results, not detector, semantic or fusion inference. It is completed before
|
||||||
operator handoff. Warm UI openings reuse the resulting 7,882,010-byte Rerun
|
operator handoff. Warm UI openings reuse the resulting 7,882,010-byte Rerun
|
||||||
overlay. The synchronized base recording contains exact range markers, so the
|
overlay. The synchronized base recording contains exact range markers, so the
|
||||||
host timeline is a 59.962-second comparison window and cannot advance into a
|
host timeline is intended to be a 59.962-second comparison window and cannot
|
||||||
static post-E21 tail.
|
advance into a static post-E21 tail.
|
||||||
|
|
||||||
|
### Operator admission regression and repair
|
||||||
|
|
||||||
|
The first E21.3 browser handoff exposed one additional boundary condition.
|
||||||
|
Rerun `rrd split` preserves complete Arrow chunks so that all split outputs
|
||||||
|
still sum to the source recording. The verified bounded RRD therefore contains
|
||||||
|
a leading boundary chunk beginning at 34.756228209 seconds even though its
|
||||||
|
declared operator window begins at 35.421857292 seconds. Its final timestamp is
|
||||||
|
the declared 95.383857292 seconds.
|
||||||
|
|
||||||
|
The original browser admission gate required both decoded endpoints to equal
|
||||||
|
the declared endpoints within 1 ms. Download and decode consequently reached
|
||||||
|
100%, but the scene could never be admitted because the harmless 665.629 ms
|
||||||
|
leading chunk spill failed the start equality check.
|
||||||
|
|
||||||
|
The repaired contract admits a digest-verified RRD when its decoded range
|
||||||
|
*covers* the declared window. The host timeline and playback controller are
|
||||||
|
then clamped to the declared 35.421857292–95.383857292-second window, and
|
||||||
|
autoplay starts at the declared boundary rather than at the preserved chunk
|
||||||
|
edge. Missing coverage at either boundary still fails closed.
|
||||||
|
|
||||||
|
Browser acceptance on the production build at `127.0.0.1:8000`:
|
||||||
|
|
||||||
|
- three clean catalog-to-E21.3 openings completed;
|
||||||
|
- the second and third openings reached `Визуализатор готов` in about 0.5 s;
|
||||||
|
- no opening remained at `Загрузка и декодирование · 100%`;
|
||||||
|
- the operator timeline was exactly `00:00.000 / 00:59.962`;
|
||||||
|
- 2D objects, segmentation and 3D cuboids were enabled together and rendered
|
||||||
|
in the synchronized composition;
|
||||||
|
- camera/AI presentation changed while playback advanced;
|
||||||
|
- browser console errors after all three openings: 0;
|
||||||
|
- frontend unit tests: 151 / 151 passed; TypeScript check and production build
|
||||||
|
passed.
|
||||||
|
|
||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue