Files
MultiRoombaRover/server/src/services/replayEngineV2/replaySources.js
T

109 lines
3.7 KiB
JavaScript

// Replay Source Selection
// Purpose: Exposes replay source discovery/validation for web and Discord replay requests.
// Scope: Handles user-visible replay source catalogs and default source selection rules.
const roverManager = require('../roverManager');
const { getRoomCameras } = require('../roomCameraService');
const ptzCameraService = require('../ptzCameraService');
function getReplaySources(socket = null) {
const roster = socket ? roverManager.getRosterForSocket(socket) : roverManager.getRoster();
const roverSources = roster
.filter((rover) => roverManager.canReplayRoverId(rover.id, socket))
.map((rover) => ({
type: 'rover',
id: String(rover.id),
label: rover.name || rover.id,
color: rover.color || null,
}));
const roomSources = getRoomCameras().map((camera) => ({
type: 'room',
id: String(camera.id),
label: camera.name || camera.id,
}));
const ptzSource = ptzCameraService.getReplaySource();
const ptzSources = ptzSource ? [ptzSource] : [];
return [...roverSources, ...roomSources, ...ptzSources];
}
function normalizeSource(entry) {
if (!entry) return null;
if (typeof entry === 'string') {
const [type, id] = entry.split(':');
if (!type || !id) return null;
return { type, id: String(id) };
}
if (typeof entry === 'object' && entry.type && entry.id) {
return { type: entry.type, id: String(entry.id) };
}
return null;
}
function validateSources(list = [], socket = null) {
const allowed = new Map();
getReplaySources(socket).forEach((source) => {
allowed.set(`${source.type}:${source.id}`, source);
});
const unique = new Map();
(Array.isArray(list) ? list : []).forEach((entry) => {
const normalized = normalizeSource(entry);
if (!normalized) return;
const key = `${normalized.type}:${normalized.id}`;
const source = allowed.get(key);
if (!source) return;
unique.set(key, { type: source.type, id: source.id, label: source.label });
});
return Array.from(unique.values());
}
function getDefaultWebSources(assignment = {}, socket = null) {
/*
PTZ ownership is intentionally tracked outside assignmentService because
taking the camera releases the user's rover assignment. Check the PTZ
service directly so a source-less web replay request, including `rs
replay`, follows the camera currently controlled by that socket just as it
follows an assigned rover below.
isOperator is deliberately stricter than PTZ access or queue membership:
spectators and users waiting for a camera turn must not silently replay a
camera they are not currently operating. Keeping this rule here also makes
every web replay entry point share the same default instead of teaching the
chat-command adapter about PTZ-specific state.
*/
if (ptzCameraService.getPublicState(socket).isOperator) {
const source = ptzCameraService.getReplaySource();
if (!source) return [];
return [{ type: source.type, id: String(source.id), label: source.label || source.id }];
}
if (assignment?.roverId) {
const id = String(assignment.roverId);
const match = getReplaySources(socket).find((entry) => entry.type === 'rover' && entry.id === id);
if (!match) return [];
return [{ type: 'rover', id, label: match.label || id }];
}
return [];
}
function getDefaultDiscordSources() {
const sources = getRoomCameras().map((camera) => ({
type: 'room',
id: String(camera.id),
label: camera.name || camera.id,
}));
const ptzSource = ptzCameraService.getReplaySource();
if (ptzSource) sources.push(ptzSource);
return sources;
}
module.exports = {
getReplaySources,
validateSources,
getDefaultWebSources,
getDefaultDiscordSources,
};