Android MediaCodec转存MP4优化:直接用原始流写入失败求助
直接复用HEVC原始流写入MediaMuxer的正确实现
你的问题核心是直接复用编码后的HEVC流写入MP4时,没有正确提供Muxer所需的关键元数据和帧信息,下面是具体的错误分析和修复方案:
你现有代码的核心错误
MediaFormat配置完全错误
你手动创建的MediaFormat缺少HEVC流必须的SPS/PPS(序列参数集/图像参数集),这是MP4容器识别视频轨道的核心元数据,没有这些Muxer根本无法正确初始化轨道。而且直接复用编码流时,不需要设置COLOR_FORMAT、BIT_RATE这些编码参数,应该用从原始流中解析出的格式信息。时间戳格式错误
presentationTimeUs要求的是微秒,但你传入的System.currentTimeMillis()是毫秒,数值差了1000倍;更关键的是,应该使用视频流本身携带的时间戳,而不是本地系统时间,否则会导致视频播放时速度异常。帧数据与flags不匹配
你设置的frameType没有明确对应MediaCodec的flag常量,比如I帧需要设置BUFFER_FLAG_SYNC_FRAME,否则Muxer无法识别关键帧,导致视频无法正常播放。
正确实现步骤与代码
步骤1:解析HEVC流中的NAL单元
WebSocket传来的HEVC流一般是Annex-B格式,需要先分割出独立的NAL单元(SPS、PPS、I帧、P帧等),识别出SPS和PPS用于构建MediaFormat。
// 简单的NAL单元分割逻辑(处理Annex-B格式的起始码) private List<byte[]> splitNalUnits(byte[] rawData) { List<byte[]> nalUnits = new ArrayList<>(); int startIndex = 0; int dataLength = rawData.length; while (startIndex < dataLength) { // 寻找起始码:0x000001 或 0x00000001 int nextStartIndex = findStartCode(rawData, startIndex + 3); if (nextStartIndex == -1) { nextStartIndex = dataLength; } int nalLength = nextStartIndex - startIndex; if (nalLength > 0) { byte[] nalUnit = Arrays.copyOfRange(rawData, startIndex, nextStartIndex); nalUnits.add(nalUnit); } startIndex = nextStartIndex; } return nalUnits; } private int findStartCode(byte[] data, int startPos) { int length = data.length; for (int i = startPos; i < length - 2; i++) { if (data[i] == 0 && data[i+1] == 0) { if (data[i+2] == 1) { return i; } else if (i+3 < length && data[i+2] == 0 && data[i+3] == 1) { return i; } } } return -1; }
步骤2:构建正确的MediaFormat并初始化Muxer
从解析出的NAL单元中提取SPS和PPS,用它们来构建MediaFormat,这是Muxer能正确工作的关键:
private MediaFormat createHevcFormat(byte[] sps, byte[] pps, int width, int height) { MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, width, height); // 设置SPS和PPS,对应csd-0和csd-1 format.setByteBuffer("csd-0", ByteBuffer.wrap(sps)); format.setByteBuffer("csd-1", ByteBuffer.wrap(pps)); // 可选:如果流中有明确的帧率,可以设置,否则Muxer会自动推断 format.setInteger(MediaFormat.KEY_FRAME_RATE, 30); return format; }
步骤3:按顺序写入NAL单元到Muxer
确保先写入SPS和PPS,再写入普通帧,同时使用正确的时间戳和flags:
private boolean muxerStarted = false; private int trackIndex = -1; private MediaMuxer mediaMuxer; private long presentationTimeUs = 0; private final long frameIntervalUs = 1000000 / 30; // 假设30fps,根据实际帧率调整 private byte[] spsData; private byte[] ppsData; private int width; private int height; public void onWebSocketDataReceived(byte[] rawData) { List<byte[]> nalUnits = splitNalUnits(rawData); for (byte[] nalUnit : nalUnits) { if (nalUnit.length < 5) continue; // 识别NAL单元类型(HEVC的NAL类型在第5个字节的低5位) int nalType = nalUnit[4] & 0x1F; if (!muxerStarted) { if (nalType == 33) { // SPS spsData = nalUnit; } else if (nalType == 34) { // PPS ppsData = nalUnit; } // 同时拿到SPS和PPS后初始化Muxer if (spsData != null && ppsData != null) { MediaFormat format = createHevcFormat(spsData, ppsData, width, height); trackIndex = mediaMuxer.addTrack(format); mediaMuxer.start(); muxerStarted = true; // 先写入SPS和PPS writeNalUnitToMuxer(spsData, 0, MediaCodec.BUFFER_FLAG_CODEC_CONFIG); writeNalUnitToMuxer(ppsData, 0, MediaCodec.BUFFER_FLAG_CODEC_CONFIG); } } else { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); bufferInfo.size = nalUnit.length; bufferInfo.offset = 0; bufferInfo.presentationTimeUs = presentationTimeUs; // 设置帧类型flag if (nalType == 19 || nalType == 20 || nalType == 21) { // I帧相关类型 bufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME; } else { bufferInfo.flags = 0; } // 写入帧数据 mediaMuxer.writeSampleData(trackIndex, ByteBuffer.wrap(nalUnit), bufferInfo); // 更新时间戳(如果流本身没有时间戳,按帧率递增) presentationTimeUs += frameIntervalUs; } } } private void writeNalUnitToMuxer(byte[] nalUnit, long timeUs, int flags) { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); bufferInfo.size = nalUnit.length; bufferInfo.offset = 0; bufferInfo.presentationTimeUs = timeUs; bufferInfo.flags = flags; mediaMuxer.writeSampleData(trackIndex, ByteBuffer.wrap(nalUnit), bufferInfo); }
关键注意事项
- 必须确保先写入SPS和PPS,否则Muxer无法正确识别视频轨道;
- 时间戳必须是微秒级,优先使用流本身携带的时间戳(如果WebSocket传输的流包含的话),如果没有再用帧率递增的方式生成;
- NAL单元必须是完整的,不能有截断,否则写入Muxer会失败;
- 结束时要正确调用
mediaMuxer.stop()和mediaMuxer.release(),否则MP4文件会损坏。
内容的提问来源于stack exchange,提问作者shao hua
相关产品推荐
相关产品推荐

