fix: QR scanner - move camera init to background thread, window shows immediately

This commit is contained in:
yumoqing 2026-06-23 23:03:48 +08:00
parent 6d1ff50074
commit f7c295c653

View File

@ -4,16 +4,17 @@ import AVFoundation
#if os(macOS) #if os(macOS)
import AppKit import AppKit
// MARK: - NSWindow SwiftUI sheet // MARK: - NSWindow
class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegate { class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegate {
var window: NSWindow? var window: NSWindow?
var session: AVCaptureSession? var session: AVCaptureSession?
var previewLayer: AVCaptureVideoPreviewLayer?
var hostView: QRScannerHostView?
weak var bridge: PlayerBridge? weak var bridge: PlayerBridge?
private var lastDetected: String? private var lastDetected: String?
private var lastDetectedTime: Date = .distantPast private var lastDetectedTime: Date = .distantPast
private weak var resultLabel: NSTextField?
private weak var previewHost: NSView?
private weak var loadingLabel: NSTextField?
init(bridge: PlayerBridge) { init(bridge: PlayerBridge) {
self.bridge = bridge self.bridge = bridge
@ -36,25 +37,37 @@ class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegat
win.isReleasedWhenClosed = false win.isReleasedWhenClosed = false
win.center() win.center()
// 宿 let container = NSView(frame: win.contentView!.bounds)
let container = NSView(frame: win.contentRect(forFrameRect: win.frame).insetBy(dx: 0, dy: 0)) container.autoresizingMask = [.width, .height]
container.wantsLayer = true container.wantsLayer = true
container.layer?.backgroundColor = NSColor.black.cgColor container.layer?.backgroundColor = NSColor.black.cgColor
let hostView = QRScannerHostView(frame: NSRect(x: 20, y: 80, width: 360, height: 240)) //
container.addSubview(hostView) let previewHost = NSView(frame: NSRect(x: 20, y: 80, width: 360, height: 240))
self.hostView = hostView previewHost.wantsLayer = true
previewHost.layer?.backgroundColor = NSColor.darkGray.cgColor
previewHost.autoresizingMask = [.width, .height]
container.addSubview(previewHost)
self.previewHost = previewHost
// "..."
let loadingLabel = NSTextField(labelWithString: "Initializing camera...")
loadingLabel.font = NSFont.systemFont(ofSize: 12)
loadingLabel.textColor = .secondaryLabelColor
loadingLabel.frame = NSRect(x: 0, y: 100, width: 360, height: 40)
loadingLabel.alignment = .center
previewHost.addSubview(loadingLabel)
self.loadingLabel = loadingLabel
// //
let label = NSTextField(labelWithString: L.scanHint) let hintLabel = NSTextField(wrappingLabelWithString: L.scanHint)
label.font = NSFont.systemFont(ofSize: 11) hintLabel.font = NSFont.systemFont(ofSize: 11)
label.textColor = .secondaryLabelColor hintLabel.textColor = .secondaryLabelColor
label.frame = NSRect(x: 20, y: 20, width: 360, height: 40) hintLabel.frame = NSRect(x: 20, y: 20, width: 360, height: 40)
label.alignment = .center hintLabel.alignment = .center
label.lineBreakMode = .byWordWrapping container.addSubview(hintLabel)
container.addSubview(label)
// //
let resultLabel = NSTextField(labelWithString: "") let resultLabel = NSTextField(labelWithString: "")
resultLabel.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) resultLabel.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
resultLabel.textColor = .labelColor resultLabel.textColor = .labelColor
@ -63,29 +76,41 @@ class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegat
resultLabel.lineBreakMode = .byTruncatingMiddle resultLabel.lineBreakMode = .byTruncatingMiddle
resultLabel.tag = 100 resultLabel.tag = 100
container.addSubview(resultLabel) container.addSubview(resultLabel)
self.resultLabel = resultLabel
win.contentView = container win.contentView = container
//
setupCapture(hostView: hostView)
win.makeKeyAndOrderFront(nil) win.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
self.window = win self.window = win
// 线
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.setupCaptureOnBackground(previewHost: previewHost)
}
} }
func close() { func close() {
session?.stopRunning() session?.stopRunning()
window?.close() DispatchQueue.main.async { [weak self] in
self?.window?.close()
}
} }
private func setupCapture(hostView: QRScannerHostView) { // 线
private func setupCaptureOnBackground(previewHost: NSView) {
let session = AVCaptureSession() let session = AVCaptureSession()
self.session = session
guard let device = AVCaptureDevice.default(for: .video), guard let device = AVCaptureDevice.default(for: .video) else {
let input = try? AVCaptureDeviceInput(device: device) else { DispatchQueue.main.async { [weak self] in
showError("No camera available") self?.showError("No camera available")
}
return
}
guard let input = try? AVCaptureDeviceInput(device: device) else {
DispatchQueue.main.async { [weak self] in
self?.showError("Cannot access camera")
}
return return
} }
@ -102,22 +127,34 @@ class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegat
let previewLayer = AVCaptureVideoPreviewLayer(session: session) let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.videoGravity = .resizeAspectFill previewLayer.videoGravity = .resizeAspectFill
hostView.previewLayer = previewLayer
self.previewLayer = previewLayer
// self.session = session
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
session.startRunning() // 线 UI
DispatchQueue.main.async { DispatchQueue.main.async { [weak self] in
self?.previewLayer?.frame = hostView.bounds guard let self = self, previewHost.window != nil else {
//
session.stopRunning()
return
}
// loading
self.loadingLabel?.removeFromSuperview()
//
previewLayer.frame = previewHost.bounds
previewHost.layer?.addSublayer(previewLayer)
//
DispatchQueue.global(qos: .userInitiated).async {
session.startRunning()
} }
} }
} }
private func showError(_ msg: String) { private func showError(_ msg: String) {
guard let resultLabel = window?.contentView?.viewWithTag(100) as? NSTextField else { return } resultLabel?.stringValue = msg
resultLabel.stringValue = msg resultLabel?.textColor = .systemRed
resultLabel.textColor = .systemRed
} }
// MARK: - AVCaptureMetadataOutputObjectsDelegate // MARK: - AVCaptureMetadataOutputObjectsDelegate
@ -125,7 +162,6 @@ class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegat
guard let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject, guard let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject,
let code = metadata.stringValue else { return } let code = metadata.stringValue else { return }
// 3
let now = Date() let now = Date()
if code == lastDetected, now.timeIntervalSince(lastDetectedTime) < 3 { if code == lastDetected, now.timeIntervalSince(lastDetectedTime) < 3 {
return return
@ -134,18 +170,14 @@ class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegat
lastDetectedTime = now lastDetectedTime = now
// //
if let resultLabel = window?.contentView?.viewWithTag(100) as? NSTextField { resultLabel?.stringValue = code
resultLabel.stringValue = code resultLabel?.textColor = .systemGreen
resultLabel.textColor = .systemGreen
}
// URL // URL
if let url = URL(string: code), (url.scheme == "http" || url.scheme == "https") { if let url = URL(string: code), (url.scheme == "http" || url.scheme == "https") {
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.bridge?.addURL(code) self?.bridge?.addURL(code)
} }
//
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
self?.close() self?.close()
} }
@ -157,23 +189,4 @@ class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegat
} }
} }
// MARK: - 宿
class QRScannerHostView: NSView {
var previewLayer: AVCaptureVideoPreviewLayer?
override init(frame: CGRect) {
super.init(frame: frame)
wantsLayer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layout() {
super.layout()
previewLayer?.frame = bounds
}
}
#endif #endif