QT + FFMPEG实现简易播放器

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;

FFmpegvideoPlayDemo::FFmpegvideoPlayDemo(QWidget* parent): QWidget(parent)
{ui.setupUi(this);//设置标题this->setWindowTitle(QString("VideoPlay Version 1.00"));//实例化视频解码线程m_readThread = new readThread();将解码线程的自定义信号updateImage信号与PlayImage绑定,直接调用槽函数,槽函数不执行完,阻塞connect(m_readThread, &readThread::updateImage, ui.playimage, &PlayImage::updateImage, Qt::DirectConnection);将解码线程的自定义播放状态改变的信号与窗口线程的on_PlayState槽函数绑定connect(m_readThread, &readThread::playState, this, &FFmpegvideoPlayDemo::on_playState);
}

三、readThread

代码逻辑:调用open函数开启QThread的线程(QThread的线程启动机制:调用start()会新建一个新线程执行run()函数);在run()中调用videoDecode类的open函数进行解码;最后通过while循环来不断发送updateImage信号来让PlayImage类来更新图片

//关键代码:
void readThread::run()
{//首先调用open函数,开始视频解码bool ret = m_videoDecode->open(m_url);if (ret){//视频解码成功;设置播放标志位为真m_play = true;//以当前线程的时间为起点,计算时间m_etime2.start();//给窗口线程发送视频状态变为play的信号emit playState(play);}//异常处理逻辑//...while (m_play){while (m_pause){sleepMesc(200);}QImage image = m_videoDecode->read();if (!image.isNull()){sleepMesc(int(m_videoDecode->pts() - m_etime2.elapsed()));
//★关键代码:emit(updateImage(image));}else{if (m_videoDecode->isEnd()){qDebug() << "read Thread over";break;}sleepMesc(1);}}//全部搞完了qDebug() << "播放结束";//关掉视频解码m_videoDecode->close();//发送视频播放完的信号emit playState(end);//到这里,视频解码线程的主要逻辑已经实现完毕
}

四、PlayImage

通过信号槽机制,每当readThread类发送updateImage信号时,自动调用updatePixmap函数来绘画窗口的图形

//关键代码
void PlayImage::updateImage(const QImage& image)
{//由于QPixmap用于绘画事件更稳定更快速,这里不处理Image格式的图片//直接转换为QPixmap再调用updatePixmapupdatePixmap(QPixmap::fromImage(image));
}void PlayImage::updatePixmap(const QPixmap& pixmap)
{//因为这里在多线程访问的时候,可能会对m_pixmap造成问题,给这个变量的更新上锁m_mutex.lock();m_pixmap = pixmap;m_mutex.unlock();//调用重绘函数paintEvent函数update();
}
/// 重写绘图事件
void PlayImage::paintEvent(QPaintEvent* event)
{//有图就重绘if (!m_pixmap.isNull()){//实例化一个绘图对象QPainter painter(this);m_mutex.lock();//把图像按父窗口的大小,保持宽高比缩小,原始图片可能不适配播放器尺寸QPixmap pixmap = m_pixmap.scaled(this->size(), Qt::KeepAspectRatio);m_mutex.unlock();//居中绘画int x = (this->width() - pixmap.width()) / 2;int y = (this->height() - pixmap.height()) / 2;painter.drawPixmap(x, y, pixmap);}//调用QWidget的绘画函数,实现绘制功能QWidget::paintEvent(event);
}

五、★videoDecode

ffmpeg相关API函数和结构体实操解码,通过ffmpeg库的解码器实现对url的视频进行解码,返回QImage

最重要的两个函数:(已删除异常处理逻辑,专注于解码流程)有些函数看不懂也没事,可以通过GPT提问

  • bool videoDecode::open(const QString& url):打开解码器,剥去封装格式,解析视频
  • QImage videoDecode::read():处理解码后的数据,生成QImage
