← 返回课程

Camera2实战

Camera全栈开发(Qcom Camx) 第 7 / 30 节

第 5 章:Camera2 实战 — 预览/拍照/录像


本章导读

第 4 章讲了 Camera2 API 的五个核心类。这章把它们串起来——写一个能跑的相机 App

不过不要以为"能跑"就够了。这章真正要教你的不是 App 代码本身,而是每行 App 代码在 HAL 层触发了什么。理解了这层映射关系,你在 HAL 层写代码时才知道"App 那边到底要什么"。


5.1 预览——最基础也最重要的循环

📊 对照这张图看本节/api/course-files/camera-fullstack/documents/diagrams/05_preview_timing.png

预览的本质:App 发起一个持续重复的 CaptureRequest,HAL 收到后每帧处理,处理完的帧通过 BufferQueue 送到 Surface 显示。

5.1.1 时序分解

对照 05_preview_timing.png,整个预览流程分 5 个阶段:

阶段 1:打开相机
  App: CameraManager.openCamera(cameraId, callback, handler)
  HAL: camera_module_t.open() → camera3_device_t.initialize()
  耗时: ~200ms(ChiContext 创建 + Static Metadata 构建)

阶段 2:创建 Session
  App: CameraDevice.createCaptureSession(surfaces, callback)
  HAL: configure_streams() → UsecaseSelector 选择 Usecase → 创建 Pipeline → 分配 Buffer
  耗时: ~100ms

阶段 3:开始预览
  App: CameraCaptureSession.setRepeatingRequest(request, callback)
  HAL: process_capture_request() → Session → Pipeline → Node 链
  第一帧延迟: ~50ms(Sensor 曝光 + ISP 处理)

阶段 4:每帧循环
  HAL 每处理完一帧 → process_capture_result() 回调
  → BufferQueue 把数据送到 Surface → Surface 更新显示
  → App 收到 onCaptureCompleted()
  循环周期: ~33ms@30fps 或 ~16ms@60fps

阶段 5:停止预览
  App: CameraCaptureSession.close() 或 CameraDevice.close()
  HAL: flush() → Pipeline StreamOff → 释放 Buffer

5.1.2 每一步的代码和 HAL 层对应

步骤 1:TextureView 就绪后打开相机

TextureView 的 SurfaceTexture 就绪后,才能创建 Surface 传给 HAL。

previewView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int w, int h) {
        openCamera();  // Surface 准备好了,可以打开相机了
    }
    // ... 其他回调省略
});

步骤 2:打开相机

private void openCamera() {
    cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice device) {
            // 对应 HAL: camera_module_t.open() 完成
            cameraDevice = device;
            startPreview();
        }

        @Override
        public void onDisconnected(CameraDevice device) {
            // HAL 层 Provider 进程崩溃时会触发这个回调
            device.close();
        }

        @Override
        public void onError(CameraDevice device, int error) {
            // HAL 返回了错误(如 configure_streams 失败)
            device.close();
        }
    }, null);
}

💡 openCamera 的异步特性

openCamera() 不是同步的。你在 onOpened() 回调中才能拿到 CameraDevice 对象。在此之前,HAL 层在初始化 ChiContext、构建 Static Metadata、加载 overrides、初始化 CSL。

这意味着:如果你在 onOpened() 之前尝试做任何相机操作,都会失败。

步骤 3:创建 Session + 开始预览

这一步是 HAL 层最忙的时候——UsecaseSelector 决策、Pipeline 创建、Buffer 分配都在这里发生。

