使用ffmpeg 的liavformat 封装,使用libavcodec encodec,实现个编码器,封装yuv 文件为flv/mp4等格式文件。

   视频编码的过程是解码的逆过程,编码的流程,从数据结构上看就是AVFrame-> AVPacket->AVCodecContext->AVFormatContext。 生成AVPacket 为封装,生成AVFrame 为encodec。其中,AVFormatContext:封装格式上下文结构体,也是统领全局的结构体,保存了视频文件 封装 格式相关信息;AVCodecContext:编码器上下文结构体,保存了视频(音频)编解码相关信息;AVPacket:存储一帧压缩编码数据;AVFrame:存储一帧解码后像素(采样)数据。

   编码过程相对于解码过程复杂点,因为解码不需要关注各种参数,而编码要设置各种复杂的参数,特使是时间戳的关系,这个搞不明白,就会出现帧率不对的情况。下面是雷神总结的流程图:

    使用 liavformat 和 libavcodec 实现编码器-LMLPHP

    他写的代码只有encode,并没有封装过程,本文完成了后面部分,并做了部分修改。源码如下:

#include <stdio.h>
#include <libavutil/opt.h>
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>

//test different codec
#define TEST_H264  1


int main(int argc, char* argv[])
{
	AVCodec *pCodec;
    AVCodecContext *pCodecCtx= NULL;
    int i, ret, got_output;
    FILE *fp_in;
	FILE *fp_out;
    AVFrame *pFrame;
    AVPacket pkt;
    AVStream *video_st;
	int y_size;
	int framecnt=0;
    int framerate=25;

	char filename_in[] = "in_1280x720.yuv"; //输入yuv文件, 必须注明分辨率
	enum AVCodecID codec_id = AV_CODEC_ID_H264;

    // 输出文件名
	char * h264_out = "out.h264";
    char * flv_out = "out.mp4";

    // 输出 format
    AVFormatContext *ofmt_ctx = NULL;

	int in_w=1280, in_h=720;	//对应的分辨率
    int out_w = 1280, out_h = 720;
	int framenum=10000;

    // 注册所有编码器
	av_register_all();

    // 初始化 out format
    avformat_alloc_output_context2(&ofmt_ctx, NULL, NULL, flv_out);
    if (!ofmt_ctx) {
        printf( "Could not create output context\n");
        ret = AVERROR_UNKNOWN;
        return -1 ;
    }

    //Open output file
    if (!(ofmt_ctx->oformat->flags & AVFMT_NOFILE)) {
        ret = avio_open(&ofmt_ctx->pb, flv_out, AVIO_FLAG_WRITE);//打开输出文件。
        if (ret < 0) {
            printf( "Could not open output file '%s'", flv_out);
            return -1;
        }
    }

    video_st = avformat_new_stream(ofmt_ctx, 0);
    // 设置帧率,有的封装格式,这个参数会被后面 avformat_write_header 覆盖

    // 输出 codec 的参数,这里的参数一般ffmpeg 可以parse arg 得到,这里暂时写死
    pCodecCtx = video_st->codec;
    if (!pCodecCtx) {
        printf("Could not allocate video codec context\n");
        return -1;
    }
    pCodecCtx->codec_id = codec_id;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->bit_rate = 5000;
    pCodecCtx->width = out_w;
    pCodecCtx->height = out_h;
    pCodecCtx->time_base.num=1;
	pCodecCtx->time_base.den=25;

    // h264重要参数,不设置会报错
    pCodecCtx->qmin = 10;
	pCodecCtx->qmax = 51;

    pCodecCtx->gop_size = 250;
    pCodecCtx->max_b_frames = 1;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;

    if (codec_id == AV_CODEC_ID_H264)
        av_opt_set(pCodecCtx->priv_data, "preset", "slow", 0);


    //打开 codec 编码器

    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        printf("Codec not found\n");
        return -1;
    }

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        printf("Could not open codec\n");
        return -1;
    }

	//Input raw data
	fp_in = fopen(filename_in, "rb");
	if (!fp_in) {
		printf("Could not open %s\n", filename_in);
		return -1;
	}
	//h264 Output bitstream
	fp_out = fopen(h264_out, "wb");
	if (!fp_out) {
		printf("Could not open %s\n", h264_out);
		return -1;
	}

    // AVDictionary* opt = NULL;
    // av_dict_set_int(&opt, "video_track_timescale", 25, 0);
    //Write file header
    if (avformat_write_header(ofmt_ctx, NULL) < 0) {
        printf( "Error occurred when opening output file\n");
        return -1;
    }

    // init pFrame
    pFrame = av_frame_alloc();
    if (!pFrame) {
        printf("Could not allocate video frame\n");
        return -1;
    }
    pFrame->format = pCodecCtx->pix_fmt;
    pFrame->width  = in_w;
    pFrame->height = in_h;

    ret = av_image_alloc(pFrame->data, pFrame->linesize, in_w, in_h,
                         pCodecCtx->pix_fmt, 1);
    if (ret < 0) {
        printf("Could not allocate raw picture buffer\n");
        return -1;
    }

    y_size = in_h * in_w;
    //Encode
    for (i; i< 5000; i++){
        av_init_packet(&pkt);
        pkt.data = NULL;    // packet data will be allocated by the encoder
        pkt.size = 0;
		//Read raw YUV data
		if (fread(pFrame->data[0],1,y_size,fp_in)<= 0||		// Y
			fread(pFrame->data[1],1,y_size/4,fp_in)<= 0||	// U
			fread(pFrame->data[2],1,y_size/4,fp_in)<= 0){	// V
			break;
		}else if(feof(fp_in)){
            printf("read yuv file meet error\n");
			break;
		}

        pFrame->pts = i;

        /* encode the pFrame */
        ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_output);
        if (ret < 0) {
            printf("Error encoding frame\n");
            return -1;
        }

        //由于裸流里本来就没有可靠的pts和timebase等数据,pts的计算依靠设置的帧率
        double calc_duration = (double)1/framerate;
        pkt.pts = (double)(framecnt*calc_duration) / (av_q2d(video_st->time_base));
        pkt.dts = pkt.pts;
        pkt.duration = calc_duration / av_q2d(video_st->time_base);

        if (got_output) {
            printf("Succeed to encode frame: %5d\tsize:%5d\n",framecnt,pkt.size);
			framecnt++;
            pkt.stream_index = video_st->index;
            fwrite(pkt.data, 1, pkt.size, fp_out);
            av_interleaved_write_frame(ofmt_ctx, &pkt); //将AVPacket(存储视频压缩码流数据)写入文件
        }


        av_free_packet(&pkt);
    }

    av_write_trailer(ofmt_ctx); // write tailer

    // 释放内存
    fclose(fp_out);
    avcodec_close(video_st->codec);
    avcodec_close(pCodecCtx);
    av_free(pCodecCtx);
    av_freep(&pFrame->data[0]);
    av_frame_free(&pFrame);

	return 0;
}

    在写编码器的过程中,码率一直控制的不准,出现了帧率控制不好的情况,有参考这个文章:https://blog.csdn.net/mj1523/article/details/50434977 ,但是后来发现,其实帧率控制不需要那么做,因为是裸流,pkg 的pts 没有设置引起的。然后加上pkg 生成部分的pts 设置,问题得以解决。

    下面大致介绍下,pts,dts 区别,为什么有这个区别,该怎么取值等问题, timebase又是个啥玩意?

    首先是为什么package 要有pts, dts 这两个玩意?首先,视频显示是一帧一帧,但是编码并不是一帧的。 视频分为I P B 帧,需要在显示B帧之前知道P帧中的信息,所以没有B 帧的时候,pts,dts 编码时间和显示时间就是一致的,但是有的时候,编码的时候P 帧需要提前。

    为什么取值的时候有timebase, ffmpeg 各个数据层之间的时间基准不一样,不能直接相互使用,否则会出现溢出的情况,比如mux/demux层的timebase,flv,MP4等一般是1:1000,ts一般是1:90*1000 。codec/decode层timebase,h264随着帧率变化例如1:25 aac根据采样率变化例如1:44100。c:Raw data 层的timebase有很多变化比如1:1000*1000 或1:1000等等。

    最后是pts 怎么取值的问题,类似转码,有稳定的输入值参考,可以直接使用输入 AVStream 的pts 给输出pkg 赋值,像本文这种情况,是使用的裸流,就需要自己通过帧率和timebase 进行计算,计算过程就较为简单,查下对应公式即可。

09-01 20:05