RK3399Pro-硬件编解码器MPP库快速上手--(二)MPP编码入门

MPP编码入门(后附demo代码链接)

mpp编码流程介绍

RK3399Pro-硬件编解码器MPP库快速上手--(二)MPP编码入门_第1张图片
整个编码分为mpp_create,mpp_init,
再通过mpp的接口mpi->control接口来进行参数设置.一般需要配置三类信息:
码率控制方式(MPPEncRcCfg),通过命令MPP_ENC_RC_CFG配置;
输入控制配置(MppEncPrepCfg),通过命令MPP_ENC_SET_PREP_CFG配置;
协议控制配置(MppEncCodecCfg),通过命令MPP_ENC_SET_CODEC_CFG配置;
详细配置细节会在下面代码注释中给出。
配置完mpp以后就可以通过mpi->encode_put_frame向编码器输入图片(注:图像的格式和数据对其有着严格要求,具体细则会在后续给出),再通过mpi->encode_get_packet获取编码好的包,再写入相应文件即可。

为了方便理解和使用,我将mpp的编码封装成了一个class,只需要再定义对象时给出编码器初始化的数据(包括输入图像的首地址、数据格式、宽高,输出视频流的编码格式、FPS以及输出文件路径),后续的使用则只需要通过“process_image(uint8_t *p)”接口向对象喂图片即可,编码器会自动将图像编码然后存入视频流文件。

编码器class定义头文件

其中结构体“MPP_ENC_DATA ”为编码器create和init时所需要的数据,class中定义了它的一个变量,这样整个编码器的配置都可以由这个MPP_ENC_DATA变量给出。
编码器在定义时需要向构造函数提供:输出文件路径,输入图像的首地址、宽高、数据格式,输出视频流的编码格式、FPS以及gop等配置信息。
init_mpp() 用于初始化mpp编码器,它会在构造函数中调用;
destroy_mpp();用于销毁mpp编码器;
process_image(uint8_t *p);为用户使用接口,输出图像的首地址,编码图像并存入文件;
read_yuv_image 用于读取输入图像的内存中的数据,并且将其对齐成mpp所需要的按16位对齐的格式。

#ifndef ENCODER_H
#define ENCODER_H
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "mpp_err.h"
#include "opencv2/opencv.hpp"
using namespace cv;
using namespace std;
#define MPP_ALIGN(x, a)         (((x)+(a)-1)&~((a)-1))
struct MPP_ENC_DATA  //编码所需要的数据
{
	// global flow control flag
	uint32_t frm_eos;
	uint32_t pkt_eos;
	uint32_t frame_count;
	uint64_t stream_size;

	// base flow context
	MppCtx ctx;
	MppApi *mpi;
	MppEncPrepCfg prep_cfg;
	MppEncRcCfg rc_cfg;
	MppEncCodecCfg codec_cfg;

	// input / output
	MppBuffer frm_buf;
	MppEncSeiMode sei_mode;

	uint32_t width;
	uint32_t height;
	uint32_t hor_stride;
	uint32_t ver_stride;
	MppFrameFormat fmt = MPP_FMT_YUV422_YUYV;
	MppCodingType type = MPP_VIDEO_CodingAVC;
	uint32_t num_frames;
	// resources
	size_t frame_size;

	int32_t gop = 60;
	int32_t fps = 30;
	int32_t bps;

	FILE *fp_output;
};
class Encoder
{
public:
	Encoder(char* outPutFileName, uint32_t width,uint32_t height,
                MppFrameFormat fmt = MPP_FMT_YUV422_YUYV, 
                    MppCodingType type = MPP_VIDEO_CodingAVC,
                        uint32_t fps = 30, uint32_t gop = 60);
    ~Encoder();
    bool process_image(uint8_t *p);
private:
    MPP_ENC_DATA mpp_enc_data;
    MPP_RET read_yuv_image(uint8_t *buf, uint8_t *image, uint32_t width, uint32_t height,
                       uint32_t hor_stride, uint32_t ver_stride, MppFrameFormat fmt);
    void init_mpp();
    void destroy_mpp();
};
#endif // ENCODER_H

编码器class代码解读

构造函数:

