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:
Nicolas Mowen 2024-05-14 07:38:03 -06:00 committed by GitHub
parent b451d0a4f1
commit b10ae68c1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 111 additions and 39 deletions

View File

@ -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(

View File

@ -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 };
} }

View File

@ -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) => {

View File

@ -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>

View File

@ -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);