上周回顾
- pc端
- 了解了一些渲染的基本原理
- rk3566端
- 无
本周计划
上周国庆出去了,继续内容
- pc端
- 搭建一个基本qt应用框架
- 技能储备
- 学会sdl渲染
- 重新掌握QT技能,最好把kvm和dstool的代码分析一边
- rk3566端
- 尝试编译一个支持拓展板上网口的uboot
- 尝试交叉编译一个qt或者sdl
本周记录
#daily/25/10/09
渲染画面
通过SDL渲染画面
四种色彩模式ARGB_8888、ARGB_4444、 RGB_565、 ALPHA_8的区别
SDL2之SDL_RenderCopy
SDL2/SDL_WIKI
具体查看 002test_sdl_rgb
效果

代码实现
#include <iostream>
#include <SDL3/SDL.h>
#define WIDTH 800
#define HEIGHT 300
int main()
{
// 初始化接口
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
std::cout << "[" << __LINE__ << "]: " << SDL_GetError() << std::endl;
return -1;
}
// 创建窗口
auto window = SDL_CreateWindow(
"Hello SDL",
WIDTH,
HEIGHT,
SDL_WINDOW_RESIZABLE
);
if (!window)
{
std::cout << "[" << __LINE__ << "]: " << SDL_GetError() << std::endl;
return -1;
}
// 创建渲染器
auto render = SDL_CreateRenderer(window, NULL);
if (!render) {
std::cout << "[" << __LINE__ << "]: " << SDL_GetError() << std::endl;
return -1;
}
// 创建纹理
auto texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, WIDTH, HEIGHT);
if (!texture)
{
std::cout << "[" << __LINE__ << "]: " << SDL_GetError() << std::endl;
return -1;
}
// 创建图像数据
std::shared_ptr<unsigned char> rgb(new unsigned char[WIDTH * HEIGHT * 4]);
auto b = rgb.get();
for (int i = 0; i < HEIGHT; i++)
{
for (int j = 0; j < WIDTH; j++)
{
int index = WIDTH * 4 * i + j * 4;
// 如果是argb8888格式,则通道顺序如下
b[index] = 0; // B
b[index + 1] = 0; // G
b[index + 2] = 255; // R
b[index + 3] = 255; // A
// 如果是rgba8888格式,则通道顺序如下
//b[index] = 255; // R
//b[index + 1] = 0; // G
//b[index + 2] = 0; // B
//b[index + 3] = 255; // A
}
}
// 加载图像为纹理
SDL_UpdateTexture(texture, NULL, b, WIDTH * 4);
// 清空渲染器
SDL_RenderClear(render);
// 复制纹理到渲染器
SDL_FRect dstRect = { 0,0,WIDTH,HEIGHT };
//SDL_RenderCopy( // SDL3中过时
SDL_RenderTexture( // SDL3新函数
render,
texture,
NULL, // 原图坐标、尺寸
&dstRect // 目标坐标、尺寸
);
// 更新窗口
SDL_RenderPresent(render);
SDL_Delay(1500);
// 关闭流程
SDL_DestroyTexture(texture); // 销毁材质
SDL_DestroyRenderer(render); // 销毁渲染器
SDL_DestroyWindow(window); // 销毁窗口
SDL_Quit(); //关闭SDL
//std::cin.get();
return 0;
}把用到的SDL_API总结一下 #done [[2025-w41#SDL库渲染流程API总结| SDL_API]]
通过QT+SD联合渲染画面
具体查看 003test_qt_sdl_rgb
效果
代码
#include "test_qt_sdl_rgb.h"
#include "SDL3/SDL.h"
#include "qdebug.h"
static SDL_Renderer* sdl_render = nullptr;
static SDL_Texture* sdl_texture = nullptr;
static SDL_Window* sdl_window = nullptr;
static int sdl_win_width = 0;
static int sdl_win_height = 0;
test_qt_sdl_rgb::test_qt_sdl_rgb(QWidget* parent)
: QMainWindow(parent)
{
ui.setupUi(this);
m_sdl_label = ui.label;
ui.label->setFixedSize(800, 300);
sdl_win_width = m_sdl_label->width();
sdl_win_height = m_sdl_label->height();
// 初始化SDL接口
SDL_Init(SDL_INIT_VIDEO);
// 创建嵌入qt的SDL窗口
WId winId = m_sdl_label->winId();
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER, (void*)winId);
SDL_Window* window = SDL_CreateWindowWithProperties(props);
sdl_window = SDL_CreateWindowWithProperties(props);
SDL_DestroyProperties(props);
// 创建渲染器
sdl_render = SDL_CreateRenderer(sdl_window, NULL);
// 创建纹理
sdl_texture = SDL_CreateTexture(sdl_render,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
sdl_win_width,
sdl_win_height
);
startTimer(16); //60fps
}
test_qt_sdl_rgb::~test_qt_sdl_rgb()
{
SDL_DestroyWindow(sdl_window);
}
void test_qt_sdl_rgb::timerEvent(QTimerEvent* ev)
{
// 创建图像数据
static int tmp = 255;
std::shared_ptr<unsigned char> rgb(new unsigned char[sdl_win_width * sdl_win_height * 4]);
auto sdl_win_pixels = rgb.get();
for (int i = 0; i < sdl_win_height; i++)
{
for (int j = 0; j < sdl_win_width; j++)
{
int index = sdl_win_width * 4 * i + j * 4;
// 如果是argb8888格式,则通道顺序如下
sdl_win_pixels[index] = 0; // B
sdl_win_pixels[index + 1] = 0; // G
sdl_win_pixels[index + 2] = 255; // R
sdl_win_pixels[index + 3] = tmp; // A
}
}
tmp--;
if (tmp<0)
tmp = 255;
// 加载图像为纹理
SDL_UpdateTexture(sdl_texture, NULL, sdl_win_pixels, sdl_win_width * 4);
// 清空渲染器
SDL_RenderClear(sdl_render);
// 复制纹理到渲染器
SDL_FRect dstRect = { 0,0,sdl_win_width,sdl_win_height };
//SDL_RenderCopy( // SDL3中过时
SDL_RenderTexture( // SDL3新函数
sdl_render,
sdl_texture,
NULL, // 原图坐标、尺寸
&dstRect // 目标坐标、尺寸
);
// 更新窗口
SDL_RenderPresent(sdl_render);
}合并两幅图片RGB渲染
#daily/25/10/10