//输入文件名、图像宽高、输入图像格式、输出视频流类型、fps、gop
Encoder::Encoder(char* outPutFileName,uint32_t width,uint32_t height,
				            MppFrameFormat fmt,MppCodingType type,
									uint32_t fps, uint32_t gop)
{
	memset(&mpp_enc_data, 0, sizeof(mpp_enc_data));
	mpp_enc_data.width = width;
	mpp_enc_data.height = height;
	//mpp编码图像的行和列都是按16位对齐的,如果输出的行列不是16的整数,则需要在编码时将数据按照16位对齐。
	//此函数就是为了得到行列补齐16整除的数据,比如行是30,通过MPP_ALIGN(30,16);的输出就是32;
	mpp_enc_data.hor_stride = MPP_ALIGN(mpp_enc_data.width, 16);
	mpp_enc_data.ver_stride = MPP_ALIGN(mpp_enc_data.height, 16);
	mpp_enc_data.fmt = fmt;//MPP_FMT_BGR565;//MPP_FMT_YUV422_YUYV;
	mpp_enc_data.type = type;
	mpp_enc_data.fps = fps;
	mpp_enc_data.gop = gop;
	//不同的图像格式所占的内存大小和其长宽的关系是不同的
	//所以要根据不同的输入图像格式为编码器编码开辟不同的内存大小,
	******详情请见我的博客-“图像格式和编码方式整理”******
	if (mpp_enc_data.fmt <= MPP_FMT_YUV420SP_VU)
		mpp_enc_data.frame_size = mpp_enc_data.hor_stride * mpp_enc_data.ver_stride * 3/2;
	else if (mpp_enc_data.fmt <= MPP_FMT_YUV422_UYVY) {
		mpp_enc_data.hor_stride *= 2;
		mpp_enc_data.frame_size = mpp_enc_data.hor_stride * mpp_enc_data.ver_stride;
	}
	else {
		mpp_enc_data.frame_size = mpp_enc_data.hor_stride * mpp_enc_data.ver_stride * 4;
	}
	mpp_enc_data.fp_output = fopen(outPutFileName, "wb+");// 打开输出文件
	init_mpp();//使用输入的配置初始化编码器
}

编码器初始化函数:

