购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.3
合并音视频

本节介绍如何利用FFmpeg合并两个文件中的视频数据和音频数据,首先描述比较时间基和转换时间基两个函数的用法,以及如何把视频流和音频流合并到一个视频文件中;然后叙述还原数据帧和压缩数据帧相关函数的用法,以及如何对视频文件中的视频数据重新压缩编码;最后阐述如何把两个视频文件中的视频数据合并到一个视频文件,以及在合并过程中如何调整新视频的时间戳。

3.3.1 合并视频流和音频流

分离音视频的逆向操作是合并音视频,那么音视频的合并代码是否为分离代码的简单逆向呢?比如从视频文件剥离音频流的时候,判断数据包的stream_index字段,如果该字段值为音频流的索引,就丢掉数据包;如果该字段值为视频流的索引,就将数据包写入目标文件。然而,把纯音频文件和纯视频文件重新合并为带音频的视频文件,这可不能把来自两路数据流的数据包简单掺和在一起,因为音频流的时间戳要和视频流的时间戳相对照,在相同时刻的音视频数据必须放在相邻位置,才能保证播放器在解码播放时能够口型对准声音,而不会出现口不对声的异样画面。

但是音频流与视频流的时间基不同,它们的数据包时间戳单位不统一,也就无法直接比较这两种时间戳的大小。若想判断两个不同基准的时间戳大小,则要引入FFmpeg提供的av_compare_ts函数,该函数的前两个参数为第一个时间戳的数值及其时间基,后两个参数为第二个时间戳的数值及其时间基。当av_compare_ts函数的返回值小于0时,表示第一个时间戳代表的时间值小于第二个时间戳代表的时间值;当返回值等于0时,表示两个时间戳代表的时间值相同;当返回值大于0时,表示第一个时间戳代表的时间值大于第二个时间戳代表的时间值。

通过比较音频时间戳和视频时间戳,从而决定两类数据包的顺序排列,这只是音视频合并的第一步。因为写入目标文件的时候,不能直接采用原来的时间戳数值,而要重新计算目标文件要求的时间戳,也就是按照目标文件的时间基统一音视频的时间戳。此时可以调用av_rescale_q函数把某个时间戳从甲时间基准转换为乙时间基准,不过AVPacket结构有两个时间戳,分别是播放时间戳pts字段和解码时间戳dts字段,另外还有持续时长duration字段,因此,如果时间基准发生变化,数据包的这三个字段都要调整时间基。幸亏FFmpeg贴心地提供了av_packet_rescale_ts函数,该函数能够一次性转换数据包的时间基,查看av_packet_rescale_ts的函数源码,会发现其内部对pts、dts、duration三个字段依次调用了av_rescale_q转换函数,因此开发者只需调用一次av_packet_rescale_ts即可实现数据包的时间基转换操作。

总结一下,合并音频流和视频流主要增加了以下两项操作:

(1)调用av_compare_ts函数比较音频包和视频包的时间戳大小,以此解决音视频数据包的排序问题。

(2)调用av_packet_rescale_ts函数根据新的时间基准转换数据包的时间戳,包括播放时间戳、解码时间戳、持续时长都要转换,以此解决新文件的音视频同步问题。

根据以上思路编写音视频合并的FFmpeg代码片段如下(完整代码见chapter03/mergeaudio.c)。

接着执行下面的编译命令:

     gcc mergeaudio.c -o mergeaudio -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib
-lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序,期望合并指定的视频文件和音频文件。

    ./mergeaudio ../fuzhous.mp4 ../fuzhous.aac

程序运行完毕,发现控制台输出以下日志信息,说明完成了将音频流和视频流合并在一起的操作。

    Success open input_file ../fuzhous.mp4.
    Success open input_file ../fuzhous.aac.
    Success open output_file output_mergeaudio.mp4.
    Success merge video and audio file.

最后打开影音播放器,可以正常播放output_mergeaudio.mp4,表明上述代码正确实现了合并音视频的功能。

