FFmpeg 新旧API编码

背景

直播SDK一开始使用的FFmpeg 2.8版本的,现在的FFmpeg最新版已经是4.4了。播放器编辑器使用的FFmpeg都是4.0的版本;新版本FFmpeg在内部结构也做了优化,效率、稳定性相比较旧版本都提升了不少。所以直播SDK FFmpeg也要必须升级了。

简介

直播SDK内部主要3部分使用了FFmpeg:

  1. 使用libavcodec 编码Audio;
  2. 使用libavcodec 编码Video;
  3. 使用libavformat 合成/推流;

我会先讲解使用旧AP Ilibavcodec编码Audio、Video的过程。然后再讲解使用新API编码Audio、Video的过程,通过前后的对比就可以很容易的知道直播SDK FFmpeg升级点在哪里?这里只是通过分析新旧API的使用来对比出我们直播SDK的更新点,同时也可以学习如何通过FFmpeg新旧API编码Audio、Video、Muxer。因为篇幅有限,这里并不会具体去讲解每个API内部的源码逻辑,感兴趣可以自行了解。

libavcodec旧API编码Audio

文本阐述感觉是比较枯燥的,这里先上图了解下通过FFmpeg libavcodec模块旧API如何编码Audio。后面在具体介绍API的作用与功能。
FFmpeg 新旧API编码

编码器注册

av_register_all()也可以使用avcodec_register_all()代替。查看源码可以发现av_register_all()内部调用了avcodec_register_all()。它的作用更就是注册所有的编解码器。

查找编码器

编码器注册好了之后就可以通过avcodec_find_encoder_by_name()、avcodec_find_encoder()获取我们想要的编码器。例如:通过avcodec_find_encoder(AV_CODEC_ID_AAC), 如果我们想使用libfdk-aac编码器编码音频必须在FFmpeg交叉编译的时候链接进去。否则我们在获取编码器的时候会使用FFmpeg内部默认的AAC编码器。

创建 AVCodecContext

当编码器创建好了之后,就需要根据编码器创建AVCodecContext,并初始化编码参数:采样里、声道数、采样格式等。

AVCodecContext *avCodecContext = avcodec_alloc_context3(codec);
avCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
avCodecContext->sample_rate = 44100;
avCodecContext->bit_rate = 64000;
avCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
avCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
avCodecContext->channels = av_get_channel_layout_nb_channels(avCodecContext->channel_layout);
avCodecContext->profile = FF_PROFILE_AAC_LOW;
avCodecContext->flags |= CODEC_FLAG_GLOBAL_HEADER;
avCodecContext->codec_id = codec->id;

打开编码器

编码器的参数设置好了之后,就可以打开编码器了。


if (avcodec_open2(avCodecContext, codec, NULL) < 0) {
   return -1;
}

创建AVFrame,并申请一块PCM内存

在FFmpeg 中编码前、解码后的数据用AVFrame表示;编码后,解码前的数据使用AVPacket表示;

这里需要为我们即将编码的数据创建一个AVFrame,并为它创建一个存放数据的空间。


// 创建AVFrame
AVFrame *encode_frame = av_frame_alloc();
encode_frame->nb_samples = avCodecContext->frame_size;
encode_frame->format = avCodecContext->sample_fmt;
encode_frame->channel_layout = avCodecContext->channel_layout;
encode_frame->sample_rate = avCodecContext->sample_rate;

// 申请一块PCM内存
int ret = av_samples_alloc_array_and_samples(&pcm_buffer, &src_samples_linesize, avCodecContext->channels, audio_nb_samples, avCodecContext->sample_fmt, 0);
if (ret < 0) {
    return -1;
}

编码

编码的过程是一个持续的循环过程。

  1. 从PCM队列中获取一帧音频数据到pcm_buffer

  2. 把pcm_buffer填充到AFrame中

  3. 音频编码,获取到编码后AVPacket数据

// 从PCM队列中获取一帧音频数据到pcm_buffer
pcm_frame_callback(pcm_buffer);

int ret;
int got_packet;
AVPacket *pkt = av_packet_alloc();
pkt->duration = (int) AV_NOPTS_VALUE;
pkt->pts = pkt->dts = 0;
  
// 把pcm_buffer填充到AFrame中
avcodec_fill_audio_frame(encode_frame, avCodecContext->channels, avCodecContext->sample_fmt, pcm_buffer[0], audioSamplesSize, 0);

// 音频编码,获取到编码后AVPacket数据
ret = avcodec_encode_audio2(avCodecContext, pkt, encode_frame, &got_packet);
if (ret < 0 || !got_packet) {
    av_packet_free(&pkt);
    return ret;
}

// write、enqueue

销毁

if (NULL != pcm_buffer) {
    av_free(pcm_buffer);
}
if (NULL != encode_frame) {
    av_frame_free(&encode_frame);
}
if (NULL != avCodecContext) {
   avcodec_close(avCodecContext);
   av_free(avCodecContext);
}

libavcodec 新API编码Audio

这里先上图了解下通过FFmpeg libavcodec模块新API如何编码Audio。后面在具体介绍核心API的作用与功能。

FFmpeg 新旧API编码

