基础配置

  • 头文件 include
  • 库文件 lib lib/x86 x64
  • 动态库文件 dll bin/x86 x64
  • 调式执行和pdb路径 bin/x86 x64
  • 源码项目路径 src/first_ffmpeg
  • ffmpeg源码 tools

image-20240619094132150

将文件配置进入项目中

(以Visual Studio 2017为例)

image-20240619093913928

image-20240619093954002

image-20240619093958478

image-20240619094003237

YUV格式

YUV444

表示$4:4:4$的YUV取样。

每个像素都有独立的Y、U、V分量。

每一个像素Y占一个字节,U占一个字节,V占一个字节,因此每个像素占:

$$1 (Y)×8 bits+1 (U)×8 bits+1 (V)×8 bits=24 bits/pixel (bpp)$$

以$2\times 2$的图像为例

1
2
3
4
像素1:Y1, U1, V1
像素2:Y2, U2, V2
像素3:Y3, U3, V3
像素4:Y4, U4, V4

YUV422

表示$4:2:2$的YUV取样。

每相邻两个像素每个像素占一个Y,共享一个U和一个V,因此两个像素占:

$$2 (Y)×8 bits+1 (U)×8 bits+1 (V)×8 bits=32 bits$$

每个像素占:

$$\displaystyle\frac{32bits}{2 pixels}=16 bits/pixel (bpp)$$

以$2\times 2$的图像为例

1
2
3
4
像素1:Y1, U1, V1
像素2:Y2, U1, V1
像素3:Y3, U2, V2
像素4:Y4, U2, V2

YUV422 格式比 YUV444 格式节省了数据量,同时仍然保持较高的图像质量。这就是为什么 YUV422 格式广泛用于视频压缩和传输的原因。

YUV411

表示$4:1:1$的YUV取样。

每相邻四个像素每个像素占一个Y,共享一个U和一个V,因此四个像素占:

$$4 (Y)×8 bits+1 (U)×8 bits+1 (V)×8 bits=48 bits$$

每个像素占:

$$\displaystyle\frac{48bits}{4 pixels}=12 bits/pixel (bpp)$$

以$2\times 2$的图像为例

1
2
3
4
像素1:Y1, U1, V1
像素2:Y2, U1, V1
像素3:Y3, U1, V1
像素4:Y4, U1, V1

YUV420

表示$4:2:0$的YUV取样。

水平每两个像素与垂直每两个像素中Y取4个,U取1个,V取1个,因此两个像素占:

$$4 (Y)×8 bits+1 (U)×8 bits+1 (V)×8 bits=48 bits$$

每个像素占:

$$\displaystyle\frac{48bits}{4 pixels}=12 bits/pixel (bpp)$$

以$2\times 2$的图像为例

1
2
3
4
像素1:Y1, U1, V1
像素2:Y2, U1, V1
像素3:Y3, U1, V1
像素4:Y4, U1, V1

这里的$4:2:0$代表了$4:2:0$和$4:0:2$两种情况,它们在奇偶行交错出现。

常见的H.264、H.265、VP8、AV1等都是以它为基础进行编解码的。

SDL_QT渲染

SDL(Simple DirectMedia Layer)在 FFmpeg 中的作用主要集中在视频和音频的实时播放方面。SDL 是一个跨平台的多媒体库,用于访问底层的硬件(如音频、视频、输入设备)并提供图形用户界面功能。

代码展示

sdlqtrgb.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include "sdlqtrgb.h"
#include <sdl/SDL.h>

// 链接 SDL2 的库文件
#pragma comment(lib, "SDL2.lib")

static SDL_Window* sdl_win = NULL;
static SDL_Renderer* sdl_render = NULL;
static SDL_Texture* sdl_texture = NULL;
static int sdl_width = 0;
static int sdl_height = 0;
static unsigned char* rgb = nullptr;
static int pix_size = 4;

