当前位置: 首页 > news >正文

Android Camera2 API实时采集视频流并编码H264的实践指南

1. 为什么选择Camera2 API?

在Android开发中处理摄像头功能时,开发者通常会面临Camera API和Camera2 API的选择。传统Camera API虽然简单易用,但随着Android系统的发展,它已经暴露出诸多局限性。Camera2 API作为Android 5.0引入的全新架构,提供了更精细的控制能力和更高的性能表现。

我刚开始接触Camera2 API时也觉得很复杂,但实际使用后发现它带来的灵活性完全值得学习成本。Camera2采用管道(Pipeline)设计模式,将摄像头操作抽象为捕获请求(CaptureRequest)和捕获结果(CaptureResult),这种设计让开发者能够精确控制每个环节。

Camera2相比Camera API有几个明显优势:

  • 更低的延迟:直接访问图像传感器数据,减少中间处理环节
  • 更丰富的功能:支持手动对焦、曝光控制、连拍等高级特性
  • 更好的性能:异步处理机制避免阻塞UI线程
  • 更稳定的帧率:可以精确控制帧率参数

2. 基础环境搭建

2.1 权限与布局配置

首先需要在AndroidManifest.xml中声明相机权限:

<uses-permission android:name="android.permission.CAMERA" />

对于Android 6.0及以上系统,还需要动态申请权限。这里我推荐使用ActivityResult API来处理权限请求:

private val cameraPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { setupCamera() } else { Toast.makeText(this, "需要相机权限", Toast.LENGTH_SHORT).show() } }

布局文件只需要一个TextureView用于预览:

<TextureView android:id="@+id/textureView" android:layout_width="match_parent" android:layout_height="match_parent" />

2.2 Camera2核心组件

Camera2 API有几个关键类需要了解:

  • CameraManager:管理系统所有摄像头设备
  • CameraCharacteristics:描述摄像头特性
  • CameraDevice:代表物理摄像头设备
  • CameraCaptureSession:管理摄像头数据流
  • CaptureRequest:定义捕获参数配置

初始化CameraManager的典型代码:

CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); String[] cameraIds = manager.getCameraIdList(); String cameraId = cameraIds[0]; // 通常后置摄像头是0

3. 配置图像采集管道

3.1 选择合适的分辨率

获取摄像头支持的分辨率列表:

CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); StreamConfigurationMap map = characteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); Size[] outputSizes = map.getOutputSizes(SurfaceTexture.class);

选择分辨率时需要考虑几个因素:

  1. 预览画面的宽高比要匹配显示区域
  2. 编码性能与分辨率成正比
  3. 不同设备支持的分辨率差异很大

我通常的做法是选择最接近1080p的分辨率,这样在画质和性能之间取得平衡。

3.2 创建ImageReader

ImageReader是我们获取YUV数据的关键:

ImageReader imageReader = ImageReader.newInstance( width, height, ImageFormat.YUV_420_888, // Camera2使用这个格式而非NV21 2); // 缓冲区数量 imageReader.setOnImageAvailableListener({ reader -> // 在这里处理每一帧图像 }, handler);

YUV_420_888是Camera2的标准格式,与Camera API的NV21不同,需要注意转换处理。

4. 视频编码实现

4.1 MediaCodec初始化

配置H264编码器:

MediaCodec codec = MediaCodec.createEncoderByType("video/avc"); MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height); format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); codec.start();

几个关键参数说明:

  • BIT_RATE:影响视频质量和文件大小,建议按分辨率设置
  • FRAME_RATE:通常设为30,太高会导致编码压力大
  • I_FRAME_INTERVAL:关键帧间隔,影响seek性能和文件大小

4.2 YUV格式转换

Camera2的YUV_420_888需要转换为MediaCodec支持的格式:

private byte[] convertYUV420888ToNV12(Image image) { ByteBuffer yBuffer = image.getPlanes()[0].getBuffer(); ByteBuffer uBuffer = image.getPlanes()[1].getBuffer(); ByteBuffer vBuffer = image.getPlanes()[2].getBuffer(); int ySize = yBuffer.remaining(); int uSize = uBuffer.remaining(); int vSize = vBuffer.remaining(); byte[] nv12 = new byte[ySize + uSize + vSize]; // Y分量 yBuffer.get(nv12, 0, ySize); // UV分量交错排列 int pos = ySize; for (int i = 0; i < uSize; i++) { nv12[pos++] = vBuffer.get(i); nv12[pos++] = uBuffer.get(i); } return nv12; }

4.3 编码流程

完整的编码处理流程:

// 从ImageReader获取图像 Image image = imageReader.acquireLatestImage(); byte[] nv12 = convertYUV420888ToNV12(image); // 输入编码器 int inputBufferIndex = codec.dequeueInputBuffer(TIMEOUT_US); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex); inputBuffer.put(nv12); codec.queueInputBuffer(inputBufferIndex, 0, nv12.length, System.nanoTime() / 1000, 0); } // 获取编码输出 MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US); while (outputBufferIndex >= 0) { ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferIndex); byte[] h264Data = new byte[bufferInfo.size]; outputBuffer.get(h264Data); // 处理编码后的H264数据 saveToFile(h264Data); codec.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US); }

5. 实战中的坑与解决方案

5.1 图像方向问题

摄像头传感器的自然方向通常是横向的,而手机竖屏使用时需要旋转90度。Camera2中可以通过设置CaptureRequest的旋转参数来解决:

captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));

辅助方法计算正确方向:

private int getOrientation(int rotation) { switch (rotation) { case Surface.ROTATION_0: return 90; case Surface.ROTATION_90: return 0; case Surface.ROTATION_180: return 270; case Surface.ROTATION_270: return 180; } return 0; }

