stream selection on single camera live view

This commit is contained in:
Josh Hawkins 2024-11-12 07:30:42 -06:00
parent 922d16fa4c
commit cbffb97ae5
3 changed files with 143 additions and 24 deletions

View File

@ -26,6 +26,7 @@ type LivePlayerProps = {
containerRef?: React.MutableRefObject<HTMLDivElement | null>; containerRef?: React.MutableRefObject<HTMLDivElement | null>;
className?: string; className?: string;
cameraConfig: CameraConfig; cameraConfig: CameraConfig;
streamName: string;
preferredLiveMode: LivePlayerMode; preferredLiveMode: LivePlayerMode;
showStillWithoutActivity?: boolean; showStillWithoutActivity?: boolean;
windowVisible?: boolean; windowVisible?: boolean;
@ -45,6 +46,7 @@ export default function LivePlayer({
containerRef, containerRef,
className, className,
cameraConfig, cameraConfig,
streamName,
preferredLiveMode, preferredLiveMode,
showStillWithoutActivity = true, showStillWithoutActivity = true,
windowVisible = true, windowVisible = true,
@ -144,6 +146,19 @@ export default function LivePlayer({
setLiveReady(false); setLiveReady(false);
}, [preferredLiveMode]); }, [preferredLiveMode]);
const [key, setKey] = useState(0);
const resetPlayer = () => {
setLiveReady(false);
setKey((prevKey) => prevKey + 1);
};
useEffect(() => {
if (streamName) {
resetPlayer();
}
}, [streamName]);
const playerIsPlaying = useCallback(() => { const playerIsPlaying = useCallback(() => {
setLiveReady(true); setLiveReady(true);
}, []); }, []);
@ -153,13 +168,14 @@ export default function LivePlayer({
} }
let player; let player;
if (!autoLive) { if (!autoLive || !streamName) {
player = null; player = null;
} else if (preferredLiveMode == "webrtc") { } else if (preferredLiveMode == "webrtc") {
player = ( player = (
<WebRtcPlayer <WebRtcPlayer
key={"webrtc_" + key}
className={`size-full rounded-lg md:rounded-2xl ${liveReady ? "" : "hidden"}`} className={`size-full rounded-lg md:rounded-2xl ${liveReady ? "" : "hidden"}`}
camera={cameraConfig.live.stream_name} camera={streamName}
playbackEnabled={cameraActive || liveReady} playbackEnabled={cameraActive || liveReady}
audioEnabled={playAudio} audioEnabled={playAudio}
microphoneEnabled={micEnabled} microphoneEnabled={micEnabled}
@ -173,8 +189,9 @@ export default function LivePlayer({
if ("MediaSource" in window || "ManagedMediaSource" in window) { if ("MediaSource" in window || "ManagedMediaSource" in window) {
player = ( player = (
<MSEPlayer <MSEPlayer
key={"mse_" + key}
className={`size-full rounded-lg md:rounded-2xl ${liveReady ? "" : "hidden"}`} className={`size-full rounded-lg md:rounded-2xl ${liveReady ? "" : "hidden"}`}
camera={cameraConfig.live.stream_name} camera={streamName}
playbackEnabled={cameraActive || liveReady} playbackEnabled={cameraActive || liveReady}
audioEnabled={playAudio} audioEnabled={playAudio}
onPlaying={playerIsPlaying} onPlaying={playerIsPlaying}
@ -194,6 +211,7 @@ export default function LivePlayer({
if (cameraActive || !showStillWithoutActivity || liveReady) { if (cameraActive || !showStillWithoutActivity || liveReady) {
player = ( player = (
<JSMpegPlayer <JSMpegPlayer
key={"jsmpeg_" + key}
className="flex justify-center overflow-hidden rounded-lg md:rounded-2xl" className="flex justify-center overflow-hidden rounded-lg md:rounded-2xl"
camera={cameraConfig.name} camera={cameraConfig.name}
width={cameraConfig.detect.width} width={cameraConfig.detect.width}

View File

@ -87,7 +87,7 @@ export interface CameraConfig {
live: { live: {
height: number; height: number;
quality: number; quality: number;
stream_name: string; streams: { [key: string]: string };
}; };
motion: { motion: {
contour_area: number; contour_area: number;
@ -320,12 +320,6 @@ export interface FrigateConfig {
camera_groups: { [groupName: string]: CameraGroupConfig }; camera_groups: { [groupName: string]: CameraGroupConfig };
live: {
height: number;
quality: number;
stream_name: string;
};
logger: { logger: {
default: string; default: string;
logs: Record<string, string>; logs: Record<string, string>;

View File

@ -57,7 +57,7 @@ import {
} from "react-icons/fa"; } from "react-icons/fa";
import { GiSpeaker, GiSpeakerOff } from "react-icons/gi"; import { GiSpeaker, GiSpeakerOff } from "react-icons/gi";
import { TbViewfinder, TbViewfinderOff } from "react-icons/tb"; import { TbViewfinder, TbViewfinderOff } from "react-icons/tb";
import { IoMdArrowRoundBack } from "react-icons/io"; import { IoIosWarning, IoMdArrowRoundBack } from "react-icons/io";
import { import {
LuEar, LuEar,
LuEarOff, LuEarOff,
@ -79,6 +79,20 @@ import { TransformWrapper, TransformComponent } from "react-zoom-pan-pinch";
import useSWR from "swr"; import useSWR from "swr";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useSessionPersistence } from "@/hooks/use-session-persistence"; import { useSessionPersistence } from "@/hooks/use-session-persistence";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { usePersistence } from "@/hooks/use-persistence";
import { TooltipPortal } from "@radix-ui/react-tooltip";
type LiveCameraViewProps = { type LiveCameraViewProps = {
config?: FrigateConfig; config?: FrigateConfig;
@ -103,17 +117,20 @@ export default function LiveCameraView({
// supported features // supported features
const [streamName, setStreamName] = usePersistence<string>(
`${camera.name}-stream`,
Object.values(camera.live.streams)[0],
);
const isRestreamed = useMemo( const isRestreamed = useMemo(
() => () =>
config && config &&
Object.keys(config.go2rtc.streams || {}).includes( Object.keys(config.go2rtc.streams || {}).includes(streamName ?? ""),
camera.live.stream_name, [config, streamName],
),
[camera, config],
); );
const { data: cameraMetadata } = useSWR<LiveStreamMetadata>( const { data: cameraMetadata } = useSWR<LiveStreamMetadata>(
isRestreamed ? `go2rtc/streams/${camera.live.stream_name}` : null, isRestreamed ? `go2rtc/streams/${streamName}` : null,
{ {
revalidateOnFocus: false, revalidateOnFocus: false,
}, },
@ -454,13 +471,16 @@ export default function LiveCameraView({
/> />
)} )}
<FrigateCameraFeatures <FrigateCameraFeatures
camera={camera.name} camera={camera}
recordingEnabled={camera.record.enabled_in_config} recordingEnabled={camera.record.enabled_in_config}
audioDetectEnabled={camera.audio.enabled_in_config} audioDetectEnabled={camera.audio.enabled_in_config}
autotrackingEnabled={ autotrackingEnabled={
camera.onvif.autotracking.enabled_in_config camera.onvif.autotracking.enabled_in_config
} }
fullscreen={fullscreen} fullscreen={fullscreen}
streamName={streamName ?? ""}
setStreamName={setStreamName}
preferredLiveMode={preferredLiveMode}
/> />
</div> </div>
</TooltipProvider> </TooltipProvider>
@ -496,6 +516,7 @@ export default function LiveCameraView({
micEnabled={mic} micEnabled={mic}
iOSCompatFullScreen={isIOS} iOSCompatFullScreen={isIOS}
preferredLiveMode={preferredLiveMode} preferredLiveMode={preferredLiveMode}
streamName={streamName ?? ""}
pip={pip} pip={pip}
containerRef={containerRef} containerRef={containerRef}
setFullResolution={setFullResolution} setFullResolution={setFullResolution}
@ -749,11 +770,14 @@ function PtzControlPanel({
} }
type FrigateCameraFeaturesProps = { type FrigateCameraFeaturesProps = {
camera: string; camera: CameraConfig;
recordingEnabled: boolean; recordingEnabled: boolean;
audioDetectEnabled: boolean; audioDetectEnabled: boolean;
autotrackingEnabled: boolean; autotrackingEnabled: boolean;
fullscreen: boolean; fullscreen: boolean;
streamName: string;
setStreamName?: (value: string | undefined) => void;
preferredLiveMode: string;
}; };
function FrigateCameraFeatures({ function FrigateCameraFeatures({
camera, camera,
@ -761,14 +785,22 @@ function FrigateCameraFeatures({
audioDetectEnabled, audioDetectEnabled,
autotrackingEnabled, autotrackingEnabled,
fullscreen, fullscreen,
streamName,
setStreamName,
preferredLiveMode,
}: FrigateCameraFeaturesProps) { }: FrigateCameraFeaturesProps) {
const { payload: detectState, send: sendDetect } = useDetectState(camera); const { payload: detectState, send: sendDetect } = useDetectState(
const { payload: recordState, send: sendRecord } = useRecordingsState(camera); camera.name,
const { payload: snapshotState, send: sendSnapshot } = );
useSnapshotsState(camera); const { payload: recordState, send: sendRecord } = useRecordingsState(
const { payload: audioState, send: sendAudio } = useAudioState(camera); camera.name,
);
const { payload: snapshotState, send: sendSnapshot } = useSnapshotsState(
camera.name,
);
const { payload: audioState, send: sendAudio } = useAudioState(camera.name);
const { payload: autotrackingState, send: sendAutotracking } = const { payload: autotrackingState, send: sendAutotracking } =
useAutotrackingState(camera); useAutotrackingState(camera.name);
// desktop shows icons part of row // desktop shows icons part of row
if (isDesktop || isTablet) { if (isDesktop || isTablet) {
@ -820,6 +852,47 @@ function FrigateCameraFeatures({
} }
/> />
)} )}
{Object.values(camera.live.streams).length > 1 && (
<Select
value={streamName}
onValueChange={(value) => {
setStreamName?.(value);
}}
>
<SelectTrigger className="w-full">
{preferredLiveMode == "jsmpeg" && (
<Tooltip>
<TooltipTrigger>
<IoIosWarning className="mr-1 size-5 text-danger" />
</TooltipTrigger>
<TooltipPortal>
<TooltipContent className="max-w-52">
Live view is in low-bandwidth mode due to buffering or
stream errors
</TooltipContent>
</TooltipPortal>
</Tooltip>
)}
{Object.keys(camera.live.streams).find(
(key) => camera.live.streams[key] === streamName,
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.entries(camera.live.streams).map(([stream, name]) => (
<SelectItem
key={stream}
className="cursor-pointer"
value={name}
>
{stream}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</> </>
); );
} }
@ -878,6 +951,40 @@ function FrigateCameraFeatures({
} }
/> />
)} )}
{Object.values(camera.live.streams).length > 1 && (
<div className="mt-1 p-2">
<div className="mb-1 text-sm">Live stream selection</div>
<Select
value={streamName}
onValueChange={(value) => {
setStreamName?.(value);
}}
>
<SelectTrigger className="w-full">
{preferredLiveMode == "jsmpeg" && (
<IoIosWarning className="mr-1 size-5 text-danger" />
)}
{Object.keys(camera.live.streams).find(
(key) => camera.live.streams[key] === streamName,
)}
</SelectTrigger>
<SelectContent>
<SelectGroup>
{Object.entries(camera.live.streams).map(([stream, name]) => (
<SelectItem
key={stream}
className="cursor-pointer"
value={name}
>
{stream}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
)}
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );