* Cleanup live activity indicators for cameras

* Rename to reviews and redirect events to reviews

* Use reviews

* Remove plural

* Simplify recordings view

* Adjust icon
This commit is contained in:
Nicolas Mowen 2024-04-10 07:40:17 -06:00 committed by GitHub
parent 503dfba719
commit 3d43c5e811
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 131 additions and 136 deletions

View File

@ -7,6 +7,7 @@ import { isDesktop, isMobile } from "react-device-detect";
import Statusbar from "./components/Statusbar"; import Statusbar from "./components/Statusbar";
import Bottombar from "./components/navigation/Bottombar"; import Bottombar from "./components/navigation/Bottombar";
import { Suspense, lazy } from "react"; import { Suspense, lazy } from "react";
import { Redirect } from "./components/navigation/Redirect";
const Live = lazy(() => import("@/pages/Live")); const Live = lazy(() => import("@/pages/Live"));
const Events = lazy(() => import("@/pages/Events")); const Events = lazy(() => import("@/pages/Events"));
@ -35,7 +36,8 @@ function App() {
<Suspense> <Suspense>
<Routes> <Routes>
<Route path="/" element={<Live />} /> <Route path="/" element={<Live />} />
<Route path="/events" element={<Events />} /> <Route path="/events" element={<Redirect to="/review" />} />
<Route path="/review" element={<Events />} />
<Route path="/export" element={<Export />} /> <Route path="/export" element={<Export />} />
<Route path="/plus" element={<SubmitPlus />} /> <Route path="/plus" element={<SubmitPlus />} />
<Route path="/system" element={<System />} /> <Route path="/system" element={<System />} />

View File

@ -20,7 +20,7 @@ export function AnimatedEventCard({ event }: AnimatedEventCardProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const onOpenReview = useCallback(() => { const onOpenReview = useCallback(() => {
navigate("events", { navigate("review", {
state: { state: {
severity: event.severity, severity: event.severity,
recording: { recording: {

View File

@ -0,0 +1,14 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
type RedirectProps = {
to: string;
};
export function Redirect({ to }: RedirectProps) {
const navigate = useNavigate();
useEffect(() => {
navigate(to);
}, [to, navigate]);
return <div />;
}

View File

@ -19,7 +19,6 @@ const unsupportedErrorCodes = [
]; ];
type HlsVideoPlayerProps = { type HlsVideoPlayerProps = {
className: string;
children?: ReactNode; children?: ReactNode;
videoRef: MutableRefObject<HTMLVideoElement | null>; videoRef: MutableRefObject<HTMLVideoElement | null>;
visible: boolean; visible: boolean;
@ -31,7 +30,6 @@ type HlsVideoPlayerProps = {
onPlaying?: () => void; onPlaying?: () => void;
}; };
export default function HlsVideoPlayer({ export default function HlsVideoPlayer({
className,
children, children,
videoRef, videoRef,
visible, visible,
@ -91,116 +89,118 @@ export default function HlsVideoPlayer({
return ( return (
<TransformWrapper minScale={1.0}> <TransformWrapper minScale={1.0}>
<div <TransformComponent
className={`relative ${className ?? ""} ${visible ? "visible" : "hidden"}`} wrapperStyle={{
onMouseOver={ position: "relative",
isDesktop display: visible ? undefined : "none",
? () => { width: "100%",
setControls(true); height: "100%",
} }}
: undefined contentStyle={{
} width: "100%",
onMouseOut={ height: isMobile ? "100%" : undefined,
isDesktop }}
? () => {
setControls(controlsOpen);
}
: undefined
}
onClick={isDesktop ? undefined : () => setControls(!controls)}
> >
<TransformComponent <video
wrapperStyle={{ ref={videoRef}
width: "100%", className={`size-full bg-black rounded-2xl ${loadedMetadata ? "" : "invisible"}`}
height: "100%", preload="auto"
}} autoPlay
contentStyle={{ controls={false}
width: "100%", playsInline
height: isMobile ? "100%" : undefined, muted
}} onPlay={() => {
> setIsPlaying(true);
<video
ref={videoRef}
className={`size-full bg-black rounded-2xl ${loadedMetadata ? "" : "invisible"}`}
preload="auto"
autoPlay
controls={false}
playsInline
muted
onPlay={() => {
setIsPlaying(true);
if (isMobile) { if (isMobile) {
setControls(true); setControls(true);
setMobileCtrlTimeout( setMobileCtrlTimeout(setTimeout(() => setControls(false), 4000));
setTimeout(() => setControls(false), 4000),
);
}
}}
onPlaying={onPlaying}
onPause={() => {
setIsPlaying(false);
if (isMobile && mobileCtrlTimeout) {
clearTimeout(mobileCtrlTimeout);
}
}}
onTimeUpdate={() =>
onTimeUpdate && videoRef.current
? onTimeUpdate(videoRef.current.currentTime)
: undefined
}
onLoadedData={onPlayerLoaded}
onLoadedMetadata={() => setLoadedMetadata(true)}
onEnded={onClipEnded}
onError={(e) => {
if (
!hlsRef.current &&
// @ts-expect-error code does exist
unsupportedErrorCodes.includes(e.target.error.code) &&
videoRef.current
) {
setLoadedMetadata(false);
setUseHlsCompat(true);
}
}}
/>
</TransformComponent>
<VideoControls
className="absolute bottom-5 left-1/2 -translate-x-1/2"
video={videoRef.current}
isPlaying={isPlaying}
show={controls}
controlsOpen={controlsOpen}
setControlsOpen={setControlsOpen}
playbackRate={videoRef.current?.playbackRate ?? 1}
hotKeys={hotKeys}
onPlayPause={(play) => {
if (!videoRef.current) {
return;
}
if (play) {
videoRef.current.play();
} else {
videoRef.current.pause();
} }
}} }}
onSeek={(diff) => { onPlaying={onPlaying}
const currentTime = videoRef.current?.currentTime; onPause={() => {
setIsPlaying(false);
if (!videoRef.current || !currentTime) { if (isMobile && mobileCtrlTimeout) {
return; clearTimeout(mobileCtrlTimeout);
} }
videoRef.current.currentTime = Math.max(0, currentTime + diff);
}} }}
onSetPlaybackRate={(rate) => onTimeUpdate={() =>
videoRef.current ? (videoRef.current.playbackRate = rate) : null onTimeUpdate && videoRef.current
? onTimeUpdate(videoRef.current.currentTime)
: undefined
} }
onLoadedData={onPlayerLoaded}
onLoadedMetadata={() => setLoadedMetadata(true)}
onEnded={onClipEnded}
onError={(e) => {
if (
!hlsRef.current &&
// @ts-expect-error code does exist
unsupportedErrorCodes.includes(e.target.error.code) &&
videoRef.current
) {
setLoadedMetadata(false);
setUseHlsCompat(true);
}
}}
/> />
{children} <div
</div> className="absolute inset-0"
onMouseOver={
isDesktop
? () => {
setControls(true);
}
: undefined
}
onMouseOut={
isDesktop
? () => {
setControls(controlsOpen);
}
: undefined
}
onClick={isDesktop ? undefined : () => setControls(!controls)}
>
<div className={`size-full relative ${visible ? "" : "hidden"}`}>
<VideoControls
className="absolute bottom-5 left-1/2 -translate-x-1/2"
video={videoRef.current}
isPlaying={isPlaying}
show={controls}
controlsOpen={controlsOpen}
setControlsOpen={setControlsOpen}
playbackRate={videoRef.current?.playbackRate ?? 1}
hotKeys={hotKeys}
onPlayPause={(play) => {
if (!videoRef.current) {
return;
}
if (play) {
videoRef.current.play();
} else {
videoRef.current.pause();
}
}}
onSeek={(diff) => {
const currentTime = videoRef.current?.currentTime;
if (!videoRef.current || !currentTime) {
return;
}
videoRef.current.currentTime = Math.max(0, currentTime + diff);
}}
onSetPlaybackRate={(rate) =>
videoRef.current ? (videoRef.current.playbackRate = rate) : null
}
/>
{children}
</div>
</div>
</TransformComponent>
</TransformWrapper> </TransformWrapper>
); );
} }

View File

@ -7,10 +7,8 @@ import MSEPlayer from "./MsePlayer";
import JSMpegPlayer from "./JSMpegPlayer"; import JSMpegPlayer from "./JSMpegPlayer";
import { MdCircle } from "react-icons/md"; import { MdCircle } from "react-icons/md";
import { useCameraActivity } from "@/hooks/use-camera-activity"; import { useCameraActivity } from "@/hooks/use-camera-activity";
import { useRecordingsState } from "@/api/ws";
import { LivePlayerMode } from "@/types/live"; import { LivePlayerMode } from "@/types/live";
import useCameraLiveMode from "@/hooks/use-camera-live-mode"; import useCameraLiveMode from "@/hooks/use-camera-live-mode";
import CameraActivityIndicator from "../indicators/CameraActivityIndicator";
type LivePlayerProps = { type LivePlayerProps = {
cameraRef?: (ref: HTMLDivElement | null) => void; cameraRef?: (ref: HTMLDivElement | null) => void;
@ -41,8 +39,7 @@ export default function LivePlayer({
}: LivePlayerProps) { }: LivePlayerProps) {
// camera activity // camera activity
const { activeMotion, activeAudio, activeTracking } = const { activeMotion, activeTracking } = useCameraActivity(cameraConfig);
useCameraActivity(cameraConfig);
const cameraActive = useMemo( const cameraActive = useMemo(
() => () =>
@ -72,8 +69,6 @@ export default function LivePlayer({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [cameraActive, liveReady]); }, [cameraActive, liveReady]);
const { payload: recording } = useRecordingsState(cameraConfig.name);
// camera still state // camera still state
const stillReloadInterval = useMemo(() => { const stillReloadInterval = useMemo(() => {
@ -171,15 +166,8 @@ export default function LivePlayer({
/> />
</div> </div>
<div className="absolute right-2 bottom-2 w-[40px]">
{(activeMotion ||
(cameraConfig.audio.enabled_in_config && activeAudio)) && (
<CameraActivityIndicator />
)}
</div>
<div className="absolute right-2 top-2 size-4"> <div className="absolute right-2 top-2 size-4">
{recording == "ON" && ( {activeMotion && (
<MdCircle className="size-2 drop-shadow-md shadow-danger text-danger animate-pulse" /> <MdCircle className="size-2 drop-shadow-md shadow-danger text-danger animate-pulse" />
)} )}
</div> </div>

View File

@ -145,7 +145,7 @@ export default function VideoControls({
className={`px-4 py-2 flex justify-between items-center gap-8 text-primary z-50 bg-background/60 rounded-lg ${className ?? ""}`} className={`px-4 py-2 flex justify-between items-center gap-8 text-primary z-50 bg-background/60 rounded-lg ${className ?? ""}`}
> >
{video && features.volume && ( {video && features.volume && (
<div className="flex justify-normal items-center gap-2"> <div className="flex justify-normal items-center gap-2 cursor-pointer">
<VolumeIcon <VolumeIcon
className="size-5" className="size-5"
onClick={(e: React.MouseEvent) => { onClick={(e: React.MouseEvent) => {

View File

@ -150,7 +150,6 @@ export default function DynamicVideoPlayer({
return ( return (
<> <>
<HlsVideoPlayer <HlsVideoPlayer
className={className ?? ""}
videoRef={playerRef} videoRef={playerRef}
visible={!(isScrubbing || isLoading)} visible={!(isScrubbing || isLoading)}
currentSource={source} currentSource={source}

View File

@ -1,8 +1,4 @@
import { import { useFrigateEvents, useMotionActivity } from "@/api/ws";
useAudioActivity,
useFrigateEvents,
useMotionActivity,
} from "@/api/ws";
import { CameraConfig } from "@/types/frigateConfig"; import { CameraConfig } from "@/types/frigateConfig";
import { MotionData, ReviewSegment } from "@/types/review"; import { MotionData, ReviewSegment } from "@/types/review";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@ -11,7 +7,6 @@ import { useTimelineUtils } from "./use-timeline-utils";
type useCameraActivityReturn = { type useCameraActivityReturn = {
activeTracking: boolean; activeTracking: boolean;
activeMotion: boolean; activeMotion: boolean;
activeAudio: boolean;
}; };
export function useCameraActivity( export function useCameraActivity(
@ -25,7 +20,6 @@ export function useCameraActivity(
const { payload: detectingMotion } = useMotionActivity(camera.name); const { payload: detectingMotion } = useMotionActivity(camera.name);
const { payload: event } = useFrigateEvents(); const { payload: event } = useFrigateEvents();
const { payload: audioRms } = useAudioActivity(camera.name);
useEffect(() => { useEffect(() => {
if (!event) { if (!event) {
@ -63,9 +57,6 @@ export function useCameraActivity(
return { return {
activeTracking: hasActiveObjects, activeTracking: hasActiveObjects,
activeMotion: detectingMotion == "ON", activeMotion: detectingMotion == "ON",
activeAudio: camera.audio.enabled_in_config
? audioRms >= camera.audio.min_volume
: false,
}; };
} }

View File

@ -1,6 +1,7 @@
import Logo from "@/components/Logo"; import Logo from "@/components/Logo";
import { FaCompactDisc, FaFlag, FaVideo } from "react-icons/fa"; import { FaCompactDisc, FaVideo } from "react-icons/fa";
import { LuConstruction } from "react-icons/lu"; import { LuConstruction } from "react-icons/lu";
import { MdVideoLibrary } from "react-icons/md";
export const navbarLinks = [ export const navbarLinks = [
{ {
@ -11,9 +12,9 @@ export const navbarLinks = [
}, },
{ {
id: 2, id: 2,
icon: FaFlag, icon: MdVideoLibrary,
title: "Events", title: "Review",
url: "/events", url: "/review",
}, },
{ {
id: 3, id: 3,

View File

@ -235,7 +235,7 @@ export default function LiveCameraView({ camera }: LiveCameraViewProps) {
className="flex items-center gap-2.5 rounded-lg" className="flex items-center gap-2.5 rounded-lg"
size="sm" size="sm"
onClick={() => { onClick={() => {
navigate("events", { navigate("review", {
state: { state: {
severity: "alert", severity: "alert",
recording: { recording: {