mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-17 16:44:29 +03:00
stream selection on single camera live view
This commit is contained in:
parent
922d16fa4c
commit
cbffb97ae5
@ -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}
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user