前提
1. 为什么需要硬解码?
一个视频文件或者一路视频流还好,如果增加到64路视频流呢,如果是4K、8K这种高分辨率的视频呢,必须安装上硬解码才是上上策。举个例子在电脑上播放4K以上的H265这类的视频文件,如果不开硬解码,很容易出现卡顿现象,在配置高的电脑也容易出现,毕竟非常的耗CPU资源,来不及刷新,上了硬解码之后,明显流畅的不要不要的,怪不得现在的显卡性能越做越牛逼,就是为了在显示这块尽可能的分担CPU的压力,以便留出CPU时间片做其他的事情。
2 为什么前面不学习硬件编码呢:没有意义
3. 硬解码需要 运行程序 的 电脑(手机/pad) 有什么硬件支持?
有的支持 Windows 平台,有的支持 linux 平台,有的支持 apple ios 平台,有的支持 android 平台。
二:Windows 平台,我们可以使用利用 DXVA2、DX11、OpenGL、Vulkan、等技术,直接显示 GPU 显卡中的数据。
FFPLAY 最新源码,使用 Vulkan 的方式来进行硬解加速渲染的。
为什么使用 Vulkan ?
A:Vulkan 跨平台;
B:Vulkan 可以做渲染;
C:Vulkan 可以做计算,做图像处理;
D:Vulkan 可以做视频解码编码;
E:Vulkan 可以和 CUDA、DRM、VAAPI 互操作;
三:使用 DXVA2 硬解加速渲染:
没有了内存复制(av_hwframe_transfer_data)。
没有了sws_scale 解码到图片也。
界面绘制由原来的 GDI 绘制,变成了 DX 绘制。
非常之高效。
四:Windows 操作系统支持 DXVA2 方式硬解。
DXVA2 封装了显卡解码。你就不用操心不同显卡了。
这也是为什么网上介绍硬解时,大多数是介绍 DXVA2 方式硬解。
当然,如果你的显卡不支持某种格式视频(H265 格式视频)的硬解,即使你使用 DXVA2 硬解,那也是行不通的。
五:Windows 操作系统支持 DXVA2 方式硬解。
不使用 FFMPEG 也是可以的。按照 DXVA2 的规范写就可以了。当然这不在本文讨论范围内。
感兴趣的可以参看:https://github.com/mofo7777/H264Dxva2Decoder.git
六:我们只要在 FFMPEG 硬解示例程序的基础上,修改硬解方式为 dxva2,就 OK 了。
其它不用作任何修改。就是 DXVA2 硬解码了。
当然我们的目的是使用 DX 的方式来渲染图像。
七:这个 DX 渲染函数很关键。
网上绝大多数的代码,都是将 hwcontext_dxva2.c 代码从 FFMPEG 源代码复制出来,
添加到自己的代码中,并改写为 ffmpeg_dxva2.cpp 。
其实完全没有这个必要。而且这种方式肯定也是不可取的。
而且我相信 FFMPEG 的源代码作者肯定也不想你这么干。
我使用的方法,适用于所有语言。
20 行的代码就可以完成 DX 绘制了。更具有通用性。
八:DX 渲染函数原理:
1:用户在调用 av_hwdevice_ctx_create 函数,初始化硬件加速上下文时,
会调用 hwcontext.c 的 av_hwdevice_ctx_create 函数进行 DXVA2 的初始化。
av_hwdevice_ctx_create(hwcontext.c) 初始化成功后,将设备上下文信息保存在 data 里面,返回给了调用者;
2:av_hwdevice_ctx_create(hwcontext.c) 会进行 DXVA2 的创建。
ret = device_ctx->internal->hw_type->device_create(device_ctx, device, opts, flags);
这里将调用 hwcontext_dxva2.c 的 dxva2_device_create 函数进行 DXVA2 的初始化。
创建成功后,将 DX 设备信息保存在设备上下文信息的 user_opaque 里面,返回给了调用者。
3:至此,我们可以拿到:设备上下文信息、DX 设备信息。可以直接进行 DX 界面绘制了。
九:测试发现:解码效率比网上那些复制改写 hwcontext_dxva2.c 的程序,快了将近 200 多帧。果然高效!!!
十:核心代码及其说明:
while ((ret = avcodec_receive_frame(videoCodecCtx, Frame)) >= 0)
{
......
surface = (LPDIRECT3DSURFACE9)Frame->data[3]; // 待绘制的数据
device_ctx = (AVHWDeviceContext*)videoCodecCtx->hw_device_ctx->data; // 设备上下文信息
priv = (DXVA2DevicePriv*)device_ctx->user_opaque; // DX 设备信息
......
/* 开始 DX 渲染 */
priv->d3d9device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
priv->d3d9device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &BackBuffer);
RECT SourceRect = { 0, 0, videoCodecCtx->width, videoCodecCtx->height };
priv->d3d9device->StretchRect(surface, &SourceRect, BackBuffer, NULL, D3DTEXF_LINEAR);
priv->d3d9device->Present(NULL, NULL, NULL, NULL);
......
}
一:FFMPEG 支持的硬解方式:如下都是了解知识
二 使用ffmpeg api 打印出 支持h264的硬解码技术有哪些
结果为:
dxva2
d3d11va
#include <iostream>
using namespace std;
extern "C" {
#include "libavcodec/avcodec.h"
}
void test01();
int main(int argc, char *argv[]) {
test01();
return 0;
}
void test01() {
//找到解码器
AVCodec* avcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
/// 硬件解码相关. 打印所有支持H264硬件解码加速方式
for (int i = 0;; i++)
{
const AVCodecHWConfig* config = avcodec_get_hw_config(avcodec, i);
if (!config) {
break;
}
if (config->device_type) {
cout << av_hwdevice_get_type_name(config->device_type) << endl;
}
}
}
结果为:
dxva2
d3d11va
三 : 具体学习 DXVA2 对于 h264 硬解码的使用
DXVA2不能单独的编解码,但是可以让硬件解码加速,也就是说,这个DXVA2肯定是要和前面学习的 avcodecContext的 解码 结合起来使用的。
核心api
1. 创建 硬解码上下文
AVBufferRef *av_hwdevice_ctx_alloc(enum AVHWDeviceType type);
函数作用:
用于创建指定类型的硬件设备上下文
参数说明:
设备类型指定 需通过参数指定硬件加速类型(如 CUDA、DXVA2、VideoToolbox 等),FFmpeg 通过枚举 AVHWDeviceType 定义支持的硬件类型67。
返回值
函数返回 AVBufferRef* 类型指针,表示对硬件设备上下文的引用,采用引用计数机制管理内存生命周期
av_hwdevice_ctx_alloc 只是创建 硬件设备 上下文,则一定需要使用 av_hwdevice_ctx_init初始化
2. 初始化 硬解码上下文
int av_hwdevice_ctx_init(AVBufferRef *ref);
通过 av_hwdevice_ctx_init 完成设备初始化,部分平台需额外配置参数(如分辨率、帧格式)
返回值 :@return 0 on success, a negative AVERROR code on failure
3. 创建 硬编码上下文 并 初始化硬编码上下文,可以设置参数
3 = 1+2
int av_hwdevice_ctx_create(AVBufferRef **device_ctx, enum AVHWDeviceType type,
const char *device, AVDictionary *opts, int flags);
函数作用:
通过 av_hwdevice_ctx_create 函数,依据AVHWDeviceType 创建 硬件加速上下文
参数说明
AVBufferRef **device_ctx
输入输出参数,代表的是硬件加速上下文,该硬件加速上下文,
需要通过 void av_buffer_unref(AVBufferRef **buf)释放
enum AVHWDeviceType type
输入参数,枚举类型(AVHWDeviceType),
指定硬件加速类型(如 AV_HWDEVICE_TYPE_CUDA、AV_HWDEVICE_TYPE_VAAPI)
const char *device
字符串类型,指定设备路径或标识符(如 /dev/dri/renderD128),部分平台可设为 NULL 自动检测
AVDictionary *opts 传递平台相关配置参数(如 CUDA 设备编号)
int flags 保留参数,当前版本未启用,通常设为 0
返回值
若返回 0 表示成功,失败时需检查错误码(如 EINVAL 表示类型不支持)
4. 将硬编码器上下文 绑定到 AVCodecContext->hw_device_ctx 中,
AVCodec* avcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
AVCodecContext *codec_ctx = avcodec_alloc_context3(avcodec );
//将硬编码器上下文device_ctx 绑定到 AVCodecContext->hw_device_ctx 中
codec_ctx->hw_device_ctx = av_buffer_ref(device_ctx);
之所以要将 需通过 av_buffer_ref
增加引用计数有目的是:
第一:在完成绑定后,我们就可以调用 void av_buffer_unref(&device_ctx) 将创建的 硬件编码器上下文 引用计数减一。并将 device_ctx 置为nullptr.
第二:而最终释放 最后再通过 void avcodec_free_context(AVCodecContext **pavctx) 的释放,完全释放 device_ctx
代码示例
#include <iostream>
using namespace std;
extern "C" {
#include "libavcodec/avcodec.h"
}
void test01();
int main(int argc, char *argv[]) {
test01();
return 0;
}
void test01() {
//找到解码器
AVCodec* avcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (avcodec == nullptr) {
cout << "avcodec_find_decoder(AV_CODEC_ID_H264) func error " << endl;
return;
}
/// 硬件解码相关. 打印所有支持H264硬件解码加速方式
for (int i = 0;; i++)
{
const AVCodecHWConfig* config = avcodec_get_hw_config(avcodec, i);
if (!config) {
break;
}
if (config->device_type) { //devicetype为枚举打印结果为
cout << av_hwdevice_get_type_name(config->device_type) << endl;
}
}
AVBufferRef* device_ctx = nullptr;
int ret = av_hwdevice_ctx_create(&device_ctx,
AV_HWDEVICE_TYPE_DXVA2,
nullptr,
nullptr,
0);
if (ret < 0 ) {
cout << "av_hwdevice_ctx_create func error " << endl;
return;
}
AVCodecContext* avcodecContext = avcodec_alloc_context3(avcodec);
if (avcodecContext == nullptr) {
cout << "avcodec_alloc_context3(avcodec) func error " << endl;
return;
}
//avcodecContext->hw_device_ctx = device_ctx;//直接传递
avcodecContext->hw_device_ctx = av_buffer_ref(device_ctx);//引用计数加1
//device_ctx 的引用计数就变成了 2了,
cout << "av_buffer_get_ref_count(device_ctx) = " << av_buffer_get_ref_count(device_ctx) << endl;
//理论上到这里就可以将 device_ctx 调用 av_buffer_unref了,
//av_buffer_unref 函数的作用:将引用计数-1,
//然后判断如果引用计数>0,则只是device_ctx == nullptr;
// 如果引用计数==0,则会将 device_ctx 里面的avbuf都释放了,并且将 device_ctx==nullptr
av_buffer_unref(&device_ctx);
//这里已经释放了 device_ctx,再去 调用 av_buffer_get_ref_count(device_ctx) 会有 error
//cout << "av_buffer_get_ref_count(device_ctx) = " << av_buffer_get_ref_count(device_ctx) << endl;
//最后释放 avcodecContext
avcodec_free_context(&avcodecContext);
}
5. 当硬件加速打开后, 通过 avcodecContext 解码后的avframe数据会有变化。
没有硬件加速的case
之前我们对于h264文件,通过 avcodec_receive_frame方法得到的avframe 的linesize 是如下的情况:符合YUV420p的数据格式,linesize[0],linesize[1],linesize[2]都是有具体的值,data[0]放的是Y分量,data[1]放的是U分量,data[2]放的是V分量,
format 是0,对应的就是YUV420P
打开硬件加速的case
当我们将硬件加速打开后,通过 avcodec_receive_frame方法得到的avframe 的linesize 和 data 是如下的情况:
data[0],data[1],data[2],中没有数据,data[3]中有数据
整个 linesize 数组都是0 。
format 是53,对应的是 AV_PIX_FMT_DXVA2_VLD
在 AV_PIX_FMT_DXVA2_VLD 的定义中,也说明了翻译一下:通过DXVA2得到的HW 解码,Picture.data[3]中包含了LPDIRECT3DSURFACE9的指针,也就是在定义中也告诉了大家,我的数据在 data[3]中
AV_PIX_FMT_DXVA2_VLD ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
结论:
也就是说:我们现在的avframe格式 是 AV_PIX_FMT_DXVA2_VLD ,是通过硬件加速设备显存完成的,存储在显存中,而不是内存中。
结论测试,
添加循环,让循环读取h264文件,为了让能够循环读取文件,我们这里改造一下代码。
主要是改动 不停的读取文件的流程
//6. 开始读取文件直到文件读取完成,这里为了测试,要写一个循环,让一直读取h264文件
while (true) {
readifstream.read((char*)readfilebuf, readfilebufsize);
if (readifstream.bad() == true) {
cout << "read file bad" << endl;
break;
}
int realfilebufreadcount = readifstream.gcount(); // gcount() 返回最后一次非格式化输入操作(如 read()、get()、getline())实际读取的字符数(字节数)
if (realfilebufreadcount <= 0) { //如果读取的数据是<=0的,说明整个h264文件已经读取完成了,或者读取h264文件的时候出现了错误了,那么就要直接退出
cout << "read file break count = " << realfilebufreadcount << endl;
//添加循环用的。先需要调用 clear()函数,通过以 state 的值赋值,设置流错误状态标志。默认赋值 std::ios_base::goodbit ,它拥有的效果为清除所有错误状态标志。
//void clear( std::ios_base::iostate state = std::ios_base::goodbit );
readifstream.clear();
//这时候将 readifstream转到 文件开头
readifstream.seekg(0, std::ios::beg);
continue; //为了循环 continue;
//break;//如果不循环,则这里要break;
}
//到这里说明读取到了 realbufreadcount 个字节的数据
cout << "realfilebufreadcount = " << realfilebufreadcount << endl;
//7. 根据读取到的字节,解析这段字节,解析出来avpacket.
//我们知道当 h264 数据是 大于 4096字节时,第一次读取的一定是4096 个字节。而从4096中肯定是能解析出来很多个avpacket的
//而解析的方法是使用 av_parser_parse2方法,av_parser_parse2方法的返回值是已经解析了多少个字节。
//因此这里要弄一个循环,让 realfilebufreadcount = (从文件读取到的 realfilebufreadcount字节) - (每次av_parser_parse2方法解析过的字节)
//如果 realbufreadcount 大于0,就可以继续解析,如果
uint8_t* tempreadfilebuf = readfilebuf;//让 tempreadfilebuf 指向 readfilebuf的指针。这里为什么不直接用readfilebuf呢?从功能实现上直接用buf也是可以的,但是这是C语言的基本功,尽量不要用原始指针。而是找一个复制的指针。
while (realfilebufreadcount > 0 ) {
int parsebufsize = av_parser_parse2(
avcodecParseContext,
avcodecContext,
&avpacket->data,
&avpacket->size,
tempreadfilebuf,
realfilebufreadcount,
0, 0, 0);
cout << "parsebufsize = " << parsebufsize << endl;
realfilebufreadcount = realfilebufreadcount - parsebufsize;
cout << " after realfilebufreadcount = " << realfilebufreadcount << endl;
tempreadfilebuf = tempreadfilebuf + parsebufsize;
cout << "before avpacket->size = " << avpacket->size << endl;
//这时候还要判断是否packet的size是大于0的,说明是正常截取了一段avpacket数据,因为av_parser_parse2方法即使分析据,也并不能保证真正的解析到 avpacket
if (avpacket->size>0) {
cout << "avpacket->size = " << avpacket->size << endl;
// 如果avpacket 的size 大于0,说明已经有了avpacket了,也就是说,到这里,已经对h264进行了分离,得到了avpacket
// 那么下来,我们就需要将 对avpacket进行处理,在最后,记得调用av_packet_unref(avpacket);
// 8. 将avpacket发送给 avcodecContext
ret = avcodec_send_packet(avcodecContext, avpacket);
if (ret == AVERROR_EOF) {
//已发送空包(pkt=NULL)触发编解码器刷新,且后续无新数据可处理(流结束标志)。
//说明 tempreadfilebuf 中的数据已经读取完成了,那么要continue 还是break呢?
//这里应该是continue比较合理,就是我们依赖 将 tempreadfilebuf 读取完成,跳出循环的条件应该是 realfilebufreadcount > 0
cout << "avcodec_send_packet return value == AVERROR_EOF " << endl;
continue;
}
else if (ret == AVERROR(EINVAL) || ret == AVERROR(ENOMEM)) {
//EINVAL :参数错误,可能原因:
// - 编解码器未通过 avcodec_open2 打开;
// - 上下文类型不匹配(如向编码器发送数据包)。
//ENOMEM:内存不足
cout << "avcodec_send_packet return value == AVERROR(EINVAL) || AVERROR(ENOMEM) ret = " << ret << endl;
//如果是这样情况,应该跳出循环比较好
break;
}
else if (ret == 0 || ret == AVERROR(EAGAIN)) {
//ret == AVERROR(EAGAIN) 表示 编解码器内部缓冲区已满,需先调用 avcodec_receive_frame 获取输出数据,释放资源后再重试。
if (ret == AVERROR(EAGAIN)) {
cout << "avcodec_send_packet return value == AVERROR(EAGAIN) " << endl;
}
// 9.正确的发送数据到 解码器了,那么就通过 avcodec_receive_frame 中得到avframe,这里还是写一个循环
while (1) {
ret = avcodec_receive_frame(avcodecContext, avframe);
if (ret == AVERROR(EAGAIN)) {
//AVERROR(EAGAIN) 当前无可用输出帧,需继续发送输入数据(avcodec_send_packet)或重复调用本函数尝试获取数据
//那就跳出当前while (1) 循环,
cout << "avcodec_receive_frame return value == EAGAIN " << endl;
break;
}
else if (ret == AVERROR_EOF) {
//编解码器已完全刷新(如调用 avcodec_send_packet(NULL) 发送结束标记后),后续无更多数据输出
cout << "avcodec_receive_frame return value == AVERROR_EOF " << endl;
break;
}
else if (ret == AVERROR(EINVAL)) {
//编解码器未正确初始化或上下文参数非法
cout << "avcodec_receive_frame return value == EINVAL " << endl;
break;
}
else if (ret == AVERROR_INPUT_CHANGED) {
//编解码器未正确初始化或上下文参数非法
cout << "avcodec_receive_frame return value == AVERROR_INPUT_CHANGED " << endl;
break;
}
else if (ret < 0 ) {
cout << "avcodec_receive_frame return value == other ret = " << ret << endl;
break;
}
else if (ret == 0 ) {
//这时候才有正确的avframe 被解析出来
//1.存储 avframe
cout << "avframe->format = " << avframe->format << endl;
cout << "avframe->width = " << avframe->width << endl;
cout << "avframe->height = " << avframe->height << endl;
cout << "avframe->linesize[0] = " << avframe->linesize[0] << endl;
cout << "avframe->linesize[1] = " << avframe->linesize[1] << endl;
cout << "avframe->linesize[2] = " << avframe->linesize[2] << endl;
cout << "avframe->pkt_size = " << avframe->pkt_size << endl;
//如果是硬件加速方式,就不能这么存储了,我们需要先转换一下。将 AV_PIX_FMT_DXVA2_VLD 格式转成 AV_PIX_FMT_YUV420P
//av_hwframe_transfer_data()
//注意这里的写法,要先写完Y,再写U,再写V
//for (int j = 0; j < avframe->height; j++) {
// writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
//}
//for (int j = 0; j < avframe->height / 2; j++) {
// writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width / 2);
//}
//for (int j = 0; j < avframe->height / 2; j++) {
// writefstream.write((char*)(avframe->data[2] + j * avframe->linesize[2]), avframe->width / 2);
//}
//解码相关。计算1秒钟解码了多少次
xjiemacount++;
//正常写法,但是有我们测试的视频太短了,不能说明问题
//auto cur = NowMs();
//if (cur -starttime>=1000) {
// cout << "1秒钟解码了" << xjiema <<"次"<< endl;
// xjiema = 0;
// cur = 0;
//}
auto cur = NowMs();
if (cur - starttime >= 100) {
cout << "bbbbbbb 1秒钟解码了" << xjiemacount * 10 << "次" << endl;
xjiemacount = 0;
starttime = cur;
}
//2.显示avframe
//加入xvideoview 相关,init的工作只需要做一次就好,因此添加一个
if (!is_init_win)
{
is_init_win = true;
xvideoview->Init(avframe->width,
avframe->height,
(XVideoView::Format)avframe->format);
}
xvideoview->DrawFrame(avframe);
//3.将avframe unref
av_frame_unref(avframe);
}
}
}
//这里为了安全期间,最好还是调用一下 av_packet_unref(avpacket);实际上不调用,应该也问题,除了在流媒体24小时不间断播放的情况下,avpacket的ref count 才有可能超过int的最大值
av_packet_unref(avpacket);//
}
}
//在循环的最后判断是否到了文件的最后.能走到这里的机会不多,
//我们假设 要读取的文件大小是4000字节,而我们每次读取1000个字节,那么第4次的时候,就刚好读取文件文件的结尾了,就能走到这个逻辑了
//实际上我们大多数情况下,文件的大小和 我们每次读取的字节,都不会刚好能整除的。
//这块逻辑实际上也是可以不写的,不写大不了再次读取一次,最终都是要会读取的文件的大小为0,也就是在前面根据 realfilebufreadcount <= 0 跳出循环
// 我们这里要加这个,主要是为了 循环读取这个文件。
if (readifstream.eof() == true) {
cout << "readifstream.eof() = true " << realfilebufreadcount << endl;
//添加循环用的。先需要调用 clear()函数,通过以 state 的值赋值,设置流错误状态标志。默认赋值 std::ios_base::goodbit ,它拥有的效果为清除所有错误状态标志。
//void clear( std::ios_base::iostate state = std::ios_base::goodbit );
readifstream.clear();
//这时候将 readifstream转到 文件开头
readifstream.seekg(0, std::ios::beg);
continue;//为了循环 continue;
//break;//如果不循环,则这里要break;
}
}
6. 将通过硬件加速得到的 在显卡中的 avframe 转成 拷贝到内存 能够播放的yuv420p的 avframe。
也就是说,我们还需要定义一个 AVFrame *, 我们这里叫做 send_avframe
int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags);
作用是:将src 转移 到 dst,转移后 dstavframe的格式 和 srcavframe的格式很大可能不一致。
核心作用
-
硬件数据到系统内存的转移
该函数负责将硬件解码器(如 GPU/NPU)输出的数据从硬件专用内存(如显存)复制到系统内存的 AVFrame 中,便于后续 CPU 处理。
示例场景:硬解后的 YUV 数据若需通过 OpenCV 处理或显示在 UI 框架中,必须通过此函数完成内存迁移。 -
格式兼容性适配
硬件解码器输出的像素格式可能与标准软件处理流程不兼容(如 NVIDIA 硬解的 NV12 格式需转换为 YUV420P)。此函数在转移过程中自动完成格式转换。
性能与限制
-
耗时问题:
转移数据涉及内存复制,高分辨率场景(如 4096x4096)可能产生显著延迟(实测达 24ms)2。若需直接使用 GPU 数据(如 UE4/Unity 渲染),应避免调用此函数,转而通过共享纹理等技术直接访问硬件内存26。 -
调用时机:
在解码流程中,需先通过avcodec_receive_frame
获取硬件帧,再调用此函数转移数据到系统帧。
实际测试:在windows 上转出来的 send_avframe的格式是 23,linesize[0],linesize[1]是有值的,23对应的是 AV_PIX_FMT_NV12,如下是说明:
AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
也就是说,通过 av_hwframe_transfer_data 得到的 avframe 格式是 AV_PIX_FMT_NV12。
如下是debug时候的结果。
那么我们如果要通过前几章 的 xvideoview 去show 这个avframe,还需要改动 xvideoview的逻辑,让其支持 显示 AV_PIX_FMT_NV12。或者得到dstavframe后,转换成我们前面 xvideoview支持的YUV420P格式,然后让其再显示。
7. 方案一 改动 xvideoview的逻辑,让其支持 渲染显示 AV_PIX_FMT_NV12
刚开始的想法是:
7.1. 在 SDL_CreateTexture 方法中参数 format为 SDL_PIXELFORMAT_NV12(对应 pixelformat 为 AV_PIX_FMT_NV12)。
7.2.那么在 使用 SDL_UpdateYUVTexture 传递 send_frame的 Y,U,V 就行了。
7.3.实际测试不行,原因是
SDL_UpdateYUVTexture设计用于平面YUV格式(如YUV420P),而NV12是半平面格式,导致参数传递错误。
extern DECLSPEC SDL_Texture * SDLCALL SDL_CreateTexture(SDL_Renderer * renderer,
Uint32 format,
int access, int w,
int h);
extern DECLSPEC int SDLCALL SDL_UpdateYUVTexture(SDL_Texture * texture,
const SDL_Rect * rect,
const Uint8 *Yplane, int Ypitch,
const Uint8 *Uplane, int Upitch,
const Uint8 *Vplane, int Vpitch);
7.4.那么就不能用 SDL_UpdateYUVTexture方法,我们再来回顾一下 NV12 的格式,如果我们把 data[0],data[1]中看成一段数据,调用SDL_UpdateTexture 呢?
extern DECLSPEC int SDLCALL SDL_UpdateTexture(SDL_Texture * texture,
const SDL_Rect * rect,
const void *pixels, int pitch);
这里要考虑字节对齐问题
8. 之前我们的还需要将解析avframe 存储 yuv文件。这里也要考虑 转换成 AV_PIX_FMT_NV12格式后的存储问题
这里也要考虑字节对齐问题
相关代码:
xvideoview.h
//
/// XVideoView 为 视频渲染接口类
/// 实现功能有如下三个:
/// 隐藏SDL实现,目的是如果今后不使用SDL实现了,可以使用OPENGL等实现,需要对客户隐藏
/// 渲染方案可替代
/// 线程安全
#pragma once
#include <mutex>
#include <thread>
#include <string>
#include <fstream>
struct AVFrame;
class XVideoView
{
public:
enum Format{
YUV420P = 0, /// 对应 pixfmt 中的为 AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
NV12 = 23, /// 对应 pixfmt 中的为 AV_PIX_FMT_NV12 planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
ARGB = 25, /// 对应 pixfmt 中的为 AV_PIX_FMT_ARGB < packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
RGBA = 26, ///< 对应 pixfmt 中的为 AV_PIX_FMT_RGBA packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
ABGR = 27, ///< 对应 pixfmt 中的为 AV_PIX_FMT_ABGR packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
BGRA = 28 ///< 对应 pixfmt 中的为 AV_PIX_FMT_BGRA packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
};
enum ViewType {
SDL_TYPE,
OPENGL_TYPE
};
/// <summary>
/// 如下的接口是要给 调用着使用的,因此需要public
/// </summary>
public:
//
/// 纯虚函数,初始化渲染窗口 线程安全
/// 子类必须实现,如果子类是XSDL,那么init方法里面就需要完成关于sdl的初始化实现
/// @param width 视频文件的分辨率 ,例如 1080 * 720,width的值就是1080
/// @param height 视频文件的分辨率 ,例如 1080 * 720,height的值就是720
/// @param fmt 类似 RGB888,YUV420P ,表示的是绘制的格式
/// @param winid 窗口句柄,如果为空,创建新窗口
/// @return 是否创建成功
///
//virtual bool Init(int width, int height, Format fmt = YUV420P, void* winid = nullptr) = 0;
virtual bool Init(int width, int height, Format fmt = YUV420P) = 0;
/// 这里将init 函数的 原本的 winid单独的拿出来,通过 setWinid接口设置,是为了在多窗口都显示视频的时候好处理
///且此接口应该是 不管是sdl 实现还是 opengl实现,设置 winid的方法,和获得winid的方法都应该一样,因此不需要设置成纯虚函数,在自己的cpp文件中就可以实现
void setWinid(void* winid);
void* getWinid();
//
/// 渲染图像 线程安全
///@para data 渲染的二进制数据
///@para linesize 一行数据的字节数,对于YUV420P就是Y一行字节数
/// linesize<=0 就根据宽度和像素格式自动算出大小
/// @return 渲染是否成功
virtual bool Draw(const unsigned char* data, int linesize = 0) = 0;
//清理所有申请的资源,包括关闭窗口
virtual void Close() = 0;
/// 处理窗口退出事件,这个方法主要是为了在不使用qt的windows的情况下,意味着创建的显示sdl的window是和 label没有关系的
/// 那么这个窗口的关闭是要接受 sdl的quit_event 事件,即:我们在 isexit 内部实现中调用 SDL_WaitEventTimeout
/// 那么在什么时候调用这个isexit方法呢?在qt的 timeevent中调用就OK了
virtual bool IsExit() = 0;
/// <summary>
///
/// 此方法在 xvideoview.cpp 中实现。
/// 此方法的目的是:在测试程序使用的 RGB或者YUV数据的时候,分开调用
/// Draw(const unsigned char* data, int linesize = 0) = 0;
/// 或者 virtual bool Draw(const unsigned char* y, int y_pitch,const unsigned char* u, int u_pitch,const unsigned char* v, int v_pitch) = 0;
/// 由于此函数内部 是调用的两个 线程安全的函数,因此函数实现中不能使用 _mutex lock,否则会导致死锁问题
/// </summary>
/// <param name="frame"></param>
/// <returns></returns>
bool DrawFrame(AVFrame* frame);
/// <summary>
///
/// </summary>
/// <param name="y"> Y 分量开始的指针</param>
/// <param name="y_pitch"> Y 分量一行的大小 ,也就是 avframe-linesize[0] 的大小 </param>
/// <param name="u">U 分量开始的指针</param>
/// <param name="u_pitch"> U 分量一行的大小 ,也就是 avframe-linesize[1] 的大小</param>
/// <param name="v">V 分量开始的指针</param>
/// <param name="v_pitch"> V 分量一行的大小 ,也就是 avframe-linesize[2] 的大小</param>
/// <returns></returns>
virtual bool Draw(
const unsigned char* y, int y_pitch,
const unsigned char* u, int u_pitch,
const unsigned char* v, int v_pitch
) = 0;
///打开文件和读取文件的操作,都通过 xvideoview 封装起来,user不需要知道实现细节。内部通过 ifstream 实现
//打开文件
bool openfile(std::string filepath);
//读取文件
AVFrame * readAVFrameFormfile();
public:
///工厂模式 ViewType 应该是一个枚举,包含支持的类型,比如说SDL, OPENGL,这一步让user 选择使用SDL 显示,还是使用 其他方式显示
///默认参数是SDL_TYPE,如果不传递参数,默认就是使用SDL 渲染图像
static XVideoView* create(ViewType type = SDL_TYPE);
protected:
int width_ = 0; //视频文件的分辨率 例如 1080 * 720,width_的值就是1080
int height_ = 0; //视频文件的分辨率 例如 1080 * 720,height_的值就是720
Format fmt_ = YUV420P; //视频文件的像素格式
std::mutex mtx_; //确保线程安全
int lablewidth_ = 0; //放置 视频的 lable 的宽和高
int lableheight_ = 0;
void* winid_ = nullptr;
//帧率相关
int render_fps_ = 0; //显示帧率
long long beg_ms_ = 0; //计时开始时间
int count_ = 0; //统计显示次数
public:
//当我们将widget的大小变化的时候,存放视频的 lable 控件的大小也要跟着变化,我们需要记录 lable的宽和高
void setscaleLable(int labelwidth, int lableheight);
int render_fps();
private:
std::ifstream ifs_;
AVFrame* avframe_ = nullptr;
uint8_t* NV12_data_ = nullptr;
public:
virtual ~XVideoView();
};
xvideoview.cpp
#include "xvideoview.h"
#include "xsdl.h"
using namespace std;
extern "C" {
#include "libavutil/frame.h"
}
void XVideoView::setWinid(void* winid)
{
this->winid_ = winid;
}
void* XVideoView::getWinid()
{
return this->winid_;
}
bool XVideoView::DrawFrame(AVFrame* frame)
{
if (frame == nullptr || frame->data[0] == nullptr) {
cout << "DrawFrame(AVFrame* frame) error because frame == nullptr || frame->data[0] == nullptr"<<endl;
return false;
}
///4.计算fps
count_++;
if (beg_ms_ <= 0)
{
beg_ms_ = clock();
}
//计算显示帧率
else if ((clock() - beg_ms_) / (CLOCKS_PER_SEC / 1000) >= 1000) //一秒计算一次fps
{
render_fps_ = count_;
count_ = 0;
beg_ms_ = clock();
}
int linesize = 0;
switch (frame->format)
{
case AV_PIX_FMT_YUV420P:
return Draw(frame->data[0],
frame->linesize[0],
frame->data[1],
frame->linesize[1],
frame->data[2],
frame->linesize[2]);
case AV_PIX_FMT_ARGB:
case AV_PIX_FMT_RGBA:
case AV_PIX_FMT_ABGR:
case AV_PIX_FMT_BGRA:
return Draw(frame->data[0], frame->linesize[0]);
case AV_PIX_FMT_NV12:
if (frame->linesize[0] <=0 || frame->height <=0 || frame->width <=0) {
cout << "DrawFrame error because frame->linesize[0] = " << frame->linesize[0]
<< " frame->height = " << frame->height
<< " frame->width = " << frame->width << endl;
return false;
}
linesize = frame->width;
if (!NV12_data_)
{
//最大支持4k的
NV12_data_ = new uint8_t[4096 * 2160 * 1.5];
//NV12_data_ = new uint8_t [frame->linesize[0] * frame->height * 1.5];
}
memset(NV12_data_, 0, frame->linesize[0] * frame->height * 1.5);
if (frame->linesize[0] == frame->width)
{
// 如果没有字节对齐问题,直接先copy avframe->data[0];然后再copy avframe->data[1]
memcpy(NV12_data_, frame->data[0], frame->linesize[0] * frame->height); //Y
memcpy(NV12_data_ + frame->linesize[0] * frame->height, frame->data[1], frame->linesize[1] * frame->height / 2); //UV
}
else //逐行复制
{
//这里字节有对齐问题的时候,为什么要这样 copy 呢?
//而且实验测试,当 avframe是 400 * 300 时候,字节对齐有问题时候,就是用这种方法就能解决。
for (int i = 0; i < frame->height; i++) //Y
{
memcpy(NV12_data_ + i * frame->width,
frame->data[0] + i * frame->linesize[0],
frame->width
);
}
for (int i = 0; i < frame->height / 2; i++) //UV
{
auto p = NV12_data_ + frame->height * frame->width;// 移位Y
memcpy(p + i * frame->width,
frame->data[1] + i * frame->linesize[1],
frame->width
);
}
}
return Draw(NV12_data_, linesize);
default:
break;
}
return false;
}
bool XVideoView::openfile(std::string filepath)
{
if (ifs_.is_open()) {
ifs_.close();
}
ifs_.open(filepath, ios_base::binary);
return ifs_.is_open();
}
AVFrame* XVideoView::readAVFrameFormfile()
{
if (width_<=0 || height_<=0 || ifs_.is_open() == false) {
//qDebug() << "readAVFrameFormfile error because width_ = " << width_
// << " height_ = " << height_
// << " ifs_.is_open() = " << ifs_.is_open() << " and return nullptr";
return nullptr;
}
//这里分为两步,第一步,分配avframe空间。第二步:从ifs中给avframe空间读取数据。
// 第一步的问题:
//如果不存在,则肯定是要创建的,且要将avframe的三要素设定。
//如果avframe_已经存在了,那么还需要创建吗?如果已经存在了,且三要素没有变化,就不需要创建了;如果存在,但是三要素有变化,则需要先销毁之前的,然后再创建
if (avframe_ !=nullptr) {
//存在情况下,则要判断 三要素是否已经改动,如果改动了销毁后,重新 av_frame_alloc()
if (avframe_->width != width_ || avframe_->height != height_ || avframe_->format != fmt_) {
av_frame_free(&avframe_);
}
}
if(avframe_ == nullptr) {
//不存在,不管是第一次,还是三要素变化了,avframe_都会变成nullptr
avframe_ = av_frame_alloc();
if (avframe_ == nullptr) {
//失败了,直接return
cout << "av_frame_alloc fail,return nullptr";
return nullptr;
}
avframe_->width = width_;
avframe_->height = height_;
avframe_->format = fmt_;
//这里根据要读取的视频的 分辨率 和 fmt,设置 avframe linesize,这里是有问题的,要保证 原始的 avframe的linesize[0] 和width一致,没有考虑字节对齐问题
if (fmt_ == AV_PIX_FMT_YUV420P) {
avframe_->linesize[0] = avframe_->width;
avframe_->linesize[1] = avframe_->width / 2;
avframe_->linesize[2] = avframe_->width / 2;
}
else if (fmt_ == AV_PIX_FMT_ARGB || fmt_ == AV_PIX_FMT_RGBA || fmt_ == AV_PIX_FMT_ABGR || fmt_ == AV_PIX_FMT_BGRA) {
avframe_->linesize[0] = avframe_->width * 4;
}
int ret = av_frame_get_buffer(avframe_, 0);
if (ret < 0) {
//av_frame_get_buffer 失败了
//1.打印log
char errbuf[1024] = { 0 };
av_strerror(ret, errbuf, sizeof(errbuf) - 1);
cout << "av_frame_get_buffer func error errbuf = " << errbuf;
//2.释放已经申请的avframe 空间
av_frame_free(&avframe_);
//3.返回 nullptr
return nullptr;
}
}
//到这里分配空间的工作就已经完成了,那么接下来就应该是读取了,实际上每次都是读取一张图片的大小
if (avframe_->format == AV_PIX_FMT_YUV420P) {
ifs_.read((char *)avframe_->data[0],avframe_->width * avframe_->height);
ifs_.read((char*)avframe_->data[1], avframe_->width * avframe_->height /4);
ifs_.read((char*)avframe_->data[2], avframe_->width * avframe_->height /4);
}
else if (fmt_ == AV_PIX_FMT_ARGB || fmt_ == AV_PIX_FMT_RGBA || fmt_ == AV_PIX_FMT_ABGR || fmt_ == AV_PIX_FMT_BGRA) {
ifs_.read((char*)avframe_->data[0], avframe_->width * avframe_->height * 4);
//qDebug()<< " read count = " << ifs_.gcount();
}
//这里还要处理一下读取到最后一张图片的操作,如果不是循环播放,这个return nullptr
if (ifs_.gcount()==0) {
//说明读取到最后啦,
//return nullptr;
//如果要循环读取
if (ifs_.eof()) {
ifs_.clear();
ifs_.seekg(0, std::ios::beg);
}
}
return avframe_;
}
XVideoView* XVideoView::create(ViewType type)
{
switch (type)
{
case XVideoView::ViewType::SDL_TYPE:
cout << "XVideoView create XSDL.class";
return new XSDL();
break;
default:
break;
}
return nullptr;
}
void XVideoView::setscaleLable(int labelwidth, int lableheight)
{
this->lablewidth_ = labelwidth;
this->lableheight_ = lableheight;
}
int XVideoView::render_fps() {
return render_fps_;
}
XVideoView::~XVideoView()
{
if (NV12_data_)
delete NV12_data_;
NV12_data_ = nullptr;
}
xsdl.h
#pragma once
#include "xvideoview.h"
#include <sdl/SDL.h>
#include <iostream>
class XSDL :
public XVideoView
{
public:
/// 初始化渲染窗口 线程安全
/// @para w 窗口宽度
/// @para h 窗口高度
/// @para fmt 绘制的像素格式
/// @return 是否创建成功
bool Init(int w, int h,
Format fmt = YUV420P) override;
//
/// 渲染图像 线程安全
///@para data 渲染的二进制数据
///@para linesize 一行数据的字节数,对于YUV420P就是Y一行字节数
/// linesize<=0 就根据宽度和像素格式自动算出大小
/// @return 渲染是否成功
bool Draw(const unsigned char* data, int linesize = 0) override;
void Close() override;
bool IsExit() override;
bool Draw(
const unsigned char* y, int y_pitch,
const unsigned char* u, int u_pitch,
const unsigned char* v, int v_pitch
) override;
private:
SDL_Window* sdlwindow_ = nullptr;
SDL_Renderer* sdlrenderer_ = nullptr;
SDL_Texture* sdltexture_ = nullptr;
};
xsdl.cpp
#include "xsdl.h"
using namespace std;
static bool InitVideo()
{
//由于我们只希望调用一次,可以使用静态变量完成.
///这里为什么要锁住呢?如果有多个线程 想要 init,我们这里只能让一个线程init成功。
///那么问题是:什么情况下,有多个线程都想要init呢?这里是有疑问的
static bool is_first = true;
static mutex mux;
unique_lock<mutex> sdl_lock(mux);
if (!is_first) {
return true;
}
is_first = false;
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0 )
{
cout << "SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) error " << SDL_GetError();
cout << SDL_GetError() << endl;
return false;
}
//锯齿问题的解决
//SDL_SetHint函数是SDL库中的一个函数,用于设置特定选项的提示值。设置不同的key,会起到不同的作用,不同的key,对应的value也不同
//我们通过 设置 影响缩放质量的 key :SDL_HINT_RENDER_SCALE_QUALITY, 的值的为 "best" 。来说明当缩放时,我们需要最好的质量。以解决 锯齿问题
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
return true;
}
bool XSDL::Init(int w, int h, Format fmt)
{
cout << "xsdl init call";
Uint32 sdlformat = SDL_PIXELFORMAT_IYUV;
int retbool = true;
if (w <= 0 || h <= 0) {
cout << "func XSDL::Init error because w = " << w <<" h = " << h;
return false;
}
//第一步,初始化 sdl init,这个在整个开发中,我们只调用一次,因此可以独立的写出来,写成static的
retbool = InitVideo();
if (retbool == false) {
return false;
}
//如果使用当前窗口,播放多个视频文件,假设一个一个播放,如果不释放 sdltexture_ 和 sdlrenderer_,
//就每当播放一个视频文件,就创建一个sdltexture_ 和 sdlrenderer_,造成内存泄漏,因此每次init的时候要判断一下,如果有这两个成员变量,直接变成 销毁,并置为nullptr
//那么问题是,SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) 和 sdlwindow_ 要不要创建多次呢?
//首先,很显然: SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO),是不需要弄多次的。
///问题是sdlwindow 需不需要创建多次呢?如果是使用 SDL_CreateWindowFrom(win_id)显然 也不需要多次,因为 win_id所代表的 label的宽和高,都会重新设置
//那么使用 SDL_CreateWindow 创建的sdl_window_ 需要吗?这里实际上也不需要,创建的时候参数 SDL_WINDOW_RESIZABLE就决定了
//因此这里只需要 将 sdltexture_ 和 sdlrenderer_ 销毁。
///这里还是有一个疑问: 为什么要销毁sdlrenderer_呢?创建sdlrenderer_的时候,又不需要width,height,format。 理论上销毁 sdltexture_ 就可以了,
if (sdltexture_) {
SDL_DestroyTexture(sdltexture_);
sdltexture_ = nullptr;
}
if (sdlrenderer_) {
SDL_DestroyRenderer(sdlrenderer_);
sdlrenderer_ = nullptr;
}
//第二步 确保线程安全,记录宽,高,
unique_lock<mutex> sdl_lock(mtx_);
width_ = w; //记录窗口宽度,由于不管使用SDL还是OPENGL,宽,高,像素格式这三要素都是需要的,因此这三个变量记录在 xsdl的父类xvideoview中
height_ = h; //记录窗口高度
fmt_ = fmt; //记录像素格式
//第三步,创建 sdlwindow,这里也是要线程安全。那么创建的 sdlwindow 是sdl特有的变量,因此放在xsdl.h中
//sdlwindows 理论上也只能创建一次,
if (sdlwindow_ == nullptr) {
if (this->winid_ == nullptr) {
sdlwindow_ = SDL_CreateWindow("使用sdl技术显示视频文件",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
width_,
height_,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (nullptr == sdlwindow_) {
//创建sdlwindow失败
cout << "SDL_CreateWindow SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,width_,height_,SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLEerror "
<< " width_ = " << width_ << " height_ = " << height_
<< SDL_GetError();
goto end;
}
}
else {
sdlwindow_ = SDL_CreateWindowFrom(this->winid_);
if (nullptr == sdlwindow_) {
//创建sdlwindow失败
cout << "SDL_CreateWindowFrom error win_id = " << this->winid_ << " SDL_GetError = " << SDL_GetError();
goto end;
}
}
}
//第四步,创建 sdlrenderer_,这里也是要线程安全。那么创建的 sdlrenderer_ 是sdl特有的变量,因此放在xsdl.h中
sdlrenderer_ = SDL_CreateRenderer(sdlwindow_, -1, SDL_RENDERER_ACCELERATED);
if (nullptr == sdlrenderer_) {
cout << "SDL_CreateRenderer(sdlwindow_, -1, SDL_RENDERER_ACCELERATED) error try to use SDL_RENDERER_SOFTWARE " << " SDL_GetError = " << SDL_GetError();
sdlrenderer_ = SDL_CreateRenderer(sdlwindow_, -1, SDL_RENDERER_SOFTWARE);
if (nullptr == sdlrenderer_) {
cout << "SDL_CreateRenderer(sdlwindow_, -1, SDL_RENDERER_ACCELERATED) and use SDL_RENDERER_SOFTWARE error " << " SDL_GetError = " << SDL_GetError();
goto end;
}
}
//第五步,创建 sdltexture_,这里也是要线程安全。那么创建的 sdltexture_ 是sdl特有的变量,因此放在xsdl.h中
switch (fmt_)
{
case XVideoView::YUV420P:
sdlformat = SDL_PIXELFORMAT_IYUV;
break;
case XVideoView::NV12:
sdlformat = SDL_PIXELFORMAT_NV12;
break;
case XVideoView::ARGB:
sdlformat = SDL_PIXELFORMAT_ARGB32;
break;
case XVideoView::RGBA:
sdlformat = SDL_PIXELFORMAT_RGBA32;
break;
case XVideoView::ABGR:
sdlformat = SDL_PIXELFORMAT_ABGR32;
break;
case XVideoView::BGRA:
sdlformat = SDL_PIXELFORMAT_BGRA32;
break;
default:
break;
}
//第六步,创建 sdltexture_,这里也是要线程安全。那么创建的 sdltexture_ 是sdl特有的变量,因此放在xsdl.h中
sdltexture_ = SDL_CreateTexture(sdlrenderer_,
sdlformat,
SDL_TEXTUREACCESS_STREAMING,
width_,
height_);
if (sdltexture_ == nullptr) {
cout << " SDL_CreateTexture(sdlrenderer_,sdlformat,SDL_TEXTUREACCESS_STREAMING,width_,height_) error "
<< " sdlformat = " << sdlformat
<< " width_ = " << width_
<< " height_ = " << height_
<< " SDL_GetError = " << SDL_GetError();
goto end;
}
//如果没有问题,sdl init就完成了,最终返回true.
return true;
//一旦有error就会走到end 这里,不管哪一步有error,最终都是返回false
end:
if (sdltexture_ != nullptr) {
SDL_DestroyTexture(sdltexture_);
sdltexture_ = nullptr;
}
if (sdlrenderer_ != nullptr) {
SDL_DestroyRenderer(sdlrenderer_);
sdlrenderer_ = nullptr;
}
if (sdlwindow_ != nullptr) {
SDL_DestroyWindow(sdlwindow_);
sdlwindow_ = nullptr;
}
SDL_Quit();
return false;
}
//sdl 的整个渲染过程,我们这里的核心是调用 SDL_UpdateYUVTexture方法,或者SDL_UpdateTexture方法,线程安全的
bool XSDL::Draw(const unsigned char* data, int linesize)
{
int ret = 0;
if (data == nullptr) {
cout << "XSDL::Draw(const char* data, int linesize) error because data = nullptr";
return false;
}
if (linesize <= 0) {
cout << "XSDL::Draw(const char* data, int linesize) error because linesize = " << linesize;
return false;
}
unique_lock<mutex> sdl_lock(mtx_);
if (!sdlwindow_ || !sdltexture_ || !sdlrenderer_ || width_ <= 0 || height_ <= 0) {
cout << "draw error ";
return false;
}
if (fmt_ == XVideoView::YUV420P) {
//如果是yuv格式的,调用 SDL_UpdateYUVTexture 方法
ret = SDL_UpdateYUVTexture(sdltexture_,
nullptr,
(const Uint8*)data, //Y 分量数据指针
linesize, // Y分量每行字节数,在没有对齐问题的情况下,linesize = width_,这里传递进来的linesize应该是考虑对齐问题后的值
(const Uint8*)data + linesize * height_, // U 分量数据指针,YUV420P的存储是先将 Y分量放置完毕,才会放置U分量,因此指针指向 _yuvdata开头 + 全部Y分量的大小
linesize / 2, // U分量 每行字节数 ,这里是_picwidth / 2,是因为 YUV420P的结构体是 Y分量与CbCr分量的水平方向比例是2:1(每2列就有1组CbCr分量);;Y分量与CbCr分量的垂直方向比例是2:1(每2行就有1组CbCr分量);;Y分量与CbCr分量的总比例是4 : 1
((const Uint8*)data + linesize * height_) + (linesize * height_ / 4),// V 分量数据指针,这里可以理解为 指针开始的位置 + ( Y分量 + U分量的) 所有大小
linesize / 2);// V分量 每行字节数
}
else if (fmt_ == XVideoView::ARGB
|| fmt_ == XVideoView::RGBA
|| fmt_ == XVideoView::ABGR
|| fmt_ == XVideoView::BGRA
|| fmt_ == XVideoView::NV12) {
//如果是RGB格式的,调用 SDL_UpdateTexture 方法
ret = SDL_UpdateTexture(sdltexture_,
NULL,
data,
linesize //一行 y的字节数
);
}
if (ret < 0 ) {
cout << "SDL_UpdateYUVTexture or SDL_UpdateTexture error fmt_ = " << fmt_
<< " linesize = " << linesize
<< " width_ = " << width_
<< " height_ = " << height_
<< " SDL_GetError () = " << SDL_GetError();
return false;
}
ret = SDL_RenderClear(sdlrenderer_);
if (ret < 0) {
cout << "SDL_RenderClear(sdlrenderer_) error "
<< " SDL_GetError () = " << SDL_GetError();
return false;
}
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
//这里 SDL_RenderCopy 第三个参数,const SDL_Rect * srcrect,意思是,你要将 texture的哪些部分拿出来显示,传递NULL,表示整个texture
//第四个参数,const SDL_Rect * dstrect,意思是:要显示的数据,应该放置在window的什么位置。
//这里第四个参数就有说头了,当我们将 widget 的大小变化的时候,希望 视频也跟着变化,因此这里要用 存放视频的lable的大小
//容错处理,实际上,在 xvideoviewrefactory的构造函数中我们 我们的设置了lable的宽和高
//那么为什么还要加这个容错处理呢?这是因为我们原先的打算就是让xsdl.cpp对 测试者(也就是 xvideoviewrefactory隐藏的)
//我们并不能保证 测试开发人员 一定会记录 labelwidth 和labelheight,因此多加了一层保护
//实现时,先要判断调用者,是否使用了 winid 去创建 sdlwindow,如果没有使用,那么就是后面两个参数都是 nullptr
if (winid_ == nullptr) {
ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, nullptr, nullptr);
}
else {
if (this->lablewidth_ <= 0) {
this->lablewidth_ = width_;
}
if (this->lableheight_ <= 0) {
this->lableheight_ = height_;
}
rect.w = lablewidth_;//要显示的位置和 当前lable的宽高保持一致
rect.h = lableheight_;
//这里不能如果要随着 user 将 widget 变大变小,要弄一个数组,记录各个 widget的大小,这里只是简单的用 整个texture(第三个参数),填充整个 sdlrenderer(第四个参数)
//ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, NULL, &rect);
ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, NULL, nullptr);
}
if (ret < 0) {
cout << "SDL_RenderCopy(sdlrenderer_, sdltexture_, NULL, &rect) "
<< " SDL_GetError () = " << SDL_GetError();
return false;
}
SDL_RenderPresent(sdlrenderer_);
return true;
}
void XSDL::Close() {
//确保线程安全
unique_lock<mutex> sdl_lock(mtx_);
if (sdltexture_ != nullptr) {
SDL_DestroyTexture(sdltexture_);
sdltexture_ = nullptr;
}
if (sdlrenderer_ != nullptr) {
SDL_DestroyRenderer(sdlrenderer_);
sdlrenderer_ = nullptr;
}
if (sdlwindow_ != nullptr) {
SDL_DestroyWindow(sdlwindow_);
sdlwindow_ = nullptr;
}
}
bool XSDL::IsExit() {
SDL_Event ev;
//每次等待1ms,如果没有 quit事件发生,就返回false,有quit事件发生,就返回true
SDL_WaitEventTimeout(&ev, 1);
if (ev.type == SDL_QUIT) {
return true;
}
return false;
}
bool XSDL::Draw(
const unsigned char* y, int y_pitch,
const unsigned char* u, int u_pitch,
const unsigned char* v, int v_pitch) {
//参数检查
if (!y || !u || !v) {
cout <<"func draw yuv error because (!y || !u || !v) ";
//return false;
}
//qDebug() << "y_pitch = " << y_pitch << " u_pitch = " << u_pitch << " v_pitch = " << v_pitch;
unique_lock<mutex> sdl_lock(mtx_);
if (!sdlwindow_ || !sdltexture_ || !sdlrenderer_ || width_ <= 0 || height_ <= 0) {
cout << "draw error ";
return false;
}
//复制内存到显显存
cout << "_fmt = " << fmt_ << endl;
auto ret = SDL_UpdateYUVTexture(sdltexture_,
nullptr,
y, y_pitch,
u, u_pitch,
v, v_pitch);
if (ret != 0)
{
cout << "Draw SDL_UpdateYUVTexture error y_pitch = " << y_pitch
<< " u_pitch = " << u_pitch
<< " v_pitch = " << v_pitch
<< " "
<< SDL_GetError()
<< endl;
return false;
}
//清空屏幕
ret = SDL_RenderClear(sdlrenderer_);
if (ret < 0) {
cout << "Draw SDL_RenderClear(sdlrenderer_) error "
<< " SDL_GetError () = " << SDL_GetError();
return false;
}
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
//这里 SDL_RenderCopy 第三个参数,const SDL_Rect * srcrect,意思是,你要将 texture的哪些部分拿出来显示,传递NULL,表示整个texture
//第四个参数,const SDL_Rect * dstrect,意思是:要显示的数据,应该放置在window的什么位置。
//这里第四个参数就有说头了,当我们将 widget 的大小变化的时候,希望 视频也跟着变化,因此这里要用 存放视频的lable的大小
//容错处理,实际上,在 xvideoviewrefactory的构造函数中我们 我们的设置了lable的宽和高
//那么为什么还要加这个容错处理呢?这是因为我们原先的打算就是让xsdl.cpp对 测试者(也就是 xvideoviewrefactory隐藏的)
//我们并不能保证 测试开发人员 一定会记录 labelwidth 和labelheight,因此多加了一层保护
//实现时,先要判断调用者,是否使用了 winid 去创建 sdlwindow,如果没有使用,那么就是后面两个参数都是 nullptr
if (winid_ == nullptr) {
ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, nullptr, nullptr);
}
else {
if (this->lablewidth_ <= 0) {
this->lablewidth_ = width_;
}
if (this->lableheight_ <= 0) {
this->lableheight_ = height_;
}
rect.w = lablewidth_;//要显示的位置和 当前lable的宽高保持一致
rect.h = lableheight_;
//这里 如果要跟随者 user 将界面变大变小 实现widgetUI 也跟着变化,那么需要弄多个数组,记录变大变小后的 size
//ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, nullptr, &rect);
ret = SDL_RenderCopy(sdlrenderer_, sdltexture_, nullptr, nullptr);
}
if (ret < 0) {
cout << "Draw SDL_RenderCopy(sdlrenderer_, sdltexture_, NULL, &rect) "
<< " SDL_GetError () = " << SDL_GetError();
return false;
}
SDL_RenderPresent(sdlrenderer_);
return true;
}
myutils.h
#pragma once
#include <ctime>
#include <iostream>
#include <thread>
void MSleep(unsigned int ms);
//获取当前时间戳 毫秒
long long NowMs();
myutils.cpp
#include "myutils.h";
using namespace std;
void MSleep(unsigned int ms)
{
auto beg = clock();
for (int i = 0; i < ms; i++)
{
this_thread::sleep_for(1ms);
if ((clock() - beg) / (CLOCKS_PER_SEC / 1000) >= ms)
break;
}
}
// clock()函数的原型是clock_t clock(void);,
// 它返回一个clock_t类型的值,表示程序从启动到函数调用时消耗的CPU时间,单位是时钟周期数。
// 这个值并不直接代表实际的时间单位(如秒或毫秒),需要通过CLOCKS_PER_SEC进行转换
//对于 CLOCKS_PER_SEC,在windows上是1000(表示的是毫秒),在linux上是 1000000(表示的是微秒)
long long NowMs()
{
return clock() / (CLOCKS_PER_SEC / 1000);
}
118ParseH264ToAVpacketToAVFrameThoughHwDeviceSimple.cpp
// 117ParseH264ToAVpacketToAVFrameAndUseSDLShowAVFrame.cpp
// 1. 从h264 裸流中 通过ffmpeg方法 中获得 avpacket
// 2. 将获得的avpacket 转换成 avframe
// 3. 将avframe存储成 xxx.yuv file
// 4. 将avframe 通过sdl 播放出来
//
#include <iostream>
#include <fstream>
using namespace std;
#include "xsdl.h"
#include "myutils.h"
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavutil/error.h"
}
#define readfilebufsize 4096 //从h264文件中每次读取的最大字节数
#undef main
int main()
{
int avframenumber = 0;
///加入xvideoview 相关。
XVideoView* xvideoview = XVideoView::create();
bool is_init_win = false;// xvideoview的init只做一次
//测试一下多线程对于解码的影响,在解码的开始记录时间,然后计算一秒中,解码了多少次
long long starttime = NowMs();//测试一下多线程对于解码的影响,
int xjiemacount = 0;//测试一下多线程对于解码的影响,
int ret = 0;
//0.要读取的文件
//ifstream readifstream("./118/400_300_25.h264", ios_base::binary);
ifstream readifstream("./118/test.h264", ios_base::binary);
//ofstream writefstream("./118/400_300_25ruanjiema_yuv420p.yuv", ios_base::binary);
ofstream writefstream("./118/400_300_25yingjiema_nv12.yuv", ios_base::binary);
if (readifstream.is_open() == false) {
cout << "open read file error" << endl;
return -1;
}
if (writefstream.is_open() == false) {
cout << "open write file error" << endl;
return -1;
}
//1.创建一个 对应解析器上下文 - AVCodecParserContext
int codec_id = AV_CODEC_ID_H264;
AVCodecParserContext* avcodecParseContext = av_parser_init(codec_id);
//2.创建解码器 和 解码器上下文。
AVCodec* avcodec = avcodec_find_decoder((enum AVCodecID)codec_id);
AVCodecContext* avcodecContext = avcodec_alloc_context3(avcodec);
avcodecContext->gop_size = 25;
avcodecContext->max_b_frames = 0; //让 b 帧的数量变成0,也就是不要b帧。可以不设置
avcodecContext->thread_count = 16;//测试开启多线程下,一秒中解析多少次。
//10. 硬解码相关
AVBufferRef* hwbuffer = nullptr;//硬加码上下文
ret = av_hwdevice_ctx_create(&hwbuffer, AVHWDeviceType::AV_HWDEVICE_TYPE_DXVA2, nullptr, nullptr, 0);
avcodecContext->hw_device_ctx = av_buffer_ref(hwbuffer);
av_buffer_unref(&hwbuffer);//顺手就将 hwbuffer 清理了。
//3.打开编码器
ret = avcodec_open2(avcodecContext, nullptr, nullptr);
if (ret != 0) {
cout << "func avcodec_open2() fail" << endl;
return -2;
}
//4.创建要读取的数据的缓存
uint8_t readfilebuf[readfilebufsize + AV_INPUT_BUFFER_PADDING_SIZE] = { 0 };
cout << "readfilebuf sizeof(readbuf) = " << sizeof(readfilebuf) << endl; // 结果是4,测试sizeof(指针大小用的,和本代码无关)
//5.1创建avpakcet,目的是 通过av_parser_parse2方法 ,从解码器中 给avpacket的data 和size赋值.
AVPacket* avpacket = av_packet_alloc();
//5.1创建avframe ,目的是 通过解码器从 avpacket中获得avframe ,注意这里我们并没有设置任何的avframe的分辨率,格式等信息
AVFrame* avframe = av_frame_alloc();
//10 硬编码相关。如果硬编码开启的,
// 那么 ,我们 从avcodecContext 获得的avframe 是NV12格式的,
// 我们 定义的这个 hw_avframe 用于在硬编码阶段 通过avcodec_receive_frame解析到的avframe
//int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *hw_avframe);
AVFrame* hw_avframe = av_frame_alloc();
//6.我们这里为了看一下使用的CPU 和 GPU 的使用情况,需要用一个比较大的文件,或者循环的解析这个文件。
//这里使用循环解析的方法,那么这里最好的方案是知道文件的大小,如果每次读取的数据之和 和 文件大小一样了,则需要gseek 到文件开始文件,重新读取
//readifstream.seekg(0, std::ios::end); // 指针移至末尾
//std::streampos fileallsize = readifstream.tellg(); // C++中tellg()是输入流文件定位函数,主要用于获取当前输入流指针的位置,由于在前面,我们是从文件开头位置 移动到 末尾,因此通过tellg() 就能获得文件的大小
//cout << "fileallsize = " << fileallsize << endl; //单位是字节。 这个可以通过每次read 后,得到的gcount 相加,和 fileallsize 的值 比较,判断是否读取到了文件结尾
//readifstream.seekg(0, std::ios::beg); // 重置指针到开头
//6. 开始读取文件直到文件读取完成,这里为了测试,要写一个循环,让一直读取h264文件
while (true) {
readifstream.read((char*)readfilebuf, readfilebufsize);
if (readifstream.bad() == true) {
cout << "read file bad" << endl;
break;
}
int realfilebufreadcount = readifstream.gcount(); // gcount() 返回最后一次非格式化输入操作(如 read()、get()、getline())实际读取的字符数(字节数)
if (realfilebufreadcount <= 0) { //如果读取的数据是<=0的,说明整个h264文件已经读取完成了,或者读取h264文件的时候出现了错误了,那么就要直接退出
cout << "read file break count = " << realfilebufreadcount << endl;
//添加循环用的。先需要调用 clear()函数,通过以 state 的值赋值,设置流错误状态标志。默认赋值 std::ios_base::goodbit ,它拥有的效果为清除所有错误状态标志。
//void clear( std::ios_base::iostate state = std::ios_base::goodbit );
//readifstream.clear();
这时候将 readifstream转到 文件开头
//readifstream.seekg(0, std::ios::beg);
//continue; //为了循环 continue;
break;//如果不循环,则这里要break;
}
//到这里说明读取到了 realbufreadcount 个字节的数据
cout << "realfilebufreadcount = " << realfilebufreadcount << endl;
//7. 根据读取到的字节,解析这段字节,解析出来avpacket.
//我们知道当 h264 数据是 大于 4096字节时,第一次读取的一定是4096 个字节。而从4096中肯定是能解析出来很多个avpacket的
//而解析的方法是使用 av_parser_parse2方法,av_parser_parse2方法的返回值是已经解析了多少个字节。
//因此这里要弄一个循环,让 realfilebufreadcount = (从文件读取到的 realfilebufreadcount字节) - (每次av_parser_parse2方法解析过的字节)
//如果 realbufreadcount 大于0,就可以继续解析,如果
uint8_t* tempreadfilebuf = readfilebuf;//让 tempreadfilebuf 指向 readfilebuf的指针。这里为什么不直接用readfilebuf呢?从功能实现上直接用buf也是可以的,但是这是C语言的基本功,尽量不要用原始指针。而是找一个复制的指针。
while (realfilebufreadcount > 0) {
int parsebufsize = av_parser_parse2(
avcodecParseContext,
avcodecContext,
&avpacket->data,
&avpacket->size,
tempreadfilebuf,
realfilebufreadcount,
0, 0, 0);
realfilebufreadcount = realfilebufreadcount - parsebufsize;
tempreadfilebuf = tempreadfilebuf + parsebufsize;
//这时候还要判断是否packet的size是大于0的,说明是正常截取了一段avpacket数据,因为av_parser_parse2方法即使分析据,也并不能保证真正的解析到 avpacket
if (avpacket->size > 0) {
cout << "avpacket->size = " << avpacket->size << endl;
// 如果avpacket 的size 大于0,说明已经有了avpacket了,也就是说,到这里,已经对h264进行了分离,得到了avpacket
// 那么下来,我们就需要将 对avpacket进行处理,在最后,记得调用av_packet_unref(avpacket);
// 8. 将avpacket发送给 avcodecContext
ret = avcodec_send_packet(avcodecContext, avpacket);
if (ret < 0) {
//已发送空包(pkt=NULL)触发编解码器刷新,且后续无新数据可处理(流结束标志)。
//说明 tempreadfilebuf 中的数据已经读取完成了,那么要continue 还是break呢?
//这里应该是continue比较合理,就是我们依赖 将 tempreadfilebuf 读取完成,跳出循环的条件应该是 realfilebufreadcount > 0
cout << "avcodec_send_packet return error " << endl;
break;;
}
if (ret == 0) {
// FFmpeg3 版本后解码接口改成了avcodec_send_packet和avcodec_receive_frame,这两个接口需要配合使用。
// 当发送一个packet后,可能需要多次调用avcodec_receive_frame来获取所有解码后的帧,尤其是当解码器内部有缓存时。
while (true) {
// 9.正确的发送数据到 解码器了,那么就通过 avcodec_receive_frame 中得到avframe,这里还是写一个循环
if (avcodecContext->hw_device_ctx != nullptr) {
//开启了硬件加速,那么将解析的数据放在 hw_avframe 中,
// hw_avframe 的 数据格式为 AV_PIX_FMT_DXVA2_VLD。
// 我们需要将hw_avframe AV_PIX_FMT_DXVA2_VLD格式的数据 转换到 avframe 中
ret = avcodec_receive_frame(avcodecContext, hw_avframe);
if (ret < 0) {
cout << "hw device func avcodec_receive_frame error" << endl;
break;
}
if (ret == 0) {
//将hw_avframe的数据转到 avframe
cout << "hw device hw_avframe->format = " << hw_avframe->format << endl;
int ret_av_hwframe_transfer_data = av_hwframe_transfer_data(avframe, hw_avframe, 0);
if (ret_av_hwframe_transfer_data < 0) {
cout << "hw device func av_hwframe_transfer_data error" << endl;
break;
}
if (ret_av_hwframe_transfer_data == 0) {
//得到正常的 avframe
//1.存储数据。
//存储 AV_PIX_FMT_NV12 格式的。
//那么就要明白 AV_PIX_FMT_NV12 格式是啥样子的,才好存储。
//AV_PIX_FMT_NV12 格式参考 : https://www.cnblogs.com/mjios/p/14686970.html
// Y Y Y Y
// Y Y Y Y
// U V U V
if (avframe->linesize[0] == avframe->width) {
//没有字节对齐问题
writefstream.write((char*)avframe->data[0], avframe->linesize[0] * avframe->height);
writefstream.write((char*)avframe->data[1], avframe->linesize[1] * avframe->height / 2);
}
else {
//有字节对齐问题时候的处理
for (int j = 0; j < avframe->height; j++) {
writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width);
}
}
}
}
}
else {
//没有开启硬件加速
ret = avcodec_receive_frame(avcodecContext, avframe);
if (ret < 0) {
cout << "software func avcodec_receive_frame error" << endl;
break;
}
if (ret == 0) {
//1.存储数据
if (avframe->linesize[0] == avframe->width) {
//没有字节对齐问题
writefstream.write((char*)avframe->data[0], avframe->linesize[0] * avframe->height);
writefstream.write((char*)avframe->data[1], avframe->linesize[1] * avframe->height / 2);
writefstream.write((char*)avframe->data[2], avframe->linesize[2] * avframe->height / 2);
}
else {
//有字节对齐问题时候的处理
// 先写完Y,再写U,再写V
for (int j = 0; j < avframe->height; j++) {
writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width / 2);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(avframe->data[2] + j * avframe->linesize[2]), avframe->width / 2);
}
}
}
}
//2.show 数据
if (!is_init_win)
{
is_init_win = true;
xvideoview->Init(avframe->width,
avframe->height,
(XVideoView::Format)avframe->format);
}
xvideoview->DrawFrame(avframe);
//3. 记录
//解码相关。计算1秒钟解码了多少次
//正常写法,但是有我们测试的视频太短了,不能说明问题
xjiemacount++;
auto cur = NowMs();
if (cur - starttime >= 1000) {
cout << "1秒钟解码了" << xjiemacount << "次" << endl;
xjiemacount = 0;
starttime = cur;
}
}
}
//这里为了安全期间,最好还是调用一下 av_packet_unref(avpacket);实际上不调用,应该也问题,除了在流媒体24小时不间断播放的情况下,avpacket的ref count 才有可能超过int的最大值
av_packet_unref(avpacket);//
}
}
//在循环的最后判断是否到了文件的最后.能走到这里的机会不多,
//我们假设 要读取的文件大小是4000字节,而我们每次读取1000个字节,那么第4次的时候,就刚好读取文件文件的结尾了,就能走到这个逻辑了
//实际上我们大多数情况下,文件的大小和 我们每次读取的字节,都不会刚好能整除的。
//这块逻辑实际上也是可以不写的,不写大不了再次读取一次,最终都是要会读取的文件的大小为0,也就是在前面根据 realfilebufreadcount <= 0 跳出循环
// 我们这里要加这个,主要是为了 循环读取这个文件。
if (readifstream.eof() == true) {
cout << "readifstream.eof() = true " << realfilebufreadcount << endl;
//添加循环用的。先需要调用 clear()函数,通过以 state 的值赋值,设置流错误状态标志。默认赋值 std::ios_base::goodbit ,它拥有的效果为清除所有错误状态标志。
//void clear( std::ios_base::iostate state = std::ios_base::goodbit );
//readifstream.clear();
这时候将 readifstream转到 文件开头
//readifstream.seekg(0, std::ios::beg);
//continue;//为了循环 continue;
break;//如果不循环,则这里要break;
}
}
//刷新缓冲
avcodec_send_packet(avcodecContext, NULL);
while (true) {
// 9.正确的发送数据到 解码器了,那么就通过 avcodec_receive_frame 中得到avframe,这里还是写一个循环
if (avcodecContext->hw_device_ctx != nullptr) {
//开启了硬件加速,那么将解析的数据放在 hw_avframe 中,
// hw_avframe 的 数据格式为 AV_PIX_FMT_DXVA2_VLD。
// 我们需要将hw_avframe AV_PIX_FMT_DXVA2_VLD格式的数据 转换到 avframe 中
ret = avcodec_receive_frame(avcodecContext, hw_avframe);
if (ret < 0) {
cout << "hw device func avcodec_receive_frame error" << endl;
break;
}
if (ret == 0) {
//将hw_avframe的数据转到 avframe
cout << "hw device hw_avframe->format = " << hw_avframe->format << endl;
int ret_av_hwframe_transfer_data = av_hwframe_transfer_data(avframe, hw_avframe, 0);
if (ret_av_hwframe_transfer_data < 0) {
cout << "hw device func av_hwframe_transfer_data error" << endl;
break;
}
if (ret_av_hwframe_transfer_data == 0) {
//得到正常的 avframe
//1.存储数据。
//存储 AV_PIX_FMT_NV12 格式的。
//那么就要明白 AV_PIX_FMT_NV12 格式是啥样子的,才好存储。
//AV_PIX_FMT_NV12 格式参考 : https://www.cnblogs.com/mjios/p/14686970.html
// Y Y Y Y
// Y Y Y Y
// U V U V
if (avframe->linesize[0] == avframe->width) {
//没有字节对齐问题
writefstream.write((char*)avframe->data[0], avframe->linesize[0] * avframe->height);
writefstream.write((char*)avframe->data[1], avframe->linesize[1] * avframe->height / 2);
}
else {
//有字节对齐问题时候的处理
for (int j = 0; j < avframe->height; j++) {
writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width);
}
}
}
}
}
else {
//没有开启硬件加速
ret = avcodec_receive_frame(avcodecContext, avframe);
if (ret < 0) {
cout << "software func avcodec_receive_frame error" << endl;
break;
}
if (ret == 0) {
//1.存储数据
if (avframe->linesize[0] == avframe->width) {
//没有字节对齐问题
writefstream.write((char*)avframe->data[0], avframe->linesize[0] * avframe->height);
writefstream.write((char*)avframe->data[1], avframe->linesize[1] * avframe->height / 2);
writefstream.write((char*)avframe->data[2], avframe->linesize[2] * avframe->height / 2);
}
else {
//有字节对齐问题时候的处理
// 先写完Y,再写U,再写V
for (int j = 0; j < avframe->height; j++) {
writefstream.write((char*)(avframe->data[0] + j * avframe->linesize[0]), avframe->width);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(avframe->data[1] + j * avframe->linesize[1]), avframe->width / 2);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(avframe->data[2] + j * avframe->linesize[2]), avframe->width / 2);
}
}
}
}
//2.show 数据
if (!is_init_win)
{
is_init_win = true;
xvideoview->Init(avframe->width,
avframe->height,
(XVideoView::Format)avframe->format);
}
xvideoview->DrawFrame(avframe);
}
if (hw_avframe != nullptr) {
av_frame_free(&hw_avframe);
}
if (avframe != nullptr) {
av_frame_free(&avframe);
}
if (writefstream) {
writefstream.close();
}
if (readifstream) {
readifstream.close();
}
if (avpacket != nullptr) {
av_packet_free(&avpacket);
}
if (avcodecContext != nullptr) {
avcodec_free_context(&avcodecContext);
}
if (avcodecParseContext != nullptr) {
av_parser_close(avcodecParseContext);
}
cout << " avframenumber = " << avframenumber << endl;
return 0;
}
bug fix
1. 在有字节对齐问题的时候,我们从 avframe的data 写入ofstream,将avframe 的data 写入 数组中。
从 yuv file 中读取数据到avframe 的data 中;;;将avfreame 的data 写入 yuvfile 中;;;将avfreame 的data 写入 数组中-CSDN博客
NV12
if (send_avframe->linesize[0] == send_avframe->width) {
//没有字节对齐问题
writefstream.write((char*)send_avframe->data[0], send_avframe->linesize[0] * send_avframe->height);
writefstream.write((char*)send_avframe->data[1], send_avframe->linesize[1] * send_avframe->height / 2);
}
else {
//有字节对齐问题时候的处理
for (int j = 0; j < send_avframe->height; j++) {
writefstream.write((char*)(send_avframe->data[0] + j * send_avframe->linesize[0]), send_avframe->width);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(send_avframe->data[1] + j * send_avframe->linesize[1]), send_avframe->width);
}
}
yuv420p
if (send_avframe->linesize[0] == send_avframe->width) {
//没有字节对齐问题
writefstream.write((char*)send_avframe->data[0], send_avframe->linesize[0] * send_avframe->height);
writefstream.write((char*)send_avframe->data[1], send_avframe->linesize[1] * send_avframe->height / 2);
writefstream.write((char*)send_avframe->data[2], send_avframe->linesize[2] * send_avframe->height / 2);
}
else {
//有字节对齐问题时候的处理
// 先写完Y,再写U,再写V
for (int j = 0; j < send_avframe->height; j++) {
writefstream.write((char*)(send_avframe->data[0] + j * send_avframe->linesize[0]), send_avframe->width);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(send_avframe->data[1] + j * send_avframe->linesize[1]), send_avframe->width / 2);
}
for (int j = 0; j < avframe->height / 2; j++) {
writefstream.write((char*)(send_avframe->data[2] + j * send_avframe->linesize[2]), send_avframe->width / 2);
}
}
从 avframe 的data 中读取数据到 数组中
NV12_data_ = new uint8_t[4096 * 2160 * 1.5];
if (frame->linesize[0] == frame->width)
{
// 如果没有字节对齐问题,直接先copy avframe->data[0];然后再copy avframe->data[1]
memcpy(NV12_data_, frame->data[0], frame->linesize[0] * frame->height); //Y
memcpy(NV12_data_ + frame->linesize[0] * frame->height, frame->data[1], frame->linesize[1] * frame->height / 2); //UV
}
else //逐行复制
{
//这里字节有对齐问题的时候,为什么要这样 copy 呢?
//而且实验测试,当 avframe是 400 * 300 时候,字节对齐有问题时候,就是用这种方法就能解决。
for (int i = 0; i < frame->height; i++) //Y
{
memcpy(NV12_data_ + i * frame->width,
frame->data[0] + i * frame->linesize[0],
frame->width
);
}
for (int i = 0; i < frame->height / 2; i++) //UV
{
auto p = NV12_data_ + frame->height * frame->width;// 移位Y
memcpy(p + i * frame->width,
frame->data[1] + i * frame->linesize[1],
frame->width
);
}
}