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" />
|
<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>
|
||||||
|
|||||||
@@ -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 { 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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user