Camera2 API 基础:从 CameraManager 到预览回调

说实话,Android 的相机开发,从 Camera1 到 Camera2 的迁移,当年可把我折腾得不轻。我记得第一次接手一个视频录制项目,打开官方文档看到那一堆回调接口,脑袋都是懵的。但用久了你会发现,Camera2 的设计其实很优雅——它把相机设备当成一个管道,你往里面塞数据,它给你吐出来。

今天我们就来拆解 Camera2 的核心组件。说白了,就四个东西:CameraManagerCameraDeviceCaptureSession,以及贯穿始终的回调机制

CameraManager:相机管家

CameraManager 是整个相机系统的入口。它负责枚举设备、打开相机、管理特性。你可以把它想象成酒店的前台——你想开哪个房间(相机),得先找它。

获取方式很简单:

CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);

拿到 manager 之后,第一件事通常是获取相机 ID 列表:

try {
    String[] cameraIdList = manager.getCameraIdList();
    for (String id : cameraIdList) {
        CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
        Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
        if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
            // 找到后置摄像头
            Log.d("Camera2", "后置摄像头 ID: " + id);
        }
    }
} catch (CameraAccessException e) {
    e.printStackTrace();
}
小技巧: 我个人习惯在获取 CameraCharacteristics 之后,顺手检查一下硬件支持级别。有些低端设备虽然支持 Camera2,但只支持 LEGACY 级别,很多高级特性用不了。

CameraDevice:相机设备本体

CameraDevice 代表一个物理相机。打开它需要传入一个回调:

manager.openCamera(cameraId, new CameraDevice.StateCallback() {
    @Override
    public void onOpened(@NonNull CameraDevice camera) {
        // 相机打开成功,可以开始干活了
        mCameraDevice = camera;
        createPreviewSession();
    }

    @Override
    public void onDisconnected(@NonNull CameraDevice camera) {
        camera.close();
        mCameraDevice = null;
    }

    @Override
    public void onError(@NonNull CameraDevice camera, int error) {
        camera.close();
        mCameraDevice = null;
        Log.e("Camera2", "相机出错,错误码: " + error);
    }
}, null);

这里有个坑——onOpened 是在子线程回调的。我曾经在 onOpened 里直接更新 UI,结果 crash 了。所以记得用 Handler 切回主线程,或者用 runOnUiThread。

CaptureSession:数据管道

CaptureSession 是 Camera2 最核心的概念。它把 CameraDevice 和 Surface 连接起来。你想想看,相机采集到的数据要送到哪里去?送到 Surface 上。Surface 可以是 TextureView、SurfaceView、ImageReader,甚至是 MediaCodec 的输入。

创建预览 Session 的典型代码:

private void createPreviewSession() {
    try {
        SurfaceTexture texture = mTextureView.getSurfaceTexture();
        texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        Surface surface = new Surface(texture);

        mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        mPreviewRequestBuilder.addTarget(surface);

        mCameraDevice.createCaptureSession(Arrays.asList(surface),
                new CameraCaptureSession.StateCallback() {
                    @Override
                    public void onConfigured(@NonNull CameraCaptureSession session) {
                        mCaptureSession = session;
                        // 开始循环预览
                        startPreview();
                    }

                    @Override
                    public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                        Log.e("Camera2", "Session 配置失败");
                    }
                }, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}
注意: createCaptureSession 的 Surface 列表顺序很重要。如果你同时传了预览 Surface 和录制 Surface,第一个 Surface 会被用于自动对焦和曝光测量。我曾经因为顺序搞反,导致录制画面正常但预览画面一直对不上焦。

预览流程:从打开到显示

整个预览流程,我画了一张图帮你理解:

Camera2 预览流程 CameraManager 获取相机列表 openCamera() StateCallback.onOpened CameraDevice 创建 CaptureRequest createCaptureSession() StateCallback.onConfigured CaptureSession setRepeatingRequest() 预览画面 TextureView / SurfaceView 回调机制 CameraCaptureSession.CaptureCallback onCaptureCompleted / onCaptureFailed

流程其实不复杂:Manager → Device → Session → 循环请求。但每一步都有回调,这也是初学者最容易晕的地方。

回调机制:无处不在的监听

Camera2 的回调,我总结为三个层次:

回调类型 触发时机 常用方法
CameraDevice.StateCallback 相机设备状态变化 onOpened, onDisconnected, onError
CameraCaptureSession.StateCallback Session 状态变化 onConfigured, onConfigureFailed
CameraCaptureSession.CaptureCallback 每一帧的捕获结果 onCaptureStarted, onCaptureCompleted, onCaptureFailed

其中 CaptureCallback 是最常用的。每一帧数据从相机出来,都会回调到这里。你可以在这里拿到帧的时间戳、对焦状态、曝光参数等。

private CameraCaptureSession.CaptureCallback mCaptureCallback = 
        new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureStarted(@NonNull CameraCaptureSession session,
                                 @NonNull CaptureRequest request,
                                 long timestamp, long frameNumber) {
        super.onCaptureStarted(session, request, timestamp, frameNumber);
        // 这一帧开始捕获了,timestamp 可以用来做同步
    }

    @Override
    public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                   @NonNull CaptureRequest request,
                                   @NonNull TotalCaptureResult result) {
        super.onCaptureCompleted(session, request, result);
        // 这一帧捕获完成,result 里包含元数据
        Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
        if (afState != null) {
            // 检查对焦状态
        }
    }
};
重点: onCaptureCompleted 返回的是 TotalCaptureResult,它包含了这一帧的所有元数据。如果你要做人脸检测、场景识别、或者判断是否对焦成功,都在这里拿数据。

