10、CaptureResult与回调:Result回调机制、Metadata提取、错误处理、FrameNumber与Timestamp

好,咱们今天来聊聊 CaptureResult。这东西说白了,就是相机硬件给你的一张“成绩单”。你通过 CaptureRequest 告诉硬件“我想要拍这样的照片”,硬件忙活完了,就通过 CaptureResult 告诉你“我拍完了,这是结果”。

我个人觉得,理解 CaptureResult 是区分“会用 Camera2”和“懂 Camera2”的分水岭。很多人卡在回调里出不来,就是因为没搞明白 Result 到底是怎么送回来的。

10.1 Result回调机制:谁在什么时候叫你?

Camera2 的回调机制,其实是一个“生产者-消费者”模型。你发送一个 Request,相机 HAL 层开始处理,处理完一个阶段,就回调一次。整个过程是异步的,你永远不知道下一帧什么时候到。

我刚开始接触时也犯过糊涂,以为 CaptureResult 是一次性到位的。后来才发现,它分两种:Partial ResultTotal 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 知识体系总览

为了让你对整个流程有个直观的认识,我画了一张图:

CaptureResult 回调流程 应用层 发送 CaptureRequest Camera Framework 分发到 HAL 层 HAL 层 硬件处理 Partial Result (onCaptureProgressed) Total Result (onCaptureCompleted) 图像数据 ImageReader 错误回调 (onCaptureFailed) Partial Result Total Result 错误回调

从图上你能看到,整个流程是分叉的。Partial Result 先回来,告诉你一些中间状态;Total Result 后回来,告诉你最终结果;图像数据走另一条路,通过 ImageReader 回调。这三条路是并行的,互不阻塞。

好了,关于 CaptureResult 的核心机制就这些。记住三点:判空、关联 FrameNumber、处理错误。把这三点做到位,你的相机应用就稳了一大半。


公众号:蓝海资料掘金营,微信deep3321