bool videoDecode::open(const QString& url)
{AVDictionary* dict = NULL;//av_dict_set()函数用于向字典中添加或修改键值对,这些参数在FFmpeg库的不同功能中起到配置作用av_dict_set(&dict, "rtsp_transport", "tcp", 0);av_dict_set(&dict, "max_delay", "3", 0);//设置最大延迟复用,禁止重新排序av_dict_set(&dict, "timeout", "1000000", 0);//设置套接字超时//打开输入流,并返回解封装上下文int ret = avformat_open_input(&m_formatContext,//保存解封装上下文url.toStdString().data(),//要打开的视频地址,要转换为char*类型NULL,//参数设置,自动选择解码器&dict);//参数字典里的参数传进来ret = avformat_find_stream_info(m_formatContext, NULL);m_totalTime = m_formatContext->duration / (AV_TIME_BASE / 1000);//信息流获取成功后,我们需要查找视频流ID//这里通过AVMediaType枚举查询视频流ID,当然也可以遍历查找m_videoIndex = av_find_best_stream(m_formatContext,AVMEDIA_TYPE_VIDEO,//媒体类型-1,//不指定流索引号,自动查找最佳的视频流-1,//不关联其他流,只考虑视频流本身NULL,//不需要返回找到的解码器0//不设置搜索标准位);//根据索引来获取视频流AVStream* videoStream = m_formatContext->streams[m_videoIndex];m_size.setWidth(videoStream->codecpar->width);m_size.setHeight(videoStream->codecpar->height);m_frameRate = rationalToDouble(&videoStream->avg_frame_rate);//获取解码器const AVCodec* codec = avcodec_find_decoder(videoStream->codecpar->codec_id);m_totalFrames = videoStream->nb_frames;m_codecContext = avcodec_alloc_context3(codec);ret = avcodec_parameters_to_context(m_codecContext, videoStream->codecpar);//允许使用不符合规范的加速技巧m_codecContext->flags2 |= AV_CODEC_FLAG2_FAST;//使用8线程解码m_codecContext->thread_count = 8;ret = avcodec_open2(m_codecContext, NULL, NULL);//给原始的数据包分配空间m_packet = av_packet_alloc();//给处理后的数据分配空间m_frame = av_frame_alloc();//分配图像空间。计算大小int size = av_image_get_buffer_size(AV_PIX_FMT_RGBA,//图像格式为RGBAm_size.width(),//图像宽度m_size.height(),//图像的高度4//每行像素的字节数);//多分配点图像空间m_buffer = new uchar[size + 1000];m_end = false;return true;//到此打开解码器,剥去封装格式,解析视频已经全部实现完,下面实现视频数据读取
}QImage videoDecode::read()
{//有东西,读取下一帧数据int readRet = av_read_frame(m_formatContext, m_packet);else{//如果是图像数据(视频流),就解码if (m_packet->stream_index == m_videoIndex){//这个虽然有误差,但是适用性更强//显示时间戳,帧在播出的时候该出现的时间,转为毫秒m_packet->pts = qRound64(m_packet->pts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));//解码时间戳,帧在解码的时间。m_packet->dts = qRound64(m_packet->dts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));//将读取到的原始数据帧传入解码器int ret = avcodec_send_packet(m_codecContext, m_packet);}}//要释放数据包av_packet_unref(m_packet);//处理解码后的数据//先接受int ret = avcodec_receive_frame(m_codecContext, m_frame);//失败m_pts = m_frame->pts;//处理图像转换上下文if (!m_swsContext){/** 获取缓存区的图像转换上下文* 首先校验参数是否一致* 校验不通过释放资源* 通过,判断上下文是否存在* 存在,直接复用* 不存在,分配新的,初始化*/m_swsContext = sws_getCachedContext(m_swsContext,m_frame->width,//输入图像的宽m_frame->height,//输入图像的高(AVPixelFormat)m_frame->format,//输入图像的像素格式m_size.width(),//输出图像的宽m_size.height(),//输出图像的高AV_PIX_FMT_RGBA,//输出图像的像素格式SWS_BILINEAR,//选择缩放算法NULL,//设置输入图像的滤波器信息NULL,//设置输出图像的滤波器信息NULL//设定缩放算法需要的参数);}//将解码后的图像格式转换为QImageuchar* data[] = { m_buffer };int lines[4];//使用像素格式pix_fmt和宽度填充图像的平面线条大小av_image_fill_linesizes(lines, AV_PIX_FMT_RGBA, m_frame->width);//将原图像的大小和颜色空间转换为输出的图像格式ret = sws_scale(m_swsContext,//缩放上下文m_frame->data,//原图像数据m_frame->linesize,//包含原图像每个平面步幅的数组0,//开始位置m_frame->height,//行数data,//目标图像数组lines);//包含目标图像每个平面的步幅的数组QImage image(m_buffer,//图像数据的指针m_frame->width,//image的宽度m_frame->height,//image的高度QImage::Format_RGBA8888);//图像的像素格式av_frame_unref(m_frame);return image;//到此QImage格式的图像已经处理完毕,视频解码的主要功能已经实现完毕,下面主要是对现有资源的释放关闭
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/498460.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springboot511基于SpringBoot视频点播系统的设计与实现(论文+源码)_kaic

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装视频点播系统软件来发挥其高效地信息处理的作用&#xff0c…

计算机网络——期末复习(4)协议或技术汇总、思维导图

思维导图 协议与技术 物理层通信协议&#xff1a;曼彻斯特编码链路层通信协议&#xff1a;CSMA/CD &#xff08;1&#xff09;停止-等待协议&#xff08;属于自动请求重传ARQ协议&#xff09;&#xff1a;确认、否认、重传、超时重传、 &#xff08;2&#xff09;回退N帧协…

计算机网络 (7)物理层下面的传输媒体

一、定义与位置 物理层是计算机网络体系结构的最低层&#xff0c;它位于传输媒体&#xff08;传输介质&#xff09;之上&#xff0c;主要作用是为数据链路层提供一个原始比特流的物理连接。这里的“比特流”是指数据以一个个0或1的二进制代码形式表示。物理层并不是特指某种传输…

Qt从入门到入土(七)-实现炫酷的登录注册界面(下)

前言 Qt从入门到入土&#xff08;六&#xff09;-实现炫酷的登录注册界面&#xff08;上&#xff09;主要讲了如何使用QSS样式表进行登录注册的界面设计&#xff0c;本篇文章将介绍如何对登录注册界面进行整体控件的布局&#xff0c;界面的切换以及实现登录、记住密码等功能。…

BOE(京东方)“向新2025”年终媒体智享会落地深圳

12月27日,BOE(京东方)“向新 2025”年终媒体智享会的收官之站在创新之都深圳圆满举行,为这场为期两周、横跨三地的年度科技盛会画上了完美句号。活动期间,全面回顾了 BOE(京东方)2024年在多个关键领域取得的卓越成绩,深入剖析其在六大维度构建的“向新”发展格局,精彩呈现了以“…

国产数据库TiDB从入门到放弃教程

国家层面战略&#xff0c;安全的角度&#xff0c;硬件、软件国产化是趋势&#xff0c;鸿蒙电脑操作系统、鸿蒙手机操作系统…数据库也会慢慢国产化&#xff0c;国产数据库TiDB用起来比OceanBase丝滑&#xff0c;本身没有那么重。 从入门到放弃 1. 介绍1.1 TiDB 的主要特点1.2 T…

初识 Conda:一站式包管理和环境管理工具

文章目录 1. 什么是 Conda&#xff1f;2. 为什么选择 Conda&#xff1f;3. Conda 的安装3.1 安装步骤&#xff08;以 Miniconda 为例&#xff09; 4. Conda 的核心功能4.1 包管理4.2 环境管理4.3 Conda Forge4.4 设置国内镜像 5. 常见使用场景5.1 数据科学项目5.2 离线安装5.3 …

chatwoot 开源客服系统搭建

1. 准备开源客服系统&#xff08;我是用的Chatwoot &#xff09; 可以选择以下开源客服系统作为基础&#xff1a; Chatwoot: 功能强大&#xff0c;支持多渠道客户对接&#xff0c;&#xff08;支持app&#xff0c;web&#xff09;。Zammad: 现代的开源工单系统。FreeScout: 免…

Linux-----进程处理(子进程创建)

【尚硅谷嵌入式Linux应用层开发&#xff0c;linux网络编程&#xff0c;linux进程线程&#xff0c;linux文件io】https://www.bilibili.com/video/BV1DJ4m1M77z?p35&vd_source342079de7c07f82982956aad8662b467 main函数 fork创建进程 fork()参数 /*** brief 创建一个子进…

【蓝桥杯选拔赛真题85】python摆放箱子 第十五届青少年组蓝桥杯python选拔赛真题 算法思维真题解析

目录 python摆放箱子 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python摆放箱子 第十五届蓝桥杯青少年组python比赛选拔赛真题详细解析 一…

【SQLi_Labs】Basic Challenges

什么是人生&#xff1f;人生就是永不休止的奋斗&#xff01; Less-1 尝试添加’注入&#xff0c;发现报错 这里我们就可以直接发现报错的地方&#xff0c;直接将后面注释&#xff0c;然后使用 1’ order by 3%23 //得到列数为3 //这里用-1是为了查询一个不存在的id,好让第一…

按照人们阅读Excel习惯来格式化BigDecimal

1、环境/问题描述 使用springboot发送邮件(附件)的方式将月度报表发送给领导查阅&#xff0c;数据是准确的&#xff0c;领导基本满意。 就是对一些数字的格式化提出了改进建议&#xff0c;比如不要让大数字自动转为科学计数法、浮点数小数点后都是0就不要带出来&#xff0c;根…

STM32 高级 WIFi案例1:测试AT指令

需求描述 测试AT指令是否能够正常控制ESP32的wifi&#xff0c;比如重启、读取设备信息等。 思路&#xff1a; stm32通过串口usart2向ESP32发布命令。ESP32通过串口1返回信息。 配置&#xff1a; 第一步&#xff1a;对ESP32芯片烧录可以读取stm32命令的固件&#xff08;fac…

Unity 读Excel,读取xlsx文件解决方案

Unity读取表格数据 效果&#xff1a; 思路&#xff1a; Unity可以解析Json&#xff0c;但是读取Excel需要插件的帮助&#xff0c;那就把这个功能分离开&#xff0c;读表插件就只管读表转Json&#xff0c;Unity就只管Json解析&#xff0c;中间需要一个存储空间&#xff0c;使用…

通过无障碍服务(AccessibilityService)实现Android设备全局水印显示

一、无障碍功能简介 首先我们先来了解下无障碍功能的官方介绍&#xff1a; 无障碍服务仅应用于帮助残障用户使用 Android 设备和应用。它们在后台运行&#xff0c;并在触发 AccessibilityEvents 时接收系统的回调。此类事件表示用户界面中的某些状态转换&#xff0c;例如焦点已…

查看vue的所有版本号和已安装的版本

1.使用npm查看Vue的所有版本&#xff1a; npm view vue versions2.查看项目中已安装的 Vue.js 版本 npm list vue

【鸿蒙NEXT】鸿蒙里面类似iOS的Keychain——关键资产(@ohos.security.asset)实现设备唯一标识

前言 在iOS开发中Keychain 是一个非常安全的存储系统&#xff0c;用于保存敏感信息&#xff0c;如密码、证书、密钥等。与 NSUserDefaults 或文件系统不同&#xff0c;Keychain 提供了更高的安全性&#xff0c;因为它对数据进行了加密&#xff0c;并且只有经过授权的应用程序才…

js ul li 事件委托

<ul><li>1111111111</li><li>2222222222</li><li>3333333333</li><li>4444444444</li> </ul>常规的 li 绑定点击事件 document.querySelectorAll(ul li).forEach((li) > {li.addEventListener(click, functio…

vue 嵌套el-dialo,当内层的弹窗弹出时,整个页面被遮罩

活不多说&#xff0c;直接上问题 当在页面上&#xff0c;点击出现第一个弹窗&#xff0c;然后在弹窗里面&#xff0c;点击在再出现一个弹窗时&#xff0c;就是如下效果。 查看Html,出现了遮罩层 Vue的建议是&#xff0c;不建议嵌套 Dialog&#xff0c;但实际上肯定存在嵌套 …

在基于Centos7的服务器上启用【Gateway】的【Clion Nova】(即 ReSharper C++ 引擎)

1. 检查启动报错日志&#xff0c;目录在 ~/.cache/JetBrains/CLion202x.x.x/log/backend.202x-xx-xx_xxxx.xxxx-err.log 2. 大致可能有两种报错 a. Process terminated. Couldnt find a valid ICU package installed on the system. 这个报错只需要装一下 libicu-devel 包即可…