diff --git a/server/src/services/idleService/index.js b/server/src/services/idleService/index.js index 1edc784e..a0fcfb69 100644 --- a/server/src/services/idleService/index.js +++ b/server/src/services/idleService/index.js @@ -1,33 +1,63 @@ // Idle Service -// Purpose: Triggers a modular idle action pipeline after a sustained no-driver period. -// Scope: Observes driver activity events and coordinates timer-based idle automation execution. +// Purpose: Triggers a modular idle action pipeline after a sustained no-operator-online period. +// Scope: Observes user/admin socket presence and coordinates timer-based idle automation execution. const logger = require('../../globals/logger').child('idleService'); -const { getActiveDrivers, turnEvents } = require('../turnService'); -const roverManager = require('../roverManager'); -const { getRecentDriveActivity } = require('../commandService'); +const io = require('../../globals/io'); +const { getRole, roleEvents } = require('../roleService'); const { IDLE_TIMEOUT_MS } = require('./constants'); const { runtime } = require('./state'); const { runIdleActions } = require('./actions'); function getActivitySnapshot() { - const active = getActiveDrivers(); - const turnCount = active && typeof active === 'object' ? Object.keys(active).length : 0; - const activeByTurn = turnCount; + let onlineUsers = 0; + let onlineAdmins = 0; + let onlineSpectators = 0; + let onlineIgnored = 0; - let liveCount = 0; - roverManager.rovers.forEach((record) => { - if (record?.drivers?.size > 0) liveCount += 1; + io.sockets.sockets.forEach((socket) => { + const role = getRole(socket); + + /* + Idle automation is about whether a real operator is present, not whether + a browser tab is merely watching. Spectators can leave the room lights, + PTZ emitters, and rovers in their automated idle state because they are + intentionally read-only and cannot be the person still using the setup. + */ + if (role === 'spectator') { + onlineSpectators += 1; + return; + } + + /* + Lockdown admins are counted with regular admins because both represent a + person with operator-level access who may be supervising the room without + actively driving a rover. Plain users also count even before they request + control, which is the behavior this service now needs. + */ + if (role === 'admin' || role === 'lockdown') { + onlineAdmins += 1; + return; + } + + if (role === 'user') { + onlineUsers += 1; + return; + } + + /* + Unknown future roles should not accidentally keep automation disabled. + If a new role should count as an operator, it should be added explicitly + above so this policy remains easy to audit. + */ + onlineIgnored += 1; }); - const activeByRoverDrivers = liveCount; - const recentDriveEvents = getRecentDriveActivity(IDLE_TIMEOUT_MS, { excludeAdmins: false }); - const activeByRecentDrive = recentDriveEvents.length; - - const totalActive = Math.max(activeByTurn, activeByRoverDrivers, activeByRecentDrive); + const totalActive = onlineUsers + onlineAdmins; return { - activeByTurn, - activeByRoverDrivers, - activeByRecentDrive, + onlineUsers, + onlineAdmins, + onlineSpectators, + onlineIgnored, totalActive, }; } @@ -53,7 +83,7 @@ function scheduleIdleTimer() { runtime.deadlineAt = null; const activity = getActivitySnapshot(); if (activity.totalActive > 0) { - logger.info('Idle automation skipped; active control detected', activity); + logger.info('Idle automation skipped; user or admin online', activity); return; } runtime.lastTriggeredAt = Date.now(); @@ -77,8 +107,18 @@ function refreshIdleState() { scheduleIdleTimer(); } -turnEvents.on('activeDriver', refreshIdleState); -turnEvents.on('queue', refreshIdleState); +io.on('connection', (socket) => { + /* + A user/admin can be online without ever touching rover controls, so socket + presence has to be a first-class idle signal. The disconnect hook is just as + important: it is what starts the idle timeout after the last non-spectator + leaves, even if no driving event happens around that departure. + */ + refreshIdleState(); + socket.on('disconnect', refreshIdleState); +}); + +roleEvents.on('change', refreshIdleState); refreshIdleState(); diff --git a/server/src/services/ptzCameraService/index.js b/server/src/services/ptzCameraService/index.js index 376ca763..e4424305 100644 --- a/server/src/services/ptzCameraService/index.js +++ b/server/src/services/ptzCameraService/index.js @@ -347,7 +347,7 @@ function startPublisher() { '-tune', 'zerolatency', '-threads', - '2', + '8', '-x264-params', 'sliced-threads=1:sync-lookahead=0:rc-lookahead=0:keyint=20:min-keyint=20:scenecut=0', '-bf',