MiniPlayer/Sources/QRScannerView.swift

180 lines
5.7 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 SwiftUI sheet
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
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.contentRect(forFrameRect: win.frame).insetBy(dx: 0, dy: 0))
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 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 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)
win.contentView = container
//
setupCapture(hostView: hostView)
win.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
self.window = win
}
func close() {
session?.stopRunning()
window?.close()
}
private func setupCapture(hostView: QRScannerHostView) {
let session = AVCaptureSession()
self.session = session
guard let device = AVCaptureDevice.default(for: .video),
let input = try? AVCaptureDeviceInput(device: device) else {
showError("No camera available")
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
hostView.previewLayer = previewLayer
self.previewLayer = previewLayer
//
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
session.startRunning()
DispatchQueue.main.async {
self?.previewLayer?.frame = hostView.bounds
}
}
}
private func showError(_ msg: String) {
guard let resultLabel = window?.contentView?.viewWithTag(100) as? NSTextField else { return }
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 }
// 3
let now = Date()
if code == lastDetected, now.timeIntervalSince(lastDetectedTime) < 3 {
return
}
lastDetected = code
lastDetectedTime = now
//
if let resultLabel = window?.contentView?.viewWithTag(100) as? NSTextField {
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()
}
}
// 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