短视频中多个MP4视频合成为一个MP4视频怎么实现

2,597 阅读3分钟

之前写过HLS视频合并为一个MP4视频:
M3U8-TS文件合并为MP4文件
HLS公开课(五)HLS合成为一整个视频
HLS公开课(七)HLS合成为一整个视频解决dts不连续的问题
有人肯定会说多个MP4视频合成一个MP4视频和HLS合成一个MP4视频的做法是不是一样的啊?
其实还是不一样的。
讲几点理由吧:

  • HLS本质上是一个视频,所以不存在多个视频对接PTS和DTS问题,多个TS文件中的PTS和DTS已经帮忙对应好了内部的关系
  • 即使内部的TS文件可能存在PTS和DTS的问题,但是这个一般情况下是切片异常导致的问题,并不是HLS合成MP4必须要解决的问题
  • 几个新的MP4文件合成成一个MP4文件,文件衔接处的PTS和DTS需要特别小心的处理。

多个MP4视频合成一个MP4视频现在在短视频应用中应用非常广泛:主要有以下应用场景。

  • 视频录制的过程中可以让用户选择分段录制,这样会产生多个MP4视频文件,生成的视频又需要生成一个MP4文件
  • 音视频编辑SDK中可以导入多个视频,然后分别处理每个视频,最终合成一个视频

但是也有需要注意的点:
  • 完整的操作过程中只是解封装和重新封装的工作,不应该存在解码和编码的工作,因为移动端解码和编码实在非常耗时,用户体验这一关过不去。
  • 合并的视频应该是封装格式一样、分辨率一样,不然合并出来的视频会出现异常。好在移动端导入的视频都是使用手机拍摄的,所以上面两个条件应该是可以满足的。

多个MP4文件合并为一个MP4文件.png

如何解决合并过程的PTS和DTS问题

记住文件结尾的PTS和DTS

在执行av_read_frame的时候,如果发现读到文件末尾了,就会出现EOF:

        AVPacket packet;
        int result = av_read_frame(input_context_, &packet);
        if (result < 0) {
            av_packet_unref(&packet);

            //说明当前的文件已经读到结尾了
            //下面如果还要读取新的文件,就要更新pts和dts了
            if (result == AVERROR_EOF) {
                current_video_index_++;
                if (current_video_index_ >= input_size_) {
                    //视频序列已经遍历结束了
                    break;
                }
                int ret;
                do {
                    ret = OpenInputVideo(current_video_index_);
                    if (ret != 0) {
                        current_video_index_++;
                    }
                } while (ret != 0);
                continue;
            }
            continue;
        }

这个文件读到结尾时,开始读下一个文件。

下一个文件对接上一个文件的PTS和DTS

全局保存到的几个关键数据:

    int64_t last_video_pts_;
    int64_t last_video_dts_;
    int64_t last_audio_pts_;
    int64_t last_audio_dts_;

    int64_t current_video_pts_;
    int64_t current_video_dts_;
    int64_t current_audio_pts_;
    int64_t current_audio_dts_;

保存上一个视频的pts和dts信息,记录当前写视频的pts和dts信息,这样在解封和封装视频的时候不断校验pts和dts信息,完全可以保证pts和dts信息是正常排列的。

注意原始视频的旋转角度问题

检查一下每个视频的旋转角度信息,我们在开始的视频要读出这个信息,设置到输出的视频metadata中,因为旋转角度的信息也需要保持一致,不然合成之后的视频会存在问题。

WX20210727-234411@2x.png

        if (input_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            AVDictionaryEntry *rotateEntry = av_dict_get(input_stream->metadata, "rotate", nullptr, 0);
            if (rotateEntry != nullptr) {
                int rotate = atoi(rotateEntry->value);
                if (rotate != 0) {
                    char rotate_str[1024];
                    sprintf(rotate_str, "%d", rotate);
                    av_dict_set(&output_stream->metadata, "rotate", rotate_str, 0);
                }
            }
        }

将原始视频的旋转角度记录下来,写入生成的文件的metadata中,这样生成的文件的旋转角度和原始视频是一样的。

WX20210728-000131@2x.png

项目已开源

开源项目参考:github.com/JeffMony/Je…

在开源项目中,提供了一个测试用例:
files文件夹下面有4个视频,其中input1.mp4、input2.mp4、input3.mp4合并成output.mp4视频,可以参考下

input1.mp4
Duration: 00:00:10.86, start: 0.000000, bitrate: 11161 kb/s

input2.mp4
Duration: 00:00:11.22, start: 0.000000, bitrate: 10798 kb/s

input3.mp4
Duration: 00:00:11.46, start: 0.000000, bitrate: 11378 kb/s

output.mp4
Duration: 00:00:33.63, start: 0.000000, bitrate: 10513 kb/s

可以看出来output.mp4确实是input1.mp4/input2.mp4/input3.mp4三个视频合并而成的。