void Encoder::init_mpp()
{
	MPP_RET ret = MPP_OK;
	//开辟编码时需要的内存
	ret = mpp_buffer_get(NULL, &mpp_enc_data.frm_buf, mpp_enc_data.frame_size);
	if (ret)
	{
		printf("failed to get buffer for input frame ret %d\n", ret);
		goto MPP_INIT_OUT;
	}
    //创建 MPP context 和 MPP api 接口
	ret = mpp_create(&mpp_enc_data.ctx, &mpp_enc_data.mpi);
	if (ret)
	{
		printf("mpp_create failed ret %d\n", ret);
		goto MPP_INIT_OUT;
	}
    /*初始化编码还是解码,以及编解码的格式
    MPP_CTX_DEC : 解码
    MPP_CTX_ENC : 编码
    MPP_VIDEO_CodingAVC : H.264
    MPP_VIDEO_CodingHEVC :  H.265
    MPP_VIDEO_CodingVP8 :  VP8
    MPP_VIDEO_CodingVP9 :  VP9
    MPP_VIDEO_CodingMJPEG : MJPEG*/
	ret = mpp_init(mpp_enc_data.ctx, MPP_CTX_ENC, mpp_enc_data.type);
	if (ret)
	{
		printf("mpp_init failed ret %d\n", ret);
		goto MPP_INIT_OUT;
	}

     /*设置编码参数:宽高、对齐后宽高等参数*/
	mpp_enc_data.bps = mpp_enc_data.width * mpp_enc_data.height / 8 * mpp_enc_data.fps;
	//mpp_enc_data.bps = 4096*1024;
	mpp_enc_data.prep_cfg.change        = MPP_ENC_PREP_CFG_CHANGE_INPUT |
			MPP_ENC_PREP_CFG_CHANGE_ROTATION |
			MPP_ENC_PREP_CFG_CHANGE_FORMAT;
	mpp_enc_data.prep_cfg.width         = mpp_enc_data.width;
	mpp_enc_data.prep_cfg.height        = mpp_enc_data.height;
	mpp_enc_data.prep_cfg.hor_stride    = mpp_enc_data.hor_stride;
	mpp_enc_data.prep_cfg.ver_stride    = mpp_enc_data.ver_stride;
	mpp_enc_data.prep_cfg.format        = mpp_enc_data.fmt;
	mpp_enc_data.prep_cfg.rotation      = MPP_ENC_ROT_0;
	ret = mpp_enc_data.mpi->control(mpp_enc_data.ctx, MPP_ENC_SET_PREP_CFG, &mpp_enc_data.prep_cfg);
	if (ret)
	{
		printf("mpi control enc set prep cfg failed ret %d\n", ret);
		goto MPP_INIT_OUT;
	}

    /*设置编码码率、质量、定码率变码率*/
	mpp_enc_data.rc_cfg.change  = MPP_ENC_RC_CFG_CHANGE_ALL;
	mpp_enc_data.rc_cfg.rc_mode = MPP_ENC_RC_MODE_VBR;
	mpp_enc_data.rc_cfg.quality = MPP_ENC_RC_QUALITY_MEDIUM;
	if (mpp_enc_data.rc_cfg.rc_mode == MPP_ENC_RC_MODE_CBR)
	{
		/* constant bitrate has very small bps range of 1/16 bps */
		mpp_enc_data.rc_cfg.bps_target   = mpp_enc_data.bps;
		mpp_enc_data.rc_cfg.bps_max      = mpp_enc_data.bps * 17 / 16;
		mpp_enc_data.rc_cfg.bps_min      = mpp_enc_data.bps * 15 / 16;
	}
	else if (mpp_enc_data.rc_cfg.rc_mode ==  MPP_ENC_RC_MODE_VBR)
	{
		if (mpp_enc_data.rc_cfg.quality == MPP_ENC_RC_QUALITY_CQP)
		{
			/* constant QP does not have bps */
			mpp_enc_data.rc_cfg.bps_target   = -1;
			mpp_enc_data.rc_cfg.bps_max      = -1;
			mpp_enc_data.rc_cfg.bps_min      = -1;
		}
		else
		{
			/* variable bitrate has large bps range */
			mpp_enc_data.rc_cfg.bps_target   = mpp_enc_data.bps;
			mpp_enc_data.rc_cfg.bps_max      = mpp_enc_data.bps * 17 / 16;
			mpp_enc_data.rc_cfg.bps_min      = mpp_enc_data.bps * 1 / 16;
		}
	}
	/* fix input / output frame rate */
	mpp_enc_data.rc_cfg.fps_in_flex      = 0;
	mpp_enc_data.rc_cfg.fps_in_num       = mpp_enc_data.fps;
	mpp_enc_data.rc_cfg.fps_in_denorm    = 1;
	mpp_enc_data.rc_cfg.fps_out_flex     = 0;
	mpp_enc_data.rc_cfg.fps_out_num      = mpp_enc_data.fps;
	mpp_enc_data.rc_cfg.fps_out_denorm   = 1;

	mpp_enc_data.rc_cfg.gop              = mpp_enc_data.gop;
	mpp_enc_data.rc_cfg.skip_cnt         = 0;

	ret = mpp_enc_data.mpi->control(mpp_enc_data.ctx, MPP_ENC_SET_RC_CFG, &mpp_enc_data.rc_cfg);
	if (ret)
	{
		printf("mpi control enc set rc cfg failed ret %d\n", ret);
		goto MPP_INIT_OUT;
	}
    /*设置264相关的其他编码参数*/
	mpp_enc_data.codec_cfg.coding = mpp_enc_data.type;
	switch (mpp_enc_data.codec_cfg.coding)
	{
	case MPP_VIDEO_CodingAVC :
	{
		mpp_enc_data.codec_cfg.h264.change = MPP_ENC_H264_CFG_CHANGE_PROFILE |
				MPP_ENC_H264_CFG_CHANGE_ENTROPY |
				MPP_ENC_H264_CFG_CHANGE_TRANS_8x8;
		/*
		 * H.264 profile_idc parameter
		 * 66  - Baseline profile
		 * 77  - Main profile
		 * 100 - High profile
		 */
		mpp_enc_data.codec_cfg.h264.profile  = 77;
		/*
		 * H.264 level_idc parameter
		 * 10 / 11 / 12 / 13    - qcif@15fps / [email protected] / cif@15fps / cif@30fps
		 * 20 / 21 / 22         - cif@30fps / half-D1@@25fps / [email protected]
		 * 30 / 31 / 32         - D1@25fps / 720p@30fps / 720p@60fps
		 * 40 / 41 / 42         - 1080p@30fps / 1080p@30fps / 1080p@60fps
		 * 50 / 51 / 52         - 4K@30fps
		 */
		mpp_enc_data.codec_cfg.h264.level    = 40;
		mpp_enc_data.codec_cfg.h264.entropy_coding_mode  = 1;
		mpp_enc_data.codec_cfg.h264.cabac_init_idc  = 0;
		mpp_enc_data.codec_cfg.h264.transform8x8_mode = 1;
	}
	break;
	case MPP_VIDEO_CodingMJPEG :
	{
		mpp_enc_data.codec_cfg.jpeg.change  = MPP_ENC_JPEG_CFG_CHANGE_QP;
		mpp_enc_data.codec_cfg.jpeg.quant   = 10;
	}
	break;
	case MPP_VIDEO_CodingVP8 :
	case MPP_VIDEO_CodingHEVC :
	default :
	{
		printf("support encoder coding type %d\n", mpp_enc_data.codec_cfg.coding);
	}
	break;
	}
	ret = mpp_enc_data.mpi->control(mpp_enc_data.ctx, MPP_ENC_SET_CODEC_CFG, &mpp_enc_data.codec_cfg);
	if (ret)
	{
		printf("mpi control enc set codec cfg failed ret %d\n", ret);
		goto MPP_INIT_OUT;
	}

	/* optional */
	mpp_enc_data.sei_mode = MPP_ENC_SEI_MODE_ONE_FRAME;
	ret = mpp_enc_data.mpi->control(mpp_enc_data.ctx, MPP_ENC_SET_SEI_CFG, &mpp_enc_data.sei_mode);
	if (ret)
	{
		printf("mpi control enc set sei cfg failed ret %d\n", ret);
		goto MPP_INIT_OUT;
	}

	
	if (mpp_enc_data.type == MPP_VIDEO_CodingAVC)
	{
		MppPacket packet = NULL;
		ret = mpp_enc_data.mpi->control(mpp_enc_data.ctx, MPP_ENC_GET_EXTRA_INFO, &packet);
		if (ret)
		{
			printf("mpi control enc get extra info failed\n");
			goto MPP_INIT_OUT;
		}

		/* get and write sps/pps for H.264 */
		if (packet)
		{
			void *ptr   = mpp_packet_get_pos(packet);
			size_t len  = mpp_packet_get_length(packet);

			if (mpp_enc_data.fp_output)
				fwrite(ptr, 1, len, mpp_enc_data.fp_output);

			packet = NULL;
		}
	}

	return;

MPP_INIT_OUT:

    if (mpp_enc_data.ctx)
    {
        mpp_destroy(mpp_enc_data.ctx);
        mpp_enc_data.ctx = NULL;
    }

    if (mpp_enc_data.frm_buf)
    {
        mpp_buffer_put(mpp_enc_data.frm_buf);
        mpp_enc_data.frm_buf = NULL;
    }

    printf("init mpp failed!\n");
}

