用FFmpeg把H264数据流解码成YUV420P

Posted 音视频开发老舅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用FFmpeg把H264数据流解码成YUV420P相关的知识,希望对你有一定的参考价值。

在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初始化:

代码如下:

//下面初始化h264解码库  
avcodec_init();  
av_register_all();  
  
AVFrame *pFrame_ = NULL;  
  
AVCodecContext *codec_ = avcodec_alloc_context();  
  
/* find the video encoder */  
AVCodec *videoCodec = avcodec_find_decoder(CODEC_ID_H264);  
  
if (!videoCodec)   
  
    cout << "codec not found!" << endl;  
    return -1;  
  
  
//初始化参数,下面的参数应该由具体的业务决定  
codec_->time_base.num = 1;  
codec_->frame_number = 1; //每包一个视频帧  
codec_->codec_type = AVMEDIA_TYPE_VIDEO;  
codec_->bit_rate = 0;  
codec_->time_base.den = 30;//帧率  
codec_->width = 1280;//视频宽  
codec_->height = 720;//视频高  
  
if(avcodec_open(codec_, videoCodec) >= 0)  
    pFrame_ = avcodec_alloc_frame();// Allocate video frame  
else  
    return -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);

本文福利, 免费领取C++音视频学习资料包、技术视频,内容包括(音视频开发,面试题,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,编解码,推拉流,srs)↓↓↓↓↓↓见下面↓↓文章底部点击免费领取↓↓

拿到的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
PS:avcodec_decode_video2这个函数会修改codec_里面的参数的,也就是说如果原来里面填的分别率是1280X720,运行avcodec_decode_video2后codec_里面会变成实际视频的分辨率。

ffmpeg视频编解码 demo初探(包含下载指定windows版本ffmpeg)将YUV图片序列作为流读入,编码封装成x264 MP4视频

参考文章:【FFmpeg编码实战】(1)将YUV420P图片集编码成H.264视频文件

文章目录

第二个项目:将YUV图片序列作为流读入,编码封装成x264 MP4视频

将YUV图片序列编码成.h264文件

直接把博主的代码拷到我们继承的上一个项目中,感觉应该有一些地方需要修改,果不其然

#pragma warning(disable : 4996)
#include <iostream>
#include <string>       // 字符串操作
#include <direct.h>     // 文件夹

extern "C"             // ffmpeg 相关头文件
#include <libavutil/avassert.h>
#include <libavutil/channel_layout.h>
#include <libavutil/opt.h>
#include <libavutil/mathematics.h>
#include <libavutil/timestamp.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include "libavutil/imgutils.h"


using namespace std;


#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avfilter.lib")
#pragma comment(lib, "swscale.lib")
#pragma comment(lib, "swresample.lib")
#pragma comment(lib, "postproc.lib")
#pragma comment(lib, "avdevice.lib")

// C++ 中 ffmpeg有些地方报错,修改这样后 OK
//
static char* av_ts_make_string1(char* buf, int64_t ts)

	if (ts == AV_NOPTS_VALUE) snprintf(buf, AV_TS_MAX_STRING_SIZE, "NOPTS");
	else                      snprintf(buf, AV_TS_MAX_STRING_SIZE, "%" PRId64, ts);
	return buf;


static char* av_ts_make_time_string1(char* buf, int64_t ts, AVRational* tb)

	if (ts == AV_NOPTS_VALUE) snprintf(buf, AV_TS_MAX_STRING_SIZE, "NOPTS");
	else                      snprintf(buf, AV_TS_MAX_STRING_SIZE, "%.6g", av_q2d(*tb) * ts);
	return buf;

//



/// <summary>
/// 输入输出文件信息
/// </summary>
const char* Out_File_Name = "video.h264";

// 源文件名:video/video_out.yuv420p.0.yuv 
const char* Folder = "video";
const char* YUV_File_Name = "video_out.yuv420p";
const char* YUV = "yuv";			// 图片文件后缀
#define YUV_Width	1050			// 宽
#define YUV_Height	540				// 高
#define Stream_Frame_Rate	25		// 帧率:每秒25帧

// 参考:doc\\examples\\encode_video.c、doc\\examples\\muxing.c