效果
代码
具体查看 004test_qt_sdl_merge
// 3.把图片添加到纹理,拼接两张图
memset(m_pixels, 0, m_pixels_len);
int copy_img1_size = m_img1.width() * m_pixel_size;
int copy_img2_size = m_img2.width() * m_pixel_size;
for (int i = 0; i < m_height; i++)
{
// 偏移量 = 每一行的起点 = 当前总行数 * 行宽 * 单个像素的字节大小
int dst_offset = i * m_width * m_pixel_size;
if (i < m_img1.height())
// m_pixels为数组起点 + 当前偏移量,复制当前行img1的宽度
memcpy(m_pixels + dst_offset, m_img1.scanLine(i), copy_img1_size);
if (i < m_img2.height())
// m_pixels为数组起点 + 当前偏移量 + img1的宽度,复制当前行img2的宽度
memcpy(m_pixels + dst_offset + m_img1.width() * m_pixel_size, m_img2.scanLine(i), copy_img2_size);
}SDL库渲染流程API总结
// 1.初始化SDL库
SDL_Init(SDL_INIT_VIDEO);
// 2.创建SDL窗口、纹理、渲染器
SDL_PropertiesID props = SDL_CreateProperties();
SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER,(void*)m_winId);
m_sdl_window = SDL_CreateWindowWithProperties(props);
m_sdl_render = SDL_CreateRenderer(m_sdl_window,NULL);
m_sdl_texture = SDL_CreateTexture(m_sdl_render
,SDL_PIXELFORMAT_ARGB8888
,SDL_TEXTUREACCESS_STREAMING
,m_width ,m_height
);
// 3. 添加图片到内存
// ...
// 4. 清空渲染器,把纹理拷贝到渲染器,更新纹理
SDL_RenderClear(m_sdl_render);
SDL_RenderTexture(m_sdl_render,m_sdl_texture,NULL,NULL);
SDL_UpdateTexture(m_sdl_texture, NULL, m_pixels, m_width * m_pixel_size);
// 5. 更新窗口
SDL_RenderPresent(m_sdl_render);FFmpeg.exe使用
#todo
视频分割
ffmpeg -i 【4K60FPS】周杰伦《搁浅》巅峰现场!已经过去20年了啊!.mp4 -c copy -map 0 -f segment -segment_time 30 -reset_timestamps 1 640_360_30_%01d.mp4- -i input.mp4:指定输入文件。
- -c copy:直接复制音视频流,无需重新编码。
- -map 0:确保所有流(音频、视频)都被复制。
- -f segment:启用分段模式。
- -segment_time 360:每段的时长为360秒(6分钟)。
- -reset_timestamps 1:重置每段的时间戳。
- output%03d.mp4:输出文件命名格式(如output001.mp4、output002.mp4)。
mp4转换为yuv
编译一下ffmpeg.exe #done
- 这个编译时间好久,而且出现了error,最后还是从官网下了别人编译完的exe文件
- Builds - CODEX FFMPEG @ gyan.dev

