top video when tabbed out...

This commit is contained in:
legop3
2026-07-19 23:48:03 -04:00
parent 90d4f778a5
commit 0ecee03f64
9 changed files with 137 additions and 37 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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -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>
+105 -16
View File
@@ -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}
/>