You need to enable JavaScript to run this app. Android Studio合并MP4视频后视频损坏但音频正常的问题求解答
优惠活动
大模型
产品
解决方案
定价
更多

Android Studio合并MP4视频后视频损坏但音频正常的问题求解答

MP4合并功能实现问题排查与修正

问题现象

  • 尝试使用Mp4parser库实现MP4合并,输出文件存在视频损坏问题
  • 所有合并方案下,音频输出质量始终保持正常
  • 基于MediaExtractor、MediaMuxer自行实现的合并逻辑,生成的视频存在严重失真、损坏问题

核心故障原因

  1. 未校验所有待合并视频的编码参数一致性
    代码仅从首个解析成功的视频、音频轨道提取MediaFormat传入Muxer,未校验所有待合并文件的分辨率、视频编码Profile/Level、码率、音频采样率、声道数是否完全匹配。任意一段文件参数与首段不一致时,写入的视频数据会因为格式不兼容出现解码花屏、失真。
  2. 未做拼接点关键帧对齐
    MediaMuxer写入规则要求,每个视频轨道的拼接位置必须从关键帧起始。代码未在切换文件时跳过首个非关键帧,直接从读取到的第一个帧开始写入,会导致拼接点之后的画面因为缺少参考帧出现撕裂、失真。
  3. 读写缓冲区大小固定不足
    代码固定设置缓冲区大小为1MB,高码率1080P/4K视频的单帧数据很容易超过这个阈值,readSampleData会直接截断帧内容,造成视频帧损坏。应该从轨道的MediaFormat中读取MAX_INPUT_SIZE参数动态分配缓冲区大小。
  4. 轨道选择逻辑存在异常风险
    当selectTrack方法返回-1(未找到对应类型轨道)时,代码没有做异常拦截,仍然执行selectTrack(-1)操作,会导致后续读取的采样数据轨道索引和预期不匹配,出现视频/音频流交叉写入的损坏问题。
  5. 时间戳计算逻辑错误
    代码每合并完一段视频就硬加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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.29 19:36:02