mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-09 04:35:25 +03:00
Show progress bar on preview thumbnail
This commit is contained in:
parent
b2485d147c
commit
c0d2d6ba57
@ -4,12 +4,12 @@ import { useApiHost } from "@/api";
|
|||||||
import Player from "video.js/dist/types/player";
|
import Player from "video.js/dist/types/player";
|
||||||
import { isCurrentHour } from "@/utils/dateUtil";
|
import { isCurrentHour } from "@/utils/dateUtil";
|
||||||
import { isSafari } from "@/utils/browserUtil";
|
import { isSafari } from "@/utils/browserUtil";
|
||||||
|
import { ReviewSegment } from "@/types/review";
|
||||||
|
import { Slider } from "../ui/slider";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
camera: string;
|
review: ReviewSegment;
|
||||||
relevantPreview?: Preview;
|
relevantPreview?: Preview;
|
||||||
startTs: number;
|
|
||||||
eventId: string;
|
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
@ -23,17 +23,17 @@ type Preview = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function PreviewThumbnailPlayer({
|
export default function PreviewThumbnailPlayer({
|
||||||
camera,
|
review,
|
||||||
relevantPreview,
|
relevantPreview,
|
||||||
startTs,
|
|
||||||
eventId,
|
|
||||||
isMobile,
|
isMobile,
|
||||||
onClick,
|
onClick,
|
||||||
}: PreviewPlayerProps) {
|
}: PreviewPlayerProps) {
|
||||||
const playerRef = useRef<Player | null>(null);
|
const playerRef = useRef<Player | null>(null);
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [hover, setHover] = useState(false);
|
||||||
const [isInitiallyVisible, setIsInitiallyVisible] = useState(false);
|
const [isInitiallyVisible, setIsInitiallyVisible] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
|
||||||
const onPlayback = useCallback(
|
const onPlayback = useCallback(
|
||||||
(isHovered: Boolean) => {
|
(isHovered: Boolean) => {
|
||||||
@ -50,13 +50,18 @@ export default function PreviewThumbnailPlayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isHovered) {
|
if (isHovered) {
|
||||||
|
setHover(true);
|
||||||
playerRef.current.play();
|
playerRef.current.play();
|
||||||
} else {
|
} else {
|
||||||
|
setHover(false);
|
||||||
|
setProgress(0);
|
||||||
playerRef.current.pause();
|
playerRef.current.pause();
|
||||||
playerRef.current.currentTime(startTs - relevantPreview.start);
|
playerRef.current.currentTime(
|
||||||
|
review.start_time - relevantPreview.start
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[relevantPreview, startTs, playerRef]
|
[relevantPreview, review, playerRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
const autoPlayObserver = useRef<IntersectionObserver | null>();
|
const autoPlayObserver = useRef<IntersectionObserver | null>();
|
||||||
@ -111,47 +116,53 @@ export default function PreviewThumbnailPlayer({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={relevantPreview ? inViewRef : null}
|
ref={relevantPreview ? inViewRef : null}
|
||||||
className="relative w-full h-full"
|
className="relative w-full h-full cursor-pointer"
|
||||||
onMouseEnter={() => onPlayback(true)}
|
onMouseEnter={() => onPlayback(true)}
|
||||||
onMouseLeave={() => onPlayback(false)}
|
onMouseLeave={() => onPlayback(false)}
|
||||||
>
|
>
|
||||||
<PreviewContent
|
<PreviewContent
|
||||||
playerRef={playerRef}
|
playerRef={playerRef}
|
||||||
|
review={review}
|
||||||
relevantPreview={relevantPreview}
|
relevantPreview={relevantPreview}
|
||||||
isVisible={visible}
|
isVisible={visible}
|
||||||
isInitiallyVisible={isInitiallyVisible}
|
isInitiallyVisible={isInitiallyVisible}
|
||||||
startTs={startTs}
|
|
||||||
camera={camera}
|
|
||||||
eventId={eventId}
|
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
|
setProgress={setProgress}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-0 left-0 right-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none" />
|
<div className="absolute top-0 left-0 right-0 rounded-2xl z-10 w-full h-[30%] bg-gradient-to-b from-black/20 to-transparent pointer-events-none" />
|
||||||
<div className="absolute bottom-0 left-0 right-0 rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none" />
|
<div className="absolute bottom-0 left-0 right-0 rounded-2xl z-10 w-full h-[10%] bg-gradient-to-t from-black/20 to-transparent pointer-events-none" />
|
||||||
|
{hover && (
|
||||||
|
<Slider
|
||||||
|
className="absolute left-0 right-0 bottom-0 z-10"
|
||||||
|
value={[progress]}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
max={100}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreviewContentProps = {
|
type PreviewContentProps = {
|
||||||
playerRef: React.MutableRefObject<Player | null>;
|
playerRef: React.MutableRefObject<Player | null>;
|
||||||
camera: string;
|
review: ReviewSegment;
|
||||||
relevantPreview: Preview | undefined;
|
relevantPreview: Preview | undefined;
|
||||||
eventId: string;
|
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isInitiallyVisible: boolean;
|
isInitiallyVisible: boolean;
|
||||||
startTs: number;
|
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
setProgress?: (progress: number) => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
function PreviewContent({
|
function PreviewContent({
|
||||||
playerRef,
|
playerRef,
|
||||||
camera,
|
review,
|
||||||
relevantPreview,
|
relevantPreview,
|
||||||
eventId,
|
|
||||||
isVisible,
|
isVisible,
|
||||||
isInitiallyVisible,
|
isInitiallyVisible,
|
||||||
startTs,
|
|
||||||
isMobile,
|
isMobile,
|
||||||
|
setProgress,
|
||||||
onClick,
|
onClick,
|
||||||
}: PreviewContentProps) {
|
}: PreviewContentProps) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
@ -183,18 +194,18 @@ function PreviewContent({
|
|||||||
|
|
||||||
if (relevantPreview && !isVisible) {
|
if (relevantPreview && !isVisible) {
|
||||||
return <div />;
|
return <div />;
|
||||||
} else if (!relevantPreview && isCurrentHour(startTs)) {
|
} else if (!relevantPreview && isCurrentHour(review.start_time)) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className="w-full"
|
className="w-full"
|
||||||
src={`${apiHost}api/preview/${camera}/${startTs}/thumbnail.jpg`}
|
src={`${apiHost}api/preview/${review.camera}/${review.start_time}/thumbnail.jpg`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (!relevantPreview && !isCurrentHour(startTs)) {
|
} else if (!relevantPreview && !isCurrentHour(review.start_time)) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className="w-[160px]"
|
className="w-[160px]"
|
||||||
src={`${apiHost}api/events/${eventId}/thumbnail.jpg`}
|
src={`${apiHost}api/events/${review.id}/thumbnail.jpg`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -224,12 +235,31 @@ function PreviewContent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const playerStartTime = review.start_time - relevantPreview.start;
|
||||||
|
|
||||||
if (!isInitiallyVisible) {
|
if (!isInitiallyVisible) {
|
||||||
player.pause(); // autoplay + pause is required for iOS
|
player.pause(); // autoplay + pause is required for iOS
|
||||||
}
|
}
|
||||||
|
|
||||||
player.playbackRate(slowPlayack ? 2 : 8);
|
player.playbackRate(slowPlayack ? 2 : 8);
|
||||||
player.currentTime(startTs - relevantPreview.start);
|
player.currentTime(playerStartTime);
|
||||||
|
player.on("timeupdate", () => {
|
||||||
|
if (!setProgress || playerRef.current?.paused()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerProgress =
|
||||||
|
(player.currentTime() || 0) - playerStartTime;
|
||||||
|
const playerDuration = review.end_time - review.start_time;
|
||||||
|
const playerPercent = (playerProgress / playerDuration) * 100;
|
||||||
|
|
||||||
|
if (playerPercent > 100) {
|
||||||
|
playerRef.current?.pause();
|
||||||
|
setProgress(100.0);
|
||||||
|
} else {
|
||||||
|
setProgress(playerPercent);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (isMobile && onClick) {
|
if (isMobile && onClick) {
|
||||||
player.on("touchstart", handleTouchStart);
|
player.on("touchstart", handleTouchStart);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Slider = React.forwardRef<
|
const Slider = React.forwardRef<
|
||||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||||
@ -15,12 +15,11 @@ const Slider = React.forwardRef<
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
<SliderPrimitive.Track className="relative h-1 w-full grow overflow-hidden rounded-full">
|
||||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
<SliderPrimitive.Range className="absolute h-full bg-blue-500" />
|
||||||
</SliderPrimitive.Track>
|
</SliderPrimitive.Track>
|
||||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
|
||||||
</SliderPrimitive.Root>
|
</SliderPrimitive.Root>
|
||||||
))
|
));
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Slider }
|
export { Slider };
|
||||||
|
|||||||
@ -186,17 +186,15 @@ export default function Events() {
|
|||||||
<div key={value.id}>
|
<div key={value.id}>
|
||||||
<div
|
<div
|
||||||
ref={lastRow ? lastReviewRef : null}
|
ref={lastRow ? lastReviewRef : null}
|
||||||
className="relative h-[234px] rounded-2xl overflow-hidden"
|
className="relative h-[234px] rounded-lg overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: detectConfig.width / detectConfig.height,
|
aspectRatio: detectConfig.width / detectConfig.height,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PreviewThumbnailPlayer
|
<PreviewThumbnailPlayer
|
||||||
|
review={value}
|
||||||
relevantPreview={relevantPreview}
|
relevantPreview={relevantPreview}
|
||||||
camera={value.camera}
|
|
||||||
startTs={value.start_time}
|
|
||||||
isMobile={false}
|
isMobile={false}
|
||||||
eventId=""
|
|
||||||
/>
|
/>
|
||||||
{(severity == "alert" || severity == "detection") && (
|
{(severity == "alert" || severity == "detection") && (
|
||||||
<div className="absolute top-1 right-1 flex">
|
<div className="absolute top-1 right-1 flex">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user