Skip to content

Commit a7e8768

Browse files
maxxgxCopilot
andauthored
feat(inspect): improve cameras picker (#3194)
* camera unique names and auto name completion Signed-off-by: Max Xiang <xiangxiang.ma@intel.com> * fix Signed-off-by: Max Xiang <xiangxiang.ma@intel.com> * fix name Signed-off-by: Max Xiang <xiangxiang.ma@intel.com> * Update application/ui/src/features/inspect/toolbar/sources/webcam/webcam-fields.component.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Max Xiang <maxx.rift@gmail.com> * Update application/ui/src/features/inspect/toolbar/sources/webcam/webcam-fields.component.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Max Xiang <maxx.rift@gmail.com> * Update application/ui/src/features/inspect/toolbar/sources/webcam/webcam-fields.component.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Max Xiang <maxx.rift@gmail.com> * Update application/ui/src/features/inspect/toolbar/sources/webcam/webcam-fields.component.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Max Xiang <maxx.rift@gmail.com> * style fix Signed-off-by: Max Xiang <xiangxiang.ma@intel.com> --------- Signed-off-by: Max Xiang <xiangxiang.ma@intel.com> Signed-off-by: Max Xiang <maxx.rift@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent fb43b08 commit a7e8768

File tree

4 files changed

+62
-8
lines changed

4 files changed

+62
-8
lines changed

application/backend/src/utils/devices.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (C) 2025 Intel Corporation
22
# SPDX-License-Identifier: Apache-2.0
3+
from collections import defaultdict
34
from functools import lru_cache
45
from typing import TypedDict
56

@@ -18,12 +19,23 @@ class Devices:
1819

1920
@staticmethod
2021
def get_webcam_devices() -> list[CameraInfo]:
21-
"""Get list of available webcam devices.
22+
"""
23+
Get list of available webcam devices.
24+
If duplicate names are present, append a suffix to make them unique.
25+
Example: ["camera", "camera"] -> ["camera", "camera (1)"]
2226
2327
Returns:
2428
list[CameraInfo]: List of dictionaries containing camera index and name.
2529
"""
26-
return [{"index": cam.index, "name": cam.name} for cam in cv2_enumerate_cameras.enumerate_cameras()]
30+
names_count: dict[str, int] = defaultdict(int)
31+
cameras: list[CameraInfo] = []
32+
for cam in cv2_enumerate_cameras.enumerate_cameras():
33+
duplicate_count = names_count[cam.name]
34+
duplicate_suffix = f" ({duplicate_count})" if duplicate_count > 0 else ""
35+
unique_camera_name = f"{cam.name}{duplicate_suffix}"
36+
names_count[cam.name] += 1
37+
cameras.append(CameraInfo(index=cam.index, name=unique_camera_name))
38+
return cameras
2739

2840
@staticmethod
2941
@lru_cache

application/backend/tests/unit/utils/test_devices.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,18 @@ def test_get_webcam_devices(self, mock_enumerate: MagicMock) -> None:
2323
assert cameras[0]["index"] == 1
2424
assert cameras[0]["name"] == "Linux Camera"
2525
mock_enumerate.assert_called_once()
26+
27+
@patch("utils.devices.cv2_enumerate_cameras.enumerate_cameras")
28+
def test_get_webcam_devices_duplicate_names(self, mock_enumerate: MagicMock) -> None:
29+
"""Ensure duplicate camera names are suffixed to remain unique."""
30+
31+
def build_cam(index: int, name: str) -> MagicMock:
32+
cam = MagicMock()
33+
cam.index = index
34+
cam.name = name
35+
return cam
36+
37+
mock_enumerate.return_value = [build_cam(0, "nikon"), build_cam(1, "nikon")]
38+
cameras = Devices.get_webcam_devices()
39+
assert [cam["name"] for cam in cameras] == ["nikon", "nikon (1)"]
40+
assert [cam["index"] for cam in cameras] == [0, 1]

application/ui/src/features/inspect/toolbar/sources/source-list/settings-list/settings-list.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ interface SettingsListProps {
1111
const CameraDeviceDisplay = ({ deviceId }: { deviceId: number }) => {
1212
const { data: cameraDevices, isLoading } = $api.useQuery('get', '/api/devices/camera');
1313
const devices = cameraDevices?.devices ?? [];
14-
const device = devices[deviceId];
14+
const device = devices.find((d) => d.index === deviceId);
1515

1616
if (isLoading) {
1717
return <span>Loading...</span>;

application/ui/src/features/inspect/toolbar/sources/webcam/webcam-fields.component.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Copyright (C) 2025 Intel Corporation
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { useState } from 'react';
5+
46
import { $api } from '@geti-inspect/api';
5-
import { ActionButton, Flex, Item, Loading, Picker, TextField } from '@geti/ui';
7+
import { ActionButton, Flex, Item, Key, Loading, Picker, TextField } from '@geti/ui';
68
import { Refresh } from '@geti/ui/icons';
79

810
import { WebcamSourceConfig } from '../util';
@@ -13,25 +15,50 @@ type WebcamFieldsProps = {
1315

1416
export const WebcamFields = ({ defaultState }: WebcamFieldsProps) => {
1517
const { data: cameraDevices, isLoading, isRefetching, refetch } = $api.useQuery('get', '/api/devices/camera');
18+
const [name, setName] = useState(defaultState.name);
19+
const [isModified, setIsModified] = useState(false);
20+
21+
const devices = (cameraDevices?.devices ?? []).map((device) => ({
22+
id: device.index,
23+
name: device.name,
24+
}));
25+
26+
const handleNameChange = (value: string) => {
27+
setName(value);
28+
setIsModified(true);
29+
};
30+
31+
const handleSelectionChange = (key: Key | null) => {
32+
if (key === null) {
33+
return;
34+
}
35+
36+
const device = devices.find((d) => d.id === Number(key));
1637

17-
const devices = (cameraDevices?.devices ?? []).map((device, index) => ({ name: device.name, id: index }));
38+
// if user modifies the name field, don't override it
39+
if (device && (!isModified || !name?.trim())) {
40+
setName(device.name);
41+
}
42+
};
1843

1944
return (
2045
<Flex direction='column' gap='size-200'>
2146
<TextField isHidden label='id' name='id' defaultValue={defaultState?.id} />
2247
<TextField isHidden label='project_id' name='project_id' defaultValue={defaultState.project_id} />
23-
<TextField width={'100%'} label='Name' name='name' defaultValue={defaultState.name} />
48+
<TextField isHidden label='name' name='name' value={name} />
49+
<TextField width={'100%'} label='Name' name='name_display' value={name} onChange={handleNameChange} />
2450

2551
<Flex alignItems='end' gap='size-200'>
2652
<Picker
2753
flex='1'
28-
label='Cameras'
54+
label='Camera'
2955
name='device_id'
3056
items={devices}
3157
isLoading={isLoading}
3258
defaultSelectedKey={defaultState.device_id}
59+
onSelectionChange={handleSelectionChange}
3360
>
34-
{(item) => <Item>{item.name}</Item>}
61+
{(item) => <Item key={item.id}>{item.name}</Item>}
3562
</Picker>
3663

3764
<ActionButton

0 commit comments

Comments
 (0)