前言
原生ffplay显示视频用的是SDL库,这里修改为用QT的openglwidget处理,通过openglwidget的shader(着色器)方式渲染。这种方式直接用GPU计算,降低了CPU占用率。
一、准备工作
先介绍几个结构体。ffplay解码后的视频保存在FrameQueue中,这里把解码后的视频,转换像素格式后保存在HFrame结构体中,后续opengl直接从HFrame中取数据显示视频。像素格式转换用到ffmpeg的库函数sws_scale()。
1、HFrame数据结构
class HFrame {
public:
HBuf buf; //储存YUV数据的buf
int w; //宽
int h; //高
int bpp; // 每个像素占用位数
int type; //像素格式
HFrame() {
w = h = bpp = type = 0;
}
bool isNull() {
return w == 0 || h == 0 || buf.isNull();
}
void copy(const HFrame& rhs) {
w = rhs.w;
h = rhs.h;
bpp = rhs.bpp;
type = rhs.type;
buf.copy(rhs.buf.base, rhs.buf.len);
}
};
保存解码数据经过像素转换后的数据,参数如下:
HBuf buf:储存像素转换后的YUV数据
int w: 像素宽
int h: 像素高
int bpp:每个像素占用位数
int type:像素格式
2、创建格式转换上下文函数sws_getContext()
struct SwsContext *sws_getContext(
int srcW, /* 输入图像的宽度 */
int srcH, /* 输入图像的高度 */
enum AVPixelFormat srcFormat, /* 输入图像的像素格式 */
int dstW, /* 输出图像的宽度 */
int dstH, /* 输出图像的高度 */
enum AVPixelFormat dstFormat, /* 输出图像的像素格式 */
int flags,/* 选择缩放算法(只有当输入输出图像大小不同时有效),一般选择SWS_FAST_BILINEAR */
SwsFilter *srcFilter, /* 输入图像的滤波器信息, 若不需要传NULL */
SwsFilter *dstFilter, /* 输出图像的滤波器信息, 若不需要传NULL */
const double *param /* 特定缩放算法需要的参数(?),默认为NULL */
);
该函数根据给定参数,创建SwsContext像素格式转换上下文,供sws_scale()使用
3、像素转换函数sws_scale()
int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],
const int srcStride[], int srcSliceY, int srcSliceH,
uint8_t *const dst[], const int dstStride[]);
sws_scale()的功能主要有图像色彩空间转换(像素转换)、分辨率缩放和前后图像滤波处理,这里我们主要用于像素格式转换,统一转换成YUV420P。以下是参数说明:
1)SwsContext *c:转换格式的上下文,也就是 sws_getContext 函数返回的结果。
2)参数 const uint8_t *const srcSlice[]: 输入图像的每个颜色通道的数据指针。就是解码后的AVFrame中的data[]数组。因为不同像素的存储格式不同,所以srcSlice[]维数也有可能不同。
以YUV420P为例,它是planar格式(平铺格式),在内存中的排布如下:
YYYYYYYY UUUU VVVV
使用FFmpeg解码后存储在AVFrame的data[]数组中时:
data[0]——-存储所有Y分量, Y1, Y2, Y3, Y4, Y5, Y6, Y7, Y8……
data[1]——-存储所有U分量, U1, U2, U3, U4……
data[2]——-存储所有V分量, V1, V2, V3, V4……
linesize[]数组中保存的是对应通道的数据宽度:
linesize[0]——-Y分量的宽度,就是图像的每行有多少个Y数据
linesize[1]——-U分量的宽度
linesize[2]——-V分量的宽度
而RGB24,它是packed格式,它在data[]数组中则只有一维,它的存储方式如下:
data[0]: R1, G1, B1, R2, G2, B2, R3, G3, B3, R4, G4, B4……
这里要注意的是,linesize[0]的值并不一定等于图片的宽度,有时候为了对齐各解码器的CPU,实际尺寸会大于图片的宽度,尤其是OpengGL硬件转换/渲染时要特别注意(opengl一般要求对齐方式为8字节),否则解码出来的图像会有异常。
3)参数const int srcStride[],输入图像的每个颜色通道的跨度。就是每个通道的行字节数,对应的是解码后的AVFrame中的linesize[]数组。根据它确立下一行的起始位置,不过stride和width不一定相同,这是因为:
a.由于数据帧存储的对齐,有可能会在每行后面增加一些填充字节这样 stride = width + N;
b.packet色彩空间下,每个像素几个通道数据混合在一起,例如RGB24,每个像素3字节连续存放,因此下一行的起始位置需要加上3*width字节。
4)参数int srcSliceY, int srcSliceH,定义在输入图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少行。如果srcSliceY=0,srcSliceH=height,表示一次性处理完整个图像。这种设置是为了多线程并行,例如可以创建两个线程,第一个线程处理 [0, h/2-1]行,第二个线程处理 [h/2, h-1]行,并行处理加快速度。
5)参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个颜色通道数据指针,每个颜色通道行字节数),这里的内存需要用户提前分配。
二、video_display()修改
在video_display()中,用sws_scale()函数把解码后的AVFrame数据,统一转换为YUV420P格式,同时发送信号通知opengl渲染YUV数据。
video_display()代码如下:
static void video_display(VideoState *is,MyFFPlay *pThis)
{
Frame *vp;
vp = frame_queue_peek_last(&is->pictq);
//先创建像素转换上下文,统一转换成YUV420P
if(is->img_convert_ctx==nullptr){
is->img_convert_ctx = sws_getContext(vp->width, vp->height,\
(AVPixelFormat)vp->format, vp->width, vp->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
}
if (is->img_convert_ctx == nullptr) {
qDebug("sws_getContext failed");
return ;
}
//初始化is->hframe,保存解码信息
//if(is->hframe.isNull()){ //第一次初始化
is->hframe.w = vp->width;
is->hframe.h = vp->height;
is->hframe.type = PIX_FMT_IYUV;
is->hframe.bpp = 12;
int y_size = vp->width * vp->height;
is->hframe.buf.resize(y_size * 3/2);
//is->hframe.buf.resize(y_size * 4);
is->hframe.buf.len = y_size * 3 / 2;
is->data[0] = (uint8_t*)is->hframe.buf.base;
is->data[1] = is->data[0] + y_size;
is->data[2] = is->data[1] + y_size / 4;
is->linesize[0] = vp->width;
is->linesize[1] = is->linesize[2] = vp->width / 2;
//}
if(is->hframe.isNull())
return;
//把转换后的数据,保存到is->hframe.buf
if (is->img_convert_ctx) {
int h = sws_scale(is->img_convert_ctx, vp->frame->data, vp->frame->linesize, 0, vp->frame->height, is->data, is->linesize);
if (h <= 0 || h != vp->frame->height) {
return;
}
}
//拷贝is->hframe的数据到GL的HFrame用于显示
if(pThis->m_videoWndFrame){
pThis->m_videoWndFrame->copy(is->hframe);
pThis->update_video();//发送信号通知opengl更新视频
}
}
函数功能如下:
1、获取渲染的视频Frame帧
通过frame_queue_peek_last(),获取要渲染的Frame帧,这里是ffplay原生的代码
2、创建格式转换上下文
创建sws_getContext()创建格式转换上下文img_convert_ctx,目标像素格式统一设置为YUV420P,高度和宽度不变,转换方式为SWS_BICUBIC
//先创建像素转换上下文,统一转换成YUV420P
if(is->img_convert_ctx==nullptr){
is->img_convert_ctx = sws_getContext(vp->width, vp->height,\
(AVPixelFormat)vp->format, vp->width, vp->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
}
//如果创建失败,直接返回
if (is->img_convert_ctx == nullptr) {
qDebug("sws_getContext failed");
return ;
}
3、创建HFrame并初始化,用于保存像素转换后的数据
在video_state结构体中,添加三个变量hframe,data数组,linesize数组。

hframe:用于保存像素转化后的数据
data数组:保存转换后每个YUV分量的数据指针,该变量在sws_scale()中使用
linesize数组:保存转换后每个YUV分量的行宽度,该变量在sws_scale()中使用
is->hframe.w = vp->width; //宽度
is->hframe.h = vp->height; //高度
is->hframe.type = PIX_FMT_IYUV; // yuv420p
is->hframe.bpp = 12; //每个像素占用12位
int y_size = vp->width * vp->height; // Y分量的大小
is->hframe.buf.resize(y_size * 3/2); //分配内存
is->hframe.buf.len = y_size * 3 / 2;//转换后的数据总大小
is->data[0] = (uint8_t*)is->hframe.buf.base;//计算Y分量的起始存储位置
is->data[1] = is->data[0] + y_size;//计算U分量的起始存储位置
is->data[2] = is->data[1] + y_size / 4;//计算V分量的起始存储位置
is->linesize[0] = vp->width;//转换后Y分量的行宽度
is->linesize[1] = is->linesize[2] = vp->width / 2;//转换后UV分量的行宽度
因为YUV420P的格式,每4个Y需要一组UV数据,一个Y需要1/4个UV数据,所以每个像素相当于占用1.5个字节,需要分配的内存数为像素数量*1.5,也就是width * height *1.5,Y分量占用内存数为width * height,UV分量占用内存数各为width * height * 1/4。对于输出的linesize[],Y分量的宽度为视频的宽度,UV分量的宽度各为行宽的一半。
4、开始转换
通过像素转换函数sws_scale()把数据转换后,保存到HFrame的buf中。
//把转换后的数据,保存到is->hframe.buf
if (is->img_convert_ctx) {
int h = sws_scale(is->img_convert_ctx, vp->frame->data, vp->frame->linesize, 0, vp->frame->height, is->data, is->linesize);
if (h <= 0 || h != vp->frame->height) {
return;
}
}
这里注意sws_scale()最后的两个参数,分别是保存YUV分量的指针数组和行宽数组,对应于is->data[]和is->linesize[]。
5、转换后拷贝到opengl中
在MyFFPlay类中,添加变量HFrame * m_videoWndFrame,该变量m_videoWndFrame指向opengl模块的HFrame last_frame。

在HVideoWidget::initUI()中,设置m_videoWndFrame为GLWnd的last_frame的指针,opengl渲染YUV时从last_frame获取数据

像素格式转换完成后,YUV数据保存在is->hframe中,调用HFrame的copy()函数,把数据拷贝到GLWnd的HFrame中,同时发送信号通知opengl。
//拷贝is->hframe的数据到GL的HFrame用于显示
if(pThis->m_videoWndFrame){
pThis->m_videoWndFrame->copy(is->hframe);
pThis->update_video();//发送信号通知opengl更新视频
}
update_video()如下所示,发送信号sig_update_video
void MyFFPlay::update_video(){
emit sig_update_video();
}
在void HVideoWidget::initConnect()中,sig_update_video信号连接到槽函数on_update_video()

槽函数on_update_video()如下,调用GLWnd的update()方法,渲染YUV数据。
void HVideoWidget::on_update_video() {
videownd->update();
}
即发送信号sig_update_video的目的是调用opengl的update()方法,最终在opengl中完成视频渲染。

被折叠的 条评论
为什么被折叠?



