您当前的位置: 首页 >  qt

DS小龙哥

暂无认证

  • 1浏览

    0关注

    679博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

QT软件开发-基于FFMPEG设计视频播放器-软解图像(一)

DS小龙哥 发布时间:2022-09-13 14:17:37 ,浏览量:1

QT软件开发-基于FFMPEG设计视频播放器-CPU软解视频(一) https://xiaolong.blog.csdn.net/article/details/126832537

QT软件开发-基于FFMPEG设计视频播放器-GPU硬解视频(二) https://xiaolong.blog.csdn.net/article/details/126833434

QT软件开发-基于FFMPEG设计视频播放器-解码音频(三) https://xiaolong.blog.csdn.net/article/details/126836582

QT软件开发-基于FFMPEG设计视频播放器-OpenGL渲染视频(四) https://xiaolong.blog.csdn.net/article/details/126842988

QT软件开发-基于FFMPEG设计视频播放器-流媒体播放器(五) https://xiaolong.blog.csdn.net/article/details/126915003

QT软件开发-基于FFMPEG设计视频播放器-视频播放器(六) https://blog.csdn.net/xiaolong1126626497/article/details/126916817

一、前言

说起ffmpeg,只要是搞音视频相关的开发应该都是听过的。FFmpeg提供了非常先进的音频/视频编解码库,并且支持跨平台。

现在互联网上ffmpeg相关的文章、教程也非常的多,ffmpeg本身主要是用来对视频、音频进行解码、编码。本身不提供图片渲染、声音输出等功能。要设计出自己的一款播放器,首先也得需要其他的一些知识配合调用ffmpeg才能完成任务。

为了能简单快速的介绍ffmpeg使用,我这里的会连续通过几篇文章,编写几个案例来循序渐进的演示ffmpeg的用法,最终完成一个完整的播放器开发,不涉及理论知识(理论知识网上太多了),主要是以代码、以实现功能为主。

我这里开发视频播放器用到的环境介绍:

ffmpeg版本:  4.2.2
Qt版本    :  5.12.6
编译器类型 : MinGW32bit 

这几篇文章循序渐进编写的内容规划与案例如下:

(1)利用ffmpeg解码视频,通过QWidget渲染解码后的图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

特点: 采用软件解码(CPU)、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。

(2)利用ffmpeg的硬件加速接口完成视频解码,支持探测当前硬件支持的加速方式,解码后从GPU里拷贝数据到CPU,完成像素转换,再通过QWidget渲染图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

特点: 采用硬件加速解码(GPU)、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的硬件解码基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。

(3)利用ffmpeg解码视频里的音频包,通过QAudioOutput播放音频数据。

特点: 只解码音频数据,忽略视频图像数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成音频数据解码,转换音频数据格式,最后通过QAudioOutput播放出来。

(4)利用ffmpeg的硬件加速接口完成视频解码,支持探测当前硬件支持的加速方式,通过QOpenGLWidget渲染解码的图像数据,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

特点: 采用硬件加速解码(GPU),OpenGL渲染、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的硬件解码和OpenGL渲染的基本使用流程。

(5)在第(4)个例子上增加流媒体播放支持,支持rtmp、rtsp、HLS(HTTP协议)等常见的流媒体格式支持,利用ffmpeg的硬件加速接口完成视频解码,支持探测当前硬件支持的加速方式,通过QOpenGLWidget渲染解码的图像数据。

特点: 采用硬件加速解码(GPU),OpenGL渲染、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的硬件解码和OpenGL渲染的基本使用流程。

(6)结合第(3)个例子和第(5)例子,增加音频包解码播放,利用ffmpeg的硬件加速接口完成视频解码,支持探测当前硬件支持的加速方式,通过QOpenGLWidget渲染解码的图像数据,通过QAudioOutput播放音频数据。支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

特点: 采用硬件加速解码(GPU),OpenGL渲染、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的硬件解码和OpenGL渲染的基本使用流程,通过QAudioOutput播放音频数据的流程。

(7)结合前面的例子合并,设计完成的视频播放器。

二、解码与渲染

