背景——在 Qt 应用程序中渲染视频时,一个常见的需求是将每一帧更新到 QLabel 控件中显示。但是,当渲染线程不在主线程时,直接更新UI可能会导致程序崩溃。
解析视频数据
处理原始字节数组
对于RGBA格式的原始字节数组,可以直接创建QImage:
QImage image(frame, width, height, width * 4, QImage::Format_RGBA8888);
image = image.scaled(label->width(), label->height(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
处理YUV数据
对于YUV格式的数据,需要先获取Y、U、V分量的buffer和pitch(步长):
uint8_t *i420_buffer[3];
int pitch[3];
i420_buffer[0] = frame->GetBufferY();
i420_buffer[1] = frame->GetBufferU();
i420_buffer[2] =frame->GetBufferV();
pitch[0] = frame->GetPitch0();
pitch[1] = frame->GetPitch1();
pitch[2] = frame->GetPitch2();
i420_buffer 数组和 pitch 数组分别包含了YUV帧的数据和每行的字节数(也称为步长)。i420_buffer[0],i420_buffer[1] 和i420_buffer[2]分别包含了Y,U和V的数据。pitch[0],pitch[1]和pitch[2]分别是Y,U和V的步长。然后我们就可以使用这些数据和步长来访问YUV帧的每个像素:
- 访问第y行第x列的Y值:
uint8_t Y = i420_buffer[0][y * pitch[0] + x];
- 访问第y行第x列的U和V值:
uint8_t U = i420_buffer[1][(y/2)* pitch[1]+(x / 2)];
uint8_t V = i420_buffer[2][(y/2)* pitch[2]+(x / 2)];
(U和V的数据是以2x2的块存储的,所以需要除以2来获取正确的索引。)
YUV到RGB的转换可以通过以下公式进行:
R = Y + 1.402 * (V - 128)
G = Y - 0.344 * (U - 128) - 0.714 * (V - 128)
B = Y + 1.772 * (U - 128)
for(int y = 0; y < height; ++y){
for(int x = 0; x < width; ++x){
uint8_t Y = i420 buffer[0][y * pitch[0] + x];
uint8_t U = i420 buffer[1][(y / 2) * pitch[1] + (x / 2)];
uint8_t V = i420 buffer[2][(y / 2) * pitch[2] + (x / 2)];
int R = Y + 1.402 * (V - 128);
int G = Y - 0,344 * (U - 128) - 0.714 * (V - 128);
int B = Y + 1.772 * (U -128);
image.setPixelColor(x,y,QColor(qBound(0, R, 255), qBound(0, G, 255), qBound(0, B, 255)));
}
}
但是!!!
使用 setPixelColor 效率太低啦,一不小心还会收到 Qt Creator 的小提醒 😂😂:
The cached device pixel ratio value was stale on window expose. Please file a QTBUG which explains how to reproduce.
我们有更好用的工具:libyuv
libyuv::I420ToARGB(src_y, src_stride_y, src_u, src_stride_u, src_v, stc_stride_v, argb_buffer, width * 4, width, height);
安全地渲染到QLabel
如果渲染视频的线程是主线程,我们可以在得到 QImage 之后直接设置到 QLabel :
label->setPixmap(QPixmap::fromImage(image));
但是如果我们是在回调函数中解析帧数据并更新(也就是在非主线程中更新UI组件),那就可能会导致崩溃或其他不稳定行为啦。Qt 的 GUI 组件不是线程安全的,这意味着我们只能在主线程中对它们进行操作。
可以使用信号和槽机制将图像传递回主线程:在非主线程中处理图像并通过信号发送图像数据,在主线程中接收信号并更新QLabel。
// 在非主线程中
void processImage() {
// 将数据转换为QImage
emit imageProcessed(image);
}
// 在主线程中
void renderImage(const QImage& image) {
// ……
label->setPixmap(QPixmap::fromImage(image));
}
还有一种更巧妙的写法(参考 Qt Forum 讨论 ):
QTimer::singleShot(0, this, [this, pixmap](){
ui->label->setPixmap(pixmap);
ui->label->setScaledContents(true);
ui->label->show();
});
这种方法会在主事件循环的下一个时间点执行更新操作,从而确保在主线程中安全地更新UI。
总结
通过以上方法,我们可以在多线程环境下安全高效地将视频帧渲染到QLabel控件中,避免程序崩溃等问题。