mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-01 12:26:50 +03:00
rework live dashboard grid layout and add natural mode
This commit is contained in:
@@ -54,6 +54,10 @@ type LivePlayerProps = {
|
||||
onError?: (error: LivePlayerError) => void;
|
||||
onMicrophoneError?: (error: TwoWayTalkError) => void;
|
||||
onResetLiveMode?: () => void;
|
||||
// Aspect of the source currently on screen: the live stream's while it is
|
||||
// playing, undefined while the still image is showing. The still comes from
|
||||
// the detect stream, which can be shaped differently than the live one.
|
||||
onLiveAspectChange?: (aspectRatio: number | undefined) => void;
|
||||
};
|
||||
|
||||
export default function LivePlayer({
|
||||
@@ -80,6 +84,7 @@ export default function LivePlayer({
|
||||
onError,
|
||||
onMicrophoneError,
|
||||
onResetLiveMode,
|
||||
onLiveAspectChange,
|
||||
}: LivePlayerProps) {
|
||||
const { t } = useTranslation(["components/player"]);
|
||||
|
||||
@@ -127,6 +132,37 @@ export default function LivePlayer({
|
||||
// camera live state
|
||||
|
||||
const [liveReady, setLiveReady] = useState(false);
|
||||
const [liveAspect, setLiveAspect] = useState<number | undefined>();
|
||||
|
||||
const handleFullResolution = useCallback(
|
||||
(value: React.SetStateAction<VideoResolutionType>) => {
|
||||
setFullResolution?.(value);
|
||||
|
||||
if (typeof value === "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
setLiveAspect(
|
||||
value.width && value.height ? value.width / value.height : undefined,
|
||||
);
|
||||
},
|
||||
[setFullResolution],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onLiveAspectChange?.(liveReady ? liveAspect : undefined);
|
||||
}, [liveReady, liveAspect, onLiveAspectChange]);
|
||||
|
||||
// The card can be a different shape than the picture (a bucketed tile, or a
|
||||
// still whose detect aspect differs from the stream), so overlays that are
|
||||
// meant to sit on the image have to be fitted to it rather than to the card.
|
||||
const pictureAspect = useMemo(() => {
|
||||
if (liveReady && liveAspect) {
|
||||
return liveAspect;
|
||||
}
|
||||
const { width, height } = cameraConfig.detect;
|
||||
return width && height ? width / height : 16 / 9;
|
||||
}, [liveReady, liveAspect, cameraConfig.detect]);
|
||||
|
||||
const liveReadyRef = useRef(liveReady);
|
||||
const cameraActiveRef = useRef(cameraActive);
|
||||
@@ -262,11 +298,12 @@ export default function LivePlayer({
|
||||
player = (
|
||||
<WebRtcPlayer
|
||||
key={"webrtc_" + key}
|
||||
className={`size-full rounded-lg md:rounded-2xl ${liveReady ? "" : "hidden"}`}
|
||||
className={`size-full ${liveReady ? "" : "hidden"}`}
|
||||
camera={streamName}
|
||||
playbackEnabled={cameraActive || liveReady}
|
||||
getStats={showStats}
|
||||
setStats={setStats}
|
||||
setFullResolution={handleFullResolution}
|
||||
audioEnabled={playAudio}
|
||||
volume={volume}
|
||||
microphoneEnabled={micEnabled}
|
||||
@@ -282,7 +319,7 @@ export default function LivePlayer({
|
||||
player = (
|
||||
<MSEPlayer
|
||||
key={"mse_" + key}
|
||||
className={`size-full rounded-lg md:rounded-2xl ${liveReady ? "" : "hidden"}`}
|
||||
className={`size-full ${liveReady ? "" : "hidden"}`}
|
||||
camera={streamName}
|
||||
playbackEnabled={cameraActive || liveReady}
|
||||
audioEnabled={playAudio}
|
||||
@@ -292,7 +329,7 @@ export default function LivePlayer({
|
||||
setStats={setStats}
|
||||
onPlaying={playerIsPlaying}
|
||||
pip={pip}
|
||||
setFullResolution={setFullResolution}
|
||||
setFullResolution={handleFullResolution}
|
||||
onError={onError}
|
||||
/>
|
||||
);
|
||||
@@ -308,7 +345,7 @@ export default function LivePlayer({
|
||||
player = (
|
||||
<JSMpegPlayer
|
||||
key={"jsmpeg_" + key}
|
||||
className="flex justify-center overflow-hidden rounded-lg md:rounded-2xl"
|
||||
className="flex justify-center overflow-hidden"
|
||||
camera={cameraConfig.name}
|
||||
width={cameraConfig.detect.width}
|
||||
height={cameraConfig.detect.height}
|
||||
@@ -325,7 +362,9 @@ export default function LivePlayer({
|
||||
player = null;
|
||||
}
|
||||
} else {
|
||||
player = <ActivityIndicator />;
|
||||
player = (
|
||||
<ActivityIndicator className={"w-full [.bg-black_&]:text-white"} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -340,10 +379,12 @@ export default function LivePlayer({
|
||||
}}
|
||||
data-camera={cameraConfig.name}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-pointer justify-center outline",
|
||||
// the card owns the corner: overflow-hidden clips the stream, the still
|
||||
// image, and every overlay to this one radius so they stay concentric
|
||||
"relative flex w-full cursor-pointer justify-center overflow-hidden rounded-lg outline md:rounded-2xl",
|
||||
activeTracking &&
|
||||
((showStillWithoutActivity && !liveReady) || liveReady)
|
||||
? "outline-3 rounded-lg shadow-severity_alert outline-severity_alert md:rounded-2xl"
|
||||
? "shadow-severity_alert outline-[3px] outline-severity_alert"
|
||||
: "outline-0 outline-background",
|
||||
"transition-all duration-500",
|
||||
className,
|
||||
@@ -357,18 +398,24 @@ export default function LivePlayer({
|
||||
>
|
||||
{cameraEnabled &&
|
||||
((showStillWithoutActivity && !liveReady) || liveReady) && (
|
||||
<ImageShadowOverlay
|
||||
upperClassName="md:rounded-2xl"
|
||||
lowerClassName="md:rounded-2xl"
|
||||
/>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 z-10 flex items-center justify-center [container-type:size]"
|
||||
style={{ "--pic-ar": pictureAspect } as React.CSSProperties}
|
||||
>
|
||||
<div className="relative aspect-[var(--pic-ar)] h-auto w-[min(100%,calc(100cqh*var(--pic-ar)))]">
|
||||
<ImageShadowOverlay />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{player}
|
||||
{cameraEnabled &&
|
||||
!offline &&
|
||||
(!showStillWithoutActivity || isReEnabling) &&
|
||||
!liveReady && (
|
||||
(
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<ActivityIndicator />
|
||||
<ActivityIndicator className={"w-full [.bg-black_&]:text-white"} />
|
||||
)
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -447,7 +494,7 @@ export default function LivePlayer({
|
||||
|
||||
{offline && inDashboard && (
|
||||
<>
|
||||
<div className="absolute inset-0 rounded-lg bg-black/50 md:rounded-2xl" />
|
||||
<div className="absolute inset-0 bg-black/50" />
|
||||
<div className="absolute inset-0 left-1/2 top-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center">
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-lg bg-background/50 p-3 text-center">
|
||||
<div>{t("streamOffline.title")}</div>
|
||||
@@ -491,7 +538,7 @@ export default function LivePlayer({
|
||||
)}
|
||||
|
||||
{!cameraEnabled && (
|
||||
<div className="relative flex h-full w-full items-center justify-center rounded-2xl border border-secondary-foreground bg-background_alt">
|
||||
<div className="relative flex h-full w-full items-center justify-center border border-secondary-foreground bg-background_alt">
|
||||
<div className="flex h-32 flex-col items-center justify-center rounded-lg p-4 md:h-48 md:w-48">
|
||||
<LuVideoOff className="mb-2 size-8 md:size-10" />
|
||||
<p className="max-w-32 text-center text-sm md:max-w-40 md:text-base">
|
||||
|
||||
Reference in New Issue
Block a user