本章主要介绍如何使用FFmpeg来将一个音频文件和一个视频文件合成一个MP4文件,以及在这个过程中我们如何对编码过程进行封装以及sample_rate 重采样的过程(由于提供的音频文件的编码类型为S16,所以我们需要转化为MP4支持的FLTP浮点类型)。

Muxer

首先我们来介绍如何封装MP4的封装器,就是我们将视频流和音频流输入封装器,封装器输出MP4文件。下面是封装器的头文件,里面有一些封装器必要的成员函数。

#ifndef MUXER_H #define MUXER_H #include <iostream> 
// 在C++文件中中导入C库需要使用extern关键字 
extern "C" { 
  #include "libavcodec/avcodec.h" 
  #include "libavformat/avformat.h" 
} 

class Muxer { 
  public: Muxer(); 
  ~Muxer(); 
  // 初始化 
  int Init(const char* url); 
  // 释放资源 
  void DeInit(); 
  // 将一条视频/音频流添加到封装器 
  int AddStream(AVCodecContext* codec_ctx); 
  // 将头发送到封装器中 
  int SendHeader(); 
  // 将数据帧发送到封装器中 
  int SendPacket(AVPacket* packet); 
  // 将尾发送到封装器中 
  int SendTrailer(); 
  // 打开输入源url 
  int Open(); private: 
  // format上下文 
  AVFormatContext* fmt_ctx_ = NULL; 
  // 输入源url,这里可能是url也可以是一个文件路径 
  std::string url_ = ""; 
  // 视频流复用器上下文 
  AVCodecContext* vid_codec_ctx_ = NULL; 
  AVCodecContext* aud_codec_ctx_ = NULL; 
  // 视频流 AVStream* vid_st_ = NULL; 
  AVStream* aud_st_ = NULL; 
  // 有没有对应的流 int video_index_ = -1; 
  int audio_index_ = -1; 
}; 
#endif // MUXER_H

接下来是封装器的具体时间,我们暂时只实现最基础的功能:

int Muxer::Init(const char *url) { 
  int ret = avformat_alloc_output_context2(&fmt_ctx_, NULL, NULL,url); 
  if(ret < 0) { 
    char errbuf[1024] = {0}; 
    av_strerror(ret, errbuf, sizeof(errbuf) - 1);
    printf("avformat_alloc_output_context2 failed:%s\n", errbuf); 
    return -1; 
  } 
  url_ = url; 
  return 0; 
}

由于这是第一个具体的函数实现,所以我就放上了获取错误的函数,后面我就不说了。avformat_alloc_output_context2用来初始化输出格式上下文。最后是将传入的url参数赋值给类成员。

void Muxer::DeInit() { 
  if(fmt_ctx_) { 
    avformat_close_input(&fmt_ctx_); 
  } 
  url_ = ""; 
  aud_codec_ctx_ = NULL; 
  aud_stream_ = NULL; 
  audio_index_ = -1; 
  vid_codec_ctx_ = NULL; 
  vid_stream_ = NULL; 
  video_index_ = -1; 
}

这里主要的功能就是关闭输出格式上下文,然后将其他的类成员设置为初始状态。

int Muxer::AddStream(AVCodecContext *codec_ctx) { 
  if(!fmt_ctx_) { 
    printf("fmt ctx is NULL\n"); 
    return -1; 
  } 
  if(!codec_ctx) { 
    printf("codec ctx is NULL\n"); 
    return -1; 
  } 
  AVStream *st = avformat_new_stream(fmt_ctx_, NULL); 
  if(!st) { 
    printf("avformat_new_stream failed\n"); return -1; } 
  // st->codecpar->codec_tag = 0; 
  // 从编码器上下文复制 
  avcodec_parameters_from_context(st->codecpar, codec_ctx);
  av_dump_format(fmt_ctx_, 0, url_.c_str(), 1); 
  // 判断当前的是视频流还是音频流 
  if(codec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) { 
    aud_codec_ctx_ = codec_ctx; 
    aud_stream_ = st; 
    audio_index_ = st->index; 
  } 
  else if(codec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) { 
    vid_codec_ctx_ = codec_ctx; 
    vid_stream_ = st; 
    video_index_ = st->index; 
  } 
  return 0; 
}
  • avformatnewstream的第二个参数通常是NULL,自动分配流。但是如果是已知编码器,可以直接传入AVCodec*。

  • 在判断当前传入的流的种类后,初始化对应的类成员。