运行下面的ffmpeg命令也可以把没有音频流的视频文件和音频文件合并为一个携带音频流的视频文件。命令中的-vcodec copy表示输出的视频编码器用原来的,-acodec aac表示输出的音频编码器采用AAC。

    ffmpeg -i ../fuzhous.mp4 -i ../fuzhous.aac -vcodec copy -acodec aac
    ff_merge_audio.mp4

3.3.2 对视频流重新编码

3.3.1节针对数据包的时间戳处理,其实不涉及数据帧的加工操作,因为数据包是由数据帧压缩而来的,也就是说,只改了个时间标记而已,整个压缩包的内容并未发生变化。若想实质编辑视频的画面,无疑要先把视频的数据包解压才行,只有解压得到原始的数据帧,才能对这个原始数据进行修改操作。等到数据帧修改完成,还要通过编码器把数据帧压缩成编码后的数据包,再把重新压缩的数据包写入目标文件。在还原与压缩的过程中,主要用到了以下转换函数。

· avcodec_send_packet:把未解压的数据包发给解码器实例。

· avcodec_receive_frame:从解码器实例获取还原后的数据帧。

· avcodec_send_frame:把原始的数据帧发给编码器实例。

· avcodec_receive_frame:从编码器实例获取压缩后的数据包。

具体到代码编写上,可将重新编码的操作过程划分为三个步骤:创建编码器的实例并对编码参数赋值、对数据包解压得到原始的数据帧、对数据帧编码得到压缩后的数据包,分别说明如下。

1.创建编码器的实例并对编码参数赋值

以对视频帧重新编码为例,其编码器实例的创建过程主要有三步:首先调用avcodec_alloc_context3函数分配编码器的实例,然后调用avcodec_parameters_to_context函数把源视频流中的编解码参数复制给编码器的实例,最后调用avcodec_open2函数打开编码器的实例。这个创建过程算是按部就班,唯一值得注意的是第二步,因为avcodec_parameters_to_context函数只会复制常规的编解码参数,不会复制帧率和时间基,所以需要开发者另外给编码器实例设置帧率和时间基。

下面是创建编码器实例并赋值的FFmpeg代码(完整代码见chapter03/recode.c)。

    enum AVCodecID video_codec_id = src_video->codecpar->codec_id;
    // 查找视频编码器
    AVCodec *video_codec = (AVCodec*) avcodec_find_encoder(video_codec_id);
    if (!video_codec) {
        av_log(NULL, AV_LOG_ERROR, "video_codec not found\n");
        return -1;
    }
    video_encode_ctx = avcodec_alloc_context3(video_codec);  // 分配编码器的实例
    if (!video_encode_ctx) {
        av_log(NULL, AV_LOG_ERROR, "video_encode_ctx is null\n");
        return -1;
    }
    // 把源视频流中的编解码参数复制给编码器的实例
    avcodec_parameters_to_context(video_encode_ctx, src_video->codecpar);
    // 注意:帧率和时间基要单独赋值,因为avcodec_parameters_to_context没复制这两个参数
    video_encode_ctx->framerate = src_video->r_frame_rate;
    // framerate.num值过大,会导致视频头一秒变灰色
    if (video_encode_ctx->framerate.num > 60) {
        video_encode_ctx->framerate = (AVRational){25, 1};  // 帧率
    }
    video_encode_ctx->time_base = src_video->time_base;
    video_encode_ctx->gop_size = 12;  // 关键帧的间隔距离
    // AV_CODEC_FLAG_GLOBAL_HEADER标志允许操作系统显示该视频的缩略图
    if (out_fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
        video_encode_ctx->flags = AV_CODEC_FLAG_GLOBAL_HEADER;
    }
    ret = avcodec_open2(video_encode_ctx, video_codec, NULL);  // 打开编码器的实例
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Can't open video_encode_ctx.\n");
        return -1;
    }
    dest_video = avformat_new_stream(out_fmt_ctx, NULL);  // 创建数据流
    // 把编码器实例的参数复制给目标视频流
    avcodec_parameters_from_context(dest_video->codecpar, video_encode_ctx);
    dest_video->codecpar->codec_tag = 0;
