音视频基础学习之【01.简单播放器demo实现】

目录

1.项目配置

2.显示界面设计

3.视频解码显示

流程描述

4.演示


最近在学习音视频基础知识,在这里感谢雷神留下的一系列指引新手入门的宝贵资源,虽然他英年早逝,但他的硕果永存。不由感慨真是天妒英才,愿雷神在天堂安好

附上学习资料地址:雷霄骅(leixiaohua1020)的专栏

选择学习ffmpeg的原因是,它具有跨平台特性,Windows、Linux、Android、IOS这些主流系统可以通吃

而且它非常全能,从视频采集、视频编码到视频传输都可以直接使用ffmpeg完成,有雷神留下的学习资料加持,学习起来自然是事半功倍

下面简单记录一下自己使用Qt来做图形界面学习ffmpeg的过程

1.项目配置

首先新建一个Qt Widgets项目

选择一个路径,名字不要带有中文

这里使用的是Qt5.6.2版本32位的工具包

新建一个继承QWidget的类

将在官网获取的ffmpeg库解压到与pro文件同级目录下,这里使用的是ffmpeg-4.2.2版本

获取地址:https://www.ffmpeg.org/download.html

其中包含bin动态库,include头文件,lib引入库

修改工程配置文件VideoPlayerDemo.pro,添加使用ffmpeg依赖的库

新建一个C++类

继承QObject

在新建的类中添加头文件,指示编译器这部分代码按C语言进行编译

先编译输出一下,在同级目录下生成debug路径

打开ffmpeg的bin目录,将其中所有文件(主要是dll动态库)拷贝到debug目录下

项目配置方面小小告一段落

2.显示界面设计

打开界面文件videoplayerdemo.ui,将外层widget设置大小为800*600,并设置最小大小为800*600

拖入两个widget控件,wdg_show——用于显示解码得到的图片、wdg_ctrl——用于添加播放控制按钮,在wdg_ctrl中添加一个pb_play按钮,用于播放视频文件

设置wdg_show、wdg_ctrl的最小大小,设置wdg_ctrl的最大高度,在外层widget上进行垂直布局,可以实现拉伸跟随缩放的效果

新建一个类VideoItem,用于重写paintEvent绘图事件(后面会说为什么要新建这个类)

3.视频解码显示

如果使用主线程进行解码,会造成客户端卡顿的现象,因此这里需要使用多线程开启一个线程去执行解码操作

考虑到需要将解码得到的图片传出,并在Qt控件上显示,因此使用Qt封装的线程函数,便于使用信号和槽机制

  1. 在videoshow.h中添加头文件<QThread>
  2. 由这个类继承QThread类
  3. 更改原有的构造函数,添加Qt线程函数run()
  4. 添加信号SIG_GetOneImage(const QImage image),这里不使用引用传递的原因是:由于是多线程操作图片,如果传递引用,可能图片在显示处理前已经被释放,所以需要拷贝一份图片
  5. 添加一会需要打开并进行解码的视频文件

流程描述

  1. 初始化 ffmpeg,调用av_register_all()才能正常使用编码器和解码器注册所用函数 
  2. 需要分配一个 AVFormatContext,ffmpeg所有的操作都要通过这个 AVFormatContext 来进行,可以理解为视频文件指针
  3. avformat_open_input()——打开视频文件,avformat_find_stream_info()——获取视频文件信息
  4. avcodec_find_decoder()——查找解码器,avcodec_open2()——打开解码器
  5. av_read_frame()——循环读取视频帧
  6. AVPacket存放的是解码得到的H.264格式的数据,AVFrame存放的是YUV420p格式的数据
  7. 将解码后的YUV420p 格式视频数据 转换 成 RGB32,抛出信号去控件显示,在Qt控件上绘图显示出来
  8. 回收数据
VideoShow::VideoShow()
{
    m_fileName = "D:/Kugou/华语群星 - 少林英雄.mp4";

}