int Muxer::SendHeader()
{
    if(!fmt_ctx_) {
        printf("fmt ctx is NULL\n");
        return -1;
    }
    /*
    * 这里其实可以选择封装参数(如mp4的faststart)
    * AVDictionary* option = NULL;
    * av_dict_set(&options, "movflags", "faststart", 0);
    */
    int ret = avformat_write_header(fmt_ctx_, NULL);
    if (ret != 0) {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof(errbuf) - 1);
        printf("avformat_write_header failed:%s\n", errbuf);
        return -1;
    }

    return 0;
}
  • 需要注意的是这个函数必须在所有流添加完成后调用,因为avformatwriteheader必须在所有流都添加完毕后调用.

  • 如果后续还要修改参数,需要在调用前完成。

int Muxer::SendPacket(AVPacket *packet)
{
    int stream_index = packet->stream_index;

    if (!packet || packet->size <=0 || packet->data) {
        printf("packet is null\n");
        if (packet) {
            av_packet_free(&packet);
        }

        return -1;
    }

    AVRational src_time_base; // 编码后的包
    AVRational dst_time_base; // mp4输出文件对应流的time_base

    if (vid_st_ && vid_codec_ctx_ && stream_index == video_index_) {
        src_time_base = vid_codec_ctx_->time_base;
        dst_time_base = vid_st_->time_base;
    }
    else if (aud_st_ && aud_codec_ctx_ && stream_index == audio_index_) {
        src_time_base = aud_codec_ctx_->time_base;
        dst_time_base = aud_st_->time_base;
    }

    packet->pts = av_rescale_q(packet->pts, src_time_base, dst_time_base);
    packet->dts = av_rescale_q(packet->dts, src_time_base, dst_time_base);
    packet->duration = av_rescale_q(packet->duration, src_time_base, dst_time_base);

    int ret = 0;
    ret = av_interleaved_write_frame(fmt_ctx_, packet);
    // ret = av_write_frame(fmt_ctx_, packet);
    av_packet_free(&packet);
    if (ret == 0) {
        return 0;
    }
    else {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof(errbuf) - 1);
        printf("avformat_write_header failed:%s\n", errbuf);
        return -1;
    }
}
  • avinterleavedwriteframe和avwrite_frame的功能其实差不多,不过前者会有一些缓存,而后者是直接写入到文件。前者的缓存目的是根据pts对帧进行排序。

  • 这里比较重要的就是时间基的转化问题。为什么要进行时间基转化呢:不同的音视频流都有自己的时间基,也就是fps,但是当我们合成的时候,就要统一这些时间基,把他们统一到新编码格式上。

int Muxer::SendTrailer()
{
    if(!fmt_ctx_) {
        printf("fmt ctx is NULL\n");
        return -1;
    }
    // 写入尾部信息
    int ret = av_write_trailer(fmt_ctx_);
    if (ret != 0) {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof(errbuf) - 1);
        printf("av_write_trailer failed:%s\n", errbuf);
        return -1;
    }

    return 0;
}
  • 这里的主要函数是avwritetrailer,它做了以下几件事:

  • 写入文件尾部信息(如MP4,MKV中的索引表);

  • 刷新内部缓冲区;

  • 调用每个AVStream的codec相关清理代码;

  • 确保生成的文件可被播放器正确读取;

  • 释放部分资源(这里还需要手动关闭avioclose()和avformatfree_context())

AudioEncoder

接下来是音频编码器,用来编码输入的音频流数据。

#ifndef AUDIOENCODER_H
#define AUDIOENCODER_H

extern "C"
{
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
}
class AudioEncoder
{
public:
    AudioEncoder();
    ~AudioEncoder();
    // 这里使用的AAC音频流,如果要适配更多的流,可以自己添加
    int InitAAC(int channels, int sample_rate, int bit_rate);
//    int InitMP3(/*int channels, int sample_rate, int bit_rate*/);
    void DeInit();  // 释放资源
    AVPacket *Encode(AVFrame *farme, int stream_index, int64_t pts, int64_t time_base);
    int GetFrameSize(); // 获取一帧数据 每个通道需要多少个采样点
    int GetSampleFormat();  // 编码器需要的采样格式
    AVCodecContext *GetCodecContext();
    int GetChannels();
    int GetSampleRate();
private:
    // 默认值
    int channels_ = 2; // 双声道
    int sample_rate_ = 44100; // 采样率
    int bit_rate_ = 128*1024; // 比特率
    int64_t pts_ = 0; // 显示时间:显示的时间  dts是解码时间:开始解码当前帧的时间
    AVCodecContext * codec_ctx_ = NULL;
};

#endif // AUDIOENCODER_H