2.对数据包解压得到原始的数据帧

因为这里只讨论视频的重新编码,所以要判断数据包的stream_index字段,只有该字段值为视频流索引时,才开展后续的视频重新编码操作。注意此时要提前分配数据帧AVFrame,用于保存解码后的视频数据,下面是对视频包解码的FFmpeg代码片段(完整代码见chapter03/recode.c)。

    AVPacket *packet = av_packet_alloc();                         // 分配一个数据包
    AVFrame *frame = av_frame_alloc();                            // 分配一个数据帧
    while (av_read_frame(in_fmt_ctx, packet) >= 0) {    // 轮询数据包
        if (packet->stream_index == video_index) {       // 视频包需要重新编码
            packet->stream_index = 0;
            recode_video(packet, frame);                          // 对视频帧重新编码
        } else {                                                  // 音频包暂不重新编码,直接写入目标文件
            packet->stream_index = 1;
            ret = av_write_frame(out_fmt_ctx, packet);   // 往文件写入一个数据包
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "write frame occur error %d.\n", ret);
                break;
           }
       }
       av_packet_unref(packet);  // 清除数据包
    }

对数据包解码的时候,先调用avcodec_send_packet函数把未解压的数据包发给解码器实例,再调用avcodec_receive_frame函数从解码器实例获取还原后的数据帧。解码过程运用的recode_video函数定义代码如下。

    // 对视频帧重新编码
    int recode_video(AVPacket *packet, AVFrame *frame) {
        // 把未解压的数据包发给解码器实例
        int ret = avcodec_send_packet(video_decode_ctx, packet);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "send packet occur error %d.\n", ret);
            return ret;
        }
        while (1) {
            // 从解码器实例获取还原后的数据帧
            ret = avcodec_receive_frame(video_decode_ctx, frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                return (ret == AVERROR(EAGAIN)) ? 0 : 1;
            } else if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "decode frame occur error %d.\n", ret);
                break;
            }
            output_video(frame);  // 给视频帧编码,并写入压缩后的视频包
        }
        return ret;
    }

可见在数据包的解码过程中,先调用avcodec_send_packet,再调用avcodec_receive_frame,但是为什么调用一次avcodec_send_packet之后,还要循环调用avcodec_receive_frame呢?这里主要考虑到B帧的影响,因为B帧是双向预测帧,它既参考了前面的视频帧,也参考了后面的视频帧。那么在收到一个B帧压缩后的数据包时,解码器实例无法立即解压出原始的数据帧,此时调用avcodec_receive_frame函数会返回AVERROR(EAGAIN),表示信息不足,需要更多的参考数据;只有在收到下一帧(比如P帧)的压缩包之后,解码器实例才能解压出前面的B帧,以及当前的P帧。如此一来,后面那一帧就得调用两次avcodec_receive_frame函数,才能依次取出之前的B帧和之后的P帧,总共两帧数据。

然而在解码之前,谁也不知道一个数据包究竟是I帧、B帧还是P帧,也就不知道avcodec_send_packet之后到底要调用几次avcodec_receive_frame。因此,通过循环调用avcodec_receive_frame并判断该函数的返回值,当返回AVERROR(EAGAIN)时,表示解码数据不足,就跳出循环继续发送下一个数据包;当返回0时,表示成功解码,把解压出来的数据帧写入文件,然后继续下一次循环;当返回AVERROR_EOF时,表示解码器实例没东西返回了,就跳出循环继续发送下一个数据包。以上数据包解码流程如图3-3所示。

图3-3 视频数据包的解码流程

3.对数据帧编码得到压缩后的数据包