void VideoShow::run()
{
    //1.初始化 FFMPEG 调用了这个才能正常使用编码器和解码器 注册所用函数
    av_register_all();
    //2.需要分配一个 AVFormatContext,FFMPEG 所有的操作都要通过这个 AVFormatContext 来进行可以理解为视频文件指针
    AVFormatContext *pFormatCtx = avformat_alloc_context();
    //中文兼容
    std::string path = m_fileName.toStdString();
    const char* file_path = path.c_str();

    //3. 打开视频文件
    if( avformat_open_input(&pFormatCtx, file_path, NULL, NULL) != 0 )
    {
        qDebug()<<"can't open file";
        return;
    }
    //3.1 获取视频文件信息
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
    {
        qDebug()<<"Could't find stream infomation.";
        return;
    }


    //4.读取视频流
    int videoStream = -1;
    int i;
    for (i = 0; i < pFormatCtx->nb_streams; i++)
    {
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            videoStream = i;
        }
    }
    //如果 videoStream 为-1 说明没有找到视频流
    if (videoStream == -1)
    {
        printf("Didn't find a video stream.");
        return;
    }
    //5.查找解码器
    auto pCodecCtx = pFormatCtx->streams[videoStream]->codec;
    auto pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL)
    {
        printf("Codec not found.");
        return;
    }
    //打开解码器
    if(avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
    {
        printf("Could not open codec.");
        return;
    }
    //6.申请解码需要的结构体 AVFrame 视频缓存的结构体
    AVFrame *pFrame, *pFrameRGB;
    pFrame = av_frame_alloc();
    pFrameRGB = av_frame_alloc();
    //分配一个 packet
    AVPacket *packet = (AVPacket *) malloc(sizeof(AVPacket));

    //7.这里我们将解码后的 YUV 数据转换成 RGB32 YUV420p 格式视频数据-->RGB32--> 图片显示出来
    static struct SwsContext *img_convert_ctx;
    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
                                     pCodecCtx->pix_fmt, pCodecCtx->width,pCodecCtx->height,
                                     AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL);
    //计算RGB一帧数据大小
    auto numBytes = avpicture_get_size(AV_PIX_FMT_RGB32,pCodecCtx->width ,pCodecCtx->height);
    //申请RGB一帧画面大小对应的空间
    uint8_t * out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    //pFrameRGB与out_buffer绑定
    avpicture_fill( (AVPicture *) pFrameRGB, out_buffer, AV_PIX_FMT_RGB32,
                    pCodecCtx->width, pCodecCtx->height);
    //8.循环读取视频帧, 转换为 RGB 格式, 抛出信号去控件显示
    int ret, got_picture;
    while(1)
    {
        //可以看出 av_read_frame 读取的是一帧视频,并存入一个 AVPacket 的结构中
        if (av_read_frame(pFormatCtx, packet) < 0)
        {
            break; //这里认为视频读取完了
        }
        //生成图片
        if (packet->stream_index == videoStream)
        {
            // 解码 packet(H264) 存在 pFrame(yuv) 里面
            ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture,packet);
            if (ret < 0)
            {
                printf("decode error");
                return ;
            }
            //有解码器解码之后得到的图像数据都是 YUV420 的格式,而这里需要将其保存成图片文件
            //因此需要将得到的 YUV420 数据转换成 RGB 格式
            if (got_picture)
            {
                //YUV420转换RGB
                sws_scale(img_convert_ctx,
                          (uint8_t const * const *) pFrame->data,
                          pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
                          pFrameRGB->linesize);
                //out_buffer 与 pFrameRGB 是捆绑的,将 out_buffer 里面的数据存在 QImage 里面
                QImage tmpImg((uchar*)out_buffer,pCodecCtx->width,pCodecCtx->height,QImage::Format_RGB32);
                //把图像复制一份 传递给界面显示
                QImage image = tmpImg.copy();
                //显示到控件 多线程 无法控制控件 所以要发射信号
                emit SIG_GetOneImage( image );
            }
        }
        av_free_packet(packet);
        msleep(5); // 停一停
    }
    //9.回收数据
    av_free(out_buffer);
    av_free(pFrame);
    av_free(pFrameRGB);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);
}

上述流程解码得到图片image,将解码得到的每一帧图片通过SIG_GetOneImage(image)信号发送出去

给pb_play按钮添加一个处理函数,在VideoPlayer类中实现,点击按钮开启线程,获取图片显示到控件

包含VideoShow类的头文件,添加一个对象,用于连接槽函数

在构造和析构中添加槽函数和回收资源的代码

可以看到接收信号处理的控件是ui->wdg_show,而处理函数slot_setImage并没有定义,这时就需要用到刚才我们新添加的类VideoItem

由于我们不是在最外层的widget上进行绘图,所以需要对控件进行重写paintEvent事件进行绘图(或者使用eventFilter,这里没有用这种方法)

因此我们添加类VideoItem,在其中添加槽函数slot_setImage进行处理,并重写paintEvent事件

槽函数中将image图片保存,并调用repaint重绘

void VideoItem::slot_setImage(const QImage  image)
{
    m_image = image;
    this->repaint();//立即刷新绘图
}
//绘图事件
void VideoItem::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);

    //先画黑色背景
    painter.setBrush( Qt::black );
    painter.drawRect( 0,0, this->width() , this->height() );
    //等比例缩放图片  等比例变成控件这么大
    if( m_image.size().width()<= 0 ) return;

    QPixmap img = QPixmap::fromImage(m_image.scaled(this->size(), Qt::KeepAspectRatio));

    //调整贴图位置,使其居中
    //x = (widget_show的宽 - 图片宽) / 2
    //y = (widget_show的高 - 图片高) / 2
    int x =  this->width() - img.width() ;
    int y =  this->height() - img.height();  
    x = x/2;
    y = y/2;
    painter.drawPixmap(x,y,img);

}

还差最后一步,就是将VideoItem类与控件wdg_show关联起来,在videoPlayer.ui中,将wdg_show提升为VideoItem(继承VideoItem)

这样就可以在wdg_show上进行绘图了

4.演示

点击Play按钮,开启线程在VideoShow类中进行解码,得到的图片通过信号SIG_GetOneImage发送出去,在VideoItem中进行重绘,显示在wdg_show控件上

猜你喜欢

转载自blog.csdn.net/qq_37348221/article/details/115407046