三、AVFrame渲染

目标:

  1. AVFrame结构体的成员含义
  2. AVFrame的内存分析:引用计数与字节对齐
  3. 将文件中YUV数据存储到AVFrame中,并将AVFrame中的YUV渲染出来
  4. 帧率的控制策略方式、精确控制的方式
  5. 通过多线程控制渲染
  6. 界面上设置并显示fps的方法。

1. AVFrame结构体和相关接口

1.1 AVFrame基本介绍

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 = &rect;
    }
    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 多线程实现渲染并控制帧率

  1. 定义信号和槽函数,槽函数内实现渲染的业务,循环读取
  2. 定义一个子线程线程,子线程内通过自定义定时器定时调用Qt的信号函数
  3. 为了线程安全,主线程需要子线程退出后才能退出,否则主线程创建的窗口等销毁了,子线程渲染对象就是无效空间。
#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

  1. 1s内每调用一次draw渲染画面,fps就++,将1s内的fps数据作为label的标题
    设置fps
  2. 定义一个QT spinBox控件,根据获取的数值来设置定时器的时间

1.定义两个对象
2. 新进程开始定时调用渲染信号函数
//因为渲染的槽函数只有一个,但需要渲染的画面却有连个,因此可以让读取和渲染函数都设为xvideoview的方法
1. 读取数据返回Frame
2. 渲染读取的Frame数据

  1. 打开文件出发打开信号函数
    1. 获取设置的文件参数
    2. 根据文件参数初始化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());
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值