天天看点

ffmpeg实现硬件转码(使用FFmpeg调用NVIDIA GPU实现H265转码H264)

使用FFmpeg调用NVIDIA GPU实现H265转码H264

    • 背景
    • H265和H264一些基本知识
      • 1、H265码流nalu头
      • 2、H264码流nalu头
      • 3、补充:IDR帧和I帧的关系
    • 转码的一些基本知识
      • 1、软编码和硬编码如何区分
      • 2、软编码和硬编码比较
      • 3、目前的主流GPU加速平台
      • 4、目前主流的GPU平台开发框架
      • 5、流程区别
    • NVIDIA+ffmpeg硬件加速部署
      • 1、环境安装部署:Windows10 + ffmpeg4.1.3 + NVIDIA GeForce GTX 1660Ti
        • 1.1 ffmpeg Windows版本的下载
        • 1.2 下载NVIDIA驱动(GTX 1660Ti)
        • 1.3 下载cuda toolkit(GTX 1660Ti)
        • 1.4 用ffmpeg.exe -hwaccels显示所有可用的硬件加速器
        • 1.5 用ffmpeg.exe -codecs查看编解码器支持
    • NVENC介绍
    • FFmpeg命令行硬件转码H265裸流文件
      • 1、H265软件解码,H264硬件编码
      • 2、全硬件转码(H265硬件解码,H264硬件编码)
    • FFmpeg API进行H265裸流文件进行转码H264
      • 1、主体代码
      • 2、流程解析
      • 3、遇到的问题
      • 4、补充
    • Download
    • 参考

背景

最近项目遇到难题,就是web前端需要播放视频设备出来的H265码流,但目前只支持播放H264的码流,所以想快速解决这个问题的话,我想出了两种解决方案。

方案1:直接对H265进行RTMP封装成自定义FLV发布给前端播放,web前端得支持解析H265播放的控件,如果使用这种方案目前基本很难找到适用的开源方案,而且后端和前端的改动可以说基本是推倒重来,所以这种方案在短时间内是很难实现的。

方案2:做一个转码服务对H265进行解码再编码成H264,web前端播放方案就无需做任何改动,转码的话有软件转码和硬件转码两种方案,由于软件转码非常耗CPU资源,基本可以排除这种想法,那就只能考虑硬件转码的方案了,本文下面要介绍的就是硬件转码的方案。

对比这两种方案,第二种方案相对会合理一些,能比较快速解决H265的播放问题。

H265和H264一些基本知识

1、H265码流nalu头

00 00 00 01 40 01 的nuh_unit_type的值为 32, 语义为视频参数集 VPS

00 00 00 01 42 01 的nuh_unit_type的值为 33, 语义为序列参数集 SPS

00 00 00 01 44 01 的nuh_unit_type的值为 34, 语义为图像参数集 PPS

00 00 00 01 4E 01 的nuh_unit_type的值为 39, 语义为补充增强信息 SEI

00 00 00 01 26 01 的nuh_unit_type的值为 19, 语义为可能有RADL图像的IDR图像的SS编码数据 IDR

00 00 00 01 02 01 的nuh_unit_type的值为 1, 语义为被参考的后置图像,且非TSA、非STSA的SS编码数据

2、H264码流nalu头

00 00 00 01 06 type的值为 06, NALU_TYPE_SEI 语义为补充增强信息 SEI

00 00 00 01 67 type的值为 67, NALU_TYPE_SPS 语义为序列参数集 SPS

00 00 00 01 68 type的值为 68, NALU_TYPE_PPS 语义为图像参数集 PPS

00 00 00 01 65 type的值为 65, NALU_TYPE_IDR 语义为IDR图像中的片 IDR

3、补充:IDR帧和I帧的关系

IDR帧就是I帧,但是I帧不一定是IDR帧,在一个完整的视频流单元中第一个图像帧是IDR帧,IDR帧是强制刷新帧,在解码过程中,当出现了IDR帧时,要更新sps、pps,原因是防止前面I帧错误,导致sps,pps参考I帧导致无法纠正。

再普及一个概念是GOP,GOP的全称是Group of picture图像组,也就是两个I帧之间的距离,GOP值越大,那么I帧率之间P帧和B帧数量越多,图像画质越精细,如果GOP是120,如果分辨率是720P,帧率是60,那么两I帧的时间就是120/60=2s.

转码的一些基本知识

1、软编码和硬编码如何区分

