目标:
- AVFrame结构体的成员含义
- AVFrame的内存分析:引用计数与字节对齐
- 将文件中YUV数据存储到AVFrame中,并将AVFrame中的YUV渲染出来
- 帧率的控制策略方式、精确控制的方式
- 通过多线程控制渲染
- 界面上设置并显示fps的方法。
1. AVFrame结构体和相关接口
1.1 AVFrame基本介绍
结构体成员
uint8_t *data[AV_NUM_DATA_POINTERS];
AV_NUM_DATA_POINTERS的数值为8
实际上data中存放着8个指针,指向AVBufferRef *buf中的地址,即数据实际上存放在buf中
int linesize[AV_NUM_DATA_POINTERS]; //字节对齐
即buf中每个data数组字节对齐后的大小
int width, height;
int format; //AVPixelFormat
AVBufferRef *buf[AV_NUM_DATA_POINTERS];
av_frame_alloc();
仅申请AVFrame对象,但内存中的buffer控件未申请
av_frame_get_buffer(frame1, 16);
对AVFrame对象中的buf申请控件,按照参数16的字节对齐,可以设置最小4字节对齐
在申请内存之前,需要先设置AVFrame的参数,否则无法得知申请的大小
frame1->width = 401;
frame1->height = 300;
frame1->format = AV_PIX_FMT_ARGB;
av_buffer_get_ref_count(frame1->buf[0])
获取frame->buf中第一个数组中的引用计数
该函数是线程安全的,访问引用计数时是引用计数不会被更改,但仅供参考
av_frame_ref(frame2, frame1);/av_frame_unref(frame2);
增加/减少引用计数
av_frame_free(&frame2);
会先减少引用计数再释放AVFrame,若引用计数已经为0则直接释放
1.2 AVFrame的创建及使用
#include <iostream>
using namespace std;
extern "C"{ //指定函数是c语言函数,函数名不包含重载标注
//引用ffmpeg头文件
#include <libavcodec/avcodec.h>
}
//预处理指令导入库
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"avutil.lib")
int main(int argc, char* argv[])
{
cout << "first ffmpeg" << endl;
cout << avcodec_configuration() << endl;
//创建frame对象
auto frame1 = av_frame_alloc();
//图像参数
frame1->width = 401;
frame1->height = 300;
frame1->format = AV_PIX_FMT_ARGB;
//分配空间 16字节对齐
int re = av_frame_get_buffer(frame1, 16);
if (re != 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf));
cout << buf << endl;
}
cout <<"frame1 linesize[0]="<< frame1->linesize[0] << endl;
if (frame1->buf[0])
{
cout << "frame1 ref count = " <<
av_buffer_get_ref_count(frame1->buf[0]); // 线程安全
cout << endl;
}
auto frame2 = av_frame_alloc();
av_frame_ref(frame2, frame1);
cout << "frame1 ref count = " <<
av_buffer_get_ref_count(frame1->buf[0]) << endl;
cout << "frame2 ref count = " <<
av_buffer_get_ref_count(frame2->buf[0]) << endl;
//引用计数-1 并将buf清零
av_frame_unref(frame2);
cout << "av_frame_unref(frame2)" << endl;
cout << "frame1 ref count = " <<
av_buffer_get_ref_count(frame1->buf[0]) << endl;
//引用计数为1 直接删除buf空间 引用计数变为0
av_frame_unref(frame1);
//是否frame对象空间,buf的引用计数减一
//buf已经为空,只删除frame对象空间
av_frame_free(&frame1);
av_frame_free(&frame2);
return 0;
}
2. YUV数据转AVFrame并渲染
RGB数据在AVFrame中的存储方式:所有的RGB数据都存放在data[0]所指向的buf中的空间,data[0]中的数据量为width x height x 4
从文件中读取的数据量也是width x height x 4
YUV数据在AVFrame中的存储方式:YUV420P的Y、U、V三个分量分别存放在data[0]、data[1]、data[2]所指向的控件,因此的数据量分别为为width x height、width x height / 2、width x height / 2。此处只涉及到行的数量,为涉及到高,UV实际上的数据量都为Y的1/4
从文件中读取的数据量分别为width x height、width x height / 4、width x height / 4
渲染YUV数据:直接调用专门渲染YUV数据的接口,将YUV分量分别传进去就可以渲染YUV数据
//读取YUV数据到AVFrame
yuv_file.read((char*)frame->data[0], sdl_width * sdl_height);//Y
yuv_file.read((char*)frame->data[1], sdl_width * sdl_height / 4);//U
yuv_file.read((char*)frame->data[2], sdl_width * sdl_height / 4);//V
//渲染AVFrame中的数据,根据格式来选择不同的读取方式和pitch数值
bool XVideoView::DrawFrame(AVFrame* frame)
{
if (!frame || !frame->data[0])return false;
switch (frame->format)
{
case AV_PIX_FMT_YUV420P:
return Draw(frame->data[0], frame->linesize[0],//Y
frame->data[1], frame->linesize[1], //U
frame->data[2], frame->linesize[2] //V
);
case AV_PIX_FMT_BGRA:
return Draw(frame->data[0], frame->linesize[0]);
default:
break;
}
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)return false;
unique_lock<mutex> sdl_lock(mtx_);
if (!texture_ || !render_ || !win_ || width_ <= 0 || height_ <= 0)
return false;
//复制内存到显显存
auto re = SDL_UpdateYUVTexture(texture_,
NULL,
y,y_pitch,
u,u_pitch,
v,v_pitch);
if (re != 0)
{
cout << SDL_GetError() << endl;
return false;
}
//清空屏幕
SDL_RenderClear(render_);
//材质复制到渲染器
SDL_Rect rect;
SDL_Rect* prect = nullptr;
if (scale_w_ > 0) //用户手动设置缩放
{
rect.x = 0; rect.y = 0;
rect.w = scale_w_;//渲染的宽高,可缩放
rect.h = scale_w_;
prect = ▭
}
re = SDL_RenderCopy(render_, texture_, NULL, prect);
if (re != 0)
{
cout << SDL_GetError() << endl;
return false;
}
SDL_RenderPresent(render_);
}
3. 多线程实现渲染
3.1 控制帧率的方式
#include <iostream>
#include <ctime>
#include <thread>
#include <windows.h>
using namespace std;
自定义定时器
一共循环10次,每次休眠1ms,若休眠的事件超过10ms,就停止
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;
}
}
int main(int argc, char* argv[])
{
//测试c++11的sleep
//测试 sleep 10毫秒 100fps
auto beg = clock(); //开始时间 ,cpu跳数
int fps = 0; //帧率
//方法一:通过休眠方式
无限循环,fps++
每循环一次就休眠10ms,直到时间到达1000ms为止,返回fps的数值
for (;;)
{
fps++;
auto tmp = clock();
this_thread::sleep_for(10ms);
//cout << clock() - tmp << " " << flush;
//1秒钟开始统计 CLOCKS_PER_SEC cpu每秒跳数
if ((clock() - beg) / (CLOCKS_PER_SEC / 1000) > 1000) //间隔毫秒数
{
cout << "sleep for fps:" << fps << endl;
break;
}
}
//测试wait 事件超时控制 帧率
原理和休眠一样,只是将休眠10ms改为等待事件发生10ms
auto handle = CreateEvent(NULL, FALSE, FALSE,NULL);
fps = 0;
beg = clock();
for (;;)
{
fps++;
WaitForSingleObject(handle, 10);// 等待超时
if ((clock() - beg) / (CLOCKS_PER_SEC / 1000) > 1000) //间隔毫秒数
{
cout << "wait fps:" << fps << endl;
break;
}
}
//原理与休眠一样,只不过油休眠10ms改为自定义的10ms定时
fps = 0;
beg = clock();
for (;;)
{
fps++;
MSleep(10);
if ((clock() - beg) / (CLOCKS_PER_SEC / 1000) > 1000) //间隔毫秒数
{
cout << "MSleep fps:" << fps << endl;
break;
}
}
getchar();
return 0;
}
3.2 多线程实现渲染并控制帧率
- 定义信号和槽函数,槽函数内实现渲染的业务,循环读取
- 定义一个子线程线程,子线程内通过自定义定时器定时调用Qt的信号函数
- 为了线程安全,主线程需要子线程退出后才能退出,否则主线程创建的窗口等销毁了,子线程渲染对象就是无效空间。
#include "sdlqtrgb.h"
#include <fstream>
#include <iostream>
#include <QMessageBox>
#include <thread>
#include <sstream>
#include "xvideo_view.h"
extern "C"
{
#include <libavcodec/avcodec.h>
}
using namespace std;
static int sdl_width = 0;
static int sdl_height = 0;
static int pix_size = 2;
static ifstream yuv_file;
static XVideoView* view = nullptr;
static AVFrame* frame = nullptr;
static long long file_size = 0;
static QLabel* view_fps = nullptr;
//槽函数
void SdlQtRGB::View()
{
//读取YUV数据
yuv_file.read((char*)frame->data[0], sdl_width * sdl_height);//Y
yuv_file.read((char*)frame->data[1], sdl_width * sdl_height / 4);//U
yuv_file.read((char*)frame->data[2], sdl_width * sdl_height / 4);//V
//循环读取YUV数据
if (yuv_file.tellg() == file_size) //读取到文件结尾
{
yuv_file.seekg(0, ios::beg);
}
//yuv_file.gcount()
//yuv_file.seekg() 结尾处seekg无效
if (view->IsExit())
{
view->Close();
exit(0);
}
view->DrawFrame(frame);
}
void SdlQtRGB::Main()
{
while (!is_exit_)
{
ViewS();
MSleep(10);
}
}
SdlQtRGB::SdlQtRGB(QWidget *parent)
: QWidget(parent)
{
//打开yuv文件
yuv_file.open("400_300_25.yuv", ios::binary);
if (!yuv_file)
{
QMessageBox::information(this, "", "open yuv failed!");
return;
}
yuv_file.seekg(0, ios::end); //移到文件结尾
file_size = yuv_file.tellg(); //文件指针位置
yuv_file.seekg(0, ios::beg);
ui.setupUi(this);
//绑定渲染信号槽
connect(this, SIGNAL(ViewS()), this, SLOT(View()));
//显示fps的控件
view_fps = new QLabel(this);
view_fps->setText("fps:100");
sdl_width = 400;
sdl_height = 300;
ui.label->resize(sdl_width, sdl_height);
view = XVideoView::Create();
//view->Init(sdl_width, sdl_height,
// XVideoView::YUV420P);
//view->Close();
view->Close();
view->Init(sdl_width, sdl_height,
XVideoView::YUV420P, (void*)ui.label->winId());
//生成frame对象空间
frame = av_frame_alloc();
frame->width = sdl_width;
frame->height = sdl_height;
frame->format = AV_PIX_FMT_YUV420P;
// Y Y
// UV
// Y Y
frame->linesize[0] = sdl_width; //Y
frame->linesize[1] = sdl_width/2; //U
frame->linesize[2] = sdl_width/2; //V
//生成图像空间 默认32字节对齐
auto re = av_frame_get_buffer(frame, 0);
if (re != 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf));
cerr << buf << endl;
}
//startTimer(10);
th_ = std::thread(&SdlQtRGB::Main, this);
}
void SdlQtRGB::resizeEvent(QResizeEvent* ev)
{
ui.label->resize(size());
ui.label->move(0, 0);
//view->Scale(width(), height());
}
3.3 界面显示fps并可设置fps
界面显示fps
- 1s内每调用一次draw渲染画面,fps就++,将1s内的fps数据作为label的标题
设置fps - 定义一个QT spinBox控件,根据获取的数值来设置定时器的时间
1.定义两个对象
2. 新进程开始定时调用渲染信号函数
//因为渲染的槽函数只有一个,但需要渲染的画面却有连个,因此可以让读取和渲染函数都设为xvideoview的方法
1. 读取数据返回Frame
2. 渲染读取的Frame数据
- 打开文件出发打开信号函数
- 获取设置的文件参数
- 根据文件参数初始化sdl
#include "sdlqtrgb.h"
#include <fstream>
#include <iostream>
#include <QMessageBox>
#include <thread>
#include <sstream>
#include <QSpinBox>
#include "xvideo_view.h"
extern "C"
{
#include <libavcodec/avcodec.h>
}
using namespace std;
static int sdl_width = 0;
static int sdl_height = 0;
static int pix_size = 2;
static ifstream yuv_file;
static XVideoView* view = nullptr;
static AVFrame* frame = nullptr;
static long long file_size = 0;
static QLabel* view_fps = nullptr; //显示fps控件
static QSpinBox* set_fps = nullptr;//设置fps控件
int fps = 25; //播放帧率
void SdlQtRGB::timerEvent(QTimerEvent* ev)
{
//yuv_file.read((char*)yuv, sdl_width * sdl_height * 1.5);
// yuv420p
// 4*2
// yyyy yyyy
// u u
// v v
yuv_file.read((char*)frame->data[0], sdl_width * sdl_height);//Y
yuv_file.read((char*)frame->data[1], sdl_width * sdl_height / 4);//U
yuv_file.read((char*)frame->data[2], sdl_width * sdl_height / 4);//V
if (view->IsExit())
{
view->Close();
exit(0);
}
view->DrawFrame(frame);
//view->Draw(yuv);
}
void SdlQtRGB::View()
{
yuv_file.read((char*)frame->data[0], sdl_width * sdl_height);//Y
yuv_file.read((char*)frame->data[1], sdl_width * sdl_height / 4);//U
yuv_file.read((char*)frame->data[2], sdl_width * sdl_height / 4);//V
if (yuv_file.tellg() == file_size) //读取到文件结尾
{
yuv_file.seekg(0, ios::beg);
}
//yuv_file.gcount()
//yuv_file.seekg() 结尾处seekg无效
if (view->IsExit())
{
view->Close();
exit(0);
}
view->DrawFrame(frame);
//显示帧率
//bool XVideoView::DrawFrame(AVFrame* frame)
//{
// if (!frame || !frame->data[0])return false;
// 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();
// }
// }
stringstream ss;
ss << "fps:" << view->render_fps();
//只能在槽函数中调用
view_fps->setText(ss.str().c_str());
//设置播放帧率
fps = set_fps->value(); //拿到播放帧率
}
void SdlQtRGB::Main()
{
while (!is_exit_)
{
ViewS();
if (fps > 0)
{
MSleep(1000 / fps);
}
else
MSleep(10);
}
}
SdlQtRGB::SdlQtRGB(QWidget *parent)
: QWidget(parent)
{
//打开yuv文件
yuv_file.open("400_300_25.yuv", ios::binary);
if (!yuv_file)
{
QMessageBox::information(this, "", "open yuv failed!");
return;
}
yuv_file.seekg(0, ios::end); //移到文件结尾
file_size = yuv_file.tellg(); //文件指针位置
yuv_file.seekg(0, ios::beg);
ui.setupUi(this);
//绑定渲染信号槽
connect(this, SIGNAL(ViewS()), this, SLOT(View()));
//显示fps的控件
view_fps = new QLabel(this);
view_fps->setText("fps:100");
//设置fps
set_fps = new QSpinBox(this);
set_fps->move(200, 0);
set_fps->setValue(25);
set_fps->setRange(1, 300);
sdl_width = 400;
sdl_height = 300;
ui.label->resize(sdl_width, sdl_height);
view = XVideoView::Create();
//view->Init(sdl_width, sdl_height,
// XVideoView::YUV420P);
//view->Close();
view->Close();
view->Init(sdl_width, sdl_height,
XVideoView::YUV420P, (void*)ui.label->winId());
//生成frame对象空间
frame = av_frame_alloc();
frame->width = sdl_width;
frame->height = sdl_height;
frame->format = AV_PIX_FMT_YUV420P;
// Y Y
// UV
// Y Y
frame->linesize[0] = sdl_width; //Y
frame->linesize[1] = sdl_width/2; //U
frame->linesize[2] = sdl_width/2; //V
//生成图像空间 默认32字节对齐
auto re = av_frame_get_buffer(frame, 0);
if (re != 0)
{
char buf[1024] = { 0 };
av_strerror(re, buf, sizeof(buf));
cerr << buf << endl;
}
//startTimer(10);
th_ = std::thread(&SdlQtRGB::Main, this);
}
void SdlQtRGB::resizeEvent(QResizeEvent* ev)
{
ui.label->resize(size());
ui.label->move(0, 0);
//view->Scale(width(), height());
}