FFmpeg视频编码的流程详解及参考demo

        本文主要讲解FFmpeg的视频编码的具体流程,API使用。最后再以一个非常简单的demo演示将一个yuv数据文件编码为H264的视频文件,也就是YUV编码为H264。

        FFmpeg的编码也有2套API接口,本文主要围绕编码新接口。其实新旧接口的大体流程是差不多的,只是在最终编码的时候的接口稍微不同。旧接口可以参考雷神大大的文章:最简单的基于FFMPEG的视频编码器(YUV编码为H.264),本文demo也是基本参考雷神的demo进行修改的。

        前面本人也写过一篇关于解码的流程详解:FFmpeg视频解码流程详解及demo,里面有对相关的结构体等进行了一些说明。因此本文就不再阐述结构体相关。就直接进入 正题,编码流程讲解和demo演示。

一、视频编码API调用流程图

        视频编码的API调用流程图如下:

         API接口简单大体讲解如下:

av_register_all():注册FFmpeg所有编解码器。

avformat_alloc_context():初始化输出码流的AVFormatContext。

avio_open():打开输出文件。

av_new_stream():创建输出码流的AVStream。

avcodec_find_encoder():查找编码器。

avcodec_open2():打开编码器。

avformat_write_header():写文件头(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

avcodec_send_frame():编码核心接口新接口,发送一帧视频给编码器。即是AVFrame(存储YUV像素数据)。

avcodec_receive_packet():编码核心接口新接口,接收编码器编码后的一帧视频,AVPacket(存储H.264等格式的码流数据)。

av_write_frame():将编码后的视频码流写入文件。

flush_encoder():输入的像素数据读取完成后调用此函数。用于输出编码器中剩余的AVPacket。

av_write_trailer():写文件尾(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

二、编码过程API调用流程

1、注册各大组件

        这一步是ffmpeg的任何程序的第一步都是需要先注册ffmpeg相关的各大组件的:

    //注册各大组件
    av_register_all();

2、打开yuv文件

        由于yuv文件是没有格式的,是视频原始数据,因此在编码前是需要提前知道它的分辨率等的,在这个简单的demo中,为了方便,先暂时在代码中直接指明。

        同时为了简单验证需要,我们只编码framenum帧,不然若yuv太大,编码太久,可以指定自己想要的多少帧来做验证即可,赋值framenum。

    FILE *in_file = fopen(inputPath, "rb");   //Input raw YUV data
    if(!in_file){
        LOGE(" fopen faile");
        return false;
    }
    int in_w = 448, in_h = 960;                 	//Input data's width and height
    int framenum = 10000;                        	//Frames to encode

3、初始化输出码流的AVFormatContext

        有两种方式,这里我们用的方式一。

        方式一:

    //方式1
    pFormatCtx = avformat_alloc_context();
    //Guess Format
    fmt = av_guess_format(NULL, outputPath, NULL);
    pFormatCtx->oformat = fmt;

        方式二:

    //方式2
    avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file);
    fmt = pFormatCtx->oformat;

4、打开输出文件

    //Open output
    if (avio_open(&pFormatCtx->pb, outputPath, AVIO_FLAG_READ_WRITE) < 0) {
        LOGE("Failed to open output file! \n");
        return false;
    }

5、创建输出码流的AVStream

    video_st = avformat_new_stream(pFormatCtx, 0);

    if (video_st == NULL) {
        return false;
    }

6、查找编码器并打开

        查找编码器之前,需要先指定配置编码器的一些参数:

//Param that must set
    pCodecCtx = video_st->codec;
    //pCodecCtx->codec_id =AV_CODEC_ID_HEVC;
    pCodecCtx->codec_id = fmt->video_codec;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    pCodecCtx->width = in_w;
    pCodecCtx->height = in_h;
    pCodecCtx->bit_rate = 400000;
    pCodecCtx->gop_size = 25;   //I帧间隔

    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;  //time_base一般是帧率的倒数,但不总是
    pCodecCtx->framerate.num = 25;  //帧率
    pCodecCtx->framerate.den = 1;


    AVCodecParameters *codecpar = video_st->codecpar;
    codecpar->codec_id = fmt->video_codec;
    codecpar->width = in_w;
    codecpar->height = in_h;
    codecpar->bit_rate = 400000;
    codecpar->format = AV_PIX_FMT_YUV420P;

    ///AVFormatContext* mFormatCtx
    ///mBitRate   = mFormatCtx->bit_rate;   ///码率存储位置
    ///mFrameRate = mFormatCtx->streams[stream_id]->avg_frame_rate.num;


    //H264
    //pCodecCtx->me_range = 16;
    //pCodecCtx->max_qdiff = 4;
    //pCodecCtx->qcompress = 0.6;
    pCodecCtx->qmin = 1;
    pCodecCtx->qmax = 20;

    //Optional Param
    pCodecCtx->max_b_frames = 0;  //不要B帧

    // Set Option
    AVDictionary *param = 0;
    //H.264
    if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {
        av_dict_set(&param, "preset", "slow", 0);
        //av_dict_set(&param, "tune", "zerolatency", 0);
        //av_dict_set(&param, "profile", "main", 0);
    }
    //H.265
    if (pCodecCtx->codec_id == AV_CODEC_ID_H265) {
        av_dict_set(&param, "preset", "ultrafast", 0);
        av_dict_set(&param, "tune", "zero-latency", 0);
    }

        可以通过av_dump_format()这个API,打印出配置后的参数格式,方便调试查看。

    //Show some Information
    av_dump_format(pFormatCtx, 0, outputPath, 1);

        参数配置好后调用avcodec_find_encoder寻找编码器

    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        LOGE("Can not find encoder!");
        return false;
    }

        最后打开编码器

    if (avcodec_open2(pCodecCtx, pCodec, &param) < 0) {
        LOGE("Failed to open encoder!");
        return false;
    }

