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

Android API22(Lollipop)模拟器MediaMuxer音视频合成失败求助

解决API22上MediaMuxer时间戳倒退的问题

嘿,我看了你遇到的麻烦——你写的这段Java音视频合成代码在API25、27模拟器和API28真机上都能正常运行,可到了API22模拟器就卡壳了,日志里抛出了这个错误:

02-19 17:15:11.326 5012-5225/packagename E/MPEG4Writer: timestampUs 40000 < lastTimestampUs 120000 for Video track

这个错误的核心是MediaMuxer要求写入的媒体样本时间戳必须严格递增,一旦出现后一个样本时间戳比前一个小,就会触发报错。我帮你梳理了代码里的几个关键问题,应该就是这些导致了低版本的兼容性问题:


代码里的关键问题点

1. 强制覆盖所有视频帧的flags

你在写入视频帧的时候,把所有帧的flags都强制设为关键帧(BUFFER_FLAG_KEY_FRAME/BUFFER_FLAG_SYNC_FRAME):

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
} else {
    videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
}

这完全错误!原视频里的非关键帧(比如P帧、B帧)的时间戳是依赖于前面的关键帧的,你强制把所有帧都标记为关键帧,会让MediaMuxer对时间戳的解析逻辑混乱,尤其是在API22这种低版本上,很容易出现时间戳倒退的判断错误。正确的做法是保留MediaExtractor读取到的原始flags。

2. 分两次循环写入音视频(先写全视频,再写全音频)

你先把所有视频帧写完,再去写音频帧,这种方式不符合MediaMuxer的最佳实践。低版本的MediaMuxer对这种非交替写入的场景兼容性极差,它期望的是按照时间戳顺序交替写入音视频样本,这样才能正确维护合成文件的时间线。

3. 音频循环里的笔误:修改了videoBufferInfo的flags

在处理音频帧的时候,你写错了操作对象:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME; // 这里应该是audioBufferInfo!
} else {
    videoBufferInfo.flags = MediaCodec.BUFFER_FLAG_SYNC_FRAME;
}

这属于低级错误,虽然不一定直接导致时间戳问题,但会导致音频帧的flags错误,影响合成后的文件播放。

4. EOS判断逻辑错误

在音频循环里,你用了videoBufferInfo.size < 0作为判断条件,但此时视频循环已经结束,videoBufferInfo.size已经被设为0,这会导致音频循环刚启动就误判为EOS,根本没写入多少音频数据。


修复后的完整代码

我把这些问题都修复了,改成了按时间戳交替写入音视频的逻辑,同时保留了原始样本的flags,修正了所有笔误:

