feat(control-station): add atomic recorded-session playback

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 17:51:24 +03:00
parent 007ff9fff2
commit e94c64eebd
35 changed files with 8907 additions and 302 deletions
@@ -6,6 +6,14 @@ import type {
ObservationSourceModality,
} from "../core/runtime/contracts";
import { MseFmp4WebSocketPlayer } from "./MseFmp4WebSocketPlayer";
import {
RecordedFmp4Player,
type RecordedObservationPlayback,
} from "./RecordedFmp4Player";
import type {
RecordedAdmissionPhase,
RecordedCameraAdmissionState,
} from "../core/observation/recordedSessionAdmission";
const sourceIcon: Record<ObservationSourceModality, IconName> = {
"point-cloud": "globe",
@@ -29,7 +37,24 @@ export function observationSourceStatusLabel(source: ObservationSourceDescriptor
return availabilityCopy[source.availability];
}
export function ObservationMedia({ source }: { source: ObservationSourceDescriptor }) {
export function ObservationMedia({
source,
playback,
prepareRecorded = true,
recordedSessionGate = "ready",
recordedAdmissionKey = null,
onRecordedAdmissionChange,
}: {
source: ObservationSourceDescriptor;
playback?: RecordedObservationPlayback | null;
prepareRecorded?: boolean;
recordedSessionGate?: RecordedAdmissionPhase;
recordedAdmissionKey?: string | null;
onRecordedAdmissionChange?: (
sourceId: string,
state: RecordedCameraAdmissionState,
) => void;
}) {
const sourceSelected = source.activation ? source.activation.selected : true;
const deliveryActive = Boolean(source.delivery && sourceSelected);
@@ -41,6 +66,32 @@ export function ObservationMedia({ source }: { source: ObservationSourceDescript
return <MseFmp4WebSocketPlayer delivery={source.delivery} label={source.label} />;
}
if (
deliveryActive &&
source.delivery?.kind === "recorded-fmp4-manifest" &&
source.modality === "video"
) {
if (!prepareRecorded) {
return (
<div className="observation-media__empty" role="status">
<span className="busy-indicator" aria-hidden="true" />
<strong>Ожидает подготовки</strong>
<span>Канал будет проверен последовательно в рамках атомарной сессии.</span>
</div>
);
}
return (
<RecordedFmp4Player
source={source}
playback={playback}
prepare
sessionGate={recordedSessionGate}
admissionKey={recordedAdmissionKey}
onAdmissionChange={(state) => onRecordedAdmissionChange?.(source.id, state)}
/>
);
}
if (deliveryActive && source.delivery?.kind === "video-url" && source.modality === "video") {
return (
<video
@@ -193,7 +244,8 @@ export function ObservationSourcePicker({
</div>
)}
<footer className="observation-source-menu__foot">
Каналы приходят из активного device-плагина; сцена не знает модель оборудования.
Каналы приходят из активного контура или выбранной сохранённой сессии; сцена не знает
модель оборудования.
</footer>
</Dropdown>
);