import { Button, Icon, Select } from "@nodedc/ui-react"; import type { ObservationTimelineMode } from "../core/runtime/contracts"; export function ObservationTimeline({ active, sourceCount, mode = "live-only", seekable = false, synchronization = "host-arrival-best-effort", rangeNs = null, currentNs = null, playing = false, onSeek, onPlayingChange, onJumpToEnd, showJumpToEnd = true, playbackRate, onPlaybackRateChange, accumulationSeconds, onAccumulationChange, onAccumulationCommit, className = "", }: { active: boolean; sourceCount: number; mode?: ObservationTimelineMode; seekable?: boolean; synchronization?: "host-arrival-best-effort" | "shared-clock" | "frame-accurate"; rangeNs?: { min: number; max: number } | null; currentNs?: number | null; playing?: boolean; onSeek?: (timeNs: number) => void; onPlayingChange?: (playing: boolean) => void; onJumpToEnd?: () => void; showJumpToEnd?: boolean; playbackRate?: number; onPlaybackRateChange?: (rate: number) => void; accumulationSeconds?: number; onAccumulationChange?: (value: number) => void; onAccumulationCommit?: () => void; className?: string; }) { const playbackRange = normalizeTimelineRange(rangeNs); const buffered = Boolean( seekable && (mode === "buffered" || mode === "recorded") && playbackRange, ); const elapsedSeconds = playbackRange ? timelineOffsetSeconds(playbackRange, currentNs ?? playbackRange.min) : 0; const durationSeconds = playbackRange ? Math.max(0, (playbackRange.max - playbackRange.min) / 1_000_000_000) : 0; const synchronizationLabel = { "host-arrival-best-effort": "Синхронизация по приходу", "shared-clock": "Общие часы", "frame-accurate": "Покадровая синхронизация", }[synchronization]; const accumulationValue = accumulationSeconds === undefined ? null : normalizeAccumulationSeconds(accumulationSeconds); return (
{accumulationValue !== null ? (
Накопление onAccumulationChange?.(normalizeAccumulationSeconds(Number(event.target.value)))} onPointerUp={onAccumulationCommit} onTouchEnd={onAccumulationCommit} onKeyUp={onAccumulationCommit} onBlur={onAccumulationCommit} /> {formatAccumulationDuration(accumulationValue)}
) : null}
) : null}
); } export function normalizeAccumulationSeconds(value: number): number { if (!Number.isFinite(value)) return 0; return Math.min(120, Math.max(0, Math.round(value))); } export function formatAccumulationDuration(value: number): string { const seconds = normalizeAccumulationSeconds(value); return seconds === 0 ? "Кадр" : `${seconds} с`; } export function normalizeTimelineRange( range: { min: number; max: number } | null | undefined, ): { min: number; max: number } | null { if (!range || !Number.isFinite(range.min) || !Number.isFinite(range.max)) return null; if (range.max <= range.min) return null; return range; } export function timelineOffsetSeconds( range: { min: number; max: number }, currentNs: number, ): number { const clamped = Math.min(Math.max(currentNs, range.min), range.max); return Math.max(0, (clamped - range.min) / 1_000_000_000); } export function formatTimelineDuration(seconds: number): string { if (!Number.isFinite(seconds) || seconds < 0) return "00:00.000"; const wholeMinutes = Math.floor(seconds / 60); const remaining = seconds - wholeMinutes * 60; return `${String(wholeMinutes).padStart(2, "0")}:${remaining.toFixed(3).padStart(6, "0")}`; }