void SDLQtRGB::timerEvent(QTimerEvent* ev)
{
static unsigned char tmp = 255;
tmp--;
for (int j = 0; j < sdl_height; j++)
{
int b = j * sdl_width * pix_size;
for (int i = 0; i < sdl_width * pix_size; i += pix_size)
{
rgb[b + i] = 0; // B
rgb[b + i + 1] = tmp; // G
rgb[b + i + 2] = 0; // G
rgb[b + i + 3] = 0; // A
}
}
SDL_UpdateTexture(sdl_texture, NULL, rgb, sdl_width * pix_size);
SDL_RenderClear(sdl_render);
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
rect.w = sdl_width;
rect.h = sdl_height;
SDL_RenderCopy(sdl_render, sdl_texture, NULL, &rect);
SDL_RenderPresent(sdl_render);
}

SDLQtRGB::SDLQtRGB(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);

// 获取label的宽高
sdl_width = ui.label->width();
sdl_height = ui.label->height();

// 初始化SDL
SDL_Init(SDL_INIT_VIDEO);

// 创建窗口
sdl_win = SDL_CreateWindowFrom((void*)ui.label->winId());

// 创建渲染器
sdl_render = SDL_CreateRenderer(sdl_win, -1, SDL_RENDERER_ACCELERATED);

// 创建材质
sdl_texture = SDL_CreateTexture(sdl_render,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
sdl_width,
sdl_height
);

rgb = new unsigned char[sdl_width * sdl_height * pix_size];

startTimer(10);
}

SDLQtRGB::~SDLQtRGB()
{}

获取label的宽高

1
2
sdl_width = ui.label->width();
sdl_height = ui.label->height();

初始化SDL

1
2
3
4
5
SDL_Init(SDL_INIT_VIDEO);

// SDL_Init源码
extern DECLSPEC int SDLCALL SDL_Init(Uint32 flags);
// 返回是一个int型变量,如果<0,表示初始化失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SDL_INIT_VIDEO
// 为宏命令

// 全部命令如下
#define SDL_INIT_TIMER 0x00000001u
#define SDL_INIT_AUDIO 0x00000010u
#define SDL_INIT_VIDEO 0x00000020u /**< SDL_INIT_VIDEO implies SDL_INIT_EVENTS */
#define SDL_INIT_JOYSTICK 0x00000200u /**< SDL_INIT_JOYSTICK implies SDL_INIT_EVENTS */
#define SDL_INIT_HAPTIC 0x00001000u
#define SDL_INIT_GAMECONTROLLER 0x00002000u /**< SDL_INIT_GAMECONTROLLER implies SDL_INIT_JOYSTICK */
#define SDL_INIT_EVENTS 0x00004000u
#define SDL_INIT_SENSOR 0x00008000u
#define SDL_INIT_NOPARACHUTE 0x00100000u /**< compatibility; this flag is ignored. */
#define SDL_INIT_EVERYTHING ( \
SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | \
SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER | SDL_INIT_SENSOR \
)

SDL_Init(SDL_INIT_VIDEO); 是用于初始化 SDL 的视频子系统的函数调用。通过这个调用,SDL 会设置视频驱动、显示模式等,使得后续的图形绘制操作可以顺利进行。在开发基于 SDL 的多媒体应用时,这通常是必不可少的初始化步骤。


创建窗口

1
2
3
static SDL_Window* sdl_win = NULL;

sdl_win = SDL_CreateWindowFrom((void*)ui.label->winId());

SDL_Window用于窗口的结构体,通常包含了有关窗口的各个消息,通常只为指针使用。

SDL_CreateWindowFrom 函数是 SDL 库中的一个函数,它允许 SDL 使用现有的窗口句柄创建一个 SDL 窗口。

1
2
3
4
5
6
// 函数原型
extern DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowFrom(const void *data);
// data:一个指向现有窗口句柄的指针。这个指针通常是从其他 GUI 库或框架中获取的窗口句柄。

// 返回值
// 一个指向 SDL_Window 结构体的指针,如果创建失败,返回 NULL。

ui.label->winId() 是 Qt 提供的一个函数,返回一个表示窗口句柄的 WId 类型(通常是一个平台相关的窗口句柄,比如 HWND 在 Windows 上)。这里我们将其强制转换为 void* 以便 SDL 可以使用。

总结】:使用Qt的label窗口句柄,创建SDL_Window。


创建渲染器

