做"相机拍完,手机马上收到照片"这个功能,最开始很多开发者都会觉得:
不就是把文件下载下来吗?
真正做到连续拍摄以后,你就会发现事情完全不是这么简单。
尤其是在:
-
活动摄影
-
婚礼摄影
-
摄影棚
-
照片直播
-
赛事拍摄
-
AI 修图
这些场景里,相机可能几秒钟就产生很多张照片。
这时候真正需要解决的问题就变成了:
怎么让照片一张不丢、不乱、不重复,并且尽可能快地进入 App?
这篇就聊聊我在做相机 USB 有线传图功能时,对这个问题的一些实践。
一、最简单的方案:发现照片,然后下载
假设相机拍摄了一张照片。
理论流程是:
相机拍照
↓
产生新图片
↓
手机发现新图片
↓
下载
↓
交给 App
单张测试的时候,这完全没问题。
问题是摄影师不会只拍一张。
假设连续拍摄:
0s Photo_001
0.3s Photo_002
0.6s Photo_003
1.0s Photo_004
1.4s Photo_005
而手机端下载速度可能是:
Photo_001 → 1.2s
Photo_002 → 1.1s
Photo_003 → 1.3s
那么瞬间就出现了一个问题:
生产速度 > 消费速度。
这时候,如果你的程序还是"发现一张,下载一张",很容易开始乱。
二、所以第一件事:一定要有传输队列
我自己最终采用的思路,本质上就是一个生产者---消费者模型。
Camera
│
▼
新照片 / Object
│
▼
┌─────────────────┐
│ Transfer Queue │
└────────┬────────┘
│
┌────────▼────────┐
│ Transfer Worker │
└────────┬────────┘
│
▼
Photo
│
▼
Business App
相机负责不断"生产"。
传输线程负责不断"消费"。
中间用 Queue 隔开。
这样即使相机短时间内连续产生 10 张照片,也不会因为手机来不及处理就直接把任务覆盖掉。
三、为什么不能直接在回调里下载?
很多 Demo 会这么写:
onNewPhoto(objectHandle) {
download(objectHandle)
}
第一张没问题。
第二张可能也没问题。
但是连续拍的时候,问题就出来了。
例如:
Photo001 → 正在下载
Photo002 → 又来了
Photo003 → 又来了
Photo004 → 又来了
如果你的回调线程同时承担:
发现 + 下载 + 文件处理
就很容易把设备通信线程堵住。
最后出现:
发现越来越慢
↓
事件积压
↓
下载延迟
↓
用户看到照片越来越慢
所以更合理的思路是:
设备层
↓
只负责发现
↓
Queue
↓
Worker
↓
负责下载
把"发现"和"传输"拆开。
四、第二个坑:重复照片
这个问题实际上非常容易发生。
比如:
相机产生 Photo001
↓
手机发现 Photo001
↓
加入队列
↓
还没下载完成
↓
再次扫描
↓
Photo001 又被发现一次
于是队列变成:
Photo001
Photo002
Photo001
Photo003
最后你会发现:
同一张照片下载了两次。
所以我建议给每一个传输任务设计唯一标识。
最简单的方式就是使用设备侧的:
Object Handle
或者结合:
对象 ID + 文件名 + 时间等信息
建立业务侧唯一键。
然后维护一个:
Pending
Downloading
Success
Failed
这样的状态机。
五、一个更完整的任务状态,可以这样设计
enum class TransferState {
PENDING,
DOWNLOADING,
SUCCESS,
FAILED
}
每张图片都有自己的任务:
data class PhotoTask(
val id: Long,
val objectHandle: Int,
val fileName: String,
var state: TransferState
)
这样一来,整个流程就比较清晰:
发现
↓
PENDING
↓
DOWNLOADING
↓
SUCCESS
失败则:
DOWNLOADING
↓
FAILED
↓
Retry
↓
DOWNLOADING
而不是失败之后直接把任务扔掉。
六、第三个坑:断线重连
这个问题在真实拍摄现场尤其常见。
USB 线稍微动一下。
或者摄影师换了相机电池。
又或者手机因为系统原因暂时回收了连接。
你都会遇到:
Connected
↓
Disconnected
↓
Session Lost
如果程序没有状态管理,后面的所有任务都可能停在那里。
所以连接状态本身也应该是一个状态机:
DISCONNECTED
↓
CONNECTING
↓
CONNECTED
↓
TRANSFERRING
↓
DISCONNECTED
↓
RECONNECTING
这其实也是为什么:
一个相机连接 SDK 和一个简单 Demo,差别非常大。
真正能用于商业项目的 SDK,不能只有:
connect()
download()
还要考虑:
disconnect()
reconnect()
retry()
resume()
cancel()
七、第四个坑:文件"被发现"不代表"已经可以处理"
这个问题特别值得注意。
当相机刚拍完一张照片的时候,设备内部可能还在完成文件写入。
如果你发现 Object 之后马上就开始进行后续处理:
发现照片
↓
立即读取
↓
AI处理
理论上可能会遇到:
文件还没准备完成。
因此实际工程里,最好加入:
可读性检查 / 重试 / 文件大小确认 / 下载完成校验
这一类机制。
最终流程类似:
发现新照片
↓
加入 Queue
↓
确认对象可访问
↓
开始下载
↓
完成
↓
校验
↓
通知业务
八、为什么"缩略图优先"也很重要?
假设摄影师拍了一张 40MB 的 RAW。
你如果直接:
下载 RAW
↓
解析 RAW
↓
显示
用户可能需要等很久。
但如果你的场景是照片直播,客户真正想看的可能只是:
"这张照片拍出来什么样?"
所以很多影像类软件都会采用:
缩略图
↓
快速预览
再:
原图 / RAW
↓
后台下载
近期一些公开的 Android USB/PTP 相机项目也开始专门针对大容量相机存储、缩略图并发、渐进式加载和照片数量较大的情况优化,这说明随着真实使用量上升,"怎么传"之外,"怎么展示传输结果"同样成为工程问题。
九、所以最后我把整个架构重新整理成了这样
┌────────────────────────────┐
│ Camera │
└─────────────┬──────────────┘
│
▼
USB / PTP / MTP
│
▼
┌────────────────────────────┐
│ Camera Connect SDK │
│ │
│ Device Manager │
│ Permission Manager │
│ PTP Session │
│ Photo Discovery │
│ Transfer Queue │
│ Retry / Reconnect │
│ File Validation │
└─────────────┬──────────────┘
│
▼
┌────────────────────────────┐
│ Business App │
│ │
│ AI修图 / 调色 / 上传 / 分享 │
└────────────────────────────┘
这样以后你的业务代码就不需要关心下面这些东西:
USB设备怎么识别
PTP Session怎么建立
新照片怎么发现
下载失败怎么办
断线怎么办
任务重复怎么办
SDK 负责。
业务负责:
拿到照片以后干什么。
十、这也是我为什么把它做成 SDK
这套代码原本就是我自己照片直播项目里的一个模块。
最开始只是希望实现:
相机拍照以后,手机可以自动拿到。
后来随着功能越来越完整,我发现它其实已经具备了比较明确的底层能力:
相机连接
↓
照片发现
↓
照片传输
↓
任务管理
↓
异常恢复
而这些东西并不属于某一个具体业务。
你的 App 可以拿它做:
照片直播
也可以做:
AI 修图
或者:
影楼系统
甚至:
自己的相机伴侣 App
目前 GitHub 上也能看到一些新的 Android 相机项目采用类似的思路,例如通过 USB PTP 实现自动传图,并进一步处理 RAW/JPEG、队列、连接恢复等问题;甚至还有项目把 Android USB PTP 独立成相机控制库。
十一、其实"相机即拍即传"最难的不是速度
很多人第一反应是:
我要做到多快?
但我觉得对于商业项目来说,更重要的是:
稳定。
比如连续拍 500 张。
你真正关心的是:
500张
↓
有没有漏?
有没有重复?
有没有乱序?
中途断线怎么办?
失败怎么办?
App重启怎么办?
速度当然重要。
但一个稳定的:
快 + 准 + 可恢复
的传输系统,往往比单纯追求峰值速度更重要。
十二、如果让我重新做一次,我会优先设计这几个模块
1. DeviceManager
负责:
USB插入
USB拔出
设备发现
Permission
连接状态
2. PtpSession
负责:
Session
Transaction
DeviceInfo
Storage
Object
3. PhotoDetector
负责:
新照片发现
重复过滤
Object管理
4. TransferQueue
负责:
任务排队
并发控制
状态管理
失败重试
5. ConnectionManager
负责:
断线
重连
Session恢复
6. Callback
给业务层:
onConnected()
onNewPhoto()
onTransferProgress()
onTransferSuccess()
onTransferFailed()
onDisconnected()
这样 SDK 会非常干净。
十三、最后一个思考:为什么这个能力越来越重要?
现在影像产品已经不只是:
拍照 → 存储
而是:
拍照 → 计算 → AI → 交付
相机负责获得高质量原始影像。
手机负责计算。
AI 负责处理。
云端负责同步和交付。
所以:
Camera
↓
Connect
↓
Mobile
↓
AI
↓
Cloud
↓
User
中间的"Connect",反而越来越重要。
现在公开的开源项目也正在往这个方向发展:从最初的"USB 相机控制",逐渐扩展到即拍即传、RAW/JPEG 管理、缩略图、断线恢复、大容量媒体浏览等完整工作流。
写在最后
如果你正在做:
Android + 单反/微单 + USB + 图片传输
我建议不要把它当成一个简单的文件下载功能。
真正应该把它理解成:
一个小型的实时媒体传输系统。
相机是生产者。
Queue 是缓冲层。
Transfer Worker 是消费者。
SDK 负责连接和恢复。
业务 App 负责处理照片。
当你把这个架构想明白之后,后面的:
AI 修图、照片直播、自动上传、云端交付
其实都只是接在后面的业务能力。
而我目前正在整理的 Camera Connect SDK,本质上就是把这层复杂的:
USB + PTP/MTP + 图片发现 + 传输队列 + 重连
抽出来,让开发者可以直接集成到自己的项目里。
后面我还会继续分享一个更底层的话题:
《Android USB PTP 到底是怎么工作的?从 DeviceInfo、Session 到 Object Transfer》
这个部分真正搞懂以后,你基本就能看明白整个相机直连手机的底层逻辑。
#Android开发 #相机SDK #USB开发 #PTP #MTP #照片传输 #照片直播 #摄影App #AI修图 #程序员 #移动端开发