mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
top video when tabbed out...
This commit is contained in:
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
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
File diff suppressed because one or more lines are too long
@@ -78,7 +78,7 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-DTsd-Jy-.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-DFlKqFdJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DCUrAu3o.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -48,6 +48,77 @@ function hasRoverAudioCapture(rover) {
|
||||
return Boolean(rover?.media?.audioCapture?.enabled && rover?.media?.audioCapture?.publishUrl);
|
||||
}
|
||||
|
||||
function useVideoVisibilityGate(enabled) {
|
||||
const containerRef = useRef(null);
|
||||
const [observedVisibility, setObservedVisibility] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
/*
|
||||
Some surfaces, notably /mini, intentionally keep video players warm while
|
||||
they are visually hidden. The explicit opt-out must bypass every part of
|
||||
this gate so those existing reconnect and rotation semantics do not change.
|
||||
*/
|
||||
if (!enabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const container = containerRef.current;
|
||||
if (!container || typeof window === 'undefined' || typeof document === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let intersectsViewport = true;
|
||||
const publishVisibility = () => {
|
||||
/*
|
||||
IntersectionObserver covers media scrolled or laid out outside the
|
||||
viewport, while the Page Visibility API covers an otherwise intersecting
|
||||
player in a background browser tab. Video should run only when both are
|
||||
true; dedicated rover audio is deliberately not connected to this state.
|
||||
*/
|
||||
setObservedVisibility(intersectsViewport && document.visibilityState !== 'hidden');
|
||||
};
|
||||
const handlePageVisibilityChange = () => publishVisibility();
|
||||
|
||||
document.addEventListener('visibilitychange', handlePageVisibilityChange);
|
||||
|
||||
if (typeof IntersectionObserver !== 'function') {
|
||||
/*
|
||||
Page visibility is still a useful safe fallback on older browsers. We
|
||||
cannot reliably infer scroll clipping without IntersectionObserver, so
|
||||
the video remains active while the page itself is visible.
|
||||
*/
|
||||
publishVisibility();
|
||||
return () => document.removeEventListener('visibilitychange', handlePageVisibilityChange);
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
intersectsViewport = Boolean(entry?.isIntersecting);
|
||||
publishVisibility();
|
||||
},
|
||||
{
|
||||
// Match room-camera behavior: do not pre-connect video before it reaches the viewport.
|
||||
root: null,
|
||||
rootMargin: '0px',
|
||||
threshold: 0,
|
||||
},
|
||||
);
|
||||
observer.observe(container);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
document.removeEventListener('visibilitychange', handlePageVisibilityChange);
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
/*
|
||||
Derive the opt-out directly instead of synchronizing it through an effect.
|
||||
That makes disabling the gate immediate and avoids an unnecessary render for
|
||||
/mini, whose behavior must remain permanently enabled.
|
||||
*/
|
||||
return { containerRef, isVisible: enabled ? observedVisibility : true };
|
||||
}
|
||||
|
||||
export default function RoverMediaPlayer({
|
||||
roverId = null,
|
||||
sessionInfo = null,
|
||||
@@ -57,6 +128,7 @@ export default function RoverMediaPlayer({
|
||||
label,
|
||||
forceMute = false,
|
||||
sensors,
|
||||
pauseVideoWhenHidden = true,
|
||||
}) {
|
||||
const assignedRoverId = useSessionSelector((state) => state.session?.assignment?.roverId ?? null);
|
||||
const effectiveRoverId = roverId ?? assignedRoverId;
|
||||
@@ -67,28 +139,44 @@ export default function RoverMediaPlayer({
|
||||
: null,
|
||||
);
|
||||
const hasAudio = hasRoverAudioCapture(rosterEntry);
|
||||
const { containerRef, isVisible: isVideoVisible } = useVideoVisibilityGate(pauseVideoWhenHidden);
|
||||
const autoVideoEnabled = videoMode ? videoMode === 'whep' : true;
|
||||
const autoAudioEnabled = hasAudio;
|
||||
const autoEntries = useMemo(() => {
|
||||
if (!effectiveRoverId) return [];
|
||||
return [
|
||||
...(autoVideoEnabled ? [{ type: 'rover', id: effectiveRoverId, key: effectiveRoverId }] : []),
|
||||
...(autoAudioEnabled
|
||||
const autoVideoEntries = useMemo(
|
||||
() =>
|
||||
effectiveRoverId && autoVideoEnabled
|
||||
? [{ type: 'rover', id: effectiveRoverId, key: effectiveRoverId }]
|
||||
: [],
|
||||
[effectiveRoverId, autoVideoEnabled],
|
||||
);
|
||||
const autoAudioEntries = useMemo(
|
||||
() =>
|
||||
effectiveRoverId && autoAudioEnabled
|
||||
? [{ type: 'rover', id: `${effectiveRoverId}-audio`, key: `${effectiveRoverId}-audio` }]
|
||||
: []),
|
||||
];
|
||||
}, [effectiveRoverId, autoVideoEnabled, autoAudioEnabled]);
|
||||
const autoSources = useVideoRequests(autoEntries, {
|
||||
enabled: Boolean(effectiveRoverId && (autoVideoEnabled || autoAudioEnabled)),
|
||||
: [],
|
||||
[effectiveRoverId, autoAudioEnabled],
|
||||
);
|
||||
/*
|
||||
Video and audio requests must be separate here. A single combined request
|
||||
list would discard the dedicated audio session whenever visibility disables
|
||||
video, even though the rover publishes audio as an independent stream.
|
||||
*/
|
||||
const autoVideoSources = useVideoRequests(autoVideoEntries, {
|
||||
enabled: Boolean(effectiveRoverId && autoVideoEnabled && isVideoVisible),
|
||||
version: mode,
|
||||
});
|
||||
const autoAudioSources = useVideoRequests(autoAudioEntries, {
|
||||
enabled: Boolean(effectiveRoverId && autoAudioEnabled),
|
||||
version: mode,
|
||||
});
|
||||
const resolvedSessionInfo =
|
||||
sessionInfo ?? (effectiveRoverId ? autoSources[effectiveRoverId] || null : null);
|
||||
sessionInfo ?? (effectiveRoverId ? autoVideoSources[effectiveRoverId] || null : null);
|
||||
const resolvedAudioSessionInfo =
|
||||
audioSessionInfo ??
|
||||
(effectiveRoverId && hasAudio ? autoSources[`${effectiveRoverId}-audio`] || null : null);
|
||||
(effectiveRoverId && hasAudio ? autoAudioSources[`${effectiveRoverId}-audio`] || null : null);
|
||||
const autoSnapshots = useRoverSnapshots(effectiveRoverId ? [effectiveRoverId] : [], {
|
||||
enabled: Boolean(effectiveRoverId && !resolvedSessionInfo?.url),
|
||||
/* Snapshot delivery is video traffic too, so it follows the same visibility gate as WHEP. */
|
||||
enabled: Boolean(effectiveRoverId && isVideoVisible && !resolvedSessionInfo?.url),
|
||||
version: mode,
|
||||
});
|
||||
const resolvedSnapshotFeed =
|
||||
@@ -317,7 +405,7 @@ export default function RoverMediaPlayer({
|
||||
}, [usingSnapshot]);
|
||||
|
||||
useEffect(() => {
|
||||
if (usingSnapshot || !resolvedSessionInfo?.url || !videoRef.current) {
|
||||
if (!isVideoVisible || usingSnapshot || !resolvedSessionInfo?.url || !videoRef.current) {
|
||||
return undefined;
|
||||
}
|
||||
let active = true;
|
||||
@@ -381,6 +469,7 @@ export default function RoverMediaPlayer({
|
||||
};
|
||||
}, [
|
||||
usingSnapshot,
|
||||
isVideoVisible,
|
||||
resolvedSessionInfo?.url,
|
||||
resolvedSessionInfo?.token,
|
||||
restartToken,
|
||||
@@ -611,7 +700,7 @@ export default function RoverMediaPlayer({
|
||||
status !== 'playing';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={containerRef} className="relative h-full w-full">
|
||||
{usingSnapshot ? (
|
||||
resolvedSnapshotFeed?.objectUrl ? (
|
||||
<img
|
||||
@@ -651,6 +740,6 @@ export default function RoverMediaPlayer({
|
||||
{renderedAudioStatus ? <span>Audio: {renderedAudioStatus}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -328,6 +328,13 @@ export default function MiniSummaryContent() {
|
||||
snapshotFeed={null}
|
||||
audioSessionInfo={isActive ? activeAudio : null}
|
||||
forceMute={!isActive}
|
||||
/*
|
||||
/mini intentionally keeps every carousel stream warm so
|
||||
rotation never waits for WHEP negotiation. Opting out
|
||||
preserves that behavior despite the shared player's
|
||||
normal offscreen-video bandwidth gate.
|
||||
*/
|
||||
pauseVideoWhenHidden={false}
|
||||
label={rover.name || rover.id}
|
||||
sensors={frames[rover.id]?.sensors || null}
|
||||
/>
|
||||
@@ -338,6 +345,8 @@ export default function MiniSummaryContent() {
|
||||
snapshotFeed={snapshotFeeds[rover.id] || null}
|
||||
audioSessionInfo={isActive ? activeAudio : null}
|
||||
forceMute={!isActive}
|
||||
/* Keep /mini snapshot rotation behavior identical to its live-video path. */
|
||||
pauseVideoWhenHidden={false}
|
||||
label={rover.name || rover.id}
|
||||
sensors={frames[rover.id]?.sensors || null}
|
||||
/>
|
||||
|
||||
@@ -100,6 +100,8 @@ export default function InfoColumn({
|
||||
videoMode={videoMode}
|
||||
snapshotFeed={snapshotFeed}
|
||||
audioSessionInfo={null}
|
||||
/* All /mini previews retain their existing always-mounted media behavior. */
|
||||
pauseVideoWhenHidden={false}
|
||||
label={rover.name || rover.id}
|
||||
sensors={frame?.sensors || null}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user