mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
snapshot user threshold..
This commit is contained in:
@@ -60,10 +60,16 @@ bandwidthSavings:
|
||||
# verifiedOnly: verified/admin users may keep multiple driver tabs; unverified users may not
|
||||
# notAllowed: every identity is limited to one driver tab
|
||||
multiTabProtection: "verifiedOnly"
|
||||
# Live video for users who are attached to a source but do not currently own
|
||||
# its active turn. "snapshots" saves upload bandwidth; "live" allows full
|
||||
# video whenever the normal mode/visibility rules allow it.
|
||||
nonTurnVideo: "snapshots"
|
||||
# Video for users who are attached to a source but do not currently own its
|
||||
# active turn. "snapshots" saves upload bandwidth; "live" allows full video
|
||||
# whenever the normal mode/visibility rules allow it.
|
||||
nonTurnVideo:
|
||||
mode: "snapshots"
|
||||
# Snapshot mode activates only when controllable users exceed this number.
|
||||
# A controllable user is attached to a rover or PTZ as operator/queue, not a
|
||||
# plain spectator. 0 preserves always-on non-turn snapshots once anyone is
|
||||
# actually attached to a controllable source.
|
||||
userThreshold: 0
|
||||
# Live video for spectators outside the local network. Local spectators are
|
||||
# not restricted by this switch because LAN traffic is not the upload limit.
|
||||
externalSpectatorVideo: "snapshots"
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -78,7 +78,7 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-B8ElczOE.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-Dqo9kxw3.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-ZpgWUPKf.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -10,7 +10,10 @@ const EXTERNAL_SPECTATOR_ACCESS_MODES = new Set(['off', 'on', 'verifiedOnly', 'a
|
||||
|
||||
const DEFAULT_BANDWIDTH_SAVINGS = Object.freeze({
|
||||
multiTabProtection: 'verifiedOnly',
|
||||
nonTurnVideo: 'snapshots',
|
||||
nonTurnVideo: Object.freeze({
|
||||
mode: 'snapshots',
|
||||
userThreshold: 0,
|
||||
}),
|
||||
externalSpectatorVideo: 'snapshots',
|
||||
externalSpectatorAccess: 'on',
|
||||
});
|
||||
@@ -25,6 +28,23 @@ function normalizeEnum(value, allowed, fallback) {
|
||||
return allowed.has(normalized) ? normalized : fallback;
|
||||
}
|
||||
|
||||
function normalizeNonTurnVideo(value) {
|
||||
const raw = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
||||
const threshold = Number(raw.userThreshold);
|
||||
/*
|
||||
userThreshold is intentionally "greater than", not "greater than or equal".
|
||||
A value of 4 means the first four controllable users can keep live non-turn
|
||||
video, and the fifth controllable user activates snapshot saving. Invalid
|
||||
or negative values fall back to zero, which preserves always-on snapshots
|
||||
for any real non-turn participant.
|
||||
*/
|
||||
const userThreshold = Number.isFinite(threshold) ? Math.max(0, Math.floor(threshold)) : 0;
|
||||
return {
|
||||
mode: normalizeEnum(raw.mode, VIDEO_MODES, DEFAULT_BANDWIDTH_SAVINGS.nonTurnVideo.mode),
|
||||
userThreshold,
|
||||
};
|
||||
}
|
||||
|
||||
function buildBandwidthSavingsPolicy(config = loadConfig()) {
|
||||
const raw = config.bandwidthSavings || {};
|
||||
return {
|
||||
@@ -33,11 +53,7 @@ function buildBandwidthSavingsPolicy(config = loadConfig()) {
|
||||
MULTI_TAB_MODES,
|
||||
DEFAULT_BANDWIDTH_SAVINGS.multiTabProtection,
|
||||
),
|
||||
nonTurnVideo: normalizeEnum(
|
||||
raw.nonTurnVideo,
|
||||
VIDEO_MODES,
|
||||
DEFAULT_BANDWIDTH_SAVINGS.nonTurnVideo,
|
||||
),
|
||||
nonTurnVideo: normalizeNonTurnVideo(raw.nonTurnVideo),
|
||||
externalSpectatorVideo: normalizeEnum(
|
||||
raw.externalSpectatorVideo,
|
||||
VIDEO_MODES,
|
||||
@@ -72,8 +88,16 @@ function shouldEnforceSingleDriverTab({ isVerified = false, isAdmin = false } =
|
||||
return !isVerified && !isAdmin;
|
||||
}
|
||||
|
||||
function shouldUseSnapshotsForNonTurnVideo() {
|
||||
return getBandwidthSavingsPolicy().nonTurnVideo === 'snapshots';
|
||||
function shouldUseSnapshotsForNonTurnVideo({ controllableUserCount = 0 } = {}) {
|
||||
const { nonTurnVideo } = getBandwidthSavingsPolicy();
|
||||
if (nonTurnVideo.mode !== 'snapshots') return false;
|
||||
/*
|
||||
The threshold is evaluated centrally so MediaMTX auth, socket-issued video
|
||||
tokens, PTZ authorization, and browser session state all agree. Using a
|
||||
strict greater-than comparison makes the configured value read like the
|
||||
maximum number of controllable users allowed before snapshots start.
|
||||
*/
|
||||
return Math.max(0, Number(controllableUserCount) || 0) > nonTurnVideo.userThreshold;
|
||||
}
|
||||
|
||||
function shouldUseSnapshotsForExternalSpectatorVideo() {
|
||||
|
||||
@@ -339,6 +339,34 @@ function getChatTargetForSocket(socketId) {
|
||||
};
|
||||
}
|
||||
|
||||
function getParticipantSocketIds() {
|
||||
/*
|
||||
PTZ has no roverManager record, so services that need a global "how many
|
||||
controllable users are online" count need a tiny PTZ-owned participant list.
|
||||
The operator and queue are the only users attached to this controllable
|
||||
camera target; spectators merely viewing snapshots/live video are excluded.
|
||||
*/
|
||||
return Array.from(new Set([
|
||||
state.operatorSocketId,
|
||||
...state.queue,
|
||||
].filter(Boolean)));
|
||||
}
|
||||
|
||||
function countControllableUsers() {
|
||||
const ids = new Set();
|
||||
io.sockets.sockets.forEach((candidate) => {
|
||||
if (!candidate?.id || getRole(candidate) === 'spectator') return;
|
||||
if (roverManager.getRoversForSocket(candidate.id).length > 0) {
|
||||
ids.add(candidate.id);
|
||||
}
|
||||
});
|
||||
getParticipantSocketIds().forEach((socketId) => {
|
||||
const socket = io.sockets.sockets.get(socketId);
|
||||
if (socket && getRole(socket) !== 'spectator') ids.add(socketId);
|
||||
});
|
||||
return ids.size;
|
||||
}
|
||||
|
||||
function canSpeakThroughPtz(socket) {
|
||||
/*
|
||||
PTZ chat uses roverId for identity, but the camera has its own queue rather
|
||||
@@ -1305,7 +1333,10 @@ function canRequestLiveVideo(socket) {
|
||||
*/
|
||||
return local || !shouldUseSnapshotsForExternalSpectatorVideo();
|
||||
}
|
||||
if (canUsePtzFeature(socket) && !shouldUseSnapshotsForNonTurnVideo()) {
|
||||
if (
|
||||
canUsePtzFeature(socket) &&
|
||||
!shouldUseSnapshotsForNonTurnVideo({ controllableUserCount: countControllableUsers() })
|
||||
) {
|
||||
/*
|
||||
Verified/VIP users who can queue or claim the camera are PTZ "turn"
|
||||
participants even before they become operator. When non-turn video is set
|
||||
@@ -1577,6 +1608,7 @@ module.exports = {
|
||||
ptzCameraEvents: events,
|
||||
getPublicState,
|
||||
getChatTargetForSocket,
|
||||
getParticipantSocketIds,
|
||||
canSpeakThroughPtz,
|
||||
speakText,
|
||||
canRequestLiveVideo,
|
||||
|
||||
@@ -42,6 +42,7 @@ const { getFeatureFlags } = require('../../helpers/features');
|
||||
const {
|
||||
canUseExternalSpectatorAccess,
|
||||
getBandwidthSavingsPolicy,
|
||||
shouldUseSnapshotsForNonTurnVideo,
|
||||
} = require('../../helpers/bandwidthSavings');
|
||||
const {
|
||||
getFeatureState,
|
||||
@@ -79,12 +80,17 @@ function hasExternalSpectatorGrant(socket) {
|
||||
return Boolean(state?.external);
|
||||
}
|
||||
|
||||
function buildBandwidthSavingsSessionState(socket) {
|
||||
function buildBandwidthSavingsSessionState(socket, controllableUserCount = 0) {
|
||||
const policy = getBandwidthSavingsPolicy();
|
||||
const local = isLocalNetwork(getSocketIp(socket));
|
||||
const granted = hasExternalSpectatorGrant(socket);
|
||||
return {
|
||||
...policy,
|
||||
nonTurnVideo: {
|
||||
...policy.nonTurnVideo,
|
||||
controllableUserCount,
|
||||
snapshotsActive: shouldUseSnapshotsForNonTurnVideo({ controllableUserCount }),
|
||||
},
|
||||
/*
|
||||
These derived fields let browser routes make clear UI choices without
|
||||
re-implementing IP/admin/grant logic. The server still enforces the same
|
||||
@@ -100,6 +106,24 @@ function buildBandwidthSavingsSessionState(socket) {
|
||||
};
|
||||
}
|
||||
|
||||
function countControllableUsers(userEntries = []) {
|
||||
const ids = new Set();
|
||||
userEntries.forEach((entry) => {
|
||||
const role = String(entry?.role || '');
|
||||
if (role === 'spectator') return;
|
||||
const socketId = String(entry?.socketId || '').trim();
|
||||
const roverId = String(entry?.roverId || '').trim();
|
||||
/*
|
||||
buildUserEntry already maps PTZ queued/operators to the PTZ pseudo-rover
|
||||
id and normal drivers to their physical rover. Counting entries after that
|
||||
normalization gives the browser the same conceptual "controllable users"
|
||||
count it shows in the user/queue panels without duplicating PTZ UI logic.
|
||||
*/
|
||||
if (socketId && roverId) ids.add(socketId);
|
||||
});
|
||||
return ids.size;
|
||||
}
|
||||
|
||||
function buildUserEntry(socket) {
|
||||
if (!socket) return null;
|
||||
const role = getRole(socket);
|
||||
@@ -124,19 +148,22 @@ function buildUserEntry(socket) {
|
||||
function buildSession(socket) {
|
||||
const overseerVote = getOverseerVoteStatus();
|
||||
const features = getFeatureFlags();
|
||||
const users = Array.from(io.sockets.sockets.values())
|
||||
const userEntries = Array.from(io.sockets.sockets.values())
|
||||
.map((sock) => buildUserEntry(sock))
|
||||
.filter(Boolean)
|
||||
.map((entry) => ({
|
||||
...entry,
|
||||
/*
|
||||
PTZ is intentionally not a roverManager record, so the normal physical
|
||||
rover visibility filter would erase the user's PTZ chat target. Preserve
|
||||
it here because getPtzChatTargetForSocket already applied the PTZ access
|
||||
and queue/operator rules before buildUserEntry returned it.
|
||||
*/
|
||||
roverId: entry.roverId === PTZ_CAMERA_ID ? entry.roverId : filterVisibleRoverId(socket, entry.roverId),
|
||||
}));
|
||||
.filter(Boolean);
|
||||
const controllableUserCount = countControllableUsers(userEntries);
|
||||
const users = userEntries.map((entry) => ({
|
||||
...entry,
|
||||
/*
|
||||
PTZ is intentionally not a roverManager record, so the normal physical
|
||||
rover visibility filter would erase the user's PTZ chat target. Preserve
|
||||
it here because getPtzChatTargetForSocket already applied the PTZ access
|
||||
and queue/operator rules before buildUserEntry returned it.
|
||||
*/
|
||||
roverId: entry.roverId === PTZ_CAMERA_ID
|
||||
? entry.roverId
|
||||
: filterVisibleRoverId(socket, entry.roverId),
|
||||
}));
|
||||
const roster = roverManager.getRosterForSocket(socket);
|
||||
const assignment = assignmentService.describeAssignment(socket?.id || '');
|
||||
const assignmentRoverId = filterVisibleRoverId(socket, assignment?.roverId);
|
||||
@@ -148,7 +175,7 @@ function buildSession(socket) {
|
||||
role: getRole(socket),
|
||||
mode: getMode(),
|
||||
isLocalNetwork: isLocalNetwork(getSocketIp(socket)),
|
||||
bandwidthSavings: buildBandwidthSavingsSessionState(socket),
|
||||
bandwidthSavings: buildBandwidthSavingsSessionState(socket, controllableUserCount),
|
||||
/*
|
||||
Features is the single UI contract for optional server capabilities. A
|
||||
disabled feature should be absent from navigation/layout decisions even
|
||||
|
||||
@@ -30,6 +30,7 @@ const { canAccessStream } = createVideoAuthPolicy({
|
||||
ptzCameraService,
|
||||
getSocketIp,
|
||||
isLocalNetwork,
|
||||
io,
|
||||
});
|
||||
|
||||
registerVideoAuthRoute({
|
||||
|
||||
@@ -19,8 +19,31 @@ function createVideoAuthPolicy(deps) {
|
||||
ptzCameraService,
|
||||
getSocketIp,
|
||||
isLocalNetwork,
|
||||
io,
|
||||
} = deps;
|
||||
|
||||
function countControllableUsers() {
|
||||
const ids = new Set();
|
||||
io.sockets.sockets.forEach((candidate) => {
|
||||
if (!candidate?.id || getRole(candidate) === 'spectator') return;
|
||||
/*
|
||||
MediaMTX can ask for authorization after a browser has already received
|
||||
a token, so this count intentionally mirrors videoSocketService instead
|
||||
of trusting the client-visible session policy snapshot.
|
||||
*/
|
||||
if (roverManager.getRoversForSocket(candidate.id).length > 0) {
|
||||
ids.add(candidate.id);
|
||||
}
|
||||
});
|
||||
if (typeof ptzCameraService.getParticipantSocketIds === 'function') {
|
||||
ptzCameraService.getParticipantSocketIds().forEach((socketId) => {
|
||||
const socket = io.sockets.sockets.get(socketId);
|
||||
if (socket && getRole(socket) !== 'spectator') ids.add(socketId);
|
||||
});
|
||||
}
|
||||
return ids.size;
|
||||
}
|
||||
|
||||
function canView(socket) {
|
||||
const mode = getMode();
|
||||
if (!socket) return false;
|
||||
@@ -78,7 +101,11 @@ function createVideoAuthPolicy(deps) {
|
||||
if (!roverManager.isDriver(roverId, socket)) {
|
||||
return false;
|
||||
}
|
||||
if (!isAudio && shouldUseSnapshotsForNonTurnVideo() && !turnService.canDrive(roverId, socket)) {
|
||||
if (
|
||||
!isAudio &&
|
||||
shouldUseSnapshotsForNonTurnVideo({ controllableUserCount: countControllableUsers() }) &&
|
||||
!turnService.canDrive(roverId, socket)
|
||||
) {
|
||||
/*
|
||||
This mirrors videoSocketService's token gate. MediaMTX can ask auth
|
||||
after a token has been issued, so the active-turn bandwidth rule must
|
||||
|
||||
@@ -85,6 +85,29 @@ function canViewRoomCamera(socket) {
|
||||
return passesMode(socket);
|
||||
}
|
||||
|
||||
function countControllableUsers() {
|
||||
const ids = new Set();
|
||||
io.sockets.sockets.forEach((candidate) => {
|
||||
if (!candidate?.id || getRole(candidate) === 'spectator') return;
|
||||
/*
|
||||
Rover drivers and PTZ participants are both "controllable" users for this
|
||||
bandwidth decision because either group can create a non-turn video view.
|
||||
Counting unique socket ids prevents someone who is transitioning between
|
||||
rover and PTZ from being counted twice.
|
||||
*/
|
||||
if (roverManager.getRoversForSocket(candidate.id).length > 0) {
|
||||
ids.add(candidate.id);
|
||||
}
|
||||
});
|
||||
if (typeof ptzCameraService.getParticipantSocketIds === 'function') {
|
||||
ptzCameraService.getParticipantSocketIds().forEach((socketId) => {
|
||||
const socket = io.sockets.sockets.get(socketId);
|
||||
if (socket && getRole(socket) !== 'spectator') ids.add(socketId);
|
||||
});
|
||||
}
|
||||
return ids.size;
|
||||
}
|
||||
|
||||
function normalizeRequest(payload = {}) {
|
||||
if (!payload) return null;
|
||||
if (payload.type && payload.id) {
|
||||
@@ -129,7 +152,7 @@ io.on('connection', (socket) => {
|
||||
!isAudio &&
|
||||
role !== 'spectator' &&
|
||||
!isAdmin(socket) &&
|
||||
shouldUseSnapshotsForNonTurnVideo() &&
|
||||
shouldUseSnapshotsForNonTurnVideo({ controllableUserCount: countControllableUsers() }) &&
|
||||
!turnService.canDrive(baseId, socket)
|
||||
) {
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user