1
2
3
static SDL_Renderer* sdl_render = NULL;

sdl_render = SDL_CreateRenderer(sdl_win, -1, SDL_RENDERER_ACCELERATED);

SDL_Renderer 是一个不透明的数据类型,通常只作为指针使用。它表示用于渲染的上下文,可以在其中绘制点、线、矩形、纹理等图形元素。

1
2
extern DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window * window,
int index, Uint32 flags);

参数解释:

  • window:指向用于渲染的窗口 (SDL_Window*)。

  • index:指定渲染器的索引。通常设置为 -1,表示让 SDL 选择第一个可用的渲染器。

  • flags

    :指定渲染器的行为,可以是以下值的组合:

    • SDL_RENDERER_SOFTWARE:使用软件渲染。
    • SDL_RENDERER_ACCELERATED:使用硬件加速渲染。
    • SDL_RENDERER_PRESENTVSYNC:与显示器的垂直刷新同步。
    • SDL_RENDERER_TARGETTEXTURE:支持渲染目标纹理。

创建材质

1
2
3
4
5
6
7
8
static SDL_Texture* sdl_texture = NULL;

sdl_texture = SDL_CreateTexture(sdl_render,
SDL_PIXELFORMAT_ARGB8888,
SDL_TEXTUREACCESS_STREAMING,
sdl_width,
sdl_height
);

SDL_Texture 是 SDL 库中的一个结构体类型,用于表示纹理。纹理是指可以被渲染器处理和显示的图像数据。

SDL_CreateTexture函数原型:

1
2
extern DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window * window,
int index, Uint32 flags);

参数解释:

rendererSDL_Renderer* 类型,指向要在其上创建纹理的渲染器。

formatUint32 类型,纹理的像素格式。常用的像素格式包括 SDL_PIXELFORMAT_ARGB8888SDL_PIXELFORMAT_RGBA8888 等。

accessint 类型,纹理的访问模式。可以是以下值之一:

  • SDL_TEXTUREACCESS_STATIC:纹理的内容不会改变。
  • SDL_TEXTUREACCESS_STREAMING:纹理的内容会经常改变,可以通过锁定纹理来访问像素数据。
  • SDL_TEXTUREACCESS_TARGET:纹理可以作为渲染目标。

wint 类型,纹理的宽度(以像素为单位)。

hint 类型,纹理的高度(以像素为单位)。


表示RGB颜色

1
2
3
4
5
static unsigned char* rgb = nullptr;

static int pix_size = 4; // 像素值大小

rgb = new unsigned char[sdl_width * sdl_height * pix_size];

unsigned char* 是一个指向 unsigned char 类型的指针。unsigned char 是一种基本数据类型,表示一个无符号的 8 位整数(通常用于表示字节数据)。

在图像处理和低级别数据操作中,unsigned char 类型经常用于表示原始字节数据,比如 RGB 颜色值。


调用QT的timerEvent槽函数(重载了的)

1
startTimer(10);

函数原型:

1
int startTimer(int interval, Qt::TimerType timerType = Qt::CoarseTimer);

参数解释:

interval (int)

  • 表示计时器的时间间隔,单位为毫秒。计时器每经过这个时间间隔就会触发一次定时器事件。
  • 例如,如果 interval 设置为 1000,那么计时器每隔 1 秒触发一次定时器事件。

timerType (Qt::TimerType)

  • 指定计时器的类型。这个参数是可选的,默认值为

    1
    Qt::CoarseTimer

    。Qt 提供了几种不同的计时器类型:

    • Qt::PreciseTimer:精确的定时器,尽量确保定时器精确地按照指定的间隔时间触发。
    • Qt::CoarseTimer:粗略的定时器,可能会稍微不精确,但消耗的资源较少,适合大多数普通应用。
    • Qt::VeryCoarseTimer:非常粗略的定时器,间隔时间可能会有较大误差,消耗的资源最少,适用于对精度要求很低的场景。

填充RGBA的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 这是将二位数组展开为一位数组进行填充
// 以下介绍按照二位数组进行介绍

