import SwiftUI import AVFoundation #if os(macOS) import AppKit // MARK: - 扫码窗口控制器(独立 NSWindow) class QRScannerWindowController: NSObject, AVCaptureMetadataOutputObjectsDelegate { var window: NSWindow? var session: AVCaptureSession? 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 super.init() } func show() { if let win = window, win.isVisible { win.makeKeyAndOrderFront(nil) return } let win = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 400, height: 380), styleMask: [.titled, .closable], backing: .buffered, defer: false ) win.title = L.scanQRCode win.isReleasedWhenClosed = false win.center() let container = NSView(frame: win.contentView!.bounds) container.autoresizingMask = [.width, .height] container.wantsLayer = true container.layer?.backgroundColor = NSColor.black.cgColor // 摄像头预览区域(占位) 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 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 resultLabel.frame = NSRect(x: 20, y: 330, width: 360, height: 30) resultLabel.alignment = .center resultLabel.lineBreakMode = .byTruncatingMiddle resultLabel.tag = 100 container.addSubview(resultLabel) self.resultLabel = resultLabel win.contentView = container 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() DispatchQueue.main.async { [weak self] in self?.window?.close() } } // 在后台线程初始化摄像头 private func setupCaptureOnBackground(previewHost: NSView) { let session = AVCaptureSession() 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 } if session.canAddInput(input) { session.addInput(input) } let output = AVCaptureMetadataOutput() if session.canAddOutput(output) { session.addOutput(output) output.setMetadataObjectsDelegate(self, queue: .main) output.metadataObjectTypes = [.qr] } let previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer.videoGravity = .resizeAspectFill 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) { resultLabel?.stringValue = msg resultLabel?.textColor = .systemRed } // MARK: - AVCaptureMetadataOutputObjectsDelegate func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { guard let metadata = metadataObjects.first as? AVMetadataMachineReadableCodeObject, let code = metadata.stringValue else { return } let now = Date() if code == lastDetected, now.timeIntervalSince(lastDetectedTime) < 3 { return } lastDetected = code lastDetectedTime = now // 显示检测结果 resultLabel?.stringValue = code resultLabel?.textColor = .systemGreen // 有效 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() } } } deinit { session?.stopRunning() } } #endif