Android音视频开发实战:Camera API与MediaCodec编码全流程解析
第一次接触Android音视频开发时,面对Camera API、YUV格式转换、MediaCodec编码这些概念,确实容易让人望而生畏。记得去年接手一个智能门铃项目时,我在摄像头数据采集和编码环节踩遍了所有能踩的坑——画面倒置、绿屏、编码失败...这些经历让我深刻理解,音视频开发需要的是系统化的知识图谱和实战经验。本文将带你完整走通从摄像头采集到H264编码的全流程,重点解决三个核心问题:如何正确获取摄像头数据?为什么要处理YUV格式转换?MediaCodec编码有哪些隐藏陷阱?
1. 开发环境准备与基础架构
1.1 项目配置与权限管理
在AndroidManifest.xml中声明摄像头权限只是第一步,现代Android开发更需要关注运行时权限管理和设备兼容性:
<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" />动态权限申请的最佳实践应当包含以下要素:
- 权限拒绝后的优雅降级处理
- 权限说明对话框的自定义文案
- 设备无摄像头时的备用方案
private fun checkCameraPermission() { when { ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> initCamera() shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> { // 显示自定义解释对话框 showPermissionExplanationDialog() } else -> requestPermissions( arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE ) } }1.2 相机预览界面搭建
SurfaceView仍是摄像头预览的最佳选择,但需要注意:
- 表面生命周期回调的精确控制
- 合适的预览尺寸选择策略
- 横竖屏切换时的处理逻辑
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder mHolder; private Camera mCamera; public CameraPreview(Context context, Camera camera) { super(context); mCamera = camera; mHolder = getHolder(); mHolder.addCallback(this); mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } @Override public void surfaceCreated(SurfaceHolder holder) { try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); } catch (IOException e) { Log.d(TAG, "Error setting camera preview: " + e.getMessage()); } } }2. 摄像头数据采集与YUV处理
2.1 Camera API的深度使用
现代Android开发推荐使用Camera2 API,但传统Camera API在兼容性方面仍有优势。关键配置点包括:
- 预览格式设置为ImageFormat.NV21
- 合适的预览帧率配置(15/30fps)
- 自动对焦和白平衡设置
Camera.Parameters parameters = mCamera.getParameters(); List<Integer> formats = parameters.getSupportedPreviewFormats(); if (formats.contains(ImageFormat.NV21)) { parameters.setPreviewFormat(ImageFormat.NV21); } List<Camera.Size> sizes = parameters.getSupportedPreviewSizes(); Camera.Size optimalSize = getOptimalPreviewSize(sizes, width, height); parameters.setPreviewSize(optimalSize.width, optimalSize.height); mCamera.setParameters(parameters); mCamera.setPreviewCallback(previewCallback);2.2 YUV格式详解与转换技巧
Android摄像头输出的NV21格式与MediaCodec需要的NV12格式差异主要体现在UV分量排列上:
| 格式特性 | NV21 (YUV420SP) | NV12 (YUV420SP) |
|---|---|---|
| Y分量排列 | 完整平面存储 | 完整平面存储 |
| UV排列 | VU交替 | UV交替 |
| 内存布局 | YYYY...VUVU... | YYYY...UVUV... |
转换代码的关键优化点:
public static byte[] nv21ToNv12(byte[] nv21, int width, int height) { byte[] nv12 = new byte[nv21.length]; int frameSize = width * height; // Y分量直接复制 System.arraycopy(nv21, 0, nv12, 0, frameSize); // UV分量交错处理 for (int i = frameSize; i < nv21.length; i += 2) { nv12[i] = nv21[i + 1]; // U分量 nv12[i + 1] = nv21[i]; // V分量 } return nv12; }2.3 画面旋转处理方案
摄像头传感器方向与屏幕方向不一致会导致画面旋转,常见解决方案对比:
Camera.setDisplayOrientation()
- 仅影响预览显示
- 不改变输出数据方向
数据层面旋转
- 需要手动处理YUV数据
- 性能开销较大但效果彻底
public static byte[] rotateNV21(byte[] input, int width, int height, int rotation) { byte[] output = new byte[input.length]; boolean swap = (rotation == 90 || rotation == 270); boolean flip = (rotation == 90 || rotation == 180); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // 计算旋转后的坐标 int xo = x, yo = y; int w = width, h = height; if (swap) { xo = y; yo = x; w = height; h = width; } if (flip) { xo = w - xo - 1; yo = h - yo - 1; } // 处理Y分量 output[yo * w + xo] = input[y * width + x]; // 处理UV分量... } } return output; }3. MediaCodec编码实战
3.1 H264编码器配置要点
创建MediaCodec实例时需要特别注意的参数组合:
MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, width, height); format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar); MediaCodec codec = MediaCodec.createEncoderByType(MIME_TYPE); codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); codec.start();关键参数说明:
- BIT_RATE:建议值为分辨率宽×高×3~5
- FRAME_RATE:需与摄像头输出帧率一致
- I_FRAME_INTERVAL:关键帧间隔(秒)
3.2 编码流程详解
MediaCodec的典型编码流程可分为五个阶段:
- 获取输入缓冲区:dequeueInputBuffer
- 填充YUV数据:将处理后的NV12数据放入缓冲区
- 提交编码任务:queueInputBuffer
- 获取编码输出:dequeueOutputBuffer
- 处理编码数据:获取H.264 NAL单元
// 输入处理 int inputBufferIndex = codec.dequeueInputBuffer(TIMEOUT_US); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex); inputBuffer.clear(); inputBuffer.put(yuvData); codec.queueInputBuffer(inputBufferIndex, 0, yuvData.length, presentationTimeUs, 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); // 处理h264Data... codec.releaseOutputBuffer(outputBufferIndex, false); outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US); }3.3 编码性能优化技巧
通过实践总结的三大优化方向:
缓冲区复用:避免频繁内存分配
private ByteBuffer[] inputBuffers; private ByteBuffer[] outputBuffers; // 初始化时获取 inputBuffers = codec.getInputBuffers(); outputBuffers = codec.getOutputBuffers();异步处理模式:使用Callback接口
codec.setCallback(new MediaCodec.Callback() { @Override public void onInputBufferAvailable(MediaCodec mc, int index) { // 处理输入缓冲区... } @Override public void onOutputBufferAvailable(MediaCodec mc, int index, MediaCodec.BufferInfo info) { // 处理输出数据... } });动态码率调整:根据网络状况调整
Bundle params = new Bundle(); params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, newBitrate); codec.setParameters(params);
4. 完整实现与调试技巧
4.1 工程结构设计建议
合理的类职责划分可以大幅降低维护成本:
├── camera │ ├── CameraManager.java # 摄像头生命周期管理 │ └── CameraConfig.java # 分辨率等配置 ├── encoder │ ├── H264Encoder.java # 编码核心逻辑 │ └── FrameProcessor.java # YUV处理 └── output ├── FileWriter.java # 文件存储 └── NetworkSender.java # 网络传输4.2 常见问题排查指南
开发过程中遇到的典型问题及解决方案:
问题1:画面绿屏或颜色异常
- 检查YUV格式转换是否正确
- 确认MediaCodec的COLOR_FORMAT配置
- 验证数据旋转是否影响UV分量
问题2:编码延迟高
- 降低预览分辨率(如1080p→720p)
- 调整GOP结构(减小I帧间隔)
- 使用硬件加速编码器
问题3:视频文件无法播放
- 确保写入正确的SPS/PPS头
- 检查H264 Annex B格式
- 验证时间戳是否单调递增
4.3 调试工具推荐
ADB命令实时监控
adb shell dumpsys media.codec # 查看编解码器状态 adb logcat -s Camera2Client # 过滤摄像头日志视频分析工具
- FFmpeg:验证H264流完整性
ffmpeg -i input.h264 -vf "split=2[a][b],[a]field=top[b]field=bottom, hstack" -f null -- CodecInfo:查看设备支持的编码格式
性能分析工具
- Android Profiler:检测内存泄漏
- Systrace:分析编码流水线瓶颈
5. 进阶方向与扩展思考
当基础功能实现后,可以考虑以下优化方向:
- 动态分辨率适配:根据网络状况调整编码参数
- 多路编码:同时生成不同质量的视频流
- 硬件加速:充分利用MediaCodec的硬件能力
- 低延迟优化:减少端到端延迟至200ms内
一个典型的动态参数调整实现:
public void adjustQuality(QualityLevel level) { switch (level) { case HIGH: setEncoderParams(1920, 1080, 4000000); break; case MEDIUM: setEncoderParams(1280, 720, 2500000); break; case LOW: setEncoderParams(854, 480, 1000000); break; } } private void setEncoderParams(int width, int height, int bitrate) { Bundle params = new Bundle(); params.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitrate); mediaCodec.setParameters(params); // 需要重新配置YUV处理逻辑 frameProcessor.updateFrameSize(width, height); }在智能家居项目的实际应用中,我们发现夜间低光照条件下的视频质量明显下降。通过分析YUV直方图,最终采用动态调整对比度和降噪参数的方案,使夜间可辨识度提升40%。这种基于具体场景的优化,才是音视频开发真正的价值所在。