Camera2 API 基础:从 CameraManager 到预览回调
说实话,Android 的相机开发,从 Camera1 到 Camera2 的迁移,当年可把我折腾得不轻。我记得第一次接手一个视频录制项目,打开官方文档看到那一堆回调接口,脑袋都是懵的。但用久了你会发现,Camera2 的设计其实很优雅——它把相机设备当成一个管道,你往里面塞数据,它给你吐出来。
今天我们就来拆解 Camera2 的核心组件。说白了,就四个东西:CameraManager、CameraDevice、CaptureSession,以及贯穿始终的回调机制。
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();
}
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();
}
}
预览流程:从打开到显示
整个预览流程,我画了一张图帮你理解:
流程其实不复杂: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) {
// 检查对焦状态
}
}
};
避坑指南:我踩过的那些坑
做 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、发请求、收回调。把这四个环节搞明白,后面做录制、推流就顺了。