Skip to content

Commit e79bad3

Browse files
committed
Fix installed vision hardware diagnostics
1 parent 6da4473 commit e79bad3

7 files changed

Lines changed: 448 additions & 38 deletions

File tree

app/camera-state.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ export function cameraErrorMessage(error) {
33
const message = String(error?.message || error || "").trim();
44
const normalized = `${name} ${message}`.toLowerCase();
55

6+
if (name === "CameraDeviceUnavailableError" || normalized.includes("could not open a usable camera")) {
7+
return "Camera devices were found, but Iris could not open a usable camera. Close other camera apps and check Windows camera privacy or driver settings, then try again.";
8+
}
9+
610
if (
711
name === "NotFoundError" ||
812
normalized.includes("requested device not found") ||
@@ -27,3 +31,84 @@ export function cameraErrorMessage(error) {
2731

2832
return message || "Camera snapshot failed.";
2933
}
34+
35+
export function rankCameraDevice(device) {
36+
const label = String(device?.label || "").toLowerCase();
37+
let score = 0;
38+
39+
if (label.includes("windows studio effects")) {
40+
score += 120;
41+
}
42+
if (label.includes("brio") || label.includes("webcam")) {
43+
score += 90;
44+
}
45+
if (label.includes("camera")) {
46+
score += 40;
47+
}
48+
if (label.includes("front")) {
49+
score += 8;
50+
}
51+
if (label.includes("ir") || label.includes("infrared") || label.includes("depth")) {
52+
score -= 120;
53+
}
54+
if (!label) {
55+
score -= 20;
56+
}
57+
58+
return score;
59+
}
60+
61+
export function cameraDiagnosticLabel(device, index) {
62+
const label = String(device?.label || "").trim();
63+
return label || `Camera ${index + 1}`;
64+
}
65+
66+
export function buildCameraCapturePlan(devices, videoConstraints) {
67+
const baseVideoConstraints = { ...videoConstraints };
68+
const attempts = [
69+
{
70+
attemptId: "default",
71+
label: "Default camera",
72+
constraints: {
73+
audio: false,
74+
video: baseVideoConstraints
75+
}
76+
}
77+
];
78+
79+
const rankedDevices = Array.from(devices || [])
80+
.map((device, index) => ({ device, index, score: rankCameraDevice(device) }))
81+
.filter(({ device }) => device?.kind === "videoinput" && device.deviceId)
82+
.sort((left, right) => right.score - left.score || left.index - right.index);
83+
84+
for (const { device, index } of rankedDevices) {
85+
attempts.push({
86+
attemptId: `device-${index}`,
87+
label: cameraDiagnosticLabel(device, index),
88+
constraints: {
89+
audio: false,
90+
video: {
91+
...baseVideoConstraints,
92+
deviceId: { exact: device.deviceId }
93+
}
94+
}
95+
});
96+
}
97+
98+
return attempts;
99+
}
100+
101+
export function cameraAttemptDiagnostic(attempt, error) {
102+
return {
103+
attemptId: String(attempt?.attemptId || "unknown"),
104+
label: String(attempt?.label || "Unknown camera"),
105+
errorName: String(error?.name || "Error"),
106+
errorMessage: String(error?.message || error || "Camera attempt failed.")
107+
};
108+
}
109+
110+
export function createCameraUnavailableError() {
111+
const error = new Error("Camera devices were found, but Iris could not open a usable camera.");
112+
error.name = "CameraDeviceUnavailableError";
113+
return error;
114+
}

app/camera-state.test.mjs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import assert from "node:assert/strict";
22
import { test } from "node:test";
3-
import { cameraErrorMessage } from "./camera-state.js";
3+
import {
4+
buildCameraCapturePlan,
5+
cameraAttemptDiagnostic,
6+
cameraErrorMessage,
7+
createCameraUnavailableError,
8+
rankCameraDevice
9+
} from "./camera-state.js";
410

511
test("camera errors show no-camera guidance instead of raw DOM exceptions", () => {
612
assert.equal(
@@ -26,3 +32,52 @@ test("busy camera errors show close-other-apps guidance", () => {
2632
test("unknown camera errors preserve useful details", () => {
2733
assert.equal(cameraErrorMessage(new Error("Camera driver crashed.")), "Camera driver crashed.");
2834
});
35+
36+
test("camera fallback errors show usable device recovery guidance", () => {
37+
assert.equal(
38+
cameraErrorMessage(createCameraUnavailableError()),
39+
"Camera devices were found, but Iris could not open a usable camera. Close other camera apps and check Windows camera privacy or driver settings, then try again."
40+
);
41+
});
42+
43+
test("camera ranking prefers visible usable cameras over IR cameras", () => {
44+
assert.ok(
45+
rankCameraDevice({ label: "Windows Studio Effects Camera" }) >
46+
rankCameraDevice({ label: "Surface IR Camera Front" })
47+
);
48+
assert.ok(
49+
rankCameraDevice({ label: "Camera MX Brio" }) >
50+
rankCameraDevice({ label: "Surface Camera Front" })
51+
);
52+
});
53+
54+
test("camera capture plan tries default then ranked device ids", () => {
55+
const plan = buildCameraCapturePlan(
56+
[
57+
{ kind: "audioinput", label: "Microphone", deviceId: "mic-1" },
58+
{ kind: "videoinput", label: "Surface IR Camera Front", deviceId: "ir-1" },
59+
{ kind: "videoinput", label: "Windows Studio Effects Camera", deviceId: "studio-1" },
60+
{ kind: "videoinput", label: "Surface Camera Front", deviceId: "front-1" }
61+
],
62+
{ width: { ideal: 640 }, height: { ideal: 480 } }
63+
);
64+
65+
assert.equal(plan[0].attemptId, "default");
66+
assert.equal(plan[1].label, "Windows Studio Effects Camera");
67+
assert.equal(plan[1].constraints.video.deviceId.exact, "studio-1");
68+
assert.equal(plan.at(-1).label, "Surface IR Camera Front");
69+
});
70+
71+
test("camera attempt diagnostics omit raw device ids", () => {
72+
const diagnostic = cameraAttemptDiagnostic(
73+
{ attemptId: "device-2", label: "Surface Camera Front" },
74+
new DOMException("Could not start video source", "NotReadableError")
75+
);
76+
77+
assert.deepEqual(diagnostic, {
78+
attemptId: "device-2",
79+
label: "Surface Camera Front",
80+
errorName: "NotReadableError",
81+
errorMessage: "Could not start video source"
82+
});
83+
});

app/main.js

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
} from "./attachment-state.js";
1212
import { requireTrustedBlobUrl } from "./attachment-url.js";
1313
import { latestBrowserPreview } from "./browser-preview.js";
14-
import { cameraErrorMessage } from "./camera-state.js";
14+
import {
15+
buildCameraCapturePlan,
16+
cameraAttemptDiagnostic,
17+
cameraErrorMessage,
18+
createCameraUnavailableError
19+
} from "./camera-state.js";
1520
import {
1621
formatDynamicContextStatus,
1722
parseDynamicContextCommand
@@ -1893,30 +1898,68 @@ async function captureCameraSnapshot() {
18931898
cameraCaptureInProgress = true;
18941899
elements.visionButton.disabled = true;
18951900
elements.hudOutput.textContent = "Camera starting.";
1896-
let stream = null;
1901+
const videoConstraints = {
1902+
width: { ideal: cameraSnapshotWidth, max: cameraSnapshotWidth },
1903+
height: { ideal: cameraSnapshotHeight, max: cameraSnapshotHeight },
1904+
frameRate: { ideal: 5, max: 10 }
1905+
};
1906+
const devices = await enumerateCameraDevices();
1907+
const capturePlan = buildCameraCapturePlan(devices, videoConstraints);
1908+
const attempts = [];
18971909
try {
1898-
stream = await navigator.mediaDevices.getUserMedia({
1899-
audio: false,
1900-
video: {
1901-
width: { ideal: cameraSnapshotWidth, max: cameraSnapshotWidth },
1902-
height: { ideal: cameraSnapshotHeight, max: cameraSnapshotHeight },
1903-
frameRate: { ideal: 5, max: 10 }
1904-
}
1905-
});
1906-
const snapshot = await snapshotFromStream(stream);
1907-
await saveCameraSnapshotDiagnostic(snapshot);
1908-
return snapshot;
1909-
} finally {
1910-
if (stream) {
1911-
for (const track of stream.getTracks()) {
1912-
track.stop();
1910+
for (const attempt of capturePlan) {
1911+
let stream = null;
1912+
try {
1913+
elements.hudOutput.textContent =
1914+
attempt.attemptId === "default"
1915+
? "Camera starting."
1916+
: `Trying ${attempt.label}.`;
1917+
stream = await navigator.mediaDevices.getUserMedia(attempt.constraints);
1918+
const snapshot = await snapshotFromStream(stream);
1919+
await saveCameraSnapshotDiagnostic(snapshot, attempt, attempts.length + 1);
1920+
return snapshot;
1921+
} catch (error) {
1922+
attempts.push(cameraAttemptDiagnostic(attempt, error));
1923+
} finally {
1924+
if (stream) {
1925+
for (const track of stream.getTracks()) {
1926+
track.stop();
1927+
}
1928+
}
19131929
}
19141930
}
1931+
1932+
const error =
1933+
capturePlan.length > 1
1934+
? createCameraUnavailableError()
1935+
: cameraErrorFromAttemptDiagnostic(attempts[0]);
1936+
await saveCameraCaptureErrorDiagnostic(error.message, attempts);
1937+
throw error;
1938+
} finally {
19151939
cameraCaptureInProgress = false;
19161940
elements.visionButton.disabled = false;
19171941
}
19181942
}
19191943

1944+
function cameraErrorFromAttemptDiagnostic(attempt) {
1945+
const error = new Error(attempt?.errorMessage || "Camera snapshot failed.");
1946+
error.name = attempt?.errorName || "Error";
1947+
return error;
1948+
}
1949+
1950+
async function enumerateCameraDevices() {
1951+
if (!navigator.mediaDevices?.enumerateDevices) {
1952+
return [];
1953+
}
1954+
1955+
try {
1956+
return await navigator.mediaDevices.enumerateDevices();
1957+
} catch (error) {
1958+
logVoice("camera_enumerate_error", String(error));
1959+
return [];
1960+
}
1961+
}
1962+
19201963
async function snapshotFromStream(stream) {
19211964
const video = document.createElement("video");
19221965
video.muted = true;
@@ -1953,23 +1996,38 @@ async function snapshotFromStream(stream) {
19531996
};
19541997
}
19551998

1956-
async function saveCameraSnapshotDiagnostic(snapshot) {
1999+
async function saveCameraSnapshotDiagnostic(snapshot, selectedAttempt, attemptCount) {
19572000
try {
19582001
const diagnostic = await call("save_camera_snapshot_diagnostic", {
19592002
imageBytes: snapshot.bytes,
19602003
width: snapshot.width,
1961-
height: snapshot.height
2004+
height: snapshot.height,
2005+
selectedDeviceLabel: selectedAttempt?.label || null,
2006+
attemptCount
19622007
});
19632008
const imagePath = diagnostic.imagePath || diagnostic.image_path || "";
19642009
logVoice(
19652010
"camera_snapshot_diagnostic",
1966-
`bytes=${snapshot.bytes.length}; width=${snapshot.width}; height=${snapshot.height}; path=${imagePath}`
2011+
`bytes=${snapshot.bytes.length}; width=${snapshot.width}; height=${snapshot.height}; device=${selectedAttempt?.label || "unknown"}; attempts=${attemptCount}; path=${imagePath}`
19672012
);
19682013
} catch (error) {
19692014
logVoice("camera_snapshot_diagnostic_error", String(error));
19702015
}
19712016
}
19722017

2018+
async function saveCameraCaptureErrorDiagnostic(message, attempts) {
2019+
try {
2020+
const diagnostic = await call("save_camera_capture_error_diagnostic", {
2021+
message,
2022+
attempts
2023+
});
2024+
const jsonPath = diagnostic.jsonPath || diagnostic.json_path || "";
2025+
logVoice("camera_capture_error_diagnostic", `attempts=${attempts.length}; path=${jsonPath}`);
2026+
} catch (error) {
2027+
logVoice("camera_capture_error_diagnostic_error", String(error));
2028+
}
2029+
}
2030+
19732031
async function snapshotFromVideoFile(file) {
19742032
validateVideoSize(file);
19752033
const video = document.createElement("video");

scripts/validate_site.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ const sitemap = readFileSync(join(siteDir, "sitemap.xml"), "utf8");
3030
const llms = readFileSync(join(siteDir, "llms.txt"), "utf8");
3131

3232
const expectedHashes = new Map([
33-
["iris-windows-installer.zip", "6c6219cde08729eb579d58fb31ac49a3f937a0f96c85f4933aa98b6283973215"],
34-
["iris-windows.zip", "843b0ff6463e2f18f78ceab4294f6f4238d7df31e7c98ebbaf4eb05ad0e4687b"],
35-
["install-iris-windows.ps1", "9ec2016adfa53e2bbd9035c2b55d149f8017f499d9e5b1550cd5191dc49f4038"],
33+
["iris-windows-installer.zip", "a38fa91c1674dd5997ac67e671cd92d49b6f519f01c4f10deccc1fc6ee3800ab"],
34+
["iris-windows.zip", "a346e276ffc08717e05634b9915006a21693b75970455e10336e3d57ce83e9fd"],
35+
["install-iris-windows.ps1", "a17c96b685c11416f6510891bad3b0c201d4ddfd014be0ea91c52b8e13f0f9cc"],
3636
]);
3737

3838
const requiredFragments = [

site/index.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,15 @@ <h3 id="checksum-title">Release checksums</h3>
146146
<dl>
147147
<div>
148148
<dt>iris-windows-installer.zip</dt>
149-
<dd><code>6c6219cde08729eb579d58fb31ac49a3f937a0f96c85f4933aa98b6283973215</code></dd>
149+
<dd><code>a38fa91c1674dd5997ac67e671cd92d49b6f519f01c4f10deccc1fc6ee3800ab</code></dd>
150150
</div>
151151
<div>
152152
<dt>iris-windows.zip</dt>
153-
<dd><code>843b0ff6463e2f18f78ceab4294f6f4238d7df31e7c98ebbaf4eb05ad0e4687b</code></dd>
153+
<dd><code>a346e276ffc08717e05634b9915006a21693b75970455e10336e3d57ce83e9fd</code></dd>
154154
</div>
155155
<div>
156156
<dt>install-iris-windows.ps1</dt>
157-
<dd><code>9ec2016adfa53e2bbd9035c2b55d149f8017f499d9e5b1550cd5191dc49f4038</code></dd>
157+
<dd><code>a17c96b685c11416f6510891bad3b0c201d4ddfd014be0ea91c52b8e13f0f9cc</code></dd>
158158
</div>
159159
</dl>
160160
<p>

site/release-manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,17 @@
66
{
77
"name": "iris-windows-installer.zip",
88
"url": "https://github.com/supermang617/IRIS/releases/download/v1/iris-windows-installer.zip",
9-
"sha256": "6c6219cde08729eb579d58fb31ac49a3f937a0f96c85f4933aa98b6283973215"
9+
"sha256": "a38fa91c1674dd5997ac67e671cd92d49b6f519f01c4f10deccc1fc6ee3800ab"
1010
},
1111
{
1212
"name": "iris-windows.zip",
1313
"url": "https://github.com/supermang617/IRIS/releases/download/v1/iris-windows.zip",
14-
"sha256": "843b0ff6463e2f18f78ceab4294f6f4238d7df31e7c98ebbaf4eb05ad0e4687b"
14+
"sha256": "a346e276ffc08717e05634b9915006a21693b75970455e10336e3d57ce83e9fd"
1515
},
1616
{
1717
"name": "install-iris-windows.ps1",
1818
"url": "https://github.com/supermang617/IRIS/releases/download/v1/install-iris-windows.ps1",
19-
"sha256": "9ec2016adfa53e2bbd9035c2b55d149f8017f499d9e5b1550cd5191dc49f4038"
19+
"sha256": "a17c96b685c11416f6510891bad3b0c201d4ddfd014be0ea91c52b8e13f0f9cc"
2020
}
2121
]
2222
}

0 commit comments

Comments
 (0)