6 Commits

4 changed files with 33 additions and 27 deletions

View File

@@ -1,3 +1,4 @@
import mpegts from 'mpegts.js';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
type CameraViewProps = { type CameraViewProps = {
@@ -11,8 +12,7 @@ export function CameraView({ title = 'Камера', wsUrl }: CameraViewProps) {
useEffect(() => { useEffect(() => {
const video = videoRef.current; const video = videoRef.current;
let player: import('mpegts.js').default.Player | null = null; let player: ReturnType<typeof mpegts.createPlayer> | null = null;
let cancelled = false;
if (!video) { if (!video) {
return; return;
@@ -22,32 +22,27 @@ export function CameraView({ title = 'Камера', wsUrl }: CameraViewProps) {
video.addEventListener('playing', clearError); video.addEventListener('playing', clearError);
setError(null); setError(null);
void import('mpegts.js').then(({ default: mpegts }) => { if (!mpegts.isSupported()) {
if (cancelled) { setError('Браузер не поддерживает MPEG-TS поток');
return; return () => {
} video.removeEventListener('playing', clearError);
};
}
if (!mpegts.isSupported()) { player = mpegts.createPlayer(
setError('Браузер не поддерживает MPEG-TS поток'); { type: 'mpegts', isLive: true, url: wsUrl },
return; { liveBufferLatencyChasing: true, liveBufferLatencyMaxLatency: 1.5 },
} );
player = mpegts.createPlayer( player.attachMediaElement(video);
{ type: 'mpegts', isLive: true, url: wsUrl }, player.load();
{ liveBufferLatencyChasing: true, liveBufferLatencyMaxLatency: 1.5 },
);
player.attachMediaElement(video); const playResult = player.play();
player.load(); if (playResult instanceof Promise) {
void playResult.then(() => setError(null)).catch(() => setError('Не удалось запустить воспроизведение'));
const playResult = player.play(); }
if (playResult instanceof Promise) {
void playResult.then(() => setError(null)).catch(() => setError('Не удалось запустить воспроизведение'));
}
});
return () => { return () => {
cancelled = true;
video.removeEventListener('playing', clearError); video.removeEventListener('playing', clearError);
player?.pause(); player?.pause();
player?.unload(); player?.unload();

View File

@@ -4,8 +4,9 @@ import { useRef } from 'react';
import { CalendarDays, CalendarClock, Check, ChevronDown, Clock3, DatabaseZap, Search } from 'lucide-react'; import { CalendarDays, CalendarClock, Check, ChevronDown, Clock3, DatabaseZap, Search } from 'lucide-react';
import type { CurrentItem } from '../../../entities/current/types'; import type { CurrentItem } from '../../../entities/current/types';
import { HistoryChart } from '../../../features/history-chart/HistoryChart'; import { HistoryChart } from '../../../features/history-chart/HistoryChart';
import type { HistoryZoomRange } from '../../../features/history-chart/chartTypes';
import { RANGE_PRESETS, createRange, type DateRangeState } from '../../../features/history/dateRange'; import { RANGE_PRESETS, createRange, type DateRangeState } from '../../../features/history/dateRange';
import { toIsoFromInput } from '../../../utils/format'; import { toInputDateTimeValue, toIsoFromInput } from '../../../utils/format';
import type { HistoryGranularity } from '../../../utils/historyGranularity'; import type { HistoryGranularity } from '../../../utils/historyGranularity';
type ArchiveViewProps = { type ArchiveViewProps = {
@@ -108,6 +109,12 @@ export function ArchiveView({
[rangePart]: updateRangeInputPart(range[rangePart], inputPart, value), [rangePart]: updateRangeInputPart(range[rangePart], inputPart, value),
}); });
}; };
const updateRangeFromZoom = (nextRange: HistoryZoomRange) => {
onRangeChange({
from: toInputDateTimeValue(new Date(nextRange.from)),
to: toInputDateTimeValue(new Date(nextRange.to)),
});
};
return ( return (
<section className="chart-section chart-section--archive"> <section className="chart-section chart-section--archive">
@@ -263,6 +270,7 @@ export function ArchiveView({
labelFormat={historyAxis.labelFormat} labelFormat={historyAxis.labelFormat}
tags={selectedTags} tags={selectedTags}
tagLabels={tagLabels} tagLabels={tagLabels}
onZoomRangeChange={updateRangeFromZoom}
/> />
) : ( ) : (
<div className="chart-placeholder">Разверните список показателей и выберите серию для графика</div> <div className="chart-placeholder">Разверните список показателей и выберите серию для графика</div>

View File

@@ -15,6 +15,7 @@ type HistoryChartProps = {
tickIntervalMs?: number; tickIntervalMs?: number;
to: string; to: string;
tagLabels?: Record<string, string>; tagLabels?: Record<string, string>;
onZoomRangeChange?: (range: HistoryZoomRange) => void;
}; };
const ZOOM_REQUEST_DELAY_MS = 350; const ZOOM_REQUEST_DELAY_MS = 350;
@@ -95,6 +96,7 @@ export function HistoryChart({
tickIntervalMs, tickIntervalMs,
to, to,
tagLabels = {}, tagLabels = {},
onZoomRangeChange,
}: HistoryChartProps) { }: HistoryChartProps) {
const [zoomRange, setZoomRange] = useState(() => const [zoomRange, setZoomRange] = useState(() =>
createInitialZoomRange({ from, granulate, labelFormat, tickIntervalMs, to }), createInitialZoomRange({ from, granulate, labelFormat, tickIntervalMs, to }),
@@ -167,8 +169,9 @@ export function HistoryChart({
zoomTimerRef.current = setTimeout(() => { zoomTimerRef.current = setTimeout(() => {
zoomRangeRef.current = nextRange; zoomRangeRef.current = nextRange;
setZoomRange(nextRange); setZoomRange(nextRange);
onZoomRangeChange?.(nextRange);
}, ZOOM_REQUEST_DELAY_MS); }, ZOOM_REQUEST_DELAY_MS);
}, []); }, [onZoomRangeChange]);
return ( return (
<HistoryChartArea <HistoryChartArea

View File

@@ -60,7 +60,7 @@ function formatChartValue(value: number): string {
}).format(value); }).format(value);
} }
/** Показывает одно значение, если min/avg/max визуально совпадают. */ /** Форматирует значение tooltip: одно число для min=avg=max или среднее с диапазоном. */
function formatTooltipValue(value: AvgPointValue): string { function formatTooltipValue(value: AvgPointValue): string {
const avg = formatChartValue(value[1]); const avg = formatChartValue(value[1]);
const min = formatChartValue(value[2]); const min = formatChartValue(value[2]);
@@ -70,7 +70,7 @@ function formatTooltipValue(value: AvgPointValue): string {
return avg; return avg;
} }
return `min ${min} · avg ${avg} · max ${max}`; return `сред. ${avg} (${min} .. ${max})`;
} }
function isAvgPointValue(value: unknown): value is AvgPointValue { function isAvgPointValue(value: unknown): value is AvgPointValue {