Android MediaCodec编码实战:从Camera预览到H.265 VPS/SPS/PPS帧提取的完整流程

Android MediaCodec编码实战:从Camera预览到H.265 VPS/SPS/PPS帧提取的完整流程 Android MediaCodec硬编码全流程从Camera采集到H.265参数帧解析实战在移动端视频处理领域高效稳定的编码流程一直是开发者面临的挑战。本文将深入探讨如何利用Android平台提供的MediaCodec API构建从摄像头采集到H.265编码的完整解决方案特别聚焦于关键参数帧VPS/SPS/PPS的提取技巧。不同于基础教程我们将通过工业级实践代码揭示那些官方文档未曾提及的实现细节和性能优化点。1. 摄像头配置与数据采集优化现代Android设备通常配备多摄像头模组正确初始化摄像头是视频处理流水线的第一步。我们需要关注几个关键参数// 摄像头选择策略优先后置主摄 int cameraId Camera.CameraInfo.CAMERA_FACING_BACK; Camera camera Camera.open(cameraId); // 最佳预览尺寸选择算法 ListCamera.Size supportedSizes camera.getParameters().getSupportedPreviewSizes(); Camera.Size optimalSize getOptimalPreviewSize(supportedSizes, targetWidth, targetHeight); // 配置YUV420SP格式兼容性最佳 Camera.Parameters params camera.getParameters(); params.setPreviewFormat(ImageFormat.NV21); params.setPreviewSize(optimalSize.width, optimalSize.height); camera.setParameters(params);注意不同设备对YUV格式的支持存在差异NV21格式具有最广泛的兼容性但部分新型设备可能支持更高效的格式。预览回调的数据处理需要特别注意线程模型private final BlockingQueuebyte[] frameQueue new LinkedBlockingQueue(5); class PreviewCallback implements Camera.PreviewCallback { Override public void onPreviewFrame(byte[] data, Camera camera) { if (!frameQueue.offer(data)) { // 队列满时丢弃最旧帧 frameQueue.poll(); frameQueue.offer(data); } camera.addCallbackBuffer(data); } }这种设计解决了两个关键问题防止回调线程阻塞导致帧率下降避免内存抖动引起的GC停顿2. MediaCodec编码器深度配置编码器初始化是流程中最复杂的环节之一需要根据目标编码格式H.264/H.265进行差异化配置2.1 编码格式选择与参数优化MediaFormat createVideoFormat(String mimeType, int width, int height) { MediaFormat format MediaFormat.createVideoFormat(mimeType, width, height); // 通用参数 format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar); format.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 5); // 5bps/pixel format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2); // 2秒一个关键帧 // H.265特有配置 if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mimeType)) { format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.HEVCProfileMain); format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel4); } // H.264特有配置 else { format.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); format.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel42); } return format; }2.2 编码器选择策略不同设备芯片组对编码器的实现差异很大推荐采用以下选择策略选择标准优先级备注硬件加速最高检查isHardwareAccelerated()分辨率支持高验证getCapabilitiesForType()色域格式中优先YUV420SemiPlanar编码延迟低实测性能对比MediaCodecInfo selectBestCodec(String mimeType) { MediaCodecList list new MediaCodecList(MediaCodecList.REGULAR_CODECS); for (MediaCodecInfo info : list.getCodecInfos()) { if (!info.isEncoder()) continue; String[] types info.getSupportedTypes(); for (String type : types) { if (type.equalsIgnoreCase(mimeType)) { MediaCodecInfo.CodecCapabilities caps info.getCapabilitiesForType(mimeType); if (caps.colorFormats ! null caps.colorFormats.length 0) { return info; } } } } return null; }3. 异步编码流程与性能优化现代Android开发推荐使用异步模式处理MediaCodec这需要精心设计线程模型private class EncoderCallback extends MediaCodec.Callback { private final OutputFrameProcessor frameProcessor; Override public void onInputBufferAvailable(MediaCodec mc, int index) { ByteBuffer buffer mc.getInputBuffer(index); byte[] frameData frameQueue.poll(100, TimeUnit.MILLISECONDS); if (frameData ! null) { // 转换NV21到编码器需要的格式 byte[] converted convertToYUV420SP(frameData); buffer.put(converted); mc.queueInputBuffer(index, 0, converted.length, System.nanoTime() / 1000, 0); } else { // 提交空帧保持编码器活跃 mc.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); } } Override public void onOutputBufferAvailable(MediaCodec mc, int index, MediaCodec.BufferInfo info) { ByteBuffer buffer mc.getOutputBuffer(index); processEncodedFrame(buffer, info); mc.releaseOutputBuffer(index, false); } // ... 其他回调方法 }关键性能优化点输入缓冲区管理使用双缓冲队列避免内存拷贝时间戳处理使用纳秒级时间戳保证音画同步异常恢复检测到错误时自动重启编码器4. H.265参数帧解析实战H.265HEVC相比H.264引入了VPSVideo Parameter Set使得参数帧解析更为复杂。我们提供两种可靠的提取方法方法一从首帧数据提取void parseParameterSetsFromFirstFrame(ByteBuffer buffer, BufferInfo info) { byte[] data new byte[info.size]; buffer.get(data); // H.265起始码为0x000001 if ((data[0] 0) (data[1] 0) (data[2] 1)) { int type (data[4] 1) 0x3F; switch (type) { case 32: // VPS extractVPS(data); break; case 33: // SPS extractSPS(data); break; case 34: // PPS extractPPS(data); break; } } }方法二从CSD格式数据提取void parseParameterSetsFromCSD(MediaFormat format) { if (format.containsKey(csd-0)) { ByteBuffer csd0 format.getByteBuffer(csd-0); byte[] data csd0.array(); // H.265的VPS/SPS/PPS都存储在csd-0中 int[] offsets findNALUOffsets(data); for (int offset : offsets) { int type (data[offset 4] 1) 0x3F; // 类型判断与处理... } } }两种方法对比特性首帧提取法CSD提取法可靠性高取决于设备实现时机编码开始后格式变更时数据完整性可能不完整通常完整适用场景实时流文件封装5. 工业级异常处理方案在实际项目中编码流程可能遇到各种异常情况需要建立健壮的错误恢复机制private void handleEncoderError(MediaCodec.CodecException e) { if (e.isTransient()) { // 临时性错误尝试恢复 resetEncoder(); } else if (e.isRecoverable()) { // 可恢复错误重新配置 releaseEncoder(); initEncoder(); } else { // 不可恢复错误 notifyFatalError(e); } } private void resetEncoder() { encoder.stop(); encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); encoder.start(); }常见错误代码处理错误代码处理策略重试次数MEDIA_ERROR_IO检查存储权限1MEDIA_ERROR_MALFORMED重置编码器3MEDIA_ERROR_UNSUPPORTED降级编码配置0MEDIA_ERROR_TIMED_OUT增加缓冲区26. 性能监控与调优工具为了确保编码流程的稳定性需要实现实时监控class EncoderMonitor { private long lastFrameTime; private int frameCount; private double averageEncodeTime; void recordFrame(MediaCodec.BufferInfo info) { long current System.nanoTime(); if (lastFrameTime 0) { double duration (current - lastFrameTime) / 1e6; averageEncodeTime 0.9 * averageEncodeTime 0.1 * duration; } lastFrameTime current; frameCount; if (frameCount % 30 0) { logMetrics(); } } void logMetrics() { Log.d(EncoderStats, String.format( AvgEncodeTime%.2fms | Frames%d, averageEncodeTime, frameCount)); } }关键性能指标阈值参考指标警告阈值错误阈值应对措施单帧编码时间33ms50ms降低分辨率内存占用80MB120MB检查泄漏队列延迟5帧10帧优化算法温度升高45°C50°C限制帧率