QT + FFMPEG实现简易播放器

news2025/1/3 14:27:12

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再调用updatePixmap
	updatePixmap(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,//图像格式为RGBA
		m_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//设定缩放算法需要的参数
		);
	}
	//将解码后的图像格式转换为QImage
	uchar* 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.coloradmin.cn/o/2268581.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux day 1129

家人们今天继续学习Linux&#xff0c;ok话不多说一起去看看吧 三.Linux常用命令 3.1 Linux命令体验 3.1.1 常用命令演示 在这一部分中&#xff0c;我们主要介绍几个常用的命令&#xff0c;让大家快速感 受以下 Linux 指令的操作方式。主要包含以下几个指令&#xff1a; ls命…

【基础篇】二、MySQL数据库的操作

文章目录 前言Ⅰ. 创建数据库1、语法2、举例 Ⅱ. 字符集和校验规则1、查看系统默认字符集以及校验规则2、查看数据库支持的字符集3、查看数据库支持的字符集校验规则4、校验规则对数据库的影响 Ⅲ. 操作数据库1、查看数据库2、显示创建语句3、使用数据库4、删除数据库5、修改数…

NLP中的神经网络基础

一&#xff1a;多层感知器模型 1&#xff1a;感知器 解释一下&#xff0c;为什么写成 wxb>0 &#xff0c;其实原本是 wx > t ,t就是阈值&#xff0c;超过这个阈值fx就为1&#xff0c;现在把t放在左边。 在感知器里面涉及到两个问题&#xff1a; 第一个&#xff0c;特征提…

2025常见的软件测试面试题

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 “ 今天我给大家介绍一些python自动化测试中常见的面试题&#xff0c;涵盖了Python基础、测试框架、测试工具、测试方法等方面的内容&#xff0c;希望能够帮助…

日常编码练习

通过投票对团队排名 题目要求&#xff1a; 解题思路&#xff1a; 思路&#xff1a;以示例1为例 1、我们需要去遍历该数据结构&#xff0c;同时记录每个字符出现的次数。为此需要定义一张哈希表来建立映射关系。 注&#xff1a;哈希表仅仅起到了两个作用 ①&#xff1a;建立映射…

Java - 日志体系_Simple Logging Facade for Java (SLF4J)日志门面_SLF4J实现原理分析

文章目录 官网SLF4J 简单使用案例分析SLF4J 获取 Logger 的原理获取 ILoggerFactory 的过程获取 Logger 的过程SLF4J 与底层日志框架的集成 小结 官网 https://slf4j.org/ Simple Logging Facade for Java &#xff08;SLF4J&#xff09; 用作各种日志记录框架&#xff08;e.g…

HALCON中用于分类的高斯混合模型create_class_gmm

目录 一、创建用于分类的高斯混合模型函数二、代码和效果展示三、相关函数 一、创建用于分类的高斯混合模型函数 create_class_gmm( : : NumDim, NumClasses, NumCenters, CovarType, Preprocessing, NumComponents, RandSeed : GMMHandle)create_class_gmm创建用于分类的高斯…

在实际开发中,如何权衡选择使用哪种数据结构和算法?

学习数据结构与算法有一段时间了&#xff0c;听音频、看视频、看专栏、看书、抄书&#xff0c;尝试了很多种方法&#xff0c;今天在 专栏 中看到一篇文章&#xff0c;觉得很不错&#xff0c;摘抄如下。 学习数据结构和算法&#xff0c;不要停留在学院派的思维中&#xff0c;只把…

Airbnb/Booking 系统设计(high level architecture)

原文地址 CodeKarle: Airbnb System Design | Booking.com System Design B站搜 “Airbnb System Design” 有视频版本 需求&#xff1a; 功能性需求 系统用户包括商家和客人。 Hotel - 商家&#xff08;拥有hotel的人&#xff09; onboarding - 商家可以入住系统。 update…

如何在谷歌浏览器中清理无效的扩展

谷歌浏览器是一款功能强大且广受欢迎的网络浏览器&#xff0c;它允许用户安装各种扩展来增强功能。然而&#xff0c;随着时间的推移&#xff0c;一些扩展可能变得无效或不再需要&#xff0c;这时就需要清理这些无效的扩展以减少浏览器的负担并提升性能。 一、步骤详解 1. 打开…