- yuv420p的p是指 plane 平面
播放yuv

YUV格式渲染
SDL播放渲染YUV视频
- 如果打开文件失败,则可能显示全绿的画面,因为视频帧没有添加到纹理中
效果
代码
具体查看 005test_qt_sdl_play_yuv
// 3. 读取yuv视频, 添加到纹理
int frame_size = m_width * m_height * 3/2; // yuv420p
QByteArray frame_data = m_yuv_file.read(frame_size); // 读取1帧yuv数据到buffer
SDL_UpdateTexture(m_sdl_texture, NULL, frame_data.data(), m_width); // 更新纹理- yuv420 的像素大小(pixels)为什么是 3/2
- Y分量(亮度),每个像素1个字节,总大小:width × height = 1
- U分量(色度),每2×2像素块共享1个U值,总大小:(width/2) × (height/2) = 1/4
- V分量(色度),每2×2像素块共享1个V值,总大小:(width/2) × (height/2) = 1/4

SDL_UpdateTexture的第四个参数是 pitch 行字节数- yuv420 的 y 就是 每行的字节数(uv都是 y 字节数的1/2,sdl内部会自动处理)
- argb8888 中 每个像素要占用4个字节(argb每个8位),它的行字节数 = 行宽 * 4
封装SDL库
#daily/25/10/11