int main(int* argc, char* argv[])


	// 1. 初始化视频文件格式上下文
	AVFormatContext* p_FormatCtx = NULL;

	avformat_alloc_output_context2(&p_FormatCtx, NULL, "h264", Out_File_Name);
	if (!p_FormatCtx) 
		cout << "无法从输出文件后缀判断视频格式,默认使用h264" << endl;
		avformat_alloc_output_context2(&p_FormatCtx, NULL, "h264", Out_File_Name);
	

	// 2. 获得输出格式 Format信息
	AVOutputFormat* p_OutputFmt = NULL;
	p_OutputFmt = p_FormatCtx->oformat;


	bool have_video = false, have_audio = false, encode_video = false, encode_audio = false;
	AVCodec* p_Video_Enc = NULL, * p_Vudio_Enc = NULL;
	AVCodecContext* p_Video_Enc_Ctx = NULL, * p_Audio_Enc_Ctx = NULL;
	AVStream* p_Video_st = NULL, * p_Audio_st = NULL;
	AVFrame* p_Video_Frame = NULL;

	// 3. 添加 视频输出流,查找并打开视频解码器
	if (p_OutputFmt->video_codec != AV_CODEC_ID_NONE) 
		// 3.1 找到视频解码器
		p_Video_Enc = avcodec_find_encoder(p_OutputFmt->video_codec);

		// 3.2 创建视频流
		p_Video_st = avformat_new_stream(p_FormatCtx, p_Video_Enc);
		p_Video_st->id = p_FormatCtx->nb_streams - 1;

		// 3.3 创建编码器上下文
		p_Video_Enc_Ctx = avcodec_alloc_context3(p_Video_Enc);

		// 3.4 配置视频格式
		p_Video_Enc_Ctx->codec_type = AVMediaType::AVMEDIA_TYPE_VIDEO;
		p_Video_Enc_Ctx->codec_id = p_OutputFmt->video_codec;	// Code id
		p_Video_Enc_Ctx->pix_fmt = AVPixelFormat::AV_PIX_FMT_YUV420P;			// 图片格式

		p_Video_Enc_Ctx->bit_rate = 2000000;	// 码率越高,画面质量越好,相应视频越大	// 采样器码率
		p_Video_Enc_Ctx->width = YUV_Width;						// 宽
		p_Video_Enc_Ctx->height = YUV_Height;					// 高

		p_Video_st->time_base.num = 1;					// 音位 1s
		p_Video_st->time_base.den = 25;					// 帧率,每秒25帧


		p_Video_Enc_Ctx->framerate.num = 25;
		p_Video_Enc_Ctx->framerate.den = 1;
		p_Video_Enc_Ctx->time_base = p_Video_st->time_base;

		p_Video_Enc_Ctx->gop_size = 12;							// 连续画面组大小

		if (p_Video_Enc_Ctx->codec_id == AV_CODEC_ID_H264)
		
			p_Video_Enc_Ctx->qmin = 10;				// 最小的量化因子
			p_Video_Enc_Ctx->qmax = 51;				// 最大的量化因子
			p_Video_Enc_Ctx->qcompress = 0.6;
		


		if (p_Video_Enc_Ctx->codec_id == AV_CODEC_ID_MPEG2VIDEO)
			p_Video_Enc_Ctx->max_b_frames = 2;
		if (p_Video_Enc_Ctx->codec_id == AV_CODEC_ID_MPEG1VIDEO)
			p_Video_Enc_Ctx->mb_decision = 2;

		if (p_OutputFmt->flags & AVFMT_GLOBALHEADER)
			p_Video_Enc_Ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

		// 3.5 打开编码器
		avcodec_open2(p_Video_Enc_Ctx, p_Video_Enc, NULL);
		avcodec_parameters_from_context(p_Video_st->codecpar, p_Video_Enc_Ctx);

		// 3.6 分配视频帧内存
		p_Video_Frame = av_frame_alloc();
		p_Video_Frame->format = p_Video_Enc_Ctx->pix_fmt;
		p_Video_Frame->width = p_Video_Enc_Ctx->width;
		p_Video_Frame->height = p_Video_Enc_Ctx->height;
		av_frame_get_buffer(p_Video_Frame, 0);

		have_video = true;
		encode_video = true;
	

	// 预留
	// 4. 添加 音频输出流,查找并打开音频解码器
	if (p_OutputFmt->audio_codec != AV_CODEC_ID_NONE) 
		have_audio = true;
		encode_video = true;
	

	// 5. 打印输出信息
	av_dump_format(p_FormatCtx, 0, Out_File_Name, 1);

	// 6. 打开输出文件
	if (!(p_OutputFmt->flags & AVFMT_NOFILE))
		avio_open(&p_FormatCtx->pb, Out_File_Name, AVIO_FLAG_WRITE);

	// 7. 写头信息
	avformat_write_header(p_FormatCtx, NULL);

	char pic_file_name[50] = "";
	int pic_index = 0;
	const int pic_size = YUV_Height * YUV_Width * 3 / 2;
	FILE* pic_file = NULL;
	uint8_t* pic_buff = (uint8_t*)av_malloc(pic_size);

	int file_end = 0;

	int ret;
	char errbuf[AV_ERROR_MAX_STRING_SIZE] =  0 ;

	int y, x;

	// 8. 开始循环写入数据
	while (file_end == 0) 
		// 打开文件 获取数据
		
			memset(pic_file_name, '\\0', 50);
			sprintf_s(pic_file_name, 50, "%s/%s.%d.%s", Folder, YUV_File_Name, pic_index, YUV);
			cout << "打开文件:" << pic_file_name << endl;

			// 注意,此处一定要,使用 "rb+" 否则一次读取的数据大小远小于预期的大小
			pic_file = fopen(pic_file_name, "rb+");			// fopen(&pic_file, pic_file_name, "rb+")
			if (pic_file == NULL) 
				cout << "打开失败:" << pic_file_name << ", 开始刷新编码缓冲区\\n";
				file_end = 1;
			

			if (file_end == 0) 
				//ret = fread_s(pic_buff, pic_size, 1, pic_size, pic_file);
				ret = fread(pic_buff, 1, pic_size, pic_file);
				cout << "读取文件大小为:" << ret << "  应读大小:" << pic_size << endl;
				if (ret <= 0) 
					cout << "读取内容失败,ret=" << ret << endl;
					break;
				
				fclose(pic_file);
				pic_file = NULL;

				/* make sure the frame data is writable */
				ret = av_frame_make_writable(p_Video_Frame);
				if (ret < 0)
				
					cout << "p_Video_Frame data 不可写,强制退出\\n";
					exit(1);
				

				for (x = 0, y = 0; x < YUV_Height; x++)
				
					memcpy(p_Video_Frame->data[0] + x * p_Video_Frame->linesize[0], pic_buff + y, YUV_Width);
					y += YUV_Width;
				
				for (x = 0, y = 0; x < YUV_Height / 2; x++)
				
					memcpy(p_Video_Frame->data[1] + x * p_Video_Frame->linesize[1], pic_buff + y + YUV_Width * YUV_Height, YUV_Width / 2);
					y += YUV_Width / 2;
				
				for (x = 0, y = 0; x < YUV_Height / 2; x++)
				
					memcpy(p_Video_Frame->data[2] + x * p_Video_Frame->linesize[2], pic_buff + y + YUV_Width * YUV_Height * 5 / 4, YUV_Width / 2);
					y += YUV_Width / 2;
				

				//赋值
					///* prepare a dummy image */
					///* Y */
					//for (y = 0; y < p_Video_Enc_Ctx->height; y++) 
					//	for (x = 0; x < p_Video_Frame->linesize[0]; x++) 
					//		if (x < YUV_Width)
					//			p_Video_Frame->data[0][y * p_Video_Frame->linesize[0] + x] = pic_buff[y * YUV_Height + x];								//x + y + pic_index * 3
					//		else
					//			p_Video_Frame->data[0][y * p_Video_Frame->linesize[0] + x] = 128;
					//	
					//
					//
					///* Cb and Cr */
					//for (y = 0; y < p_Video_Enc_Ctx->height / 2; y++) 
					//	for (x = 0; x < p_Video_Frame->linesize[1]; x++) 
					//		if (x < YUV_Width / 2) 
					//			p_Video_Frame->data[1][y * p_Video_Frame->linesize[1] + x] = pic_buff[y * YUV_Height/2 + x + YUV_Height * YUV_Width];	//128 + y + pic_index * 2;
					//			p_Video_Frame->data[2][y * p_Video_Frame->linesize[2] + x] = pic_buff[y * YUV_Height/2 + x + YUV_Height * YUV_Width * 5 / 4];	// 64 + x + pic_index * 5;
					//		
					//		else 
					//			p_Video_Frame->data[1][y * p_Video_Frame->linesize[1] + x] = 128;
					//			p_Video_Frame->data[2][y * p_Video_Frame->linesize[2] + x] = 128;
					//		

					//	
					//
					//
					//cout << " linesize: " << p_Video_Frame->linesize[0] << "  " << p_Video_Frame->linesize[1] << "  " << p_Video_Frame->linesize[2] << "  " << p_Video_Frame->linesize[3] << endl;
			
		

		// 开始编码
		
			// PTS: 设置播放时间
			p_Video_Frame->pts = pic_index * (p_Video_st->time_base.den) / (p_Video_st->time_base.num * 25);
			// Encode 开始编码
			if (file_end == 0)
				ret = avcodec_send_frame(p_Video_Enc_Ctx, p_Video_Frame);
			else
				ret = avcodec_send_frame(p_Video_Enc_Ctx, NULL);
			if (ret < 0) 
				cout << "编码失败:" << av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE) << endl;
				exit(1);
			
			// 接收编码后的数据
			while (ret >= 0) 
				AVPacket pkt =  0 ;
				ret = avcodec_receive_packet(p_Video_Enc_Ctx, &pkt);
				if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
					break;
				else if (ret < 0) 
					cout << "编码失败:" << av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE) << endl;
					break;
				

				av_packet_rescale_ts(&pkt, p_Video_Enc_Ctx->time_base, p_Video_st->time_base);
				pkt.stream_index = p_Video_st->index;

				AVRational* time_base = &p_FormatCtx->streams[pkt.stream_index]->time_base;
				cout << "pts:" << av_ts_make_string1(errbuf, pkt.pts) << " pts_time:" << av_ts_make_time_string1(errbuf, pkt.pts, time_base);
				cout << " dts:" << av_ts_make_string1(errbuf, pkt.dts) << " duration:" << av_ts_make_string1(errbuf, pkt.duration);
				cout << " duration_time:" << av_ts_make_time_string1(errbuf, pkt.duration, time_base) << " stream_index:" << pkt.stream_index << endl;

				// 将编码后的数据写入文件中
				ret = av_interleaved_write_frame(p_FormatCtx, &pkt);
				av_packet_unref(&pkt);

				if (ret < 0) 
					cout << "Error while writing output packet: " << av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE) << endl;
					break;
				
			
		

		pic_index++;
	

	// 9. 写入尾信息
	av_write_trailer(p_FormatCtx);

	// 10.关闭文件
	if (!(p_FormatCtx->flags & AVFMT_NOFILE))
		avio_closep(&p_FormatCtx->pb);



	// 11. 释放资源
	if (have_video) 
		if (pic_buff)
			av_free(pic_buff);
		if (p_Video_Frame)
			av_frame_free(&p_Video_Frame);
		if (p_Video_Enc_Ctx)
			avcodec_free_context(&p_Video_Enc_Ctx);	// 释放视频解码器
	

	if (p_FormatCtx)
		avformat_free_context(p_FormatCtx);

	system("ffplay video.h264");

	return 0;



