room camera use its own video players

This commit is contained in:
legop3
2025-11-20 15:57:00 -05:00
parent a92294dba8
commit e05f258fb3
7 changed files with 110 additions and 42 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
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title> <title>webui</title>
<script type="module" crossorigin src="/assets/index-Cpx-3dPK.js"></script> <script type="module" crossorigin src="/assets/index-B_IFrOiI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CT_S70Fr.css"> <link rel="stylesheet" crossorigin href="/assets/index-ZQklSSJR.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+92
View File
@@ -0,0 +1,92 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { WhepPlayer } from '../lib/whepPlayer.js';
const RESTART_DELAY_MS = 2500;
const RETRYABLE_STATUSES = new Set(['error', 'failed', 'disconnected', 'closed', 'stopped']);
export default function RoomCameraFeed({ sessionInfo, label }) {
const videoRef = useRef(null);
const restartTimer = useRef(null);
const [status, setStatus] = useState('idle');
const [detail, setDetail] = useState(null);
const [restartToken, setRestartToken] = useState(0);
const scheduleRestart = useCallback(() => {
clearTimeout(restartTimer.current);
restartTimer.current = setTimeout(() => setRestartToken((token) => token + 1), RESTART_DELAY_MS);
}, []);
const ensurePlayback = useCallback(async () => {
const video = videoRef.current;
if (!video) return;
try {
video.muted = true;
await video.play();
} catch {
// Autoplay might fail; we'll retry after reconnect.
}
}, []);
useEffect(() => {
return () => {
clearTimeout(restartTimer.current);
};
}, []);
useEffect(() => {
if (!sessionInfo?.url || !videoRef.current) {
return undefined;
}
let active = true;
const handleStatus = (nextStatus, info) => {
if (!active) return;
setStatus(nextStatus);
setDetail(info || null);
if (nextStatus === 'playing') {
ensurePlayback();
}
if (RETRYABLE_STATUSES.has(nextStatus)) {
scheduleRestart();
}
};
const player = new WhepPlayer({
url: sessionInfo.url,
token: sessionInfo.token,
video: videoRef.current,
onStatus: handleStatus,
});
player.start().catch((err) => {
if (!active) return;
setStatus('error');
setDetail(err.message);
scheduleRestart();
});
return () => {
active = false;
player?.stop();
};
}, [sessionInfo?.url, sessionInfo?.token, restartToken, scheduleRestart, ensurePlayback]);
const renderedStatus = !sessionInfo?.url
? 'Waiting for stream session'
: status === 'error'
? `Error: ${detail || 'unknown'}`
: detail
? `${status} (${detail})`
: status;
return (
<div className="space-y-0.5">
<div className="relative aspect-video w-full overflow-hidden bg-black">
<video ref={videoRef} muted playsInline autoPlay controls={false} className="h-full w-full object-cover" />
<div className="pointer-events-none absolute left-0 top-0 bg-black/70 px-0.5 py-0.5 text-xs font-semibold text-white">
{label}
</div>
</div>
<p className="text-xs text-slate-500">{renderedStatus}</p>
</div>
);
}
+2 -7
View File
@@ -1,6 +1,6 @@
import { useSession } from '../context/SessionContext.jsx'; import { useSession } from '../context/SessionContext.jsx';
import { useVideoRequests } from '../hooks/useVideoRequests.js'; import { useVideoRequests } from '../hooks/useVideoRequests.js';
import VideoTile from './VideoTile.jsx'; import RoomCameraFeed from './RoomCameraFeed.jsx';
function EmptyState() { function EmptyState() {
return ( return (
@@ -37,12 +37,7 @@ export default function RoomCameraPanel() {
<p className="text-lg font-semibold text-white">{camera.name || camera.id}</p> <p className="text-lg font-semibold text-white">{camera.name || camera.id}</p>
{camera.description && <p className="text-xs text-slate-500">{camera.description}</p>} {camera.description && <p className="text-xs text-slate-500">{camera.description}</p>}
</header> </header>
<VideoTile <RoomCameraFeed sessionInfo={sessionInfo} label={camera.name || camera.id} />
sessionInfo={sessionInfo}
label={camera.name || camera.id}
forceMute
showBatteryBar={false}
/>
</article> </article>
); );
})} })}
+2 -21
View File
@@ -4,14 +4,7 @@ import { WhepPlayer } from '../lib/whepPlayer.js';
const RESTART_DELAY_MS = 2000; const RESTART_DELAY_MS = 2000;
const UNMUTE_RETRY_MS = 3000; const UNMUTE_RETRY_MS = 3000;
export default function VideoTile({ export default function VideoTile({ sessionInfo, label, forceMute = false, telemetryFrame, batteryConfig }) {
sessionInfo,
label,
forceMute = false,
telemetryFrame,
batteryConfig,
showBatteryBar = true,
}) {
const videoRef = useRef(null); const videoRef = useRef(null);
const restartTimer = useRef(null); const restartTimer = useRef(null);
const unmuteTimer = useRef(null); const unmuteTimer = useRef(null);
@@ -153,14 +146,6 @@ export default function VideoTile({
? `${status} (${detail})` ? `${status} (${detail})`
: status; : status;
const renderStatusSection = () => (
<div className="panel-section space-y-0.5 text-sm">
<div className="flex items-center justify-between text-xs text-slate-400">
<span>{renderedStatus}</span>
</div>
</div>
);
return ( return (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<div className="relative w-full overflow-hidden bg-black aspect-video"> <div className="relative w-full overflow-hidden bg-black aspect-video">
@@ -175,11 +160,7 @@ export default function VideoTile({
<HudOverlay frame={telemetryFrame} label={label}/> <HudOverlay frame={telemetryFrame} label={label}/>
<OvercurrentOverlay motors={overcurrentMotors} /> <OvercurrentOverlay motors={overcurrentMotors} />
</div> </div>
{showBatteryBar ? ( <BatteryBar charge={batteryCharge} config={batteryConfig} status={renderedStatus} />
<BatteryBar charge={batteryCharge} config={batteryConfig} label={label} status={renderedStatus} />
) : (
renderStatusSection()
)}
</div> </div>
); );
} }