Android API22(Lollipop)模拟器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; }
额外说明
- 保留原始flags:现在代码里用
videoExtractor.getSampleFlags()和audioExtractor.getSampleFlags()获取原始样本的flags,不会再强制修改,这样MediaMuxer能正确识别关键帧和非关键帧,时间戳逻辑就正常了。 - 交替写入逻辑:通过比较音视频当前样本的时间戳,每次写入时间更早的那个样本,保证Muxer接收到的样本时间戳是严格递增的,这是MediaMuxer的标准用法,兼容性最好。
- 修正了Extractor的seek方式:用
SEEK_TO_PREVIOUS_SYNC代替SEEK_TO_CLOSEST_SYNC,确保从第一个关键帧开始读取,避免起始时间的偏移。 - 添加了资源释放:之前的代码没释放MediaExtractor,容易导致资源泄漏,现在加上了完整的资源释放逻辑。
你可以试试这段修复后的代码,应该能解决API22上的时间戳问题。
内容的提问来源于stack exchange,提问作者Ashvin solanki