static unsigned char tmp = 255;
tmp--;
for (int j = 0; j < sdl_height; j++) // 遍历每一行
{
int b = j * sdl_width * pix_size; // 找到当前行的起始位置
for (int i = 0; i < sdl_width * pix_size; i += pix_size) // 填充这一行的值
{
rgb[b + i] = 0; // B
rgb[b + i + 1] = tmp; // G
rgb[b + i + 2] = 0; // R
rgb[b + i + 3] = 0; // A
}
}

/*
3*3的像素
BGRA BGRA BGRA
BGRA BGRA BGRA
BGRA BGRA BGRA
*/

更新纹理内容

1
SDL_UpdateTexture(sdl_texture, NULL, rgb, sdl_width * pix_size);

SDL_UpdateTexture函数用于更新纹理的内容,即将新的像素数据上传到指定的纹理中。

源码:

1
2
3
4
extern DECLSPEC int SDLCALL SDL_UpdateTexture(SDL_Texture * texture,
const SDL_Rect * rect,
const void *pixels,
int pitch);

参数说明:

texture (SDL_Texture)*

  • 指向要更新的纹理对象。在你的代码中,这个纹理对象是 sdl_texture

rect (const SDL_Rect)*

  • 指定要更新的纹理区域。传递 NULL 表示更新整个纹理。
  • 在你的代码中,传递的是 NULL,因此整个纹理将被更新。

pixels (const void)*

  • 指向新像素数据的指针。在你的代码中,这个指针是 rgb,指向包含新像素数据的内存。

pitch (int)

  • 每行像素数据的字节数,通常是纹理的宽度乘以每像素的字节数(即像素格式的字节数)。
  • 在你的代码中,这是 sdl_width * pix_size,表示每行像素数据的字节数。
  • 从上到下,从左到右进行纹理更新

进行渲染

1
2
3
4
5
6
7
8
SDL_RenderClear(sdl_render);
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
rect.w = sdl_width;
rect.h = sdl_height;
SDL_RenderCopy(sdl_render, sdl_texture, NULL, &rect);
SDL_RenderPresent(sdl_render);
  • SDL_RenderClear(sdl_render):清除当前渲染目标。

  • SDL_Rect rect:定义一个矩形区域,用于指定纹理在窗口中的位置和大小。

  • rect.xrect.y:设置矩形的左上角位置为 (0, 0)。

  • rect.wrect.h:设置矩形的宽度和高度为纹理的宽度和高度。

  • SDL_RenderCopy(sdl_render, sdl_texture, NULL, &rect):将纹理复制到渲染目标(窗口)。

  • SDL_RenderPresent(sdl_render):更新屏幕,显示当前渲染的内容。

SDL_RenderCopy函数原型:

1
2
3
4
extern DECLSPEC int SDLCALL SDL_RenderCopy(SDL_Renderer * renderer,
SDL_Texture * texture,
const SDL_Rect * srcrect,
const SDL_Rect * dstrect);

参数解释:

  1. renderer (SDL_Renderer)*
    • 指向渲染器的指针,用于绘制操作。在你的代码中,这是 sdl_render
    • 渲染器是一个用于管理所有的绘制操作的对象,它与特定的窗口相关联。
  2. texture (SDL_Texture)*
    • 指向纹理的指针,这是要复制的图像数据。在你的代码中,这是 sdl_texture
    • 纹理包含了要绘制的像素数据。
  3. srcrect const SDL_Rect)(源矩阵)*
    • 指向 SDL_Rect 结构的指针,用于指定从纹理的哪一部分复制。如果为 NULL,则表示从整个纹理复制。
    • 在你的代码中,这个参数是 NULL,因此整个纹理都会被复制。
  4. dstrect (const SDL_Rect)(目标矩阵)*
    • 指向 SDL_Rect 结构的指针,用于指定纹理复制到渲染目标的哪一部分。如果为 NULL,则表示纹理将被复制到整个渲染目标。
    • 在你的代码中,这是 &rect,表示纹理将被复制到渲染目标中由 rect 指定的区域。

SDL_Rect 结构体:

SDL_Rect 结构体用于定义一个矩形区域,包含以下成员:

  • int x:矩形左上角的 x 坐标。
  • int y:矩形左上角的 y 坐标。
  • int w:矩形的宽度。
  • int h:矩形的高度。