封装后不出图像
不出图像的原因是因为变量在函数中重复定义了,读取的变量的地址不一样
- ![[../../attachments/images/Pasted image 20251011123921.png]]
- 
Create可以多次创建
static变量和成员变量的区别
成员变量需要实例化才有,也就是说每个实例都有一个独立的成员变量
static变量是属于类的,每个实例都共享这一个static变量
这里即使调用多次XSDL的构造函数,多个XSDL实例也只共享这同一个 isFirst 变量
按比例缩放
test_xvideo_view.cpp
void test_xvideo_view::resizeEvent(QResizeEvent* event)
{
m_label_sdl->resize(size());
m_label_sdl->move(0, 0);
m_view->Scale(m_label_sdl->width(),m_label_sdl->height());
}XvideoView.cpp
void XvideoView::Scale(int width, int height)
{
m_scale_width = width;
m_scale_height = height;
}XSDL.cpp
bool XSDL::Draw(const void* data, int linesize)
{
// 3. 加载帧图像数据到纹理
SDL_UpdateTexture(m_sdl_texture, NULL, data, linesize);
// 4. 清空渲染器、将纹理拷贝到渲染器、更新渲染器
SDL_RenderClear(m_sdl_render);
SDL_FRect dstrect = {0,0,m_scale_width,m_scale_height};
SDL_RenderTexture(m_sdl_render,m_sdl_texture,NULL,&dstrect);
SDL_RenderPresent(m_sdl_render);
return true;
}线程安全
//#include <iostream>
//using namespace std;
//mutex m_sdl_mutex;
//unique_lock<mutex> sdl_lock(m_sdl_mutex);等价
#include "qmutex.h"
QMutex m_sdl_mutex;
QMutexLocker sdl_locker(&m_sdl_mutex);清理接口
xvideoview.h
virtual bool Clear(XvideoView*) = 0;XSDL.cpp
bool XSDL::Clear(XvideoView*)
{
// 清理SDL资源
if (!m_sdl_window)
{
SDL_DestroyWindow(m_sdl_window);
m_sdl_window = nullptr;
}
if (!m_sdl_texture)
{
SDL_DestroyTexture(m_sdl_texture);
m_sdl_texture = nullptr;
}
if (!m_sdl_render)
{
SDL_DestroyRenderer(m_sdl_render);
m_sdl_render = nullptr;
}
return true;
}内存泄漏引起闪退
在这个定时器里,由于会不断地new数组,但没有正确的释放,之后一定会造成内存泄漏的
- m_view 智能指针解决
- m_view2 手动释放空间解决
void test_xvideo_view::timerEvent(QTimerEvent* event)
{
#ifdef VIEW2_ENABLED
if (m_view2->IsExit())
{
killTimer(event->timerId());
m_view2->Clear();
m_view->Clear(); // Added back m_view->Clear() based on prior logic
close();
return;
}
#endif
// 3.加载yuv图像帧数据、拷贝到纹理
int frame_size = m_width * m_height * 3 / 2;
int frame_buffer_size = m_width * m_height * 2;
//unsigned char* frame_buffer = new unsigned char[frame_buffer_size];
auto frame_buffer = std::make_unique<unsigned char[]>(frame_buffer_size); // 使用智能指针管理内存
// 当文件已经读完
if (m_file_sdl.atEnd())
{
m_file_sdl.seek(0); // 重新回到文件头,循环播放
}
m_file_sdl.read((char*)frame_buffer.get(), frame_size);
m_view->Draw(frame_buffer.get(), m_width);
#ifdef VIEW2_ENABLED
// 加载rgb图像帧数据,拷贝到纹理
int frame_size2 = m_width * m_height * 4;
int frame_buffer_size2 = m_width * m_height * 4;
unsigned char* frame_buffer2 = new unsigned char[frame_buffer_size2];
static int tmp = 255;
for (int i = 0; i < m_height; i++)
{
for (int j = 0; j < m_width; j++)
{
int index = i * m_width * 4 + j * 4;
frame_buffer2[index + 0] = 0; // B
frame_buffer2[index + 1] = 0; // G
frame_buffer2[index + 2] = 255; // R
frame_buffer2[index + 3] = tmp; // A
}
}
tmp--;
if (tmp < 0) tmp = 255;
m_view2->Draw(frame_buffer2, m_width * 4);
delete[] frame_buffer2; // 手动释放内存
#endif
}缩放抗锯齿
抗锯齿接口在sdl3中并不适用 #todo
SDL退出事件
Multiple Windows and SDL_EVENT_QUIT failure · Issue #10115 · libsdl-org/SDL
SDL2中使用 SDL_QUIT
SDL3中使用
- SDL_EVENT_QUIT (只有在请求关闭最后一个窗口时才会发送的退出事件)
- SDL_EVENT_WINDOW_CLOSE_REQUESTED (关闭任意一个窗口就发送)
bool XSDL::IsExit()
{
SDL_Event ev;
SDL_WaitEventTimeout(&ev, 0);
if (ev.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)
return true;
else
return false;
}YUV镜像显示
#homework
YUV理解
记一次 YUV 图像翻转的性能优化 - Piasy的博客 | Piasy Blog
libyuv库可以直接对YUV进行水平翻转等处理
图解YU12、I420、YV12、NV12、NV21、YUV420P、YUV420SP、YUV422P、YUV444P的区别
将YUV420P图像水平镜像翻转——音视频(一) - 仓俊 - 博客园
yuv实际上最初只有y,只能显示亮度的变化,因此都是黑白的图像,y的宽度就是画面的宽度,y高度就是画面的高度
Y0 Y1 Y2 Y3
Y4 Y5 Y6 Y7
Y8 Y9 Y10 Y11
Y12 Y13 Y14 Y15
后面需要显示彩色图像,又希望兼容老的设备,于是在y分量后面添加了uv分量用来表示色度。

I420: YYYYYYYY UU VV =>YUV420P
YV12: YYYYYYYY VV UU =>YUV420P
NV12: YYYYYYYY UVUV =>YUV420SP
NV21: YYYYYYYY VUVU =>YUV420SP
即 yuv420p数据格式是:(每4个y公用一个uv)
Y0 Y1 Y2 Y3
Y4 Y5 Y6 Y7
Y8 Y9 Y10 Y11
Y12 Y13 Y14 Y15
U0 U1 U2 U3
V0 V1 V2 V3
因此反转后就是
Y3 Y2 Y1 Y0
Y7 Y6 Y5 Y4
Y11 Y10 Y9 Y8
Y15 Y14 Y13 Y12
U3 U2 U1 U0
V3 V2 V1 V0
效果

