音视频开发中如何使用ffmpeg 一帧H264解码YUV420P?

作为在音视频行业持续发力多年的视频服务厂商,TSINGSEE青犀视频研发了开源平台EasyDarwin,还有多款音视频流媒体平台,我们开发流媒体平台基本都要使用ffmpeg,在ffmpeg中,H264在编码前必须要转换成YUV420P,本文就分享一下怎么将h264转成YUV420P。

以下就是yuv420:

八个像素为:[Y0 U0 V0] [Y1 U1 V1] [Y2 U2 V2] [Y3 U3 V3][Y5 U5 V5] [Y6 U6 V6] [Y7U7 V7] [Y8 U8 V8]
码流为:Y0 U0 Y1 Y2 U2 Y3 Y5 V5 Y6 Y7 V7 Y8
映射出的像素点为:[Y0 U0 V5] [Y1 U0 V5] [Y2 U2 V7] [Y3 U2 V7][Y5 U0 V5] [Y6 U0 V5] [Y7U2 V7] [Y8 U2 V7]

注意:码流12字节个代表8个像素

理解需要画矩阵,如下:

码流数据:(4:2:0 ~ 4:0:2)

Y0 U0
Y1
Y2 U2
Y3

Y5     V5
Y6
Y7	   V7
Y8

映射像素:

Y0 U0 V5
Y1 U0 V5
Y2 U2 V7
Y3 U2 V7

Y5 U0 V5
Y6 U0 V5
Y7 U2 V7
Y8 U2 V7

YUV 4:2:0采样,每四个Y共用一组UV分量。
所以要把H264解码YUV420。首先需要把ffmpeg初始化:
代码如下:

typedef struct __DECODER_OBJ
{
	AVCodec	 *pVideoCodec;
	AVCodecContext *pVideoCodecCtx;
	AVFrame *mVideoFrame420;            ///< 视频帧
	AVPicture pYuvFrame;
	struct SwsContext *pSws_ctx;
	uint8_t	 *pBuffYuv420;

	int		codec;
	int		width;
	int		height;
	int		outputFormat;
	int		frameType;
	int		numBytes;

	bool	isInit;
}DECODER_OBJ;

avfilter_register_all();
avcodec_register_all();/*注册所有的编码解码器*/
av_register_all();// //注册所有可解码类型


decoderObj.pVideoCodec = avcodec_find_decoder(avCodecId);//H264
	if (NULL == decoderObj.pVideoCodec) {
		ReleaseVideoDecoder();
		return -3;
	}

	decoderObj.pVideoCodecCtx = avcodec_alloc_context3(decoderObj.pVideoCodec);
	if (NULL == decoderObj.pVideoCodecCtx) {
		ReleaseVideoDecoder();
		return -4;
	}

	AVDictionary *opts = NULL;
	int ret = avcodec_open2(decoderObj.pVideoCodecCtx, decoderObj.pVideoCodec, &opts);
	if (ret < 0) {
		ReleaseVideoDecoder();
		return -5;
	}
	decoderObj.mVideoFrame420 = av_frame_alloc();
	if (NULL == decoderObj.mVideoFrame420) {
		ReleaseVideoDecoder();
		return -6;
	}
	avpicture_alloc(&decoderObj.pYuvFrame, AV_PIX_FMT_YUV420P, width, height);

	decoderObj.numBytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1);

初始化完成,然后就需要把h264帧传进去进行解码出YUV420:
代码如下:

AVPacket pAvPacket = { 0 };
	decoderObj.mVideoFrame420->pict_type = picType;
	pAvPacket.data = buf;
	pAvPacket.size = size;

	int res = 0;
	int gotPic = 0;
	res = avcodec_decode_video2(decoderObj.pVideoCodecCtx, decoderObj.mVideoFrame420, &gotPic, &pAvPacket);
	if (!gotPic) return -9;

	decoderObj.pSws_ctx = sws_getContext(decoderObj.pVideoCodecCtx->width, decoderObj.pVideoCodecCtx->height,
		decoderObj.pVideoCodecCtx->pix_fmt, decoderObj.pVideoCodecCtx->width, decoderObj.pVideoCodecCtx->height,
		AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, NULL, NULL, NULL);
	sws_scale(decoderObj.pSws_ctx, decoderObj.mVideoFrame420->data, decoderObj.mVideoFrame420->linesize, 0,
		decoderObj.mVideoFrame420->height, decoderObj.pYuvFrame.data, decoderObj.pYuvFrame.linesize);

拿到的decoderObj.pYuvFrame.data[0]就是YUV420数据。
最后也不要忘记释放内存。
代码如下:

if (NULL != decoderObj.mVideoFrame420)
	{
		av_frame_free(&decoderObj.mVideoFrame420);
		decoderObj.mVideoFrame420 = NULL;
	}
	if (NULL != decoderObj.pVideoCodecCtx)
	{
		avcodec_close(decoderObj.pVideoCodecCtx);
		if (NULL != decoderObj.pVideoCodecCtx->priv_data)	free(decoderObj.pVideoCodecCtx->priv_data);
		if (NULL != decoderObj.pVideoCodecCtx->extradata)	free(decoderObj.pVideoCodecCtx->extradata);
		avcodec_free_context(&decoderObj.pVideoCodecCtx);
		decoderObj.pVideoCodecCtx = NULL;
	}
	if (NULL != &decoderObj.pYuvFrame)
	{
		avpicture_free(&decoderObj.pYuvFrame);
		//decoderObj.pYuvFrame = NULL;
	}
	if (NULL != decoderObj.pSws_ctx)
	{
		sws_freeContext(decoderObj.pSws_ctx);
		decoderObj.pSws_ctx = NULL;
	}
	if (NULL != decoderObj.pVideoCodec)
	{
		decoderObj.pVideoCodec = NULL;
	}
	if (NULL != decoderObj.pBuffYuv420)
	{
		av_free(decoderObj.pBuffYuv420);
		decoderObj.pBuffYuv420 = NULL;
	}
	if (decoderObj.pSws_ctx) {
		sws_freeContext(decoderObj.pSws_ctx);
		decoderObj.pSws_ctx = NULL;
	}

最终效果:使用ffplay指令播放yuv一帧数据

ffplay -i -video_size 700*700 $FILE

在TSINGSEE青犀视频开发的流媒体平台中,EasyNVR、EasyDSS都已经是成熟稳定的视频流媒体平台,可以直接下载测试,EasyRTC的重制版还正在开发当中,其架构有了新的方向,在不久之后新的版本也会上线和大家见面,TSINGSEE青犀视频云边端架构全平台都欢迎大家测试和了解。

原文地址:https://www.cnblogs.com/TSINGSEE/p/15099871.html