mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Tweaks and fixes (#11372)
* Ensure camera activity is up to date * Persist playback rate between cameras * Add setting for default playback rate * Fix audio events saving image * Formatting * Use select component
This commit is contained in:
parent
b451d0a4f1
commit
b10ae68c1f
@ -68,7 +68,8 @@ class PendingReviewSegment:
|
|||||||
self.last_update = frame_time
|
self.last_update = frame_time
|
||||||
|
|
||||||
# thumbnail
|
# thumbnail
|
||||||
self.frame = np.zeros((THUMB_HEIGHT * 3 // 2, THUMB_WIDTH), np.uint8)
|
self._frame = np.zeros((THUMB_HEIGHT * 3 // 2, THUMB_WIDTH), np.uint8)
|
||||||
|
self.has_frame = False
|
||||||
self.frame_active_count = 0
|
self.frame_active_count = 0
|
||||||
self.frame_path = os.path.join(
|
self.frame_path = os.path.join(
|
||||||
CLIPS_DIR, f"review/thumb-{self.camera}-{self.id}.webp"
|
CLIPS_DIR, f"review/thumb-{self.camera}-{self.id}.webp"
|
||||||
@ -101,25 +102,27 @@ class PendingReviewSegment:
|
|||||||
color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
||||||
color_frame = color_frame[region[1] : region[3], region[0] : region[2]]
|
color_frame = color_frame[region[1] : region[3], region[0] : region[2]]
|
||||||
width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0])
|
width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0])
|
||||||
self.frame = cv2.resize(
|
self._frame = cv2.resize(
|
||||||
color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA
|
color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.frame is not None:
|
if self._frame is not None:
|
||||||
|
self.has_frame = True
|
||||||
cv2.imwrite(
|
cv2.imwrite(
|
||||||
self.frame_path, self.frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
|
self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
|
||||||
)
|
)
|
||||||
|
|
||||||
def save_full_frame(self, camera_config: CameraConfig, frame):
|
def save_full_frame(self, camera_config: CameraConfig, frame):
|
||||||
color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
||||||
width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0])
|
width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0])
|
||||||
self.frame = cv2.resize(
|
self._frame = cv2.resize(
|
||||||
color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA
|
color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.frame is not None:
|
if self._frame is not None:
|
||||||
|
self.has_frame = True
|
||||||
cv2.imwrite(
|
cv2.imwrite(
|
||||||
self.frame_path, self.frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
|
self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60]
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_data(self, ended: bool) -> dict:
|
def get_data(self, ended: bool) -> dict:
|
||||||
@ -194,7 +197,10 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Update segment."""
|
"""Update segment."""
|
||||||
prev_data = segment.get_data(ended=False)
|
prev_data = segment.get_data(ended=False)
|
||||||
|
|
||||||
|
if frame is not None:
|
||||||
segment.update_frame(camera_config, frame, objects)
|
segment.update_frame(camera_config, frame, objects)
|
||||||
|
|
||||||
new_data = segment.get_data(ended=False)
|
new_data = segment.get_data(ended=False)
|
||||||
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data)
|
self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data)
|
||||||
self.requestor.send_data(
|
self.requestor.send_data(
|
||||||
@ -282,33 +288,23 @@ class ReviewSegmentMaintainer(threading.Thread):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
if not segment.has_frame:
|
||||||
|
try:
|
||||||
|
frame_id = f"{camera_config.name}{frame_time}"
|
||||||
|
yuv_frame = self.frame_manager.get(
|
||||||
|
frame_id, camera_config.frame_shape_yuv
|
||||||
|
)
|
||||||
|
segment.save_full_frame(camera_config, yuv_frame)
|
||||||
|
self.frame_manager.close(frame_id)
|
||||||
|
self.update_segment(segment, camera_config, None, [])
|
||||||
|
except FileNotFoundError:
|
||||||
|
return
|
||||||
|
|
||||||
if segment.severity == SeverityEnum.alert and frame_time > (
|
if segment.severity == SeverityEnum.alert and frame_time > (
|
||||||
segment.last_update + THRESHOLD_ALERT_ACTIVITY
|
segment.last_update + THRESHOLD_ALERT_ACTIVITY
|
||||||
):
|
):
|
||||||
if segment.frame is None:
|
|
||||||
try:
|
|
||||||
frame_id = f"{camera_config.name}{frame_time}"
|
|
||||||
yuv_frame = self.frame_manager.get(
|
|
||||||
frame_id, camera_config.frame_shape_yuv
|
|
||||||
)
|
|
||||||
segment.save_full_frame(camera_config, yuv_frame)
|
|
||||||
self.frame_manager.close(frame_id)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.end_segment(segment)
|
self.end_segment(segment)
|
||||||
elif frame_time > (segment.last_update + THRESHOLD_DETECTION_ACTIVITY):
|
elif frame_time > (segment.last_update + THRESHOLD_DETECTION_ACTIVITY):
|
||||||
if segment.frame is None:
|
|
||||||
try:
|
|
||||||
frame_id = f"{camera_config.name}{frame_time}"
|
|
||||||
yuv_frame = self.frame_manager.get(
|
|
||||||
frame_id, camera_config.frame_shape_yuv
|
|
||||||
)
|
|
||||||
segment.save_full_frame(camera_config, yuv_frame)
|
|
||||||
self.frame_manager.close(frame_id)
|
|
||||||
except FileNotFoundError:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.end_segment(segment)
|
self.end_segment(segment)
|
||||||
|
|
||||||
def check_if_new_segment(
|
def check_if_new_segment(
|
||||||
|
@ -204,13 +204,26 @@ export function useFrigateStats(): { payload: FrigateStats } {
|
|||||||
return { payload: JSON.parse(payload as string) };
|
return { payload: JSON.parse(payload as string) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInitialCameraState(camera: string): {
|
export function useInitialCameraState(
|
||||||
|
camera: string,
|
||||||
|
refreshOnStart: boolean,
|
||||||
|
): {
|
||||||
payload: FrigateCameraState;
|
payload: FrigateCameraState;
|
||||||
} {
|
} {
|
||||||
const {
|
const {
|
||||||
value: { payload },
|
value: { payload },
|
||||||
} = useWs("camera_activity", "");
|
send: sendCommand,
|
||||||
|
} = useWs("camera_activity", "onConnect");
|
||||||
const data = JSON.parse(payload as string);
|
const data = JSON.parse(payload as string);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (refreshOnStart) {
|
||||||
|
sendCommand("onConnect");
|
||||||
|
}
|
||||||
|
// only refresh when onRefresh value changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [refreshOnStart]);
|
||||||
|
|
||||||
return { payload: data ? data[camera] : undefined };
|
return { payload: data ? data[camera] : undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
|
|||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useOverlayState } from "@/hooks/use-overlay-state";
|
import { useOverlayState } from "@/hooks/use-overlay-state";
|
||||||
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
|
|
||||||
// Android native hls does not seek correctly
|
// Android native hls does not seek correctly
|
||||||
const USE_NATIVE_HLS = !isAndroid;
|
const USE_NATIVE_HLS = !isAndroid;
|
||||||
@ -111,6 +112,11 @@ export default function HlsVideoPlayer({
|
|||||||
const [isPlaying, setIsPlaying] = useState(true);
|
const [isPlaying, setIsPlaying] = useState(true);
|
||||||
const [muted, setMuted] = useOverlayState("playerMuted", true);
|
const [muted, setMuted] = useOverlayState("playerMuted", true);
|
||||||
const [volume, setVolume] = useOverlayState("playerVolume", 1.0);
|
const [volume, setVolume] = useOverlayState("playerVolume", 1.0);
|
||||||
|
const [defaultPlaybackRate] = usePersistence("playbackRate", 1);
|
||||||
|
const [playbackRate, setPlaybackRate] = useOverlayState(
|
||||||
|
"playbackRate",
|
||||||
|
defaultPlaybackRate ?? 1,
|
||||||
|
);
|
||||||
const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState<NodeJS.Timeout>();
|
const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState<NodeJS.Timeout>();
|
||||||
const [controls, setControls] = useState(isMobile);
|
const [controls, setControls] = useState(isMobile);
|
||||||
const [controlsOpen, setControlsOpen] = useState(false);
|
const [controlsOpen, setControlsOpen] = useState(false);
|
||||||
@ -162,7 +168,7 @@ export default function HlsVideoPlayer({
|
|||||||
}}
|
}}
|
||||||
setControlsOpen={setControlsOpen}
|
setControlsOpen={setControlsOpen}
|
||||||
setMuted={(muted) => setMuted(muted, true)}
|
setMuted={(muted) => setMuted(muted, true)}
|
||||||
playbackRate={videoRef.current?.playbackRate ?? 1}
|
playbackRate={playbackRate ?? 1}
|
||||||
hotKeys={hotKeys}
|
hotKeys={hotKeys}
|
||||||
onPlayPause={(play) => {
|
onPlayPause={(play) => {
|
||||||
if (!videoRef.current) {
|
if (!videoRef.current) {
|
||||||
@ -184,9 +190,13 @@ export default function HlsVideoPlayer({
|
|||||||
|
|
||||||
videoRef.current.currentTime = Math.max(0, currentTime + diff);
|
videoRef.current.currentTime = Math.max(0, currentTime + diff);
|
||||||
}}
|
}}
|
||||||
onSetPlaybackRate={(rate) =>
|
onSetPlaybackRate={(rate) => {
|
||||||
videoRef.current ? (videoRef.current.playbackRate = rate) : null
|
setPlaybackRate(rate);
|
||||||
|
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.playbackRate = rate;
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
onUploadFrame={async () => {
|
onUploadFrame={async () => {
|
||||||
if (videoRef.current && onUploadFrame) {
|
if (videoRef.current && onUploadFrame) {
|
||||||
const resp = await onUploadFrame(videoRef.current.currentTime);
|
const resp = await onUploadFrame(videoRef.current.currentTime);
|
||||||
@ -255,9 +265,15 @@ export default function HlsVideoPlayer({
|
|||||||
onLoadedMetadata={() => {
|
onLoadedMetadata={() => {
|
||||||
handleLoadedMetadata();
|
handleLoadedMetadata();
|
||||||
|
|
||||||
if (videoRef.current && volume) {
|
if (videoRef.current) {
|
||||||
|
if (playbackRate) {
|
||||||
|
videoRef.current.playbackRate = playbackRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (volume) {
|
||||||
videoRef.current.volume = volume;
|
videoRef.current.volume = volume;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onEnded={onClipEnded}
|
onEnded={onClipEnded}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
|
@ -9,6 +9,17 @@ import { Button } from "../ui/button";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import { del as delData } from "idb-keyval";
|
import { del as delData } from "idb-keyval";
|
||||||
|
import { usePersistence } from "@/hooks/use-persistence";
|
||||||
|
import { isSafari } from "react-device-detect";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
} from "../ui/select";
|
||||||
|
|
||||||
|
const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16];
|
||||||
|
|
||||||
export default function General() {
|
export default function General() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
@ -38,6 +49,8 @@ export default function General() {
|
|||||||
document.title = "General Settings - Frigate";
|
document.title = "General Settings - Frigate";
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col md:flex-row size-full">
|
<div className="flex flex-col md:flex-row size-full">
|
||||||
@ -64,6 +77,36 @@ export default function General() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="flex my-2 bg-secondary" />
|
<Separator className="flex my-2 bg-secondary" />
|
||||||
|
<div className="mt-2 space-y-6">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<div className="text-md">Default Playback Rate</div>
|
||||||
|
<div className="text-sm text-muted-foreground my-2">
|
||||||
|
<p>Default playback rate for recordings playback.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
value={playbackRate?.toString()}
|
||||||
|
onValueChange={(value) => setPlaybackRate(parseFloat(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
{`${playbackRate}x`}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{PLAYBACK_RATE_DEFAULT.map((rate) => (
|
||||||
|
<SelectItem
|
||||||
|
key={rate}
|
||||||
|
className="cursor-pointer"
|
||||||
|
value={rate.toString()}
|
||||||
|
>
|
||||||
|
{rate}x
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Separator className="flex my-2 bg-secondary" />
|
||||||
<div className="mt-2 space-y-6">
|
<div className="mt-2 space-y-6">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<div className="text-md">Low Data Mode</div>
|
<div className="text-md">Low Data Mode</div>
|
||||||
|
@ -19,12 +19,16 @@ type useCameraActivityReturn = {
|
|||||||
|
|
||||||
export function useCameraActivity(
|
export function useCameraActivity(
|
||||||
camera: CameraConfig,
|
camera: CameraConfig,
|
||||||
|
refreshOnStart: boolean = true,
|
||||||
): useCameraActivityReturn {
|
): useCameraActivityReturn {
|
||||||
const [objects, setObjects] = useState<ObjectType[]>([]);
|
const [objects, setObjects] = useState<ObjectType[]>([]);
|
||||||
|
|
||||||
// init camera activity
|
// init camera activity
|
||||||
|
|
||||||
const { payload: initialCameraState } = useInitialCameraState(camera.name);
|
const { payload: initialCameraState } = useInitialCameraState(
|
||||||
|
camera.name,
|
||||||
|
refreshOnStart,
|
||||||
|
);
|
||||||
|
|
||||||
const updatedCameraState = useDeepMemo(initialCameraState);
|
const updatedCameraState = useDeepMemo(initialCameraState);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user