diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index f48a7d475..fc2672b68 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -109,12 +109,41 @@ export default function LivePlayer({ offline, } = useCameraActivity(cameraConfig); - const cameraActive = useMemo( - () => - !showStillWithoutActivity || - (windowVisible && (activeMotion || activeTracking)), - [activeMotion, activeTracking, showStillWithoutActivity, windowVisible], - ); + const [stickyActive, setStickyActive] = useState(false); + + useEffect(() => { + if (showStillWithoutActivity) { + setStickyActive(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (activeMotion || activeTracking) { + setStickyActive(true); + } + }, [activeMotion, activeTracking]); + + const cameraActive = useMemo(() => { + // continuous: всегда активна + if (!showStillWithoutActivity) { + return true; + } + + // smart, но уже "залипла" → ведём себя как continuous + if (stickyActive) { + return true; + } + + // обычный smart до первого движения + return windowVisible && (activeMotion || activeTracking); + }, [ + activeMotion, + activeTracking, + showStillWithoutActivity, + windowVisible, + stickyActive, + ]); // camera live state diff --git a/web/src/components/player/MsePlayer.tsx b/web/src/components/player/MsePlayer.tsx index 1a2b1b6cb..7c9619e46 100644 --- a/web/src/components/player/MsePlayer.tsx +++ b/web/src/components/player/MsePlayer.tsx @@ -465,11 +465,11 @@ function MSEPlayer({ bufLen = 0; sb.appendBuffer(data); } else if (sb.buffered && sb.buffered.length) { - const end = sb.buffered.end(sb.buffered.length - 1) - 15; + const end = sb.buffered.end(sb.buffered.length - 1) - 10; const start = sb.buffered.start(0); if (end > start) { sb.remove(start, end); - msRef.current?.setLiveSeekableRange(end, end + 15); + msRef.current?.setLiveSeekableRange(end, end + 10); } } } catch (e) { @@ -511,7 +511,7 @@ function MSEPlayer({ if (buffered.length > 0) { const liveEdge = buffered.end(buffered.length - 1); // Jump to the live edge - videoRef.current.currentTime = liveEdge - 0.75; + videoRef.current.currentTime = liveEdge - 0.45; lastJumpTimeRef.current = Date.now(); } }; @@ -520,21 +520,21 @@ function MSEPlayer({ const filledEntries = bufferTimes.current.length; const sum = bufferTimes.current.reduce((a, b) => a + b, 0); const averageBufferTime = filledEntries ? sum / filledEntries : 0; - return averageBufferTime * (isSafari || isIOS ? 3 : 1.5); + return averageBufferTime * (isSafari || isIOS ? 3 : 0.6); }; const calculateAdaptivePlaybackRate = ( bufferTime: number, bufferThreshold: number, ) => { - const alpha = 0.2; // aggressiveness of playback rate increase - const beta = 0.5; // steepness of exponential growth + const alpha = 0.1; // aggressiveness of playback rate increase + const beta = 0.3; // steepness of exponential growth // don't adjust playback rate if we're close enough to live // or if we just started streaming if ( - ((bufferTime <= bufferThreshold && bufferThreshold < 3) || - bufferTime < 3) && + ((bufferTime <= bufferThreshold && bufferThreshold < 1) || + bufferTime < 1) && bufferTimes.current.length <= MAX_BUFFER_ENTRIES ) { return 1; @@ -548,7 +548,7 @@ function MSEPlayer({ if ( videoRef.current && - (videoRef.current.playbackRate === 1 || bufferTime < 3) + (videoRef.current.playbackRate === 1 || bufferTime < 1) ) { if (bufferTimes.current.length < MAX_BUFFER_ENTRIES) { bufferTimes.current.push(bufferTime); @@ -563,7 +563,7 @@ function MSEPlayer({ // if we have > 3 seconds of buffered data and we're still not playing, // something might be wrong - maybe codec issue, no audio, etc // so mark the player as playing so that error handlers will fire - if (!isPlaying && playbackEnabled && bufferTime > 3) { + if (!isPlaying && playbackEnabled && bufferTime > 1) { setIsPlaying(true); lastJumpTimeRef.current = Date.now(); onPlaying?.(); @@ -592,8 +592,7 @@ function MSEPlayer({ // time if (videoRef.current && isPlaying && playbackEnabled) { if ( - (isSafari || isIOS) && - bufferTime > 3 && + bufferTime > 0.4 && Date.now() - lastJumpTimeRef.current > BUFFERING_COOLDOWN_TIMEOUT ) { // Jump to live on Safari/iOS due to a change of playback rate causing re-buffering diff --git a/web/src/views/live/DraggableGridLayout.tsx b/web/src/views/live/DraggableGridLayout.tsx index 1b680967f..91fba4483 100644 --- a/web/src/views/live/DraggableGridLayout.tsx +++ b/web/src/views/live/DraggableGridLayout.tsx @@ -35,7 +35,7 @@ import useSWR from "swr"; import { isDesktop, isMobile } from "react-device-detect"; import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer"; import LivePlayer from "@/components/player/LivePlayer"; -import { IoClose } from "react-icons/io5"; +import { IoClose, IoStatsChart } from "react-icons/io5"; import { LuLayoutDashboard, LuPencil } from "react-icons/lu"; import { cn } from "@/lib/utils"; import { EditGroupDialog } from "@/components/filter/CameraGroupSelector"; @@ -404,16 +404,44 @@ export default function DraggableGridLayout({ }; // audio and stats states + const [globalStreamStatsEnabled, setGlobalStreamStatsEnabled] = + useState(false); + + const getStreamStatsFromStorage = (): boolean => { + const storedValue = localStorage.getItem("globalStreamStatsEnabled"); + return storedValue === "true"; + }; + + const setStreamStatsToStorage = (value: boolean): void => { + localStorage.setItem("globalStreamStatsEnabled", value.toString()); + }; + + const toggleGlobalStreamStats = () => { + setGlobalStreamStatsEnabled((prevState) => { + const newState = !prevState; + setStreamStatsToStorage(newState); + return newState; + }); + }; const [audioStates, setAudioStates] = useState({}); const [volumeStates, setVolumeStates] = useState({}); - const [statsStates, setStatsStates] = useState(() => { - const initialStates: StatsState = {}; + const [statsStates, setStatsStates] = useState({}); + + useEffect(() => { + const initialStreamStatsState = getStreamStatsFromStorage(); + setGlobalStreamStatsEnabled(initialStreamStatsState); + }, []); + + useEffect(() => { + const updatedStatsState: StatsState = {}; + cameras.forEach((camera) => { - initialStates[camera.name] = false; + updatedStatsState[camera.name] = globalStreamStatsEnabled; }); - return initialStates; - }); + + setStatsStates(updatedStatsState); + }, [globalStreamStatsEnabled, cameras]); const toggleStats = (cameraName: string): void => { setStatsStates((prev) => ({ @@ -628,7 +656,7 @@ export default function DraggableGridLayout({ } audioState={audioStates[camera.name]} toggleAudio={() => toggleAudio(camera.name)} - statsState={statsStates[camera.name]} + statsState={statsStates[camera.name] ?? true} toggleStats={() => toggleStats(camera.name)} volumeState={volumeStates[camera.name]} setVolumeState={(value) => @@ -665,7 +693,7 @@ export default function DraggableGridLayout({ cameraConfig={camera} preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"} playInBackground={false} - showStats={statsStates[camera.name]} + showStats={statsStates[camera.name] ?? true} onClick={() => { !isEditMode && onSelectCamera(camera.name); }} @@ -673,9 +701,7 @@ export default function DraggableGridLayout({ setPreferredLiveModes((prevModes) => { const newModes = { ...prevModes }; if (e === "mse-decode") { - newModes[camera.name] = "webrtc"; - } else { - newModes[camera.name] = "jsmpeg"; + delete newModes[camera.name]; } return newModes; }); @@ -699,6 +725,21 @@ export default function DraggableGridLayout({ "z-50 flex flex-row gap-2", )} > + + +
+ +
+
+ + {globalStreamStatsEnabled + ? t("streamStats.disable") + : t("streamStats.enable")} + +
( `${camera.name}-stream`, - Object.values(camera.live.streams)[0], + Object.values(camera.live.streams)[1], ); const isRestreamed = useMemo( @@ -273,7 +273,7 @@ export default function LiveCameraView({ false, ); - const [showStats, setShowStats] = useState(false); + const [showStats, setShowStats] = useState(true); const [debug, setDebug] = useState(false); useSearchEffect("debug", (value: string) => { diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index a25741f63..7e808f33f 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -566,7 +566,7 @@ export default function LiveDashboardView({ } audioState={audioStates[camera.name]} toggleAudio={() => toggleAudio(camera.name)} - statsState={statsStates[camera.name]} + statsState={statsStates[camera.name] ?? true} toggleStats={() => toggleStats(camera.name)} volumeState={volumeStates[camera.name] ?? 1} setVolumeState={(value) => @@ -599,7 +599,7 @@ export default function LiveDashboardView({ alwaysShowCameraName={displayCameraNames} useWebGL={useWebGL} playInBackground={false} - showStats={statsStates[camera.name]} + showStats={statsStates[camera.name] ?? true} streamName={streamName} onClick={() => onSelectCamera(camera.name)} onError={(e) => handleError(camera.name, e)}