QT + FFMPEG实现浅显播放器

打印 上一主题 下一主题

主题 1045|帖子 1045|积分 3135

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
QT + FFMPEG实现浅显播放器

   项目环境:vs2022 + QT5.14 + ffmpeg5.0(第三方库文件已上传到源码中)
  项目技能:要求有QT基础(信号槽、变乱机制)、音视频解码操作中对FFMPEG相关API库的调用
  项目阐明:接纳ffmpeg库对视频流进行解封装后转成QImage格式,再通过paintEvent变乱将其绘画到窗口。
  源码:https://github.com/say-Hai/FFmpeg-videoPlayDemo
  项目详细运行流程请检察github源码的md文档(先把代码跑起来再说)
  一、代码文件阐明



  • FFmpegvideoPlayDemo.cpp:程序的窗口类(正常名称为mainwindow,只是vs中会自动根据项目改名)
  • main.cpp:QT运行程序的入口(啥也没改)
  • PlayImage.cpp:程序视频播放的窗口,继承QWidget,实现对QImage(解码器返回的参数)的更新和重写paintEvent绘图变乱
  • readThread.cpp:继承QThread的线程类,负责开启/暂停视频的解码操作
  • ★videoDecode.cpp:本程序重点,视频的解码类,通过调用FFmpeg的相关库来解码视频得到AVFrame原始格式,并最后通过sws_scale()转换为RGBA格式,再赋值给QImage类后返回。
二、FFmpegvideoPlayDemo

   此函数逻辑很简朴,很容易看懂,就是通过QT的信号槽机制,调用对应的操作逻辑(如:调用线程执行open函数)
  重要成员变量:readThread* m_readThread = NULL;
  1. FFmpegvideoPlayDemo::FFmpegvideoPlayDemo(QWidget* parent)
  2.         : QWidget(parent)
  3. {
  4.         ui.setupUi(this);
  5.         //设置标题
  6.         this->setWindowTitle(QString("VideoPlay Version 1.00"));
  7.         //实例化视频解码线程
  8.         m_readThread = new readThread();
  9.         将解码线程的自定义信号updateImage信号与PlayImage绑定,直接调用槽函数,槽函数不执行完,阻塞
  10.         connect(m_readThread, &readThread::updateImage, ui.playimage, &PlayImage::updateImage, Qt::DirectConnection);
  11.         将解码线程的自定义播放状态改变的信号与窗口线程的on_PlayState槽函数绑定
  12.         connect(m_readThread, &readThread::playState, this, &FFmpegvideoPlayDemo::on_playState);
  13. }
复制代码
三、readThread

   代码逻辑:调用open函数开启QThread的线程(QThread的线程启动机制:调用start()会新建一个新线程执行run()函数);在run()中调用videoDecode类的open函数进行解码;最后通过while循环来不断发送updateImage信号来让PlayImage类来更新图片
  1. //关键代码:
  2. void readThread::run()
  3. {
  4.         //首先调用open函数,开始视频解码
  5.         bool ret = m_videoDecode->open(m_url);
  6.         if (ret)
  7.         {
  8.                 //视频解码成功;设置播放标志位为真
  9.                 m_play = true;
  10.                 //以当前线程的时间为起点,计算时间
  11.                 m_etime2.start();
  12.                 //给窗口线程发送视频状态变为play的信号
  13.                 emit playState(play);
  14.         }
  15.     //异常处理逻辑
  16.     //...
  17.    
  18.         while (m_play)
  19.         {
  20.                 while (m_pause)
  21.                 {
  22.                         sleepMesc(200);
  23.                 }
  24.                 QImage image = m_videoDecode->read();
  25.                 if (!image.isNull())
  26.                 {
  27.                         sleepMesc(int(m_videoDecode->pts() - m_etime2.elapsed()));
  28. //★关键代码:
  29.                         emit(updateImage(image));
  30.                 }
  31.                 else
  32.                 {
  33.                         if (m_videoDecode->isEnd())
  34.                         {
  35.                                 qDebug() << "read Thread over";
  36.                                 break;
  37.                         }
  38.                         sleepMesc(1);
  39.                 }
  40.         }
  41.         //全部搞完了
  42.         qDebug() << "播放结束";
  43.         //关掉视频解码
  44.         m_videoDecode->close();
  45.         //发送视频播放完的信号
  46.         emit playState(end);
  47.         //到这里,视频解码线程的主要逻辑已经实现完毕
  48. }
