import { useCallback, type MutableRefObject } from 'react';
/**
* A ref for a camera stream
that gives it its src only once React has
* put it on the page, and closes the stream of every img the component drops:
* unmounted, re-keyed, or hidden by a conditional.
*
* Pass the stream URL here and render the img WITHOUT a src prop. React sets
* an element's props while it renders, before anything is committed, and the
* browser opens an img's request the moment its src is set, attached or not.
* A render React throws away (an interrupted or restarted one) never commits,
* so no ref sees its img and nothing can close it: one camera overlay mounting
* during page load opened about twenty MJPEG streams that way, each held open
* on a detached img until the tab closed (#3431). Set here, the src only ever
* reaches an img that is on the page.
*
* The src is (re)applied when the img is attached and when the URL changes,
* not on other re-renders: the viewers clear it themselves to close a stream
* (reconnect, restart, closing), and an unrelated re-render must not reopen it.
* ``undefined`` or '' removes it, as React does for an empty src prop.
*
* A detached img keeps its MJPEG connection open (measured in Chromium) until
* the browser closes: the backend stream, the chamber light it holds and the
* /camera/stop it blocks stay with it. Removing the src closes it, also on a
* detached img. Done once React's commit is over and only for an img no longer
* in the page: React runs a ref's cleanup with the img still attached, and in
* development (StrictMode) also for an img that stays.
*
* Removing the src fires no event. Setting it to '' (the viewers' reconnect and
* restart code does, on the img still shown) fires error with an empty src:
* error handlers must ignore an img whose src is empty or missing.
*
* Takes the component's own ref (kept up to date with the img shown) and the
* stream URL, and returns the callback to pass as the img's ref.
*/
export function useStreamImgRef(imgRef: MutableRefObject, src: string | undefined) {
return useCallback(
(img: HTMLImageElement | null) => {
imgRef.current = img;
if (!img) return;
if (!src) {
if (img.hasAttribute('src')) img.removeAttribute('src');
} else if (img.getAttribute('src') !== src) {
img.src = src;
}
return () => {
if (imgRef.current === img) imgRef.current = null;
queueMicrotask(() => {
if (!img.isConnected) img.removeAttribute('src');
});
};
},
[imgRef, src],
);
}