mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-05 22:57:40 +03:00
Add drag-to-zoom and always-on RelativeMove for PTZ cameras
Frontend: - Drag a rectangle on the live view to zoom into that region - Click to move to a point without zooming (unchanged behavior) - Blue rectangle overlay shown while dragging (>20px threshold) - Box expanded to camera aspect ratio before calculating zoom - TransformWrapper panning disabled when click-to-move is active - Touch support included Backend: - RelativeMove/FOV capability detection no longer requires autotracking to be enabled — works for all PTZ cameras - GetConfigurationOptions and RelativeMove request setup runs whenever the camera supports it - move_relative command accepts optional zoom parameter - Properly clears residual zoom from zeep move_request object between calls to prevent unwanted zoom on click-to-move - Includes Dockerfile.local for easy local testing by layering changes on the official Frigate image Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dc27d4ad16
commit
3a258d758a
@ -218,11 +218,10 @@ class OnvifController:
|
|||||||
move_request.ProfileToken = profile.token
|
move_request.ProfileToken = profile.token
|
||||||
self.cams[camera_name]["move_request"] = move_request
|
self.cams[camera_name]["move_request"] = move_request
|
||||||
|
|
||||||
# extra setup for autotracking cameras
|
# Probe PTZ configuration options for RelativeMove/AbsoluteMove support.
|
||||||
if (
|
# This is needed for click-to-move, drag-to-zoom, and autotracking.
|
||||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
fov_space_id = None
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
try:
|
||||||
):
|
|
||||||
request = ptz.create_type("GetConfigurationOptions")
|
request = ptz.create_type("GetConfigurationOptions")
|
||||||
request.ConfigurationToken = profile.PTZConfiguration.token
|
request.ConfigurationToken = profile.PTZConfiguration.token
|
||||||
ptz_config = await ptz.GetConfigurationOptions(request)
|
ptz_config = await ptz.GetConfigurationOptions(request)
|
||||||
@ -244,7 +243,7 @@ class OnvifController:
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# status request for autotracking and filling ptz-parameters
|
# status request for position reporting
|
||||||
status_request = ptz.create_type("GetStatus")
|
status_request = ptz.create_type("GetStatus")
|
||||||
status_request.ProfileToken = profile.token
|
status_request.ProfileToken = profile.token
|
||||||
self.cams[camera_name]["status_request"] = status_request
|
self.cams[camera_name]["status_request"] = status_request
|
||||||
@ -255,12 +254,7 @@ class OnvifController:
|
|||||||
logger.warning(f"Unable to get status from camera: {camera_name}: {e}")
|
logger.warning(f"Unable to get status from camera: {camera_name}: {e}")
|
||||||
status = None
|
status = None
|
||||||
|
|
||||||
# autotracking relative panning/tilting needs a relative zoom value set to 0
|
# Check for relative zoom support
|
||||||
# if camera supports relative movement
|
|
||||||
if (
|
|
||||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
|
||||||
!= ZoomingModeEnum.disabled
|
|
||||||
):
|
|
||||||
zoom_space_id = next(
|
zoom_space_id = next(
|
||||||
(
|
(
|
||||||
i
|
i
|
||||||
@ -272,22 +266,19 @@ class OnvifController:
|
|||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# setup relative moving request for autotracking
|
# setup relative moving request (for click-to-move, drag-to-zoom, and autotracking)
|
||||||
|
if fov_space_id is not None:
|
||||||
move_request = ptz.create_type("RelativeMove")
|
move_request = ptz.create_type("RelativeMove")
|
||||||
move_request.ProfileToken = profile.token
|
move_request.ProfileToken = profile.token
|
||||||
logger.debug(f"{camera_name}: Relative move request: {move_request}")
|
logger.debug(f"{camera_name}: Relative move request: {move_request}")
|
||||||
if move_request.Translation is None and fov_space_id is not None:
|
if move_request.Translation is None:
|
||||||
move_request.Translation = status.Position
|
move_request.Translation = status.Position
|
||||||
move_request.Translation.PanTilt.space = ptz_config["Spaces"][
|
move_request.Translation.PanTilt.space = ptz_config["Spaces"][
|
||||||
"RelativePanTiltTranslationSpace"
|
"RelativePanTiltTranslationSpace"
|
||||||
][fov_space_id]["URI"]
|
][fov_space_id]["URI"]
|
||||||
|
|
||||||
# try setting relative zoom translation space
|
# set up relative zoom translation space if supported
|
||||||
try:
|
try:
|
||||||
if (
|
|
||||||
self.config.cameras[camera_name].onvif.autotracking.zooming
|
|
||||||
!= ZoomingModeEnum.disabled
|
|
||||||
):
|
|
||||||
if zoom_space_id is not None:
|
if zoom_space_id is not None:
|
||||||
move_request.Translation.Zoom.space = ptz_config["Spaces"][
|
move_request.Translation.Zoom.space = ptz_config["Spaces"][
|
||||||
"RelativeZoomTranslationSpace"
|
"RelativeZoomTranslationSpace"
|
||||||
@ -303,15 +294,9 @@ class OnvifController:
|
|||||||
and "Zoom" in move_request["Speed"]
|
and "Zoom" in move_request["Speed"]
|
||||||
):
|
):
|
||||||
del move_request["Speed"]["Zoom"]
|
del move_request["Speed"]["Zoom"]
|
||||||
logger.debug(
|
|
||||||
f"{camera_name}: Relative move request after deleting zoom: {move_request}"
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.config.cameras[
|
|
||||||
camera_name
|
|
||||||
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Disabling autotracking zooming for {camera_name}: Relative zoom not supported. Exception: {e}"
|
f"Relative zoom not supported for {camera_name}: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if move_request.Speed is None:
|
if move_request.Speed is None:
|
||||||
@ -321,11 +306,14 @@ class OnvifController:
|
|||||||
)
|
)
|
||||||
self.cams[camera_name]["relative_move_request"] = move_request
|
self.cams[camera_name]["relative_move_request"] = move_request
|
||||||
|
|
||||||
# setup absolute moving request for autotracking zooming
|
# setup absolute moving request (for zoom positioning)
|
||||||
move_request = ptz.create_type("AbsoluteMove")
|
move_request = ptz.create_type("AbsoluteMove")
|
||||||
move_request.ProfileToken = profile.token
|
move_request.ProfileToken = profile.token
|
||||||
self.cams[camera_name]["absolute_move_request"] = move_request
|
self.cams[camera_name]["absolute_move_request"] = move_request
|
||||||
|
|
||||||
|
except (Fault, ONVIFError, TransportError, Exception) as e:
|
||||||
|
logger.debug(f"PTZ configuration options not available for {camera_name}: {e}")
|
||||||
|
|
||||||
# setup existing presets
|
# setup existing presets
|
||||||
try:
|
try:
|
||||||
presets: list[dict] = await ptz.GetPresets({"ProfileToken": profile.token})
|
presets: list[dict] = await ptz.GetPresets({"ProfileToken": profile.token})
|
||||||
@ -417,9 +405,7 @@ class OnvifController:
|
|||||||
logger.debug(f"Focus not supported for {camera_name}: {e}")
|
logger.debug(f"Focus not supported for {camera_name}: {e}")
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.config.cameras[camera_name].onvif.autotracking.enabled_in_config
|
fov_space_id is not None
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.enabled
|
|
||||||
and fov_space_id is not None
|
|
||||||
and configs.DefaultRelativePanTiltTranslationSpace is not None
|
and configs.DefaultRelativePanTiltTranslationSpace is not None
|
||||||
):
|
):
|
||||||
supported_features.append("pt-r-fov")
|
supported_features.append("pt-r-fov")
|
||||||
@ -550,8 +536,11 @@ class OnvifController:
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
"zoom-r" in self.cams[camera_name]["features"]
|
"zoom-r" in self.cams[camera_name]["features"]
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.zooming
|
and (
|
||||||
|
zoom != 0
|
||||||
|
or self.config.cameras[camera_name].onvif.autotracking.zooming
|
||||||
== ZoomingModeEnum.relative
|
== ZoomingModeEnum.relative
|
||||||
|
)
|
||||||
):
|
):
|
||||||
move_request.Speed = {
|
move_request.Speed = {
|
||||||
"PanTilt": {
|
"PanTilt": {
|
||||||
@ -561,6 +550,19 @@ class OnvifController:
|
|||||||
"Zoom": {"x": speed},
|
"Zoom": {"x": speed},
|
||||||
}
|
}
|
||||||
move_request.Translation.Zoom.x = zoom
|
move_request.Translation.Zoom.x = zoom
|
||||||
|
else:
|
||||||
|
move_request.Speed = {
|
||||||
|
"PanTilt": {
|
||||||
|
"x": speed,
|
||||||
|
"y": speed,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
# Ensure no residual zoom from previous calls
|
||||||
|
try:
|
||||||
|
if move_request.Translation is not None:
|
||||||
|
move_request.Translation.Zoom = None
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
await self.cams[camera_name]["ptz"].RelativeMove(move_request)
|
await self.cams[camera_name]["ptz"].RelativeMove(move_request)
|
||||||
|
|
||||||
@ -568,12 +570,11 @@ class OnvifController:
|
|||||||
move_request.Translation.PanTilt.x = 0
|
move_request.Translation.PanTilt.x = 0
|
||||||
move_request.Translation.PanTilt.y = 0
|
move_request.Translation.PanTilt.y = 0
|
||||||
|
|
||||||
if (
|
try:
|
||||||
"zoom-r" in self.cams[camera_name]["features"]
|
if move_request.Translation.Zoom is not None:
|
||||||
and self.config.cameras[camera_name].onvif.autotracking.zooming
|
|
||||||
== ZoomingModeEnum.relative
|
|
||||||
):
|
|
||||||
move_request.Translation.Zoom.x = 0
|
move_request.Translation.Zoom.x = 0
|
||||||
|
except (AttributeError, KeyError):
|
||||||
|
pass
|
||||||
|
|
||||||
self.cams[camera_name]["active"] = False
|
self.cams[camera_name]["active"] = False
|
||||||
|
|
||||||
@ -717,8 +718,10 @@ class OnvifController:
|
|||||||
elif command == OnvifCommandEnum.preset:
|
elif command == OnvifCommandEnum.preset:
|
||||||
await self._move_to_preset(camera_name, param)
|
await self._move_to_preset(camera_name, param)
|
||||||
elif command == OnvifCommandEnum.move_relative:
|
elif command == OnvifCommandEnum.move_relative:
|
||||||
_, pan, tilt = param.split("_")
|
parts = param.split("_")
|
||||||
await self._move_relative(camera_name, float(pan), float(tilt), 0, 1)
|
_, pan, tilt = parts[0], parts[1], parts[2]
|
||||||
|
zoom = float(parts[3]) if len(parts) > 3 else 0
|
||||||
|
await self._move_relative(camera_name, float(pan), float(tilt), zoom, 1)
|
||||||
elif command in (OnvifCommandEnum.zoom_in, OnvifCommandEnum.zoom_out):
|
elif command in (OnvifCommandEnum.zoom_in, OnvifCommandEnum.zoom_out):
|
||||||
await self._zoom(camera_name, command)
|
await self._zoom(camera_name, command)
|
||||||
elif command in (OnvifCommandEnum.focus_in, OnvifCommandEnum.focus_out):
|
elif command in (OnvifCommandEnum.focus_in, OnvifCommandEnum.focus_out):
|
||||||
|
|||||||
@ -213,45 +213,156 @@ export default function LiveCameraView({
|
|||||||
};
|
};
|
||||||
}, [audioTranscriptionState, sendTranscription]);
|
}, [audioTranscriptionState, sendTranscription]);
|
||||||
|
|
||||||
// click overlay for ptzs
|
// click/drag overlay for ptzs
|
||||||
|
|
||||||
const [clickOverlay, setClickOverlay] = useState(false);
|
const [clickOverlay, setClickOverlay] = useState(false);
|
||||||
const clickOverlayRef = useRef<HTMLDivElement>(null);
|
const clickOverlayRef = useRef<HTMLDivElement>(null);
|
||||||
const { send: sendPtz } = usePtzCommand(camera.name);
|
const { send: sendPtz } = usePtzCommand(camera.name);
|
||||||
|
|
||||||
const handleOverlayClick = useCallback(
|
// Drag-to-zoom state
|
||||||
(
|
const [dragStart, setDragStart] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null>(null);
|
||||||
|
const [dragCurrent, setDragCurrent] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null>(null);
|
||||||
|
const isDragging = dragStart !== null && dragCurrent !== null;
|
||||||
|
|
||||||
|
const getClientPos = (
|
||||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
|
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
|
||||||
) => {
|
): { x: number; y: number } | null => {
|
||||||
if (!clickOverlay) {
|
if ("TouchEvent" in window && e.nativeEvent instanceof TouchEvent) {
|
||||||
|
const touch = e.nativeEvent.touches[0] || e.nativeEvent.changedTouches[0];
|
||||||
|
if (touch) return { x: touch.clientX, y: touch.clientY };
|
||||||
|
} else if (e.nativeEvent instanceof MouseEvent) {
|
||||||
|
return { x: e.nativeEvent.clientX, y: e.nativeEvent.clientY };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOverlayMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => {
|
||||||
|
if (!clickOverlay || !clickOverlayRef.current) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const pos = getClientPos(e);
|
||||||
|
if (pos) {
|
||||||
|
setDragStart(pos);
|
||||||
|
setDragCurrent(pos);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clickOverlay],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOverlayMouseMove = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => {
|
||||||
|
if (!dragStart) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const pos = getClientPos(e);
|
||||||
|
if (pos) {
|
||||||
|
setDragCurrent(pos);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dragStart],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOverlayMouseUp = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => {
|
||||||
|
if (!clickOverlay || !clickOverlayRef.current || !dragStart) {
|
||||||
|
setDragStart(null);
|
||||||
|
setDragCurrent(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let clientX;
|
const pos = getClientPos(e);
|
||||||
let clientY;
|
if (!pos) {
|
||||||
if ("TouchEvent" in window && e.nativeEvent instanceof TouchEvent) {
|
setDragStart(null);
|
||||||
clientX = e.nativeEvent.touches[0].clientX;
|
setDragCurrent(null);
|
||||||
clientY = e.nativeEvent.touches[0].clientY;
|
return;
|
||||||
} else if (e.nativeEvent instanceof MouseEvent) {
|
|
||||||
clientX = e.nativeEvent.clientX;
|
|
||||||
clientY = e.nativeEvent.clientY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clickOverlayRef.current && clientX && clientY) {
|
|
||||||
const rect = clickOverlayRef.current.getBoundingClientRect();
|
const rect = clickOverlayRef.current.getBoundingClientRect();
|
||||||
|
const dx = Math.abs(pos.x - dragStart.x);
|
||||||
|
const dy = Math.abs(pos.y - dragStart.y);
|
||||||
|
|
||||||
const normalizedX = (clientX - rect.left) / rect.width;
|
// Minimum drag distance of 20px to distinguish from click
|
||||||
const normalizedY = (clientY - rect.top) / rect.height;
|
if (dx < 20 && dy < 20) {
|
||||||
|
// Click (not drag) — move to point without zoom
|
||||||
|
const normalizedX = (pos.x - rect.left) / rect.width;
|
||||||
|
const normalizedY = (pos.y - rect.top) / rect.height;
|
||||||
const pan = (normalizedX - 0.5) * 2;
|
const pan = (normalizedX - 0.5) * 2;
|
||||||
const tilt = (0.5 - normalizedY) * 2;
|
const tilt = (0.5 - normalizedY) * 2;
|
||||||
|
|
||||||
sendPtz(`move_relative_${pan}_${tilt}`);
|
sendPtz(`move_relative_${pan}_${tilt}`);
|
||||||
|
} else {
|
||||||
|
// Drag — zoom to rectangle
|
||||||
|
const x1 = Math.min(dragStart.x, pos.x);
|
||||||
|
const y1 = Math.min(dragStart.y, pos.y);
|
||||||
|
const x2 = Math.max(dragStart.x, pos.x);
|
||||||
|
const y2 = Math.max(dragStart.y, pos.y);
|
||||||
|
|
||||||
|
// Normalize to 0-1 within the overlay
|
||||||
|
const normX1 = (x1 - rect.left) / rect.width;
|
||||||
|
const normY1 = (y1 - rect.top) / rect.height;
|
||||||
|
const normX2 = (x2 - rect.left) / rect.width;
|
||||||
|
const normY2 = (y2 - rect.top) / rect.height;
|
||||||
|
|
||||||
|
let boxW = normX2 - normX1;
|
||||||
|
let boxH = normY2 - normY1;
|
||||||
|
|
||||||
|
// Expand box to match camera aspect ratio
|
||||||
|
const frameAspect = rect.width / rect.height;
|
||||||
|
const boxAspect = boxW / boxH;
|
||||||
|
if (boxAspect > frameAspect) {
|
||||||
|
// Box is wider than frame aspect — expand height
|
||||||
|
boxH = boxW / frameAspect;
|
||||||
|
} else {
|
||||||
|
// Box is taller — expand width
|
||||||
|
boxW = boxH * frameAspect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Center of the box
|
||||||
|
const centerX = (normX1 + normX2) / 2;
|
||||||
|
const centerY = (normY1 + normY2) / 2;
|
||||||
|
const pan = (centerX - 0.5) * 2;
|
||||||
|
const tilt = (0.5 - centerY) * 2;
|
||||||
|
|
||||||
|
// Zoom: ratio of box to frame (smaller box = more zoom)
|
||||||
|
const zoom = 1 - Math.max(boxW, boxH);
|
||||||
|
const clampedZoom = Math.max(0, Math.min(1, zoom));
|
||||||
|
|
||||||
|
// Send single command with pan, tilt, and zoom.
|
||||||
|
// The proxy translates this to a Set3DPos command for
|
||||||
|
// atomic pan+tilt+zoom in one camera movement.
|
||||||
|
sendPtz(`move_relative_${pan}_${tilt}_${clampedZoom}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragStart(null);
|
||||||
|
setDragCurrent(null);
|
||||||
},
|
},
|
||||||
[clickOverlayRef, clickOverlay, sendPtz],
|
[clickOverlayRef, clickOverlay, dragStart, sendPtz],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Calculate drag rectangle for rendering
|
||||||
|
const dragRect = React.useMemo(() => {
|
||||||
|
if (!isDragging || !clickOverlayRef.current) return null;
|
||||||
|
const dx = Math.abs(dragCurrent.x - dragStart.x);
|
||||||
|
const dy = Math.abs(dragCurrent.y - dragStart.y);
|
||||||
|
if (dx < 20 && dy < 20) return null; // Don't show rectangle for small movements
|
||||||
|
|
||||||
|
const rect = clickOverlayRef.current.getBoundingClientRect();
|
||||||
|
const x1 = Math.min(dragStart.x, dragCurrent.x) - rect.left;
|
||||||
|
const y1 = Math.min(dragStart.y, dragCurrent.y) - rect.top;
|
||||||
|
const x2 = Math.max(dragStart.x, dragCurrent.x) - rect.left;
|
||||||
|
const y2 = Math.max(dragStart.y, dragCurrent.y) - rect.top;
|
||||||
|
return {
|
||||||
|
left: x1,
|
||||||
|
top: y1,
|
||||||
|
width: x2 - x1,
|
||||||
|
height: y2 - y1,
|
||||||
|
};
|
||||||
|
}, [isDragging, dragStart, dragCurrent]);
|
||||||
|
|
||||||
// pip state
|
// pip state
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -440,7 +551,8 @@ export default function LiveCameraView({
|
|||||||
<TransformWrapper
|
<TransformWrapper
|
||||||
minScale={1.0}
|
minScale={1.0}
|
||||||
wheel={{ smoothStep: 0.005 }}
|
wheel={{ smoothStep: 0.005 }}
|
||||||
disabled={debug}
|
disabled={debug || clickOverlay}
|
||||||
|
panning={{ disabled: clickOverlay }}
|
||||||
>
|
>
|
||||||
<Toaster position="top-center" closeButton={true} />
|
<Toaster position="top-center" closeButton={true} />
|
||||||
<div
|
<div
|
||||||
@ -636,11 +748,47 @@ export default function LiveCameraView({
|
|||||||
<div
|
<div
|
||||||
className={`flex flex-col items-center justify-center ${growClassName}`}
|
className={`flex flex-col items-center justify-center ${growClassName}`}
|
||||||
ref={clickOverlayRef}
|
ref={clickOverlayRef}
|
||||||
onClick={handleOverlayClick}
|
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: constrainedAspectRatio,
|
aspectRatio: constrainedAspectRatio,
|
||||||
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{clickOverlay && (
|
||||||
|
<div
|
||||||
|
onMouseDown={handleOverlayMouseDown}
|
||||||
|
onMouseMove={handleOverlayMouseMove}
|
||||||
|
onMouseUp={handleOverlayMouseUp}
|
||||||
|
onTouchStart={handleOverlayMouseDown}
|
||||||
|
onTouchMove={handleOverlayMouseMove}
|
||||||
|
onTouchEnd={handleOverlayMouseUp}
|
||||||
|
onDragStart={(e) => e.preventDefault()}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
zIndex: 40,
|
||||||
|
cursor: "crosshair",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isDragging && dragRect && clickOverlay && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: dragRect.left,
|
||||||
|
top: dragRect.top,
|
||||||
|
width: dragRect.width,
|
||||||
|
height: dragRect.height,
|
||||||
|
border: "2px solid rgba(59, 130, 246, 0.8)",
|
||||||
|
backgroundColor: "rgba(59, 130, 246, 0.15)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
zIndex: 50,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<LivePlayer
|
<LivePlayer
|
||||||
key={camera.name}
|
key={camera.name}
|
||||||
className={`${fullscreen ? "*:rounded-none" : ""}`}
|
className={`${fullscreen ? "*:rounded-none" : ""}`}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user