如果要做一个视频播放器,主要解决3个问题:(1)解码 (2)渲染 (3)音视频同步

2.1 解码

ffmpeg支持纯软件解码和硬件加速解码。 纯软件解码只要是依靠CPU,如果分辨率较大(4K及以上)的视频软解会占用很高的CPU,并且解码速度也比较慢,加上渲染的时间,整体视频播放器就有卡顿现象。 如果利用硬件加速(GPU)解码,解码的时间会大大缩短。我的电脑是i7低功耗CPU,在我电脑上测试:一个分辨率为3840x2160的视频,软解一帧耗时300ms左右,如果启用硬件加速解码,一帧耗时10ms左右,可以速度相差是非常大的。 并且软解时,CPU占用几乎是100%,如果通过GPU解码,CPU的负荷就降的非常低,可以腾出更多时间去干别的事情。

软解和硬解本身差别不是很大,因为ffmpeg已经将API全部封装好了,只需要调用即可,不需要去了解底层的很多东西,开发起来非常方便。

ffmpeg源码下提供了很多例子,其中就有视频解码的例子。

ffmpeg\doc\examples\decode_video.c
ffmpeg\doc\examples\hw_decode.c

这两个例子都很有参考价值,其中 hw_decode.c 就是ffmpeg硬件加速解码的例子,通过这个例子就可以理解ffmpeg如何调用GPU进行硬件解码。

当然,ffmpeg也带了一个命令行的播放器,源码就是ffplay.c,这个代码实现的很完善,就是一个播放器,只不过ffplay.c的代码比较多,除了ffmpeg本身的API调用以外,渲染的部分是通过SDL实现的,如果前期对ffmpeg、SDL不怎么熟悉,直接去看ffplay.c可能效果不是太好。

那最好的办法就是先从简单开始,循序渐进的理解,最后再去看ffplay.c,这样效果就会好很多。

2.2 渲染

ffmpeg本身只是解码、编码的库,解码出来的图像渲染要自己实现。所谓的渲染就是显示ffmpeg解码视频之后得到的图片数据。 渲染也分为软件渲染、硬件加速渲染。 当前我这里的UI是采用Qt做的,在Qt里显示图片的方式很多,可以直接通过Qwidget绘制、QLabel显示等。这种方式是最常规的方式,也是最简单的方式,这种方式绘制就是采用CPU,对CPU占用较高,而且要通过Qwidget、QLabel等方式显示,需要将ffmpeg解码出来的数据转换像素格式,再封装为QImage格式,这个过程是很费时间的。如果要降低CPU占用,加快渲染速度,可以采用OpenGL渲染,Qt里封装了QOpenGLWidget,调用OpenGL也相对很方便。

三、视频播放器设计 3.1 设计说明

利用ffmpeg解码视频,通过QWidget渲染解码后的图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。

特点: 采用软件解码(CPU)、只解码图像数据,忽略音频数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。

视频解码采用独立子线程,解码后将得到的图像数据通过信号槽发方式传递给UI界面进行渲染。

3.2 解码流程

下面是ffmpeg软件解码的基本流程:

//1.打开多媒体流,并且获取一些信息
avformat_open_input(&format_ctx, m_MediaFile, nullptr, nullptr)


//2. 读取媒体文件的数据包以获取流信息
avformat_find_stream_info(format_ctx, nullptr)


//3. 查找解码器
AVCodec *video_pCodec=avcodec_find_decoder(stream->codecpar->codec_id);

//4. 打开解码器
avcodec_open2(stream->codec,video_pCodec,nullptr)

//5. 读取一帧数据
av_read_frame(format_ctx, &pkt)

//6. 发送视频帧
avcodec_send_packet(format_ctx->streams[video_stream_index]->codec,&pkt)

//7. 对视频帧进行解码
avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, SRC_VIDEO_pFrame)

//8. 转换像素格式
sws_scale(img_convert_ctx,
	   (uint8_t const **) SRC_VIDEO_pFrame->data,
	   SRC_VIDEO_pFrame->linesize, 0, video_height, RGB24_pFrame->data,
	   RGB24_pFrame->linesize);

