fix(lab): restore canonical spatial replay

This commit is contained in:
DCCONSTRUCTIONS
2026-08-29 19:52:06 +03:00
parent c06b709fd7
commit 5179e93f4a
8 changed files with 263 additions and 53 deletions
@@ -850,6 +850,8 @@ export function RecordedFmp4Player({
const playbackRate = playback?.rate && Number.isFinite(playback.rate) const playbackRate = playback?.rate && Number.isFinite(playback.rate)
? Math.min(4, Math.max(0.25, playback.rate)) ? Math.min(4, Math.max(0.25, playback.rate))
: 1; : 1;
const playbackRateRef = useRef(playbackRate);
playbackRateRef.current = playbackRate;
const presentationEpoch = useMemo( const presentationEpoch = useMemo(
() => selectRecordedMediaEpoch(archive?.manifest.epochs ?? [], currentSeconds), () => selectRecordedMediaEpoch(archive?.manifest.epochs ?? [], currentSeconds),
[archive?.manifest.epochs, currentSeconds], [archive?.manifest.epochs, currentSeconds],
@@ -1183,13 +1185,42 @@ export function RecordedFmp4Player({
message, 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) { if (rollingTarget && previousTarget) {
runtime.target = { runtime.target = {
...candidateTarget, ...candidateTarget,
revision: previousTarget.revision, revision: previousTarget.revision,
}; };
runtime.onTargetBuffered = null; runtime.onTargetBuffered = null;
void pumpRecordedSegmentWindow(runtime).catch(reportPumpError); void pumpRecordedSegmentWindow(runtime)
.then(() => resumePlaybackIfRequested(previousTarget.revision))
.catch(reportPumpError);
return; return;
} }
@@ -1251,6 +1282,7 @@ export function RecordedFmp4Player({
byteLength: archiveByteLength, byteLength: archiveByteLength,
message: null, message: null,
}); });
await resumePlaybackIfRequested(bufferedTarget.revision);
} catch (error) { } catch (error) {
if ( if (
targetReadyAbort.signal.aborted targetReadyAbort.signal.aborted
@@ -1400,6 +1432,41 @@ export function RecordedFmp4Player({
visualState, 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(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
if ((!interactive && onPlaybackChange === undefined) || !video || !epoch || visualState !== "ready") return; if ((!interactive && onPlaybackChange === undefined) || !video || !epoch || visualState !== "ready") return;
@@ -59,6 +59,16 @@ export function laboratoryRecordedClipEndExclusiveNs(
return last.sourceTimeNs + typicalDelta; 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({ export function LaboratoryRecordedClipPlayer({
source, source,
segmentCount, segmentCount,
@@ -94,7 +104,8 @@ export function LaboratoryRecordedClipPlayer({
}) { }) {
const [companionSpatialSize, setCompanionSpatialSize] = useState(69); const [companionSpatialSize, setCompanionSpatialSize] = useState(69);
const lastEmittedSequenceRef = useRef(sequence); const lastEmittedSequenceRef = useRef(sequence);
lastEmittedSequenceRef.current = sequence; const lastObservedSequenceRef = useRef<number | null>(sequence);
const pendingSequenceRef = useRef<number | null>(null);
const frame = useMemo( const frame = useMemo(
() => frames.find((candidate) => candidate.sequence === sequence) ?? frames[0] ?? null, () => frames.find((candidate) => candidate.sequence === sequence) ?? frames[0] ?? null,
[frames, sequence], [frames, sequence],
@@ -113,23 +124,42 @@ export function LaboratoryRecordedClipPlayer({
if (!continuousPlayback && playing) onPlayingChange(false); if (!continuousPlayback && playing) onPlayingChange(false);
}, [continuousPlayback, onPlayingChange, playing]); }, [continuousPlayback, onPlayingChange, playing]);
useEffect(() => {
if (lastObservedSequenceRef.current !== sequence) {
pendingSequenceRef.current = sequence;
}
lastEmittedSequenceRef.current = sequence;
}, [sequence]);
const emitSequence = useCallback((nextSequence: number) => { const emitSequence = useCallback((nextSequence: number) => {
if (lastEmittedSequenceRef.current === nextSequence) return; if (lastEmittedSequenceRef.current === nextSequence) return;
lastEmittedSequenceRef.current = nextSequence; lastEmittedSequenceRef.current = nextSequence;
onSequenceChange(nextSequence); onSequenceChange(nextSequence);
}, [onSequenceChange]); }, [onSequenceChange]);
const requestSequence = useCallback((nextSequence: number) => {
pendingSequenceRef.current = nextSequence;
emitSequence(nextSequence);
}, [emitSequence]);
const handlePlaybackChange = useCallback((next: RecordedObservationPlayback) => { const handlePlaybackChange = useCallback((next: RecordedObservationPlayback) => {
const sourceTimeNs = Math.round(next.currentSeconds * 1_000_000_000); const sourceTimeNs = Math.round(next.currentSeconds * 1_000_000_000);
const first = frames[0]; const first = frames[0];
if (!first || endExclusiveNs === null) return; if (!first || endExclusiveNs === null) return;
if (sourceTimeNs >= endExclusiveNs) { if (sourceTimeNs >= endExclusiveNs) {
emitSequence(first.sequence); requestSequence(first.sequence);
return; return;
} }
const nearest = nearestLaboratoryRecordedClipFrame(frames, sourceTimeNs); const nearest = nearestLaboratoryRecordedClipFrame(frames, sourceTimeNs);
if (nearest) emitSequence(nearest.sequence); if (!nearest) return;
}, [emitSequence, endExclusiveNs, frames]); 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 timelineStart = frames[0]?.sourceTimeNs ?? 0;
const timelineEnd = frames.at(-1)?.sourceTimeNs ?? timelineStart + 1; const timelineEnd = frames.at(-1)?.sourceTimeNs ?? timelineStart + 1;
@@ -142,7 +172,7 @@ export function LaboratoryRecordedClipPlayer({
className="laboratory-recorded-clip-player__spatial" className="laboratory-recorded-clip-player__spatial"
aria-hidden={cameraPresentation === "primary"} aria-hidden={cameraPresentation === "primary"}
> >
{cameraPresentation !== "primary" ? alternativeScene : null} {alternativeScene}
</div> </div>
); );
const cameraPane = ( const cameraPane = (
@@ -202,7 +232,7 @@ export function LaboratoryRecordedClipPlayer({
onPlayingChange={continuousPlayback ? onPlayingChange : undefined} onPlayingChange={continuousPlayback ? onPlayingChange : undefined}
onSeek={(timeNs) => { onSeek={(timeNs) => {
const nearest = nearestLaboratoryRecordedClipFrame(frames, timeNs); const nearest = nearestLaboratoryRecordedClipFrame(frames, timeNs);
if (nearest) emitSequence(nearest.sequence); if (nearest) requestSequence(nearest.sequence);
}} }}
showJumpToEnd={false} showJumpToEnd={false}
/> />
@@ -29,7 +29,6 @@ export interface E31LaboratoryResult {
limitations: readonly string[]; limitations: readonly string[];
access: "read-only"; access: "read-only";
} }
export interface E32LaboratoryResult { export interface E32LaboratoryResult {
resultId: string; resultId: string;
createdAtUtc: string | null; createdAtUtc: string | null;
@@ -53,7 +52,6 @@ export interface E32LaboratoryResult {
}; };
access: "read-only"; access: "read-only";
} }
export interface E33LaboratoryResult { export interface E33LaboratoryResult {
resultId: string; resultId: string;
createdAtUtc: string | null; 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 { Icon, IconButton, StatusBadge } from "@nodedc/ui-react";
import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer"; import { LaboratoryEvidenceViewer } from "../../components/laboratory/LaboratoryEvidenceViewer";
import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer"; import { LaboratoryRecordedClipPlayer } from "../../components/laboratory/LaboratoryRecordedClipPlayer";
import { RerunViewport } from "../../components/RerunViewport";
import { import {
LaboratoryEvidence, LaboratoryEvidence,
LaboratoryResultSummary, LaboratoryResultSummary,
@@ -24,6 +25,11 @@ import {
} from "../../core/laboratory/vegetationShadow"; } from "../../core/laboratory/vegetationShadow";
import { recordedObservationSources } from "../../core/observation/recordedObservationSources"; import { recordedObservationSources } from "../../core/observation/recordedObservationSources";
import { resolveObservationSessionReplay } from "../../core/observation/useObservationSessions"; 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 type { ObservationSourceDescriptor } from "../../core/runtime/contracts";
import { import {
fetchM49TgsFullShadowResult, fetchM49TgsFullShadowResult,
@@ -31,6 +37,10 @@ import {
} from "../../core/laboratory/m49TgsFullShadow"; } from "../../core/laboratory/m49TgsFullShadow";
import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual"; import { M4ReplayThreatVisual } from "./M4ReplayThreatVisual";
import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence"; import { M49TgsFullShadowEvidence } from "./M49TgsFullShadowEvidence";
import {
M48EvidenceModeRail,
type M48BlindEvidenceMode,
} from "./annotation/M48EvidenceModeControls";
function decimal(value: number, digits = 1): string { function decimal(value: number, digits = 1): string {
return value.toLocaleString("ru-RU", { maximumFractionDigits: digits }); return value.toLocaleString("ru-RU", { maximumFractionDigits: digits });
@@ -76,8 +86,12 @@ function FullRouteReviewEvidence({
const [playbackRate, setPlaybackRate] = useState(1); const [playbackRate, setPlaybackRate] = useState(1);
const [mode, setMode] = useState<typeof FULL_ROUTE_MODES[number]["value"]>("vegetation"); const [mode, setMode] = useState<typeof FULL_ROUTE_MODES[number]["value"]>("vegetation");
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [evidenceMode, setEvidenceMode] = useState<M48BlindEvidenceMode>("3d");
const [cameraVisible, setCameraVisible] = useState(true);
const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null); const [videoSource, setVideoSource] = useState<ObservationSourceDescriptor | null>(null);
const [replayLaunch, setReplayLaunch] = useState<ObservationSessionReplayLaunch | null>(null);
const [videoError, setVideoError] = useState<string | null>(null); const [videoError, setVideoError] = useState<string | null>(null);
const spatialControllerRef = useRef<RerunPlaybackController | null>(null);
const frames = useMemo( const frames = useMemo(
() => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({ () => review.frameSourceTimesNs.map((sourceTimeNs, index) => ({
sequence: index + 1, sequence: index + 1,
@@ -97,6 +111,7 @@ function FullRouteReviewEvidence({
useEffect(() => { useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();
setVideoSource(null); setVideoSource(null);
setReplayLaunch(null);
setVideoError(null); setVideoError(null);
void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal }) void resolveObservationSessionReplay(review.sessionId, { signal: controller.signal })
.then((launch) => { .then((launch) => {
@@ -112,7 +127,10 @@ function FullRouteReviewEvidence({
if (!source) { if (!source) {
throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline."); throw new Error("RIGHT-видео не совпало с sealed RAVNOVES004TREE timeline.");
} }
if (!controller.signal.aborted) setVideoSource(source); if (!controller.signal.aborted) {
setVideoSource(source);
setReplayLaunch(launch);
}
}) })
.catch((caught: unknown) => { .catch((caught: unknown) => {
if (!controller.signal.aborted) { if (!controller.signal.aborted) {
@@ -128,6 +146,55 @@ function FullRouteReviewEvidence({
review.timelineStartSeconds, 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 ( return (
<LaboratoryEvidenceViewer <LaboratoryEvidenceViewer
label="RAVNOVES004TREE full recorded review" label="RAVNOVES004TREE full recorded review"
@@ -139,6 +206,7 @@ function FullRouteReviewEvidence({
onExpandedChange={setExpanded} onExpandedChange={setExpanded}
chromeLayout="stacked" chromeLayout="stacked"
> >
<div className="m48-evidence-stage">
{videoSource ? ( {videoSource ? (
<LaboratoryRecordedClipPlayer <LaboratoryRecordedClipPlayer
source={videoSource} source={videoSource}
@@ -147,12 +215,22 @@ function FullRouteReviewEvidence({
sequence={sequence} sequence={sequence}
playing={playing} playing={playing}
playbackRate={playbackRate} playbackRate={playbackRate}
cameraPresentation="primary" cameraPresentation={cameraPresentation}
continuousPlayback continuousPlayback
sourceCount={1} sourceCount={2}
onSequenceChange={setSequence} onSequenceChange={setSequence}
onPlayingChange={setPlaying} onPlayingChange={setPlaying}
onPlaybackRateChange={setPlaybackRate} 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>
)}
cameraOverlay={( cameraOverlay={(
<> <>
<div className="m48-clip-player__pane-label" data-pane="camera"> <div className="m48-clip-player__pane-label" data-pane="camera">
@@ -180,6 +258,17 @@ function FullRouteReviewEvidence({
{videoError ?? "Открываем автономный recorded source…"} {videoError ?? "Открываем автономный recorded source…"}
</div> </div>
)} )}
{videoSource ? (
<M48EvidenceModeRail
mode={evidenceMode}
cameraVisible={cameraVisible}
spatialAvailable={Boolean(spatialProfile)}
planAvailable={false}
onModeChange={setEvidenceMode}
onCameraVisibleChange={setCameraVisible}
/>
) : null}
</div>
</LaboratoryEvidenceViewer> </LaboratoryEvidenceViewer>
); );
} }
@@ -228,7 +317,7 @@ function FullRouteReviewResult({
<LaboratoryEvidence <LaboratoryEvidence
eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE FULL VIDEO" eyebrow="M4.7 TEMPLATE · RAVNOVES004TREE FULL VIDEO"
title="SOURCE / EoMT CITY / DDRNet NATURE · 6830/6830 · TRUTH отсутствует" title="SOURCE / EoMT CITY / DDRNet NATURE · 6830/6830 · TRUTH отсутствует"
kind="diagnostic-model" kind="recorded-replay"
resizable resizable
> >
<FullRouteReviewEvidence resultId={resultId} review={review} /> <FullRouteReviewEvidence resultId={resultId} review={review} />
@@ -10,6 +10,7 @@ interface M48EvidenceModeControlProps {
mode: M48BlindEvidenceMode; mode: M48BlindEvidenceMode;
cameraVisible: boolean; cameraVisible: boolean;
spatialAvailable: boolean; spatialAvailable: boolean;
planAvailable?: boolean;
onModeChange: (mode: M48BlindEvidenceMode) => void; onModeChange: (mode: M48BlindEvidenceMode) => void;
onCameraVisibleChange: (visible: boolean) => void; onCameraVisibleChange: (visible: boolean) => void;
} }
@@ -34,6 +35,7 @@ export function M48EvidenceModeControls({
mode, mode,
cameraVisible, cameraVisible,
spatialAvailable, spatialAvailable,
planAvailable = spatialAvailable,
onModeChange, onModeChange,
onCameraVisibleChange, onCameraVisibleChange,
}: M48EvidenceModeControlProps) { }: M48EvidenceModeControlProps) {
@@ -68,9 +70,9 @@ export function M48EvidenceModeControls({
<IconButton <IconButton
label={spatialMode === "plan" ? "Скрыть план" : "Показать план"} label={spatialMode === "plan" ? "Скрыть план" : "Показать план"}
aria-pressed={spatialMode === "plan"} aria-pressed={spatialMode === "plan"}
disabled={!spatialAvailable || (!cameraVisible && spatialMode === "plan")} disabled={!planAvailable || (!cameraVisible && spatialMode === "plan")}
onClick={() => { onClick={() => {
if (!spatialAvailable) return; if (!planAvailable) return;
onModeChange(nextM48SpatialMode(mode, cameraVisible, "plan")); onModeChange(nextM48SpatialMode(mode, cameraVisible, "plan"));
}} }}
> >
@@ -18,6 +18,7 @@ let nextM48ObjectId;
let laboratoryMetricLegendEntries; let laboratoryMetricLegendEntries;
let nearestLaboratoryRecordedClipFrame; let nearestLaboratoryRecordedClipFrame;
let laboratoryRecordedClipEndExclusiveNs; let laboratoryRecordedClipEndExclusiveNs;
let laboratoryRecordedClipClockGate;
let m48SpatialPlaybackWindow; let m48SpatialPlaybackWindow;
let trimM48SpatialPlaybackCache; let trimM48SpatialPlaybackCache;
let nextM48CameraVisibility; let nextM48CameraVisibility;
@@ -45,6 +46,7 @@ before(async () => {
({ ({
nearestLaboratoryRecordedClipFrame, nearestLaboratoryRecordedClipFrame,
laboratoryRecordedClipEndExclusiveNs, laboratoryRecordedClipEndExclusiveNs,
laboratoryRecordedClipClockGate,
} = await server.ssrLoadModule( } = await server.ssrLoadModule(
"/src/components/laboratory/LaboratoryRecordedClipPlayer.tsx", "/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); 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", () => { test("M4.8 camera and spatial visibility are independent without an empty viewer", () => {
assert.equal(nextM48CameraVisibility("camera", true), true); assert.equal(nextM48CameraVisibility("camera", true), true);
assert.equal(nextM48CameraVisibility("3d", true), false); 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, /requestedSegmentSequence = segmentSequence \?\?/);
assert.match(source, /waitForRecordedVideoInitialFrame/); assert.match(source, /waitForRecordedVideoInitialFrame/);
assert.match(source, /setSegmentRecoveryGeneration/); 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.equal(recordedMediaDecodeStartSequence([1, 1491, 1501], 1500), 1491);
assert.deepEqual( 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 mixed route review/);
assert.match(resultSource, /RAVNOVES004TREE full recorded review/); assert.match(resultSource, /RAVNOVES004TREE full recorded review/);
assert.match(resultSource, /LaboratoryRecordedClipPlayer/); 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, /className="m48-clip-player__overlay"/);
assert.match(resultSource, /linkedTgsResultId/); assert.match(resultSource, /linkedTgsResultId/);
assert.match(benchmarkSource, /M48MaskComparisonVisual/); assert.match(benchmarkSource, /M48MaskComparisonVisual/);