用户接口:图像编码函数

/****************************************************************************
MppPacket  :   存放编码数据,例如264、265数据
MppFrame  :   存放解码的数据,例如YUV、RGB数据
MppTask   :     一次编码或者解码的session

编码就是push MppFrame,输出MppPacket;
解码就是push MppPacket,输出MppFrame;

MPI包含两套接口做编解码:
一套是简易接口, 类似 decode_put_packet / decode_get_frame 这样put/get即可
一套是高级接口, 类似 poll / enqueue/ dequeue 这样的对input output队列进行操作
*****************************************************************************/
bool Encoder::process_image(uint8_t *p)
{
	MPP_RET ret = MPP_OK;
    MppFrame frame = NULL;
    MppPacket packet = NULL;
    //获得开辟的内存的首地址
    void *buf = mpp_buffer_get_ptr(mpp_enc_data.frm_buf);

    //TODO: improve performance here?
	//从输入图像的首地址开始读取图像数据,但是读取时会考虑16位对齐,即读取的长和宽都是16的整数倍。
	//若图像一行或者一列不满16整数倍,则会用空数据补齐行和列
	read_yuv_image((uint8_t *)buf,  p,  mpp_enc_data.width, mpp_enc_data.height,
                       mpp_enc_data.hor_stride, mpp_enc_data.ver_stride, mpp_enc_data.fmt);
    ret = mpp_frame_init(&frame);
    if (ret)
    {
    	printf("mpp_frame_init failed\n");
    	return true;
    }
	//设置编码图像的格式
    mpp_frame_set_width(frame, mpp_enc_data.width);
    mpp_frame_set_height(frame, mpp_enc_data.height);
    mpp_frame_set_hor_stride(frame, mpp_enc_data.hor_stride);
    mpp_frame_set_ver_stride(frame, mpp_enc_data.ver_stride);
    mpp_frame_set_fmt(frame, mpp_enc_data.fmt);
    mpp_frame_set_buffer(frame, mpp_enc_data.frm_buf);
    mpp_frame_set_eos(frame, mpp_enc_data.frm_eos);
	//输入图像进行编码
    ret = mpp_enc_data.mpi->encode_put_frame(mpp_enc_data.ctx, frame);
    if (ret)
    {
    	printf("mpp encode put frame failed\n");
    	return true;
    }
	//获得编码后的packet
    ret = mpp_enc_data.mpi->encode_get_packet(mpp_enc_data.ctx, &packet);
    if (ret)
    {
    	printf("mpp encode get packet failed\n");
    	return true;
    }
	//获得编码后的数据长度和首地址,将其写入文件
    if (packet)
    {
    	// write packet to file here
    	void *ptr   = mpp_packet_get_pos(packet);
    	size_t len  = mpp_packet_get_length(packet);

    	mpp_enc_data.pkt_eos = mpp_packet_get_eos(packet);

    	if (mpp_enc_data.fp_output)
    		fwrite(ptr, 1, len, mpp_enc_data.fp_output);
    	mpp_packet_deinit(&packet);

    	//printf("encoded frame %d size %d\n", mpp_enc_data.frame_count, len);
    	mpp_enc_data.stream_size += len;
    	mpp_enc_data.frame_count++;

    	if (mpp_enc_data.pkt_eos)
    	{
    		printf("found last packet\n");
    	}
    }
    if (mpp_enc_data.num_frames && mpp_enc_data.frame_count >= mpp_enc_data.num_frames)
    {
    	printf("encode max %d frames", mpp_enc_data.frame_count);
    	return false;
    }
    if (mpp_enc_data.frm_eos && mpp_enc_data.pkt_eos)
    	return false;
    return true;
}

