音视频开发35 FFmpeg 编码- 将YUV 和 pcm合成一个mp4文件

一 程序的目的

/***
 *该程序的目的是:
 * 将 一个pcm文件 和 一个 yuv文件,合成为一个 0804_out.mp4文件
 * pcm文件和yuv文件是从哪里来的呢?是从 sound_in_sync_test.mp4 文件中,使用ffmpeg命令 抽取出来的。
 * 这样做的目的是为了对比前后两个mp4(sound_in_sync_test.mp4  和 0804_out.mp4 ) 文件。
 *
 * 1. 从sound_in_sync_test.mp4 文件 中抽取 pcm命令如下:
 * ffmpeg -i sound_in_sync_test.mp4 -vn -ar 44100 -ac 2 -f s16le 44100_2_s16le.pcm
 * -vn 表示不处理视频
 *
 * 2. 从sound_in_sync_test.mp4 文件 中抽取 yuv命令如下:
 *
 * ffmpeg -i sound_in_sync_test.mp4 -pix_fmt yuv420p 720x576_yuv420p.yuv
 *
 * 3.播放测试
 *  对于 pcm 数据
 ffplay -ac 2 -ar 44100 -f s16le 44100_2_s16le.pcm
 *  对于 YUV 数据
 ffplay -pixel_format yuv420p -video_size 720x576 -framerate 25  720x576_yuv420p.yuv
 ***/

原始 sound_in_sync_test.mp4 的信息

二 流程 以及 思路

整体思路是这样的:

1. 将video 的yuv数据,变成avframe,然后变成avpacket,

2. 然后需要将avpacket 通过  av_write_frame(this->_avformatContext,avpacket); 发送出去,那么这就需要有 avformatContext。顺便要有avstream,

3. 对于video 来pcm数据来说,应该还需要 audioresample

设计思路

1. 视频编码相关类 videoencoder.h

///设计思路:该函数的作用是,将yuv_data数据存储到 avframe中,然后将avframe中的数据发送到 编码器上下文,经过编码后变成 avpacket。

///核心函数1:初始化h264,要初始化h264,核心是找到编码器;初始化编码器上下文;设置编码器上下文参数;而设置编码器参数,如下的5个参数都需要
/// yuv视频的宽 yuvwidth,yuv视频的高 yuvheight, yuv视频格式 AVPixelFormat yuvpix_fmt, yuv视频fps int yuvfps,视频 比特率 video_bit_rate
///     int InitH264(int yuvwidth, int yuvheight, AVPixelFormat yuvpix_fmt, int yuvfps, int video_bit_rate);
/// 在初始化中,我们还可以定义一个类变量avframe,顺便设定avframe的参数,为了后续方便使用 avframe 做准备。
/// 此方法中和 audio 不同的一点是,会设置 avcodecContext的timebase为1000000,这是因为

///核心函数2:将yuv数据变成 avpacket
///第一个参数为 yuv_data,是一张yuv数据的指针,
/// 第二个参数为 第一个参数yuv_data的大小
/// 第三个参数为 avstream中 video_index,这个参数从 avstream中获得,通过muxer内部变量传递过来,作用是 给avpacket设置 video_index。
/// 第四个参数为 yuv数据一帧图片需要花费的时间,当yuv_fps为25时候,那么一帧数据显示的时刻就是 1/25秒,也就是1000000 * (1/25) 微秒,注意时间。
/// 那么第一张yuv图片 pts 就是 40 000  微秒,
///    第二章yuv图片 pts 就是 80 000  微秒,
/// 第四个参数 pts 是yuv数据的 读取到这时候的时间,参考调用时候 double video_frame_duration = 1.0/yuv_fps * video_time_base;
/// 第五个参数为 yuv数据的时间基,我们设定为 1000000
/// 第四个参数和第五个参数的目的是:使用 yuv的pts  和 yuv 的时间基 转化成 avframe的 pts,这里需要使用到 avcodecContex的timebase,这个avcodecContext 的timebase 是在前面 InitH264 函数中设定的。
/// 注意,video的avcodecContext的timebase 和 audio 的avcodecContext 的timebase 获得方式是不同的,
/// audio 的avcodecContext 的timebase 是 ffmpeg 中在avcodec_open2方法中设定的,video的avcodecContext 的timebase 是程序员自己设定的
/// 然后存储于到 最后一个参数 vector<AVPacket *> 中
/// return 小于0没有packet
/// int videoencoder::Encode(uint8_t *yuv_data, int yuv_size,int stream_index, int64_t pts, int64_t time_base,std::vector<AVPacket *> &packets)


 

