FFmpeg入门教程10.07:软解并使用QWidget播放视频(YUV420P转RGB32)

系列索引:FFmpeg入门系列索引

上一篇:FFmpeg入门教程10.06:解码视频并保存为YUV格式文件

前两篇介绍了视频帧解码和帧数据解码保存,都是不够实时和直观,本篇介绍使用Qt作为界面来显示解码后的数据。

使用ffmpeg解码视频每一帧,因为比较耗时,所以独立一个线程。解码完成后的数据发送给界面,界面渲染显示图像数据,界面显示一个线程。

解码流程和之前一样。

解码流程图为:

flowchart TB
    F --Yes--> I
    K --下一帧--> F
    I --No--> F
    subgraph init
        direction TB
        A(开始) --> B[打开文件]
        B --> C[查找流信息]
        C --> D[查找对应解码器]
        D --> E[打开解码器]
        E --> F{读取帧}
        F --No--> G(结束)
    end
    subgraph decode
        direction TB
        I{是视频帧?} --Yes--> J[发送帧给解码器]
        J --> K[从解码器获取结果]
    end
flowchart TB
    F --Yes--> I
    K --Next Frame--> F
    I --No--> F
    subgraph init
        direction TB
        A(Start) --> B[avformat_open_input]
        B --> C[av_find_stream_info]
        C --> D[avcodec_find_decoder]
        D --> E[avcodec_open]
        E --> F{av_read_frame}
        F --No--> G(End)
    end
    subgraph decode
        direction TB
        I{Video Packet?} --Yes--> J[avcodec_send_packet]
        J --> K[avcodec_receive_frame]
    end

Qt显示流程为

flowchart TB

A(开始) --> B[创建图形对象img]
B --> C[解码结果信号槽]
C --> D[接收解码数据]
D --> E[更新img对象]
E --> F[更新界面update]
F --> G[paintEvent]
G --> H(结束)

视频解码显示流程图为:

flowchart TB

    K --signals/slots--> L
    subgraph decode
        direction TB
        A(Start) --> B[avformat_open_input]
        B --> C[av_find_stream_info]
        C --> D[avcodec_find_decoder]
        D --> E[avcodec_open]
        E --> F{av_read_frame}
        F --No--> G(End)
        F --Yes--> H[avcodec_send_packet]
        H --> I[avcodec_receive_frame]
        I --> J[sws_scale]
        J --> K[QImage]
    end
    subgraph display
        direction TB
        L[receiveQImage] --> M[paintEvent]
    end

解码部分

解码部分和之前的一样,不过需要调整一下。

像初始化变量、打开文件、分配解码器上下文、打开解码器等等,这些操作只需要一次,并且耗时很短,不需要放在独立线程里面。而发送数据给解码器、解码、接收解码器解码结果、格式转换这些操作会一直持续直到视频处理结束,此部分最耗时,放在独立线程中。

qt的多线程结构的一种为:

1
2
3
4
5
6
7
8
class FFmpegVideo:public QThread{
public:
...
protected:
void run();
private:
...
}

继承QThread之后,重写run()函数,所有耗时操作在此函数中,且只有此函数的内容是在其他线程中。

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
void FFmpegVideo::run()
{
if(!openFlag){//是否有过初始化、打开文件的操作
qDebug()<<"Please open file first.";
return;
}

while(av_read_frame(fmtCtx,pkt)>=0){//读取一帧
if(pkt->stream_index == videoStreamIndex){//视频流
if(avcodec_send_packet(videoCodecCtx,pkt)>=0){//发送帧给解码器
int ret;
while((ret=avcodec_receive_frame(videoCodecCtx,yuvFrame))>=0){//接收解码之后的结果
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
fprintf(stderr, "Error during decoding\n");
exit(1);
}
//格式转换
sws_scale(img_ctx,
yuvFrame->data,yuvFrame->linesize,
0,videoCodecCtx->height,
rgbFrame->data,rgbFrame->linesize);

//将数据处理为QImage格式
QImage img(out_buffer,
videoCodecCtx->width,videoCodecCtx->height,
QImage::Format_RGB32);
emit sendQImage(img);//发送结果数据信号
QThread::msleep(30);//延时30ms
}
}
av_packet_unref(pkt);
}
}
}

显示部分

首先创建一个基于QWidget的类,名为FFmpegWidget。使用时,在ui中添加一个基本QWidget控件

ui

使用时将此控件提升为FFmpegWidget就可以了。这样对FFmpegWidget的操作就会直接作用与界面。

prompt

一帧解码完成之后会将数据转换为QImage的格式,然后发送信号emit sendQImage(const QImage img);,将打包好的QImage图像发送出来。在显示界面有个对应的处理槽receiveQImage(const QImage img);将接收到的图像数据赋值给界面类的全局变量。

1
2
3
4
5
void FFmpegWidget::receiveQImage(const QImage &rImg)
{
img = rImg.scaled(this->size());
update();
}

更新img数据后,调用update()函数刷新界面,此函数会自动调用paintEvent函数绘制界面。

重写qt的paintEvent函数,界面会循环调用这个函数来在界面上绘图(当然,调用update()函数时也会调用此函数。)。

1
2
3
4
5
void FFmpegWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.drawImage(0,0,img);
}

界面为:

ui

效果为:

result

cpu占用率为:

cpu rate

源码在ffmpeg_beginner10.07.video_decode_by_cpu_display_by_qwidget下。

下一篇:FFmpeg入门教程10.08:软解并使用QOpenGL播放视频(YUV420P->OpenGL)


FFmpeg入门教程10.07:软解并使用QWidget播放视频(YUV420P转RGB32)
https://blog.jackeylea.com/ffmpeg/ffmpeg-video-decode-by-cpu-display-by-qwidget-in-yuv420p/
作者
JackeyLea
发布于
2021年1月8日
许可协议