mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-21 23:58:22 +03:00
Merge pull request #13 from ibs0d/codex/apply-code-changes-from-diff
Live view & MSE playback tuning; add global stream-stats toggle
This commit is contained in:
commit
6f97f3e873
@ -109,12 +109,41 @@ export default function LivePlayer({
|
|||||||
offline,
|
offline,
|
||||||
} = useCameraActivity(cameraConfig);
|
} = useCameraActivity(cameraConfig);
|
||||||
|
|
||||||
const cameraActive = useMemo(
|
const [stickyActive, setStickyActive] = useState(false);
|
||||||
() =>
|
|
||||||
!showStillWithoutActivity ||
|
useEffect(() => {
|
||||||
(windowVisible && (activeMotion || activeTracking)),
|
if (showStillWithoutActivity) {
|
||||||
[activeMotion, activeTracking, showStillWithoutActivity, windowVisible],
|
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
|
// camera live state
|
||||||
|
|
||||||
|
|||||||
@ -465,11 +465,11 @@ function MSEPlayer({
|
|||||||
bufLen = 0;
|
bufLen = 0;
|
||||||
sb.appendBuffer(data);
|
sb.appendBuffer(data);
|
||||||
} else if (sb.buffered && sb.buffered.length) {
|
} 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);
|
const start = sb.buffered.start(0);
|
||||||
if (end > start) {
|
if (end > start) {
|
||||||
sb.remove(start, end);
|
sb.remove(start, end);
|
||||||
msRef.current?.setLiveSeekableRange(end, end + 15);
|
msRef.current?.setLiveSeekableRange(end, end + 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -511,7 +511,7 @@ function MSEPlayer({
|
|||||||
if (buffered.length > 0) {
|
if (buffered.length > 0) {
|
||||||
const liveEdge = buffered.end(buffered.length - 1);
|
const liveEdge = buffered.end(buffered.length - 1);
|
||||||
// Jump to the live edge
|
// Jump to the live edge
|
||||||
videoRef.current.currentTime = liveEdge - 0.75;
|
videoRef.current.currentTime = liveEdge - 0.45;
|
||||||
lastJumpTimeRef.current = Date.now();
|
lastJumpTimeRef.current = Date.now();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -520,21 +520,21 @@ function MSEPlayer({
|
|||||||
const filledEntries = bufferTimes.current.length;
|
const filledEntries = bufferTimes.current.length;
|
||||||
const sum = bufferTimes.current.reduce((a, b) => a + b, 0);
|
const sum = bufferTimes.current.reduce((a, b) => a + b, 0);
|
||||||
const averageBufferTime = filledEntries ? sum / filledEntries : 0;
|
const averageBufferTime = filledEntries ? sum / filledEntries : 0;
|
||||||
return averageBufferTime * (isSafari || isIOS ? 3 : 1.5);
|
return averageBufferTime * (isSafari || isIOS ? 3 : 0.6);
|
||||||
};
|
};
|
||||||
|
|
||||||
const calculateAdaptivePlaybackRate = (
|
const calculateAdaptivePlaybackRate = (
|
||||||
bufferTime: number,
|
bufferTime: number,
|
||||||
bufferThreshold: number,
|
bufferThreshold: number,
|
||||||
) => {
|
) => {
|
||||||
const alpha = 0.2; // aggressiveness of playback rate increase
|
const alpha = 0.1; // aggressiveness of playback rate increase
|
||||||
const beta = 0.5; // steepness of exponential growth
|
const beta = 0.3; // steepness of exponential growth
|
||||||
|
|
||||||
// don't adjust playback rate if we're close enough to live
|
// don't adjust playback rate if we're close enough to live
|
||||||
// or if we just started streaming
|
// or if we just started streaming
|
||||||
if (
|
if (
|
||||||
((bufferTime <= bufferThreshold && bufferThreshold < 3) ||
|
((bufferTime <= bufferThreshold && bufferThreshold < 1) ||
|
||||||
bufferTime < 3) &&
|
bufferTime < 1) &&
|
||||||
bufferTimes.current.length <= MAX_BUFFER_ENTRIES
|
bufferTimes.current.length <= MAX_BUFFER_ENTRIES
|
||||||
) {
|
) {
|
||||||
return 1;
|
return 1;
|
||||||
@ -548,7 +548,7 @@ function MSEPlayer({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
videoRef.current &&
|
videoRef.current &&
|
||||||
(videoRef.current.playbackRate === 1 || bufferTime < 3)
|
(videoRef.current.playbackRate === 1 || bufferTime < 1)
|
||||||
) {
|
) {
|
||||||
if (bufferTimes.current.length < MAX_BUFFER_ENTRIES) {
|
if (bufferTimes.current.length < MAX_BUFFER_ENTRIES) {
|
||||||
bufferTimes.current.push(bufferTime);
|
bufferTimes.current.push(bufferTime);
|
||||||
@ -563,7 +563,7 @@ function MSEPlayer({
|
|||||||
// if we have > 3 seconds of buffered data and we're still not playing,
|
// if we have > 3 seconds of buffered data and we're still not playing,
|
||||||
// something might be wrong - maybe codec issue, no audio, etc
|
// something might be wrong - maybe codec issue, no audio, etc
|
||||||
// so mark the player as playing so that error handlers will fire
|
// so mark the player as playing so that error handlers will fire
|
||||||
if (!isPlaying && playbackEnabled && bufferTime > 3) {
|
if (!isPlaying && playbackEnabled && bufferTime > 1) {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
lastJumpTimeRef.current = Date.now();
|
lastJumpTimeRef.current = Date.now();
|
||||||
onPlaying?.();
|
onPlaying?.();
|
||||||
@ -592,8 +592,7 @@ function MSEPlayer({
|
|||||||
// time
|
// time
|
||||||
if (videoRef.current && isPlaying && playbackEnabled) {
|
if (videoRef.current && isPlaying && playbackEnabled) {
|
||||||
if (
|
if (
|
||||||
(isSafari || isIOS) &&
|
bufferTime > 0.4 &&
|
||||||
bufferTime > 3 &&
|
|
||||||
Date.now() - lastJumpTimeRef.current > BUFFERING_COOLDOWN_TIMEOUT
|
Date.now() - lastJumpTimeRef.current > BUFFERING_COOLDOWN_TIMEOUT
|
||||||
) {
|
) {
|
||||||
// Jump to live on Safari/iOS due to a change of playback rate causing re-buffering
|
// Jump to live on Safari/iOS due to a change of playback rate causing re-buffering
|
||||||
|
|||||||
@ -35,7 +35,7 @@ import useSWR from "swr";
|
|||||||
import { isDesktop, isMobile } from "react-device-detect";
|
import { isDesktop, isMobile } from "react-device-detect";
|
||||||
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
|
||||||
import LivePlayer from "@/components/player/LivePlayer";
|
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 { LuLayoutDashboard, LuPencil } from "react-icons/lu";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EditGroupDialog } from "@/components/filter/CameraGroupSelector";
|
import { EditGroupDialog } from "@/components/filter/CameraGroupSelector";
|
||||||
@ -404,16 +404,44 @@ export default function DraggableGridLayout({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// audio and stats states
|
// 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<AudioState>({});
|
const [audioStates, setAudioStates] = useState<AudioState>({});
|
||||||
const [volumeStates, setVolumeStates] = useState<VolumeState>({});
|
const [volumeStates, setVolumeStates] = useState<VolumeState>({});
|
||||||
const [statsStates, setStatsStates] = useState<StatsState>(() => {
|
const [statsStates, setStatsStates] = useState<StatsState>({});
|
||||||
const initialStates: StatsState = {};
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initialStreamStatsState = getStreamStatsFromStorage();
|
||||||
|
setGlobalStreamStatsEnabled(initialStreamStatsState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updatedStatsState: StatsState = {};
|
||||||
|
|
||||||
cameras.forEach((camera) => {
|
cameras.forEach((camera) => {
|
||||||
initialStates[camera.name] = false;
|
updatedStatsState[camera.name] = globalStreamStatsEnabled;
|
||||||
});
|
});
|
||||||
return initialStates;
|
|
||||||
});
|
setStatsStates(updatedStatsState);
|
||||||
|
}, [globalStreamStatsEnabled, cameras]);
|
||||||
|
|
||||||
const toggleStats = (cameraName: string): void => {
|
const toggleStats = (cameraName: string): void => {
|
||||||
setStatsStates((prev) => ({
|
setStatsStates((prev) => ({
|
||||||
@ -628,7 +656,7 @@ export default function DraggableGridLayout({
|
|||||||
}
|
}
|
||||||
audioState={audioStates[camera.name]}
|
audioState={audioStates[camera.name]}
|
||||||
toggleAudio={() => toggleAudio(camera.name)}
|
toggleAudio={() => toggleAudio(camera.name)}
|
||||||
statsState={statsStates[camera.name]}
|
statsState={statsStates[camera.name] ?? true}
|
||||||
toggleStats={() => toggleStats(camera.name)}
|
toggleStats={() => toggleStats(camera.name)}
|
||||||
volumeState={volumeStates[camera.name]}
|
volumeState={volumeStates[camera.name]}
|
||||||
setVolumeState={(value) =>
|
setVolumeState={(value) =>
|
||||||
@ -665,7 +693,7 @@ export default function DraggableGridLayout({
|
|||||||
cameraConfig={camera}
|
cameraConfig={camera}
|
||||||
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
preferredLiveMode={preferredLiveModes[camera.name] ?? "mse"}
|
||||||
playInBackground={false}
|
playInBackground={false}
|
||||||
showStats={statsStates[camera.name]}
|
showStats={statsStates[camera.name] ?? true}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
!isEditMode && onSelectCamera(camera.name);
|
!isEditMode && onSelectCamera(camera.name);
|
||||||
}}
|
}}
|
||||||
@ -673,9 +701,7 @@ export default function DraggableGridLayout({
|
|||||||
setPreferredLiveModes((prevModes) => {
|
setPreferredLiveModes((prevModes) => {
|
||||||
const newModes = { ...prevModes };
|
const newModes = { ...prevModes };
|
||||||
if (e === "mse-decode") {
|
if (e === "mse-decode") {
|
||||||
newModes[camera.name] = "webrtc";
|
delete newModes[camera.name];
|
||||||
} else {
|
|
||||||
newModes[camera.name] = "jsmpeg";
|
|
||||||
}
|
}
|
||||||
return newModes;
|
return newModes;
|
||||||
});
|
});
|
||||||
@ -699,6 +725,21 @@ export default function DraggableGridLayout({
|
|||||||
"z-50 flex flex-row gap-2",
|
"z-50 flex flex-row gap-2",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer rounded-lg bg-secondary text-secondary-foreground opacity-60 transition-all duration-300 hover:bg-muted hover:opacity-100"
|
||||||
|
onClick={toggleGlobalStreamStats}
|
||||||
|
>
|
||||||
|
<IoStatsChart className="size-5 md:m-[6px]" />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{globalStreamStatsEnabled
|
||||||
|
? t("streamStats.disable")
|
||||||
|
: t("streamStats.enable")}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -150,7 +150,7 @@ export default function LiveCameraView({
|
|||||||
const [streamName, setStreamName, streamNameLoaded] =
|
const [streamName, setStreamName, streamNameLoaded] =
|
||||||
useUserPersistence<string>(
|
useUserPersistence<string>(
|
||||||
`${camera.name}-stream`,
|
`${camera.name}-stream`,
|
||||||
Object.values(camera.live.streams)[0],
|
Object.values(camera.live.streams)[1],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isRestreamed = useMemo(
|
const isRestreamed = useMemo(
|
||||||
@ -273,7 +273,7 @@ export default function LiveCameraView({
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [showStats, setShowStats] = useState(false);
|
const [showStats, setShowStats] = useState(true);
|
||||||
const [debug, setDebug] = useState(false);
|
const [debug, setDebug] = useState(false);
|
||||||
|
|
||||||
useSearchEffect("debug", (value: string) => {
|
useSearchEffect("debug", (value: string) => {
|
||||||
|
|||||||
@ -566,7 +566,7 @@ export default function LiveDashboardView({
|
|||||||
}
|
}
|
||||||
audioState={audioStates[camera.name]}
|
audioState={audioStates[camera.name]}
|
||||||
toggleAudio={() => toggleAudio(camera.name)}
|
toggleAudio={() => toggleAudio(camera.name)}
|
||||||
statsState={statsStates[camera.name]}
|
statsState={statsStates[camera.name] ?? true}
|
||||||
toggleStats={() => toggleStats(camera.name)}
|
toggleStats={() => toggleStats(camera.name)}
|
||||||
volumeState={volumeStates[camera.name] ?? 1}
|
volumeState={volumeStates[camera.name] ?? 1}
|
||||||
setVolumeState={(value) =>
|
setVolumeState={(value) =>
|
||||||
@ -599,7 +599,7 @@ export default function LiveDashboardView({
|
|||||||
alwaysShowCameraName={displayCameraNames}
|
alwaysShowCameraName={displayCameraNames}
|
||||||
useWebGL={useWebGL}
|
useWebGL={useWebGL}
|
||||||
playInBackground={false}
|
playInBackground={false}
|
||||||
showStats={statsStates[camera.name]}
|
showStats={statsStates[camera.name] ?? true}
|
||||||
streamName={streamName}
|
streamName={streamName}
|
||||||
onClick={() => onSelectCamera(camera.name)}
|
onClick={() => onSelectCamera(camera.name)}
|
||||||
onError={(e) => handleError(camera.name, e)}
|
onError={(e) => handleError(camera.name, e)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user