mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-06 15:17:37 +03:00
Merge pull request #94 from ibs0d/claude/add-camera-rotation-support-Lg2l9
Add ui.rotate support to PreviewPlayer (scrubbing preview in recordings)
This commit is contained in:
commit
fc5e59ec54
@ -33,6 +33,7 @@ type PreviewPlayerProps = {
|
|||||||
isScrubbing: boolean;
|
isScrubbing: boolean;
|
||||||
forceAspect?: number;
|
forceAspect?: number;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
rotate?: boolean;
|
||||||
onControllerReady: (controller: PreviewController) => void;
|
onControllerReady: (controller: PreviewController) => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
@ -45,6 +46,7 @@ export default function PreviewPlayer({
|
|||||||
startTime,
|
startTime,
|
||||||
isScrubbing,
|
isScrubbing,
|
||||||
isVisible = true,
|
isVisible = true,
|
||||||
|
rotate,
|
||||||
onControllerReady,
|
onControllerReady,
|
||||||
onClick,
|
onClick,
|
||||||
}: PreviewPlayerProps) {
|
}: PreviewPlayerProps) {
|
||||||
@ -69,6 +71,7 @@ export default function PreviewPlayer({
|
|||||||
isScrubbing={isScrubbing}
|
isScrubbing={isScrubbing}
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
currentHourFrame={currentHourFrame}
|
currentHourFrame={currentHourFrame}
|
||||||
|
rotate={rotate}
|
||||||
onControllerReady={onControllerReady}
|
onControllerReady={onControllerReady}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
setCurrentHourFrame={setCurrentHourFrame}
|
setCurrentHourFrame={setCurrentHourFrame}
|
||||||
@ -83,6 +86,7 @@ export default function PreviewPlayer({
|
|||||||
camera={camera}
|
camera={camera}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
startTime={startTime}
|
startTime={startTime}
|
||||||
|
rotate={rotate}
|
||||||
onControllerReady={onControllerReady}
|
onControllerReady={onControllerReady}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
setCurrentHourFrame={setCurrentHourFrame}
|
setCurrentHourFrame={setCurrentHourFrame}
|
||||||
@ -127,6 +131,7 @@ type PreviewVideoPlayerProps = {
|
|||||||
isScrubbing: boolean;
|
isScrubbing: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
currentHourFrame?: string;
|
currentHourFrame?: string;
|
||||||
|
rotate?: boolean;
|
||||||
onControllerReady: (controller: PreviewVideoController) => void;
|
onControllerReady: (controller: PreviewVideoController) => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
setCurrentHourFrame: (src: string | undefined) => void;
|
setCurrentHourFrame: (src: string | undefined) => void;
|
||||||
@ -142,6 +147,7 @@ function PreviewVideoPlayer({
|
|||||||
isScrubbing,
|
isScrubbing,
|
||||||
isVisible,
|
isVisible,
|
||||||
currentHourFrame,
|
currentHourFrame,
|
||||||
|
rotate,
|
||||||
onControllerReady,
|
onControllerReady,
|
||||||
onClick,
|
onClick,
|
||||||
setCurrentHourFrame,
|
setCurrentHourFrame,
|
||||||
@ -183,6 +189,33 @@ function PreviewVideoPlayer({
|
|||||||
controller.scrubbing = isScrubbing;
|
controller.scrubbing = isScrubbing;
|
||||||
}, [controller, isScrubbing]);
|
}, [controller, isScrubbing]);
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
|
|
||||||
const [firstLoad, setFirstLoad] = useState(true);
|
const [firstLoad, setFirstLoad] = useState(true);
|
||||||
@ -282,67 +315,103 @@ function PreviewVideoPlayer({
|
|||||||
ref={visibilityRef}
|
ref={visibilityRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full justify-center overflow-hidden rounded-lg bg-black md:rounded-2xl",
|
"relative flex w-full justify-center overflow-hidden rounded-lg bg-black md:rounded-2xl",
|
||||||
|
rotate && "h-full",
|
||||||
onClick && "cursor-pointer",
|
onClick && "cursor-pointer",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
data-camera={camera}
|
data-camera={camera}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<img
|
<div
|
||||||
className={`absolute size-full object-contain ${currentHourFrame ? "visible" : "invisible"}`}
|
ref={rotateContainerRef}
|
||||||
src={currentHourFrame}
|
className="size-full"
|
||||||
onLoad={() => {
|
style={
|
||||||
if (changeoverTimeout) {
|
rotate
|
||||||
clearTimeout(changeoverTimeout);
|
? { position: "relative" as const, overflow: "hidden" as const }
|
||||||
setChangeoverTimeout(undefined);
|
: 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%" }
|
||||||
}
|
}
|
||||||
|
|
||||||
previewRef.current?.load();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{isVisible && (
|
|
||||||
<video
|
|
||||||
ref={previewRef}
|
|
||||||
className={`absolute size-full ${currentHourFrame ? "invisible" : "visible"}`}
|
|
||||||
preload="auto"
|
|
||||||
autoPlay
|
|
||||||
playsInline
|
|
||||||
muted
|
|
||||||
disableRemotePlayback
|
|
||||||
disablePictureInPicture
|
|
||||||
onSeeked={onPreviewSeeked}
|
|
||||||
onLoadedData={() => {
|
|
||||||
if (firstLoad) {
|
|
||||||
setFirstLoad(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller) {
|
|
||||||
controller.previewReady();
|
|
||||||
} else {
|
|
||||||
previewRef.current?.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previewRef.current) {
|
|
||||||
setVideoSize([
|
|
||||||
previewRef.current.videoWidth,
|
|
||||||
previewRef.current.videoHeight,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (startTime && currentPreview) {
|
|
||||||
previewRef.current.currentTime =
|
|
||||||
startTime - currentPreview.start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{currentPreview != undefined && (
|
<img
|
||||||
<source
|
className={`absolute size-full object-contain ${currentHourFrame ? "visible" : "invisible"}`}
|
||||||
src={`${baseUrl}${currentPreview.src.substring(1)}`}
|
style={
|
||||||
type={currentPreview.type}
|
rotate
|
||||||
/>
|
? { transform: "rotate(90deg)", transformOrigin: "center center" }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
src={currentHourFrame}
|
||||||
|
onLoad={() => {
|
||||||
|
if (changeoverTimeout) {
|
||||||
|
clearTimeout(changeoverTimeout);
|
||||||
|
setChangeoverTimeout(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
previewRef.current?.load();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isVisible && (
|
||||||
|
<video
|
||||||
|
ref={previewRef}
|
||||||
|
className={`absolute size-full ${currentHourFrame ? "invisible" : "visible"}`}
|
||||||
|
style={
|
||||||
|
rotate
|
||||||
|
? { transform: "rotate(90deg)", transformOrigin: "center center" }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
preload="auto"
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
muted
|
||||||
|
disableRemotePlayback
|
||||||
|
disablePictureInPicture
|
||||||
|
onSeeked={onPreviewSeeked}
|
||||||
|
onLoadedData={() => {
|
||||||
|
if (firstLoad) {
|
||||||
|
setFirstLoad(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller) {
|
||||||
|
controller.previewReady();
|
||||||
|
} else {
|
||||||
|
previewRef.current?.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previewRef.current) {
|
||||||
|
setVideoSize([
|
||||||
|
previewRef.current.videoWidth,
|
||||||
|
previewRef.current.videoHeight,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (startTime && currentPreview) {
|
||||||
|
previewRef.current.currentTime =
|
||||||
|
startTime - currentPreview.start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentPreview != undefined && (
|
||||||
|
<source
|
||||||
|
src={`${baseUrl}${currentPreview.src.substring(1)}`}
|
||||||
|
type={currentPreview.type}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</video>
|
||||||
)}
|
)}
|
||||||
</video>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{cameraPreviews && !currentPreview && (
|
{cameraPreviews && !currentPreview && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
|
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-background_alt text-primary dark:bg-black md:rounded-2xl">
|
||||||
{t("noPreviewFoundFor", { camera: cameraName })}
|
{t("noPreviewFoundFor", { camera: cameraName })}
|
||||||
@ -452,6 +521,7 @@ type PreviewFramesPlayerProps = {
|
|||||||
camera: string;
|
camera: string;
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
startTime?: number;
|
startTime?: number;
|
||||||
|
rotate?: boolean;
|
||||||
onControllerReady: (controller: PreviewController) => void;
|
onControllerReady: (controller: PreviewController) => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
setCurrentHourFrame: (src: string) => void;
|
setCurrentHourFrame: (src: string) => void;
|
||||||
@ -461,6 +531,7 @@ function PreviewFramesPlayer({
|
|||||||
camera,
|
camera,
|
||||||
timeRange,
|
timeRange,
|
||||||
startTime,
|
startTime,
|
||||||
|
rotate,
|
||||||
setCurrentHourFrame,
|
setCurrentHourFrame,
|
||||||
onControllerReady,
|
onControllerReady,
|
||||||
onClick,
|
onClick,
|
||||||
@ -504,6 +575,33 @@ function PreviewFramesPlayer({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [imgRef, frameTimes, imgRef.current]);
|
}, [imgRef, frameTimes, imgRef.current]);
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
|
|
||||||
const [firstLoad, setFirstLoad] = useState(true);
|
const [firstLoad, setFirstLoad] = useState(true);
|
||||||
@ -555,17 +653,48 @@ function PreviewFramesPlayer({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full justify-center",
|
"relative flex w-full justify-center",
|
||||||
|
rotate && "h-full",
|
||||||
className,
|
className,
|
||||||
onClick && "cursor-pointer",
|
onClick && "cursor-pointer",
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<img
|
<div
|
||||||
ref={imgRef}
|
ref={rotateContainerRef}
|
||||||
className={`size-full rounded-lg bg-black object-contain md:rounded-2xl`}
|
className="size-full"
|
||||||
loading="lazy"
|
style={
|
||||||
onLoad={onImageLoaded}
|
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%" }
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
ref={imgRef}
|
||||||
|
className={`size-full rounded-lg bg-black object-contain md:rounded-2xl`}
|
||||||
|
style={
|
||||||
|
rotate
|
||||||
|
? { transform: "rotate(90deg)", transformOrigin: "center center" }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
loading="lazy"
|
||||||
|
onLoad={onImageLoaded}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{previewFrames?.length === 0 && (
|
{previewFrames?.length === 0 && (
|
||||||
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
|
<div className="-y-translate-1/2 align-center absolute inset-x-0 top-1/2 rounded-lg bg-background_alt text-center text-primary dark:bg-black md:rounded-2xl">
|
||||||
{t("noPreviewFoundFor", { cameraName: cameraName })}
|
{t("noPreviewFoundFor", { cameraName: cameraName })}
|
||||||
|
|||||||
@ -337,6 +337,7 @@ export default function DynamicVideoPlayer({
|
|||||||
cameraPreviews={cameraPreviews}
|
cameraPreviews={cameraPreviews}
|
||||||
startTime={startTimestamp}
|
startTime={startTimestamp}
|
||||||
isScrubbing={isScrubbing}
|
isScrubbing={isScrubbing}
|
||||||
|
rotate={rotate}
|
||||||
onControllerReady={(previewController) =>
|
onControllerReady={(previewController) =>
|
||||||
setPreviewController(previewController)
|
setPreviewController(previewController)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user