通过上图可以知道,FFmpeg新API在总编码过程中思想是不变的,只是调用的API改变了。这里重点介绍新API的使用,与旧API相同的部分就不再讲解。

创建AVFrame,并申请一块PCM内存

旧API创建方式:先申请一块内存,待填充好了pcm数据之后,再把该内存挂载到AVFrame上。

新API直接可以通过av_frame_get_buffer()为AVFrame创建好了内存。不过在调用av_frame_get_buffer()之前必须为AVFrame设置好采样率、声道数、采样格式、采样大小。

// 创建AVFrame
AVFrame *encode_frame = av_frame_alloc();
encode_frame->nb_samples = avCodecContext->frame_size;
encode_frame->format = avCodecContext->sample_fmt;
encode_frame->channel_layout = avCodecContext->channel_layout;
encode_frame->sample_rate = avCodecContext->sample_rate;

// 申请一块PCM内存
int ret = av_frame_get_buffer(encode_frame, 0);
if (ret < 0) {
    return -1;
}

编码

FFmpeg新API编码通过avcodec_send_frame()、avcodec_receive_packet()实现。他们内部实现原理可以参考文章底部的介绍。

    AVPacket pkt = { 0 };
    av_init_packet(&pkt);
    pkt.duration = (int) AV_NOPTS_VALUE;
    pkt.pts = pkt.dts = 0;
   
    while (true){
        do{
            ret = avcodec_receive_packet(avCodecContext, &pkt);
            // ret >= 0 获取编码后的视频流
            if(ret >= 0){
                
                av_free_packet(&pkt);
                return ret;
            }
            //
            if (ret == AVERROR(EAGAIN)) {
                // 跳出该循环。
                break;
            }
            // 编码出错
            if (ret < 0) {
                av_free_packet(&pkt);
                return ret;
            }
        }while (true);

        // 获取pcm数据
        pcm_frame_callback(encode_frame->data);
        ret = avcodec_send_frame(avCodecContext, encode_frame);
        if(ret >= 0){
//            LOGI("avcodec_send_frame success");
        }else{
            LOGI("avcodec_send_frame error: %s\n", av_err2str(ret));
        }
        av_packet_unref(&pkt);
    }

销毁

if (NULL != encode_frame) {
    av_frame_free(&encode_frame);
}
if (NULL != avCodecContext) {
   avcodec_free_context(avCodecContext);
}

libavcodec 旧API编码Video

FFmpeg中通过libavcodec旧API编码Video,它的过程与libavcodec旧API编码Audio是非常相似的。我这里只给出API调用流程图,流程图过程中的每一步就不再详细分析了。你只要看懂了上面的分析,这里是非常简单的。
FFmpeg 新旧API编码

libavcodec新API编码Video

FFmpeg中通过libavcode新API编码Video,它的过程与libavcodec新API编码Audio是非常相似的。我这里只给出API调用流程图,流程图过程中的每一步就不再详细分析了。你只要看懂了上面的分析,这里是非常简单的。
FFmpeg 新旧API编码

音视频编解码器基本原理

在FFMPEG中 avcodec_send_frame() 和 avcodec_receive_packet() 通常是同时使用的,先调用 avcodec_send_frame() 送入要编码的音视频帧,然后调用 avcodec_receive_packet()获取编码后的数据包。但是需要注意的是:编码器内部是有缓冲区数据处理的,因此并不保证每送入一个音视频帧,就一定有相应的编码数据包输出,这两个函数对于数据处理,在时序上并不同步,这一点特别需要注意。

通常解码开始,通过avcodec_send_frame()送入几十个音视频帧,对应的avcodec_receive_packet()都没有数据包输出。等送入的帧足够多后,avcodec_receive_packet()才开始输出前面一开始送入进行编码的数据包。最后几十没有数据帧送入了,也要调用avcodec_send_frame()送入空帧,以驱动编码模块继续编码缓冲区中的数据,此时avcodec_receive_packet()还是会有数据包输出,直到返回AVERROR_EOF才表示所有音视频帧编码完成。

FFmpeg 新旧API编码

举个例子:

总计有100个视频帧要送入编码器编码,最终输出视频帧也是100个数据包输出。

  1. 前面20次调用 avcodec_send_frame()可以不断的送入第1~20个视频帧,但是前面20次调用avcodec_receive_packet()函数总是返回AVERROR(EAGAIN),没有数据包输出。

  2. 从第21次调用avcodec_send_frame()送入第21个视频帧开始,这次再调用avcodec_receive_packet()函数可以返回0,并且有数据包输出,但是输出的数据包pts是0(也即第一个数据包对应的视频帧),之后avcodec_send_frame()不断送入第22、23…个视频帧,avcodec_receive_packet()不断输出第1、2…个数据包

  3. 最后第100个数据包通过avcodec_send_frame()送入完成了,但是此时avcodec_receive_packet()才获取到第82帧输出数据包,此时需要继续不断调用avcodec_send_frame()送入空帧,同时不断调用avcodec_receive_packet()获取输出数据包,直到返回AVERROR_EOF,可以获取到最后第100个输出的数据包。

FFmpeg 新旧API编码

音视频编解码器基本原理参考文档:
https://zhuanlan.zhihu.com/p/346010443

上一篇:Web Component API-自定义组件


下一篇:01_邂逅vue3开发