public void muxer(File videoFile, File audioFile, long dowloadId) {
    String outputFile = "";
    String TAG = "MURGE";
    publishProgress(0, 0, dowloadId, 0, "Parsing");
    try {
        File finalDir = new File(DataStore.getInstance(this).getPathDownload() + "/Final");
        if (!finalDir.exists()) {
            finalDir.mkdirs();
        }
        File output = new File(finalDir, filename);
        output.createNewFile();
        outputFile = output.getAbsolutePath();

        Log.e(TAG, "Video File Present: " + videoFile.isFile());
        Log.e(TAG, "Audio File Present: " + audioFile.isFile());

        MediaExtractor videoExtractor = new MediaExtractor();
        videoExtractor.setDataSource(videoFile.getPath());
        MediaExtractor audioExtractor = new MediaExtractor();
        audioExtractor.setDataSource(audioFile.getPath());

        Log.d(TAG, "Video Extractor Track Count " + videoExtractor.getTrackCount());
        Log.d(TAG, "Audio Extractor Track Count " + audioExtractor.getTrackCount());

        MediaMuxer muxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);

        // 选择音视频轨道并添加到Muxer
        videoExtractor.selectTrack(0);
        MediaFormat videoFormat = videoExtractor.getTrackFormat(0);
        int videoTrack = muxer.addTrack(videoFormat);

        audioExtractor.selectTrack(0);
        MediaFormat audioFormat = audioExtractor.getTrackFormat(0);
        int audioTrack = muxer.addTrack(audioFormat);

        Log.d(TAG, "Video Format " + videoFormat.toString());
        Log.d(TAG, "Audio Format " + audioFormat.toString());

        // 初始化缓冲区和BufferInfo
        int sampleSize = 1024 * 1024;
        ByteBuffer videoBuf = ByteBuffer.allocate(sampleSize);
        ByteBuffer audioBuf = ByteBuffer.allocate(sampleSize);
        MediaCodec.BufferInfo videoBufferInfo = new MediaCodec.BufferInfo();
        MediaCodec.BufferInfo audioBufferInfo = new MediaCodec.BufferInfo();

        // 重置Extractor到起始位置(从第一个关键帧开始)
        videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
        audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);

        muxer.start();

        long videoTime = videoExtractor.getSampleTime();
        long audioTime = audioExtractor.getSampleTime();
        int frameCount = 0;

        // 交替写入音视频样本,按时间戳顺序
        while (videoTime != -1 || audioTime != -1) {
            // 选择时间戳更早的样本写入
            if (videoTime != -1 && (audioTime == -1 || videoTime <= audioTime)) {
                // 写入视频帧
                videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, 0);
                videoBufferInfo.offset = 0;
                videoBufferInfo.presentationTimeUs = videoTime;
                // 保留原始flags,不要强制修改
                videoBufferInfo.flags = videoExtractor.getSampleFlags();

                muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo);
                videoExtractor.advance();
                videoTime = videoExtractor.getSampleTime();

                frameCount++;
                Log.d(TAG, "Video Frame (" + frameCount + ") | TimeUs: " + videoBufferInfo.presentationTimeUs 
                        + " | Flags: " + videoBufferInfo.flags + " | Size(KB): " + videoBufferInfo.size / 1024);
            } else if (audioTime != -1) {
                // 写入音频帧
                audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, 0);
                audioBufferInfo.offset = 0;
                audioBufferInfo.presentationTimeUs = audioTime;
                // 保留原始flags
                audioBufferInfo.flags = audioExtractor.getSampleFlags();

                muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo);
                audioExtractor.advance();
                audioTime = audioExtractor.getSampleTime();

                frameCount++;
                Log.d(TAG, "Audio Frame (" + frameCount + ") | TimeUs: " + audioBufferInfo.presentationTimeUs 
                        + " | Flags: " + audioBufferInfo.flags + " | Size(KB): " + audioBufferInfo.size / 1024);
            }

            // 更新进度(可选,根据实际情况调整)
            int totalFrames = getTotalFrames(videoExtractor, audioExtractor);
            int progress = totalFrames > 0 ? (int) (((float)frameCount / totalFrames) * 100) : 0;
            publishProgress(progress, 0, 0, 0, "Muxing");
        }

        // 收尾工作:释放资源
        muxer.stop();
        muxer.release();
        videoExtractor.release();
        audioExtractor.release();

        videoFile.delete();
        audioFile.delete();

        Log.e(TAG, "Muxing completed successfully! Total frames processed: " + frameCount);
    } catch (IOException e) {
        e.printStackTrace();
        Log.d(TAG, "Mixer Error 1 " + e.getMessage());
    } catch (Exception e) {
        e.printStackTrace();
        Log.d(TAG, "Mixer Error 2 " + e.getMessage());
    }
}

// 辅助方法:计算音视频总帧数(用于进度计算)
private int getTotalFrames(MediaExtractor videoExtractor, MediaExtractor audioExtractor) {
    int videoFrames = 0;
    while (videoExtractor.advance()) {
        videoFrames++;
    }
    videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);

    int audioFrames = 0;
    while (audioExtractor.advance()) {
        audioFrames++;
    }
    audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);

    return videoFrames + audioFrames;
}

额外说明

  1. 保留原始flags:现在代码里用videoExtractor.getSampleFlags()和audioExtractor.getSampleFlags()获取原始样本的flags,不会再强制修改,这样MediaMuxer能正确识别关键帧和非关键帧,时间戳逻辑就正常了。
  2. 交替写入逻辑:通过比较音视频当前样本的时间戳,每次写入时间更早的那个样本,保证Muxer接收到的样本时间戳是严格递增的,这是MediaMuxer的标准用法,兼容性最好。
  3. 修正了Extractor的seek方式:用SEEK_TO_PREVIOUS_SYNC代替SEEK_TO_CLOSEST_SYNC,确保从第一个关键帧开始读取,避免起始时间的偏移。
  4. 添加了资源释放:之前的代码没释放MediaExtractor,容易导致资源泄漏,现在加上了完整的资源释放逻辑。

你可以试试这段修复后的代码,应该能解决API22上的时间戳问题。

内容的提问来源于stack exchange,提问作者Ashvin solanki

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 03:59:49