mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-07 19:55:26 +03:00
Use poster image for preview on video player instead of using separate image view
This commit is contained in:
parent
d09e142b22
commit
2ac3173b4a
@ -6,6 +6,7 @@ import useSWR from "swr";
|
|||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import VideoPlayer from "../player/VideoPlayer";
|
import VideoPlayer from "../player/VideoPlayer";
|
||||||
import { Card } from "../ui/card";
|
import { Card } from "../ui/card";
|
||||||
|
import { useApiHost } from "@/api";
|
||||||
|
|
||||||
type TimelineItemCardProps = {
|
type TimelineItemCardProps = {
|
||||||
timeline: Timeline;
|
timeline: Timeline;
|
||||||
@ -18,35 +19,44 @@ export default function TimelineItemCard({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}: TimelineItemCardProps) {
|
}: TimelineItemCardProps) {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="relative m-2 flex w-full h-32 cursor-pointer" onClick={onSelect}>
|
<Card
|
||||||
|
className="relative m-2 flex w-full h-32 cursor-pointer"
|
||||||
|
onClick={onSelect}
|
||||||
|
>
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
{relevantPreview && (
|
<VideoPlayer
|
||||||
<VideoPlayer
|
options={{
|
||||||
options={{
|
preload: "auto",
|
||||||
preload: "auto",
|
height: "114",
|
||||||
height: "114",
|
width: "202",
|
||||||
width: "202",
|
autoplay: true,
|
||||||
autoplay: true,
|
controls: false,
|
||||||
controls: false,
|
fluid: false,
|
||||||
fluid: false,
|
muted: true,
|
||||||
muted: true,
|
loadingSpinner: false,
|
||||||
loadingSpinner: false,
|
poster: relevantPreview
|
||||||
sources: [
|
? ""
|
||||||
{
|
: `${apiHost}api/preview/${timeline.camera}/${timeline.timestamp}/thumbnail.jpg`,
|
||||||
src: `${relevantPreview.src}`,
|
sources: relevantPreview
|
||||||
type: "video/mp4",
|
? [
|
||||||
},
|
{
|
||||||
],
|
src: `${relevantPreview.src}`,
|
||||||
}}
|
type: "video/mp4",
|
||||||
seekOptions={{}}
|
},
|
||||||
onReady={(player) => {
|
]
|
||||||
|
: [],
|
||||||
|
}}
|
||||||
|
seekOptions={{}}
|
||||||
|
onReady={(player) => {
|
||||||
|
if (relevantPreview) {
|
||||||
player.pause(); // autoplay + pause is required for iOS
|
player.pause(); // autoplay + pause is required for iOS
|
||||||
player.currentTime(timeline.timestamp - relevantPreview.start);
|
player.currentTime(timeline.timestamp - relevantPreview.start);
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
)}
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
<div className="capitalize font-semibold text-sm">
|
<div className="capitalize font-semibold text-sm">
|
||||||
|
|||||||
@ -14,6 +14,10 @@ export default function TimelineEventOverlay({
|
|||||||
timeline,
|
timeline,
|
||||||
cameraConfig,
|
cameraConfig,
|
||||||
}: TimelineEventOverlayProps) {
|
}: TimelineEventOverlayProps) {
|
||||||
|
if (!timeline.data.box) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const boxLeftEdge = Math.round(timeline.data.box[0] * 100);
|
const boxLeftEdge = Math.round(timeline.data.box[0] * 100);
|
||||||
const boxTopEdge = Math.round(timeline.data.box[1] * 100);
|
const boxTopEdge = Math.round(timeline.data.box[1] * 100);
|
||||||
const boxRightEdge = Math.round(
|
const boxRightEdge = Math.round(
|
||||||
@ -25,6 +29,10 @@ export default function TimelineEventOverlay({
|
|||||||
|
|
||||||
const [isHovering, setIsHovering] = useState<boolean>(false);
|
const [isHovering, setIsHovering] = useState<boolean>(false);
|
||||||
const getHoverStyle = () => {
|
const getHoverStyle = () => {
|
||||||
|
if (!timeline.data.box) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
if (boxLeftEdge < 15) {
|
if (boxLeftEdge < 15) {
|
||||||
// show object stats on right side
|
// show object stats on right side
|
||||||
return {
|
return {
|
||||||
@ -40,12 +48,20 @@ export default function TimelineEventOverlay({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getObjectArea = () => {
|
const getObjectArea = () => {
|
||||||
|
if (!timeline.data.box) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
const width = timeline.data.box[2] * cameraConfig.detect.width;
|
const width = timeline.data.box[2] * cameraConfig.detect.width;
|
||||||
const height = timeline.data.box[3] * cameraConfig.detect.height;
|
const height = timeline.data.box[3] * cameraConfig.detect.height;
|
||||||
return Math.round(width * height);
|
return Math.round(width * height);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getObjectRatio = () => {
|
const getObjectRatio = () => {
|
||||||
|
if (!timeline.data.box) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
const width = timeline.data.box[2] * cameraConfig.detect.width;
|
const width = timeline.data.box[2] * cameraConfig.detect.width;
|
||||||
const height = timeline.data.box[3] * cameraConfig.detect.height;
|
const height = timeline.data.box[3] * cameraConfig.detect.height;
|
||||||
return Math.round(100 * (width / height)) / 100;
|
return Math.round(100 * (width / height)) / 100;
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
|
||||||
import VideoPlayer from "./VideoPlayer";
|
import VideoPlayer from "./VideoPlayer";
|
||||||
import useSWR from "swr";
|
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -12,6 +10,7 @@ import { useApiHost } from "@/api";
|
|||||||
import Player from "video.js/dist/types/player";
|
import Player from "video.js/dist/types/player";
|
||||||
import { AspectRatio } from "../ui/aspect-ratio";
|
import { AspectRatio } from "../ui/aspect-ratio";
|
||||||
import { LuPlayCircle } from "react-icons/lu";
|
import { LuPlayCircle } from "react-icons/lu";
|
||||||
|
import { isCurrentHour } from "@/utils/dateUtil";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
camera: string;
|
camera: string;
|
||||||
@ -38,7 +37,6 @@ export default function PreviewThumbnailPlayer({
|
|||||||
isMobile,
|
isMobile,
|
||||||
onClick,
|
onClick,
|
||||||
}: PreviewPlayerProps) {
|
}: PreviewPlayerProps) {
|
||||||
const { data: config } = useSWR("config");
|
|
||||||
const playerRef = useRef<Player | null>(null);
|
const playerRef = useRef<Player | null>(null);
|
||||||
const isSafari = useMemo(() => {
|
const isSafari = useMemo(() => {
|
||||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
@ -105,6 +103,7 @@ export default function PreviewThumbnailPlayer({
|
|||||||
{
|
{
|
||||||
threshold: 1.0,
|
threshold: 1.0,
|
||||||
root: document.getElementById("pageRoot"),
|
root: document.getElementById("pageRoot"),
|
||||||
|
rootMargin: "-15% 0px -15% 0px",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (node) autoPlayObserver.current.observe(node);
|
if (node) autoPlayObserver.current.observe(node);
|
||||||
@ -131,7 +130,6 @@ export default function PreviewThumbnailPlayer({
|
|||||||
isInitiallyVisible={isInitiallyVisible}
|
isInitiallyVisible={isInitiallyVisible}
|
||||||
startTs={startTs}
|
startTs={startTs}
|
||||||
camera={camera}
|
camera={camera}
|
||||||
config={config}
|
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
isSafari={isSafari}
|
isSafari={isSafari}
|
||||||
@ -143,7 +141,6 @@ export default function PreviewThumbnailPlayer({
|
|||||||
|
|
||||||
type PreviewContentProps = {
|
type PreviewContentProps = {
|
||||||
playerRef: React.MutableRefObject<Player | null>;
|
playerRef: React.MutableRefObject<Player | null>;
|
||||||
config: FrigateConfig;
|
|
||||||
camera: string;
|
camera: string;
|
||||||
relevantPreview: Preview | undefined;
|
relevantPreview: Preview | undefined;
|
||||||
eventId: string;
|
eventId: string;
|
||||||
@ -156,7 +153,6 @@ type PreviewContentProps = {
|
|||||||
};
|
};
|
||||||
function PreviewContent({
|
function PreviewContent({
|
||||||
playerRef,
|
playerRef,
|
||||||
config,
|
|
||||||
camera,
|
camera,
|
||||||
relevantPreview,
|
relevantPreview,
|
||||||
eventId,
|
eventId,
|
||||||
@ -195,22 +191,13 @@ function PreviewContent({
|
|||||||
|
|
||||||
if (relevantPreview && !isVisible) {
|
if (relevantPreview && !isVisible) {
|
||||||
return <div />;
|
return <div />;
|
||||||
} else if (!relevantPreview) {
|
} else if (!relevantPreview && !isCurrentHour(startTs)) {
|
||||||
if (isCurrentHour(startTs)) {
|
return (
|
||||||
return (
|
<img
|
||||||
<img
|
className="w-[160px]"
|
||||||
className={`${getPreviewWidth(camera, config)}`}
|
src={`${apiHost}api/events/${eventId}/thumbnail.jpg`}
|
||||||
src={`${apiHost}api/preview/${camera}/${startTs}/thumbnail.jpg`}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className="w-[160px]"
|
|
||||||
src={`${apiHost}api/events/${eventId}/thumbnail.jpg`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -223,17 +210,26 @@ function PreviewContent({
|
|||||||
controls: false,
|
controls: false,
|
||||||
muted: true,
|
muted: true,
|
||||||
loadingSpinner: false,
|
loadingSpinner: false,
|
||||||
sources: [
|
poster: relevantPreview
|
||||||
{
|
? ""
|
||||||
src: `${relevantPreview.src}`,
|
: `${apiHost}api/preview/${camera}/${startTs}/thumbnail.jpg`,
|
||||||
type: "video/mp4",
|
sources: relevantPreview
|
||||||
},
|
? [
|
||||||
],
|
{
|
||||||
|
src: `${relevantPreview.src}`,
|
||||||
|
type: "video/mp4",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
}}
|
}}
|
||||||
seekOptions={{}}
|
seekOptions={{}}
|
||||||
onReady={(player) => {
|
onReady={(player) => {
|
||||||
playerRef.current = player;
|
playerRef.current = player;
|
||||||
|
|
||||||
|
if (!relevantPreview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isInitiallyVisible) {
|
if (!isInitiallyVisible) {
|
||||||
player.pause(); // autoplay + pause is required for iOS
|
player.pause(); // autoplay + pause is required for iOS
|
||||||
}
|
}
|
||||||
@ -249,28 +245,10 @@ function PreviewContent({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<LuPlayCircle className="absolute z-10 left-1 bottom-1 w-4 h-4 text-white text-opacity-60" />
|
{relevantPreview && (
|
||||||
|
<LuPlayCircle className="absolute z-10 left-1 bottom-1 w-4 h-4 text-white text-opacity-60" />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCurrentHour(timestamp: number) {
|
|
||||||
const now = new Date();
|
|
||||||
now.setMinutes(0, 0, 0);
|
|
||||||
return timestamp > now.getTime() / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPreviewWidth(camera: string, config: FrigateConfig) {
|
|
||||||
const detect = config.cameras[camera].detect;
|
|
||||||
|
|
||||||
if (detect.width / detect.height < 1) {
|
|
||||||
return "w-1/2";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (detect.width / detect.height < 16 / 9) {
|
|
||||||
return "w-2/3";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "w-full";
|
|
||||||
}
|
|
||||||
|
|||||||
@ -285,3 +285,9 @@ export function getRangeForTimestamp(timestamp: number) {
|
|||||||
const end = date.getTime() / 1000;
|
const end = date.getTime() / 1000;
|
||||||
return { start, end };
|
return { start, end };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isCurrentHour(timestamp: number) {
|
||||||
|
const now = new Date();
|
||||||
|
now.setMinutes(0, 0, 0);
|
||||||
|
return timestamp > now.getTime() / 1000;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user