7、写文件头

        对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS。

    //写文件头(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。
    avformat_write_header(pFormatCtx, NULL);

8、读取原文件,并进行视频编码

        这一步会一帧一帧读取yuv原文件数据,然后发送给编码器,再通过接收编码器编码后的数据即可拿到。编码的核心代码是

ret = avcodec_send_frame(pCodecCtx, pFrame);
if (ret < 0){
	LOGE("Error sending a frame for encoding\n");
	return false;
}

while (ret >= 0)
{
	ret = avcodec_receive_packet(pCodecCtx, pkt);
	if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
		break;
	}
	else if (ret < 0){
		LOGE("Error during encoding\n");
		break;
	}

	framecnt++;

	av_packet_unref(pkt);
}

9、编码,保存数据,写入文件

        通过av_write_frame写入文件,在获得编码后的数据后写入。

ret = av_write_frame(pFormatCtx, pkt);

        同时为了确保数据齐全,获取到最后数据的时候,需要将缓冲区中的数据清理出来

     //Flush Encoder
    // 输入的像素数据读取完成后调用此函数,用于输出编码器中剩余的AVPacket
    ret = flush_encoder(pFormatCtx, 0);
    if (ret < 0) {
        LOGE("Flushing encoder failed\n");
        return false;
    }

        完整的编码,写入文件流程如下:

    for (int i = 0; i < framenum; i++){
        int ret = 0;
        //Read raw YUV data
        if ((ret = fread(picture_buf, 1, yuv_420_size, in_file)) <= 0){
            LOGE("fread raw data failed\n");
            getc(in_file);
            if(feof(in_file)) {
                LOGE(" -> Because this is the file feof! \n");
                break;
            }
            return false;
        }

        pFrame->data[0] = picture_buf;              // Y
        pFrame->data[1] = picture_buf + y_size;      // U
        pFrame->data[2] = picture_buf + y_size * 5 / 4;  // V
        //PTS
        pFrame->pts = i;

        ret = avcodec_send_frame(pCodecCtx, pFrame);
        if (ret < 0){
            LOGE("Error sending a frame for encoding\n");
            return false;
        }

        while (ret >= 0)
        {
            ret = avcodec_receive_packet(pCodecCtx, pkt);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
                break;
            }
            else if (ret < 0){
                LOGE("Error during encoding\n");
                break;
            }

            framecnt++;
            ret = av_write_frame(pFormatCtx, pkt);
            av_packet_unref(pkt);
        }

    }

    //Flush Encoder
    // 输入的像素数据读取完成后调用此函数,用于输出编码器中剩余的AVPacket
    ret = flush_encoder(pFormatCtx, 0);
    if (ret < 0) {
        LOGE("Flushing encoder failed\n");
        return false;
    }

        其中flush_encoder:

