完善 OTG 相机有线传图:支持 Sony/Canon 自动识别,页面 detach 保持会话以便再次进入复连。

引入进程级 CameraTetheringSession 与共享 USB Browser;离开传图页仅取消 UI 回调,App 终止时再完整 shutdown 释放 PTP 与 Browser 资源。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-01 15:45:35 +08:00
parent 5f7ef24683
commit 90d135b5ac
34 changed files with 2870 additions and 310 deletions

View File

@ -26,10 +26,21 @@ final class CameraTransferPipeline {
private let maxRetries = 3
private let deferredNotifyInterval: TimeInterval = 0.15
private let maxConcurrentUploads = 3
private var isUIAttached = false
///
init(cameraService: any CameraServiceProtocol) {
self.cameraService = cameraService
}
/// UI OTG attach
func attachToUI(
onConnectionStateChange: @escaping (CameraConnectionState) -> Void,
onTasksUpdated: @escaping ([CameraTransferTask]) -> Void
) {
isUIAttached = true
self.onTasksUpdated = onTasksUpdated
cameraService.onConnectionStateChange = onConnectionStateChange
cameraService.onNewAsset = { [weak self] asset in
Task { @MainActor in
await self?.handleNewAsset(asset, skipIfExists: false)
@ -37,6 +48,18 @@ final class CameraTransferPipeline {
}
}
/// UI
func detachFromUI() {
guard isUIAttached else { return }
isUIAttached = false
pendingDeferredNotifyTask?.cancel()
pendingDeferredNotifyTask = nil
activeUploadAssetIDs.removeAll()
inFlightAssetIDs.removeAll()
onTasksUpdated = nil
cameraService.detachFromUI()
}
/// Sink
func configure(
uploadSink: (any CameraAssetUploadSink)?,
@ -50,14 +73,33 @@ final class CameraTransferPipeline {
}
}
///
///
func connect() async {
await cameraService.connect()
if cameraService.connectionState.isConnected {
await syncExistingPhotos()
}
}
///
/// App teardown
func shutdown() async {
detachFromUI()
await cameraService.shutdown()
}
/// shutdown
func disconnect() async {
await cameraService.disconnect()
await shutdown()
}
/// USB
func restartCameraDiscovery() async {
if let autoDetect = cameraService as? AutoDetectCameraService {
await autoDetect.restartDiscovery()
return
}
await cameraService.shutdown()
await cameraService.connect()
}
///
@ -65,14 +107,6 @@ final class CameraTransferPipeline {
cameraService.connectionState
}
///
var onConnectionStateChange: ((CameraConnectionState) -> Void)? {
get { nil }
set {
cameraService.onConnectionStateChange = newValue
}
}
///
func retryFailedUploads() async {
guard uploadEnabled, uploadSink != nil else { return }