mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Ui Tweaks (#10920)
* 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:
parent
503dfba719
commit
3d43c5e811
@ -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 />} />
|
||||||
|
@ -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: {
|
||||||
|
14
web/src/components/navigation/Redirect.tsx
Normal file
14
web/src/components/navigation/Redirect.tsx
Normal 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 />;
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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) => {
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user