fall back to the next usable camera instead of failing the qr scan on one reporting no resolutions
What changed, and why it matters
This commit fixes a bug in Sparrow Wallet's QR code scanner. Previously, if one connected camera reported no supported video resolutions, the entire QR scan would fail. Now the app ignores such cameras and tries the next usable one instead. It also prevents a crash when the selected camera device becomes null. This is a robustness improvement, not a security vulnerability fix.
No immediate security action required. Treat as a normal bug-fix/robustness patch. Reviewers may verify that ignoring no-resolution cameras does not mask malicious virtual camera devices, though the diff provides no evidence of such a threat model.
Security signals we found
No security-relevant signals present in the diff
Change is defensive hardening against malformed/unusable camera state
Null-pointer-avoidance in device property listener
Evidence from the diff
The patch modifies QRScanDialog.java and WebcamService.java. In WebcamService, it filters availableDevices to exclude CaptureDevice instances with empty getFormats(), logs unsupported cameras, and loops through remaining devices if opening a stream throws an exception. Previously, the code selected the first device and threw UnsupportedOperationException if that single device had no formats. In QRScanDialog, the removal of stale foundDevices is deferred until after device matching, and the webcamDeviceProperty listener now null-checks newValue before calling Config setters or webcamService.cancel(). The changes improve error handling and device fallback behavior.
Changed components
src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.javasrc/main/java/com/sparrowwallet/sparrow/control/WebcamService.javaInspect captured patch +33 / −21
### src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
@@ -133,7 +133,6 @@ public QRScanDialog() {
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getAvailableDevices());
newDevices.removeAll(foundDevices);
foundDevices.addAll(newDevices);
- foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
if(webcamService.getDevice() != null) {
for(CaptureDevice device : foundDevices) {
@@ -143,6 +142,8 @@ public QRScanDialog() {
}
}
+ foundDevices.removeIf(device -> !webcamService.getAvailableDevices().contains(device));
+
updateList(availableResolutions, webcamService.getResolutions());
webcamResolutionProperty.set(webcamService.getResolution());
} finally {
@@ -189,10 +190,12 @@ public QRScanDialog() {
}
});
webcamDeviceProperty.addListener((_, _, newValue) -> {
- Config.get().setWebcamDevice(newValue.getName());
- Config.get().setWebcamDeviceId(newValue.getUniqueId());
- if(!Objects.equals(webcamService.getDevice(), newValue)) {
- webcamService.cancel();
+ if(newValue != null) {
+ Config.get().setWebcamDevice(newValue.getName());
+ Config.get().setWebcamDeviceId(newValue.getUniqueId());
+ if(!Objects.equals(webcamService.getDevice(), newValue)) {
+ webcamService.cancel();
+ }
}
});
### src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java
@@ -133,15 +133,24 @@ protected Image call() throws Exception {
try {
if(devices == null) {
devices = capture.getDevices();
- availableDevices = new ArrayList<>(devices);
+ availableDevices = devices.stream().filter(d -> !d.getFormats().isEmpty()).collect(Collectors.toCollection(ArrayList::new));
+
+ List<String> unsupportedDevices = devices.stream().filter(d -> d.getFormats().isEmpty()).map(CaptureDevice::getName).toList();
+ if(!unsupportedDevices.isEmpty()) {
+ log.warn("Ignoring cameras with no supported resolutions: " + String.join(", ", unsupportedDevices));
+ }
if(devices.isEmpty()) {
throw new UnsupportedOperationException("No cameras available");
}
+
+ if(availableDevices.isEmpty()) {
+ throw new UnsupportedOperationException("No resolutions supported by cameras " + String.join(", ", unsupportedDevices));
+ }
}
while(stream == null && !availableDevices.isEmpty()) {
- CaptureDevice selectedDevice = availableDevices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(availableDevices.getFirst());
+ CaptureDevice selectedDevice = availableDevices.getFirst();
if(device != null) {
for(CaptureDevice webcam : availableDevices) {
@@ -165,10 +174,6 @@ protected Image call() throws Exception {
device = selectedDevice;
- if(device.getFormats().isEmpty()) {
- throw new UnsupportedOperationException("No resolutions supported by camera " + device.getName());
- }
-
List<CaptureFormat> deviceFormats = new ArrayList<>(device.getFormats());
//On *nix prioritise supported camera pixel formats, preferring RGB3, then YUYV, then MJPG
@@ -206,23 +211,27 @@ protected Image call() throws Exception {
log.debug("Opening capture stream on " + device + " with format " + format.formatInfo().width() + "x" + format.formatInfo().height() + " (" + WebcamPixelFormat.fourCCToString(format.formatInfo().fourcc()) + ")");
}
- opening.set(true);
- stream = device.openStream(format);
- opening.set(false);
-
try {
- zoomLimits = stream.getPropertyLimits(CaptureProperty.ZOOM);
- } catch(Throwable e) {
- log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
+ opening.set(true);
+ stream = device.openStream(format);
+ } catch(Exception e) {
+ log.warn("Error opening capture stream on " + device.getName() + ", trying next available camera", e);
+ availableDevices.remove(device);
+ } finally {
+ opening.set(false);
}
- if(stream == null) {
- availableDevices.remove(device);
+ if(stream != null) {
+ try {
+ zoomLimits = stream.getPropertyLimits(CaptureProperty.ZOOM);
+ } catch(Throwable e) {
+ log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
+ }
}
}
if(stream == null) {
- throw new UnsupportedOperationException("No usable cameras available, tried " + devices);
+ throw new UnsupportedOperationException("No usable cameras available, tried " + devices.stream().map(CaptureDevice::getName).collect(Collectors.joining(", ")));
}
opened.set(true);Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.