4. CameraCaptureSession:创建CaptureSession、配置输出Surface、处理Session状态回调

好,咱们接着往下聊。上一节我们把 CameraDevice 打开了,但光有设备还不行——你得告诉它「往哪儿输出数据」。这就轮到 CameraCaptureSession 登场了。

说白了,CaptureSession 就是摄像头和输出目标之间的桥梁。你想想看,摄像头采集到的每一帧数据,总得有个去处吧?是显示到屏幕上,还是编码成视频文件,或者交给图像处理器做分析?这些目标,就是 Surface。而 CaptureSession 负责把摄像头和这些 Surface 绑定起来,然后启动数据流。

核心要点:CaptureSession 的生命周期 = 配置 Surface → 创建会话 → 处理回调 → 关闭会话。一步都不能乱。

4.1 创建 CaptureSession 的完整流程

创建会话,我习惯分三步走:

  1. 准备好输出 Surface——比如 TextureView 的 Surface、MediaCodec 的输入 Surface、ImageReader 的 Surface。
  2. 构建请求模板——用 CameraDevice.createCaptureRequest() 生成 CaptureRequest。
  3. 调用 CameraDevice.createCaptureSession()——传入 Surface 列表和回调。

来看一段实际代码。这是我项目里常用的写法:

// 1. 准备输出 Surface
SurfaceTexture texture = textureView.getSurfaceTexture();
texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
Surface previewSurface = new Surface(texture);

// 2. 构建预览请求
CaptureRequest.Builder previewRequestBuilder = cameraDevice.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW
);
previewRequestBuilder.addTarget(previewSurface);

// 3. 创建 CaptureSession
cameraDevice.createCaptureSession(
    Arrays.asList(previewSurface),
    new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            // 会话创建成功,可以开始预览了
            captureSession = session;
            try {
                session.setRepeatingRequest(previewRequestBuilder.build(), null, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
            // 配置失败,需要处理
            Log.e(TAG, "会话配置失败");
        }
    },
    backgroundHandler
);

嗯,这里要注意:createCaptureSession() 是异步方法。你调用它之后,不会立刻得到会话对象。系统会在内部完成配置,然后通过回调告诉你结果。所以千万别在调用之后马上用 session,否则你会拿到 null。

我曾经踩过的坑:早期做项目时,我在 onConfigured 回调外面持有了 session 引用,结果在另一个线程里直接调用 session.capture()——崩溃了。原因是回调还没触发,session 还是 null。后来我加了个 AtomicReference 做保护,才稳下来。

4.2 配置输出 Surface:单路与多路输出

大多数场景下,我们只需要一个输出 Surface——比如预览。但有些需求需要同时输出多路数据,比如:

  • 预览 + 拍照
  • 预览 + 视频编码
  • 预览 + 图像分析(比如人脸检测)

这时候,你传给 createCaptureSession() 的 Surface 列表里就要包含多个 Surface。但有个限制:所有 Surface 必须来自同一个 CameraDevice,而且它们的尺寸和格式要兼容。

我个人习惯用 SessionConfiguration 来配置多路输出,这是 Android 8.0 引入的 API,更清晰:

List<OutputConfiguration> outputConfigs = new ArrayList<>();
outputConfigs.add(new OutputConfiguration(previewSurface));
outputConfigs.add(new OutputConfiguration(imageReaderSurface));

SessionConfiguration sessionConfig = new SessionConfiguration(
    SessionConfiguration.SESSION_REGULAR,
    outputConfigs,
    backgroundExecutor,
    stateCallback
);

cameraDevice.createCaptureSession(sessionConfig);

为什么推荐这种方式?因为 OutputConfiguration 可以设置更多参数,比如 Surface 的旋转角度、压缩格式等。而且它支持 SurfaceSharing——多个 CaptureRequest 共享同一个 Surface,这在某些场景下能省内存。

小技巧:如果你只需要预览 + 拍照,其实不用创建两个 Surface。拍照时临时把预览的 Surface 替换成 ImageReader 的 Surface 就行。但如果你需要同时预览和录像,那就必须用两个 Surface 了。

4.3 处理 Session 状态回调

CameraCaptureSession.StateCallback 里有几个关键回调,我列个表方便你对照:

回调方法 触发时机 常见处理
onConfigured 会话配置成功,可以开始捕获 启动预览循环(setRepeatingRequest)
onConfigureFailed 配置失败,无法使用会话 释放资源,提示用户重试
onReady 会话空闲,没有正在进行的请求 可以安全地关闭会话或切换配置
onActive 会话开始处理请求 更新 UI 状态(比如显示录制中)
onClosed 会话已关闭 清理资源,置空引用
onSurfacePrepared 某个 Surface 已准备好接收数据 用于延迟添加 Surface 的场景

这里面,onConfigured 和 onConfigureFailed 是最常用的。但 onReady 和 onActive 也很有用——比如你想在用户开始录像时显示一个红点,就可以在 onActive 里更新 UI。

我记得有一次做直播项目,用户反馈偶尔会黑屏。排查了半天,发现是 onConfigureFailed 回调里没有做任何处理,导致会话创建失败后程序继续往下走,最终 Surface 一直没数据。后来我在回调里加了重试逻辑,问题就解决了。

避坑指南:onConfigureFailed 不常见,但一旦触发,通常意味着 Surface 配置有问题。比如 Surface 尺寸不被支持,或者 Surface 已经被释放了。建议在回调里打印详细日志,并尝试用 CameraCharacteristics 查询支持的尺寸。

4.4 核心逻辑流程图

下面这张图概括了 CaptureSession 的完整生命周期。我画的时候特意把「创建」和「关闭」两个关键路径标红了,你重点关注一下。

CameraCaptureSession 生命周期 创建 Session onConfigured onActive(开始捕获) onReady(空闲) 关闭 Session onConfigureFailed setRepeatingRequest 正常路径 失败/关闭路径

从图里你能看到,正常流程是:创建 → onConfigured → onActive(开始捕获)→ onReady(空闲)→ 关闭。如果配置失败,直接跳到 onConfigureFailed,然后也必须关闭会话。

这里有个细节:onReady 和 onActive 会交替触发。当你调用 setRepeatingRequest 后,会话进入 onActive;当所有请求完成(比如你停止了重复请求),会话回到 onReady。所以 onReady 是关闭会话的最佳时机——确保没有正在进行的请求,避免数据丢失。

4.5 会话关闭与资源释放

最后聊一下关闭。关闭会话很简单,调用 session.close() 就行。但要注意顺序:

  1. 先停止所有正在进行的请求(调用 session.stopRepeating() 或 session.abortCaptures())
  2. 再关闭会话
  3. 最后关闭 CameraDevice

为什么?因为如果你先关设备,会话还在跑,系统会报错。我见过不少开发者把顺序搞反,结果 logcat 里刷出一堆 IllegalStateException。

我的习惯:在 onPause() 或 onDestroy() 里统一做清理。我会用一个状态机来跟踪当前会话的状态,确保不会重复关闭。比如用一个 int 变量表示状态:STATE_IDLE、STATE_OPENING、STATE_OPENED、STATE_CLOSING。这样能避免很多并发问题。

好了,关于 CaptureSession 的核心内容就这些。你只要记住:创建时配好 Surface,回调里处理状态,关闭时注意顺序,基本就不会出大问题。下一节我们会聊到 CaptureRequest 的详细配置,到时候你会看到 Session 和 Request 是怎么配合工作的。


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