main.cpp

1
2
3
4
5
6
7
8
9
10
11
#include "sdlqtrgb.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
SDLQtRGB w;
w.show();
return a.exec();
}

sdlqtrgb.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#pragma once

#include <QtWidgets/QWidget>
#include "ui_sdlqtrgb.h"

class SDLQtRGB : public QWidget
{
Q_OBJECT

public:
SDLQtRGB(QWidget *parent = nullptr);
~SDLQtRGB();
void timerEvent(QTimerEvent* ev) override;

private:
Ui::SDLQtRGBClass ui;
};

使用 override 关键字的主要目的包括:

  1. 编译时检查:确保函数签名与基类中的虚函数匹配,防止由于拼写错误或参数类型错误导致的意外行为。
  2. 提高可读性:明确表明哪些函数是重写基类中的虚函数,便于代码维护和理解。

xvideo_view

代码展示

xsdl.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
#include "xsdl.h"
#include <sdl/SDL.h>
#include <iostream>
#pragma comment(lib, "SDL2.lib")

static bool InitVideo()
{
static bool is_first = true;
static std::mutex mux;
std::unique_lock<std::mutex> sdl_lock(mux);
if (!is_first) return true;
is_first = false;
if (SDL_Init(SDL_INIT_VIDEO))
{
std::cout << SDL_GetError() << std::endl;
return false;
}

// 设定缩放算法解决锯齿问题,线性差值算法
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");

return true;
}

bool XSDL::IsExit()
{
SDL_Event ev;

// SDL_WaitEventTimeout函数的作用是等待事件发生,直到超时为止
// (接收事件, 这里表示最多等待1毫秒)
SDL_WaitEventTimeout(&ev, 1);

// SDL_QUIT事件通常在用户请求关闭应用程序窗口时触发,例如点击窗口的关闭按钮。
if (ev.type == SDL_QUIT)
return true;
return false;
}

void XSDL::Close()
{
// 防止内存泄漏

std::unique_lock<std::mutex> sdl_lock(mtx_);

if (texture_)
{
SDL_DestroyTexture(texture_);
texture_ = nullptr;
}

if (render_)
{
SDL_DestroyRenderer(render_);
render_ = nullptr;
}

if (win_)
{
SDL_DestroyWindow(win_);
win_ = nullptr;
}
}

