The previous commit caused a double dimension swap for rotated cameras:
- LiveCameraView was inverting the aspect ratio (1/ratio) → portrait container
- MsePlayer was then swapping width/height again internally when
isRotatedGrid=true → video got zero/invalid dimensions, nothing visible
The MsePlayer already handles the full rotation internally via CSS variables
(transform + width/height swap). The container in LiveCameraView should
keep the original (landscape) aspect ratio, matching the grid cell behavior
in DraggableGridLayout where this works correctly.
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
- Invert cameraAspectRatio when camera.ui?.rotate is true so the
container dimensions match the rotated video (width↔height swap)
- Pass CSS variables --frigate-mse-grid-rotated and
--frigate-mse-grid-rotation to LivePlayer, enabling the existing
MsePlayer rotation/swap logic for single-camera view
- Fullscreen orientation lock works automatically: an inverted ratio
< 1 causes portrait lock for a normally-landscape camera
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
When clicking the History button on a specific camera's Live view,
append `?cameras=<camera_name>` to the review URL so the camera
filter is pre-set to that camera instead of showing "All Cameras".
The Events (Review) page already supports reading the `cameras` URL
parameter via useSearchEffect - no changes needed there.
Fixes: #12776, #16987https://claude.ai/code/session_01PnMA1HcuKsEXcvVLaXRgF1
Same pattern as DraggableGridLayout: render the dot outside
TransformComponent so it doesn't scale with pinch/zoom.
LivePlayer gets showMotionDot={false} to avoid duplicate.
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
setVolumeStates was replacing the entire state object instead of
merging, so changing one camera's volume reset all others to default.
Uses the functional update pattern to preserve existing state, matching
how toggleAudio already works.
Two fixes:
1. useCameraActivity: replace broken ternary priority with OR — "OFF"
(truthy string) was silently blocking camera_activity.motion fallback.
Now: motion === true (from camera_activity) OR detectingMotion === "ON".
2. DraggableGridLayout: render CameraMotionDot outside the zoom transform
div so the dot doesn't scale with camera zoom. LivePlayer gets
showMotionDot={false} to avoid duplicate rendering.
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
Remove the external CameraMotionDot component and showMotionDot={false}
override. The ws.ts fix (camera_activity -> motion topic sync) ensures
useCameraActivity gets fresh data, so the built-in dot in LivePlayer works.
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
autoLive ?? globalAutoLive can be undefined when useUserPersistence
hasn't hydrated yet. Change the prop type to optional boolean and
treat undefined as the default-true value (show dot unless explicitly
set to false via no-streaming mode).
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
The previous approach (useEffect → onActiveMotionChange callback →
parent state update) was unreliable: the dot only appeared if motion
was active at the moment of initial mount but did not react to
subsequent WS motion events.
Root cause: the intermediate state chain breaks because React's
useEffect batching and component re-render timing can cause the
parent state to lag behind or miss updates when motion changes after
mount.
Fix: replace the mechanism entirely with a dedicated CameraMotionDot
component that calls useCameraActivity directly. Being a proper React
component it subscribes to the {camera}/motion WS topic via
useSyncExternalStore and re-renders immediately and reliably whenever
motion state changes — no intermediate callbacks or parent state needed.
- Remove onActiveMotionChange prop from LivePlayer; add showMotionDot
boolean prop (default true) to suppress the internal dot in grid view
- Remove cameraMotionStates state and setCameraMotionStates from
DraggableGridLayout
- Add CameraMotionDot component with direct useCameraActivity hook
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
Move PlayerStats, ActivityIndicator and motion dot rendering outside the
zoom transform div in DraggableGridLayout so they are not scaled when
the user zooms with Shift+Wheel.
- Add onStatsUpdate, onLoadingChange, onActiveMotionChange callback props
to LivePlayer; when provided, suppress the internal overlay elements
and bubble state up to the parent instead
- In DraggableGridLayout, maintain per-camera overlay states and render
the three overlays as siblings to the zoom div (inside the clipping
viewport) so they remain at natural size regardless of zoom level
https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62
Set margin and containerPadding to [0,0] in ResponsiveGridLayout,
removed px-2/my-2/pb-8 from the wrapper div, and updated cellHeight
formula to not account for margins.
https://claude.ai/code/session_01THf2SuS7hLt9NgstxvKdg8
useLayoutEffect with [] deps only ran on the initial render when
gridContainerRef was null (grid div was hidden behind skeleton).
After skeleton disappeared the div mounted but useLayoutEffect never
re-ran, leaving containerWidth=0 and Responsive invisible (blank screen).
A callback ref fires every time the element mounts, so containerWidth
is always set immediately when the grid div first appears.
useResizeObserver reads ref.current during render (before commit), so on
first render ref.current is null, no observation starts, and containerWidth
stays 0 if no subsequent re-render happens (e.g. page refresh with cached
SWR data). useLayoutEffect runs after refs are committed, so ref.current
is always the real DOM element.
This fixes both the right-column overflow (no window.innerWidth fallback
needed — width is always the actual container width) and the black screen
on refresh (containerWidth is reliable before the first paint).
https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
useResizeObserver reads ref.current at render time; on page refresh with
fast SWR cache, no re-render occurs after mount so ref.current remains null
in the effect, observation never starts, and containerWidth stays 0 forever.
Add a useLayoutEffect that measures offsetWidth synchronously before paint
as a seed value (effectiveWidth = containerWidth || initialWidth). Once
ResizeObserver fires normally, containerWidth takes over. The Responsive
grid is gated on effectiveWidth > 0 so it always renders correctly on both
first load and refresh.
https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
Gate <Responsive> rendering on containerWidth > 0 so it only mounts after
ResizeObserver has measured the container. Use availableWidth directly as
the width prop (no window.innerWidth fallback) since the component now only
renders when containerWidth is known. This prevents the grid from rendering
wider than its container (which caused the rightmost column to overflow the
right edge).
https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
availableWidth starts at 0 (not null/undefined) before ResizeObserver fires.
The ?? operator passes 0 through instead of falling back to window.innerWidth,
making cellHeight negative and causing react-grid-layout to render a ~10px
container. The overflow-x-hidden div then becomes an implicit scroll container,
producing the 'cards squeezed in a small rectangle' symptom.
Changing ?? to || makes 0 trigger the window.innerWidth fallback, giving a
reasonable initial rowHeight until the real container width is measured.
https://claude.ai/code/session_01H1sqbcFmtwwsdNTJcJHJWd
Grid tiles explicitly set --frigate-mse-object-fit:fill so video stretches
to fill the card without preserving aspect ratio. The MsePlayer default
is contain, so History preview and all other contexts keep correct proportions.
https://claude.ai/code/session_01EwdaKGsrRLZ74smmCQ1MgW
* fix useImageLoaded hook running on every render
* fix volume not applying for all cameras
* Fix maximum update depth exceeded errors on Review page
- use-overlay-state: use refs for location to keep setter identity
stable across renders, preventing cascading re-render loops when
effects depend on the setter. Add Object.is bail-out guard to skip
redundant navigate calls. Move setPersistedValue after bail-out to
avoid unnecessary IndexedDB writes.
* don't try to fetch previews when motion search dialog is open
* revert unneeded changes
re-rendering was caused by the overlay state hook, not this one
* filter dicts to only use id field in sync recordings