软编码:使用CPU进行编码

硬编码:使用非CPU进行编码,如显卡GPU、专用的DSP、FPGA芯片等

2、软编码和硬编码比较

软编码:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。

硬编码:性能高,低码率下通常质量低于软编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。

3、目前的主流GPU加速平台

NVIDIA、INTEL、AMD等
           

4、目前主流的GPU平台开发框架

CUDA:NVIDIA的封闭编程框架,通过框架可以调用GPU计算资源

AMD APP:AMD为自己的GPU提出的一套通用并行编程框架,标准开放,通过在CPU、GPU同时支持OpenCL框架,进行计算力融合。

OpenCL:开放计算语言,为异构平台编写程序的该框架,异构平台可包含CPU、GPU以及其他计算处理器,目标是使相同的运算能支持不同平台硬件加速。

Inel QuickSync:集成于Intel显卡中的专用视频编解码模块。

5、流程区别

硬解软编:read(ffmpeg)->decoder(NVIDIA cuvid)->encoder(ffmpeg)

软解软编:read(ffmpeg)->decoder(ffmpeg)->encoder(ffmpeg)

软解硬编:read(ffmpeg)->decoder(ffmpeg)->encoder(NVIDIA nvenc)

NVIDIA+ffmpeg硬件加速部署

1、环境安装部署:Windows10 + ffmpeg4.1.3 + NVIDIA GeForce GTX 1660Ti

注意:需要你的电脑上有如下https://developer.nvidia.com/video-encode-decode-gpu-support-matrix#Encoder之一的NVIDIA显卡,我自己使用的是GeForce GTX 1660Ti这个型号的显卡(在NVIDIA tesla T4这块显卡上面也验证过,可以同时转码八路以上)

1.1 ffmpeg Windows版本的下载

下载地址:https://ffmpeg.zeranoe.com/builds/

ffmpeg实现硬件转码(使用FFmpeg调用NVIDIA GPU实现H265转码H264)

支持下载static/dev/shared

我用的ffmpeg版本是4.1.3

1.2 下载NVIDIA驱动(GTX 1660Ti)

下载地址:

版本451.77 (11.0)地址:https://www.nvidia.cn/Download/driverResults.aspx/162530/cn

ffmpeg实现硬件转码(使用FFmpeg调用NVIDIA GPU实现H265转码H264)

1.3 下载cuda toolkit(GTX 1660Ti)

下载地址:https://developer.nvidia.com/cuda-toolkit

版本451.48(11.0)地址:https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64&target_version=10&target_type=exelocal

ffmpeg实现硬件转码(使用FFmpeg调用NVIDIA GPU实现H265转码H264)

安装就不做介绍了。

1.4 用ffmpeg.exe -hwaccels显示所有可用的硬件加速器

我下载的FFmpeg就已经包含了英伟达的硬件加速器了

ffmpeg实现硬件转码(使用FFmpeg调用NVIDIA GPU实现H265转码H264)

1.5 用ffmpeg.exe -codecs查看编解码器支持

DEV.LS h264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10 (decoders: h264 h264_qsv h264_cuvid ) (encoders: libx264 libx264rgb h264_amf h264_nvenc h264_qsv nvenc nvenc_h264 )

DEV.L. hevc H.265 / HEVC (High Efficiency Video Coding) (decoders: hevc hevc_qsv hevc_cuvid ) (encoders: libx265 nvenc_hevc hevc_amf hevc_nvenc hevc_qsv )

h264_cuvid:h264硬件解码器

h264_nvenc:h264硬件编码器

hevc_cuvid:h265硬件解码器

hevc_nvenc:h265硬件编码器

ffmpeg实现硬件转码(使用FFmpeg调用NVIDIA GPU实现H265转码H264)

NVENC介绍

NVENC是由NVIDIA开发的一个API允许使用NVIDIA GPU显卡执行h.264和HEVC(就是H.265)编码。FFmpeg通过h264_nvenc和hevc_nvenc编码器支持NVENC。

为了在FFmpeg中启用它,你需要:

一个支持硬件编解码的英伟达GPU

英伟达GPU驱动程序

没有配置——disable-nvenc的ffmpeg

使用的例子:

ffmpeg -i input -c:v h264_nvenc -profile high444p -pixel_format yuv444p -preset default output.mp4
           

你可以通过ffmpeg -h encoder=h264_nvenc或ffmpeg -h encoder=hevc_nvenc看到可用的预设值、其他选项和编码器信息。

