Merge pull request #91 from ibs0d/claude/add-camera-rotation-support-Lg2l9

Add ui.rotate support to RecordingView / HlsVideoPlayer
This commit is contained in:
ibs0d 2026-03-21 19:45:49 +11:00 committed by GitHub
commit e0ee08ac15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 79 additions and 7 deletions

View File

@ -59,6 +59,7 @@ type HlsVideoPlayerProps = {
camera?: string; camera?: string;
currentTimeOverride?: number; currentTimeOverride?: number;
transformedOverlay?: ReactNode; transformedOverlay?: ReactNode;
rotate?: boolean;
}; };
export default function HlsVideoPlayer({ export default function HlsVideoPlayer({
@ -84,6 +85,7 @@ export default function HlsVideoPlayer({
camera, camera,
currentTimeOverride, currentTimeOverride,
transformedOverlay, transformedOverlay,
rotate,
}: HlsVideoPlayerProps) { }: HlsVideoPlayerProps) {
const { t } = useTranslation("components/player"); const { t } = useTranslation("components/player");
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
@ -99,6 +101,33 @@ export default function HlsVideoPlayer({
const [loadedMetadata, setLoadedMetadata] = useState(false); const [loadedMetadata, setLoadedMetadata] = useState(false);
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>(); 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( const applyVideoDimensions = useCallback(
(width: number, height: number) => { (width: number, height: number) => {
if (setFullResolution) { if (setFullResolution) {
@ -388,9 +417,42 @@ export default function HlsVideoPlayer({
/> />
</div> </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 <video
ref={videoRef} ref={videoRef}
className={`size-full rounded-lg bg-black md:rounded-2xl ${loadedMetadata ? "" : "invisible"} cursor-pointer`} 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" preload="auto"
autoPlay autoPlay
controls={!frigateControls} controls={!frigateControls}
@ -508,6 +570,8 @@ export default function HlsVideoPlayer({
} }
}} }}
/> />
</div>
</div>
</div> </div>
</TransformComponent> </TransformComponent>
</TransformWrapper> </TransformWrapper>

View File

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

View File

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