对数据帧编码时,先调用avcodec_send_frame函数把原始的数据帧发给编码器实例,再调用avcodec_receive_packet函数从编码器实例获取压缩后的数据包,然后才把压缩数据包写入目标文件。注意在写文件之前,要把视频包的时间基转换成目标文件的时间基,也就是调用av_packet_rescale_ts函数执行时间戳的基准转换操作。下面是对视频帧编码并写入目标文件的FFmpeg代码片段(完整代码见chapter03/recode.c)。

    // 给视频帧编码,并写入压缩后的视频包
    int output_video(AVFrame *frame) {
        // 把原始的数据帧发给编码器实例
        int ret = avcodec_send_frame(video_encode_ctx, frame);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "send frame occur error %d.\n", ret);
            return ret;
        }
        while (1) {
            AVPacket *packet = av_packet_alloc();  // 分配一个数据包
            // 从编码器实例获取压缩后的数据包
            ret = avcodec_receive_packet(video_encode_ctx, packet);
           if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
               return (ret == AVERROR(EAGAIN)) ? 0 : 1;
           } else if (ret < 0) {
               av_log(NULL, AV_LOG_ERROR, "encode frame occur error %d.\n", ret);
               break;
           }
           // 把数据包的时间戳从一个时间基转换为另一个时间基
           av_packet_rescale_ts(packet, src_video->time_base, dest_video->time_base);
           packet->stream_index = 0;
           ret = av_write_frame(out_fmt_ctx, packet);  // 往文件写入一个数据包
           if (ret < 0) {
               av_log(NULL, AV_LOG_ERROR, "write frame occur error %d.\n", ret);
               break;
           }
           av_packet_unref(packet);  // 清除数据包
       }
       return ret;
    }

从以上代码看到,调用了一次avcodec_send_frame函数之后,紧接着循环调用avcodec_receive_packet函数,这里同样考虑到了B帧的影响,因为压缩一个B帧需要参考后面的其他帧,只有收到后面的帧,前面的B帧才能被编码压缩。

接着执行下面的编译命令:

     gcc recode.c -o recode -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat
-lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行下面的命令启动测试程序,期望对视频文件的视频数据重新编码。

    ./recode ../fuzhou.mp4

程序运行完毕,发现控制台输出以下日志信息,说明完成了把视频文件重新编码保存为新文件的操作。

    Success open input_file ../fuzhou.mp4.
    Success open output_file output_recode.mp4.
    Success copy video parameters_to_context.
    Success open video codec.
    Success recode file.

最后打开影音播放器可以正常播放output_recode.mp4,表明上述代码初步实现了对视频文件重新编码的功能。

然而仔细观看output_recode.mp4,发觉该视频末尾好像丢失了片段,播放时长也比原视频短了一点。进一步跟踪发现新视频少了末尾的几十帧,造成视频时长意外缩短了,这便是FFmpeg入门时经常遇到的视频丢帧问题。

究其原因,是因为FFmpeg在编解码的时候运用了缓存机制,表面上调用av_read_frame函数已经读到了源文件的末尾,实际上只有大部分视频数据写到了目标文件,还剩一小部分视频数据留在编码器实例的缓存中。此时需要向编码器实例发送一个空帧,表示已经没货了,请把缓存里的东西全都冲出来吧。具体到代码上,只需在调用output_video函数时传入NULL即可,示例代码如下:

    output_video(NULL);  // 传入一个空帧,冲走编码缓存

除编码器实例外,解码器实例也有缓存,因此在源文件全部读完之后,也要给解码器实例发送一个空包。不过这个空包不能直接传NULL,而是给数据包的data字段赋值NULL,size字段赋值0,表示这个数据包的内容为空且大小为0。向解码器实例发送空包的代码片段如下:

    packet->data = NULL;          // 传入一个空包,冲走解码缓存
    packet->size = 0;
    recode_video(packet, frame);  // 对视频帧重新编码

