diff --git a/frigate/events/external.py b/frigate/events/external.py index 6794ce4eb..46ce6f12c 100644 --- a/frigate/events/external.py +++ b/frigate/events/external.py @@ -119,7 +119,11 @@ class ExternalEventProcessor: ( self.event_camera[event_id], end_time, - {"state": ManualEventState.end, "event_id": event_id}, + { + "state": ManualEventState.end, + "event_id": event_id, + "end_time": end_time, + }, ) ) self.event_camera.pop(event_id) diff --git a/web/src/components/filter/ReviewFilterGroup.tsx b/web/src/components/filter/ReviewFilterGroup.tsx index ab597c85a..7e33c424f 100644 --- a/web/src/components/filter/ReviewFilterGroup.tsx +++ b/web/src/components/filter/ReviewFilterGroup.tsx @@ -53,6 +53,7 @@ type ReviewFilterGroupProps = { reviewSummary?: ReviewSummary; filter?: ReviewFilter; motionOnly: boolean; + filterLabels?: string[]; onUpdateFilter: (filter: ReviewFilter) => void; setMotionOnly: React.Dispatch>; }; @@ -63,12 +64,17 @@ export default function ReviewFilterGroup({ reviewSummary, filter, motionOnly, + filterLabels, onUpdateFilter, setMotionOnly, }: ReviewFilterGroupProps) { const { data: config } = useSWR("config"); const allLabels = useMemo(() => { + if (filterLabels) { + return filterLabels; + } + if (!config) { return []; } @@ -93,7 +99,7 @@ export default function ReviewFilterGroup({ }); return [...labels].sort(); - }, [config, filter]); + }, [config, filterLabels, filter]); const filterValues = useMemo( () => ({ @@ -197,6 +203,7 @@ export default function ReviewFilterGroup({ filter={filter} currentSeverity={currentSeverity} reviewSummary={reviewSummary} + allLabels={allLabels} onUpdateFilter={onUpdateFilter} // not applicable as exports are not used camera="" diff --git a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx index 7bd92807a..44ed91311 100644 --- a/web/src/components/overlay/MobileReviewSettingsDrawer.tsx +++ b/web/src/components/overlay/MobileReviewSettingsDrawer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Button } from "../ui/button"; import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa"; @@ -10,8 +10,6 @@ import { SelectSeparator } from "../ui/select"; import { ReviewFilter, ReviewSeverity, ReviewSummary } from "@/types/review"; import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { GeneralFilterContent } from "../filter/ReviewFilterGroup"; -import useSWR from "swr"; -import { FrigateConfig } from "@/types/frigateConfig"; import { toast } from "sonner"; import axios from "axios"; import SaveExportOverlay from "./SaveExportOverlay"; @@ -37,6 +35,7 @@ type MobileReviewSettingsDrawerProps = { range?: TimeRange; mode: ExportMode; reviewSummary?: ReviewSummary; + allLabels: string[]; onUpdateFilter: (filter: ReviewFilter) => void; setRange: (range: TimeRange | undefined) => void; setMode: (mode: ExportMode) => void; @@ -51,11 +50,11 @@ export default function MobileReviewSettingsDrawer({ range, mode, reviewSummary, + allLabels, onUpdateFilter, setRange, setMode, }: MobileReviewSettingsDrawerProps) { - const { data: config } = useSWR("config"); const [drawerMode, setDrawerMode] = useState("none"); // exports @@ -102,32 +101,6 @@ export default function MobileReviewSettingsDrawer({ // filters - const allLabels = useMemo(() => { - if (!config) { - return []; - } - - const labels = new Set(); - const cameras = filter?.cameras || Object.keys(config.cameras); - - cameras.forEach((camera) => { - if (camera == "birdseye") { - return; - } - const cameraConfig = config.cameras[camera]; - cameraConfig.objects.track.forEach((label) => { - labels.add(label); - }); - - if (cameraConfig.audio.enabled_in_config) { - cameraConfig.audio.listen.forEach((label) => { - labels.add(label); - }); - } - }); - - return [...labels].sort(); - }, [config, filter]); const [currentLabels, setCurrentLabels] = useState( filter?.labels, ); diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 66124631b..c52bddb59 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -14,6 +14,7 @@ import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { AxiosResponse } from "axios"; import { toast } from "sonner"; +import { useOverlayState } from "@/hooks/use-overlay-state"; // Android native hls does not seek correctly const USE_NATIVE_HLS = !isAndroid; @@ -108,8 +109,8 @@ export default function HlsVideoPlayer({ // controls const [isPlaying, setIsPlaying] = useState(true); - const [muted, setMuted] = useState(true); - const [volume, setVolume] = useState(1.0); + const [muted, setMuted] = useOverlayState("playerMuted", true); + const [volume, setVolume] = useOverlayState("playerVolume", 1.0); const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState(); const [controls, setControls] = useState(isMobile); const [controlsOpen, setControlsOpen] = useState(false); @@ -160,7 +161,7 @@ export default function HlsVideoPlayer({ fullscreen: !isIOS, }} setControlsOpen={setControlsOpen} - setMuted={setMuted} + setMuted={(muted) => setMuted(muted, true)} playbackRate={videoRef.current?.playbackRate ?? 1} hotKeys={hotKeys} onPlayPause={(play) => { @@ -226,7 +227,9 @@ export default function HlsVideoPlayer({ controls={false} playsInline muted={muted} - onVolumeChange={() => setVolume(videoRef.current?.volume ?? 1.0)} + onVolumeChange={() => + setVolume(videoRef.current?.volume ?? 1.0, true) + } onPlay={() => { setIsPlaying(true); @@ -249,7 +252,13 @@ export default function HlsVideoPlayer({ : undefined } onLoadedData={onPlayerLoaded} - onLoadedMetadata={handleLoadedMetadata} + onLoadedMetadata={() => { + handleLoadedMetadata(); + + if (videoRef.current && volume) { + videoRef.current.volume = volume; + } + }} onEnded={onClipEnded} onError={(e) => { if ( diff --git a/web/src/components/player/VideoControls.tsx b/web/src/components/player/VideoControls.tsx index d165b8b44..270d3cf9d 100644 --- a/web/src/components/player/VideoControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -1,5 +1,5 @@ import { useCallback, useMemo, useState } from "react"; -import { isSafari } from "react-device-detect"; +import { isMobileOnly, isSafari } from "react-device-detect"; import { LuPause, LuPlay } from "react-icons/lu"; import { DropdownMenu, @@ -48,6 +48,7 @@ const CONTROLS_DEFAULT: VideoControls = { fullscreen: false, }; const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16]; +const MIN_ITEMS_WRAP = 6; type VideoControlsProps = { className?: string; @@ -170,8 +171,12 @@ export default function VideoControls({ return (
feat).length > + MIN_ITEMS_WRAP && + "flex-wrap min-w-[75%]", )} > {video && features.volume && ( diff --git a/web/src/views/events/EventView.tsx b/web/src/views/events/EventView.tsx index cda7828cc..391eb9d4f 100644 --- a/web/src/views/events/EventView.tsx +++ b/web/src/views/events/EventView.tsx @@ -232,6 +232,21 @@ export default function EventView({ 100, ); + // review filter info + + const reviewLabels = useMemo(() => { + const uniqueLabels = new Set(); + + reviewItems?.all?.forEach((rev) => { + rev.data.objects.forEach((obj) => + uniqueLabels.add(obj.replace("-verified", "")), + ); + rev.data.audio.forEach((aud) => uniqueLabels.add(aud)); + }); + + return [...uniqueLabels]; + }, [reviewItems]); + if (!config) { return ; } @@ -297,8 +312,9 @@ export default function EventView({ currentSeverity={severityToggle} reviewSummary={reviewSummary} filter={filter} - onUpdateFilter={updateFilter} motionOnly={motionOnly} + filterLabels={reviewLabels} + onUpdateFilter={updateFilter} setMotionOnly={setMotionOnly} /> ) : ( diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index d49bfe01e..c619d196f 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -110,6 +110,18 @@ export function RecordingView({ () => chunkedTimeRange[selectedRangeIdx], [selectedRangeIdx, chunkedTimeRange], ); + const reviewLabels = useMemo(() => { + const uniqueLabels = new Set(); + + reviewItems?.forEach((rev) => { + rev.data.objects.forEach((obj) => + uniqueLabels.add(obj.replace("-verified", "")), + ); + rev.data.audio.forEach((aud) => uniqueLabels.add(aud)); + }); + + return [...uniqueLabels]; + }, [reviewItems]); // export @@ -402,8 +414,9 @@ export function RecordingView({ filters={["date", "general"]} reviewSummary={reviewSummary} filter={filter} - onUpdateFilter={updateFilter} motionOnly={false} + filterLabels={reviewLabels} + onUpdateFilter={updateFilter} setMotionOnly={() => {}} /> )} @@ -445,6 +458,7 @@ export function RecordingView({ latestTime={timeRange.before} mode={exportMode} range={exportRange} + allLabels={reviewLabels} onUpdateFilter={updateFilter} setRange={setExportRange} setMode={setExportMode}