Files
suixinkan_ios_new/suixinkan/Core/CameraTethering/AutoDetectCameraService.swift
汉秋 90d135b5ac 完善 OTG 相机有线传图:支持 Sony/Canon 自动识别,页面 detach 保持会话以便再次进入复连。
引入进程级 CameraTetheringSession 与共享 USB Browser;离开传图页仅取消 UI 回调,App 终止时再完整 shutdown 释放 PTP 与 Browser 资源。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:45:35 +08:00

214 lines
7.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 Foundation
import ImageCaptureCore
/// USB
@MainActor
final class AutoDetectCameraService: NSObject, CameraServiceProtocol {
private(set) var brand: CameraBrand = .sony
private(set) var connectionState: CameraConnectionState = .disconnected
var onConnectionStateChange: ((CameraConnectionState) -> Void)?
var onNewAsset: ((CameraAsset) -> Void)?
private var activeService: (any CameraServiceProtocol)?
private var pendingCamera: ICCameraDevice?
private let usbBrowser = SharedUSBCameraBrowserHub.controller
private var isConnectingCamera = false
private var isDisconnecting = false
override init() {
super.init()
bindUSBBrowserCallbacks()
}
func connect() async {
CameraTetheringLogger.log("AutoDetectCameraService.connect()")
if let activeService {
CameraTetheringLogger.log("AutoDetect: 已有活动服务,复用连接")
connectionState = activeService.connectionState
rebindActiveServiceCallbacks()
flushPendingAssetsIfNeeded(on: activeService)
onConnectionStateChange?(connectionState)
return
}
isConnectingCamera = false
connectionState = .searching
onConnectionStateChange?(.searching)
let started = await usbBrowser.start()
guard started else {
connectionState = .error("权限被拒绝")
onConnectionStateChange?(.error("权限被拒绝"))
return
}
}
/// UI ICC/PTP Browser
func detachFromUI() {
CameraTetheringLogger.log("AutoDetectCameraService.detachFromUI()")
onConnectionStateChange = nil
onNewAsset = nil
activeService?.detachFromUI()
}
func shutdown() async {
guard !isDisconnecting else { return }
isDisconnecting = true
defer { isDisconnecting = false }
CameraTetheringLogger.log("AutoDetectCameraService.shutdown()")
isConnectingCamera = false
if let activeService {
await activeService.shutdown()
}
activeService = nil
pendingCamera = nil
usbBrowser.stop()
USBCameraReconnectHint.markDisconnected()
try? await Task.sleep(nanoseconds: 300_000_000)
connectionState = .disconnected
onConnectionStateChange?(.disconnected)
}
/// USB App
func restartDiscovery() async {
CameraTetheringLogger.log("AutoDetectCameraService.restartDiscovery()")
guard activeService == nil else {
CameraTetheringLogger.log("AutoDetect: 已连接,跳过 restartDiscovery")
return
}
isConnectingCamera = false
connectionState = .searching
onConnectionStateChange?(.searching)
await usbBrowser.restart()
}
func listAssets() async throws -> [CameraAsset] {
guard let activeService else {
throw CameraServiceError.notConnected
}
return try await activeService.listAssets()
}
func downloadAsset(_ asset: CameraAsset) async throws -> URL {
guard let activeService else {
throw CameraServiceError.notConnected
}
return try await activeService.downloadAsset(asset)
}
private func bindUSBBrowserCallbacks() {
usbBrowser.onCameraDiscovered = { [weak self] camera, source in
Task { @MainActor in
await self?.handleCameraDiscovered(camera, source: source)
}
}
usbBrowser.onCameraRemoved = { [weak self] device in
Task { @MainActor in
await self?.handleCameraRemoved(device)
}
}
usbBrowser.onAuthorizationDenied = { [weak self] in
Task { @MainActor in
guard let self else { return }
self.connectionState = .error("权限被拒绝")
self.onConnectionStateChange?(.error("权限被拒绝"))
}
}
usbBrowser.onScanTimeout = { [weak self] in
Task { @MainActor in
guard let self, self.activeService == nil, !self.isConnectingCamera else { return }
let message = "未发现 USB 相机。请确认:① 相机 USB 设为「照片传输/电脑遥控」② 存储卡内至少有 1 张照片 ③ 先连手机再连相机 ④ 进入页面后重新插拔 USB 线"
self.connectionState = .error(message)
self.onConnectionStateChange?(.error(message))
}
}
}
private func handleCameraDiscovered(_ camera: ICCameraDevice, source: String) async {
guard activeService == nil, !isConnectingCamera, !isDisconnecting else {
CameraTetheringLogger.log("AutoDetect: 忽略重复发现 (\(source))")
return
}
let vendorID = Int(camera.usbVendorID)
guard let detectedBrand = CameraBrand.detect(from: camera) else {
let message = "不支持的相机品牌 (Vendor ID: 0x\(String(vendorID, radix: 16)))"
CameraTetheringLogger.log(message)
connectionState = .error(message)
onConnectionStateChange?(.error(message))
return
}
isConnectingCamera = true
await activateService(for: camera, brand: detectedBrand)
isConnectingCamera = false
}
private func handleCameraRemoved(_ device: ICDevice) async {
guard !isDisconnecting else { return }
guard activeService != nil else { return }
guard device.uuidString == pendingCamera?.uuidString else { return }
CameraTetheringLogger.log("AutoDetect: 相机已物理移除 \(device.name ?? "?")")
pendingCamera = nil
if let activeService {
await activeService.shutdown()
}
activeService = nil
usbBrowser.resetNotifiedDevices()
connectionState = .disconnected
onConnectionStateChange?(.disconnected)
}
private func activateService(for camera: ICCameraDevice, brand detectedBrand: CameraBrand) async {
brand = detectedBrand
let service = CameraServiceFactory.make(brand: detectedBrand)
activeService = service
pendingCamera = camera
rebindActiveServiceCallbacks()
connectionState = .connecting
onConnectionStateChange?(.connecting)
CameraTetheringLogger.log("AutoDetect: 挂载 \(detectedBrand.displayName) 服务 (source attach)…")
switch detectedBrand {
case .sony:
if let sony = service as? SonyCameraService {
await sony.attach(to: camera)
}
case .canon:
if let canon = service as? CanonCameraService {
await canon.attach(to: camera)
}
case .nikon:
connectionState = .error("Nikon 相机尚未实现")
onConnectionStateChange?(.error("Nikon 相机尚未实现"))
}
}
private func rebindActiveServiceCallbacks() {
guard let activeService else { return }
activeService.onConnectionStateChange = { [weak self] state in
Task { @MainActor in
self?.connectionState = state
self?.onConnectionStateChange?(state)
}
}
activeService.onNewAsset = { [weak self] asset in
self?.onNewAsset?(asset)
}
}
private func flushPendingAssetsIfNeeded(on service: any CameraServiceProtocol) {
if let sony = service as? SonyCameraService {
sony.reattachToUI()
} else if let canon = service as? CanonCameraService {
canon.reattachToUI()
}
}
}