综合上述两处代码优化,既要向编码器实例发送空帧,又要向解码器实例发送空包。优化后对视频帧重新编码的代码示例如下(完整代码见chapter03/recode2.c)。

    AVPacket *packet = av_packet_alloc();                          // 分配一个数据包
    AVFrame *frame = av_frame_alloc();                             // 分配一个数据帧
    while (av_read_frame(in_fmt_ctx, packet) >= 0) {               // 轮询数据包
        if (packet->stream_index == video_index) {                 // 视频包需要重新编码
            packet->stream_index = 0;
            recode_video(packet, frame);                      // 对视频帧重新编码
        } else {                                              // 音频包暂不重新编码,直接写入目标文件
            packet->stream_index = 1;
            ret = av_write_frame(out_fmt_ctx, packet);             // 往文件写入一个数据包
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "write frame occur error %d.\n", ret);
                break;
            }
        }
        av_packet_unref(packet);                              // 清除数据包
    }
    packet->data = NULL;                                      // 传入一个空包,冲走解码缓存
    packet->size = 0;
    recode_video(packet, frame);                              // 对视频帧重新编码
    output_video(NULL);                                       // 传入一个空帧,冲走编码缓存
    av_write_trailer(out_fmt_ctx);                            // 写文件尾

接着执行下面的编译命令:

     gcc recode2.c -o recode2 -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat
-lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序,期望对视频文件的视频数据重新编码。

    ./recode2 ../fuzhou.mp4

程序运行完毕,发现控制台输出以下日志信息,说明完成了把视频文件重新编码保存为新文件的操作。

    Success open input_file ../fuzhou.mp4.
    Success open output_file output_recode2.mp4.
    Success recode file.

最后打开影音播放器可以正常播放output_recode2.mp4,并且视频时长没有缩短,表明上述代码解决了对视频重新编码会丢帧的问题。

运行下面的ffmpeg命令也可以对视频文件重新按照H.264格式编码。命令中的-vcodec h264表示输出的视频编码器采用H.264(libx264)。

    ffmpeg -i ../fuzhous.mp4 -vcodec h264 ff_recode_video.mp4

3.3.3 合并两个视频文件

3.3.2节对视频重新编码,最终生成的还是那个视频文件,乍看起来没有什么实际意义。其实视频的绝大部分加工编辑操作都依赖于视频数据的重新编码,比如把两个视频合并为一个视频,就需要将两个视频的码流按照目标文件的编码器统一实施编码,从而保证目标文件的视频流编码规范、格式统一。

合并两个视频文件的过程,相比之前的视频文件重新编码操作,主要存在三个不同之处:把两个源文件的信息读取到相关数组,根据第一个文件的时长计算第二个文件的开始时间戳,以及调整第二个文件的数据帧时间戳。下面对这三处分别加以说明。

1.把两个源文件的信息读取到相关数组

