mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-07 02:18:07 +01:00
Miscellaneous Fixes (#21005)
* update live view docs * use swr as single source of truth for searchDetail rather than maintaining a separate state, derive the selected item from swr cache. fixes websocket sync when regenerating descriptions or fetching transcriptions * fix key warning in console * don't try to fetch event from review item for audio events * update audio transcription toast wording * Add a community supported badge to specific detectors in the info summaries to better separate * Make object classification publish to tracked object update and add examples for state classification * Add item to advanced docs about tensorflow limiting * Don't show submission for in progress objects * fix for ios not reporting video dimensions on initial metadata load in testing, polling with requestAnimationFrame finds the dimensions within 2 frames * Catch jetson nvidia device tree --------- Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
@@ -572,9 +572,8 @@ export function SortTypeContent({
|
||||
className="w-full space-y-1"
|
||||
>
|
||||
{availableSortTypes.map((value) => (
|
||||
<div className="flex flex-row gap-2">
|
||||
<div key={value} className="flex flex-row gap-2">
|
||||
<RadioGroupItem
|
||||
key={value}
|
||||
value={value}
|
||||
id={`sort-${value}`}
|
||||
className={
|
||||
|
||||
@@ -42,9 +42,10 @@ export default function DetailActionsMenu({
|
||||
return `start/${startTime}/end/${endTime}`;
|
||||
}, [search]);
|
||||
|
||||
const { data: reviewItem } = useSWR<ReviewSegment>([
|
||||
`review/event/${search.id}`,
|
||||
]);
|
||||
// currently, audio event ids are not saved in review items
|
||||
const { data: reviewItem } = useSWR<ReviewSegment>(
|
||||
search.data?.type === "audio" ? null : [`review/event/${search.id}`],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
|
||||
@@ -1295,6 +1295,7 @@ function ObjectDetailsTab({
|
||||
|
||||
{search.data.type === "object" &&
|
||||
config?.plus?.enabled &&
|
||||
search.end_time != undefined &&
|
||||
search.has_snapshot && (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -94,24 +94,52 @@ export default function HlsVideoPlayer({
|
||||
const [loadedMetadata, setLoadedMetadata] = useState(false);
|
||||
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
|
||||
|
||||
const applyVideoDimensions = useCallback(
|
||||
(width: number, height: number) => {
|
||||
if (setFullResolution) {
|
||||
setFullResolution({ width, height });
|
||||
}
|
||||
setVideoDimensions({ width, height });
|
||||
if (height > 0) {
|
||||
setTallCamera(width / height < ASPECT_VERTICAL_LAYOUT);
|
||||
}
|
||||
},
|
||||
[setFullResolution],
|
||||
);
|
||||
|
||||
const handleLoadedMetadata = useCallback(() => {
|
||||
setLoadedMetadata(true);
|
||||
if (videoRef.current) {
|
||||
const width = videoRef.current.videoWidth;
|
||||
const height = videoRef.current.videoHeight;
|
||||
|
||||
if (setFullResolution) {
|
||||
setFullResolution({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
setVideoDimensions({ width, height });
|
||||
|
||||
setTallCamera(width / height < ASPECT_VERTICAL_LAYOUT);
|
||||
if (!videoRef.current) {
|
||||
return;
|
||||
}
|
||||
}, [videoRef, setFullResolution]);
|
||||
|
||||
const width = videoRef.current.videoWidth;
|
||||
const height = videoRef.current.videoHeight;
|
||||
|
||||
// iOS Safari occasionally reports 0x0 for videoWidth/videoHeight
|
||||
// Poll with requestAnimationFrame until dimensions become available (or timeout).
|
||||
if (width > 0 && height > 0) {
|
||||
applyVideoDimensions(width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 120; // ~2 seconds at 60fps
|
||||
const tryGetDims = () => {
|
||||
if (!videoRef.current) return;
|
||||
const w = videoRef.current.videoWidth;
|
||||
const h = videoRef.current.videoHeight;
|
||||
if (w > 0 && h > 0) {
|
||||
applyVideoDimensions(w, h);
|
||||
return;
|
||||
}
|
||||
if (attempts < maxAttempts) {
|
||||
attempts += 1;
|
||||
requestAnimationFrame(tryGetDims);
|
||||
}
|
||||
};
|
||||
requestAnimationFrame(tryGetDims);
|
||||
}, [videoRef, applyVideoDimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoRef.current) {
|
||||
|
||||
@@ -91,7 +91,7 @@ function MSEPlayer({
|
||||
(error: LivePlayerError, description: string = "Unknown error") => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`${camera} - MSE error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-view-faq`,
|
||||
`${camera} - MSE error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-player-error-messages`,
|
||||
);
|
||||
onError?.(error);
|
||||
},
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function WebRtcPlayer({
|
||||
(error: LivePlayerError, description: string = "Unknown error") => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`${camera} - WebRTC error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-view-faq`,
|
||||
`${camera} - WebRTC error '${error}': ${description} See the documentation: https://docs.frigate.video/configuration/live/#live-player-error-messages`,
|
||||
);
|
||||
onError?.(error);
|
||||
},
|
||||
|
||||
@@ -16,7 +16,6 @@ import ImageLoadingIndicator from "@/components/indicators/ImageLoadingIndicator
|
||||
import useImageLoaded from "@/hooks/use-image-loaded";
|
||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||
import { useTrackedObjectUpdate } from "@/api/ws";
|
||||
import { isEqual } from "lodash";
|
||||
import TimeAgo from "@/components/dynamic/TimeAgo";
|
||||
import SearchResultActions from "@/components/menu/SearchResultActions";
|
||||
import { SearchTab } from "@/components/overlay/detail/SearchDetailDialog";
|
||||
@@ -25,14 +24,12 @@ import { useTranslation } from "react-i18next";
|
||||
import { getTranslatedLabel } from "@/utils/i18n";
|
||||
|
||||
type ExploreViewProps = {
|
||||
searchDetail: SearchResult | undefined;
|
||||
setSearchDetail: (search: SearchResult | undefined) => void;
|
||||
setSimilaritySearch: (search: SearchResult) => void;
|
||||
onSelectSearch: (item: SearchResult, ctrl: boolean, page?: SearchTab) => void;
|
||||
};
|
||||
|
||||
export default function ExploreView({
|
||||
searchDetail,
|
||||
setSearchDetail,
|
||||
setSimilaritySearch,
|
||||
onSelectSearch,
|
||||
@@ -83,20 +80,6 @@ export default function ExploreView({
|
||||
}
|
||||
}, [wsUpdate, mutate]);
|
||||
|
||||
// update search detail when results change
|
||||
|
||||
useEffect(() => {
|
||||
if (searchDetail && events) {
|
||||
const updatedSearchDetail = events.find(
|
||||
(result) => result.id === searchDetail.id,
|
||||
);
|
||||
|
||||
if (updatedSearchDetail && !isEqual(updatedSearchDetail, searchDetail)) {
|
||||
setSearchDetail(updatedSearchDetail);
|
||||
}
|
||||
}
|
||||
}, [events, searchDetail, setSearchDetail]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
|
||||
|
||||
@@ -19,7 +19,6 @@ import useKeyboardListener, {
|
||||
import scrollIntoView from "scroll-into-view-if-needed";
|
||||
import InputWithTags from "@/components/input/InputWithTags";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { isEqual } from "lodash";
|
||||
import { formatDateToLocaleString } from "@/utils/dateUtil";
|
||||
import SearchThumbnailFooter from "@/components/card/SearchThumbnailFooter";
|
||||
import ExploreSettings from "@/components/settings/SearchSettings";
|
||||
@@ -213,7 +212,7 @@ export default function SearchView({
|
||||
|
||||
// detail
|
||||
|
||||
const [searchDetail, setSearchDetail] = useState<SearchResult>();
|
||||
const [selectedId, setSelectedId] = useState<string>();
|
||||
const [page, setPage] = useState<SearchTab>("snapshot");
|
||||
|
||||
// remove duplicate event ids
|
||||
@@ -229,6 +228,16 @@ export default function SearchView({
|
||||
return results;
|
||||
}, [searchResults]);
|
||||
|
||||
const searchDetail = useMemo(() => {
|
||||
if (!selectedId) return undefined;
|
||||
// summary view
|
||||
if (defaultView === "summary" && exploreEvents) {
|
||||
return exploreEvents.find((r) => r.id === selectedId);
|
||||
}
|
||||
// grid view
|
||||
return uniqueResults.find((r) => r.id === selectedId);
|
||||
}, [selectedId, uniqueResults, exploreEvents, defaultView]);
|
||||
|
||||
// search interaction
|
||||
|
||||
const [selectedObjects, setSelectedObjects] = useState<string[]>([]);
|
||||
@@ -256,7 +265,7 @@ export default function SearchView({
|
||||
}
|
||||
} else {
|
||||
setPage(page);
|
||||
setSearchDetail(item);
|
||||
setSelectedId(item.id);
|
||||
}
|
||||
},
|
||||
[selectedObjects],
|
||||
@@ -295,26 +304,12 @@ export default function SearchView({
|
||||
}
|
||||
};
|
||||
|
||||
// update search detail when results change
|
||||
|
||||
// clear selected item when search results clear
|
||||
useEffect(() => {
|
||||
if (searchDetail) {
|
||||
const results =
|
||||
defaultView === "summary" ? exploreEvents : searchResults?.flat();
|
||||
if (results) {
|
||||
const updatedSearchDetail = results.find(
|
||||
(result) => result.id === searchDetail.id,
|
||||
);
|
||||
|
||||
if (
|
||||
updatedSearchDetail &&
|
||||
!isEqual(updatedSearchDetail, searchDetail)
|
||||
) {
|
||||
setSearchDetail(updatedSearchDetail);
|
||||
}
|
||||
}
|
||||
if (!searchResults && !exploreEvents) {
|
||||
setSelectedId(undefined);
|
||||
}
|
||||
}, [searchResults, exploreEvents, searchDetail, defaultView]);
|
||||
}, [searchResults, exploreEvents]);
|
||||
|
||||
const hasExistingSearch = useMemo(
|
||||
() => searchResults != undefined || searchFilter != undefined,
|
||||
@@ -340,7 +335,7 @@ export default function SearchView({
|
||||
? results.length - 1
|
||||
: (currentIndex - 1 + results.length) % results.length;
|
||||
|
||||
setSearchDetail(results[newIndex]);
|
||||
setSelectedId(results[newIndex].id);
|
||||
}
|
||||
}, [uniqueResults, exploreEvents, searchDetail, defaultView]);
|
||||
|
||||
@@ -357,7 +352,7 @@ export default function SearchView({
|
||||
const newIndex =
|
||||
currentIndex === -1 ? 0 : (currentIndex + 1) % results.length;
|
||||
|
||||
setSearchDetail(results[newIndex]);
|
||||
setSelectedId(results[newIndex].id);
|
||||
}
|
||||
}, [uniqueResults, exploreEvents, searchDetail, defaultView]);
|
||||
|
||||
@@ -509,7 +504,7 @@ export default function SearchView({
|
||||
<SearchDetailDialog
|
||||
search={searchDetail}
|
||||
page={page}
|
||||
setSearch={setSearchDetail}
|
||||
setSearch={(item) => setSelectedId(item?.id)}
|
||||
setSearchPage={setPage}
|
||||
setSimilarity={
|
||||
searchDetail && (() => setSimilaritySearch(searchDetail))
|
||||
@@ -629,7 +624,7 @@ export default function SearchView({
|
||||
detail: boolean,
|
||||
) => {
|
||||
if (detail && selectedObjects.length == 0) {
|
||||
setSearchDetail(value);
|
||||
setSelectedId(value.id);
|
||||
} else {
|
||||
onSelectSearch(
|
||||
value,
|
||||
@@ -724,8 +719,7 @@ export default function SearchView({
|
||||
defaultView == "summary" && (
|
||||
<div className="scrollbar-container flex size-full flex-col overflow-y-auto">
|
||||
<ExploreView
|
||||
searchDetail={searchDetail}
|
||||
setSearchDetail={setSearchDetail}
|
||||
setSearchDetail={(item) => setSelectedId(item?.id)}
|
||||
setSimilaritySearch={setSimilaritySearch}
|
||||
onSelectSearch={onSelectSearch}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user