这边的音频编码器只封装了AAC的音频流,并且设置了一些原始数据,后面可以再拓展。

int AudioEncoder::InitAAC(int channels, int sample_rate, int bit_rate)
{
    // 初始化当前参数
    channels_ = channels;
    sample_rate_ = sample_rate;
    bit_rate_ = bit_rate;
    // 根据ID寻找编码器
    AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);
    if(!codec) {
        printf("avcodec_find_encoder AV_CODEC_ID_AAC failed\n");
        return -1;
    }
    // 为编码器分配上下文
    codec_ctx_ = avcodec_alloc_context3(codec);
    if(!codec_ctx_) {
        printf("avcodec_alloc_context3 AV_CODEC_ID_AAC failed\n");
        return -1;
    }
    // 配置编码器上下文参数
    codec_ctx_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 取消AAC的adts头
    codec_ctx_->sample_rate = sample_rate;
    codec_ctx_->bit_rate = bit_rate;
    // 这是新的写法,这个函数会配置nb_channels和channel_layout
    av_channel_layout_default(&codec_ctx_->ch_layout, channels);
    // 编码采样格式
    codec_ctx_->sample_fmt = AV_SAMPLE_FMT_FLTP; // 平面浮点数

    int ret = avcodec_open2(codec_ctx_, NULL, NULL);
    if (ret != 0) {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof(errbuf) - 1);
        printf("avcodec_open2 failed:%s\n", errbuf);
        return -1;
    }

    printf("InitAAC success\n");
    return 0;
  • 这里需要注意的是avchannellayout_default,这是新的写法,之前需要单独分来对channel相关变量进行赋值。

// 这就是编码函数了
AVPacket *AudioEncoder::Encode(AVFrame *frame, int stream_index, int64_t pts, int64_t time_base)
{
    if (!codec_ctx_) {
        printf("codec_ctx_ null\n");
        return NULL;
    }
    // 时间基转换
    pts = av_rescale_q(pts, AVRational{1, (int)time_base}, codec_ctx_->time_base);
    if (frame) {
        frame->pts = pts;
    }

    int ret = avcodec_send_frame(codec_ctx_, frame);
    if (ret != 0) {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof (errbuf) - 1);
        printf("avcodec_send_frame failed:%s\n", errbuf);
        return NULL;
    }
    AVPacket*  packet = av_packet_alloc();
    ret = avcodec_receive_packet(codec_ctx_, packet);
    if (ret != 0) {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof (errbuf) - 1);
        printf("avcodec_send_frame failed:%s\n", errbuf);
        return NULL;
    }

    packet->stream_index = stream_index;
    return packet;
}
  • 设置好编码器参数后就是编码了,将数据帧一个一个编码为packet,最后记得设置一下index返回

  • 每一条音频和视频都是分开的,有自己的index(编号)。

Main

主函数的内容还是比较多的,由于涉及到一些常规的操作,比如打开文件等,这里就不都解释了,我们主要看一些比较重要的需要记录的地方。

  • 首先来看一些宏定义,他们定义了我们转化视频的一些参数。

// 视频的宽和高
#define YUV_WIDTH 720
#define YUV_HEIGHT 576
#define YUV_FPS 25
// 比特率
#define VIDEO_BIT_RATE 512*1024
// 采样率
#define PCM_SAMPLE_RATE 44100
#define PCM_CHANNELS 2

#define AUDIO_BIT_RATE 128*1024

// 基准时间 本例子中的时间是5s 也就是下面的时间*5
#define AUDIO_TIME_BASE 1000000
#define VIDEO_TIME_BASE 1000000
  • 接着这里有一个计算YUV420P编码格式帧大小的地方。

  int y_frame_size = yuv_width * yuv_height;
  int u_frame_size = yuv_width * yuv_height / 4;
  int v_frame_size = yuv_width * yuv_height / 4;
  int yuv_frame_size = y_frame_size * u_frame_size * v_frame_size;
/*
* 可以看到这里YUV三个方向的size计算方式不同
# 这是因为在YUV420中,UV方向的比特率都是Y方向的1/4
*/
  • 最后看一下主循环

