• 音视频学习笔记——实现PCM和H264合成MP4功能


    本文主要记录实现PCMH264合成MP4功能的整个框架,各个模块中FFmpegapi使用流程,便于后续学习和复盘。


    本专栏知识点是通过<零声教育>的音视频流媒体高级开发课程进行系统学习,梳理总结后写下文章,对音视频相关内容感兴趣的读者,可以点击观看课程网址:零声教育


    1. MP4合成

    MP4合成包括音频视频以及封装器3部分,框架如下图所示。
    在这里插入图片描述

    2. muxer类

    首先,在h.文件中声明相关函数和参数。
    在这里插入图片描述
    以下是各个函数中重要的api使用。
    Init():初始化

    	//初始化一个用于输出的AVFormatContext结构体。其声明位于libavformat\avformat.h,
    	avformat_alloc_output_context2(&fmt_ctx_,NULL,NULL,url)//
    
    • 1
    • 2

    DeInit():资源释放

    	//关闭打开的流
    	avformat_close_input(&fmt_ctx_); 
    
    • 1
    • 2

    *AddStream(AVCodecContext codec_ctx):创建流

    	//创建流
    	AVStream *st = avformat_new_stream(fmt_ctx_,NULL);
    	//从编码器上下文复制
        avcodec_parameters_from_context(st->codecpar, codec_ctx);
        //打印输入流 
        av_dump_format(fmt_ctx_, 0, url_.c_str(), 1);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    SendHeader():写header文件

    	//分配一个 stream 的私有数据而且写 stream 的 header 到一个输出的媒体文件。
    	int ret = avformat_write_header(fmt_ctx_, NULL); 
    
    • 1
    • 2

    SendPacket():写packet,与

     	AVRational src_time_base; //编码后的包
        AVRational dst_time_vase; //mp4输出文件对应流的time_base
         //时间基转换
        packet->pts = av_rescale_q(packet->pts,src_time_base,dst_time_vase);
        packet->dts = av_rescale_q(packet->dts,src_time_base,dst_time_vase);
        packet->duration = av_rescale_q(packet->duration,src_time_base,dst_time_vase);
        
        ret = av_interleaved_write_frame(fmt_ctx_,packet); //不是立即写入文件,内部缓存,主要是对pts进行排序
        //ret = av_write_frame(fmt_ctx_,packet);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    SendTrailer():输出文件尾

    	//用于输出文件尾
    	av_write_trailer(fmt_ctx_);
    
    • 1
    • 2

    在对音频和视频进行编码得到数据流后,用muxer类实现将音视频流编码成mp4格式。

    3. audioencoder类

    h.文件中声明相关函数和参数。
    在这里插入图片描述
    主要函数实现:

    	1.初始化AAC:InitAAC(int channels, int sample_rate, int bit_rate);
    	2.编码:*Encode(AVFrame *frame, int stream_index, int64_t pts, int64_t time_base);
    	3.返回一些常用的参数
    		int GetFrameSize(); //获取一帧数据,每个通道需要多少个采样点
    	    int GetSampleFormat();  //编码器需要的采样格式
    	    int GetChannels(); //获取通道数
    	    int GetSampleRate(); //获取采样率
    	    AVCodecContext *GetCodecContext();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    InitAAC():初始化
    参数:

    • pcm_channels:pcm通道数
    • pcm_sample_rate:pcm样本采样率
    • audio_bit_rate:音频比特率
    	//1.avcodec_find_encoder() 用于查找 FFmpeg 的编码器,
    	AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_AAC);//获取的是默认的AAC。
    	//2.avcodec_alloc_context3()主要是创建了 AVCodecContext ,并给结构体参数赋予初值。
    	//初值设置主要分成两块,1. 所有编码器都相同的部分;2.每个编码器独有的参数设置。
    	codec_ctx_ = avcodec_alloc_context3(codec);
    	//配置参数
    	codec_ctx_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; //编码后的aac文件不会带ADTS Header
        codec_ctx_->bit_rate = bit_rate_;
        codec_ctx_->sample_rate = sample_rate_;
        codec_ctx_->sample_fmt = AV_SAMPLE_FMT_FLTP;
        codec_ctx_->channels = channels_;
        codec_ctx_->channel_layout = av_get_default_channel_layout(codec_ctx_->channels);
        //3.初始化一个视音频编解码器的 AVCodecContext
        int ret = avcodec_open2(codec_ctx_, NULL, NULL);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Encode():编码
    参数:

    • frame:帧
    • stream_index:数据流序号
    • pts:显示时间
    • time_base:时间基
    	//时间转换
    	frame->pts = av_rescale_q(pts,AVRational{1, (int)time_base}, codec_ctx_->time_base);
    	//1.avcodec_send_frame()首先判断编码器有没打开、是否为编码器。
    	//发送AVFrame
    	int ret = avcodec_send_frame(codec_ctx_,frame);
    	//av_packet_alloc(),申请的AVPacket*
    	AVPacket *packet = av_packet_alloc();
    	//接受packet
        ret = avcodec_receive_packet(codec_ctx_,packet);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    4. videoencoder类

    实现与audioencoder类相似,但细节处不同。
    在这里插入图片描述

    1.初始化H264:int InitH264(int width, int height, int fps, int bit_rate);
    2.编码:AVPacket *Encode(uint8_t *yuv_data, int yuv_size,
                         int stream_index, int64_t pts, int64_t time_base);
    
    • 1
    • 2
    • 3

    InitH264():

    • width_ :画面宽度
    • height_ :画面高度
    • fps_ :帧率
    • bit_rate_ :比特率
    	//1.avcodec_find_encoder() 用于查找 FFmpeg 的编码器,
    	AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    	//2.avcodec_alloc_context3()主要是创建了 AVCodecContext ,并给结构体参数赋予初值。
    	//初值设置主要分成两块,1. 所有编码器都相同的部分;2.每个编码器独有的参数设置。
    	codec_ctx_ = avcodec_alloc_context3(codec);
    	//配置参数
    	codec_ctx_->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; //编码后的aac文件不会带ADTS Header
        codec_ctx_->bit_rate = bit_rate_;
        codec_ctx_->width = width_;
        codec_ctx_->height = height_;
        codec_ctx_->framerate = {fps, 1};
        codec_ctx_->time_base = {1,1000000}; //单位为微妙
        codec_ctx_->gop_size =fps_;
        codec_ctx_->max_b_frames =0; //B帧数量
        codec_ctx_->pix_fmt = AV_PIX_FMT_YUV420P;
        //3.初始化一个视音频编解码器的 AVCodecContext
        int ret = avcodec_open2(codec_ctx_, NULL, NULL); 
        frame_ =av_frame_alloc();//视频与音频实现不同之处,需要声明下frame帧
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Encode():
    参数:

    • yuv_data:存放yuv帧的buffer
    • yuv_size:yuv帧的大小
    • stream_index:数据流序号
    • pts:显示时间
    • time_base:时间基
    	//时间转换
    	frame->pts = av_rescale_q(pts,AVRational{1, (int)time_base}, codec_ctx_->time_base);
    	//不同之处,将yuv填充成需要的格式
    	int ret_size = av_image_fill_arrays(frame_->data, frame_->linesize,
                                 yuv_data, (AVPixelFormat)frame_->format,
                                 frame_->width,frame_->height,1);
    
    	int ret = avcodec_send_frame(codec_ctx_,frame);
    	AVPacket *packet = av_packet_alloc();
        ret = avcodec_receive_packet(codec_ctx_,packet); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    5. audioresampler类

    在这里插入图片描述

    1.初始化S16转FLTP:int InitFormS16ToFLTP(int in_channles,int in_sample_rate, int out_channles, int out_sample_rate);
    2.重采样:int ResampleFormS16ToFLTP(uint8_t *in_data, AVFrame *out_frame);
    
    • 1
    • 2

    InitFormS16ToFLTP():
    参数:

    • in_channles_:输入通道数
    • in_sample_rate_:输入采样率
    • out_channles_:输出通道数
    • out_sample_rate_:输出采样率
    	//重采样参数设置
    	ctx_ = swr_alloc_set_opts(ctx_,
    	                              av_get_default_channel_layout(out_channles_),
    	                              AV_SAMPLE_FMT_FLTP,
    	                              out_sample_rate_,
    	                              av_get_default_channel_layout(in_channles_),
    	                              AV_SAMPLE_FMT_S16,
    	                              in_sample_rate_,
    	                              0,
    	                              NULL);
    	//初始化一个重采样                            
    	int ret = swr_init(ctx_);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    ResampleFormS16ToFLTP():
    参数:

    • in_data: pcm帧buffer
    • out_fream: fktp帧
    int AudioResampler::ResampleFormS16ToFLTP(uint8_t *in_data, AVFrame *out_frame)
    {
        const uint8_t *indata[AV_NUM_DATA_POINTERS] = {0};
        indata[0] = in_data;
        //进行转换
        int samples = swr_convert(ctx_, out_frame->data, out_frame->nb_samples,
                              indata,out_frame->nb_samples);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    6. mian函数:

    实现流程:
    1. 打开yuv、pcm文件
2. 初始化编码器,包括视频、音频编码器,分配 yuv、pcm的帧buffer,初始化重采样
3. mp4初始化,包括新建流,open io, send header
4. 处理时间戳,在while循环读取yuv、pcm进行编码然后发送给mp4 muxer
5. 释放资源

    6.1 打开yuv、pcm文件

    	// 打开YUV文件
    	n_yuv_fd = fopen(in_yuv_name, "rb");
    	// 打开PCM文件
        in_pcm_fd = fopen(in_pcm_name, "rb");
    
    • 1
    • 2
    • 3
    • 4

    6.2 初始化编码器,包括视频、音频编码器,分配yuv、pcm的帧buffer

    	//2.1 初始化video
        //初始化编码器
        video_encoder.InitH264(yuv_width, yuv_height, yuv_fps, video_bit_rate);
        //分配 yuv buf
        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);
    
    
    	//2.2 初始化 audio
        //初始化音频编码器
        audio_encoder.InitAAC(pcm_channels,pcm_sample_rate, audio_bit_rate);
        //分配pcm buf
        // pcm_frame_size = 单个字节点占用的字节 * 通道数量 * 每个通道有多少给采样点
        int pcm_frame_size = av_get_bytes_per_sample((AVSampleFormat)pcm_sample_format)
                *pcm_channels * audio_encoder.GetFrameSize();
        uint8_t *pcm_frame_buf = (uint8_t *)malloc(pcm_frame_size);
    
    
    	//2.3 初始化重采样
    	AudioResampler audio_resampler;
        audio_resampler.InitFormS16ToFLTP(pcm_channels, pcm_sample_rate,
                                          audio_encoder.GetChannels(),audio_encoder.GetSampleRate());
    	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    6.3 mp4初始化,包括新建流,open io, send header

        Muxer mp4_muxer;
        mp4_muxer.Init(out_mp4_name);
        //创建视频流、音频流
        mp4_muxer.AddStream(video_encoder.GetCodecContext());
        mp4_muxer.AddStream(audio_encoder.GetCodecContext());
        mp4_muxer.Open();
        mp4_muxer.SendHeader();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    6.4.在while循环读取yuv、pcm进行编码然后发送给mp4 muxer

    	//1. 时间戳相关
        int64_t audio_time_base = AUDIO_TIME_BASE;
        int64_t video_time_base = VIDEO_TIME_BASE;
    
        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;
    	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 //audio和video都还有数据,优先audio(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){
                    ret = video_encoder.Encode(yuv_frame_buf,yuv_frame_size, video_index,
                                                  video_pts, video_time_base, packets);
    
                }else{
                    printf("flush video encoder\n");
                    ret = video_encoder.Encode(NULL, 0, video_index,
                                                  video_pts, video_time_base, packets);
                }
                video_pts += video_frame_duration; //叠加pts
                if(ret >= 0){
                    for(int i = 0; i<packets.size(); ++i){
                        ret = mp4_muxer.SendPacket(packets[i]);
                    }
                }
                packets.clear();
            }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.ResampleFormS16ToFLTP(pcm_frame_buf, fltp_frame);
                    if(ret < 0){
                        printf("ResampleFormS16ToFLTP failed\n");
                    }
                    ret = audio_encoder.Encode(fltp_frame, audio_index,
                                                  audio_pts, audio_time_base, packets);
    
                    FreePcmFrame(fltp_frame);
                }else{
                    printf("flush audio encoder\n");
                    ret = audio_encoder.Encode(NULL, audio_index,
                                                  audio_pts, audio_time_base, packets);
                }
                audio_pts += audio_frame_duration; //叠加pts
                if(ret >= 0){
                    for(int i = 0; i<packets.size(); ++i){
                        ret = mp4_muxer.SendPacket(packets[i]);
                    }
                }
                packets.clear();
    
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    注意点
    一、时间基问题
    音频编码中,时间基在avcodec_open2(codec_ctx_, NULL, NULL)执行后,会自动根据所打开的编码器设置改变。
    视频编码,需要自己手动设置。
    如果不主动设置报错: The encoder timebase is not set
    codec_ctx_->time_base = {1,1000000}; //单位为微妙
    2.有很大延迟,需要设置0延迟,进行如下修改。

    	h.定义
    	AVDictionary *dict_ =NULL;
    	cpp修改
    	av_dict_set(&dict_,"tune","zerolatency", 0);
        int ret = avcodec_open2(codec_ctx_, NULL, dict_);
    	//释放内存
    	if(dict_){
            av_dict_free(&dict_);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
  • 相关阅读:
    香港硬防服务器的防御有什么优缺点?
    【字符串函数内功修炼】strncpy + strncat + strncmp(二)
    移动通信网络规划:面、线、点覆盖规划
    如何在 Spring Boot 中提高应用程序的安全性
    HBase 计划外启动 Major Compaction 的原因
    matlab|电动汽车充放电V2G模型
    UVM项目实战(1)
    Unity 脚本单例
    【无标题】
    半年报信号!良品铺子的稳健增长与长期势能
  • 原文地址:https://blog.csdn.net/qq_45087381/article/details/136506088