# CameraTethering

## 职责

提供 USB OTG 相机有线连接与 Sony/Canon PTP 边拍边传能力，不包含业务 API 或 OSS 上传。

## 核心组件

| 组件 | 说明 |
| --- | --- |
| `CameraServiceProtocol` | 相机服务抽象，上层只依赖此协议 |
| `CameraTetheringSession` | 进程级单例，持有共享 `AutoDetectCameraService` |
| `AutoDetectCameraService` | 按 USB Vendor ID 自动识别品牌并委托 |
| `SonyCameraService` | Sony 相机实现，封装 SDIO/PTP 下载 |
| `CanonCameraService` | Canon EOS 相机实现，封装 GetEvent/PTP 下载 |
| `ImageCaptureDeviceManager` | Sony ICC 设备管理 |
| `CanonImageCaptureDeviceManager` | Canon ICC 设备管理 |
| `PTPHelper` | 共享 PTP 编解码与命令发送 |
| `CameraDownloadStorage` | 本地下载作用域、目录与文件名消毒 |

## 品牌识别

插入 USB 相机后，`AutoDetectCameraService` 读取 `ICCameraDevice.usbVendorID`：

- `0x054C` → Sony
- `0x04A9` → Canon
- 其他 → 提示不支持的相机品牌

## 业务流程

1. `AutoDetectCameraService.connect()` 启动 USB 设备搜索（或复用已有连接）。
2. 发现相机并完成系统授权后，按 Vendor ID 创建品牌 Service 并 attach。
3. 品牌 Service 打开 ICC 会话并执行品牌专有 PTP 握手（Sony SDIO / Canon SetRemoteMode）。
4. 拍摄事件触发 PTP 下载或 catalog 回退，底层先写入临时目录。
5. 通过 `onNewAsset` 回调通知上层有新照片。

## 相机端前置条件

- **Sony**：设为电脑遥控（PC Remote）模式
- **Canon**：设为电脑遥控 / PC Remote / 照片传输（PTP）模式
- 均需关闭「仅充电」模式

## 依赖

- ImageCaptureCore（系统框架）
- 无第三方库依赖

## 页面生命周期与资源释放

OTG 相机连接由 `CameraTetheringSession.sharedService` 进程级持有；`CameraServiceFactory.makeAutoDetect()` 始终返回该单例。

**进入 OTG 页：**

1. `.task` 调用 `WiredCameraTransferViewModel.start()`。
2. `pipeline.attachToUI(...)` 绑定 UI 回调。
3. `connect()` 启动或复用 USB 搜索；若已连接则跳过 attach，同步状态并补发 detach 期间缓冲的新片。
4. 发现相机后按 Vendor ID attach 品牌服务，打开 ICC 会话并执行 PTP 握手。

**离开 OTG 页（detach）：**

1. `.task` 取消或 `onDisappear` 触发 `detachFromUI()`（幂等）。
2. 仅清空 `onNewAsset` / `onConnectionStateChange` 等 UI 回调，取消 Pipeline 上传/通知任务。
3. **不** PTP CloseSession、**不** stop Browser、**不** nil `activeService`。
4. 底层 D215 轮询与 PTP 下载可继续，新片写入 `pendingAssetsWhileDetached`，再次进入时 flush。

**App 即将终止（shutdown）：**

1. `AppDelegate.applicationWillTerminate` 调用 `CameraTetheringSession.shutdown()`。
2. 品牌 Service 完整断开（Sony 先 `Position_Key_Setting=Local` 再 PTP CloseSession）。
3. `usbBrowser.stop()` 停止 USB Browser 并释放授权状态。

进入后台**不**主动 shutdown；系统挂起 USB 时由 `deviceBrowserDidResumeOperations` 处理恢复。

若返回后再进入无法连接，优先检查日志中是否出现「会话关闭超时」或在非 terminate 场景误调用了 `shutdown()`。
