mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
room camera use its own video players
This commit is contained in:
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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user