3 Commits

4 changed files with 41 additions and 7 deletions

View File

@@ -6,6 +6,21 @@ type CameraViewProps = {
wsUrl: string;
};
const CAMERA_PLAYER_CONFIG = {
enableWorker: true,
enableStashBuffer: true,
stashInitialSize: 512,
autoCleanupSourceBuffer: true,
autoCleanupMaxBackwardDuration: 10,
autoCleanupMinBackwardDuration: 5,
liveBufferLatencyChasing: true,
liveBufferLatencyMaxLatency: 5.0,
liveBufferLatencyMinRemain: 2.0,
liveSync: false,
lazyLoad: false,
deferLoadAfterSourceOpen: false,
};
export function CameraView({ title = 'Камера', wsUrl }: CameraViewProps) {
const videoRef = useRef<HTMLVideoElement | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -13,6 +28,7 @@ export function CameraView({ title = 'Камера', wsUrl }: CameraViewProps) {
useEffect(() => {
const video = videoRef.current;
let player: ReturnType<typeof mpegts.createPlayer> | null = null;
const handlePlayerError = () => setError('Ошибка видеопотока');
if (!video) {
return;
@@ -30,20 +46,22 @@ export function CameraView({ title = 'Камера', wsUrl }: CameraViewProps) {
}
player = mpegts.createPlayer(
{ type: 'mpegts', isLive: true, url: wsUrl },
{ liveBufferLatencyChasing: true, liveBufferLatencyMaxLatency: 1.5 },
{ type: 'mpegts', isLive: true, url: wsUrl, hasAudio: false },
CAMERA_PLAYER_CONFIG,
);
player.on(mpegts.Events.ERROR, handlePlayerError);
player.attachMediaElement(video);
player.load();
const playResult = player.play();
if (playResult instanceof Promise) {
void playResult.then(() => setError(null)).catch(() => setError('Не удалось запустить воспроизведение'));
void playResult.then(() => setError(null)).catch(() => undefined);
}
return () => {
video.removeEventListener('playing', clearError);
player?.off(mpegts.Events.ERROR, handlePlayerError);
player?.pause();
player?.unload();
player?.detachMediaElement();

View File

@@ -1,4 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { EChartsOption } from 'echarts';
import { getHistoryGranularity, type HistoryAxisLabelFormat } from '../../utils/historyGranularity';
import type { DataZoomEventBatch, DataZoomState, HistoryZoomRange } from './chartTypes';
import { HistoryChartArea } from './HistoryChartArea';
@@ -139,6 +140,7 @@ export function HistoryChart({
const baseRangeRef = useRef(baseRange);
const zoomRangeRef = useRef(zoomRange);
const zoomTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const lastReadyOptionRef = useRef<EChartsOption | null>(null);
const lines = useHistoryChartQueries({
edge,
from: zoomRange.from,
@@ -150,6 +152,7 @@ export function HistoryChart({
const series = useHistoryChartSeries(lines, zoomRange.granulate);
const legendData = useMemo(() => lines.map((line) => line.label), [lines]);
const loading = lines.some((line) => line.loading);
const hasData = series.length > 0;
useEffect(() => {
baseRangeRef.current = baseRange;
@@ -184,6 +187,18 @@ export function HistoryChart({
[baseRange, legendData, series, zoomRange],
);
// Во время lazy-загрузки нового zoom-диапазона оставляем на экране последний готовый график.
// Так canvas не заменяется loader-ом и пользователь не теряет визуальный контекст.
const fallbackOption = loading && !hasData ? lastReadyOptionRef.current : null;
const displayOption = fallbackOption ?? option;
const displayHasData = hasData || Boolean(fallbackOption);
useEffect(() => {
if (hasData) {
lastReadyOptionRef.current = option;
}
}, [hasData, option]);
const handleDataZoom = useCallback((event: DataZoomEventBatch) => {
const state = (event.batch ?? [event]).find(isXAxisDataZoom);
@@ -211,10 +226,10 @@ export function HistoryChart({
return (
<HistoryChartArea
hasData={series.length > 0}
hasData={displayHasData}
hasSelection={tags.length > 0}
loading={loading}
option={option}
option={displayOption}
onDataZoom={handleDataZoom}
/>
);

View File

@@ -31,7 +31,7 @@ export function HistoryChartArea({
return <div className="chart-placeholder">Разверните список показателей и выберите серию для графика</div>;
}
if (loading) {
if (!hasData && loading) {
return <div className="chart-placeholder">Загрузка графика...</div>;
}

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import { useQueries } from '@tanstack/react-query';
import { keepPreviousData, useQueries } from '@tanstack/react-query';
import { getHistory } from '../../entities/history/api';
import type { HistoryPoint } from '../../entities/history/types';
@@ -35,6 +35,7 @@ export function useHistoryChartQueries({
queryKey: ['history', edge, tag, from, to, granulate],
queryFn: ({ signal }: { signal: AbortSignal }) => getHistory({ edge, tag, from, to, granulate }, signal),
enabled,
placeholderData: keepPreviousData,
})),
});