注意:如果你发现没有NVENC功能的设备的错误,请确保你的编码是支持的像素格式。见编码器信息如上所示。

CUDA / CUVID / NvDecode

CUVID现在也被Nvidia称为nvdec,可以在Windows和Linux上进行解码。结合nvenc,它提供了完整的硬件转码。

CUVID提供H264, HEVC, MJPEG, mpeg1/2/4, vp8/9, vc1解码器。编解码器支持因硬件而异。

1、使用CUVID解码器,本例中CUVID解码器将帧复制到系统内存中:

ffmpeg -c:v h264_cuvid -i input output.mkv
           

2、使用CUVID和NVENC实现全硬件转码:

ffmpeg -hwaccel cuvid -c:v h264_cuvid -i input -c:v h264_nvenc -preset slow output.mkv
           

3、部分硬件转码,帧通过系统内存(这是必要的转码10位内容):

ffmpeg -c:v h264_cuvid -i input -c:v h264_nvenc -preset slow output.mkv
           

4、如果编译ffmpeg时支持libnpp,可以使用它在链中插入一个基于GPU的scaler:

ffmpeg -hwaccel_device 0 -hwaccel cuvid -c:v h264_cuvid -i input -vf scale_npp=-1:720 -c:v h264_nvenc -preset slow output.mkv
           

hwaccel_device选项可以用来指定ffmpeg中的cuvid hwaccel要使用的GPU:。

FFmpeg命令行硬件转码H265裸流文件

使用NVIDIA GTX1660ti显卡 + ffmpeg4.1.3

1、H265软件解码,H264硬件编码

ffmpeg.exe -i h265toh264.h265 -vcodec h264_nvenc -r 30 -y h265toh264.h264
           

2、全硬件转码(H265硬件解码,H264硬件编码)

ffmpeg.exe -hwaccel cuvid -c:v hevc_cuvid -i h265toh264.h265 -c:v h264_nvenc -r 30 -y h265toh264.h264
           

FFmpeg API进行H265裸流文件进行转码H264

在VS2017工程下面使用ffmpeg API的方式实现H265的软件解码成YUV并使用h264_nvenc(NVIDIA硬件编码器)或libx264(h264软件编码器)实现YUV编码成H264.

直接上源码:

1、主体代码

#include "H265Decoder.h"
#include "H264Encoder.h"

#define ISSTORAGEYUV 0

int main()
{
	int fpts = 0;
	char frame_buf[1024] = { 0 };
	int H265Lenth = 0;
	char H265Buf[1024 * 512];

	H265Decoder h265Decoder;
	H264Encoder h264Encoder;
	//初始化编解码器时需要把正确的分辨率送进去
	/*h265Decoder.Init(1920, 1080);
	h264Encoder.Init(1920, 1080);*/
	h265Decoder.Init(480, 272);
	h264Encoder.Init(480, 272);
	
	//读取文件
	FILE * InFile = fopen("h265toh264.h265", "rb");
	//输出文件
#if ISSTORAGEYUV
	FILE * OutFile = fopen("h265toYUV.YUV", "wb");
#else
	FILE * OutFile = fopen("h265toh264.h264", "wb");
#endif
	//打印ffmpeg输出日志等级
	printf("log level %d \n", av_log_get_level());
	//设置ffmpeg输出日志等级
	av_log_set_level(AV_LOG_INFO);
	
	while (true)
	{
		int iReadSize = fread(frame_buf, 1, 512, InFile);
		if (iReadSize <= 0)
		{
			break;
		}
		memcpy(H265Buf + H265Lenth, frame_buf, iReadSize);
		H265Lenth = H265Lenth + iReadSize;
		//获取一帧数据
		while (true)
		{
			bool OneFrame = false;
			if (H265Lenth <= 8)
			{
				break;
			}
			for (int i = 4; i < H265Lenth - 4; i++)
			{
				//解析H265的nalu头
				if (H265Buf[i] == 0x00 && H265Buf[i + 1] == 0x00 && H265Buf[i + 2] == 0x00 && H265Buf[i + 3] == 0x01)
				{
					h265Decoder.AddData(H265Buf, i);
					while (true)
					{
						char * pOutData = NULL;
						int OutSize = 0;
#if ISSTORAGEYUV
						//获取H265解码后的YUV并存文件
						if (!h265Decoder.GetYUVData(pOutData, OutSize))
						{
							break;
						}
						fwrite(pOutData, 1, OutSize, OutFile);
#else
						//获取H265解码frame并编码H264存文件
						AVFrame *frame = NULL;
						frame = h265Decoder.GetData();
						if (!frame) {
							break;
						}
						frame->pts = fpts++;
						h264Encoder.encode(frame, pOutData, OutSize);
						fwrite(pOutData, 1, OutSize, OutFile);
#endif

					}
					H265Lenth = H265Lenth - i;
					memcpy(H265Buf, H265Buf + i, H265Lenth);
					OneFrame = true;
					break;
				}
			}
			if (OneFrame)
			{
				continue;
			}
			else
			{
				break;
			}
		}
	}
	if (OutFile) {
		fclose(OutFile);
		OutFile = NULL;
	}
	if (InFile) {
		fclose(InFile);
		InFile = NULL;
	}
	printf("pasing end\r\n");
	return 0;
}
           