videoencoder.h 代码

#ifndef VIDEOENCODER_H
#define VIDEOENCODER_H


///设计思路:该函数的作用是,将yuv_data数据存储到 avframe中,然后将avframe中的数据发送到 编码器上下文,经过编码后变成 avpacket。

///核心函数1:初始化h264,要初始化h264,核心是找到编码器;初始化编码器上下文;设置编码器上下文参数;而设置编码器参数,如下的5个参数都需要
/// yuv视频的宽 yuvwidth,yuv视频的高 yuvheight, yuv视频格式 AVPixelFormat yuvpix_fmt, yuv视频fps int yuvfps,视频 比特率 video_bit_rate
///     int InitH264(int yuvwidth, int yuvheight, AVPixelFormat yuvpix_fmt, int yuvfps, int video_bit_rate);
/// 在初始化中,我们还可以定义一个类变量avframe,顺便设定avframe的参数,为了后续方便使用 avframe 做准备。
/// 此方法中和 audio 不同的一点是,会设置 avcodecContext的timebase为1000000,这是因为

///核心函数2:将yuv数据变成 avpacket
///第一个参数为 yuv_data,是一张yuv数据的指针,
/// 第二个参数为 第一个参数yuv_data的大小
/// 第三个参数为 avstream中 video_index,这个参数从 avstream中获得,通过muxer内部变量传递过来,作用是 给avpacket设置 video_index。
/// 第四个参数为 yuv数据一帧图片需要花费的时间,当yuv_fps为25时候,那么一帧数据显示的时刻就是 1/25秒,也就是1000000 * (1/25) 微秒,注意时间。
/// 那么第一张yuv图片 pts 就是 40 000  微秒,
///    第二章yuv图片 pts 就是 80 000  微秒,
/// 第四个参数 pts 是yuv数据的 读取到这时候的时间,参考调用时候 double video_frame_duration = 1.0/yuv_fps * video_time_base;
/// 第五个参数为 yuv数据的时间基,我们设定为 1000000
/// 第四个参数和第五个参数的目的是:使用 yuv的pts  和 yuv 的时间基 转化成 avframe的 pts,这里需要使用到 avcodecContex的timebase,这个avcodecContext 的timebase 是在前面 InitH264 函数中设定的。
/// 注意,video的avcodecContext的timebase 和 audio 的avcodecContext 的timebase 获得方式是不同的,
/// audio 的avcodecContext 的timebase 是 ffmpeg 中在avcodec_open2方法中设定的,video的avcodecContext 的timebase 是程序员自己设定的
/// 然后存储于到 最后一个参数 vector<AVPacket *> 中
/// return 小于0没有packet
/// int videoencoder::Encode(uint8_t *yuv_data, int yuv_size,int stream_index, int64_t pts, int64_t time_base,std::vector<AVPacket *> &packets)








#include "iostream"
using namespace std;
extern "C" {
    #include "libavutil/avassert.h" // include 后面<> 表示会从标准库路径中查找指定的文件,""表示从当前当前目录(即包含 #include 指令的文件所在的目录)中查找指定的文件
    #include "libavutil/channel_layout.h"
    #include "libavutil/opt.h"
    #include "libavutil/mathematics.h"
    #include "libavutil/timestamp.h"
    #include "libswscale/swscale.h"
    #include "libswresample/swresample.h"
    #include "libavutil/error.h"
    #include "libavutil/common.h"
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavutil/imgutils.h"
}
#include <vector>

class videoencoder
{
public:
    videoencoder();
    ~videoencoder();
    //该函数的作用是 找到h264编码器,通过h264编码器上下文,打开h264编码器上下文,设置h264编码器上下文参数,分配avframe,以及设置avframe参数
    int InitH264(int yuvwidth, int yuvheight, AVPixelFormat yuvpix_fmt, int yuvfps, int video_bit_rate);
    void DeInit();

