Qcom Camx全栈开发

Android Camera 全栈课程(Qcom Camx):Camera2 实战 — 预览/拍照/录像

本章导读

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

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


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

📊 对照这张图看本节:

5-.png

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

5.1.1 时序分解

对照 05_preview_timing.puml,整个预览流程分 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: CaptureSession.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: CaptureSession.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 );}

💡 setRepeatingRequest 和 capture 的本质区别

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 → BPS → IPE → Stream 0 (Display) ← 实时 Pipeline 持续工作 Stream 1 (不输出) ← Offline Pipeline 空闲 拍照帧: IFE → BPS → IPE → Stream 0 (Display) ← 预览不中断 Stream 1 → JPEG Node → JPEG 编码 → Stream 1 Buffer ↑ 这是第 13 章讲的 Offline Pipeline

5.2.2 代码

// 创建 JPEG 输出的 ImageReaderprivate 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 SurfacecaptureBuilder.addTarget(imageReader.getSurface()); // 预览断流一帧 // ✅ 预览流畅:同时给两个 SurfacecaptureBuilder.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 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 的数量、格式、分辨率与你的代码一致。

常见误解

  • "createCaptureSession 就是拍照"
     — 不对。它只是配置数据流,不产生任何图像。开始预览或触发拍照需要后续的 setRepeatingRequest 或 capture
  • "多开几个 ImageReader 没关系"
     — 每多一个 ImageReader 就多一条 Stream,多一份 Buffer 池。ISP 带宽是有限的。

(课程相关内容,会陆续在下面专栏更新,感兴趣的同学可以扫码订阅)
图片

欢迎扫码关注「小驰行动派」公众号 10 年Camera开发 | Camera技术干货 | 行业洞察 | Camera实战分享
小驰行动派公众号 扫一扫关注
分享到: 复制链接
← 上一篇 《搞定offer-面试知识点》系列,CAMX Usecase ↔ Pipeline 匹配流程 下一篇 → Android Camera 全栈课程(Qcom Camx):第 4 章:Camera2 API 核心概念

相关文章

推荐课程

想系统学习 Camera 开发?看看这些课程

评论 (0)

暂无评论,快来抢沙发吧