Add camera QR pairing, Bonjour discovery and pinned address recovery
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import Darwin
|
||||
|
||||
struct DiscoveredComputer: Identifiable, Equatable {
|
||||
let name: String
|
||||
let endpoint: String
|
||||
let fingerprint: String
|
||||
var id: String { fingerprint + endpoint }
|
||||
|
||||
static func parse(name: String, host: String, port: Int, txt: [String: Data]) -> Self? {
|
||||
guard txt["v"] == Data("1".utf8), let data = txt["id"],
|
||||
let pin = String(data: data, encoding: .utf8),
|
||||
let connection = try? Connection(endpoint: "https://\(host):\(port)", fingerprint: pin) else { return nil }
|
||||
return Self(name: String(name.prefix(80)), endpoint: connection.endpoint.absoluteString,
|
||||
fingerprint: connection.fingerprint)
|
||||
}
|
||||
|
||||
// An advertisement is a routing hint, never a replacement for saved trust.
|
||||
func reconnect(_ saved: Connection) -> Connection? {
|
||||
guard fingerprint == saved.fingerprint else { return nil }
|
||||
return try? Connection(endpoint: endpoint, fingerprint: saved.fingerprint, token: saved.token)
|
||||
}
|
||||
}
|
||||
|
||||
// Started on the main run loop; Foundation delivers browser/service delegates there.
|
||||
final class DiscoveryStore: NSObject, ObservableObject, NetServiceBrowserDelegate, NetServiceDelegate {
|
||||
@Published private(set) var computers: [DiscoveredComputer] = []
|
||||
@Published private(set) var message = "Поиск ПК не запущен"
|
||||
private var browser: NetServiceBrowser?
|
||||
private var services: [String: NetService] = [:]
|
||||
private var records: [String: [DiscoveredComputer]] = [:]
|
||||
private var refresh: Timer?
|
||||
|
||||
func start() {
|
||||
guard browser == nil else { return }
|
||||
message = "Ищем MusicBridge в локальной сети…"
|
||||
let browser = NetServiceBrowser()
|
||||
self.browser = browser
|
||||
browser.delegate = self
|
||||
browser.searchForServices(ofType: "_musicbridge._tcp.", inDomain: "local.")
|
||||
refresh = Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
for service in self.services.values {
|
||||
service.stop()
|
||||
service.resolve(withTimeout: 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
refresh?.invalidate(); refresh = nil
|
||||
browser?.delegate = nil; browser?.stop(); browser = nil
|
||||
for service in services.values { service.delegate = nil; service.stop() }
|
||||
services.removeAll(); records.removeAll(); computers = []
|
||||
message = "Поиск ПК приостановлен"
|
||||
}
|
||||
|
||||
func restart() { stop(); start() }
|
||||
private func key(_ service: NetService) -> String { service.domain + service.type + service.name }
|
||||
|
||||
private func publish() {
|
||||
computers = Array(Set(records.values.flatMap { $0 }.map(\.id))).sorted().compactMap { id in
|
||||
records.values.lazy.flatMap { $0 }.first { $0.id == id }
|
||||
}
|
||||
message = computers.isEmpty ? "ПК пока не найден. Проверьте Wi-Fi и LAN-режим агента." : "Найдено адресов: \(computers.count)"
|
||||
}
|
||||
|
||||
func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
|
||||
guard self.browser === browser, services.count < 64, services[key(service)] == nil else { return }
|
||||
services[key(service)] = service
|
||||
service.delegate = self
|
||||
service.resolve(withTimeout: 5)
|
||||
}
|
||||
|
||||
func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
|
||||
guard self.browser === browser else { return }
|
||||
let old = services.removeValue(forKey: key(service))
|
||||
old?.delegate = nil; old?.stop()
|
||||
records.removeValue(forKey: key(service)); publish()
|
||||
}
|
||||
|
||||
func netServiceBrowser(_ browser: NetServiceBrowser, didNotSearch errorDict: [String: NSNumber]) {
|
||||
guard self.browser === browser else { return }
|
||||
stop()
|
||||
message = "Поиск недоступен. Разрешите MusicBridge доступ к локальной сети в Настройках и повторите поиск."
|
||||
}
|
||||
|
||||
func netServiceDidResolveAddress(_ sender: NetService) {
|
||||
guard services[key(sender)] === sender else { return }
|
||||
let txt = sender.txtRecordData().map(NetService.dictionary(fromTXTRecord:)) ?? [:]
|
||||
records[key(sender)] = (sender.addresses ?? []).prefix(16).compactMap { data in
|
||||
guard let host = Self.ipv4(data) else { return nil }
|
||||
return DiscoveredComputer.parse(name: sender.name, host: host, port: sender.port, txt: txt)
|
||||
}
|
||||
publish()
|
||||
}
|
||||
|
||||
func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) {
|
||||
guard services[key(sender)] === sender else { return }
|
||||
records.removeValue(forKey: key(sender)); publish()
|
||||
}
|
||||
|
||||
static func ipv4(_ data: Data) -> String? {
|
||||
guard data.count >= MemoryLayout<sockaddr_in>.size else { return nil }
|
||||
var address = data.withUnsafeBytes { $0.loadUnaligned(as: sockaddr_in.self) }
|
||||
guard address.sin_family == sa_family_t(AF_INET) else { return nil }
|
||||
var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN))
|
||||
guard inet_ntop(AF_INET, &address.sin_addr, &buffer, socklen_t(INET_ADDRSTRLEN)) != nil else { return nil }
|
||||
return String(cString: buffer)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user