    ///第一个参数为 yuv_data,是yuv数据的指针,
    /// 第二个参数为 第一个参数yuv_data的大小
    /// 第三个参数为 avstream中 video_index
    /// 第四个参数为 yuv数据一帧图片需要花费的时间,当yuv_fps为25时候,那么一帧数据显示的时刻就是 1/25秒,也就是1000000 * (1/25) 微秒,注意时间。
    /// 那么第一张yuv图片 pts 就是 40 000  微秒,
    ///    第二章yuv图片 pts 就是 80 000  微秒,
    /// 第四个参数 参考调用时候 double video_frame_duration = 1.0/yuv_fps * video_time_base;
    /// 第五个参数为 时间基 1000000
    /// 该函数的作用是,将yuv_data数据存储到 avframe中,然后将avframe中的数据发送到 编码器上下文,经过编码后变成 avpacket。返回该avpacket
    AVPacket *Encode(uint8_t *yuv_data, int yuv_size,
                     int stream_index, int64_t pts, int64_t time_base);

    ///第一个参数为 yuv_data,是yuv数据的指针,
    /// 第二个参数为 第一个参数yuv_data的大小
    /// 第三个参数为 avstream中 video_index
    /// 第四个参数为 yuv数据一帧图片需要花费的时间,当yuv_fps为25时候,那么一帧数据显示的时刻就是 1/25秒,也就是1000000 * (1/25) 微秒,注意时间。
    /// 那么第一张yuv图片 pts 就是 40 000  微秒,
    ///    第二章yuv图片 pts 就是 80 000  微秒,
    /// 第四个参数 参考调用时候 double video_frame_duration = 1.0/yuv_fps * video_time_base;
    /// 第五个参数为 时间基 1000000
    /// 该函数的作用是,将yuv_data数据存储到 avframe中,然后将avframe中的数据发送到 编码器上下文,经过编码后变成 avpacket。
    /// 然后存储于到 最后一个参数 vector<AVPacket *> 中
    /// 小于0没有packet
    int Encode(uint8_t *yuv_data, int yuv_size, int stream_index, int64_t pts, int64_t time_base,
               std::vector<AVPacket *> &packets);
    AVCodecContext *GetCodecContext();

private:
    int _width = 0;
    int _height = 0;
    AVPixelFormat _pix_fmt = AV_PIX_FMT_NONE;
    int _fps = 25;  // 这个默认值,也不会用到,所有的参数都会在构造方法中传递真正的参数
    int _bit_rate = 500*1024; //  这个默认值,也不会用到,所有的参数都会在构造方法中传递真正的参数
    int64_t _pts = 0;
    AVCodecContext * _avcodecContext = NULL;
    AVFrame *_avframe = NULL;
    AVDictionary *_avdictionary = NULL;

};

#endif // VIDEOENCODER_H

videoencoder.cpp

#include "videoencoder.h"

videoencoder::videoencoder()
{

}

videoencoder::~videoencoder()
{
    if(this->_avcodecContext) {
        DeInit();
    }
}


