fix(lab): restore canonical spatial replay
This commit is contained in:
@@ -850,6 +850,8 @@ export function RecordedFmp4Player({
|
||||
const playbackRate = playback?.rate && Number.isFinite(playback.rate)
|
||||
? Math.min(4, Math.max(0.25, playback.rate))
|
||||
: 1;
|
||||
const playbackRateRef = useRef(playbackRate);
|
||||
playbackRateRef.current = playbackRate;
|
||||
const presentationEpoch = useMemo(
|
||||
() => selectRecordedMediaEpoch(archive?.manifest.epochs ?? [], currentSeconds),
|
||||
[archive?.manifest.epochs, currentSeconds],
|
||||
@@ -1183,13 +1185,42 @@ export function RecordedFmp4Player({
|
||||
message,
|
||||
});
|
||||
};
|
||||
const resumePlaybackIfRequested = async (revision: number) => {
|
||||
if (
|
||||
runtime.disposed
|
||||
|| segmentedRuntimeRef.current !== runtime
|
||||
|| runtime.target?.revision !== revision
|
||||
|| !playbackPlayingRef.current
|
||||
) return;
|
||||
video.playbackRate = playbackRateRef.current;
|
||||
try {
|
||||
await video.play();
|
||||
} catch {
|
||||
if (
|
||||
runtime.disposed
|
||||
|| segmentedRuntimeRef.current !== runtime
|
||||
|| runtime.target?.revision !== revision
|
||||
) return;
|
||||
onPlayingRejectedRef.current?.();
|
||||
setReadyGeneration(null);
|
||||
setErrorMessage("Запуск записанной камеры отклонён браузером.");
|
||||
setState("error");
|
||||
reportAdmission({
|
||||
phase: "error",
|
||||
byteLength: archiveByteLength,
|
||||
message: "Запуск записанной камеры отклонён браузером.",
|
||||
});
|
||||
}
|
||||
};
|
||||
if (rollingTarget && previousTarget) {
|
||||
runtime.target = {
|
||||
...candidateTarget,
|
||||
revision: previousTarget.revision,
|
||||
};
|
||||
runtime.onTargetBuffered = null;
|
||||
void pumpRecordedSegmentWindow(runtime).catch(reportPumpError);
|
||||
void pumpRecordedSegmentWindow(runtime)
|
||||
.then(() => resumePlaybackIfRequested(previousTarget.revision))
|
||||
.catch(reportPumpError);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1251,6 +1282,7 @@ export function RecordedFmp4Player({
|
||||
byteLength: archiveByteLength,
|
||||
message: null,
|
||||
});
|
||||
await resumePlaybackIfRequested(bufferedTarget.revision);
|
||||
} catch (error) {
|
||||
if (
|
||||
targetReadyAbort.signal.aborted
|
||||
@@ -1400,6 +1432,41 @@ export function RecordedFmp4Player({
|
||||
visualState,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video || !segmented || !playback?.playing || visualState !== "ready") return;
|
||||
let cancelled = false;
|
||||
const resumeIfDecoderReady = () => {
|
||||
if (cancelled || !playbackPlayingRef.current || !video.paused) return;
|
||||
const runtime = segmentedRuntimeRef.current;
|
||||
const target = runtime?.target;
|
||||
if (
|
||||
!runtime
|
||||
|| !target
|
||||
|| runtime.disposed
|
||||
|| video.seeking
|
||||
|| video.readyState < HTMLMediaElement.HAVE_CURRENT_DATA
|
||||
|| Math.abs(video.currentTime - target.targetSeconds) > 0.25
|
||||
|| !recordedMediaTimeRangesContain(video.buffered, target.targetSeconds)
|
||||
) return;
|
||||
playAttemptRevisionRef.current += 1;
|
||||
const playAttemptRevision = playAttemptRevisionRef.current;
|
||||
video.playbackRate = playbackRateRef.current;
|
||||
void video.play().catch(() => {
|
||||
if (cancelled || playAttemptRevisionRef.current !== playAttemptRevision) return;
|
||||
onPlayingRejectedRef.current?.();
|
||||
});
|
||||
};
|
||||
const queueResume = () => window.queueMicrotask(resumeIfDecoderReady);
|
||||
const events = ["pause", "canplay", "seeked"] as const;
|
||||
for (const event of events) video.addEventListener(event, queueResume);
|
||||
resumeIfDecoderReady();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
for (const event of events) video.removeEventListener(event, queueResume);
|
||||
};
|
||||
}, [bufferRevision, playback?.playing, segmented, visualState]);
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if ((!interactive && onPlaybackChange === undefined) || !video || !epoch || visualState !== "ready") return;
|
||||
|
||||
@@ -59,6 +59,16 @@ export function laboratoryRecordedClipEndExclusiveNs(
|
||||
return last.sourceTimeNs + typicalDelta;
|
||||
}
|
||||
|
||||
export function laboratoryRecordedClipClockGate(
|
||||
pendingSequence: number | null,
|
||||
observedSequence: number,
|
||||
): { accept: boolean; pendingSequence: number | null } {
|
||||
if (pendingSequence !== null && pendingSequence !== observedSequence) {
|
||||
return { accept: false, pendingSequence };
|
||||
}
|
||||
return { accept: true, pendingSequence: null };
|
||||
}
|
||||
|
||||
export function LaboratoryRecordedClipPlayer({
|
||||
source,
|
||||
segmentCount,
|
||||
@@ -94,7 +104,8 @@ export function LaboratoryRecordedClipPlayer({
|
||||
}) {
|
||||
const [companionSpatialSize, setCompanionSpatialSize] = useState(69);
|
||||
const lastEmittedSequenceRef = useRef(sequence);
|
||||
lastEmittedSequenceRef.current = sequence;
|
||||
const lastObservedSequenceRef = useRef<number | null>(sequence);
|
||||
const pendingSequenceRef = useRef<number | null>(null);
|
||||
const frame = useMemo(
|
||||
() => frames.find((candidate) => candidate.sequence === sequence) ?? frames[0] ?? null,
|
||||
[frames, sequence],
|
||||
@@ -113,23 +124,42 @@ export function LaboratoryRecordedClipPlayer({
|
||||
if (!continuousPlayback && playing) onPlayingChange(false);
|
||||
}, [continuousPlayback, onPlayingChange, playing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastObservedSequenceRef.current !== sequence) {
|
||||
pendingSequenceRef.current = sequence;
|
||||
}
|
||||
lastEmittedSequenceRef.current = sequence;
|
||||
}, [sequence]);
|
||||
|
||||
const emitSequence = useCallback((nextSequence: number) => {
|
||||
if (lastEmittedSequenceRef.current === nextSequence) return;
|
||||
lastEmittedSequenceRef.current = nextSequence;
|
||||
onSequenceChange(nextSequence);
|
||||
}, [onSequenceChange]);
|
||||
|
||||
const requestSequence = useCallback((nextSequence: number) => {
|
||||
pendingSequenceRef.current = nextSequence;
|
||||
emitSequence(nextSequence);
|
||||
}, [emitSequence]);
|
||||
|
||||
const handlePlaybackChange = useCallback((next: RecordedObservationPlayback) => {
|
||||
const sourceTimeNs = Math.round(next.currentSeconds * 1_000_000_000);
|
||||
const first = frames[0];
|
||||
if (!first || endExclusiveNs === null) return;
|
||||
if (sourceTimeNs >= endExclusiveNs) {
|
||||
emitSequence(first.sequence);
|
||||
requestSequence(first.sequence);
|
||||
return;
|
||||
}
|
||||
const nearest = nearestLaboratoryRecordedClipFrame(frames, sourceTimeNs);
|
||||
if (nearest) emitSequence(nearest.sequence);
|
||||
}, [emitSequence, endExclusiveNs, frames]);
|
||||
if (!nearest) return;
|
||||
lastObservedSequenceRef.current = nearest.sequence;
|
||||
const gate = laboratoryRecordedClipClockGate(
|
||||
pendingSequenceRef.current,
|
||||
nearest.sequence,
|
||||
);
|
||||
pendingSequenceRef.current = gate.pendingSequence;
|
||||
if (gate.accept) emitSequence(nearest.sequence);
|
||||
}, [emitSequence, endExclusiveNs, frames, requestSequence]);
|
||||
|
||||
const timelineStart = frames[0]?.sourceTimeNs ?? 0;
|
||||
const timelineEnd = frames.at(-1)?.sourceTimeNs ?? timelineStart + 1;
|
||||
@@ -142,7 +172,7 @@ export function LaboratoryRecordedClipPlayer({
|
||||
className="laboratory-recorded-clip-player__spatial"
|
||||
aria-hidden={cameraPresentation === "primary"}
|
||||
>
|
||||
{cameraPresentation !== "primary" ? alternativeScene : null}
|
||||
{alternativeScene}
|
||||
</div>
|
||||
);
|
||||
const cameraPane = (
|
||||
@@ -202,7 +232,7 @@ export function LaboratoryRecordedClipPlayer({
|
||||
onPlayingChange={continuousPlayback ? onPlayingChange : undefined}
|
||||
onSeek={(timeNs) => {
|
||||
const nearest = nearestLaboratoryRecordedClipFrame(frames, timeNs);
|
||||
if (nearest) emitSequence(nearest.sequence);
|
||||
if (nearest) requestSequence(nearest.sequence);
|
||||
}}
|
||||
showJumpToEnd={false}
|
||||
/>
|
||||
|
||||
@@ -29,7 +29,6 @@ export interface E31LaboratoryResult {
|
||||
limitations: readonly string[];
|
||||
access: "read-only";
|
||||
}
|
||||
|
||||
export interface E32LaboratoryResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string | null;
|
||||
@@ -53,7 +52,6 @@ export interface E32LaboratoryResult {
|
||||
};
|
||||
access: "read-only";
|
||||
}
|
||||
|
||||
export interface E33LaboratoryResult {
|
||||
resultId: string;
|
||||
createdAtUtc: string | null;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
|
||||
|
||||
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
|
||||
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
|
||||
import { RerunViewport } from "../../components/RerunViewport";
|
||||
import {
|
||||
LaboratoryEvidence,
|
||||
LaboratoryResultSummary,
|
||||
@@ -24,6 +25,11 @@ import {
|
||||
} from "../../core/laboratory/vegetationShadow";
|
||||
import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
|
||||
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions";
|
||||
import type { ObservationSessionReplayLaunch } from "../../core/observation/sessionArchive";
|
||||
import {
|
||||
recordedSessionRerunProfile,
|
||||
type RerunPlaybackController,
|
||||
} from "../../core/observation/viewerProfile";
|
||||
import type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
|
||||
import {
|
||||
fetchM49TgsFullShadowResult,
|
||||
@@ -31,6 +37,10 @@ import {
|
||||
} from "../../core/laboratory/m49TgsFullShadow";
|
||||
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
|
||||
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
|
||||
import {
|
||||
M48EvidenceModeRail,
|
||||
type M48BlindEvidenceMode,
|
||||
} from "./annotation/M48EvidenceModeControls";
|
||||
|
||||
function decimal(value: number, digits = 1): string {
|
||||
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
|
||||
@@ -76,8 +86,12 @@ function FullRouteReviewEvidence({
|
||||
const [playbackRate, setPlaybackRate] = useState(1);
|
||||
const [mode, setMode] = useState<typeof FULL_ROUTE_MODES[number]["value"]>("vegetation");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [evidenceMode, setEvidenceMode] = useState<M48BlindEvidenceMode>("3d");
|
||||
const [cameraVisible, setCameraVisible] = useState(true);
|
||||
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
|
||||
const [replayLaunch, setReplayLaunch] = useState<ObservationSessionReplayLaunch | null>(null);
|
||||
const [videoError, setVideoError] = useState<string | null>(null);
|
||||
const spatialControllerRef = useRef<RerunPlaybackController | null>(null);
|
||||
const frames = useMemo(
|
||||
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
|
||||
sequence: index + 1,
|
||||
@@ -97,6 +111,7 @@ function FullRouteReviewEvidence({
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
setVideoSource(null);
|
||||
setReplayLaunch(null);
|
||||
setVideoError(null);
|
||||
void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal })
|
||||
.then((launch) => {
|
||||
@@ -112,7 +127,10 @@ function FullRouteReviewEvidence({
|
||||
if (!source) {
|
||||
throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline.");
|
||||
}
|
||||
if (!controller.signal.aborted) setVideoSource(source);
|
||||
if (!controller.signal.aborted) {
|
||||
setVideoSource(source);
|
||||
setReplayLaunch(launch);
|
||||
}
|
||||
})
|
||||
.catch((caught: unknown) => {
|
||||
if (!controller.signal.aborted) {
|
||||
@@ -128,6 +146,55 @@ function FullRouteReviewEvidence({
|
||||
review.timelineStartSeconds,
|
||||
]);
|
||||
|
||||
const spatialProfile = useMemo(() => replayLaunch ? recordedSessionRerunProfile({
|
||||
sourceUrl: replayLaunch.viewerSourceUrl,
|
||||
artifact: {
|
||||
sourceUrl: replayLaunch.sourceUrl,
|
||||
viewerSourceUrl: replayLaunch.viewerSourceUrl,
|
||||
byteLength: replayLaunch.byteLength,
|
||||
sha256: replayLaunch.sha256,
|
||||
},
|
||||
autoplayWhenReady: false,
|
||||
presentationGate: "ready",
|
||||
expectedTimelineStartSeconds: replayLaunch.timelineStartSeconds,
|
||||
expectedTimelineEndSeconds: replayLaunch.timelineEndSeconds,
|
||||
initialPlaybackStartSeconds: review.timelineStartSeconds,
|
||||
view: "spatial",
|
||||
viewResetGeneration: 0,
|
||||
followTrajectory: false,
|
||||
perceptionLayers: {
|
||||
enabled: false,
|
||||
detections2d: false,
|
||||
segmentation: false,
|
||||
cuboids3d: false,
|
||||
},
|
||||
perceptionRetryGeneration: 0,
|
||||
lockPerceptionCameraInteraction: false,
|
||||
}) : null, [replayLaunch, review.timelineStartSeconds]);
|
||||
const activeFrame = frames.find((candidate) => candidate.sequence === sequence)
|
||||
?? frames[0]
|
||||
?? null;
|
||||
const activeSourceTimeNsRef = useRef(activeFrame?.sourceTimeNs ?? null);
|
||||
activeSourceTimeNsRef.current = activeFrame?.sourceTimeNs ?? null;
|
||||
const handleSpatialControllerChange = useCallback((controller: RerunPlaybackController | null) => {
|
||||
spatialControllerRef.current = controller;
|
||||
const sourceTimeNs = activeSourceTimeNsRef.current;
|
||||
if (!controller || sourceTimeNs === null) return;
|
||||
controller.setPlaying(false);
|
||||
controller.seek(sourceTimeNs);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = spatialControllerRef.current;
|
||||
if (!controller || !activeFrame) return;
|
||||
controller.setPlaying(false);
|
||||
controller.seek(activeFrame.sourceTimeNs);
|
||||
}, [activeFrame]);
|
||||
|
||||
const cameraPresentation = evidenceMode === "camera"
|
||||
? "primary"
|
||||
: cameraVisible ? "companion" : "hidden";
|
||||
|
||||
return (
|
||||
<LaboratoryEvidenceViewer
|
||||
label="RAVNOVES004TREE full recorded review"
|
||||
@@ -139,47 +206,69 @@ function FullRouteReviewEvidence({
|
||||
onExpandedChange={setExpanded}
|
||||
chromeLayout="stacked"
|
||||
>
|
||||
{videoSource ? (
|
||||
<LaboratoryRecordedClipPlayer
|
||||
source={videoSource}
|
||||
segmentCount={review.frameCount}
|
||||
frames={frames}
|
||||
sequence={sequence}
|
||||
playing={playing}
|
||||
playbackRate={playbackRate}
|
||||
cameraPresentation="primary"
|
||||
continuousPlayback
|
||||
sourceCount={1}
|
||||
onSequenceChange={setSequence}
|
||||
onPlayingChange={setPlaying}
|
||||
onPlaybackRateChange={setPlaybackRate}
|
||||
cameraOverlay={(
|
||||
<>
|
||||
<div className="m48-clip-player__pane-label" data-pane="camera">
|
||||
{mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`}
|
||||
<div className="m48-evidence-stage">
|
||||
{videoSource ? (
|
||||
<LaboratoryRecordedClipPlayer
|
||||
source={videoSource}
|
||||
segmentCount={review.frameCount}
|
||||
frames={frames}
|
||||
sequence={sequence}
|
||||
playing={playing}
|
||||
playbackRate={playbackRate}
|
||||
cameraPresentation={cameraPresentation}
|
||||
continuousPlayback
|
||||
sourceCount={2}
|
||||
onSequenceChange={setSequence}
|
||||
onPlayingChange={setPlaying}
|
||||
onPlaybackRateChange={setPlaybackRate}
|
||||
alternativeScene={spatialProfile ? (
|
||||
<RerunViewport
|
||||
profile={spatialProfile}
|
||||
onPlaybackControllerChange={handleSpatialControllerChange}
|
||||
/>
|
||||
) : (
|
||||
<div className="m4-replay-threat-visual__pane-status" role="status">
|
||||
Открываем sealed RRD и point-cloud evidence…
|
||||
</div>
|
||||
{layer && semantic ? (
|
||||
<div className="m48-clip-player__overlay">
|
||||
<RecordedEvidenceSemanticMaskOverlay
|
||||
src={vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", maskSequence)}
|
||||
prefetchSrcs={prefetchSrcs}
|
||||
imageWidth={review.width}
|
||||
imageHeight={review.height}
|
||||
classes={semantic.classes}
|
||||
palette={semantic.palette}
|
||||
opacity={0.76}
|
||||
ariaLabel={`${layer.name} semantic prediction`}
|
||||
/>
|
||||
)}
|
||||
cameraOverlay={(
|
||||
<>
|
||||
<div className="m48-clip-player__pane-label" data-pane="camera">
|
||||
{mode === "source" ? "SOURCE" : `${mode === "city" ? "EoMT CITY" : "DDRNet NATURE"} · КАДР ${sequence}/${review.frameCount}`}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
|
||||
{videoError ?? "Открываем автономный recorded source…"}
|
||||
</div>
|
||||
)}
|
||||
{layer && semantic ? (
|
||||
<div className="m48-clip-player__overlay">
|
||||
<RecordedEvidenceSemanticMaskOverlay
|
||||
src={vegetationFullRouteMaskUrl(resultId, mode as "city" | "vegetation", maskSequence)}
|
||||
prefetchSrcs={prefetchSrcs}
|
||||
imageWidth={review.width}
|
||||
imageHeight={review.height}
|
||||
classes={semantic.classes}
|
||||
palette={semantic.palette}
|
||||
opacity={0.76}
|
||||
ariaLabel={`${layer.name} semantic prediction`}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div className="m4-replay-threat-visual__pane-status" role={videoError ? "alert" : "status"}>
|
||||
{videoError ?? "Открываем автономный recorded source…"}
|
||||
</div>
|
||||
)}
|
||||
{videoSource ? (
|
||||
<M48EvidenceModeRail
|
||||
mode={evidenceMode}
|
||||
cameraVisible={cameraVisible}
|
||||
spatialAvailable={Boolean(spatialProfile)}
|
||||
planAvailable={false}
|
||||
onModeChange={setEvidenceMode}
|
||||
onCameraVisibleChange={setCameraVisible}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</LaboratoryEvidenceViewer>
|
||||
);
|
||||
}
|
||||
@@ -228,7 +317,7 @@ function FullRouteReviewResult({
|
||||
<LaboratoryEvidence
|
||||
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE FULL VIDEO"
|
||||
title="SOURCE / EoMT CITY / DDRNet NATURE · 6830/6830 · TRUTH отсутствует"
|
||||
kind="diagnostic-model"
|
||||
kind="recorded-replay"
|
||||
resizable
|
||||
>
|
||||
<FullRouteReviewEvidence resultId={resultId} review={review} />
|
||||
|
||||
+4
-2
@@ -10,6 +10,7 @@ interface M48EvidenceModeControlProps {
|
||||
mode: M48BlindEvidenceMode;
|
||||
cameraVisible: boolean;
|
||||
spatialAvailable: boolean;
|
||||
planAvailable?: boolean;
|
||||
onModeChange: (mode: M48BlindEvidenceMode) => void;
|
||||
onCameraVisibleChange: (visible: boolean) => void;
|
||||
}
|
||||
@@ -34,6 +35,7 @@ export function M48EvidenceModeControls({
|
||||
mode,
|
||||
cameraVisible,
|
||||
spatialAvailable,
|
||||
planAvailable = spatialAvailable,
|
||||
onModeChange,
|
||||
onCameraVisibleChange,
|
||||
}: M48EvidenceModeControlProps) {
|
||||
@@ -68,9 +70,9 @@ export function M48EvidenceModeControls({
|
||||
<IconButton
|
||||
label={spatialMode === "plan" ? "Скрыть план" : "Показать план"}
|
||||
aria-pressed={spatialMode === "plan"}
|
||||
disabled={!spatialAvailable || (!cameraVisible && spatialMode === "plan")}
|
||||
disabled={!planAvailable || (!cameraVisible && spatialMode === "plan")}
|
||||
onClick={() => {
|
||||
if (!spatialAvailable) return;
|
||||
if (!planAvailable) return;
|
||||
onModeChange(nextM48SpatialMode(mode, cameraVisible, "plan"));
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,6 +18,7 @@ let nextM48ObjectId;
|
||||
let laboratoryMetricLegendEntries;
|
||||
let nearestLaboratoryRecordedClipFrame;
|
||||
let laboratoryRecordedClipEndExclusiveNs;
|
||||
let laboratoryRecordedClipClockGate;
|
||||
let m48SpatialPlaybackWindow;
|
||||
let trimM48SpatialPlaybackCache;
|
||||
let nextM48CameraVisibility;
|
||||
@@ -45,6 +46,7 @@ before(async () => {
|
||||
({
|
||||
nearestLaboratoryRecordedClipFrame,
|
||||
laboratoryRecordedClipEndExclusiveNs,
|
||||
laboratoryRecordedClipClockGate,
|
||||
} = await server.ssrLoadModule(
|
||||
"/src/components/laboratory/LaboratoryRecordedClipPlayer.tsx",
|
||||
));
|
||||
@@ -342,6 +344,21 @@ test("shared recorded clip clock selects exact frames and one stable loop bounda
|
||||
assert.equal(laboratoryRecordedClipEndExclusiveNs(frames), 1_300_000_000);
|
||||
});
|
||||
|
||||
test("shared recorded clip rejects stale media callbacks until an explicit seek lands", () => {
|
||||
assert.deepEqual(laboratoryRecordedClipClockGate(1, 123), {
|
||||
accept: false,
|
||||
pendingSequence: 1,
|
||||
});
|
||||
assert.deepEqual(laboratoryRecordedClipClockGate(1, 1), {
|
||||
accept: true,
|
||||
pendingSequence: null,
|
||||
});
|
||||
assert.deepEqual(laboratoryRecordedClipClockGate(null, 124), {
|
||||
accept: true,
|
||||
pendingSequence: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("M4.8 camera and spatial visibility are independent without an empty viewer", () => {
|
||||
assert.equal(nextM48CameraVisibility("camera", true), true);
|
||||
assert.equal(nextM48CameraVisibility("3d", true), false);
|
||||
|
||||
@@ -188,6 +188,9 @@ test("production replay derives bounded fragments and retains native range fallb
|
||||
assert.match(source, /requestedSegmentSequence = segmentSequence \?\?/);
|
||||
assert.match(source, /waitForRecordedVideoInitialFrame/);
|
||||
assert.match(source, /setSegmentRecoveryGeneration/);
|
||||
assert.match(source, /resumePlaybackIfRequested/);
|
||||
assert.match(source, /!playbackPlayingRef\.current/);
|
||||
assert.match(source, /await video\.play\(\)/);
|
||||
|
||||
assert.equal(recordedMediaDecodeStartSequence([1, 1491, 1501], 1500), 1491);
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -384,6 +384,10 @@ test("vegetation realtime LAB and archival benchmark use separate admitted instr
|
||||
assert.match(resultSource, /RAVNOVES004TREE mixed route review/);
|
||||
assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
|
||||
assert.match(resultSource, /LaboratoryRecordedClipPlayer/);
|
||||
assert.match(resultSource, /RerunViewport/);
|
||||
assert.match(resultSource, /M48EvidenceModeRail/);
|
||||
assert.match(resultSource, /planAvailable=\{false\}/);
|
||||
assert.match(resultSource, /kind="recorded-replay"/);
|
||||
assert.match(resultSource, /className="m48-clip-player__overlay"/);
|
||||
assert.match(resultSource, /linkedTgsResultId/);
|
||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||
|
||||
Reference in New Issue
Block a user