10、CaptureResult与回调:Result回调机制、Metadata提取、错误处理、FrameNumber与Timestamp
好,咱们今天来聊聊 CaptureResult。这东西说白了,就是相机硬件给你的一张“成绩单”。你通过 CaptureRequest 告诉硬件“我想要拍这样的照片”,硬件忙活完了,就通过 CaptureResult 告诉你“我拍完了,这是结果”。
我个人觉得,理解 CaptureResult 是区分“会用 Camera2”和“懂 Camera2”的分水岭。很多人卡在回调里出不来,就是因为没搞明白 Result 到底是怎么送回来的。
10.1 Result回调机制:谁在什么时候叫你?
Camera2 的回调机制,其实是一个“生产者-消费者”模型。你发送一个 Request,相机 HAL 层开始处理,处理完一个阶段,就回调一次。整个过程是异步的,你永远不知道下一帧什么时候到。
我刚开始接触时也犯过糊涂,以为 CaptureResult 是一次性到位的。后来才发现,它分两种:Partial Result 和 Total Result。
| 回调类型 | 触发时机 | 包含内容 |
|---|---|---|
| Partial Result | 每个 Capture 阶段完成时 | 部分 Metadata(如 AE 状态、AF 状态) |
| Total Result | 整个 Capture 完全结束时 | 全部 Metadata + 图像数据 |
你想想看,为什么要有 Partial Result?因为有些场景下,你需要“实时”知道某些状态。比如自动对焦,你总不希望等到整张照片拍完了才知道“哦,刚才没对上焦”吧?
关键点:CameraCaptureSession.CaptureCallback 中有多个回调方法,最常用的是 onCaptureCompleted() 和 onCaptureProgressed()。前者对应 Total Result,后者对应 Partial Result。
// 典型的回调写法
private CameraCaptureSession.CaptureCallback captureCallback =
new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureResult partialResult) {
// 部分结果到了,可以看看 AE/AF 状态
Integer aeState = partialResult.get(CaptureResult.CONTROL_AE_STATE);
if (aeState != null) {
// 更新 UI 上的曝光指示
}
}
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
// 完整结果到了,可以提取 Metadata 了
processTotalResult(result);
}
};
嗯,这里要注意:onCaptureCompleted 不保证图像数据已经可用。图像数据是通过 ImageReader 的 onImageAvailable 回调单独送回来的。这两个回调是并行的,没有先后顺序。
10.2 Metadata提取:从 Result 里“挖”出你需要的信息
CaptureResult 里塞满了各种 Metadata,从曝光时间到传感器温度,应有尽有。提取的方式很简单,就是 get() 方法,传入你需要的 Key。
但这里有个坑:不是所有 Key 在所有设备上都支持。我曾在某款低端机上吃过亏,想取 SENSOR_SENSITIVITY(ISO),结果返回 null,导致 App 直接闪退。
避坑指南:提取 Metadata 前,一定要做 null 检查。我曾经因为没检查,线上出了好几个 ANR。后来养成了习惯:所有 get() 结果都判空。
// 安全的 Metadata 提取方式
private void extractMetadata(CaptureResult result) {
// 曝光时间
Long exposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME);
if (exposureTime != null) {
Log.d(TAG, "Exposure time: " + exposureTime + " ns");
}
// 感光度
Integer iso = result.get(CaptureResult.SENSOR_SENSITIVITY);
if (iso != null) {
Log.d(TAG, "ISO: " + iso);
}
// 对焦距离
Float focusDistance = result.get(CaptureResult.LENS_FOCUS_DISTANCE);
if (focusDistance != null) {
Log.d(TAG, "Focus distance: " + focusDistance + " diopters");
}
// 帧编号 - 这个很重要,后面会讲
Long frameNumber = result.getFrameNumber();
Log.d(TAG, "Frame: " + frameNumber);
}
常用的 Metadata 我列一下,方便你查阅:
- CONTROL_AE_STATE:自动曝光状态(LOCKED、CONVERGED 等)
- CONTROL_AF_STATE:自动对焦状态(FOCUSED、NOT_FOCUSED 等)
- CONTROL_AWB_STATE:自动白平衡状态
- SENSOR_EXPOSURE_TIME:曝光时间(纳秒)
- SENSOR_SENSITIVITY:ISO 感光度
- LENS_FOCUS_DISTANCE:对焦距离(屈光度)
- STATISTICS_FACES:检测到的人脸信息
10.3 错误处理:Result 里的“坏消息”
不是每次 Capture 都能成功。硬件可能出错,缓冲区可能溢出,HAL 层可能崩溃。这些错误会通过 onCaptureFailed() 回调告诉你。
我印象很深的一次,是在做多摄像头切换时,有个摄像头在切换过程中返回了 ERROR_CAMERA_DEVICE。当时没处理这个错误,结果相机界面直接卡死,用户怎么点都没反应。
核心原则:永远假设 Capture 会失败。在 onCaptureFailed() 里做降级处理,比如重新发送 Request,或者切换到备用摄像头。
@Override
public void onCaptureFailed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureFailure failure) {
// 获取失败原因
int reason = failure.getReason();
long frameNumber = failure.getFrameNumber();
switch (reason) {
case CaptureFailure.REASON_ERROR:
// 硬件错误,需要重新配置 Session
Log.e(TAG, "Hardware error on frame " + frameNumber);
reopenCamera();
break;
case CaptureFailure.REASON_FLUSHED:
// Session 被 flush,正常情况
Log.w(TAG, "Capture flushed: " + frameNumber);
break;
default:
Log.e(TAG, "Unknown failure: " + reason);
break;
}
}
常见的错误原因有:
- REASON_ERROR:硬件或 HAL 层错误,需要重新打开相机
- REASON_FLUSHED:Session 被主动 flush,通常是因为你调用了 abortCaptures()
10.4 FrameNumber 与 Timestamp:给每一帧一个“身份证”
FrameNumber 和 Timestamp 是 CaptureResult 里两个最重要的标识字段。它们帮你把不同的回调关联起来。
FrameNumber 是一个单调递增的整数,从你打开相机设备开始计数。每次你发送一个 Request,就会分配一个 FrameNumber。这个数字在 Partial Result、Total Result 和 Image 里都是一致的。
Timestamp 是硬件时间戳,单位是纳秒。它表示曝光开始的时间点。这个时间戳在同一个相机设备内是单调递增的,但不同设备之间没有可比性。
实用技巧:当你需要把 Metadata 和图像数据对齐时,用 FrameNumber 做关联。比如你在 onCaptureCompleted 里拿到了 AE 状态,又在 ImageReader 里拿到了图像,通过 FrameNumber 就能知道这张图对应的是哪次曝光。
// 用 FrameNumber 关联 Metadata 和图像
private final SparseArray<CaptureResult> resultMap = new SparseArray<>();
// 在 CaptureCallback 中
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
long frameNumber = result.getFrameNumber();
resultMap.put((int) frameNumber, result);
}
// 在 ImageReader 的 onImageAvailable 中
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
if (image != null) {
long frameNumber = image.getFrameNumber();
CaptureResult result = resultMap.get((int) frameNumber);
if (result != null) {
// 现在你可以把 Metadata 和图像关联起来了
processImageWithMetadata(image, result);
resultMap.remove((int) frameNumber);
}
image.close();
}
}
这里有个细节:Timestamp 的单位是纳秒,不是毫秒。我见过有人直接拿它当 System.currentTimeMillis() 用,结果算出来的时间差了 6 个数量级。Timestamp 是用来做帧间时间差计算的,不是用来显示时间的。
10.5 知识体系总览
为了让你对整个流程有个直观的认识,我画了一张图:
从图上你能看到,整个流程是分叉的。Partial Result 先回来,告诉你一些中间状态;Total Result 后回来,告诉你最终结果;图像数据走另一条路,通过 ImageReader 回调。这三条路是并行的,互不阻塞。
好了,关于 CaptureResult 的核心机制就这些。记住三点:判空、关联 FrameNumber、处理错误。把这三点做到位,你的相机应用就稳了一大半。