Skip to content

Commit d855283

Browse files
KaliszWiktoriaktaube26Copilot
authored
[ViPPET] Cameras view (open-edge-platform#1919)
Co-authored-by: Taube, Krystian <krystian.taube@intel.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 361071a commit d855283

File tree

13 files changed

+429
-19
lines changed

13 files changed

+429
-19
lines changed

tools/visual-pipeline-and-platform-evaluation-tool/ui/src/config/navigation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
Home,
88
ListTodo,
99
GitFork,
10+
Camera,
1011
} from "lucide-react";
1112
import { redirect, type RouteObject } from "react-router";
1213
import { Home as HomePage } from "@/pages/Home.tsx";
@@ -20,6 +21,7 @@ import { PerformanceJobDetail } from "@/pages/PerformanceJobDetail.tsx";
2021
import { DensityJobDetail } from "@/pages/DensityJobDetail.tsx";
2122
import { OptimizationJobDetail } from "@/pages/OptimizationJobDetail.tsx";
2223
import { Pipelines2 } from "@/pages/Pipelines2";
24+
import { Cameras } from "@/pages/Cameras";
2325

2426
export type NavigationItem = {
2527
url: string;
@@ -38,6 +40,7 @@ export const menuItems: Array<NavigationItem> = [
3840
{ url: "/pipelines", title: "Pipelines", icon: GitFork },
3941
{ url: "/models", title: "Models", icon: Cpu },
4042
{ url: "/videos", title: "Videos", icon: Film },
43+
{ url: "/cameras", title: "Cameras", icon: Camera },
4144
{
4245
url: "/tests/performance",
4346
title: "Performance",
@@ -53,6 +56,7 @@ export const routeConfig: Array<RouteObject> = [
5356
{ path: "pipelines/:id/:variant", Component: Pipelines },
5457
{ path: "models", Component: Models },
5558
{ path: "videos", Component: Videos },
59+
{ path: "cameras", Component: Cameras },
5660
{ path: "tests/performance", Component: PerformanceTests },
5761
{ path: "tests/density", Component: DensityTests },
5862
{
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { useState } from "react";
2+
import { useForm } from "react-hook-form";
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogTrigger,
11+
} from "@/components/ui/dialog.tsx";
12+
import { Button } from "@/components/ui/button.tsx";
13+
import { Input } from "@/components/ui/input.tsx";
14+
import { Field, FieldError, FieldLabel } from "@/components/ui/field.tsx";
15+
import { useLoadCameraProfilesMutation } from "@/api/api.generated.ts";
16+
import { toast } from "sonner";
17+
import { handleApiError } from "@/lib/apiUtils";
18+
19+
type CameraAuthFormData = {
20+
username: string;
21+
password: string;
22+
};
23+
24+
type CameraAuthDialogProps = {
25+
cameraId: string;
26+
cameraName: string;
27+
onSuccess?: () => void;
28+
};
29+
30+
export const CameraAuthDialog = ({
31+
cameraId,
32+
cameraName,
33+
onSuccess,
34+
}: CameraAuthDialogProps) => {
35+
const [open, setOpen] = useState(false);
36+
const [loadCameraProfiles, { isLoading }] = useLoadCameraProfilesMutation();
37+
38+
const {
39+
register,
40+
handleSubmit,
41+
formState: { errors },
42+
reset,
43+
} = useForm<CameraAuthFormData>({
44+
defaultValues: {
45+
username: "",
46+
password: "",
47+
},
48+
});
49+
50+
const onSubmit = async (data: CameraAuthFormData) => {
51+
try {
52+
await loadCameraProfiles({
53+
cameraId,
54+
cameraProfilesRequest: {
55+
username: data.username,
56+
password: data.password,
57+
},
58+
}).unwrap();
59+
60+
toast.success("Camera authorized", {
61+
description: "Camera authorized successfully",
62+
});
63+
64+
setOpen(false);
65+
reset();
66+
onSuccess?.();
67+
} catch (error) {
68+
handleApiError(error, "Camera authorization failed");
69+
}
70+
};
71+
72+
const handleOpenChange = (newOpen: boolean) => {
73+
setOpen(newOpen);
74+
if (!newOpen) {
75+
reset();
76+
}
77+
};
78+
79+
return (
80+
<Dialog open={open} onOpenChange={handleOpenChange}>
81+
<DialogTrigger asChild>
82+
<Button size="sm">Authorize</Button>
83+
</DialogTrigger>
84+
<DialogContent className="sm:max-w-[500px]">
85+
<DialogHeader>
86+
<DialogTitle>Authorize Camera</DialogTitle>
87+
<DialogDescription>
88+
Enter credentials to authorize {cameraName}
89+
</DialogDescription>
90+
</DialogHeader>
91+
<form onSubmit={handleSubmit(onSubmit)}>
92+
<div className="space-y-4 py-4">
93+
<Field>
94+
<FieldLabel htmlFor="username">Username</FieldLabel>
95+
<Input
96+
id="username"
97+
type="text"
98+
autoComplete="username"
99+
placeholder="Enter username"
100+
{...register("username", {
101+
required: "Username is required",
102+
})}
103+
/>
104+
{errors.username && (
105+
<FieldError>{errors.username.message}</FieldError>
106+
)}
107+
</Field>
108+
109+
<Field>
110+
<FieldLabel htmlFor="password">Password</FieldLabel>
111+
<Input
112+
id="password"
113+
type="password"
114+
autoComplete="current-password"
115+
placeholder="Enter password"
116+
{...register("password", {
117+
required: "Password is required",
118+
})}
119+
/>
120+
{errors.password && (
121+
<FieldError>{errors.password.message}</FieldError>
122+
)}
123+
</Field>
124+
</div>
125+
126+
<DialogFooter>
127+
<Button
128+
type="button"
129+
variant="outline"
130+
onClick={() => handleOpenChange(false)}
131+
disabled={isLoading}
132+
>
133+
Cancel
134+
</Button>
135+
<Button type="submit" disabled={isLoading}>
136+
{isLoading ? "Authorizing..." : "Authorize"}
137+
</Button>
138+
</DialogFooter>
139+
</form>
140+
</DialogContent>
141+
</Dialog>
142+
);
143+
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { type Camera } from "@/api/api.generated.ts";
2+
3+
const isMockCamerasFromEnv = import.meta.env.VITE_MOCK_CAMERAS === "1";
4+
5+
const isMockCamerasFromQuery =
6+
typeof window !== "undefined" &&
7+
new URLSearchParams(window.location.search).get("mockCameras") === "1";
8+
9+
export const isCameraMockEnabled =
10+
isMockCamerasFromEnv || isMockCamerasFromQuery;
11+
12+
export const mockCameras: Camera[] = [
13+
{
14+
device_id: "usb-1",
15+
device_name: "Thronmax StreamGo Webcam: Thron",
16+
device_type: "USB",
17+
details: {
18+
device_path: "/dev/video2",
19+
best_capture: {
20+
fourcc: "MJPG",
21+
width: 1920,
22+
height: 1080,
23+
fps: 30,
24+
},
25+
},
26+
},
27+
{
28+
device_id: "usb-2",
29+
device_name: "C270 HD WEBCAM",
30+
device_type: "USB",
31+
details: {
32+
device_path: "/dev/video0",
33+
best_capture: {
34+
fourcc: "MJPG",
35+
width: 1280,
36+
height: 720,
37+
fps: 30,
38+
},
39+
},
40+
},
41+
{
42+
device_id: "net-1",
43+
device_name: "ONVIF Camera 10.91.106.249",
44+
device_type: "NETWORK",
45+
details: {
46+
ip: "10.91.106.249",
47+
port: 1000,
48+
profiles: [],
49+
best_profile: null,
50+
},
51+
},
52+
];

tools/visual-pipeline-and-platform-evaluation-tool/ui/src/features/demo/DemoMode.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import {
5353
parsePipelineVariantReference,
5454
resolvePipelineVariantLabel,
5555
} from "@/features/pipeline-tests/pipelineVariantReference";
56+
import { filterOutTransportStreams } from "@/lib/videoUtils.ts";
5657

5758
const nodeTypeToTag: Record<string, string> = {
5859
// Sources
@@ -276,7 +277,7 @@ const DemoMode = () => {
276277
const [selectedVariantByPipelineId, setSelectedVariantByPipelineId] =
277278
useState<Record<string, string>>({});
278279
const videoFilenames = useMemo(
279-
() => videos.map((video) => video.filename),
280+
() => filterOutTransportStreams(videos).map((video) => video.filename),
280281
[videos],
281282
);
282283
const getFilenameFromPath = (value: unknown): string => {
@@ -1891,10 +1892,6 @@ const DemoMode = () => {
18911892
}
18921893
className={`w-full px-2 py-1.5 bg-slate-900/90 border border-slate-400/40 rounded text-slate-200 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 ${isReadOnly ? "opacity-60 cursor-not-allowed" : ""}`}
18931894
>
1894-
<option value="">
1895-
Select
1896-
filename
1897-
</option>
18981895
{videoFilenames.map(
18991896
(
19001897
filename,

tools/visual-pipeline-and-platform-evaluation-tool/ui/src/features/pipeline-editor/NodeDataPanel.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useAppSelector } from "@/store/hooks";
1010
import { selectModels } from "@/store/reducers/models";
1111
import DeviceSelect from "@/components/shared/DeviceSelect";
1212
import { useGetCamerasQuery, useGetVideosQuery } from "@/api/api.generated";
13+
import { filterOutTransportStreams } from "@/lib/videoUtils.ts";
1314

1415
type NodePropertyConfig = {
1516
key: string;
@@ -46,6 +47,7 @@ const NodeDataPanel = ({
4647
const cameraOptions = cameras.map((camera) => {
4748
const details = camera.details as Record<string, unknown> | undefined;
4849
let value = "";
50+
let disabled = false;
4951

5052
if (camera.device_type === "USB") {
5153
const devicePath =
@@ -54,6 +56,16 @@ const NodeDataPanel = ({
5456
: undefined;
5557
value = typeof devicePath === "string" ? devicePath : "";
5658
} else {
59+
// NETWORK camera
60+
const profiles =
61+
details && typeof details === "object" && "profiles" in details
62+
? details["profiles"]
63+
: undefined;
64+
const hasProfiles = Array.isArray(profiles) && profiles.length > 0;
65+
66+
// Disable if network camera is not authorized (no profiles loaded)
67+
disabled = !hasProfiles;
68+
5769
const bestProfile =
5870
details && typeof details === "object" && "best_profile" in details
5971
? details["best_profile"]
@@ -67,10 +79,14 @@ const NodeDataPanel = ({
6779
value = typeof rtspUrl === "string" ? rtspUrl : "";
6880
}
6981

70-
return { label: camera.device_name, value };
82+
return {
83+
label: camera.device_name,
84+
value,
85+
disabled,
86+
};
7187
});
7288

73-
const videoOptions = videos.map((video) => ({
89+
const videoOptions = filterOutTransportStreams(videos).map((video) => ({
7490
label: video.filename,
7591
value: video.filename,
7692
}));
@@ -211,9 +227,6 @@ const NodeDataPanel = ({
211227
onChange={(e) => handleInputChange(keyStr, e.target.value)}
212228
className="w-full bg-background text-xs border border-gray-300 px-2 py-1"
213229
>
214-
<option value="">
215-
Select {propConfig?.label ?? keyStr}
216-
</option>
217230
{(selectedNode.type === "filesrc"
218231
? videoOptions
219232
: editableData.kind === "camera"
@@ -223,8 +236,21 @@ const NodeDataPanel = ({
223236
<option
224237
key={(option.value || option.label) as string}
225238
value={option.value}
239+
disabled={
240+
"disabled" in option
241+
? Boolean(option.disabled)
242+
: false
243+
}
244+
className={
245+
"disabled" in option && option.disabled
246+
? "text-gray-400 dark:text-gray-500"
247+
: ""
248+
}
226249
>
227250
{option.label}
251+
{"disabled" in option && option.disabled
252+
? " (Not authorized)"
253+
: ""}
228254
</option>
229255
))}
230256
</select>
@@ -234,7 +260,6 @@ const NodeDataPanel = ({
234260
onChange={(e) => handleInputChange(keyStr, e.target.value)}
235261
className="w-full bg-background text-xs border border-gray-300 px-2 py-1"
236262
>
237-
<option value="">Select {propConfig?.label}</option>
238263
{propConfig?.options?.map((option) => (
239264
<option key={option} value={option}>
240265
{option}

tools/visual-pipeline-and-platform-evaluation-tool/ui/src/features/pipelines/CreatePipelineDialog.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import {
7171
createPipelineSchema,
7272
} from "./pipelineSchemas";
7373
import { PipelineTagsCombobox } from "./PipelineTagsCombobox";
74+
import { isSupportedVideoFilename } from "@/lib/videoUtils.ts";
7475

7576
type CreatePipelineDialogProps = {
7677
children: ReactNode;
@@ -116,7 +117,9 @@ export const CreatePipelineDialog = ({
116117

117118
const tags = watch("tags");
118119

119-
const videoOptions = videos.map((v) => v.filename);
120+
const videoOptions = videos
121+
.filter((video) => isSupportedVideoFilename(video.filename))
122+
.map((video) => video.filename);
120123

121124
const [createPipeline, { isLoading: isCreating }] =
122125
useCreatePipelineMutation();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const isSupportedVideoFilename = (filename: string): boolean =>
2+
!filename.toLowerCase().endsWith(".ts");
3+
4+
export const filterOutTransportStreams = <T extends { filename: string }>(
5+
files: T[],
6+
): T[] => files.filter((file) => isSupportedVideoFilename(file.filename));

0 commit comments

Comments
 (0)