private void startPreview() {
    // 从 TextureView 获取 SurfaceTexture,设置缓冲区大小
    SurfaceTexture texture = previewView.getSurfaceTexture();
    texture.setDefaultBufferSize(1920, 1080);
    Surface surface = new Surface(texture);

    // 创建 PREVIEW 模板的 Request Builder
    CaptureRequest.Builder builder =
        cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
    builder.addTarget(surface);  // 指定输出目标

    // createCaptureSession — HAL 层开始 configure_streams()
    cameraDevice.createCaptureSession(
        Arrays.asList(surface),  // 所有输出 Surface 的列表
        new CameraCaptureSession.StateCallback() {
            @Override
            public void onConfigured(CameraCaptureSession session) {
                // Session 就绪,开始重复请求
                captureSession = session;
                try {
                    session.setRepeatingRequest(
                        builder.build(),
                        new CameraCaptureSession.CaptureCallback() {
                            @Override
                            public void onCaptureCompleted(
                                    CameraCaptureSession s,
                                    CaptureRequest req,
                                    TotalCaptureResult result) {
                                // 每帧回调。可以在这里读取 AE/AF 状态
                                Long exposureNs = result.get(
                                    CaptureResult.SENSOR_EXPOSURE_TIME);
                                Integer iso = result.get(
                                    CaptureResult.SENSOR_SENSITIVITY);
                            }
                        },
                        null  // handler = null 用主线程 Looper
                    );
                } catch (CameraAccessException e) { }
            }

            @Override
            public void onConfigureFailed(CameraCaptureSession session) {
                // 配置失败 — 通常是 Surface 分辨率不被支持
                // 或所有 Surface 总带宽超出 ISP 能力
            }
        },
        null
    );
}

💡 setRepeatingRequestcapture 的本质区别

HAL 层并不区分"预览"和"拍照"——它只看到 process_capture_request。区别在于:

  • setRepeatingRequest:HAL 持续收到请求,Pipeline 保持在 STREAM_ON 状态,不停处理
  • capture:HAL 收到一次请求,处理完就不再处理下一个(直到下次 capture)

CamX 内部通过 Request 的频率来决定是预览还是拍照。

5.1.3 每个 Surface 对应一个 HAL Stream

💡 关于 Buffer:这里提到的 Surface、Stream、Buffer 的详细机制在第 9 章有完整讲解(Buffer 轮转、句柄传递、Gralloc 分配器)。如果你现在对 "Buffer 怎么传" 感到模糊,先继续往下看,学完第 9 章再回来复习这一节会豁然开朗。

createCaptureSession 传入的 Surface 列表,在 HAL 层会被转换为 Stream:

App 层的 createCaptureSession([previewSurface])
  → HAL 的 configure_streams()
    → Stream 0: format=PRIVATE, size=1920x1080, usage=PREVIEW
      → 分配 Gralloc Buffer × 4~8 个
      → BufferQueue consumer = previewSurface

如果你传入两个 Surface:

App: createCaptureSession([previewSurface, imageReaderSurface])
  → HAL:
    → Stream 0: PRIVATE, 1920x1080, PREVIEW → previewSurface
    → Stream 1: JPEG, 4032x3024, STILL_CAPTURE → imageReaderSurface

每个 Stream 都要分配独立的 Buffer 池。 如果你配置了太多 Stream,ISP 带宽不够,configure_streams 就会失败。


5.2 拍照——多了一条 Stream

拍照不是在预览之外另起一个 Session。而是在同一个 Session 中加了一条 JPEG Stream。

5.2.1 原理

拍照时的 Session 配置:

createCaptureSession 时传入:
  [previewSurface, jpegReaderSurface]

HAL 层:
  Stream 0 (PREVIEW): PRIVATE, 1920x1080 → previewSurface
  Stream 1 (JPEG):    JPEG, 4032x3024  → jpegReaderSurface

关键是 HAL 内部的处理:预览时 Stream 1 没有数据。只有当你调用 capture() 时,HAL 才会把 JPEG 数据写入 Stream 1。

预览帧:
  IFE → IPE → Stream 0 (Display)    ← 实时 Pipeline 持续工作
                      Stream 1 (不输出)    ← Offline Pipeline 空闲

拍照帧:
  IFE → IPE → Stream 0 (Display)    ← 预览不中断
                      Stream 1 → BPS → IPE → JPEG Node → JPEG 编码 → Stream 1 Buffer
                                    ↑
                              这是第 13 章讲的 Offline Pipeline(BPS 在此做 demosaic)

