useStreamImgRef.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. import { useCallback, type MutableRefObject } from 'react';
  2. /**
  3. * A ref for a camera stream <img> that gives it its src only once React has
  4. * put it on the page, and closes the stream of every img the component drops:
  5. * unmounted, re-keyed, or hidden by a conditional.
  6. *
  7. * Pass the stream URL here and render the img WITHOUT a src prop. React sets
  8. * an element's props while it renders, before anything is committed, and the
  9. * browser opens an img's request the moment its src is set, attached or not.
  10. * A render React throws away (an interrupted or restarted one) never commits,
  11. * so no ref sees its img and nothing can close it: one camera overlay mounting
  12. * during page load opened about twenty MJPEG streams that way, each held open
  13. * on a detached img until the tab closed (#3431). Set here, the src only ever
  14. * reaches an img that is on the page.
  15. *
  16. * The src is (re)applied when the img is attached and when the URL changes,
  17. * not on other re-renders: the viewers clear it themselves to close a stream
  18. * (reconnect, restart, closing), and an unrelated re-render must not reopen it.
  19. * ``undefined`` or '' removes it, as React does for an empty src prop.
  20. *
  21. * A detached img keeps its MJPEG connection open (measured in Chromium) until
  22. * the browser closes: the backend stream, the chamber light it holds and the
  23. * /camera/stop it blocks stay with it. Removing the src closes it, also on a
  24. * detached img. Done once React's commit is over and only for an img no longer
  25. * in the page: React runs a ref's cleanup with the img still attached, and in
  26. * development (StrictMode) also for an img that stays.
  27. *
  28. * Removing the src fires no event. Setting it to '' (the viewers' reconnect and
  29. * restart code does, on the img still shown) fires error with an empty src:
  30. * error handlers must ignore an img whose src is empty or missing.
  31. *
  32. * Takes the component's own ref (kept up to date with the img shown) and the
  33. * stream URL, and returns the callback to pass as the img's ref.
  34. */
  35. export function useStreamImgRef(imgRef: MutableRefObject<HTMLImageElement | null>, src: string | undefined) {
  36. return useCallback(
  37. (img: HTMLImageElement | null) => {
  38. imgRef.current = img;
  39. if (!img) return;
  40. if (!src) {
  41. if (img.hasAttribute('src')) img.removeAttribute('src');
  42. } else if (img.getAttribute('src') !== src) {
  43. img.src = src;
  44. }
  45. return () => {
  46. if (imgRef.current === img) imgRef.current = null;
  47. queueMicrotask(() => {
  48. if (!img.isConnected) img.removeAttribute('src');
  49. });
  50. };
  51. },
  52. [imgRef, src],
  53. );
  54. }