Replace cameras array dependency in the reset useEffect with a stable
cameraKey string (sorted camera names joined by comma). Since cameras
is recreated on every parent render, the previous effect fired on each
re-render, immediately clearing the persisted order. The key changes
only when the actual set of cameras changes.
https://claude.ai/code/session_018sRNpvMwxLgt5Un8PCjfRU
Replace in-memory fitLayoutOverride (useState) with fitCameraOrder
(useUserPersistence<string[]>) keyed as `${cameraGroup}-fit-camera-order`.
The fit layout now restores the saved camera order on load and recomputes
positions from current fitGridParams, so the order survives page reloads
and window resizes without storing screen-size-dependent coordinates.
https://claude.ai/code/session_018sRNpvMwxLgt5Un8PCjfRU
fitGridParams is recreated on every useMemo run, causing the useEffect to
fire and clear fitLayoutOverride after each swap. Switch deps to
fitGridParams?.gridUnitsPerCam and fitGridParams?.colsPerRow (primitives)
so the reset only triggers when the grid geometry actually changes.
Also remove all debug console.log added during investigation.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
Replace getBoundingClientRect+clientX/Y with newItem.x/y from react-grid-layout.
With noCompactor, newItem reports the free grid position where the element was
dropped — reliable across all rows without pixel math or scroll issues.
Also remove handleFitDrag/fitDragRef (no longer needed) and generate a full
snapBack layout so displaced items snap back correctly on no-op drops.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
In fit-to-screen mode, pass noCompactor to <Responsive> so elements
follow the mouse freely without vertical compaction pushing them down.
The swap-on-drop logic (mouse coordinates in onDragStop) stays unchanged.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
All LayoutItem args are nullable (LayoutItem | null) and event is Event
not MouseEvent — matching the actual react-grid-layout EventCallback type.
fitDragRef simplified to string | null. Cast event to MouseEvent inside
the handler to access clientX/Y for pixel-accurate slot detection.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
rgl with compactType=vertical doesn't move items horizontally, so
layoutItem.x in onDragStop stays near its origin, making horizontal
slot detection wrong.
Switch to tracking event.clientX/Y from onDrag (fitDragRef), then in
onDragStop translate the final mouse position against the .grid-layout
element's bounding rect to get pixel-accurate targetCol/targetRow.
This makes horizontal, vertical, and long-distance swaps all reliable.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
Previous approach sorted react-grid-layout's post-drag positions to infer
order, which broke for non-adjacent and horizontal moves because rgl pushes
items down instead of swapping them.
New approach:
- onDrag records which item is being dragged (draggedItemRef)
- onDragStop uses the dragged item's final x/y to compute the target slot
in our own grid, then performs a clean swap in the ordered name array
- Layout is always fully regenerated from our order array, ignoring rgl's
position arithmetic entirely
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
handleFitDragStop now sorts dragged items by position to determine new
order, then recalculates all x/y coords into a strict dense grid instead
of spreading react-grid-layout's arbitrary y values — prevents cards from
being pushed off-screen after a drag.
Also replaces LuMaximize with LuScanBarcode for the fit-to-screen button.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
In fitToScreen mode, drag is now enabled so users can reorder cameras
while in edit mode. A fitLayoutOverride state captures the new order
after each drag, normalizing w/h back to gridUnitsPerCam to prevent
size changes. The override resets automatically when the camera list or
grid parameters change.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
- PreviewPlayer: add rotate prop, pass through to PreviewVideoPlayer and
PreviewFramesPlayer
- PreviewVideoPlayer: add rotate prop + ResizeObserver; wrap <img> and
<video> in width/height-swap container with rotate(90deg) transform;
add h-full when rotate to fix height chain
- PreviewFramesPlayer: same pattern for <img> frame previews
- DynamicVideoPlayer: pass rotate={rotate} to PreviewPlayer
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
Adds a toggle button to the Live view toolbar that automatically arranges
all cameras to fit within the viewport without scrolling. Uses a brute-force
algorithm to find the optimal number of columns that maximizes camera size
while keeping all cameras visible. State persists via IndexedDB.
https://claude.ai/code/session_01Cu7YDRKZrYX3sBs6g9w2dy
TransformComponent's contentStyle had height: undefined on desktop,
so the rotate container's ResizeObserver measured height=0, causing
the inner div to get width=0 and the video to be invisible.
Adding || rotate to the height condition ensures the height chain is
intact when rotation is active, matching the isMobile path that already
set height: "100%".
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
- HlsVideoPlayer: add rotate prop; when true, wraps <video> in a
ResizeObserver-tracked container that swaps width/height and applies
rotate(90deg) transform, mirroring the MsePlayer grid-rotation logic
- DynamicVideoPlayer: thread rotate prop through to HlsVideoPlayer
- RecordingView: invert getCameraAspect ratio (1/ratio) for cameras
with ui.rotate so the outer container gets portrait proportions;
pass rotate={camera.ui?.rotate} to DynamicVideoPlayer
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
Root cause: LivePlayer's outer div has no explicit height (only w-full),
so when MsePlayer reads containerSize.height via ResizeObserver it gets 0.
With isRotatedGrid=true, MsePlayer sets the inner div width:
containerSize.height → width: 0 → video invisible.
Fix:
- Add size-full to LivePlayer className when camera.ui?.rotate, ensuring
height: 100% propagates through the chain so MsePlayer gets real dims
- Re-add cameraAspectRatio inversion (1/ratio) for portrait container
layout; now that the height chain is intact this works correctly:
portrait container → LivePlayer size-full → MsePlayer real dims → swap+rotate
https://claude.ai/code/session_01CDLHQPGpf8w44jpsG8g8nM
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
When scrubbing in RecordingView, tall cameras passed size-full (width:100% + height:100%)
to PreviewPlayer, causing the browser to ignore aspect-ratio. Replacing size-full with
w-full lets height be computed from width + aspect-ratio, preserving correct proportions.
https://claude.ai/code/session_019sUH2h6HoVswdtD7EbhAJa