mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
fixe
This commit is contained in:
@@ -140,6 +140,9 @@ ptzCamera:
|
||||
# changes while the integration still remains a single-camera feature.
|
||||
profileToken: "003"
|
||||
turnDurationMs: 300000
|
||||
# PTZ replay capture needs a known-good replay encoder on the server. Keep it
|
||||
# off by default so adding live PTZ does not start a broken replay worker loop.
|
||||
replayEnabled: false
|
||||
|
||||
kinect:
|
||||
enabled: false
|
||||
|
||||
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-B3VrYCWz.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DrIw695l.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DwpUkSbG.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -25,6 +25,7 @@ const DEFAULT_ONVIF_PORT = 8000;
|
||||
const DEFAULT_PROFILE_TOKEN = '003';
|
||||
const DEFAULT_TURN_DURATION_MS = 5 * 60 * 1000;
|
||||
const DOCK_GRACE_MS = 60 * 1000;
|
||||
const DEFAULT_REPLAY_ENABLED = false;
|
||||
const SNAPSHOT_DIR = process.env.ROVER_SNAPSHOT_DIR || '/var/lib/rover-snapshots';
|
||||
const SNAPSHOT_POLL_MS = 300;
|
||||
const SNAPSHOT_STREAM_INTERVAL_MS = 2000;
|
||||
@@ -78,6 +79,15 @@ function getTurnDurationMs() {
|
||||
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_TURN_DURATION_MS;
|
||||
}
|
||||
|
||||
function isReplayEnabled() {
|
||||
/*
|
||||
Keep this as a single helper so the replay source catalog and the replay
|
||||
worker catalog cannot drift apart. If PTZ replay is off, the UI should not
|
||||
advertise a source that no worker is recording.
|
||||
*/
|
||||
return cameraConfig.replayEnabled === undefined ? DEFAULT_REPLAY_ENABLED : Boolean(cameraConfig.replayEnabled);
|
||||
}
|
||||
|
||||
function passesMode(socket) {
|
||||
const mode = getMode();
|
||||
if (mode === MODES.LOCKDOWN) return isLockdownAdmin(socket);
|
||||
@@ -573,6 +583,23 @@ function sendSnapshotFrame(socket, buffer, ts) {
|
||||
socket.emit('ptzCamera:snapshotFrame', { id: PTZ_CAMERA_ID, ts }, buffer);
|
||||
}
|
||||
|
||||
function normalizeSocketArgs(firstArg, secondArg) {
|
||||
/*
|
||||
Socket.IO does not reserve a payload slot. If the browser emits only an ack
|
||||
callback, the callback arrives as the first argument; if it emits no ack,
|
||||
there is no callback at all. PTZ movement is sometimes fire-and-forget from
|
||||
the shared rover control pipeline, so every handler needs the same small
|
||||
normalizer before it calls back.
|
||||
*/
|
||||
if (typeof firstArg === 'function') {
|
||||
return { payload: {}, cb: firstArg };
|
||||
}
|
||||
return {
|
||||
payload: firstArg && typeof firstArg === 'object' ? firstArg : {},
|
||||
cb: typeof secondArg === 'function' ? secondArg : () => {},
|
||||
};
|
||||
}
|
||||
|
||||
events.on('snapshot:frame', ({ buffer, ts }) => {
|
||||
const subscribers = snapshotSubscribers.get(PTZ_CAMERA_ID);
|
||||
if (!subscribers || !buffer) return;
|
||||
@@ -598,56 +625,64 @@ events.on('snapshot:status', ({ error }) => {
|
||||
|
||||
function registerSocketHandlers() {
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('ptzCamera:claim', async (_payload = {}, cb = () => {}) => {
|
||||
socket.on('ptzCamera:claim', async (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, state: await claim(socket) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:release', async (_payload = {}, cb = () => {}) => {
|
||||
socket.on('ptzCamera:release', async (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, state: await release(socket) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:move', async (payload = {}, cb = () => {}) => {
|
||||
socket.on('ptzCamera:move', async (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb(await move(socket, payload));
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:stop', async (_payload = {}, cb = () => {}) => {
|
||||
socket.on('ptzCamera:stop', async (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb(await stop(socket));
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:status', async (_payload = {}, cb = () => {}) => {
|
||||
socket.on('ptzCamera:status', async (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, status: await getStatus(socket) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:spotlight', async (payload = {}, cb = () => {}) => {
|
||||
socket.on('ptzCamera:spotlight', async (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, light: await setSpotlight(socket, payload) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:ir', async (payload = {}, cb = () => {}) => {
|
||||
socket.on('ptzCamera:ir', async (firstArg, secondArg) => {
|
||||
const { payload, cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
cb({ ok: true, ir: await setIr(socket, payload) });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
socket.on('ptzCamera:snapshotSubscribe', (_payload = {}, cb = () => {}) => {
|
||||
socket.on('ptzCamera:snapshotSubscribe', (firstArg, secondArg) => {
|
||||
const { cb } = normalizeSocketArgs(firstArg, secondArg);
|
||||
try {
|
||||
if (!passesMode(socket)) throw new Error('Not authorized for PTZ snapshots');
|
||||
addSnapshotSubscription(socket);
|
||||
@@ -705,12 +740,24 @@ module.exports = {
|
||||
ptzCameraEvents: events,
|
||||
getPublicState,
|
||||
canRequestLiveVideo,
|
||||
getReplaySource: () => enabled ? { type: 'ptz', id: PTZ_CAMERA_ID, label: cameraConfig.name || 'PTZ Camera' } : null,
|
||||
getReplayWorkerSource: () => enabled ? {
|
||||
id: PTZ_CAMERA_ID,
|
||||
sourceType: 'ptz',
|
||||
kind: 'video',
|
||||
label: cameraConfig.name || 'PTZ Camera',
|
||||
inputUrl: `srt://127.0.0.1:9000?streamid=read:${encodeURIComponent(PTZ_STREAM_PATH)}`,
|
||||
} : null,
|
||||
getReplaySource: () => enabled && isReplayEnabled()
|
||||
? { type: 'ptz', id: PTZ_CAMERA_ID, label: cameraConfig.name || 'PTZ Camera' }
|
||||
: null,
|
||||
getReplayWorkerSource: () => {
|
||||
/*
|
||||
PTZ replay capture is optional because the server ffmpeg build must be
|
||||
able to produce browser/Discord-friendly replay segments. The camera live
|
||||
feed can remain raw for MediaMTX while replay capture is left off until
|
||||
the actual server has a working encoder or a copy-only PTZ replay path is
|
||||
intentionally designed.
|
||||
*/
|
||||
if (!enabled || !isReplayEnabled()) return null;
|
||||
return {
|
||||
id: PTZ_CAMERA_ID,
|
||||
sourceType: 'ptz',
|
||||
kind: 'video',
|
||||
label: cameraConfig.name || 'PTZ Camera',
|
||||
inputUrl: `srt://127.0.0.1:9000?streamid=read:${encodeURIComponent(PTZ_STREAM_PATH)}`,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -113,7 +113,15 @@ export class WhepPlayer {
|
||||
signal: this.abortController.signal,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`WHEP request failed: ${response.status}`);
|
||||
/*
|
||||
MediaMTX includes the useful rejection reason in the response body
|
||||
for many 4xx WHEP failures, such as unsupported codecs or malformed
|
||||
SDP. Surface that body so camera/video debugging does not stop at a
|
||||
generic HTTP status code.
|
||||
*/
|
||||
const body = await response.text().catch(() => '');
|
||||
const detail = body ? `: ${body.slice(0, 180)}` : '';
|
||||
throw new Error(`WHEP request failed: ${response.status}${detail}`);
|
||||
}
|
||||
const answerSdp = await response.text();
|
||||
await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });
|
||||
|
||||
Reference in New Issue
Block a user