Apply requested live view/player tuning changes

This commit is contained in:
ibs0d 2026-03-08 13:57:38 +11:00
parent adb91dfa9e
commit 9f05a677e2
5 changed files with 102 additions and 33 deletions

View File

@ -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

View File

@ -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

View File

@ -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<AudioState>({});
const [volumeStates, setVolumeStates] = useState<VolumeState>({});
const [statsStates, setStatsStates] = useState<StatsState>(() => {
const initialStates: StatsState = {};
const [statsStates, setStatsStates] = useState<StatsState>({});
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",
)}
>
<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>
<TooltipTrigger asChild>
<div

View File

@ -150,7 +150,7 @@ export default function LiveCameraView({
const [streamName, setStreamName, streamNameLoaded] =
useUserPersistence<string>(
`${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) => {

View File

@ -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)}