复制代码
四、PlayImage

   通过信号槽机制,每当readThread类发送updateImage信号时,自动调用updatePixmap函数来绘画窗口的图形
  1. //关键代码
  2. void PlayImage::updateImage(const QImage& image)
  3. {
  4.         //由于QPixmap用于绘画事件更稳定更快速,这里不处理Image格式的图片
  5.         //直接转换为QPixmap再调用updatePixmap
  6.         updatePixmap(QPixmap::fromImage(image));
  7. }
  8. void PlayImage::updatePixmap(const QPixmap& pixmap)
  9. {
  10.         //因为这里在多线程访问的时候,可能会对m_pixmap造成问题,给这个变量的更新上锁
  11.         m_mutex.lock();
  12.         m_pixmap = pixmap;
  13.         m_mutex.unlock();
  14.         //调用重绘函数paintEvent函数
  15.         update();
  16. }
  17. /// 重写绘图事件
  18. void PlayImage::paintEvent(QPaintEvent* event)
  19. {
  20.         //有图就重绘
  21.         if (!m_pixmap.isNull())
  22.         {
  23.                 //实例化一个绘图对象
  24.                 QPainter painter(this);
  25.                 m_mutex.lock();
  26.                 //把图像按父窗口的大小,保持宽高比缩小,原始图片可能不适配播放器尺寸
  27.                 QPixmap pixmap = m_pixmap.scaled(this->size(), Qt::KeepAspectRatio);
  28.                 m_mutex.unlock();
  29.                 //居中绘画
  30.                 int x = (this->width() - pixmap.width()) / 2;
  31.                 int y = (this->height() - pixmap.height()) / 2;
  32.                 painter.drawPixmap(x, y, pixmap);
  33.         }
  34.         //调用QWidget的绘画函数,实现绘制功能
  35.         QWidget::paintEvent(event);
  36. }
