mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
ugh
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
const EventEmitter = require('events');
|
||||
const logger = require('../globals/logger').child('roomCameraService');
|
||||
const { loadConfig } = require('../helpers/configLoader');
|
||||
|
||||
const events = new EventEmitter();
|
||||
const config = loadConfig();
|
||||
|
||||
const cameraMap = new Map();
|
||||
|
||||
function normalizeCamera(camera) {
|
||||
if (!camera) return null;
|
||||
const id = camera.id || camera.name;
|
||||
if (!id) {
|
||||
logger.warn('Room camera missing id', camera);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: String(id),
|
||||
name: camera.name || camera.id || String(id),
|
||||
description: camera.description || null,
|
||||
};
|
||||
}
|
||||
|
||||
function loadFromConfig() {
|
||||
cameraMap.clear();
|
||||
const list = Array.isArray(config.roomCameras) ? config.roomCameras : [];
|
||||
list.forEach((camera) => {
|
||||
const normalized = normalizeCamera(camera);
|
||||
if (normalized) {
|
||||
cameraMap.set(normalized.id, normalized);
|
||||
}
|
||||
});
|
||||
logger.info('Loaded room cameras', { count: cameraMap.size });
|
||||
events.emit('update', getRoomCameras());
|
||||
}
|
||||
|
||||
function getRoomCameras() {
|
||||
return Array.from(cameraMap.values());
|
||||
}
|
||||
|
||||
function getRoomCamera(id) {
|
||||
if (!id) return null;
|
||||
return cameraMap.get(String(id)) || null;
|
||||
}
|
||||
|
||||
loadFromConfig();
|
||||
|
||||
module.exports = {
|
||||
getRoomCameras,
|
||||
getRoomCamera,
|
||||
roomCameraEvents: events,
|
||||
};
|
||||
@@ -6,6 +6,7 @@ const roverManager = require('./roverManager');
|
||||
const { managerEvents } = roverManager;
|
||||
const assignmentService = require('./assignmentService');
|
||||
const { getActiveDrivers, turnEvents } = require('./turnService');
|
||||
const { getRoomCameras, roomCameraEvents } = require('./roomCameraService');
|
||||
|
||||
function buildSession(socket) {
|
||||
return {
|
||||
@@ -15,6 +16,7 @@ function buildSession(socket) {
|
||||
roster: roverManager.getRoster(),
|
||||
assignment: assignmentService.describeAssignment(socket?.id || ''),
|
||||
activeDrivers: getActiveDrivers(),
|
||||
roomCameras: getRoomCameras(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,6 +80,11 @@ turnEvents.on('activeDriver', () => {
|
||||
syncAll();
|
||||
});
|
||||
|
||||
roomCameraEvents.on('update', () => {
|
||||
logger.info('Room camera change detected; syncing all clients');
|
||||
syncAll();
|
||||
});
|
||||
|
||||
// sync all sockets 5 seconds
|
||||
setInterval(() => {
|
||||
logger.info('Periodic session sync for all clients');
|
||||
|
||||
@@ -24,10 +24,10 @@ function getPathPrefix() {
|
||||
const whepPathPrefix = getPathPrefix().replace(/\/+$/, '').replace(/^\/+/, '');
|
||||
const whepPrefixSegments = whepPathPrefix ? whepPathPrefix.split('/').filter(Boolean) : [];
|
||||
|
||||
function extractRoverId(path) {
|
||||
function extractStreamInfo(path) {
|
||||
const segments = (path || '').split('/').filter(Boolean);
|
||||
if (!segments.length) {
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
|
||||
let start = 0;
|
||||
@@ -43,10 +43,14 @@ function extractRoverId(path) {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
if (end - start !== 1) {
|
||||
return '';
|
||||
const remaining = segments.slice(start, end);
|
||||
if (remaining.length === 1) {
|
||||
return { type: 'rover', id: remaining[0] || '' };
|
||||
}
|
||||
return segments[start] || '';
|
||||
if (remaining.length === 2 && remaining[0] === 'room') {
|
||||
return { type: 'room', id: remaining[1] || '' };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function canView(socket) {
|
||||
@@ -67,17 +71,17 @@ app.post('/mediamtx/auth', (req, res) => {
|
||||
const body = req.body || {};
|
||||
const path = (body.path || '').replace(/^\//, '');
|
||||
const sessionId = body.user;
|
||||
const roverId = extractRoverId(path);
|
||||
logger.info('video auth request', { path: body.path, sessionId, roverId });
|
||||
const streamInfo = extractStreamInfo(path);
|
||||
logger.info('video auth request', { path: body.path, sessionId, stream: streamInfo });
|
||||
|
||||
if (!sessionId || !roverId) {
|
||||
logger.warn('auth missing session or rover (session=%s path=%s)', sessionId, path);
|
||||
if (!sessionId || !streamInfo?.id) {
|
||||
logger.warn('auth missing session or stream (session=%s path=%s)', sessionId, path);
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
const info = videoSessions.getSession(sessionId);
|
||||
if (!info || info.roverId !== roverId) {
|
||||
logger.warn('invalid session %s for rover %s', sessionId, roverId);
|
||||
if (!info || info.sourceType !== streamInfo.type || info.sourceId !== streamInfo.id) {
|
||||
logger.warn('invalid session %s for stream %s:%s', sessionId, streamInfo.type, streamInfo.id);
|
||||
return res.status(401).end();
|
||||
}
|
||||
const socket = io.sockets.sockets.get(info.socketId);
|
||||
@@ -89,8 +93,8 @@ app.post('/mediamtx/auth', (req, res) => {
|
||||
return res.status(401).end();
|
||||
}
|
||||
const role = getRole(socket);
|
||||
if (role !== 'spectator' && !isAdmin(socket)) {
|
||||
if (!roverManager.isDriver(roverId, socket)) {
|
||||
if (streamInfo.type === 'rover' && role !== 'spectator' && !isAdmin(socket)) {
|
||||
if (!roverManager.isDriver(streamInfo.id, socket)) {
|
||||
return res.status(401).end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const io = require('../globals/io');
|
||||
|
||||
const sessions = new Map(); // sessionId -> { socketId, roverId }
|
||||
const sessions = new Map(); // sessionId -> { socketId, sourceType, sourceId }
|
||||
const socketSessions = new Map(); // socketId -> Set(sessionId)
|
||||
|
||||
function createSession(socket, roverId) {
|
||||
function validateSource(source = {}) {
|
||||
const { type, id } = source;
|
||||
if (!type || !id) {
|
||||
throw new Error('Invalid video source');
|
||||
}
|
||||
return { type, id };
|
||||
}
|
||||
|
||||
function createSession(socket, source) {
|
||||
const { type, id } = validateSource(source);
|
||||
const sessionId = uuidv4();
|
||||
sessions.set(sessionId, { socketId: socket.id, roverId });
|
||||
sessions.set(sessionId, { socketId: socket.id, sourceType: type, sourceId: id });
|
||||
if (!socketSessions.has(socket.id)) {
|
||||
socketSessions.set(socket.id, new Set());
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ const { getMode, MODES } = require('./modeManager');
|
||||
const { isAdmin, isLockdownAdmin, getRole } = require('./roleService');
|
||||
const videoSessions = require('./videoSessions');
|
||||
const roverManager = require('./roverManager');
|
||||
const { getRoomCamera } = require('./roomCameraService');
|
||||
const { loadConfig } = require('../helpers/configLoader');
|
||||
|
||||
const config = loadConfig();
|
||||
const mediaConfig = config.media || {};
|
||||
|
||||
function buildWhepUrl(roverId) {
|
||||
function getMediaPrefix() {
|
||||
const base = mediaConfig.whepBaseUrl;
|
||||
if (!base) {
|
||||
return '';
|
||||
@@ -21,12 +22,22 @@ function buildWhepUrl(roverId) {
|
||||
} catch (err) {
|
||||
// leave prefix as-is when URL parsing fails; fall back to string cleanup below
|
||||
}
|
||||
const cleanBase = prefix.replace(/\/+$/, '');
|
||||
const encodedId = encodeURIComponent(roverId);
|
||||
return `${cleanBase}/${encodedId}/whep`;
|
||||
return prefix.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function canView(socket, roverId) {
|
||||
function buildWhepUrlForSource(source) {
|
||||
const cleanBase = getMediaPrefix();
|
||||
if (!cleanBase) return '';
|
||||
const segments = [];
|
||||
if (source.type === 'room') {
|
||||
segments.push('room', encodeURIComponent(source.id));
|
||||
} else {
|
||||
segments.push(encodeURIComponent(source.id));
|
||||
}
|
||||
return `${cleanBase}/${segments.join('/')}/whep`;
|
||||
}
|
||||
|
||||
function passesMode(socket) {
|
||||
const mode = getMode();
|
||||
if (mode === MODES.LOCKDOWN && !isLockdownAdmin(socket)) {
|
||||
return false;
|
||||
@@ -34,6 +45,13 @@ function canView(socket, roverId) {
|
||||
if (mode === MODES.ADMIN && !isAdmin(socket)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function canViewRover(socket, roverId) {
|
||||
if (!passesMode(socket)) {
|
||||
return false;
|
||||
}
|
||||
const role = getRole(socket);
|
||||
if (role === 'spectator' || isAdmin(socket)) {
|
||||
return true;
|
||||
@@ -41,27 +59,56 @@ function canView(socket, roverId) {
|
||||
return roverManager.isDriver(roverId, socket);
|
||||
}
|
||||
|
||||
function canViewRoomCamera(socket) {
|
||||
return passesMode(socket);
|
||||
}
|
||||
|
||||
function normalizeRequest(payload = {}) {
|
||||
if (!payload) return null;
|
||||
if (payload.type && payload.id) {
|
||||
return { type: payload.type, id: String(payload.id) };
|
||||
}
|
||||
if (payload.roverId) {
|
||||
return { type: 'rover', id: String(payload.roverId) };
|
||||
}
|
||||
if (payload.roomCameraId) {
|
||||
return { type: 'room', id: String(payload.roomCameraId) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('video:request', ({ roverId } = {}, cb = () => {}) => {
|
||||
socket.on('video:request', (payload = {}, cb = () => {}) => {
|
||||
try {
|
||||
if (!roverId) {
|
||||
throw new Error('roverId required');
|
||||
const target = normalizeRequest(payload);
|
||||
if (!target) {
|
||||
throw new Error('video source required');
|
||||
}
|
||||
if (!roverManager.rovers.has(roverId)) {
|
||||
throw new Error('Rover offline');
|
||||
if (target.type === 'rover') {
|
||||
if (!roverManager.rovers.has(target.id)) {
|
||||
throw new Error('Rover offline');
|
||||
}
|
||||
if (!canViewRover(socket, target.id)) {
|
||||
throw new Error('Not authorized for video');
|
||||
}
|
||||
} else if (target.type === 'room') {
|
||||
if (!getRoomCamera(target.id)) {
|
||||
throw new Error('Unknown room camera');
|
||||
}
|
||||
if (!canViewRoomCamera(socket)) {
|
||||
throw new Error('Not authorized for room camera');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unsupported video source');
|
||||
}
|
||||
if (!canView(socket, roverId)) {
|
||||
throw new Error('Not authorized for video');
|
||||
}
|
||||
const url = buildWhepUrl(roverId);
|
||||
const url = buildWhepUrlForSource(target);
|
||||
if (!url) {
|
||||
throw new Error('Server video base URL missing');
|
||||
}
|
||||
const sessionId = videoSessions.createSession(socket, roverId);
|
||||
cb({ url, token: sessionId });
|
||||
const sessionId = videoSessions.createSession(socket, target);
|
||||
cb({ url, token: sessionId, type: target.type, id: target.id });
|
||||
} catch (err) {
|
||||
logger.warn('video request failed: %s', err.message);
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user