2. Android Camera开发基础:Camera2 API概览、打开与预览Camera、配置预览尺寸与格式
各位同学,咱们今天来啃一块硬骨头——Camera2 API。说实话,我刚接触这个API的时候,心里是有点抵触的。为什么?因为Camera1用得好好的,突然冒出来一套全新的架构,学习曲线陡得很。但后来我在一个直播项目里被Camera1的性能瓶颈卡了整整两周,才明白Google为什么要推Camera2。
Camera2说白了,就是把相机控制权真正交到了开发者手里。它不再是Camera1那种「你按快门,我拍照」的黑盒模式,而是把整个拍照流程拆成了一个个可定制的管道。你想想看,做音视频传输,我们需要精确控制每一帧的采集时机、尺寸、格式,Camera1根本做不到。
2.1 Camera2 API的核心架构
先看一张图,把Camera2的脉络理清楚。
这张图我建议你多看几遍。整个流程是这样的:CameraManager负责发现和打开相机,拿到CameraDevice之后,用它创建CameraCaptureSession。会话里,我们提交CaptureRequest(告诉相机要做什么),相机处理后返回CaptureResult(告诉你做了什么),最终数据流向Surface。
我个人习惯把Camera2理解成「请求-响应」模式。你发一个请求,相机给你一帧数据。这在做RTMP推流时特别有用——我们可以精确控制每一帧的采集时机,而不是像Camera1那样被动接收。
核心概念速记:
- CameraManager:相机管家,负责枚举和打开相机
- CameraDevice:代表一个物理相机,前摄或后摄
- CameraCaptureSession:数据通道,管理请求和结果的流转
- CaptureRequest:你给相机的「指令书」,包含各种参数
- CaptureResult:相机给你的「回执」,包含实际使用的参数
- Surface:数据最终渲染或读取的地方
2.2 打开Camera并预览
好,理论说完了,咱们直接上代码。打开相机预览,我把它拆成三步走。
第一步:获取CameraManager
// 获取系统服务
CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
这一步没什么好说的,就是个系统服务。但注意,从Android 6.0开始,相机权限是运行时权限,别忘了在打开相机前检查并申请。
我曾经踩过的坑:在Fragment里直接getActivity().getSystemService(),结果Activity还没attach,返回null。后来我改成在onAttach之后才获取,或者在Application里初始化一个全局的CameraManager实例。
第二步:打开相机
// 获取后置摄像头ID
String cameraId = null;
for (String id : cameraManager.getCameraIdList()) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
cameraId = id;
break;
}
}
// 打开相机
cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
// 相机打开成功
mCameraDevice = camera;
// 接下来创建预览会话
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
mCameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
camera.close();
mCameraDevice = null;
}
}, backgroundHandler);
这里有个细节:openCamera是异步的,结果通过回调返回。你想想看,如果它是同步的,主线程卡住怎么办?所以Google设计成异步,我们必须在onOpened回调里继续后续操作。
第三步:创建预览会话
// 假设你已经有了一个TextureView
SurfaceTexture texture = textureView.getSurfaceTexture();
texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
Surface surface = new Surface(texture);
// 创建CaptureRequest.Builder
CaptureRequest.Builder previewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(surface);
// 创建会话
mCameraDevice.createCaptureSession(Arrays.asList(surface),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mCaptureSession = session;
// 设置重复请求,开始预览
session.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler);
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
// 配置失败,通常是因为Surface不合法
}
}, backgroundHandler);
嗯,这里要注意:createCaptureSession也是异步的。我刚开始写的时候,以为调用完就立刻能用,结果在onConfigured之前就提交了请求,直接崩溃。所以一定要在回调里操作。
小技巧:如果你用SurfaceView代替TextureView,性能会更好一些。因为SurfaceView有独立的窗口,渲染不走View的合成流程。但TextureView更方便做动画和变换。做RTMP推流时,我建议用TextureView,因为我们可以方便地做镜像、旋转等处理。
2.3 配置预览尺寸与格式
配置尺寸,说白了就是选一个合适的输出分辨率。但这里有个坑——不是所有分辨率都支持。每个摄像头都有自己的支持列表,你得从里面挑一个。
获取支持的尺寸列表
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
StreamConfigurationMap configMap = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] outputSizes = configMap.getOutputSizes(SurfaceTexture.class);
// 遍历,找到最合适的尺寸
Size previewSize = null;
for (Size size : outputSizes) {
// 比如我们想要16:9的比例,宽度不超过1920
if (size.getWidth() * 9 == size.getHeight() * 16 && size.getWidth() <= 1920) {
if (previewSize == null || size.getWidth() > previewSize.getWidth()) {
previewSize = size;
}
}
}
我个人习惯写一个工具方法,传入目标宽高比和最大分辨率,自动从支持列表里选最优的。为什么?因为不同手机的支持列表差异很大,你写死一个分辨率,很可能在某些机型上直接黑屏。
关于输出格式
Camera2支持多种输出格式,但最常用的是这两个:
| 格式 | 说明 | 适用场景 |
|---|---|---|
| ImageFormat.YUV_420_888 | YUV420平面格式,通用性强 | 视频编码、RTMP推流 |
| ImageFormat.NV21 | YUV420半平面格式,Camera1常用 | 兼容旧代码 |
| ImageFormat.JPEG | JPEG压缩格式 | 拍照 |
| ImageFormat.RGB_565 | RGB格式,质量较低 | 简单预览 |
做RTMP推流时,我们一般用YUV_420_888。为什么?因为H.264编码器直接吃YUV数据,省去了颜色空间转换的开销。我在项目中遇到过用NV21推流,结果编码器不支持,又得手动转格式,白白浪费CPU周期。
配置格式的代码示例:
// 使用ImageReader获取YUV数据
ImageReader imageReader = ImageReader.newInstance(
width, height,
ImageFormat.YUV_420_888,
2 // 最大图片数
);
imageReader.setOnImageAvailableListener(reader -> {
Image image = reader.acquireLatestImage();
if (image != null) {
// 这里拿到YUV数据,可以送给编码器
// 记得用完要close()
image.close();
}
}, backgroundHandler);
// 把ImageReader的Surface加入CaptureRequest
Surface imageSurface = imageReader.getSurface();
previewRequestBuilder.addTarget(imageSurface);
我曾经踩过的坑:ImageReader的maxImages参数设得太小(比如1),结果处理速度跟不上相机输出速度,导致丢帧。设得太大又占内存。我一般设2或3,刚好够用。另外,acquireLatestImage比acquireNextImage好,因为它会跳过积压的旧帧,保证你拿到的是最新数据。
2.4 实战中的几个关键点
最后,分享几个我在实际项目中积累的经验。
关于预览尺寸的选择:不要一味追求大分辨率。1080p的预览在大多数手机上已经足够清晰,而且编码压力小。我做过一个测试,4K预览在骁龙845上CPU占用率直接飙到80%,而1080p只有30%。做RTMP推流,稳定比清晰更重要。
关于旋转处理:手机摄像头传感器是固定的,但手机可以旋转。你拿到的数据可能是旋转90度或270度的。记得通过CameraCharacteristics.SENSOR_ORIENTATION获取传感器方向,然后在TextureView上做旋转,或者在编码前做旋转。我个人习惯在TextureView上做,因为不损失画质。
关于性能优化:如果发现预览卡顿,先检查是不是在主线程做了耗时操作。Camera2的回调默认在后台线程,但如果你在onImageAvailable里做大量计算,还是会卡。我一般用双缓冲——一个线程收数据,另一个线程处理数据。
好了,Camera2的基础就讲到这里。记住,Camera2的核心是「管道+请求」的模型,理解了这个,后面的推流实现就水到渠成了。
公众号:蓝海资料掘金营,微信deep3321