避坑指南:我踩过的那些坑

做 Camera2 开发,有些坑几乎是必踩的。我分享一下我的血泪史:

  • Session 关闭顺序:我曾经在 Activity 的 onPause 里直接 close CameraDevice,结果 Session 还没释放,导致崩溃。正确的顺序是:先关闭 Session,再关闭 Device。
  • Surface 生命周期:TextureView 销毁后,它的 Surface 就失效了。如果你还在用这个 Surface 创建 Session,会收到 onConfigureFailed。所以一定要监听 Surface 的可用性。
  • 预览尺寸选择:不要直接写死 1920x1080。不同设备支持的尺寸不一样。我习惯从 CameraCharacteristics 的 SCALER_STREAM_CONFIGURATION_MAP 里获取支持的尺寸列表,然后选一个最合适的。
  • 回调线程:所有回调默认都在同一个 HandlerThread 上执行。如果你在回调里做耗时操作,会阻塞后续帧的处理。我的做法是:回调里只做轻量级操作,复杂逻辑 post 到工作线程。

一个完整的预览启动代码

最后,我把前面讲的东西串起来,给你一个完整的预览启动流程:

private void startCamera() {
    CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
    try {
        String cameraId = manager.getCameraIdList()[0]; // 简单起见取第一个
        manager.openCamera(cameraId, new CameraDevice.StateCallback() {
            @Override
            public void onOpened(@NonNull CameraDevice camera) {
                mCameraDevice = camera;
                SurfaceTexture texture = mTextureView.getSurfaceTexture();
                texture.setDefaultBufferSize(1280, 720);
                Surface surface = new Surface(texture);

                try {
                    CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                    builder.addTarget(surface);

                    camera.createCaptureSession(Arrays.asList(surface),
                            new CameraCaptureSession.StateCallback() {
                                @Override
                                public void onConfigured(@NonNull CameraCaptureSession session) {
                                    mSession = session;
                                    try {
                                        session.setRepeatingRequest(builder.build(), mCaptureCallback, null);
                                    } catch (CameraAccessException e) {
                                        e.printStackTrace();
                                    }
                                }

                                @Override
                                public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                                    Log.e("Camera2", "配置失败");
                                }
                            }, null);
                } catch (CameraAccessException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onDisconnected(@NonNull CameraDevice camera) {
                camera.close();
            }

            @Override
            public void onError(@NonNull CameraDevice camera, int error) {
                camera.close();
            }
        }, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

嗯,代码看着有点长,但核心逻辑就那几步。你写多了就会发现,Camera2 的套路其实很固定——开设备、配 Session、发请求、收回调。把这四个环节搞明白,后面做录制、推流就顺了。

我的建议: 刚开始学的时候,不要急着写复杂的录制功能。先把预览跑通,把回调里的日志打出来,看看每一帧的 onCaptureStarted 和 onCaptureCompleted 是怎么触发的。理解了数据流,后面就好办了。

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