You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android MediaCodec转存MP4优化:直接用原始流写入失败求助

直接复用HEVC原始流写入MediaMuxer的正确实现

你的问题核心是直接复用编码后的HEVC流写入MP4时,没有正确提供Muxer所需的关键元数据和帧信息,下面是具体的错误分析和修复方案:

你现有代码的核心错误

  1. MediaFormat配置完全错误
    你手动创建的MediaFormat缺少HEVC流必须的SPS/PPS(序列参数集/图像参数集),这是MP4容器识别视频轨道的核心元数据,没有这些Muxer根本无法正确初始化轨道。而且直接复用编码流时,不需要设置COLOR_FORMAT、BIT_RATE这些编码参数,应该用从原始流中解析出的格式信息。

  2. 时间戳格式错误
    presentationTimeUs要求的是微秒,但你传入的System.currentTimeMillis()是毫秒,数值差了1000倍;更关键的是,应该使用视频流本身携带的时间戳,而不是本地系统时间,否则会导致视频播放时速度异常。

  3. 帧数据与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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 18:07:03