int videoencoder::InitH264(int yuvwidth, int yuvheight, AVPixelFormat yuvpix_fmt, int yuvfps, int video_bit_rate)
{
    int ret =0;
    cout <<" videoencoder.InitH264 call yuvwidth = " << yuvwidth
         <<" yuvheight = "<< yuvheight
         <<" yuvpix_fmt = " << yuvpix_fmt
         <<" yuvfps = " <<yuvfps
         <<" video_bit_rate = " << video_bit_rate
         << endl;

    //1.使用VideoEncoder内部的变量记住 传递进来的值

    this->_width = yuvwidth;
    this->_height = yuvheight;
    this->_pix_fmt = yuvpix_fmt;
    this->_fps = yuvfps;
    this->_bit_rate = video_bit_rate;

    //2.找到视频编码器
    const AVCodec * avcodec =  avcodec_find_encoder(AV_CODEC_ID_H264);
    if(avcodec == nullptr){
        ret = -1;
        cout<<" func InitAAC error because avcodec_find_encoder(AV_CODEC_ID_H264) error "<<endl;
        return ret;
    }

    //3.通过视频编码器找到视频编码器上下文
    this->_avcodecContext = avcodec_alloc_context3(avcodec);
    if(_avcodecContext == nullptr){
        ret = -1;
        cout<<" func InitAAC error because avcodec_alloc_context3(avcodec) error "<<endl;
        return ret;
    }


    //3.1 设定 视频编码器上下文 的参数,这里除了要设定 三要素之外,需要设置 flag 的值为 AV_CODEC_FLAG_GLOBAL_HEADER

    _avcodecContext->width = yuvwidth;
    _avcodecContext->height = yuvheight;
    _avcodecContext->pix_fmt = yuvpix_fmt;
    _avcodecContext->framerate = {yuvfps, 1};


    ///AV_CODEC_FLAG_GLOBAL_HEADER参数相关--中文翻译:将全局标头放置在extradata中,而不是每个关键帧中。
    ///这里要明白为什么加这个参数,需要知道如下的两个知识点:
    /// 1. mp4文件中的 aac 是不带 adst header的,因此我们在将 aac 合成为mp4的时候,不能给每个aac帧的前面加 adst header
    /// 2. AV_CODEC_FLAG_GLOBAL_HEADER 参数的含义就是:将全局标头放置在extradata中,而不是每个关键帧中。,对于aac来说,在每一帧的前面不加 adst header
    /// 这里扩展一下h264,对于h264,有Annexb 和 AVCC 两种存储模式,MP4中的存储的是h264是AVCC格式的,AVCC格式是只有一个头文件在最前面,后面的都是h264纯数据,因此,h264编码成mp4的时候,应该也需要添加 AV_CODEC_FLAG_GLOBAL_HEADER这个标志flag
    _avcodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    // 视频的 如果不主动设置:The encoder timebase is not set
    _avcodecContext->time_base = {1, 1000000};   // 单位为微妙


    ///设置比特率
    _avcodecContext->bit_rate = video_bit_rate;

    ///设置 gop 的大小 和 fps一致。gop size 可以是fps的整数倍
    _avcodecContext->gop_size = yuvfps;

    //设置所有的 图像没有b帧
    _avcodecContext->max_b_frames = 0;

    //设置 aac 额外的参数,通过 _avdictionary设置
    //    av_dict_set(&_avdictionary, "tune", "zerolatency", 0);


    //4.打开编码器上下文
    ret = avcodec_open2(_avcodecContext, NULL, &_avdictionary);
    if(ret != 0) {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof(errbuf) - 1);
        printf("avcodec_open2 failed:%s\n", errbuf);
        return -1;
    }


    //5.分配 avframe内存,为什么要在这里分配呢?

    this->_avframe = av_frame_alloc();
    if(!_avframe) {
        printf("av_frame_alloc failed\n");
        return -1;
    }
    _avframe->width = _width;
    _avframe->height = _height;
    _avframe->format = _avcodecContext->pix_fmt;
    printf("Inith264 success\n");

    return ret;


}

void videoencoder::DeInit()
{
    if(this->_avcodecContext) {
        avcodec_free_context(&_avcodecContext);
    }
    if(this->_avframe) {
        av_frame_free(&_avframe);
    }
    if(this->_avdictionary) {
        av_dict_free(&_avdictionary);
    }
}



