Use poster image for preview on video player instead of using separate image view

This commit is contained in:
Nick Mowen 2024-01-14 19:59:08 -07:00
parent d09e142b22
commit 2ac3173b4a
4 changed files with 83 additions and 73 deletions

View File

@ -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,11 +19,14 @@ 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",
@ -33,20 +37,26 @@ export default function TimelineItemCard({
fluid: false, fluid: false,
muted: true, muted: true,
loadingSpinner: false, loadingSpinner: false,
sources: [ poster: relevantPreview
? ""
: `${apiHost}api/preview/${timeline.camera}/${timeline.timestamp}/thumbnail.jpg`,
sources: relevantPreview
? [
{ {
src: `${relevantPreview.src}`, src: `${relevantPreview.src}`,
type: "video/mp4", type: "video/mp4",
}, },
], ]
: [],
}} }}
seekOptions={{}} seekOptions={{}}
onReady={(player) => { 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">

View File

@ -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;

View File

@ -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 (
<img
className={`${getPreviewWidth(camera, config)}`}
src={`${apiHost}api/preview/${camera}/${startTs}/thumbnail.jpg`}
/>
);
} else {
return ( return (
<img <img
className="w-[160px]" className="w-[160px]"
src={`${apiHost}api/events/${eventId}/thumbnail.jpg`} 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
? ""
: `${apiHost}api/preview/${camera}/${startTs}/thumbnail.jpg`,
sources: relevantPreview
? [
{ {
src: `${relevantPreview.src}`, src: `${relevantPreview.src}`,
type: "video/mp4", 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>
{relevantPreview && (
<LuPlayCircle className="absolute z-10 left-1 bottom-1 w-4 h-4 text-white text-opacity-60" /> <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";
}

View File

@ -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;
}