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