new ptz ui

This commit is contained in:
legop3
2026-07-12 01:19:36 -04:00
parent fd1caf2df9
commit 09ce66e7a4
17 changed files with 1044 additions and 208 deletions
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -78,8 +78,8 @@
<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-DT82w9Sn.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Dyg2cJnP.css">
<script type="module" crossorigin src="/assets/index-YgdVe0Du.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-17XZRVBQ.css">
</head>
<body>
<div id="root"></div>
@@ -7,8 +7,15 @@ const roverManager = require('../roverManager');
const { getRole } = require('../roleService');
const { describeAssignment } = require('../assignmentService');
const { getNickname } = require('../nicknameService');
const ptzCameraService = require('../ptzCameraService');
function resolvePtzChatTarget(socketId) {
return ptzCameraService.getChatTargetForSocket(socketId) || null;
}
function resolveRoverId(socketId) {
const ptzTarget = resolvePtzChatTarget(socketId);
if (ptzTarget?.roverId) return ptzTarget.roverId;
const primary = roverManager.getPrimaryRoverForSocket(socketId);
if (primary) return primary;
const assignment = describeAssignment(socketId);
@@ -17,10 +24,22 @@ function resolveRoverId(socketId) {
function resolveRoverColor(roverId) {
if (!roverId) return null;
if (String(roverId) === ptzCameraService.PTZ_CAMERA_ID) {
return ptzCameraService.getPublicState()?.color || null;
}
const record = roverManager.rovers.get(String(roverId));
return record?.meta?.color || null;
}
function resolveRoverName(roverId) {
if (!roverId) return null;
if (String(roverId) === ptzCameraService.PTZ_CAMERA_ID) {
return ptzCameraService.getPublicState()?.name || null;
}
const record = roverManager.rovers.get(String(roverId));
return record?.meta?.name || null;
}
function isPrivateClosedRoverId(roverId) {
if (!roverId) return false;
return roverManager.canReplayRoverId(roverId) !== true;
@@ -100,6 +119,7 @@ function buildRoverCtxSnapshot(roverId) {
function buildMessage(socket, text, meta = {}) {
const roverId = meta.roverId || resolveRoverId(socket?.id);
const roverColor = meta.roverColor ?? resolveRoverColor(roverId);
const roverName = meta.roverName ?? resolveRoverName(roverId);
const toolCalls = Array.isArray(meta.toolCalls)
? meta.toolCalls
.map((entry) => {
@@ -122,6 +142,7 @@ function buildMessage(socket, text, meta = {}) {
nickname: meta.nickname || getNickname(socket) || null,
role: meta.role || getRole(socket),
roverId,
roverName,
roverColor,
fromDiscord: Boolean(meta.fromDiscord),
discordGuildId: meta.discordGuildId || null,
@@ -143,6 +164,7 @@ function buildMessage(socket, text, meta = {}) {
function buildTypingPayload(socket, meta = {}) {
const roverId = meta.roverId || resolveRoverId(socket?.id);
const roverColor = meta.roverColor ?? resolveRoverColor(roverId);
const roverName = meta.roverName ?? resolveRoverName(roverId);
const socketId = socket?.id || null;
const fromDiscord = Boolean(meta.fromDiscord);
let typingId = meta.typingId || null;
@@ -165,6 +187,7 @@ function buildTypingPayload(socket, meta = {}) {
nickname: meta.nickname || getNickname(socket) || null,
role: meta.role || getRole(socket),
roverId,
roverName,
roverColor,
fromDiscord,
discordGuildId: meta.discordGuildId || null,
@@ -27,6 +27,7 @@ const DEFAULT_TURN_DURATION_MS = 5 * 60 * 1000;
// PTZ is a normal replay source now, so capture should be on unless the feature
// explicitly disables replay for the camera.
const DEFAULT_REPLAY_ENABLED = true;
const DEFAULT_PTZ_COLOR = '#387bf8';
const SNAPSHOT_DIR = process.env.ROVER_SNAPSHOT_DIR || '/var/lib/rover-snapshots';
const SNAPSHOT_POLL_MS = 300;
const SNAPSHOT_STREAM_INTERVAL_MS = 2000;
@@ -268,6 +269,7 @@ function getPublicState(socket = null) {
enabled,
id: PTZ_CAMERA_ID,
name: cameraConfig.name || 'PTZ Camera',
color: cameraConfig.color || DEFAULT_PTZ_COLOR,
initialized: state.initialized,
error: state.error,
streamPath: state.streamPath,
@@ -286,6 +288,25 @@ function getPublicState(socket = null) {
};
}
function getChatTargetForSocket(socketId) {
/*
Chat badges are rendered through the same rover badge component on the
browser, so PTZ presents itself as a rover-like chat target while a user is
actively operating or waiting for the camera. Keeping this mapping in the
PTZ service avoids making chat infer camera queue details from public state.
*/
if (!enabled || !socketId) return null;
const normalized = String(socketId);
const waiting = state.queue.includes(normalized);
const operating = state.operatorSocketId === normalized;
if (!waiting && !operating) return null;
return {
roverId: PTZ_CAMERA_ID,
roverName: cameraConfig.name || 'PTZ Camera',
roverColor: cameraConfig.color || DEFAULT_PTZ_COLOR,
};
}
function callOnvif(method, options = {}) {
return new Promise((resolve, reject) => {
if (!onvifCam || typeof onvifCam[method] !== 'function') {
@@ -1131,6 +1152,7 @@ module.exports = {
PTZ_STREAM_PATH,
ptzCameraEvents: events,
getPublicState,
getChatTargetForSocket,
canRequestLiveVideo,
disableEmittersForIdle,
getReplaySource: () => enabled && isReplayEnabled()