bandwidth savings configs

This commit is contained in:
legop3
2026-07-14 17:04:08 -04:00
parent 1c401ff90a
commit 0d6b4d68de
21 changed files with 424 additions and 39 deletions
+12 -2
View File
@@ -540,13 +540,23 @@ function buildPtzTurnModel(ptz, selfId) {
function PtzMediaPane({ ptz, open, framed = true }) {
const isOperator = Boolean(ptz?.isOperator);
const nonTurnVideoPolicy = useSessionSelector(
(state) => state.session?.bandwidthSavings?.nonTurnVideo || 'snapshots',
);
const selfId = useSessionSelector((state) => state.session?.socketId || null);
const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: open && !isOperator });
/*
PTZ has its own turn queue, so non-operators are the camera equivalent of a
non-active rover driver. The server enforces the same policy in
canRequestLiveVideo(); this branch only chooses the expected browser render
path and never unlocks movement controls.
*/
const shouldUseLiveVideo = isOperator || nonTurnVideoPolicy === 'live';
const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: open && !shouldUseLiveVideo });
const snapshot = snapshotFeeds[PTZ_CAMERA_ID] || null;
const turnModel = useMemo(() => buildPtzTurnModel(ptz, selfId), [ptz, selfId]);
const media = (
<>
{isOperator ? (
{shouldUseLiveVideo ? (
<PtzLiveVideo enabled={open} startMuted={false} />
) : (
<PtzSnapshotPreview feed={snapshot} label={ptz?.name || 'PTZ Camera'} />
@@ -4,6 +4,7 @@ import RoverDescriptionOverlay from '../HudOverlays/RoverDescriptionOverlay/inde
import OvercurrentOverlay from '../HudOverlays/OvercurrentOverlay/index.jsx';
import LowBatteryOverlay from '../HudOverlays/LowBatteryOverlay/index.jsx';
import VerticalBatteryOverlay from '../HudOverlays/VerticalBatteryOverlay/index.jsx';
import { useSessionSelector } from '../../context/SessionContext.jsx';
export default function SpectateVideo({
roverId = null,
@@ -11,12 +12,24 @@ export default function SpectateVideo({
fitParent = false,
layoutFormat = 'desktop',
}) {
const isExternalSpectatorSnapshotOnly = useSessionSelector((state) =>
state.session?.role === 'spectator' &&
state.session?.isLocalNetwork === false &&
state.session?.bandwidthSavings?.externalSpectatorVideo === 'snapshots',
);
/*
Server auth already denies external spectator WHEP when this policy is set,
but explicitly selecting snapshot mode avoids expected authorization denials
and keeps analytics focused on actual stream failures.
*/
const videoMode = isExternalSpectatorSnapshotOnly ? 'snapshot' : null;
return (
<div className={`flex flex-col gap-0.5 ${fitParent ? 'h-full' : ''}`}>
<div className={`relative w-full overflow-hidden bg-black ${fitParent ? 'h-full flex-1' : 'aspect-[4/3]'}`}>
<RoverMediaPlayer
roverId={roverId}
label={label}
videoMode={videoMode}
/>
<RoverDescriptionOverlay
roverId={roverId}
+11 -2
View File
@@ -326,9 +326,18 @@ function PtzControlReference() {
function PtzController({ open, onClose, layout = 'desktop' }) {
const ptz = useSessionSelector((state) => state.session?.ptzCamera || null);
const isOperator = Boolean(ptz?.isOperator);
const nonTurnVideoPolicy = useSessionSelector(
(state) => state.session?.bandwidthSavings?.nonTurnVideo || 'snapshots',
);
const isMobile = layout === 'mobile-portrait' || layout === 'mobile-landscape';
const { ptzRelease } = useSessionActions();
const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: open && !isOperator });
/*
The VIP fullscreen surface is available to queued PTZ users too. Let queued
users see live video only when the central non-turn video policy allows it;
all movement and light controls still remain guarded by isOperator.
*/
const shouldUseLiveVideo = isOperator || nonTurnVideoPolicy === 'live';
const snapshotFeeds = usePtzCameraSnapshots([PTZ_CAMERA_ID], { enabled: open && !shouldUseLiveVideo });
const snapshot = snapshotFeeds[PTZ_CAMERA_ID] || null;
const [releasePending, setReleasePending] = useState(false);
@@ -431,7 +440,7 @@ function PtzController({ open, onClose, layout = 'desktop' }) {
bodyClassName={`grid h-full min-h-0 overflow-hidden ${sidebarWidthClass}`}
>
<main className="relative min-h-0 min-w-0 bg-black">
{isOperator ? (
{shouldUseLiveVideo ? (
<PtzLiveVideo enabled startMuted={false} />
) : (
<PtzSnapshotPreview feed={snapshot} label={ptz?.name || 'PTZ Camera'} />
+10 -1
View File
@@ -9,6 +9,9 @@ export function useDriverVideoModePolicy(roverId) {
const turnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
const socketId = useSessionSelector((state) => state.session?.socketId || null);
const activeDrivers = useSessionSelector((state) => state.session?.activeDrivers ?? {});
const nonTurnVideoPolicy = useSessionSelector(
(state) => state.session?.bandwidthSavings?.nonTurnVideo || 'snapshots',
);
const isTurnsMode = mode === 'turns';
/*
This policy only switches preview/full video around a multi-second turn
@@ -43,7 +46,13 @@ export function useDriverVideoModePolicy(roverId) {
});
return unique.size;
}, [users]);
const shouldUsePreviewByLoad = isTurnsMode && totalDrivers > totalRovers;
/*
The server sends the bandwidth policy because the same rule is enforced in
video authorization. The hook only mirrors that policy so the UI avoids
requesting live video when snapshots are the intended non-turn experience.
*/
const shouldUsePreviewByLoad =
nonTurnVideoPolicy === 'snapshots' && isTurnsMode && totalDrivers > totalRovers;
const isPreSwitchWindow =
isTurnsMode && isNextDriver && msUntilTurn != null && msUntilTurn <= 5000 && msUntilTurn > 0;
const showNotTurnNotice = isTurnsMode && !isActiveDriver;
+11 -1
View File
@@ -6,6 +6,7 @@ import { useSession } from '../context/SessionContext.jsx';
export function useSpectatorMode() {
const { session, setRole, subscribeAll, connected } = useSession();
const [ready, setReady] = useState(false);
const canUseSpectatorAccess = session?.bandwidthSavings?.canUseExternalSpectatorAccess !== false;
useEffect(() => {
let cancelled = false;
@@ -14,6 +15,15 @@ export function useSpectatorMode() {
setReady(false);
return;
}
if (!canUseSpectatorAccess) {
/*
The server will reject the role change too, but stopping here prevents
a blocked external spectator from retrying on every session sync while
the page is intentionally waiting for an admin grant or config change.
*/
setReady(false);
return;
}
try {
if (session?.role !== 'spectator') {
await setRole('spectator');
@@ -33,7 +43,7 @@ export function useSpectatorMode() {
return () => {
cancelled = true;
};
}, [connected, session?.mode, session?.role, setRole, subscribeAll]);
}, [canUseSpectatorAccess, connected, session?.mode, session?.role, setRole, subscribeAll]);
return ready;
}
@@ -23,6 +23,8 @@ export default function SpectatorContent() {
const latestReplay = useSessionSelector((state) => state.latestReplay);
const { clearLatestReplay } = useSessionActions();
const inLockdown = session?.mode === 'lockdown';
const canUseSpectatorAccess = session?.bandwidthSavings?.canUseExternalSpectatorAccess !== false;
const spectatorAccessMode = session?.bandwidthSavings?.externalSpectatorAccess || 'on';
useDefaultNickname();
// The spectator route is not rendered through App.jsx, so it must opt into
// the same persisted identity heartbeat here. That keeps the existing
@@ -44,6 +46,26 @@ export default function SpectatorContent() {
);
}
if (session && !canUseSpectatorAccess) {
/*
The server keeps this socket in the normal user role when external
spectating is blocked. Rendering a full-page gate makes that intentional
state obvious instead of showing an empty spectator shell that repeatedly
fails to subscribe.
*/
const message = spectatorAccessMode === 'admin'
? 'This external spectator identity needs admin approval before it can view the spectator page.'
: 'External spectator access is disabled on this server.';
return (
<div className="flex min-h-screen items-center justify-center bg-black text-slate-200">
<div className="surface max-w-md space-y-0.5 p-1 text-center text-sm">
<p className="text-lg font-semibold text-white">Spectate access required.</p>
<p className="text-slate-300">{message}</p>
</div>
</div>
);
}
const mainClass = isPortraitLayout
? 'flex min-h-screen flex-col bg-black text-slate-100 md:h-screen md:overflow-hidden'
: 'grid min-h-screen grid-cols-1 gap-0.5 bg-black text-slate-100 md:h-full md:min-h-0 md:grid-cols-[minmax(0,1fr)_18rem] lg:grid-cols-[minmax(0,1fr)_20rem]';
@@ -81,6 +81,19 @@ function PtzSnapshotFallback({ label, source }) {
}
function PtzLiveOrSnapshot({ label }) {
const isExternalSpectatorSnapshotOnly = useSessionSelector((state) =>
state.session?.role === 'spectator' &&
state.session?.isLocalNetwork === false &&
state.session?.bandwidthSavings?.externalSpectatorVideo === 'snapshots',
);
/*
PTZ live authorization is server-owned, but the spectator page knows when
the configured outcome is snapshot-only. Rendering the fallback directly
avoids a guaranteed denied WHEP request for every external spectator card.
*/
if (isExternalSpectatorSnapshotOnly) {
return <PtzSnapshotFallback label={label} source={null} />;
}
return (
<PtzLiveVideo
enabled