5.2.2 代码

// 创建 JPEG 输出的 ImageReader
private void setupImageReader() {
    imageReader = ImageReader.newInstance(
        4032, 3024, ImageFormat.JPEG, 1);  // maxImages=1

    imageReader.setOnImageAvailableListener(reader -> {
        Image image = reader.acquireLatestImage();
        if (image != null) {
            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            byte[] jpegData = new byte[buffer.remaining()];
            buffer.get(jpegData);
            saveToFile(jpegData);
            image.close();  // ← 不 close 会阻塞后续帧!
        }
    }, null);
}

// 拍照——发一次 capture 请求
private void captureStill() {
    CaptureRequest.Builder captureBuilder =
        cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);

    // ⚠ 很多教程只 addTarget JPEG Surface,导致拍照时预览卡顿
    captureBuilder.addTarget(previewSurface);       // 保持预览
    captureBuilder.addTarget(imageReader.getSurface());  // 同时出 JPEG
    captureBuilder.set(CaptureRequest.JPEG_QUALITY, (byte) 95);

    captureSession.capture(captureBuilder.build(), null, null);
}

💡 预览不卡的关键——同时 addTarget 两个 Surface

// ❌ 可能卡顿:只给 JPEG Surface
captureBuilder.addTarget(imageReader.getSurface());  // 预览断流一帧

// ✅ 预览流畅:同时给两个 Surface
captureBuilder.addTarget(previewSurface);        // 预览继续
captureBuilder.addTarget(imageReader.getSurface()); // 同时出 JPEG

HAL 层看到 Request 的 output targets 包含两个 Surface,就会同时给两条 Stream 输出数据。预览和 JPEG 并行处理,互不阻塞。


5.3 录像——绕过 CPU 的 Buffer 共享

录像的核心:ISP 输出的 YUV buffer 通过 DMA-BUF fd 直接传给 MediaCodec 编码器,省掉 CPU 拷贝。

5.3.1 数据流

Sensor → ISP → YUV
            ├──→ SurfaceView(显示预览)
            └──→ MediaCodec Surface(编码 H.264 → .mp4)
                       ↑
                 ISP DMA 写入 Gralloc buffer → 通过 dma-buf fd 传 handle → 编码器硬件直接读
                 全程没有 CPU memcpy!

这就是为什么 COLOR_FormatSurface 很重要——它告诉编码器"通过 gralloc handle 直接访问 buffer,不用 CPU 拷贝数据"。

5.3.2 代码

private MediaCodec setupMediaCodec(int width, int height) {
    MediaCodec codec = MediaCodec.createEncoderByType(
        MediaFormat.MIMETYPE_VIDEO_AVC);  // H.264

    MediaFormat format = MediaFormat.createVideoFormat(
        MediaFormat.MIMETYPE_VIDEO_AVC, width, height);
    format.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 3);
    format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
    format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
        MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        // ↑ 这个参数让编码器通过 gralloc handle 直接访问 ISP 写入的 buffer
    format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);  // 每秒一个关键帧

    codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

    // 获取编码器的输入 Surface——把它传给 Camera
    Surface encoderSurface = codec.getInputSurface();

    // createCaptureSession 时把这个 Surface 加进去
    List<Surface> surfaces = Arrays.asList(
        new Surface(previewView.getSurfaceTexture()),  // 预览
        encoderSurface                                 // 录像
    );
    cameraDevice.createCaptureSession(surfaces, ...);

    codec.start();
    return codec;
}

5.3.3 码率设置

码率 = width × height × fps × 压缩因子

典型值(H.264):
  1080p@30fps: 1920 × 1080 × 30 × 0.08 ≈ 5 Mbps
  4K@30fps:    3840 × 2160 × 30 × 0.08 ≈ 20 Mbps
  4K@60fps:    3840 × 2160 × 60 × 0.08 ≈ 40 Mbps

设太低 → 画面糊
设太高 → 文件大,编码器跟不上帧率

5.4 Surface 选择策略