int flush_encoder(AVFormatContext *fmt_ctx, unsigned int stream_index)
{
    int ret;
    int got_frame;
    AVPacket enc_pkt;
    if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities & AV_CODEC_CAP_DELAY)){
        return 0;
    }

    while (1){
        enc_pkt.data = NULL;
        enc_pkt.size = 0;
        av_init_packet(&enc_pkt);
        ret = avcodec_encode_video2(fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                    NULL, &got_frame);

        av_frame_free(NULL);
        if (ret < 0)
            break;
        if (!got_frame) {
            ret = 0;
            break;
        }
        LOGE("Flush Encoder: Succeed to encode 1 frame!\tsize:%5d\n", enc_pkt.size);
        /* mux encoded frame */
        ret = av_write_frame(fmt_ctx, &enc_pkt);
        if (ret < 0)
            break;
    }
    return ret;
}

10、写文件尾

        对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS。

    //Write file trailer
    av_write_trailer(pFormatCtx);

11、收尾,释放相关资源

    //Clean
    if (video_st) {
        avcodec_close(video_st->codec);
        av_free(pFrame);
        av_free(picture_buf);
    }
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
    av_packet_free(&pkt);

    fclose(in_file);

        以上便是yuv编码为H264的整个流程,若是其他编码文件,步骤类似,但是需要调整打开编码器那一步的相关参数。若要做成通用性demo,可以在这一步加上对编码格式的判断,从而进行参数的选择。

三、demo运行

        demo中指定了编码的输入文件和输出文件,输入文件是/sdcar/videoEncodeIn.yuv,输出文件是/sdcard/videoOutput.h264,需要改的话,需要在这里改:

@Override
public void run() {
	String PATH = Environment.getExternalStorageDirectory().getPath();
	//视频编码
	String input = PATH + File.separator + "videoEncodeIn.yuv";
	String output = PATH + File.separator + "videoOutput.h264";
	encode_test(input,1,output);
	Toast.makeText(MainActivity.this, "编码完成,请自行从手机中拉取h264文件等", Toast.LENGTH_SHORT).show();
}

        同时,根据前面介绍,yuv文件是不包含格式的,因为需要我们在代码中提前你要编码的yuv文件的分辨率,也就是宽高,我的测试文件是448*960的:

        至于yuv测试原文件如何获取,可以考虑在上一篇解码的demo中,搞个mp4文件先去解码一下,得到yuv文件,然后再拿来测测编码。

    FILE *in_file = fopen(inputPath, "rb");   //Input raw YUV data
    if(!in_file){
        LOGE(" fopen faile");
        return false;
    }
    int in_w = 448, in_h = 960;    //指明宽高

        运行demo:

         点击按钮开始编码,编码过程可能比较久,界面不会有变化,可以通过看log看过程:      

        编码结束后,界面上会有提示词:“编码完成,请自行从手机中拉取h264文件等”,同时log也能看到"--- video encode finished ---"

        然后我们看到/sdcard/下已经有编码后的文件,看到编码后的文件比原始yuv文件就要小很多:

         我们把它拉取到电脑上,然后通过h264播放工具来播放看看,我用的h264播放工具是Elecard StreamEye:

         打开它,然后选择h264文件,点击右下方的播放,若能正常显示和播放,说明这个文件是标准h264文件没错,说明我们的编码demo运行成功:

         完整例子已经放到github上,如下

 https://github.com/weekend-y/FFmpeg_Android_Demo/tree/master/demo5_videoEncode

猜你喜欢

转载自blog.csdn.net/weekend_y45/article/details/125180948