MiniPlayer/Sources/QRScannerView.swift

201 lines
6.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
// QR
if output.availableMetadataObjectTypes.contains(.qr) {
output.metadataObjectTypes = [.qr]
} else {
DispatchQueue.main.async { [weak self] in
self?.showError("Camera does not support QR code scanning")
}
return
}
}
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