Surface 类型 能否 CPU 读取 性能 适用场景
SurfaceView 不能 最高(硬件 overlay) 纯显示预览
TextureView 不能 高(比 SurfaceView 多一次合成) 需要变换预览(缩放/旋转)
ImageReader(YUV) 中(CPU 可访问,有一份拷贝) 需要读像素做处理
ImageReader(JPEG) 拍照输出
MediaCodec Surface 不能 最高(DMA-BUF 共享,无 CPU 拷贝) 录像

常见组合

场景 1: 仅预览
  createCaptureSession([previewSurface])

场景 2: 预览 + 拍照
  createCaptureSession([previewSurface, jpegReaderSurface])

场景 3: 预览 + 录像
  createCaptureSession([previewSurface, mediaCodecSurface])

场景 4: 预览 + 拍照 + 录像(3 条 Stream)
  createCaptureSession([previewSurface, jpegReaderSurface, mediaCodecSurface])
  ⚠ 三条 Stream 意味着 ISP 需要同时输出三路数据——确认 ISP 带宽够用

5.5 常见问题

Q1: createCaptureSession 配置了最大分辨率,但报错 onConfigureFailed

根因:所有 Surface 的总带宽超出 ISP 处理能力。同时配 48MP JPEG + 4K YUV + 1080p 预览可能撑不住。

解决:降低一个或多个 Surface 的分辨率。或者分时复用。

Q2: ImageReader 收不到回调

四个检查点:

  1. ImageReader 的 Surface 在 createCaptureSession 时传入了吗?
  2. capture 的 Request 中 addTarget 了吗?
  3. newInstance 时 maxImages 是几?设为 1 的话,前一张不 close 会卡住
  4. image.close() 调了吗?不调会阻塞后续帧——这是最常见的泄漏原因

Q3: 拍照后预览卡了一帧

原因:你的 capture Request 只 addTarget 了 JPEG Surface,没加预览 Surface。HAL 在这一帧不给预览 Stream 输出数据,预览就"停"了一帧。

修复:拍照的 Request 同时 addTarget(previewSurface)addTarget(jpegReaderSurface)

Q4: 预览画面方向不对

原因:Sensor 的安装方向(SENSOR_ORIENTATION)和屏幕方向不一致。

int orientation = chars.get(CameraCharacteristics.SENSOR_ORIENTATION);
// 后摄通常 90°,前摄通常 270°
if (facing == LENS_FACING_FRONT) {
    orientation = (360 - orientation) % 360;  // 前摄需镜像
}
textureView.setTransform(transformMatrix);

5.6 本章总结

预览 = setRepeatingRequest(持续发请求,Pipeline STREAM_ON)
拍照 = capture(单次请求,多一条 JPEG Stream,触发 Offline Pipeline)
录像 = MediaCodec + COLOR_FormatSurface(DMA-BUF 共享,省 CPU 拷贝)

HAL 层对应的操作:
  openCamera()        → initialize() + configure_streams()
  setRepeatingRequest → 每隔 ~33ms 一次 process_capture_request()
  capture()           → 一次 process_capture_request() with JPEG target
  close()             → flush() + StreamOff + 释放 Buffer

Surface 的选择决定性能和灵活性:
  预览用 SurfaceView/TextureView
  读像素用 ImageReader(YUV)
  拍照用 ImageReader(JPEG)
  录像用 MediaCodec Surface

动手验证

  1. 用上面的代码片段拼一个能预览的 App。打开相机,确认画面正常。

  2. 在 logcat 中找到 configure_streams 对应的日志,确认 Stream 配置和你传入的 Surface 列表一致。

  3. 在拍照的 capture Request 中先不加 previewSurface 的 addTarget,然后拍照——观察预览是否卡了一帧。再加上 addTarget(previewSurface),对比效果。

  4. 查看 dumpsys 输出中当前 Session 的 Stream 列表:

    adb shell dumpsys media.camera | grep -A 15 "Stream"
    

    确认 Stream 的数量、格式、分辨率与你的代码一致。

常见误解