bool XSDL::Init(int w, int h,
Format fmt,
void *win_id)
{
if (w <= 0 || h <= 0) return false;

// 初始化SDL 视频库
InitVideo();

// 锁,确保线程安全
std::unique_lock<std::mutex> sdl_lock(mtx_);

width_ = w;
height_ = h;
fmt_ = fmt;

if (texture_) // 由于渲染包含材质,所以要先检查
SDL_DestroyTexture(texture_);

if (render_)
SDL_DestroyRenderer(render_);

//////// 1. 创建窗口
if (!win_)
{
if (win_id)
{
// 渲染到现有窗口
win_ = SDL_CreateWindowFrom(win_id);
}
else
{
// 新建窗口
win_ = SDL_CreateWindow("Video", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
w, h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
// SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED 表示由系统决定
// SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE 窗口支持OpenGL渲染和用户可以拖动窗口边框来改变尺寸
}
}

if (!win_)
{
std::cerr << SDL_GetError() << std::endl;
return false;
}

//////// 2. 创建渲染器
render_ = SDL_CreateRenderer(win_, -1, SDL_RENDERER_ACCELERATED);
if (!render_)
{
std::cerr << SDL_GetError() << std::endl;
return false;
}


//////// 3. 创建材质
unsigned int sdl_fmt = SDL_PIXELFORMAT_RGBA8888;

switch (fmt_)
{
case XVideoView::RGBA:
break;
case XVideoView::YUV420P:
sdl_fmt = SDL_PIXELFORMAT_IYUV;
break;
case XVideoView::ARGB:
sdl_fmt = SDL_PIXELFORMAT_ARGB32;
break;
default:
break;
}


texture_ = SDL_CreateTexture(render_,
sdl_fmt, // 像素格式
SDL_TEXTUREACCESS_STREAMING, // 频繁修改的渲染(带锁,但是用不到,内部以及有锁了)
w, h // 材质大小
);
if (!texture_)
{
std::cerr << SDL_GetError() << std::endl;
return false;
}

return true;
}

bool XSDL::Draw(const unsigned char* data,
int linesize)
{
if (!data) return false;

std::unique_lock<std::mutex> sdl_lock(mtx_);

// 如果任何一个资源未初始化或者宽度和高度小于等于0,则返回false。
if (!texture_ || !render_ || !win_ || width_ <= 0 || height_ <= 0)
return false;

if (linesize <= 0)
{
switch (fmt_)
{
case XVideoView::RGBA:
break;
case XVideoView::YUV420P:
// 每个像素占用的内存布局较为复杂,通常宽度直接决定了linesize。
linesize = width_;
break;
case XVideoView::ARGB:
// 每个像素由4个字节组成,分别对应A、R、G和B四个通道。
linesize = width_ * 4;
break;
default:
break;
}
}
if (linesize <= 0) return false;

/* 复制内存到显存
源码:
extern DECLSPEC int SDLCALL SDL_UpdateTexture(SDL_Texture * texture,
const SDL_Rect * rect,
const void *pixels,
int pitch);
*SDL_Texture texture:
指向要更新的纹理对象的指针。

*const SDL_Rect rect:
指向 SDL_Rect 结构体的指针,定义了要更新的纹理区域。如果传入 NULL,则更新整个纹理。

*const void pixels:
指向包含新像素数据的内存区域的指针。数据格式必须与创建纹理时指定的格式相匹配。

int pitch:
每一行像素数据的字节数。这通常是图像宽度乘以每个像素的字节数。例如,对于 ARGB 格式的图像,
如果宽度是 width_,那么 pitch 就是 width_ * 4。

*/
auto re = SDL_UpdateTexture(texture_, NULL, data, linesize);

if (re != 0)
{
std::cout << SDL_GetError() << std::endl;
return false;
}

// 清空屏幕
SDL_RenderClear(render_);

// 材质复制到渲染器
if (scale_w_ <= 0) scale_w_ = width_;
if (scale_h_ <= 0) scale_h_ = height_;

SDL_Rect rect;
SDL_Rect *prect = nullptr;
if (scale_w_ > 0) // 用户手动设置缩放
{
rect.x = 0;
rect.y = 0;
rect.h = scale_h_; // 渲染的宽高,可缩放
rect.w = scale_w_;
prect = &rect;
}

re = SDL_RenderCopy(render_, texture_, NULL, prect);
if (re != 0)
{
std::cout << SDL_GetError() << std::endl;
return false;
}

SDL_RenderPresent(render_);
return true;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static bool InitVideo()
{
static bool is_first = true;
static std::mutex mux;
std::unique_lock<std::mutex> sdl_lock(mux);
if (!is_first) return true;
is_first = false;
if (SDL_Init(SDL_INIT_VIDEO))
{
std::cout << SDL_GetError() << std::endl;
return false;
}

// 设定缩放算法解决锯齿问题,线性差值算法
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");

return true;
}

xsdl.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#pragma once
#include "xvideo_view.h"

struct SDL_Window; // 能够使用SDL_WINDOW指针
struct SDL_Renderer;
struct SDL_Texture;

class XSDL :public XVideoView
{
public:

void Close() override;


// 初始化渲染窗口 《线程安全》
// @para w 窗口宽度
// @para h 窗口高度
// @para fmt 绘制的像素格式
// @para win_id 窗口距离,如果为空,就创建新窗口
// @return 是否创建成功
bool Init(int w, int h,
Format fmt = RGBA,
void *win_id = nullptr) override;



// 渲染图像 《线程安全》
// @para data 渲染的二进制数据
// @para linesize 一行数据的字节数,对于YUV420P就是Y一行字节数
// 如果linesize <= 0 就根据宽度和像素格式自动算出大小
// @return 渲染是否成功
bool Draw(const unsigned char* data,
int linesize = 0) override;

bool IsExit() override;



private:
SDL_Window *win_ = nullptr;
SDL_Renderer *render_ = nullptr;
SDL_Texture *texture_ = nullptr;
};


xvideo_view.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "xsdl.h"

XVideoView* XVideoView::Create(RenderType type)
{
switch (type)
{
case XVideoView::SDL:
return new XSDL();
break;
default:
break;
}

return nullptr;
}

xvideo_view.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#ifndef XVIDEO_VIEW_H
#define XVIDEO_VIEW_H
#include <mutex>
/*
视频渲染接口类
隐藏SDL实现
渲染方案可替代
线程安全
*/

class XVideoView
{
public:
enum Format
{
RGBA = 0,
YUV420P,
ARGB
};

enum RenderType
{
SDL = 0
};

static XVideoView* Create(RenderType type=SDL);



// 初始化渲染窗口 《线程安全,可多次调用》
// @para w 窗口宽度
// @para h 窗口高度
// @para fmt 绘制的像素格式
// @para win_id 窗口距离,如果为空,就创建新窗口
// @return 是否创建成功
virtual bool Init(int w, int h,
Format fmt = RGBA,
void *win_id = nullptr) = 0;



// 渲染图像 《线程安全》
// @para data 渲染的二进制数据
// @para linesize 一行数据的字节数,对于YUV420P就是Y一行字节数
// 如果linesize <= 0 就根据宽度和像素格式自动算出大小
// @return 渲染是否成功
virtual bool Draw(const unsigned char* data, int linesize = 0) = 0;


// 清理所有申请的资源,包括关闭窗口
virtual void Close() = 0;


// 处理窗口退出事件
virtual bool IsExit() = 0;

// 显示缩放
void Scale(int w, int h)
{
scale_w_ = w;
scale_h_ = h;
}
protected:
int width_ = 0; // 材质宽度
int height_ = 0; // 材质高度
Format fmt_ = RGBA; // 像素格式
std::mutex mtx_; // 确保线程安全

int scale_w_ = 0; // 显示大小
int scale_h_ = 0;
};

#endif

sdlqtvideo.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "sdlqtrgb.h"
#include <fstream>
#include <QMessageBox>
#include "xvideo_view.h"

static int sdl_width = 0;
static int sdl_height = 0;
static unsigned char* yuv = nullptr;
static int pix_size = 2;
static std::ifstream yuv_file;
static XVideoView *view = nullptr;

void SDLQtRGB::timerEvent(QTimerEvent* ev)
{
yuv_file.read((char*)yuv, sdl_width * sdl_height * 1.5);
if (view->IsExit())
{
view->Close();
exit(0);
}

view->Draw(yuv);
}

SDLQtRGB::SDLQtRGB(QWidget *parent)
: QWidget(parent)
{
// 打开yuv文件
yuv_file.open("400_300_25.yuv", std::ios::binary);
if (!yuv_file)
{
QMessageBox::information(this, "", "open yuv failed!");
return;
}

ui.setupUi(this);

sdl_width = 400;
sdl_height = 300;
this->resize(sdl_width, sdl_height); // 调整主窗口的尺寸
ui.label->resize(sdl_width, sdl_height);
view = XVideoView::Create();
view->Init(sdl_width, sdl_height,
XVideoView::YUV420P);

view->Close();
view->Init(sdl_width, sdl_height,
XVideoView::YUV420P, (void*)ui.label->winId());


yuv = new unsigned char[sdl_width * sdl_height * pix_size];

startTimer(40);
}

void SDLQtRGB::resizeEvent(QResizeEvent *ev)
{
ui.label->resize(size());
ui.label->move(0, 0);
view->Scale(width(), height());
}

SDLQtRGB::~SDLQtRGB()
{}

sdlqtvideo.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once

#include <QtWidgets/QWidget>
#include "ui_sdlqtrgb.h"

class SDLQtRGB : public QWidget
{
Q_OBJECT

public:
SDLQtRGB(QWidget *parent = nullptr);
~SDLQtRGB();
void timerEvent(QTimerEvent* ev) override;
void resizeEvent(QResizeEvent *ev) override;

private:
Ui::SDLQtRGBClass ui;
};