2、流程解析

1、初始化编解码器,需要把H265正确的分辨率格式送进去初始化

h265Decoder.Init(480, 272);
	h264Encoder.Init(480, 272);
           

2、循环读取H265文件,组成一帧完整的H265(根据H265的nalu头00 00 00 01进行判断)

3、把一帧H265送进去H265解码器进行软件解码

4、获取H265解码后的YUV可以选择存YUV文件或送进去H264编码器进行编码后存成H264文件

#if ISSTORAGEYUV
			//获取H265解码后的YUV并存文件
			if (!h265Decoder.GetYUVData(pOutData, OutSize))
			{
				break;
			}
			fwrite(pOutData, 1, OutSize, OutFile);
#else
			//获取H265解码frame并编码H264存文件
			AVFrame *frame = NULL;
			frame = h265Decoder.GetData();
			if (!frame) {
				break;
			}
			frame->pts = fpts++;
			h264Encoder.encode(frame, pOutData, OutSize);
			fwrite(pOutData, 1, OutSize, OutFile);
#endif
           

3、遇到的问题

调试期间,有遇到调用h264_nvenc硬件编码器的时候出错,排查后发现,H265解码后的原始视频格式是YUVJ420P,原因是有的视频设备出来的H265视频流,编码前的数据是YUVJ420P,不全部都是YUV420P,如果直接把这个原始的YUVJ420P送进去NVIDIA硬件编码器的时候会直接导致程序奔溃,后来经过一番排查,才发现原来NVIDIA是不支持对YUVJ420P直接进行编码的,如下图所示,只对YUV420P或者YUV444P支持H264编码。

ffmpeg实现硬件转码(使用FFmpeg调用NVIDIA GPU实现H265转码H264)

针对这个问题,只能用ffmpeg的sws_scale把YUVJ420P转换成YUV420P后再送给h264_nvenc硬件编码器进行编码。

#if ISCHANGEFRAME
	if (frame->format == AV_PIX_FMT_YUVJ420P) {
		sws_scale(convert_ctx,frame->data, frame->linesize, 0, Height, frameYUV->data, frameYUV->linesize);
		frameYUV->format = AV_PIX_FMT_YUV420P;
		frameYUV->width = Width;
		frameYUV->height = Height;
		return frameYUV;
	}
#endif
           

4、补充

1、libx264是可以对YUVJ420P直接进行264编码的,不需要做格式转换。

2、YUVJ420P和YUV420P简单介绍

YUVJ420P的字面含义是“使用了JPEG颜色范围的YUV420P,像素使用表示颜色的数值范围发生了变化。

YUV420p的像素颜色范围是[16,235],16表示黑色,235表示白色

YUVJ420P的像素颜色范围是[0,255]。0表示黑色,255表示白色

FFmpeg的定义和解释:

AV_PIX_FMT_YUV420P,   ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
AV_PIX_FMT_YUVJ420P,  ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV420P and setting color_range
           

Download

1、测试视频

2、FFmpeg API H265转码H264源码

参考

1、https://developer.nvidia.com/ffmpeg

2、ffmpeg nvidia硬件加速方案(参考的博客)

https://blog.csdn.net/zhengbin6072/article/details/88202268

3、ffmpeg关于硬解码和软解码一些参考

https://www.cnblogs.com/chenpingzhao/p/12359725.html

4、YUV420P和YUVJ420P介绍

https://blog.csdn.net/samsung12345678/article/details/102383708