完善 OTG 相机有线传图:支持 Sony/Canon 自动识别,页面 detach 保持会话以便再次进入复连。
引入进程级 CameraTetheringSession 与共享 USB Browser;离开传图页仅取消 UI 回调,App 终止时再完整 shutdown 释放 PTP 与 Browser 资源。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
213
suixinkan/Core/CameraTethering/AutoDetectCameraService.swift
Normal file
213
suixinkan/Core/CameraTethering/AutoDetectCameraService.swift
Normal file
@ -0,0 +1,213 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user