5.2 帧率不稳定问题

在实际测试中我发现,如果不做特殊处理,帧率经常会波动。解决方案是在创建CaptureRequest时设置目标帧率范围:

captureRequestBuilder.set( CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(frameRate, frameRate));

5.3 内存泄漏预防

Camera2 API使用不当很容易导致内存泄漏,特别是没有正确关闭资源时。我的经验是建立完善的生命周期管理:

@Override protected void onPause() { super.onPause(); closeCamera(); } private void closeCamera() { if (captureSession != null) { captureSession.close(); captureSession = null; } if (cameraDevice != null) { cameraDevice.close(); cameraDevice = null; } if (imageReader != null) { imageReader.close(); imageReader = null; } }

6. 性能优化技巧

6.1 选择合适的YUV格式

不同设备支持的YUV格式可能不同,通过查询编解码器能力选择最优格式:

MediaCodecInfo.CodecCapabilities caps = codec.getCodecInfo() .getCapabilitiesForType("video/avc"); int[] colorFormats = caps.colorFormats;

我通常优先选择COLOR_FormatYUV420Flexible,它在Android 5.0+上广泛支持。

6.2 使用Surface提高效率

直接将TextureView的Surface传递给MediaCodec可以避免CPU拷贝,大幅提升性能:

Surface encoderSurface = codec.createInputSurface(); captureRequestBuilder.addTarget(encoderSurface);

6.3 动态码率调整

根据网络状况或存储空间动态调整码率:

if (networkIsSlow) { format.setInteger(MediaFormat.KEY_BIT_RATE, lowBitrate); codec.setParameters(format); }

7. 完整实现示例

以下是核心代码的整合版本:

public class Camera2Encoder { private CameraDevice cameraDevice; private CameraCaptureSession captureSession; private ImageReader imageReader; private MediaCodec mediaCodec; private void setupCamera() { CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE); // 选择后置摄像头 String cameraId = manager.getCameraIdList()[0]; // 创建ImageReader imageReader = ImageReader.newInstance(width, height, ImageFormat.YUV_420_888, 2); imageReader.setOnImageAvailableListener({ reader -> processImage(reader.acquireLatestImage()); }, handler); // 初始化编码器 initEncoder(); // 打开摄像头 manager.openCamera(cameraId, new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice camera) { cameraDevice = camera; createCaptureSession(); } // ...其他回调方法 }, handler); } private void createCaptureSession() { List<Surface> surfaces = new ArrayList<>(); surfaces.add(imageReader.getSurface()); surfaces.add(mediaCodec.createInputSurface()); cameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { captureSession = session; startPreview(); } // ...其他回调方法 }, handler); } private void processImage(Image image) { // 转换格式并编码 byte[] nv12 = convertYUV420888ToNV12(image); encodeFrame(nv12); image.close(); } private void encodeFrame(byte[] data) { // 编码实现... } }

在实际项目中,我发现合理设置缓冲区数量和选择合适的Handler线程对性能影响很大。通常我会使用专门的HandlerThread来处理相机回调,避免阻塞主线程。

http://www.jsqmd.com/news/650098/

相关文章:

  • 5个OpenRocket火箭仿真技巧:从零到一的完整指南
  • C语言初学者必看:如何用冒泡排序实现英文单词长度排序(附完整代码)
  • 从Socket到RDMA:一个Java后端开发者的真实踩坑与性能对比实验(附代码)
  • 讲讲服务不错的GEO推广机构,如何选出适合自己的品牌 - 工业品网
  • eNSP静态路由配置全网可达入门练习
  • 告别反复烧录:用STM32F407 IAP打造你的产品远程固件更新方案
  • 保姆级教程:在Ubuntu 20.04上搞定OMNeT++ 4.6安装与环境配置(含依赖库完整清单)
  • 2025终极方案:八大网盘直链下载助手LinkSwift完全解析
  • Clear Temporary Files
  • 从复位到运行:深入解析STM32 Boot模式的选择与实战
  • 2026年性价比高的AI推广品牌企业盘点,为你揭秘优质之选 - myqiye
  • PowerISO:虚拟光驱软件解决映像文件打开与编辑难题
  • 如何快速掌握canvas-editor:开源富文本编辑器的完整指南
  • 别再傻傻分不清了!Linux exec函数族(execl/execv)保姆级选择指南与实战避坑
  • 避坑指南:用R做批量单因素Logistic回归时,你的分类变量处理对了吗?
  • Faster-Whisper-GUI:一键将音频视频转换为精准字幕的高效解决方案
  • HandheldCompanion:如何让Windows掌机获得完美控制器兼容性
  • 基于C++实现的简单的SMTP服务器
  • 用Python+OpenCV复现DWT-DCT-SVD图像水印:从原理到代码的保姆级教程
  • 一键解决米哈游游戏登录难题:MHY_Scanner完整使用教程
  • 别再浪费480MHz主频!手把手教你优化STM32H750的Keil工程内存布局
  • Portainer部署实战:一键配置默认管理员凭据
  • 如何轻松下载国家中小学智慧教育平台电子课本:3步搞定完整教材获取
  • 还在为3D模型查看而烦恼?3步解锁浏览器中的专业级3D查看体验
  • PS4存档管理终极指南:Apollo Save Tool完全使用教程
  • AI+地图:智能进化下的出行革命
  • 海康威视MV_CC_GetImageBuffer接口详解:如何正确释放缓存与避免内存泄漏
  • Python自动化抢票脚本:3步构建大麦网秒级响应系统
  • 死亡是万物的基石
  • 从游戏地图到城市设计:Voronoi算法在Unity和GIS中的实战应用对比