feat(video-section): tab-visibility cleanup and currently-using subline
This commit is contained in:
@@ -64,6 +64,13 @@ export function VideoSection() {
|
|||||||
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
|
||||||
const [listOpen, setListOpen] = useState(false);
|
const [listOpen, setListOpen] = useState(false);
|
||||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||||
|
// Tracks the deviceId the browser actually picked for the active preview track.
|
||||||
|
// Drives the "Currently using: …" subline shown only in Auto mode.
|
||||||
|
const [activeDeviceId, setActiveDeviceId] = useState<string | null>(null);
|
||||||
|
// Bumped on visibilitychange to force the dual-mode preview effect to re-run
|
||||||
|
// (cleanup releases the camera light when the tab is hidden, then a fresh run
|
||||||
|
// restarts the preview when the tab becomes visible again).
|
||||||
|
const [restartTick, setRestartTick] = useState(0);
|
||||||
// Tracks mount state so async probe handlers don't write to state after unmount.
|
// Tracks mount state so async probe handlers don't write to state after unmount.
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
// Anchor for the dropdown's click-outside-to-close behaviour.
|
// Anchor for the dropdown's click-outside-to-close behaviour.
|
||||||
@@ -84,6 +91,19 @@ export function VideoSection() {
|
|||||||
return () => document.removeEventListener('mousedown', onMouseDown);
|
return () => document.removeEventListener('mousedown', onMouseDown);
|
||||||
}, [listOpen]);
|
}, [listOpen]);
|
||||||
|
|
||||||
|
// Tab-visibility cleanup: release the camera light when the tab is hidden.
|
||||||
|
// The dual-mode preview effect depends on `restartTick`; bumping it forces its
|
||||||
|
// cleanup to run (which calls fullStop()) and the effect re-runs. The effect
|
||||||
|
// itself short-circuits while document.visibilityState === 'hidden', so no new
|
||||||
|
// stream is opened until the tab is visible again.
|
||||||
|
useEffect(() => {
|
||||||
|
const onVisibility = () => {
|
||||||
|
setRestartTick((t) => t + 1);
|
||||||
|
};
|
||||||
|
document.addEventListener('visibilitychange', onVisibility);
|
||||||
|
return () => document.removeEventListener('visibilitychange', onVisibility);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Run the probe once on mount.
|
// Run the probe once on mount.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mountedRef.current = true;
|
mountedRef.current = true;
|
||||||
@@ -150,6 +170,13 @@ export function VideoSection() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (permState !== 'granted') return;
|
if (permState !== 'granted') return;
|
||||||
|
|
||||||
|
// Tab is hidden — the previous run's cleanup already fired when restartTick
|
||||||
|
// changed, releasing the camera. Don't start a new stream while hidden;
|
||||||
|
// we'll re-run again on the next visibilitychange when the tab returns.
|
||||||
|
if (document.visibilityState === 'hidden') {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
// Stop the pre-call getUserMedia stream we own. Releases the camera light.
|
// Stop the pre-call getUserMedia stream we own. Releases the camera light.
|
||||||
@@ -170,6 +197,7 @@ export function VideoSection() {
|
|||||||
const fullStop = () => {
|
const fullStop = () => {
|
||||||
stopPreCall();
|
stopPreCall();
|
||||||
detachInCall();
|
detachInCall();
|
||||||
|
setActiveDeviceId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns true iff an LK camera track was successfully attached to the preview.
|
// Returns true iff an LK camera track was successfully attached to the preview.
|
||||||
@@ -194,6 +222,7 @@ export function VideoSection() {
|
|||||||
// wrapper does not duplicate or take ownership of the track.
|
// wrapper does not duplicate or take ownership of the track.
|
||||||
videoEl.srcObject = new MediaStream([mst]);
|
videoEl.srcObject = new MediaStream([mst]);
|
||||||
videoEl.play().catch(() => {});
|
videoEl.play().catch(() => {});
|
||||||
|
setActiveDeviceId(mst.getSettings().deviceId ?? null);
|
||||||
setPreviewError(null);
|
setPreviewError(null);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
@@ -219,6 +248,8 @@ export function VideoSection() {
|
|||||||
// Autoplay errors on muted video are common and benign.
|
// Autoplay errors on muted video are common and benign.
|
||||||
videoEl.play().catch(() => {});
|
videoEl.play().catch(() => {});
|
||||||
}
|
}
|
||||||
|
const id = stream.getVideoTracks()[0]?.getSettings().deviceId ?? null;
|
||||||
|
setActiveDeviceId(id);
|
||||||
setPreviewError(null);
|
setPreviewError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
@@ -246,7 +277,7 @@ export function VideoSection() {
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
fullStop();
|
fullStop();
|
||||||
};
|
};
|
||||||
}, [permState, cameraDeviceId, isCameraOn]);
|
}, [permState, cameraDeviceId, isCameraOn, restartTick]);
|
||||||
|
|
||||||
const displayLabels = useMemo(() => buildDisplayLabels(devices), [devices]);
|
const displayLabels = useMemo(() => buildDisplayLabels(devices), [devices]);
|
||||||
|
|
||||||
@@ -368,6 +399,17 @@ export function VideoSection() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{cameraDeviceId === null && activeDeviceId && (() => {
|
||||||
|
const m = devices.find((d) => d.deviceId === activeDeviceId);
|
||||||
|
const label = m
|
||||||
|
? displayLabels.get(activeDeviceId) ?? (m.label || 'detected camera')
|
||||||
|
: 'detected camera';
|
||||||
|
return (
|
||||||
|
<div className="text-xs text-txt-tertiary mt-1">
|
||||||
|
Currently using: {label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{devices.length === 0 && (
|
{devices.length === 0 && (
|
||||||
<div className="text-xs text-txt-tertiary mt-1">No cameras detected.</div>
|
<div className="text-xs text-txt-tertiary mt-1">No cameras detected.</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user