Add ui.rotate support to RecordingView / HlsVideoPlayer

- 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
This commit is contained in:
Claude 2026-03-21 08:41:56 +00:00
parent 25a869eb43
commit 4a35ce1f70
No known key found for this signature in database
3 changed files with 79 additions and 7 deletions

View File

@ -59,6 +59,7 @@ type HlsVideoPlayerProps = {
camera?: string;
currentTimeOverride?: number;
transformedOverlay?: ReactNode;
rotate?: boolean;
};
export default function HlsVideoPlayer({
@ -84,6 +85,7 @@ export default function HlsVideoPlayer({
camera,
currentTimeOverride,
transformedOverlay,
rotate,
}: HlsVideoPlayerProps) {
const { t } = useTranslation("components/player");
const { data: config } = useSWR<FrigateConfig>("config");
@ -99,6 +101,33 @@ export default function HlsVideoPlayer({
const [loadedMetadata, setLoadedMetadata] = useState(false);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
// rotation support
const rotateContainerRef = useRef<HTMLDivElement>(null);
const [rotateContainerSize, setRotateContainerSize] = useState({
width: 0,
height: 0,
});
useEffect(() => {
if (!rotate) return;
const container = rotateContainerRef.current;
if (!container) return;
const updateSize = () => {
setRotateContainerSize({
width: container.clientWidth,
height: container.clientHeight,
});
};
updateSize();
const resizeObserver = new ResizeObserver(updateSize);
resizeObserver.observe(container);
return () => resizeObserver.disconnect();
}, [rotate]);
const applyVideoDimensions = useCallback(
(width: number, height: number) => {
if (setFullResolution) {
@ -388,9 +417,42 @@ export default function HlsVideoPlayer({
/>
</div>
)}
<div
ref={rotateContainerRef}
className="size-full"
style={
rotate
? { position: "relative" as const, overflow: "hidden" as const }
: undefined
}
>
<div
style={
rotate
? {
position: "absolute" as const,
top: "50%",
left: "50%",
width: rotateContainerSize.height || "100%",
height: rotateContainerSize.width || "100%",
transform: "translate(-50%, -50%)",
}
: { width: "100%", height: "100%" }
}
>
<video
ref={videoRef}
className={`size-full rounded-lg bg-black md:rounded-2xl ${loadedMetadata ? "" : "invisible"} cursor-pointer`}
style={
rotate
? {
transform: "rotate(90deg)",
transformOrigin: "center center",
width: "100%",
height: "100%",
}
: undefined
}
preload="auto"
autoPlay
controls={!frigateControls}
@ -508,6 +570,8 @@ export default function HlsVideoPlayer({
}
}}
/>
</div>
</div>
</div>
</TransformComponent>
</TransformWrapper>

View File

@ -48,6 +48,7 @@ type DynamicVideoPlayerProps = {
toggleFullscreen: () => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
transformedOverlay?: ReactNode;
rotate?: boolean;
};
export default function DynamicVideoPlayer({
className,
@ -67,6 +68,7 @@ export default function DynamicVideoPlayer({
toggleFullscreen,
containerRef,
transformedOverlay,
rotate,
}: DynamicVideoPlayerProps) {
const { t } = useTranslation(["components/player"]);
const apiHost = useApiHost();
@ -322,6 +324,7 @@ export default function DynamicVideoPlayer({
camera={contextCamera || camera}
currentTimeOverride={currentTime}
transformedOverlay={transformedOverlay}
rotate={rotate}
/>
)}
<PreviewPlayer

View File

@ -379,17 +379,21 @@ export function RecordingView({
return undefined;
}
let ratio: number;
if (cam == mainCamera && fullResolution.width && fullResolution.height) {
return fullResolution.width / fullResolution.height;
ratio = fullResolution.width / fullResolution.height;
} else {
const camera = config.cameras[cam];
if (!camera) {
return undefined;
}
ratio = camera.detect.width / camera.detect.height;
}
const camera = config.cameras[cam];
if (!camera) {
return undefined;
}
return camera.detect.width / camera.detect.height;
return camera?.ui?.rotate ? 1 / ratio : ratio;
},
[config, fullResolution, mainCamera],
);
@ -811,6 +815,7 @@ export function RecordingView({
<DynamicVideoPlayer
className={grow}
camera={mainCamera}
rotate={config?.cameras[mainCamera]?.ui?.rotate}
timeRange={currentTimeRange}
cameraPreviews={allPreviews ?? []}
startTimestamp={playbackStart}