Android Studio合并MP4视频后视频损坏但音频正常的问题求解答
MP4合并功能实现问题排查与修正
问题现象
- 尝试使用Mp4parser库实现MP4合并,输出文件存在视频损坏问题
- 所有合并方案下,音频输出质量始终保持正常
- 基于MediaExtractor、MediaMuxer自行实现的合并逻辑,生成的视频存在严重失真、损坏问题
核心故障原因
- 未校验所有待合并视频的编码参数一致性
代码仅从首个解析成功的视频、音频轨道提取MediaFormat传入Muxer,未校验所有待合并文件的分辨率、视频编码Profile/Level、码率、音频采样率、声道数是否完全匹配。任意一段文件参数与首段不一致时,写入的视频数据会因为格式不兼容出现解码花屏、失真。 - 未做拼接点关键帧对齐
MediaMuxer写入规则要求,每个视频轨道的拼接位置必须从关键帧起始。代码未在切换文件时跳过首个非关键帧,直接从读取到的第一个帧开始写入,会导致拼接点之后的画面因为缺少参考帧出现撕裂、失真。 - 读写缓冲区大小固定不足
代码固定设置缓冲区大小为1MB,高码率1080P/4K视频的单帧数据很容易超过这个阈值,readSampleData会直接截断帧内容,造成视频帧损坏。应该从轨道的MediaFormat中读取MAX_INPUT_SIZE参数动态分配缓冲区大小。 - 轨道选择逻辑存在异常风险
当selectTrack方法返回-1(未找到对应类型轨道)时,代码没有做异常拦截,仍然执行selectTrack(-1)操作,会导致后续读取的采样数据轨道索引和预期不匹配,出现视频/音频流交叉写入的损坏问题。 - 时间戳计算逻辑错误
代码每合并完一段视频就硬加10ms的固定时间偏移,会累计造成整体播放时长偏差,引发音画不同步。计算偏移量时应该取当前文件所有轨道的最大结束PTS,不要硬编码间隔值。
修正后实现代码
VideoCombiner类
public class VideoCombiner { private static final String TAG = "VideoCombiner"; private static final boolean VERBOSE = true; // 缓冲区默认大小,优先从媒体格式读取实际需要的尺寸 private static final int DEFAULT_BUFF_SIZE = 1048576; private List<String> mVideoList; private String mDestPath; private MediaMuxer mMuxer; private ByteBuffer mReadBuf; private int mOutAudioTrackIndex; private int mOutVideoTrackIndex; private MediaFormat mAudioFormat; private MediaFormat mVideoFormat; private VideoCombineListener mCombineListener; public interface VideoCombineListener { /** * 合并任务启动 */ void onCombineStart(); /** * 合并进度回调 * @param current 当前已合并的视频序号 * @param sum 待合并视频总数 */ void onCombineProcessing(int current, int sum); /** * 合并任务结束 * @param success 合并是否成功 */ void onCombineFinished(boolean success); } public VideoCombiner(List<String> videoList, String destPath, VideoCombineListener listener) { mVideoList = videoList; mDestPath = destPath; mCombineListener = listener; } /** * 执行视频合并 */ @SuppressLint("WrongConstant") public void combineVideo() { boolean hasAudioFormat = false; boolean hasVideoFormat = false; Iterator<String> videoIterator = mVideoList.iterator(); if (mCombineListener != null) { mCombineListener.onCombineStart(); } // 遍历所有文件,提取首个可用的视频、音频轨道格式 while (videoIterator.hasNext()) { String videoPath = videoIterator.next(); MediaExtractor extractor = new MediaExtractor(); try { extractor.setDataSource(videoPath); } catch (Exception ex) { ex.printStackTrace(); } int trackIndex; if (!hasVideoFormat) { trackIndex = selectTrack(extractor, "video/"); if(trackIndex < 0) { Log.e(TAG, "未在文件中找到视频轨道: " + videoPath); } else { extractor.selectTrack(trackIndex); mVideoFormat = extractor.getTrackFormat(trackIndex); hasVideoFormat = true; // 动态读取最大缓冲区大小 int maxBuffer = mVideoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE); mReadBuf = ByteBuffer.allocate(maxBuffer > 0 ? maxBuffer : DEFAULT_BUFF_SIZE); } } if (!hasAudioFormat) { trackIndex = selectTrack(extractor, "audio/"); if(trackIndex < 0) { Log.e(TAG, "未在文件中找到音频轨道: " + videoPath); } else { extractor.selectTrack(trackIndex); mAudioFormat = extractor.getTrackFormat(trackIndex); hasAudioFormat = true; } } extractor.release(); if (hasVideoFormat && hasAudioFormat) { break; } } try { mMuxer = new MediaMuxer(mDestPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); } catch (IOException e) { e.printStackTrace(); } if (hasVideoFormat) { mOutVideoTrackIndex = mMuxer.addTrack(mVideoFormat); } if (hasAudioFormat) { mOutAudioTrackIndex = mMuxer.addTrack(mAudioFormat); } mMuxer.start(); // 逐文件读取采样数据,通过MediaMuxer写入输出文件 long ptsOffset = 0L; Iterator<String> trackIndex = mVideoList.iterator(); int currentVideo = 0; boolean combineResult = true; while (trackIndex.hasNext()) { currentVideo++; if (mCombineListener != null) { mCombineListener.onCombineProcessing(currentVideo, mVideoList.size()); } String videoPath = trackIndex.next(); boolean hasVideo = true; boolean hasAudio = true; // 初始化视频轨道提取器 MediaExtractor videoExtractor = new MediaExtractor(); try { videoExtractor.setDataSource(videoPath); } catch (Exception e) { e.printStackTrace(); } int inVideoTrackIndex = selectTrack(videoExtractor, "video/"); if(inVideoTrackIndex < 0) { hasVideo = false; } else { videoExtractor.selectTrack(inVideoTrackIndex); // 跳过拼接点开头的非关键帧,确保从关键帧开始写入 while (hasVideo) { int flags = videoExtractor.getSampleFlags(); if ((flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { break; } if (videoExtractor.readSampleData(mReadBuf, 0) < 0) { hasVideo = false; break; } videoExtractor.advance(); } } // 初始化音频轨道提取器 MediaExtractor audioExtractor = new MediaExtractor(); try { audioExtractor.setDataSource(videoPath); } catch (Exception e) { e.printStackTrace(); } int inAudioTrackIndex = selectTrack(audioExtractor, "audio/"); if (inAudioTrackIndex < 0) { hasAudio = false; } else { audioExtractor.selectTrack(inAudioTrackIndex); } // 无有效轨道时终止合并 if (!hasVideo && !hasAudio) { combineResult = false; videoExtractor.release(); audioExtractor.release(); break; } boolean bMediaDone = false; long presentationTimeUs = 0L; long audioPts = 0L; long videoPts = 0L; while (!bMediaDone) { if(!hasVideo && !hasAudio) { break; } int outTrackIndex; MediaExtractor extractor; int currentTrackIndex; // 按时间戳同步音视频读取顺序 if ((!hasVideo || audioPts - videoPts <= 50000L) && hasAudio) { currentTrackIndex = inAudioTrackIndex; outTrackIndex = mOutAudioTrackIndex; extractor = audioExtractor; } else { currentTrackIndex = inVideoTrackIndex; outTrackIndex = mOutVideoTrackIndex; extractor = videoExtractor; } if (VERBOSE) { Log.d(TAG, "当前读取轨道: " + currentTrackIndex + ", 输出轨道: " + outTrackIndex); } mReadBuf.rewind(); int frameSize = extractor.readSampleData(mReadBuf, 0); if (frameSize < 0) { if (currentTrackIndex == inVideoTrackIndex) { hasVideo = false; } else if (currentTrackIndex == inAudioTrackIndex) { hasAudio = false; } } else { if (extractor.getSampleTrackIndex() != currentTrackIndex) { Log.e(TAG, "读取到的采样轨道为 " + extractor.getSampleTrackIndex() + ", 预期轨道为 " + currentTrackIndex); } presentationTimeUs = extractor.getSampleTime(); if (currentTrackIndex == inVideoTrackIndex) { videoPts = presentationTimeUs; } else { audioPts = presentationTimeUs; } BufferInfo info = new BufferInfo(); info.offset = 0; info.size = frameSize; info.presentationTimeUs = ptsOffset + presentationTimeUs; if ((extractor.getSampleFlags() & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) { info.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME; } mReadBuf.rewind(); mMuxer.writeSampleData(outTrackIndex, mReadBuf, info); extractor.advance(); } } // 计算下一段文件的时间偏移量 ptsOffset += Math.max(videoPts, audioPts); if (VERBOSE) { Log.d(TAG, "完成单个文件合并,当前时间偏移量 " + ptsOffset); } videoExtractor.release(); audioExtractor.release(); } // 释放Muxer资源 if (mMuxer != null) { try { mMuxer.stop(); mMuxer.release(); } catch (Exception e) { Log.e(TAG, "Muxer关闭失败,无有效数据写入"); } finally { mMuxer = null; } } if (VERBOSE) { Log.d(TAG, "视频合并流程结束"); } if (mCombineListener != null) { mCombineListener.onCombineFinished(combineResult); } } /** * 选择对应类型的媒体轨道 * @param extractor 媒体提取器 * @param mimePrefix 轨道MIME类型前缀 * @return 轨道索引,未找到返回-1 */ private int selectTrack(MediaExtractor extractor, String mimePrefix) { int numTracks = extractor.getTrackCount(); for(int i = 0; i < numTracks; ++i) { MediaFormat format = extractor.getTrackFormat(i); String mime = format.getString(MediaFormat.KEY_MIME); if (mime.startsWith(mimePrefix)) { return i; } } return -1; } }
VideoCombineManager类
public final class VideoCombineManager { private static final String TAG = "VideoCombineManage"; private static VideoCombineManager mInstance; public static VideoCombineManager getInstance() { if (mInstance == null) { mInstance = new VideoCombineManager(); } return mInstance; } /** * 启动视频合并任务 * @param videoPath 待合并的视频路径列表 * @param destPath 合并输出文件路径 * @param listener 合并过程回调 */ public void startVideoCombiner(final List<String> videoPath, final String destPath, final VideoCombiner.VideoCombineListener listener) { new Thread(new Runnable() { @Override public void run() { VideoCombiner videoCombiner = new VideoCombiner(videoPath, destPath, listener); videoCombiner.combineVideo(); } }).start(); } }
内容的提问来源于stack exchange,提问作者Kerols afifi
相关产品推荐
相关产品推荐