从内存读取YUV图像函数

MPP_RET Encoder::read_yuv_image(uint8_t *buf, uint8_t *image, uint32_t width, uint32_t height,
                       uint32_t hor_stride, uint32_t ver_stride, MppFrameFormat fmt)
{
    MPP_RET ret = MPP_OK;
    uint32_t read_size;
    uint32_t row = 0;
    //YUV格式的图像都是将YUV三个通道分为三部分存取,YYYYYY*****UUUUU*****VVVVV,所以在将其按照16位对齐copy时先将YUV三个通道的起始地址指定好。
    uint8_t *buf_y = buf;
    uint8_t *buf_u = buf_y + hor_stride * ver_stride; // NOTE: diff from gen_yuv_image
    uint8_t *buf_v = buf_u + hor_stride * ver_stride / 4; // NOTE: diff from gen_yuv_image
	uint32_t ptr = 0;
	//然后按照不同的格式,按照16位对齐copy图像数据到buf下,不同格式读取方式不同。
    switch (fmt) {
    case MPP_FMT_YUV420SP : {
        for (row = 0; row < height; row++) {
			memcpy(buf_y + row * hor_stride, image,width);
			image += width;
        }
        for (row = 0; row < height / 2; row++) {
			memcpy(buf_u + row * hor_stride, image, width);
			image += width;           
        }
    } break;
    case MPP_FMT_YUV420P : {
        for (row = 0; row < height; row++) {
			memcpy(buf_y + row * hor_stride, image, width);
			image += width;
        }
        for (row = 0; row < height / 2; row++) {
            memcpy(buf_u + row * hor_stride/2, image, width/2);
			image += width/2;
        }
        for (row = 0; row < height / 2; row++) {
			memcpy(buf_v + row * hor_stride/2, image, width/2);
			image += width/2;
        }
    } break;
    case MPP_FMT_ARGB8888 : {
        for (row = 0; row < height; row++) {
			memcpy(buf_y + row * hor_stride*4, image, width*4);
			image += width*4;
        }
    } break;
    case MPP_FMT_YUV422_YUYV :
    case MPP_FMT_YUV422_UYVY : {
        for (row = 0; row < height; row++) {
			memcpy(buf_y + row * hor_stride, image, width*2);
			image += width*2;
        }
    } break;
    default : {
        cout << "read image do not support fmt "<< endl;
        ret = MPP_ERR_VALUE;
    } break;
    }

err:
    return ret;
}

总代码链接:https://download.csdn.net/download/qq_39839546/67314289
已设置成了0积分下载,欢迎大家下载分享。

后面的博客会讲解图像格式和存储方式、mpp程序编译、关于16位对齐方式等内容。

你可能感兴趣的:(RK3399入门,音视频)