while(1) {
      if (audio_finish && video_finish)
          break;

      printf("apts:%0.0lf, vpts:%0.0lf\n", audio_pts/1000, video_pts/1000);
      if ((video_finish != 1 && audio_pts > video_pts)
              || (video_finish != 1 && audio_finish ==1)) {
          read_len = fread(yuv_frame_buf, 1, yuv_frame_size, in_yuv_fd);
        // 文件中的视频帧内容已经消耗完
          if (read_len < yuv_frame_size) {
              video_finish = 1;
              printf("fread yuv_frame_buf finish\n");
          }

          if (video_finish != 1) {
              packet = video_encoder.Encode(yuv_frame_buf, yuv_frame_size, video_index,
                                            video_pts, video_time_base);
          }
          else {
            // 这里有一个冲刷编码器的过程
              packet = video_encoder.Encode(NULL, 0, video_index,
                                            video_pts, video_time_base);
          }
        // 叠加pts
          video_pts += video_frame_duration; // 叠加pts
          if (packet) {
              mp4_muxer.SendPacket(packet);
          }
      }
      else if (audio_finish != 1) {
          read_len = fread(pcm_frame_buf, 1, pcm_frame_size, in_pcm_fd);
          if (read_len < pcm_frame_size) {
              audio_finish = 1;
              printf("fread pcm_frame_buf finish\n");
          }

          if (audio_finish != 1) {
              AVFrame* fltp_frame = AllocFltpPcmFrame(pcm_channels, audio_encoder.GetFrameSize());
              ret = audio_resampler.ResampleFromS16ToFLTP(pcm_frame_buf, fltp_frame);

              packet = audio_encoder.Encode(fltp_frame, audio_index,
                                            audio_pts, audio_time_base);
              FreePcmFrame(fltp_frame);
          }
          else {
              packet = audio_encoder.Encode(NULL, audio_index,
                                            audio_pts, audio_time_base);
          }
          audio_pts += audio_frame_duration;
          if (packet) {
              mp4_muxer.SendPacket(packet);
          }
      }
  }

下面是主函数的本体:

#include <iostream>
#include "audioencoder.h"
#include "videoencoder.h"
#include "muxer.h"
#include "audioresampler.h"

using namespace std;

#define YUV_WIDTH 720
#define YUV_HEIGHT 576
#define YUV_FPS 25

#define VIDEO_BIT_RATE 512*1024

#define PCM_SAMPLE_RATE 44100
#define PCM_CHANNELS 2

#define AUDIO_BIT_RATE 128*1024

// 基准时间 本例子中的时间是5s 也就是下面的时间*5
#define AUDIO_TIME_BASE 1000000
#define VIDEO_TIME_BASE 1000000

