feat(k1): add live cameras and reliable spatial following

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 00:26:03 +03:00
parent a281faf923
commit 2bda1986bd
33 changed files with 2927 additions and 330 deletions
@@ -5,6 +5,7 @@ import type {
ObservationSourceDescriptor,
ObservationSourceModality,
} from "../core/runtime/contracts";
import { MseFmp4WebSocketPlayer } from "./MseFmp4WebSocketPlayer";
const sourceIcon: Record<ObservationSourceModality, IconName> = {
"point-cloud": "globe",
@@ -29,7 +30,38 @@ export function observationSourceStatusLabel(source: ObservationSourceDescriptor
}
export function ObservationMedia({ source }: { source: ObservationSourceDescriptor }) {
if (source.previewUrl && source.modality === "video") {
const sourceSelected = source.activation ? source.activation.selected : true;
const deliveryActive = Boolean(source.delivery && sourceSelected);
if (
deliveryActive &&
source.delivery?.kind === "mse-fmp4-websocket" &&
source.modality === "video"
) {
return <MseFmp4WebSocketPlayer delivery={source.delivery} label={source.label} />;
}
if (deliveryActive && source.delivery?.kind === "video-url" && source.modality === "video") {
return (
<video
className="observation-media__asset"
src={source.delivery.url}
autoPlay
muted
playsInline
/>
);
}
if (
deliveryActive &&
source.delivery?.kind === "image-url" &&
(source.modality === "image" || source.modality === "depth")
) {
return <img className="observation-media__asset" src={source.delivery.url} alt={source.label} />;
}
if (sourceSelected && source.previewUrl && source.modality === "video") {
return (
<video
className="observation-media__asset"
@@ -41,7 +73,11 @@ export function ObservationMedia({ source }: { source: ObservationSourceDescript
);
}
if (source.previewUrl && (source.modality === "image" || source.modality === "depth")) {
if (
sourceSelected &&
source.previewUrl &&
(source.modality === "image" || source.modality === "depth")
) {
return <img className="observation-media__asset" src={source.previewUrl} alt={source.label} />;
}
@@ -51,7 +87,11 @@ export function ObservationMedia({ source }: { source: ObservationSourceDescript
<strong>{observationSourceStatusLabel(source)}</strong>
<span>
{source.modality === "video"
? "Канал известен, browser-preview ещё не подключён"
? source.activation?.controllable
? source.activation.selected
? "Повторите подключение — локальный адаптер перезапустит выбранную камеру"
: "Откройте канал — локальный адаптер подключит выбранную камеру"
: "Канал известен, browser-preview сейчас недоступен"
: source.description}
</span>
</div>
@@ -61,11 +101,13 @@ export function ObservationMedia({ source }: { source: ObservationSourceDescript
export function ObservationSourcePicker({
sources,
visibleSourceIds,
pendingSourceIds,
onToggle,
}: {
sources: readonly ObservationSourceDescriptor[];
visibleSourceIds: ReadonlySet<string>;
onToggle: (sourceId: string) => void;
pendingSourceIds?: ReadonlySet<string>;
onToggle: (sourceId: string) => void | Promise<boolean>;
}) {
const visibleCount = sources.filter((source) => visibleSourceIds.has(source.id)).length;
@@ -108,6 +150,14 @@ export function ObservationSourcePicker({
<div className="observation-source-menu__list">
{sources.map((source) => {
const selected = visibleSourceIds.has(source.id);
const pending = pendingSourceIds?.has(source.id) ?? false;
const canOpen = Boolean(
selected ||
source.modality === "point-cloud" ||
source.previewUrl ||
source.delivery ||
source.activation?.controllable,
);
return (
<button
key={source.id}
@@ -115,7 +165,8 @@ export function ObservationSourcePicker({
className="nodedc-dropdown-option observation-source-option"
data-selected={selected ? "true" : undefined}
aria-pressed={selected}
onClick={() => onToggle(source.id)}
disabled={pending || !canOpen}
onClick={() => void onToggle(source.id)}
>
<span className="nodedc-dropdown-option__icon">
<Icon name={sourceIcon[source.modality]} size={16} />
@@ -124,7 +175,7 @@ export function ObservationSourcePicker({
<span className="nodedc-dropdown-option__label">{source.label}</span>
<span className="nodedc-dropdown-option__description">
<i data-availability={source.availability} aria-hidden="true" />
{observationSourceStatusLabel(source)} · {source.endpointLabel || source.transport}
{pending ? "Переключение" : observationSourceStatusLabel(source)} · {source.endpointLabel || source.transport}
</span>
</span>
<span className="nodedc-dropdown-option__check">