因为要打开两个输入的视频文件,所以事先声明几个长度为2的指针数组,同时给open_input_file函数增加一个表示序号的输入参数,用于区分当前读取的是第几个输入文件。于是声明数组以及读取两个音视频文件的FFmpeg代码示例如下(完整代码见chapter03/mergevideo.c)。

     AVFormatContext *in_fmt_ctx[2] = {NULL, NULL};                            // 输入文件的封装器实例
     AVCodecContext *video_decode_ctx[2] = {NULL, NULL};                       // 视频解码器的实例
     int video_index[2] = {-1 -1};                                             // 视频流的索引
     AVStream *src_video[2] = {NULL, NULL};                                    // 源文件的视频流
     // 打开输入文件
     int open_input_file(int seq, const char *src_name) {
         // 打开音视频文件
         int ret = avformat_open_input(&in_fmt_ctx[seq], src_name, NULL, NULL);
         if (ret < 0) {
             av_log(NULL, AV_LOG_ERROR, "Can't open file %s.\n", src_name);
             return -1;
         }
         av_log(NULL, AV_LOG_INFO, "Success open input_file %s.\n", src_name);
         // 查找音视频文件中的流信息
         ret = avformat_find_stream_info(in_fmt_ctx[seq], NULL);
         if (ret < 0) {
             av_log(NULL, AV_LOG_ERROR, "Can't find stream information.\n");
             return -1;
         }
         // 找到视频流的索引
         video_index[seq] = av_find_best_stream(in_fmt_ctx[seq], AVMEDIA_TYPE_VIDEO, -1, -1,
NULL, 0);
         if (video_index[seq] >= 0) {
             src_video[seq] = in_fmt_ctx[seq]->streams[video_index[seq]];
             enum AVCodecID video_codec_id = src_video[seq]->codecpar->codec_id;
             // 查找视频解码器
             AVCodec *video_codec = (AVCodec*) avcodec_find_decoder(video_codec_id);
             if (!video_codec) {
                 av_log(NULL, AV_LOG_ERROR, "video_codec not found\n");
                 return -1;
             }
             video_decode_ctx[seq] = avcodec_alloc_context3(video_codec); // 分配解码器的实例
             if (!video_decode_ctx) {
                 av_log(NULL, AV_LOG_ERROR, "video_decode_ctx is null\n");
                 return -1;
             }
             // 把视频流中的编解码参数复制给解码器的实例
             avcodec_parameters_to_context(video_decode_ctx[seq],
src_video[seq]->codecpar);
             ret = avcodec_open2(video_decode_ctx[seq], video_codec, NULL);  // 打开解码器的实例
             av_log(NULL, AV_LOG_INFO, "Success open video codec.\n");
             if (ret < 0) {
                 av_log(NULL, AV_LOG_ERROR, "Can't open video_decode_ctx.\n");
                 return -1;
             }
         } else {
             av_log(NULL, AV_LOG_ERROR, "Can't find video stream.\n");
             return -1;
         }
         return 0;
     }

然后在主程序的main函数中调用两次open_input_file,序号分别传入0和1,表示依次打开两个音视频文件。调用代码如下。

    if (open_input_file(0, src_name0) < 0) {  // 打开第一个输入文件
        return -1;
    }
    if (open_input_file(1, src_name1) < 0) {  // 打开第二个输入文件
        return -1;
    }
2.根据第一个文件的时长计算第二个文件的开始时间戳

为了方便起见,目标文件的编码器与第一个输入文件保持一致,那么对于第一个输入文件,可把它的所有数据包原样写入目标文件。但是对于第二个输入文件就不能这么做了,因为正常音视频文件内部的时间戳是连续递增的,显然第二个输入文件的时间戳不能直接照搬写入目标文件。此时要先根据第一个输入文件的视频时长计算该文件末尾的时间戳,对于合并过来的第二个输入文件,其数据包的时间戳必须在第一个文件的基础上递增。

根据3.2.2节推出来的时间戳转换式子,假设某个以秒为单位的时间点为A,则其对应的时间戳=A÷av_q2d(time_base)。考虑到视频播放时长的duration字段单位是微秒,则duration字段对应的时间戳=duration÷1000÷1000÷av_q2d(time_base)。于是包含时间戳计算过程的视频合并代码片段如下。

    // 首先原样复制第一个视频
    AVPacket *packet = av_packet_alloc();  // 分配一个数据包
    AVFrame *frame = av_frame_alloc();          // 分配一个数据帧
    while (av_read_frame(in_fmt_ctx[0], packet) >= 0) {                  // 轮询数据包
        if (packet->stream_index == video_index[0]) {                    // 视频包需要重新编码
            recode_video(0, packet, frame, 0);                           // 对视频帧重新编码
        }
        av_packet_unref(packet);                                         // 清除数据包
    }
    packet->data = NULL;                                                 // 传入一个空包,冲走解码缓存
    packet->size = 0;
    recode_video(0, packet, frame, 0);                                   // 对视频帧重新编码
    // 然后在末尾追加第二个视频
    int64_t begin_sec = in_fmt_ctx[0]->duration;                         // 获取视频时长,单位为微秒
    // 计算第一个视频末尾的时间基,作为第二个视频开头的时间基
    int64_t begin_video_pts = begin_sec / ( 1000.0 * 1000.0 *
av_q2d(src_video[0]->time_base));
    while (av_read_frame(in_fmt_ctx[1], packet) >= 0) {                  // 轮询数据包
        if (packet->stream_index == video_index[1]) {                    // 视频包需要重新编码
            recode_video(1, packet, frame, begin_video_pts);  // 对视频帧重新编码
        }
        av_packet_unref(packet);                                         // 清除数据包
    }
    packet->data = NULL;                                                 // 传入一个空包,冲走解码缓存
    packet->size = 0;
    recode_video(1, packet, frame, begin_video_pts);                     // 对视频帧重新编码
    output_video(NULL);                                                  // 传入一个空帧,冲走编码缓存
    av_write_trailer(out_fmt_ctx);                                       // 写文件尾