int main(int argc, char* argv[])
{
    if (argc != 4) {
        printf("usage -> exe in.yuv in.pcm out.mp4");
        return -1;
    }

    const char* in_yuv_name = argv[1];
    const char* in_pcm_name = argv[2];
    const char* out_mp4_name = argv[3];
    FILE* in_yuv_fd = NULL;
    FILE* in_pcm_fd = NULL;

    in_yuv_fd = fopen(in_yuv_name, "rb");
    if (!in_yuv_fd) {
        printf("Failed to open %s file\n", in_yuv_fd);
        return -1;
    }

    in_pcm_fd = fopen(in_pcm_name, "rb");
    if (!in_pcm_fd) {
        printf("Failed to open %s file\n", in_pcm_fd);
        return -1;
    }

    int ret = 0;
    // 初始化编码器,包括视频,音频编码器
    int yuv_width = YUV_WIDTH;
    int yuv_height = YUV_HEIGHT;
    int yuv_fps = YUV_FPS;
    int video_bit_rate = VIDEO_BIT_RATE;
    VideoEncoder video_encoder;
    ret = video_encoder.InitH264(yuv_width, yuv_height, yuv_fps, video_bit_rate);
    if (ret < 0) {
        printf("video_encoder.InitH264 failed\n");
        return -1;
    }

    int y_frame_size = yuv_width * yuv_height;
    int u_frame_size = yuv_width * yuv_height / 4;
    int v_frame_size = yuv_width * yuv_height / 4;
    int yuv_frame_size = y_frame_size * u_frame_size * v_frame_size;
    uint8_t* yuv_frame_buf = (uint8_t*)malloc(yuv_frame_size);
    if (!yuv_frame_buf) {
        printf("malloc(yuv_frame_size\n");
        return -1;
    }

    int pcm_channels = PCM_CHANNELS;
    int pcm_sample_rate = PCM_SAMPLE_RATE;
    int pcm_sample_format = AV_SAMPLE_FMT_FLTP;
    int audio_bit_rate = AUDIO_BIT_RATE;
    int pcm_frame_size = av_get_bytes_per_sample((AVSampleFormat)pcm_sample_format);
    AudioEncoder audio_encoder;
    ret = audio_encoder.InitAAC(pcm_channels, pcm_sample_rate, audio_bit_rate);
    if (ret < 0) {
        printf("audio_encoder.InitAAC failed\n");
        return -1;
    }

    uint8_t* pcm_frame_buf = (uint8_t*)malloc(pcm_frame_size);

    // 这里需要进行一下重采样 将 S16 转化为 FLTP
    AudioResampler audio_resampler;
    ret = audio_resampler.InitFromS16ToFLTP(pcm_channels, pcm_sample_rate,
                                            audio_encoder.GetChannels(), audio_encoder.GetSampleFormat());
    if (ret < 0) {
        printf("audio_resampler.InitFromS16ToFLTP failed\n");
        return -1;
    }

    Muxer mp4_muxer;
    ret = mp4_muxer.Init(out_mp4_name);
    if (ret < 0) {
        printf("mp4_muxer.Init failed\n");
        return -1;
    }

    // 将流添加到封装器中
    ret = mp4_muxer.AddStream(video_encoder.GetCodecContext());
    if (ret < 0) {
        printf("mp4_muxer.AddStream video failed\n");
        return -1;
    }


    ret = mp4_muxer.AddStream(audio_encoder.GetCodecContext());
    if (ret < 0) {
        printf("mp4_muxer.AddStream video failed\n");
        return -1;
    }

    ret = mp4_muxer.Open();
    if (ret < 0) {
        return -1;
    }

    ret = mp4_muxer.SendHeader();
    if (ret < 0) {
        return -1;
    }

    int64_t audio_time_base = AUDIO_TIME_BASE;
    int64_t video_time_base = VIDEO_TIME_BASE;
    double audio_pts = 0;
    double video_pts = 0;
    double audio_frame_duration = 1.0 * audio_encoder.GetFrameSize()/pcm_sample_rate*audio_time_base;
    double video_frame_duration = 1.0/yuv_fps * video_time_base;

    int audio_finish = 0;
    int video_finish = 0;

    size_t read_len = 0;
    AVPacket* packet = NULL;
    int audio_index = mp4_muxer.GetAudioStreamIndex();
    int video_index = mp4_muxer.GetVideoStreamIndex();

    while(1) {
        if (audio_finish && video_finish)
            break;

        printf("apts:%0.0lf, vpts:%0.0lf\n", audio_pts/1000, video_pts/1000);
        if ((video_finish != 1 && audio_pts > video_pts)
                || (video_finish != 1 && audio_finish ==1)) {
            read_len = fread(yuv_frame_buf, 1, yuv_frame_size, in_yuv_fd);
            if (read_len < yuv_frame_size) {
                video_finish = 1;
                printf("fread yuv_frame_buf finish\n");
            }

            if (video_finish != 1) {
                packet = video_encoder.Encode(yuv_frame_buf, yuv_frame_size, video_index,
                                              video_pts, video_time_base);
            }
            else {
                packet = video_encoder.Encode(NULL, 0, video_index,
                                              video_pts, video_time_base);
            }
            video_pts += video_frame_duration; // 叠加pts
            if (packet) {
                mp4_muxer.SendPacket(packet);
            }
        }
        else if (audio_finish != 1) {
            read_len = fread(pcm_frame_buf, 1, pcm_frame_size, in_pcm_fd);
            if (read_len < pcm_frame_size) {
                audio_finish = 1;
                printf("fread pcm_frame_buf finish\n");
            }

            if (audio_finish != 1) {
                AVFrame* fltp_frame = AllocFltpPcmFrame(pcm_channels, audio_encoder.GetFrameSize());
                ret = audio_resampler.ResampleFromS16ToFLTP(pcm_frame_buf, fltp_frame);

                packet = audio_encoder.Encode(fltp_frame, audio_index,
                                              audio_pts, audio_time_base);
                FreePcmFrame(fltp_frame);
            }
            else {
                packet = audio_encoder.Encode(NULL, audio_index,
                                              audio_pts, audio_time_base);
            }
            audio_pts += audio_frame_duration;
            if (packet) {
                mp4_muxer.SendPacket(packet);
            }
        }
    }
    ret = mp4_muxer.SendTrailer();
    if (ret < 0) {
        printf("mp4_muxer.SendTrailer failed\n");
    }

    printf("write mp4 finish\n");

    if (yuv_frame_buf)
        free(yuv_frame_buf);
    if (pcm_frame_buf)
        free(pcm_frame_buf);
    if (in_yuv_fd)
        fclose(in_yuv_fd);
    if (in_pcm_fd)
        fclose(in_pcm_fd);

    return 0;
}