useCameraStreamToken.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import { useEffect, useRef } from 'react';
  2. import { useQuery, useQueryClient } from '@tanstack/react-query';
  3. import { api, setStreamToken, getStreamToken, withStreamToken } from '../api/client';
  4. import { useAuth } from '../contexts/AuthContext';
  5. /**
  6. * Fetches and caches a stream token for <img>/<video> src URLs.
  7. * Stores the token globally via setStreamToken() so URL generators
  8. * in client.ts can use withStreamToken() automatically.
  9. *
  10. * Also listens for global image load errors on token-protected URLs
  11. * and automatically refreshes the token (e.g., after backend restart
  12. * invalidates in-memory tokens).
  13. *
  14. * Mount this hook once near the app root (e.g., in App.tsx or a layout component).
  15. * Components that need token-protected URLs can import withStreamToken directly.
  16. */
  17. export function useStreamTokenSync() {
  18. const { authEnabled } = useAuth();
  19. const queryClient = useQueryClient();
  20. const refreshingRef = useRef(false);
  21. const { data } = useQuery({
  22. queryKey: ['camera-stream-token'],
  23. queryFn: () => api.getCameraStreamToken(),
  24. enabled: authEnabled,
  25. staleTime: 50 * 60 * 1000, // refresh at 50 min (tokens expire at 60)
  26. refetchInterval: 50 * 60 * 1000,
  27. });
  28. useEffect(() => {
  29. setStreamToken(data?.token ?? null);
  30. return () => setStreamToken(null);
  31. }, [data?.token]);
  32. // Listen for image/video load errors on token-protected URLs.
  33. // When the backend restarts, in-memory stream tokens are lost and all
  34. // thumbnail/stream requests return 401. This handler detects that and
  35. // forces a token refresh so images recover without a page reload.
  36. useEffect(() => {
  37. if (!authEnabled) return;
  38. const handleError = (event: Event) => {
  39. const el = event.target;
  40. if (!(el instanceof HTMLImageElement || el instanceof HTMLVideoElement)) return;
  41. const src = el.src || '';
  42. const token = getStreamToken();
  43. if (!token || !src.includes(`token=${encodeURIComponent(token)}`)) return;
  44. // This image/video used our stream token and failed — token likely invalid
  45. if (refreshingRef.current) return;
  46. refreshingRef.current = true;
  47. queryClient.invalidateQueries({ queryKey: ['camera-stream-token'] });
  48. // Reset after a delay so future errors can trigger another refresh
  49. setTimeout(() => {
  50. refreshingRef.current = false;
  51. }, 5000);
  52. };
  53. // Use capture phase to catch errors before they're swallowed
  54. document.addEventListener('error', handleError, true);
  55. return () => document.removeEventListener('error', handleError, true);
  56. }, [authEnabled, queryClient]);
  57. }
  58. /**
  59. * Hook for components that need to wrap URLs with the stream token.
  60. * Returns a withToken function that appends ?token=xxx when auth is enabled.
  61. */
  62. export function useCameraStreamToken() {
  63. return { withToken: withStreamToken };
  64. }