///第一个参数为 yuv_data,是yuv数据的指针,
/// 第二个参数为 第一个参数yuv_data的大小
/// 第三个参数为 avstream中 video_index
/// 第四个参数为 yuv数据一帧图片需要花费的时间,当yuv_fps为25时候,那么一帧数据显示的时刻就是 1/25秒,也就是1000000 * (1/25) 微秒,注意时间。
/// 那么第一张yuv图片 pts 就是 40 000  微秒,
///    第二章yuv图片 pts 就是 80 000  微秒,
/// 第四个参数 参考调用时候 double video_frame_duration = 1.0/yuv_fps * video_time_base;
/// 第五个参数为 时间基 1000000
/// 该函数的作用是,将yuv_data数据存储到 avframe中,然后将avframe中的数据发送到 编码器上下文,经过编码后变成 avpacket。
AVPacket *videoencoder::Encode(uint8_t *yuv_data,
                               int yuv_size,
                               int stream_index,
                               int64_t pts,
                               int64_t time_base)
{
    if(!this->_avcodecContext) {
        printf("codec_ctx_ null\n");
        return NULL;
    }
    int ret = 0;
    /// 时间基 转换, 当第一帧的时候: pts = 40000;
    /// 视频编码器上下文的 time_base是user自己设定的,音频编码器上下文中的timebase是 ffmpeg在 avcodec_open2 方法中实现的。
    /// 但是视频编码器上下文的time_base并没有在 ffmpeg源码中实现,需要user自己设置,我们在前面的代码中已经设置了
    /// 前面设置的timebase为1,1000000   :    _avcodecContext->time_base = {1, 1000000}
    /// 计算值:第一帧时候重新计算的pts的值为:  (40000 *(1/1000000)) / (1/1000000) = 40000
    /// 那为什么还要计算呢?
    pts = av_rescale_q(pts, AVRational{1, (int)time_base}, _avcodecContext->time_base);
    this->_avframe->pts = pts;


    //将yuv_data数据填充到avframe中,调用的时候有两种方式,一种是yuv_data真的有数据,一种是yuv_data的值为nullptr,目的是刷新编码器上下文

    if(yuv_data){
        int ret_size = av_image_fill_arrays(this->_avframe->data, this->_avframe->linesize,
                                            yuv_data, (AVPixelFormat)this->_avframe->format,
                                            this->_avframe->width, this->_avframe->height, 1);
        if(ret_size != yuv_size) {
            printf("ret_size:%d != yuv_size:%d -> failed\n", ret_size, yuv_size);
            return NULL;
        }
        ret = avcodec_send_frame(this->_avcodecContext, this->_avframe);

    } else {
        ret = avcodec_send_frame(this->_avcodecContext, NULL);
    }


    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(this->_avcodecContext, packet);
    if(ret != 0) {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof(errbuf) - 1);
        printf("h264 avcodec_receive_packet failed:%s\n", errbuf);
        av_packet_free(&packet);
        return NULL;
    }
    packet->stream_index = stream_index;
    return packet;
}


int videoencoder::Encode(uint8_t *yuv_data, int yuv_size,
                         int stream_index, int64_t pts, int64_t time_base,
                         std::vector<AVPacket *> &packets)
{
    if(!this->_avcodecContext) {
        printf("codec_ctx_ null\n");
        return -1;
    }
    int ret = 0;

    pts = av_rescale_q(pts, AVRational{1, (int)time_base}, this->_avcodecContext->time_base);
    this->_avframe->pts = pts;
    if(yuv_data) {
        int ret_size = av_image_fill_arrays(_avframe->data, _avframe->linesize,
                                            yuv_data, (AVPixelFormat)_avframe->format,
                                            _avframe->width, _avframe->height, 1);
        if(ret_size != yuv_size) {
            printf("ret_size:%d != yuv_size:%d -> failed\n", ret_size, yuv_size);
            return -1;
        }
        ret = avcodec_send_frame(this->_avcodecContext, this->_avframe);
    } else {
        ret = avcodec_send_frame(this->_avcodecContext, NULL);
    }

    if(ret != 0) {
        char errbuf[1024] = {0};
        av_strerror(ret, errbuf, sizeof(errbuf) - 1);
        printf("avcodec_send_frame failed:%s\n", errbuf);
        return -1;
    }
    while(1)
    {
        AVPacket *packet = av_packet_alloc();
        ret = avcodec_receive_packet(this->_avcodecContext, packet);
        packet->stream_index = stream_index;
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            ret = 0;
            av_packet_free(&packet);
            break;
        } else if (ret < 0) {
            char errbuf[1024] = {0};
            av_strerror(ret, errbuf, sizeof(errbuf) - 1);
            printf("h264 avcodec_receive_packet failed:%s\n", errbuf);
            av_packet_free(&packet);
            ret = -1;
        }
        printf("h264 pts:%lld\n", packet->pts);
        packets.push_back(packet);
    }
    return ret;
}

AVCodecContext *videoencoder::GetCodecContext()
{
    if(this->_avcodecContext){
        return this->_avcodecContext;
    }
    return nullptr;
}

2.重采样编码相关类 audioresample.h

audioresample.h

#ifndef AUDIORESAMPLER_H
#de
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值