复制代码
五、★videoDecode

   ffmpeg相关API函数和结构体实操解码,通过ffmpeg库的解码器实现对url的视频进行解码,返回QImage
  最重要的两个函数:(已删除异常处理逻辑,专注于解码流程)有些函数看不懂也没事,可以通过GPT提问
  

  • bool videoDecode:pen(const QString& url):打开解码器,剥去封装格式,解析视频
  • QImage videoDecode::read():处明白码后的数据,天生QImage
  1. bool videoDecode::open(const QString& url)
  2. {
  3.         AVDictionary* dict = NULL;
  4.         //av_dict_set()函数用于向字典中添加或修改键值对,这些参数在FFmpeg库的不同功能中起到配置作用
  5.         av_dict_set(&dict, "rtsp_transport", "tcp", 0);
  6.         av_dict_set(&dict, "max_delay", "3", 0);//设置最大延迟复用,禁止重新排序
  7.         av_dict_set(&dict, "timeout", "1000000", 0);//设置套接字超时
  8.         //打开输入流,并返回解封装上下文
  9.         int ret = avformat_open_input(&m_formatContext,//保存解封装上下文
  10.                 url.toStdString().data(),//要打开的视频地址,要转换为char*类型
  11.                 NULL,//参数设置,自动选择解码器
  12.                 &dict);//参数字典里的参数传进来
  13.    
  14.         ret = avformat_find_stream_info(m_formatContext, NULL);
  15.         m_totalTime = m_formatContext->duration / (AV_TIME_BASE / 1000);
  16.         //信息流获取成功后,我们需要查找视频流ID
  17.         //这里通过AVMediaType枚举查询视频流ID,当然也可以遍历查找
  18.         m_videoIndex = av_find_best_stream(m_formatContext,
  19.                 AVMEDIA_TYPE_VIDEO,//媒体类型
  20.                 -1,//不指定流索引号,自动查找最佳的视频流
  21.                 -1,//不关联其他流,只考虑视频流本身
  22.                 NULL,//不需要返回找到的解码器
  23.                 0//不设置搜索标准位
  24.         );
  25.        
  26.         //根据索引来获取视频流
  27.         AVStream* videoStream = m_formatContext->streams[m_videoIndex];
  28.    
  29.         m_size.setWidth(videoStream->codecpar->width);
  30.         m_size.setHeight(videoStream->codecpar->height);
  31.    
  32.         m_frameRate = rationalToDouble(&videoStream->avg_frame_rate);
  33.         //获取解码器
  34.         const AVCodec* codec = avcodec_find_decoder(videoStream->codecpar->codec_id);
  35.         m_totalFrames = videoStream->nb_frames;
  36.         m_codecContext = avcodec_alloc_context3(codec);
  37.    
  38.         ret = avcodec_parameters_to_context(m_codecContext, videoStream->codecpar);
  39.    
  40.         //允许使用不符合规范的加速技巧
  41.         m_codecContext->flags2 |= AV_CODEC_FLAG2_FAST;
  42.         //使用8线程解码
  43.         m_codecContext->thread_count = 8;
  44.         ret = avcodec_open2(m_codecContext, NULL, NULL);
  45.         //给原始的数据包分配空间
  46.         m_packet = av_packet_alloc();
  47.         //给处理后的数据分配空间
  48.         m_frame = av_frame_alloc();
  49.         //分配图像空间。计算大小
  50.         int size = av_image_get_buffer_size(AV_PIX_FMT_RGBA,//图像格式为RGBA
  51.                 m_size.width(),//图像宽度
  52.                 m_size.height(),//图像的高度
  53.                 4//每行像素的字节数
  54.         );
  55.         //多分配点图像空间
  56.         m_buffer = new uchar[size + 1000];
  57.         m_end = false;
  58.         return true;
  59.         //到此打开解码器,剥去封装格式,解析视频已经全部实现完,下面实现视频数据读取
  60. }
  61. QImage videoDecode::read()
  62. {
  63.         //有东西,读取下一帧数据
  64.         int readRet = av_read_frame(m_formatContext, m_packet);
  65.         else
  66.         {
  67.                 //如果是图像数据(视频流),就解码
  68.                 if (m_packet->stream_index == m_videoIndex)
  69.                 {
  70.                         //这个虽然有误差,但是适用性更强
  71.                    //显示时间戳,帧在播出的时候该出现的时间,转为毫秒
  72.                         m_packet->pts = qRound64(m_packet->pts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
  73.                         //解码时间戳,帧在解码的时间。
  74.                         m_packet->dts = qRound64(m_packet->dts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
  75.                         //将读取到的原始数据帧传入解码器
  76.                         int ret = avcodec_send_packet(m_codecContext, m_packet);
  77.                 }
  78.         }
  79.         //要释放数据包
  80.         av_packet_unref(m_packet);
  81.         //处理解码后的数据
  82.         //先接受
  83.         int ret = avcodec_receive_frame(m_codecContext, m_frame);
  84.         //失败
  85.        
  86.         m_pts = m_frame->pts;
  87.         //处理图像转换上下文
  88.         if (!m_swsContext)
  89.         {
  90.                 /*
  91.                  * 获取缓存区的图像转换上下文
  92.                  * 首先校验参数是否一致
  93.                  * 校验不通过释放资源
  94.                  * 通过,判断上下文是否存在
  95.                  * 存在,直接复用
  96.                  * 不存在,分配新的,初始化
  97.                 */
  98.                 m_swsContext = sws_getCachedContext(m_swsContext,
  99.                         m_frame->width,//输入图像的宽
  100.                         m_frame->height,//输入图像的高
  101.                         (AVPixelFormat)m_frame->format,//输入图像的像素格式
  102.                         m_size.width(),//输出图像的宽
  103.                         m_size.height(),//输出图像的高
  104.                         AV_PIX_FMT_RGBA,//输出图像的像素格式
  105.                         SWS_BILINEAR,//选择缩放算法
  106.                         NULL,//设置输入图像的滤波器信息
  107.                         NULL,//设置输出图像的滤波器信息
  108.                         NULL//设定缩放算法需要的参数
  109.                 );
  110.         }
  111.         //将解码后的图像格式转换为QImage
  112.         uchar* data[] = { m_buffer };
  113.         int lines[4];
  114.         //使用像素格式pix_fmt和宽度填充图像的平面线条大小
  115.         av_image_fill_linesizes(lines, AV_PIX_FMT_RGBA, m_frame->width);
  116.         //将原图像的大小和颜色空间转换为输出的图像格式
  117.         ret = sws_scale(m_swsContext,//缩放上下文
  118.                 m_frame->data,//原图像数据
  119.                 m_frame->linesize,//包含原图像每个平面步幅的数组
  120.                 0,//开始位置
  121.                 m_frame->height,//行数
  122.                 data,//目标图像数组
  123.                 lines);//包含目标图像每个平面的步幅的数组
  124.         QImage image(m_buffer,//图像数据的指针
  125.                 m_frame->width,//image的宽度
  126.                 m_frame->height,//image的高度
  127.                 QImage::Format_RGBA8888);//图像的像素格式
  128.         av_frame_unref(m_frame);
  129.         return image;
  130.         //到此QImage格式的图像已经处理完毕,视频解码的主要功能已经实现完毕,下面主要是对现有资源的释放关闭
  131. }
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

汕尾海湾

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表