3.调整第二个文件的数据帧时间戳

注意到上述代码给recode_video函数添加了两个输入参数,其中一个是文件序号,另一个是该文件的起始时间戳。对于第一个输入文件,起始时间戳默认为0,重新编码时无须特殊处理。对于第二个输入文件,起始时间戳为第一个文件的末尾时间戳,重新编码时要把数据帧的pts字段加上这个时间戳。据此编写的recode_video函数代码示例如下。

    // 对视频帧重新编码
    int recode_video(int seq, AVPacket *packet, AVFrame *frame, int64_t begin_video_pts) {
        // 把未解压的数据包发给解码器实例
        int ret = avcodec_send_packet(video_decode_ctx[seq], packet);
        if (ret < 0) {
            av_log(NULL, AV_LOG_ERROR, "send packet occur error %d.\n", ret);
            return ret;
        }
        while (1) {
            // 从解码器实例获取还原后的数据帧
            ret = avcodec_receive_frame(video_decode_ctx[seq], frame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                return (ret == AVERROR(EAGAIN)) ? 0 : 1;
            } else if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "decode frame occur error %d.\n", ret);
                break;
            }
        if (seq == 1) {  // 第二个输入文件
                 // 把时间戳从一个时间基改为另一个时间基
                 int64_t pts = av_rescale_q(frame->pts, src_video[1]->time_base,
src_video[0]->time_base);
                 frame->pts = pts + begin_video_pts;  // 加上增量时间戳
             }
             output_video(frame);  // 给视频帧编码,并写入压缩后的视频包
         }
         return ret;
     }

接着执行下面的编译命令:

     gcc mergevideo.c -o mergevideo -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib
-lavformat -lavdevice -lavfilter -lavcodec -lavutil -lswscale -lswresample -lpostproc -lm

编译完成后,执行以下命令启动测试程序,期望合并两个指定的视频文件。

    ./mergevideo ../fuzhous.mp4 ../seas.mp4

程序运行完毕,发现控制台输出以下日志信息,说明完成了把两个视频文件合并为一个视频文件的操作。

    Success open input_file ../fuzhous.mp4.
    Success open input_file ../seas.mp4.
    Success open output_file output_mergevideo.mp4.
    Success open video codec.
    Success merge two video file.

最后打开影音播放器可以正常播放output_mergevideo.mp4,新文件果然由原来的两个视频内容构成,表明上述代码正确实现了合并两个视频的功能。

运行下面的ffmpeg命令也可以把两个视频拼接成一个视频文件。命令中的-f concat表示输入源为前后连接的文件列表。

    ffmpeg -f concat -i concat_video.txt -c copy ff_merge_video.mp4

上面的命令用到的concat_video.txt文件格式如下: KyWV1vZFAGQQpN/7962nlGKspDAFiYOOi0nRjKKI7E2zMhsd55ih5/8eoOMX1+6F

    file 'ff_recode_video.mp4'
    file 'ff_recode_video.mp4'
点击中间区域
呼出菜单
上一章
目录
下一章
×