1pub fn is_real_camera(name: &str) -> bool {
7 let name_lower = name.to_lowercase();
8 !name_lower.contains("infrared")
9 && !name_lower.contains("ir camera")
10 && !name_lower.contains("integrated i")
11 && !name_lower.contains("integrated ir")
12 && !name_lower.contains("depth camera")
13}
14
15pub fn clean_camera_name(name: &str) -> String {
16 let trimmed = name.trim();
17 if trimmed.starts_with("Integrated Camera:") {
18 return "Integrated Camera".to_string();
19 }
20 if trimmed.starts_with("Integrated Webcam:") {
21 return "Integrated Webcam".to_string();
22 }
23 trimmed.to_string()
24}
25
26#[cfg(target_os = "macos")]
27pub fn parse_macos_camera(stdout: &str) -> Vec<String> {
28 let mut devices = Vec::new();
29 let mut in_cameras = false;
30 for line in stdout.lines() {
31 let trimmed = line.trim();
32 let indent = line.len() - line.trim_start().len();
33 if trimmed.starts_with("Video Support:")
34 || trimmed.starts_with("Camera:")
35 || trimmed.starts_with("Cameras:")
36 {
37 in_cameras = true;
38 continue;
39 }
40 if in_cameras {
41 if indent < 4
42 && !trimmed.is_empty()
43 && !trimmed.starts_with("Camera")
44 && !trimmed.starts_with("Video Support")
45 {
46 in_cameras = false;
47 continue;
48 }
49 if (indent == 4 || indent == 6 || indent == 8) && trimmed.ends_with(':') {
50 let name = trimmed.trim_end_matches(':').trim().to_string();
51 if !name.is_empty() && is_real_camera(&name) {
52 let cleaned = clean_camera_name(&name);
53 if !devices.contains(&cleaned) {
54 devices.push(cleaned);
55 }
56 }
57 }
58 }
59 }
60 devices
61}
62
63pub(crate) fn detect_camera() -> Vec<String> {
64 #[cfg(target_os = "linux")]
65 {
66 let mut cameras = Vec::new();
67 if let Ok(entries) = std::fs::read_dir("/sys/class/video4linux") {
68 for entry in entries.filter_map(|e| e.ok()) {
69 let path = entry.path().join("name");
70 if path.exists() {
71 if let Ok(name) = std::fs::read_to_string(path) {
72 let trimmed = name.trim().to_string();
73 if !trimmed.is_empty() && is_real_camera(&trimmed) {
74 let cleaned = clean_camera_name(&trimmed);
75 if !cameras.contains(&cleaned) {
76 cameras.push(cleaned);
77 }
78 }
79 }
80 }
81 }
82 }
83 cameras
84 }
85
86 #[cfg(target_os = "macos")]
87 {
88 let mut cameras = crate::macos_ffi::get_usb_cameras();
89 cameras.retain(|name| is_real_camera(name));
91 cameras
92 }
93
94 #[cfg(target_os = "windows")]
95 {
96 let mut cameras = Vec::new();
100 for guid in [
101 &crate::win_setupapi::GUID_DEVCLASS_CAMERA,
102 &crate::win_setupapi::GUID_DEVCLASS_IMAGE,
103 ] {
104 for name in crate::win_setupapi::present_device_names(guid) {
105 if is_real_camera(&name) {
106 let cleaned = clean_camera_name(&name);
107 if !cleaned.is_empty() && !cameras.contains(&cleaned) {
108 cameras.push(cleaned);
109 }
110 }
111 }
112 }
113 cameras
114 }
115
116 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
117 {
118 Vec::new()
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_is_real_camera() {
128 assert!(!is_real_camera("Infrared Camera"));
129 assert!(!is_real_camera("IR Camera"));
130 assert!(!is_real_camera("Integrated IR Camera"));
131 assert!(!is_real_camera("Depth Camera"));
132 assert!(is_real_camera("FaceTime HD Camera"));
133 assert!(is_real_camera("Integrated Camera"));
134 assert!(is_real_camera("HD Webcam C920"));
135 }
136
137 #[test]
138 fn test_clean_camera_name() {
139 assert_eq!(
140 clean_camera_name("Integrated Camera: Real"),
141 "Integrated Camera"
142 );
143 assert_eq!(
144 clean_camera_name("Integrated Webcam: HD"),
145 "Integrated Webcam"
146 );
147 assert_eq!(clean_camera_name(" HD Webcam C920 "), "HD Webcam C920");
148 assert_eq!(
149 clean_camera_name("FaceTime HD Camera"),
150 "FaceTime HD Camera"
151 );
152 }
153
154 #[cfg(target_os = "macos")]
155 #[test]
156 fn test_parse_macos_camera() {
157 let sample = "Camera:\n\n FaceTime HD Camera:\n\n Model ID: UVC Camera VendorID_1452 ProductID_34068\n Unique ID: 0x8020000005ac8514\n";
158 assert_eq!(
159 parse_macos_camera(sample),
160 vec!["FaceTime HD Camera".to_string()]
161 );
162 }
163}