optional sidebar

This commit is contained in:
legop3
2026-04-28 13:13:42 -04:00
parent 928c1112a1
commit b0cd29c075
8 changed files with 112 additions and 35 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-lJfgnUIc.js"></script>
<script type="module" crossorigin src="/assets/index-bndWXjZU.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DVTOmRBl.css">
</head>
<body>
+9 -2
View File
@@ -450,7 +450,7 @@ function buildReplayCaption({ requester, usedSources = [], missingSources = [],
return lines.join('\n');
}
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '') {
async function sendReplayToChannel(channelId, requester, sources = [], explicitTitle = '', includeSidebar = true) {
if (!channelId) {
throw new Error('Replay channel not configured');
}
@@ -459,6 +459,7 @@ async function sendReplayToChannel(channelId, requester, sources = [], explicitT
sources,
title: resolvedTitle,
requester,
includeSidebar,
});
const filenameBase = sanitizeReplayTitleForFilename(resolvedTitle);
const attachment = new AttachmentBuilder(buffer, { name: `${filenameBase}.mp4` });
@@ -1619,7 +1620,13 @@ function handleBusEvent(event) {
schedulePresenceRotation();
break;
case 'replay.requested':
sendReplayToChannel(payload?.channelId, payload?.requester, payload?.sources || [], payload?.title || '').catch((err) => {
sendReplayToChannel(
payload?.channelId,
payload?.requester,
payload?.sources || [],
payload?.title || '',
payload?.includeSidebar !== false,
).catch((err) => {
logger.warn('Replay send failed', err.message);
});
break;
+29 -21
View File
@@ -724,7 +724,7 @@ async function probeMaxFrameSize(paths) {
return { maxWidth, maxHeight };
}
async function buildReplayVideo({ sources = [], title = '', requester = '' } = {}) {
async function buildReplayVideo({ sources = [], title = '', requester = '', includeSidebar = true } = {}) {
if (!Array.isArray(sources) || !sources.length) {
throw new Error('No replay sources selected');
}
@@ -843,22 +843,6 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
tileWidth = clampEven(tileWidth);
tileHeight = clampEven(tileHeight);
const selectedRoverIds = usedSources
.filter((entry) => entry?.type === 'rover')
.map((entry) => String(entry.id));
const driverBatteryLines = buildDriverBatterySnapshot(selectedRoverIds);
const chatEvents = buildChatEventsForWindow(tStart, tEnd);
const durationSec = BUILD_DURATION_MS / 1000;
const sidebarPath = await renderSidebarVideo({
tmpDir,
title: resolvedTitle,
durationSec,
height: clampEven(tileHeight * layout.rows),
windowStartMs: tStart,
driverBatteryLines,
chatEvents,
});
const inputArgs = [];
const filterParts = [];
const layoutParts = [];
@@ -872,9 +856,29 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
layoutParts.push(`${x}_${y}`);
}
inputArgs.push('-i', sidebarPath);
const sidebarInputIndex = normalizedVideos.length;
const audioInputStart = normalizedVideos.length + 1;
let audioInputStart = normalizedVideos.length;
let sidebarInputIndex = -1;
if (includeSidebar) {
const selectedRoverIds = usedSources
.filter((entry) => entry?.type === 'rover')
.map((entry) => String(entry.id));
const driverBatteryLines = buildDriverBatterySnapshot(selectedRoverIds);
const chatEvents = buildChatEventsForWindow(tStart, tEnd);
const durationSec = BUILD_DURATION_MS / 1000;
const sidebarPath = await renderSidebarVideo({
tmpDir,
title: resolvedTitle,
durationSec,
height: clampEven(tileHeight * layout.rows),
windowStartMs: tStart,
driverBatteryLines,
chatEvents,
});
inputArgs.push('-i', sidebarPath);
sidebarInputIndex = normalizedVideos.length;
audioInputStart = normalizedVideos.length + 1;
}
for (const audioPath of normalizedAudios) {
inputArgs.push('-i', audioPath);
}
@@ -887,7 +891,11 @@ async function buildReplayVideo({ sources = [], title = '', requester = '' } = {
`xstack=inputs=${normalizedVideos.length}:layout=${layoutParts.join('|')}:fill=black[vgrid]`,
);
}
filterParts.push(`[vgrid][${sidebarInputIndex}:v]hstack=inputs=2[vout]`);
if (includeSidebar) {
filterParts.push(`[vgrid][${sidebarInputIndex}:v]hstack=inputs=2[vout]`);
} else {
filterParts.push('[vgrid]null[vout]');
}
if (normalizedAudios.length) {
const audioRefs = normalizedAudios.map((_, idx) => `[${audioInputStart + idx}:a]`).join('');
@@ -20,6 +20,11 @@ function normalizeReplayTitle(value) {
return value.trim().slice(0, 120);
}
function normalizeIncludeSidebar(value) {
if (typeof value === 'boolean') return value;
return true;
}
io.on('connection', (socket) => {
socket.on('replay:trigger', (payload = {}, cb = () => {}) => {
if (getMode() === MODES.LOCKDOWN) {
@@ -43,6 +48,7 @@ io.on('connection', (socket) => {
}
const requester = buildRequesterLabel(socket);
const title = normalizeReplayTitle(payload?.title);
const includeSidebar = normalizeIncludeSidebar(payload?.includeSidebar);
const attempt = tryTriggerReplay({ by: { source: 'web', requester } });
if (!attempt.ok) {
cb({ error: 'Replay cooldown active', remainingMs: attempt.remainingMs, state: attempt.state });
@@ -55,6 +61,7 @@ io.on('connection', (socket) => {
channelId,
requester,
title,
includeSidebar,
sources,
requestedBy: { socketId: socket.id },
},