跑完后,项目根目录多了个video.h264文件

用VLC播放器能打开浏览

将YUV图片序列编码成mp4文件

参考文章:ffmpeg:将YUV原始数据编码封装为mp4格式

还是老样子,把博主代码搞下来,vs配置一下,跑

#pragma warning(disable : 4996)

// 2-muxing编码视频.cpp

#include <iostream>
#include <string>       // 字符串操作
#include <direct.h>     // 文件夹
#include <stdbool.h>

extern "C"             // ffmpeg 相关头文件
#include "libavcodec/avcodec.h"
#include <libavutil/opt.h>
#include "libavutil/imgutils.h"


using namespace std;

#pragma comment(lib, "avformat.lib")
#pragma comment(lib, "avutil.lib")
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avfilter.lib")
#pragma comment(lib, "swscale.lib")
#pragma comment(lib, "swresample.lib")
#pragma comment(lib, "postproc.lib")
#pragma comment(lib, "avdevice.lib")

/// 输入输出文件信息
//const ch

以上是关于用FFmpeg把H264数据流解码成YUV420P的主要内容,如果未能解决你的问题,请参考以下文章

FFmpeg视频编码 YUV420P编码H264

FFmpeg视频编码 YUV420P编码H264

ffmpeg视频编解码 demo初探(包含下载指定windows版本ffmpeg)分离视频文件中的视频流每一帧YUV图片

ffmpeg视频编解码 demo初探(包含下载指定windows版本ffmpeg)分离视频文件中的视频流每一帧YUV图片

2023-02-25:请用go语言调用ffmpeg,解码mp4文件并保存为YUV420SP格式文件,YUV420P不要转换成YUV420SP。

2023-02-24:请用go语言调用ffmpeg,解码mp4文件并保存为YUV420SP格式文件,采用YUV420P转YUV420SP的方式。