rework live dashboard grid layout and add natural mode

This commit is contained in:
Josh Hawkins
2026-09-24 13:26:57 -05:00
parent 40f8ba1f7f
commit e760868048
19 changed files with 1375 additions and 253 deletions
+61 -14
View File
@@ -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">