MySQLOCP考试过了,题库很稳,经验分享。

前几天&#xff0c;本人参加了Oracle认证 MySQLOCP工程师认证考试 &#xff0c;先说下考这个证书的初衷&#xff1a; 1、首先本人是从事数据库运维的&#xff0c;今年开始单位逐步要求DBA持证上岗。 2、本人的工作是涉及数据库维护&#xff0c;对这块的内容比较熟悉&#xff…

艾体宝产品丨加速开发:Redis 首款 VS Code 扩展上线!

Redis 宣布推出其首款专为 VS Code 设计的 Redis 扩展。这一扩展将 Redis 功能直接整合进您的集成开发环境&#xff08;IDE&#xff09;&#xff0c;旨在简化您的工作流程&#xff0c;提升工作效率。 我们一直致力于构建强大的开发者生态系统&#xff0c;并在您工作的每一步提…

银河麒麟系统安装Wireshark抓包工具

麒麟系统安装Wireshark抓包工具 1. 麒麟SP1安装Wireshark 【1】. 安装Wireshark 在线安装wireshark 命令如下&#xff1a; apt-get install wireshark-qt软件包设置 弹出对话框&#xff0c;选择【是】选项。 安装完成。 【2】. 使用Wireshark 如果是普通用户&#xff0c;…

WPF 绘制过顶点的圆滑曲线 (样条,贝塞尔)

在一个WPF项目中要用到样条曲线&#xff0c;必须过顶点&#xff0c;圆滑后还不能太走样&#xff0c;捣鼓一番&#xff0c;发现里面颇有玄机&#xff0c;于是把我多方抄来改造的方法发出来&#xff0c;方便新手&#xff1a; 如上图&#xff0c;看代码吧&#xff1a; ----------…

Redis--持久化策略(AOF与RDB)

持久化策略&#xff08;AOF与RDB&#xff09; 持久化Redis如何实现数据不丢失&#xff1f;RDB 快照是如何实现的呢&#xff1f;执行时机RDB原理执行快照时&#xff0c;数据能被修改吗&#xff1f; AOF持久化是怎么实现的&#xff1f;AOF原理三种写回策略AOF重写机制 RDB和AOF合…

学习路之VScode--自定义按键写注释(插件)

1. 安装 "KoroFileHeader" 插件 首先&#xff0c;在 VScode 中搜索并安装名为 "KoroFileHeader" 的插件。你可以通过在扩展商店中搜索插件名称来找到并安装它。 2. 进入 VScode 设置页面 点击 VScode 左下角的设置图标&#xff0c;然后选择 "设置&q…

开源电子书转有声书整合包ebook2audiobookV2.0.0

ebook2audiobook&#xff1a;将电子书转换为有声书的开源项目 项目地址 GitHub - DrewThomasson/ebook2audiobook 整合包下载 更新至v2.0.0 https://pan.quark.cn/s/22956c5559d6 修改:页面已转为中文 项目简介 ebook2audiobook 是一个开源项目&#xff0c;它能够将电子…

3.5mm耳机接口硬件连接

结构 以最复杂的结构为例 简单的结构无非就是没有MIC&#xff08;麦克风&#xff09;接口 上图的5就是Detect的作用 上面这两款产品都为3.5mm的音频插座&#xff0c;图一 为连接4节的音频座&#xff0c;而且有两个开关&#xff0c;1接地&#xff0c;2接MIC&#xff0c;3接左声…

【贪心算法】贪心算法七

贪心算法七 1.整数替换2.俄罗斯套娃信封问题3.可被三整除的最大和4.距离相等的条形码5.重构字符串 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1f…

一文大白话讲清楚CSS元素的水平居中和垂直居中

文章目录 一文大白话讲清楚CSS元素的水平居中和垂直居中1.已知元素宽高的居中方案1.1 利用定位margin:auto1.2 利用定位margin负值1.3 table布局 2.未知元素宽高的居中方案2.1利用定位transform2.2 flex弹性布局2.3 grid网格布局 3. 内联元素的居中布局 一文大白话讲清楚CSS元素…