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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title>
<script type="module" crossorigin src="/assets/index-Cpx-3dPK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CT_S70Fr.css">
<script type="module" crossorigin src="/assets/index-B_IFrOiI.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-ZQklSSJR.css">
</head>
<body>
<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 { useVideoRequests } from '../hooks/useVideoRequests.js';
import VideoTile from './VideoTile.jsx';
import RoomCameraFeed from './RoomCameraFeed.jsx';
function EmptyState() {
return (
@@ -37,12 +37,7 @@ export default function RoomCameraPanel() {
<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>}
</header>
<VideoTile
sessionInfo={sessionInfo}
label={camera.name || camera.id}
forceMute
showBatteryBar={false}
/>
<RoomCameraFeed sessionInfo={sessionInfo} label={camera.name || camera.id} />
</article>
);
})}
+2 -21
View File
@@ -4,14 +4,7 @@ import { WhepPlayer } from '../lib/whepPlayer.js';
const RESTART_DELAY_MS = 2000;
const UNMUTE_RETRY_MS = 3000;
export default function VideoTile({
sessionInfo,
label,
forceMute = false,
telemetryFrame,
batteryConfig,
showBatteryBar = true,
}) {
export default function VideoTile({ sessionInfo, label, forceMute = false, telemetryFrame, batteryConfig }) {
const videoRef = useRef(null);
const restartTimer = useRef(null);
const unmuteTimer = useRef(null);
@@ -153,14 +146,6 @@ export default function VideoTile({
? `${status} (${detail})`
: 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 (
<div className="flex flex-col gap-0.5">
<div className="relative w-full overflow-hidden bg-black aspect-video">
@@ -175,11 +160,7 @@ export default function VideoTile({
<HudOverlay frame={telemetryFrame} label={label}/>
<OvercurrentOverlay motors={overcurrentMotors} />
</div>
{showBatteryBar ? (
<BatteryBar charge={batteryCharge} config={batteryConfig} label={label} status={renderedStatus} />
) : (
renderStatusSection()
)}
<BatteryBar charge={batteryCharge} config={batteryConfig} status={renderedStatus} />
</div>
);
}