代码
具体查看 006test_xvideo_view
m_file_sdl.read((char*)frame_buffer.get(), frame_size); // 读文件到数组
int frame_size = m_width * m_height * 3 / 2;
int frame_buffer_size = m_width * m_height * 2;
//原frame_buf
auto frame_buffer = std::make_unique<unsigned char[]>(frame_buffer_size);
//反转frame_buf
auto frame_buffer_reverse = std::make_unique<unsigned char[]>(frame_buffer_size);
// 计算反转frame
reverse_frame_buffer(frame_buffer.get(), frame_buffer_reverse.get());
m_view->Draw(frame_buffer_reverse.get(), m_width); // 渲染反转framevoid test_xvideo_view::reverse_frame_buffer(unsigned char* src, unsigned char* dst)
{
int frame_buffer_size = m_width * m_height * 2;
memset(dst, 0, frame_buffer_size);
// y分量
for (int row = 0; row < m_height; row++)
{
for (int col = 0; col < m_width; col++)
{
int src_index = row * m_width + col;
int dst_index = row * m_width + (m_width - col - 1);
dst[dst_index] = src[src_index];
}
}
// u分量
for (int row = m_height; row < m_height * 5 / 4; row++)
{
for (int col = 0; col < m_width; col++)
{
int src_index = row * m_width + col;
int dst_index = row * m_width + (m_width - col - 1);
dst[dst_index] = src[src_index];
}
}
// v分量
for (int row = m_height + m_height * 1 / 4; row < m_height *3 / 2; row++)
{
for (int col = 0; col < m_width; col++)
{
int src_index = row * m_width + col;
int dst_index = row * m_width + (m_width - col - 1);
dst[dst_index] = src[src_index];
}
}
}工厂模式总结
定义:
- 不能实例化,但提供静态方法去实例化派生类
- 有纯虚函数,需要派生类去具体实现
- 有枚举类型供选择,不同的内部接口
目的 - 隐藏内部的实现:本质就是在类外再套了一层
- 方便后期扩展接口
SDL实现类外套了一层XVideoView,用户就只需要用XVideoView提供的接口,就可以操作SDL了,后期也可以扩展OpenGL
具体查看 006test_xvideo_view
#include <mutex>
class XvideoView
{
public:
enum PixFormat
{
RGBA = 0,
ARGB,
YUV420
};
enum ViewType
{
SDL = 0
};
static XvideoView* Create(ViewType type = SDL);
virtual bool Init(int width, int height, PixFormat fmt, void* win_id) = 0;
virtual bool Draw(const void *data, int linesize) = 0;
virtual bool Clear() = 0;
virtual bool IsExit() = 0;
void Scale(int width, int height);
protected:
int m_scale_width;
int m_scale_height;
std::mutex m_mutex;
};智能指针、智能锁
智能指针、智能锁 #todo
AVFrame
#daily/25/10/12
AVFrame理解
AVFrame接口
av_frame_alloc创建avframe对象- 此时只有结构体中只有引用计数
AVBufferRef的指针变量,但不会给它分配空间,avbuffer是通过AVBufferRef来访问的,所以此时不会有引用计数,调用av_buffer_get_ref_count会报错
- 此时只有结构体中只有引用计数
av_frame_get_buffer给avframe对象分配缓冲区内存- 此时才分配了avbuffer,此时引用计数为1
- 参数源码里说推荐写0,会会根据当前cpu自动选择对齐方式
- qimage里是16字节对齐
- 每一次有函数对avframe对象进行引用时,引用计数都会+1
av_buffer_get_ref_count查看avframe对象的引用计数av_frame_ref引用对象,会共享同一片缓冲区(内存),但拥有独立的结构体存宽高等av_frame_unref清除avframe对象的引用计数- 引用计数为0的时候avbuffer会自动被释放,若此时还需要使用缓冲区需要重新用
av_frame_get_buffer申请
- 引用计数为0的时候avbuffer会自动被释放,若此时还需要使用缓冲区需要重新用
av_frame_free释放avframe对象
引用计数(AVBufferRef)的原理
引用计数本质就是一个智能指针
- 每一次对这个指针的引用都会让引用计数+1,离开作用域的时候-1
- 计数归0时,回调会自动释放
AVBuffer的空间
调用 av_buffer_alloc 的时候就会自动创建这个引用计数 AVBufferRef,通过引用计数 AVBufferRef就可以访问到真正的数据缓冲区 AVBuffer
AVFrame接口使用示例
具体查看 007test_avframe
int main()
{
cout << "ffmpeg version = " << av_version_info() << endl;
// 创建对象
AVFrame * frame = av_frame_alloc();
// 设置参数
frame->width = 400;
frame->height = 300;
frame->format = AV_PIX_FMT_ARGB;
// 分配空间-缓冲区
int re = av_frame_get_buffer(frame,0); // 这里frame的引用计数+1,即为1
if (re)
{
char errbuf[100];
av_strerror(re, errbuf, 100);
}
AVFrame * frame_dst = av_frame_alloc(); // 这里frame的引用计数+1,即为2
av_frame_ref(frame_dst,frame);
av_frame_unref(frame);
if (frame->buf[0])
cout << "frame_src ref count = " << av_buffer_get_ref_count(frame->buf[0]) << endl;
// 释放frame对象
av_frame_free(&frame);
av_frame_free(&frame_dst);
}从AVFrame获取数据,用SDL来渲染
- read的时候就直接将
y、u、v三个分量分别读取到三个buf中 - 再通过
SDL_UpdateYUVTexture这个接口来添加y、u、v三个分量SDL_UpdateTexture这个接口是添加 yuv 的宽高来实现添加到显存的
详细步骤
具体查看 010test_xvideo_view_fps
初始化 AVFrame 对象
// 创建对象
m_yuv_frame = av_frame_alloc();
// 设置属性
m_yuv_frame->width = m_width;
m_yuv_frame->height = m_height;
m_yuv_frame->format = AV_PIX_FMT_YUV420P;
m_yuv_frame->linesize[0] = m_width; // Y
m_yuv_frame->linesize[1] = m_width / 2; // U
m_yuv_frame->linesize[2] = m_width / 2; // V
// 分配缓冲区
av_frame_get_buffer(m_yuv_frame, 0);把YUV图像帧写入AVFrame
void test_xvideo_view::OnUpdateFrame()
{
//...
// 不能只读步长m_width,而是要把这一帧中所有的y都读到
m_file_sdl.read((char*)m_yuv_frame->data[0], m_height * m_width);
m_file_sdl.read((char*)m_yuv_frame->data[1], m_height * m_width / 4);
m_file_sdl.read((char*)m_yuv_frame->data[2], m_height * m_width / 4);
}用SDL渲染
bool XSDL::Draw(const unsigned char* y_data, int y_pitch,
const unsigned char* u_data, int u_pitch,
const unsigned char* v_data, int v_pitch)
{
//...
SDL_UpdateYUVTexture(m_sdl_texture,
NULL,
y_data, y_pitch,
u_data, u_pitch,
v_data, v_pitch
);
//...
}问题
这里的 sdl_width/2 实际存的时候不就是 sdl_width 吗,高度是1/4 ? #answer
yuv420p存储不是下面这样的吗
yyyy
yyyy
yyyy
yyyy
uuuu
vvvv回答:
在AVFrame中的YUV并不像 YUV文件中那样是连续存储的
Y0 Y1 Y2 Y3
Y4 Y5 Y6 Y7
U0 U1 V0 V1在AVFrame中是分了三个数组去存的
frame->linesize[0]yframe->linesize[1]uframe->linesize[2]v
因此需要明确的linesize行字节数 ,来表明这是YUV420p的排列方式
// y区
Y0 Y1 Y2 Y3
Y4 Y5 Y6 Y7
// u区
U0 U1
// v区
V0 V1
用QT显示帧率控制与显示
效果
具体查看010test_xvideo_view_fps
调节FPS
#done
fps = 每1秒渲染图像帧的次数 ===> 渲染1次图像帧需要 1000 / fps 毫秒
也就是用
startTimer(16); // 1000 / 60 = 16ms 这样就可以实控制刷新图片的速度了想要FPS高就调低,定时器的间隔
- 定时器、延时函数会受到任务队列的影响,当任务较多的时候可能会出现时间不准的问题
- 通过单开线程+延时函数就可以提供较为准确的时间
// 渲染线程
m_thread_render = std::thread([&]() {
while (!m_is_exited)
{
emit UpdateFrame();
MSleep(1000 / m_fps_set);
//std::this_thread::sleep_for(std::chrono::milliseconds(1000 / m_fps_set));
}
});这个 clock() 是根据cpu跳数来算时间的,不受任务队列的影响
void MSleep(int ms)
{
auto start = clock();
for (int i = 0; i < ms; i++)
{
std::this_thread::sleep_for(std::chrono::milliseconds(1));
// 为了防止,因为任务调度,导致延时函数出现的精度问题
if ((clock() - start) / (CLOCKS_PER_SEC / 1000) >= ms)
break;
}
}更新fps显示
和控制fps类似,也是通过定时1秒通过 `emit UpdateFps(); 来更新FPS的控件
void test_xvideo_view::OnUpdateFrame()
{
// ...
m_fps++;
if (m_start_time <= 0)
{
m_start_time = clock();
}
else if ((clock() - m_start_time) / (CLOCKS_PER_SEC / 1000) >= 1000)
{
emit UpdateFps();
m_start_time = clock();
m_fps = 0;
}
}