//9. 渲染

这里面耗时较多的是sws_scale,视频分辨率越大,消耗的时间越长。其次就是avcodec_receive_frame,和渲染,如果软解的视频超过4K,解码就特别消耗时间,如果低于4K分辨率,解码消耗的时间还能接受的。

3.3 运行效果

image-20220913140928520

四、源代码 4.1 解码线程.cpp
//指定文件的编码为UTF-8
#pragma execution_character_set("utf-8")

#include "ReverseDecodThread.h"

ReverseDecodThread::ReverseDecodThread()
{
    qDebug() codec,video_pCodec,nullptr)!=0)
            {
                  LogSend(tr("解码器打开失败.\n"));
                  return -1;
            }
            video_stream_index = i;
            //得到视频帧的宽高
            video_width=stream->codecpar->width;
            video_height=stream->codecpar->height;

            LogSend(tr("视频帧的尺寸(以像素为单位): (宽X高)%1x%2 像素格式: %3\n").arg(
                stream->codecpar->width).arg(stream->codecpar->height).arg(stream->codecpar->format));
        }
    }

    if (video_stream_index == -1)
    {
         LogSend("没有检测到视频流.\n");
         return -1;
    }

    AVRational frameRate = format_ctx->streams[video_stream_index]->avg_frame_rate;

    /*设置视频转码器*/
    SRC_VIDEO_pFrame = av_frame_alloc();
    RGB24_pFrame = av_frame_alloc();// 存放解码后YUV数据的缓冲区

    //将解码后的YUV数据转换成RGB24
    img_convert_ctx = sws_getContext(video_width, video_height,
            format_ctx->streams[video_stream_index]->codec->pix_fmt,video_width, video_height,
            AV_PIX_FMT_RGB24, SWS_BICUBIC, nullptr, nullptr, nullptr);

    //计算RGB图像所占字节大小
    int numBytes=avpicture_get_size(AV_PIX_FMT_RGB24,video_width,video_height);

    //申请空间存放RGB图像数据
    out_buffer_rgb = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));

    // avpicture_fill函数将ptr指向的数据填充到picture内,但并没有拷贝,只是将picture结构内的data指针指向了ptr的数据
    avpicture_fill((AVPicture *) RGB24_pFrame, out_buffer_rgb, AV_PIX_FMT_RGB24,
            video_width, video_height);

    qDebug()label_duration->setText(QString("%1/%2").arg(pts).arg(duration));
}


void Widget::on_pushButton_play_clicked()
{
    DecodeWorkThread.SetSate(0);
    DecodeWorkThread.quit();
    DecodeWorkThread.wait();

    DecodeWorkThread.SetSate(1);

    QString filename = QFileDialog::getOpenFileName(this, "选择打开的文件", "C:/", tr("*.*"));
    DecodeWorkThread.set_VideoFile(filename);
    DecodeWorkThread.start();
}


void Widget::on_pushButton_pause_clicked()
{
    if (DecodeWorkThread.GetSate() == 2)
    {
        DecodeWorkThread.SetSate(1);
    }
    else if(DecodeWorkThread.GetSate() == 1)
    {
        DecodeWorkThread.SetSate(2);
    }
}


void Widget::on_pushButton_stop_clicked()
{
     DecodeWorkThread.SetSate(0);
}


bool Widget::eventFilter(QObject *obj, QEvent *event)
{
    //解决QSlider点击不能到鼠标指定位置的问题
    if(obj==ui->horizontalSlider_time)
    {
        if (event->type()==QEvent::MouseButtonPress) //判断类型
        {
            QMouseEvent *mouseEvent = static_cast(event);
            if (mouseEvent->button() == Qt::LeftButton)	//判断左键
            {
               int value = QStyle::sliderValueFromPosition(ui->horizontalSlider_time->minimum(), ui->horizontalSlider_time->maximum(), mouseEvent->pos().x(), ui->horizontalSlider_time->width());
               ui->horizontalSlider_time->setValue(value);

               qDebug()            
关注
打赏
1664009229
查看更多评论
0.0432s