iOS pairing scanner: add capture diagnostics (temporary)
Black preview persists on a real device though the session reports running, so instrument the capture lifecycle to pinpoint it: logs the authorization status, the selected device, isRunning/inputs/outputs after startRunning, and on AVCaptureSessionDidStartRunning the preview layer's connection (present/active/enabled) plus the view/layer bounds. Also observes AVCaptureSessionRuntimeError and ...WasInterrupted. Filterable via os.Logger subsystem com.nucleic.remote / category scanner (marker "📷"). To be trimmed once the root cause is fixed. Also sets an explicit .high preset and gates metadataObjectTypes on availability. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b5853eee67
commit
783aac7683
@@ -1,6 +1,11 @@
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
import NucleicProtocol
|
||||
import os
|
||||
|
||||
/// Filterable scanner diagnostics — in Console.app / Xcode, filter by subsystem
|
||||
/// `com.nucleic.remote` (category `scanner`) or search the "📷" marker.
|
||||
private let scannerLog = Logger(subsystem: "com.nucleic.remote", category: "scanner")
|
||||
|
||||
/// Why the live camera feed isn't showing, so the scanner never sits on a silent black screen.
|
||||
enum ScannerCameraState: Equatable {
|
||||
@@ -127,13 +132,24 @@ final class QRScannerController: UIViewController, AVCaptureMetadataOutputObject
|
||||
previewView.previewLayer.session = session
|
||||
previewView.previewLayer.videoGravity = .resizeAspectFill
|
||||
|
||||
let nc = NotificationCenter.default
|
||||
nc.addObserver(self, selector: #selector(sessionDidStart),
|
||||
name: .AVCaptureSessionDidStartRunning, object: session)
|
||||
nc.addObserver(self, selector: #selector(sessionRuntimeError(_:)),
|
||||
name: .AVCaptureSessionRuntimeError, object: session)
|
||||
nc.addObserver(self, selector: #selector(sessionInterrupted(_:)),
|
||||
name: .AVCaptureSessionWasInterrupted, object: session)
|
||||
|
||||
// Gate on camera authorization first — without this the prompt may never appear and the
|
||||
// feed silently fails (and on a denied device it would just stay black).
|
||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||
scannerLog.notice("📷 authorization status=\(status.rawValue, privacy: .public)")
|
||||
switch status {
|
||||
case .authorized:
|
||||
configureSession()
|
||||
case .notDetermined:
|
||||
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||
scannerLog.notice("📷 requestAccess granted=\(granted, privacy: .public)")
|
||||
granted ? self?.configureSession() : self?.report(.denied)
|
||||
}
|
||||
case .denied, .restricted:
|
||||
@@ -149,27 +165,57 @@ final class QRScannerController: UIViewController, AVCaptureMetadataOutputObject
|
||||
sessionQueue.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.session.beginConfiguration()
|
||||
guard let device = AVCaptureDevice.default(for: .video),
|
||||
let input = try? AVCaptureDeviceInput(device: device),
|
||||
self.session.canAddInput(input) else {
|
||||
self.session.commitConfiguration(); self.report(.unavailable); return
|
||||
if self.session.canSetSessionPreset(.high) { self.session.sessionPreset = .high }
|
||||
guard let device = AVCaptureDevice.default(for: .video) else {
|
||||
self.session.commitConfiguration()
|
||||
scannerLog.error("📷 no video device — reporting unavailable")
|
||||
self.report(.unavailable); return
|
||||
}
|
||||
scannerLog.notice("📷 device=\(device.localizedName, privacy: .public)")
|
||||
guard let input = try? AVCaptureDeviceInput(device: device), self.session.canAddInput(input) else {
|
||||
self.session.commitConfiguration()
|
||||
scannerLog.error("📷 cannot create/add camera input")
|
||||
self.report(.unavailable); return
|
||||
}
|
||||
self.session.addInput(input)
|
||||
|
||||
let output = AVCaptureMetadataOutput()
|
||||
guard self.session.canAddOutput(output) else {
|
||||
self.session.commitConfiguration(); self.report(.unavailable); return
|
||||
self.session.commitConfiguration()
|
||||
scannerLog.error("📷 cannot add metadata output")
|
||||
self.report(.unavailable); return
|
||||
}
|
||||
self.session.addOutput(output)
|
||||
output.setMetadataObjectsDelegate(self, queue: .main)
|
||||
output.metadataObjectTypes = [.qr]
|
||||
self.session.commitConfiguration()
|
||||
// availableMetadataObjectTypes is only populated after the output is connected.
|
||||
output.metadataObjectTypes = output.availableMetadataObjectTypes.contains(.qr)
|
||||
? [.qr] : output.availableMetadataObjectTypes
|
||||
|
||||
self.session.startRunning()
|
||||
scannerLog.notice("📷 startRunning -> isRunning=\(self.session.isRunning, privacy: .public) inputs=\(self.session.inputs.count, privacy: .public) outputs=\(self.session.outputs.count, privacy: .public)")
|
||||
self.report(.scanning)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func sessionDidStart() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
let connection = self.previewView.previewLayer.connection
|
||||
scannerLog.notice("📷 didStartRunning: previewConnection=\(connection != nil, privacy: .public) active=\(connection?.isActive ?? false, privacy: .public) enabled=\(connection?.isEnabled ?? false, privacy: .public) viewBounds=\(self.view.bounds.debugDescription, privacy: .public) layerBounds=\(self.previewView.previewLayer.bounds.debugDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func sessionRuntimeError(_ note: Notification) {
|
||||
let error = note.userInfo?[AVCaptureSessionErrorKey] as? NSError
|
||||
scannerLog.error("📷 runtimeError=\(error?.description ?? "nil", privacy: .public)")
|
||||
}
|
||||
|
||||
@objc private func sessionInterrupted(_ note: Notification) {
|
||||
let reason = (note.userInfo?[AVCaptureSessionInterruptionReasonKey] as? Int) ?? -1
|
||||
scannerLog.error("📷 sessionInterrupted reason=\(reason, privacy: .public)")
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
sessionQueue.async { [session] in if session.isRunning { session.stopRunning() } }
|
||||
|
||||
Reference in New Issue
Block a user