Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计

与解码相关的主要代码在上一篇博客中已经做了介绍,本篇我们会先讨论一下如何控制解码速度再提供一个我个人的封装思路。最后回归到界面设计环节重点看一下如何保证播放器界面在缩放和拖动的过程中保证视频画面的宽高比例。

一、解码速度

播放器播放媒体文件的时候播放进度需要我们自己控制。基本的控制方法有两种:

  1. 根据FPS控制视频的播放帧率,让音频跟随。
  2. 控制音频的播放解码速度,让视频跟随。

媒体文件在编码的时候,正常情况下视频数据和音频输出是交替写入的。换句话说,解码每一帧视频数据伴随需要播放的音频数据也应该被解码。所以,方案一的实现就比较简单和直接。但是在有些情况下也可能会出现音视频编码不同步的问题,大部分情况是视频提前于音频。万一遇到这样的情况,如果需要让我们的播放器带有一定纠错功能就必须采用第二种方案。方案二的设计思路是当遇到音频数据时正常播放,遇到视频数据时先缓冲起来,再根据pts参数同步。

方案一

QTime t;
QIODevice ioDevice;
t.restart();
AVPacket *pkt = readPacket();
if (pkt->stream_index == videoIndex) { // 当前为视频帧,计算视频播放每帧的间隔时间(1000/fps) - 解码消耗的时间(毫秒) = 实际解码间隔时间interval
    codecPacket(pkt);int el = t.elapsed();int interval = 1000 / fps - el > 0 ? 1000 / fps - el : 1;QThread::msleep(interval);
}
else if (pkt->stream_index == audioIndex) { // 当前为音频帧,直接让Qt的音频播放器播放
    codecPacket(pkt);char data[10000] = { 0 };int len = toPCM(data);ioDevice->write(data, len);
}

方案二

AVPacket *pkt = readPacket();if (pkt->stream_index == audioIndex) {codecPacket(pkt);char data[AUDIO_IODEVICE_WRITE_SIZE] = { 0 };int len = toPCM(data);ioDevice->write(data, len);
}
else if (pkt->stream_index == videoIndex) {videoPacketList.push_back(pkt);
}while (videoPacketList.size() > 0 && videoPts < audioPts) {AVPacket *pkt = videoPacketList.front();videoPacketList.pop_front();codecPacket(pkt);
}

这个方案遇到的另外一个问题是我们如何获取videoPts和audioPts这两个值。我个人的解决思路是在解码环节进行,即,每次对pkt进行一次解码就根据pkt的stream_index值分别记录解码后的AVFrame的pts。不过音频的pts和视频的pts不能直接比较。我们还需要根据各自的AVRational做一次换算。算法如下:

AVRational r;
frame->pts * (double)r.num / (double)r.den;

二、封装思路讨论

代码封装实际是一个见仁见智的工作,可能不同的人对代码结构的理解不同,实现的封装方式也会存在差异。包括我们的解决方案到底针对哪些需求也会按照不同的思路做封装。在这里插一句题外话,大家认为程序开发到底是一种什么样的工作性质?是仅仅为了实现客户的需求吗?如果你只能理解到这一层,那恐怕还远远不够!客户需求只能算是抛给你的一个问题,而你反馈给客户的应该是一套合理的解决方案。从这个观点出发我们进行再抽象,程序开发应该是一种从问题空间到解空间的映射。既然如此,我们就不能将自己的工作仅仅停留在功能实现这个层面,我们还应该提供更好的解决思路——最佳实践。

基本上,如果我们只需要设计一个简单的播放器。大概需要三个模块的支持:

界面模块(av_player):包括了界面的样式和基础互动功能

解码模块(Decoder):这个部分主要通过对FFmpeg的功能二次封装,并对外提供接口支持

播放器模块(PlayerWidget):负责界面和解码模块的连接,界面中嵌入播放器模块,视频显示和音频播放都由播放器模块独立负责。

下面看一下我设计的解码模块对外提供的接口:Decoder.h

class Decoder : protected QThread
{
public:Decoder();virtual ~Decoder();bool open(const char *filename);void close();// 从文件中读取一个压缩报文AVPacket* readPacket();// 解码报文并释放空间,返回值为当前解码报文的pts时间(毫秒)int codecPacket(AVPacket* pkt);// 将解码帧Frame转码为RGB或PCMint toRGB(char *outData, int outWidth, int outHeight);int toPCM(char *outData);int durationMsec; // 文件时长int fps; // 视频FPSint srcWidth; // 视频宽度int srcHeight; // 视频高度int videoIndex; // 视频通道int audioIndex; // 音频通道int sampleRate; // 音频采样率int channels; // 声道int sampleSize; // 样本位数bool endFlag; // 线程结束标志bool pauseFlag; // 线程暂停标志// 记录当前的音视频所处在的pts时间戳(毫秒)int videoPts;int audioPts;// 记录音视频的编解码格式int sampleFmt;int pixFmt;/************************************************************************//* default: CD音质(16bit 44100Hz stereo)                              *//************************************************************************/int dstSampleRate = 44100; // 采样率int dstSampleSize = 16; // 采样大小int dstChannels = 2; // 通道数// 线程启动的代理方法void start();// 音频输出QAudioOutput *audioOutput = NULL;
protected:void run();
private:QMutex mtx;AVFormatContext *pFormatCtx = NULL;SwsContext *videoSwsCtx = NULL;AVFrame *yuv = NULL;SwrContext *audioSwrCtx = NULL;AVFrame *pcm = NULL;QIODevice *ioDevice = NULL;std::list<AVPacket*> videoPacketList;AVInputTypeEnum avType = AVInputTypeEnum::NOTYPE;QString fileName;
};

乍一看很复杂,我们稍微理一下思路。首先Decoder继承了QThread,并重写了start()方法。重写的好处是,在对调用者完全透明的情况下,我们可以在这个函数中做一些初始化工作。在设计模式中,它数据代理模式。其他方法介绍:

  • bool open(const char *filename):开发多媒体文件
  • void close():关闭和析构所有编码,这个步骤在音视频编解码的开发中非常重要
  • AVPacket* readPacket():读取一帧数据并返回
  • int codecPacket(AVPacket* pkt):解码之前读取到的一帧数据,返回该帧数据表示的pts值并将传入的pkt析构释放内存空间
  • int toRGB(char *outData, int outWidth, int outHeight):转码视频帧,将yuv转换为rgb
  • int toPCM(char *outData):转码音频帧

播放器模块:PlayerWidget.h

class PlayerWidget : public QOpenGLWidget
{
public:PlayerWidget(Decoder *dec, QWidget *parent, int interval);virtual ~PlayerWidget();/************************************************************************//* default: 720p 25fps                                                  *//************************************************************************/int videoWidth = 720;int videoHeight = 480;int m_interval = 40;/************************************************************************//* default: CD音质(16bit 44100Hz stereo)                              *//************************************************************************/int sampleRate = 44100; // 采样率int sampleSize = 16; // 采样大小int channels = 2; // 通道数
protected:void timerEvent(QTimerEvent *e);void paintEvent(QPaintEvent *e);
private:Decoder *decoder = NULL;QAudioOutput *out;QIODevice *io;
};

这个模块继承自QOpenGLWidget,并包含了QAudioOutput。这两个Qt类分别代表了视频播放和音频播放。

界面模块:在这个模块中有一个重要的工作就是当我们在播放视频的时候放大和缩小播放器窗口如何保证视频画面依然保持正确的宽高比,为此我写了一个静态函数:

struct AspectRatio {double width;double height;
};static AspectRatio* fitRatio(int outWidth, int outHeight, int inWidth, int inHeight) {double r1 = ((double)outWidth / (double)outHeight);double r2 = ((double)inWidth / (double)inHeight);AspectRatio *ar = new AspectRatio;if (r1 > r2) {int newWidth = (double)(outHeight * inWidth) / (double)inHeight;ar->width = newWidth;ar->height = outHeight;return ar;}else {int newHeight = (double)(inHeight * outWidth) / (double)inWidth;ar->width = outWidth;ar->height = newHeight;return ar;}
}

最后附上我自己设计的播放器界面

项目源码:https://gitee.com/learnhow/ffmpeg_studio/tree/master/_64bit/src/av_player

转载于:https://www.cnblogs.com/learnhow/p/8970893.html

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

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

相关文章

Bzoj1051 受欢迎的牛

每一头牛的愿望就是变成一头最受欢迎的牛。现在有 N 头牛&#xff0c;给你 M 对整数 (A,B)&#xff0c;表示牛 A 认为牛 B 受欢迎。这种关系是具有传递性的&#xff0c;如果 A 认为 B 受欢迎&#xff0c;B 认为 C 受欢迎&#xff0c;那么牛 A 也认为牛 C 受欢迎。你的任务是求出…

node --- 模块加载机制

1. Node.js中模块加载机制 1.1 模块查找规则-当模块拥有路径但没有后缀时 require(./find.js); require(./find);require方法根据模块路径查找模块,如果是完整路径,直接进入模块如果模块后缀省略,先找同名JS文件再找同名JS文件夹 require(./find); // 以上会先找到命令行目录…

51Nod 蜥蜴和地下室(搜索)

哈利喜欢玩角色扮演的电脑游戏《蜥蜴和地下室》。此时&#xff0c;他正在扮演一个魔术师。在最后一关&#xff0c;他必须和一排的弓箭手战斗。他唯一能消灭他们的办法是一个火球咒语。如果哈利用他的火球咒语攻击第i个弓箭手&#xff08;他们从左到右标记&#xff09;&#xff…

多线程——实现Runnable接口实现一个多线程

实现Runnable接口实现一个多线程 Runnable接口源码&#xff1a; package java.lang; //Runnable接口源码只有一个run方法 public interface Runnable {public abstract void run(); } 实现Runnable的两个多线程类&#xff1a; public class RunnableThread1 implements Runnabl…

javascript --- 文件上传即时预览 闭包实现多图片即时预览

使用javascript原生功能实现,点击上传文件,然后再网页上显示出来 1. 初级显示 1.1 准备一个input标签和一个img标签 <input typefile id"file"> <img id"preview" src"">1.2 js代码如下 // 将上传的图片显示到页面上function sho…

第一次作业:深入Linux源码分析进程模型

一.进程的概念 第一&#xff0c;进程是一个实体。每一个进程都有它自己的地址空间&#xff0c;一般情况下&#xff0c;包括文本区域&#xff08;text region&#xff09;、数据区域&#xff08;data region&#xff09;和堆栈&#xff08;stack region&#xff09;。文本区域存…

关于模型验证那点事儿

今天应笑笑老师之问&#xff0c;做了一个模型验证的例子&#xff0c;发现之前对这个东西的理解太片面&#xff0c;重新整理了一下思路 字段验证优先级高于类验证 什么是类验证呢&#xff1f;就是两个字段组合的验证&#xff0c;比如你Admin不允许修改密码&#xff0c;你修改密码…

mongoose --- createUser

说明 源代码记录、遗忘回顾mongoDB默认不需要使用账号密码即可访问数据库.下面是给mongoDB添加超级管理员和普通用户的方法 以系统管理员的方式运行powershell连接数据库 mongo查看数据库: show dbs切换到admin数据库: use admin创建超级管理员账户: db.createUser({user: roo…

Win10安装MySQL5.7.22 解压缩版(手动配置)方法

1.下载地址&#xff1a;https://dev.mysql.com/downloads/mysql/5.7.html#downloads 直接点击下载项 下载后&#xff1a; 2.可以把解压的内容随便放到一个目录&#xff0c;我的是如下目录&#xff08;放到C盘的话&#xff0c;可能在修改ini文件时涉及权限问题&#xff0c;之后我…

Elemant-UI日期范围的表单验证

Form 组件提供了表单验证的功能&#xff0c;只需要通过 rules 属性传入约定的验证规则&#xff0c;并将 Form-Item 的 prop 属性设置为需校验的字段名即可。但是官网的示例只有普通日期类型的验证&#xff0c;没有时间范围的验证。 一开始&#xff0c;我认为时间时间范围的是一…

node --- [express项目] 开发环境下使用morgan控制台输出访问信息

说明 源代码记录、遗忘回顾 process.env node中提供了一个process.env接口用于访问计算机中的系统环境变量. 可以利用以上属性来区分当前的环境是开发环境还是生产环境,代码如下: if (process.env.NODE_ENV development) {console.log(当前环境是开发环境) } else {consol…

Dynamics CRM 访问团队的使用

访问团队和负责人团队的区别是&#xff1a;负责人团队可以拥有记录&#xff0c;访问团队不能拥有记录也不能加入解决方案中。 访问团队用法1&#xff1a;可以将不同组织的人员加入到访问组实现数据的更新、删除、共享 访问团队用法2&#xff1a;访问团队模板的使用 步骤一&…

业务逻辑

快捷支付接口规范 问题背景 持卡人身份验证持卡人在发卡银行提供的身份验证服务器进行验证&#xff0c;将结果告知商户资金清算资金清算在身份验证通过后进行即时清算&#xff0c;也可能是通过专用资金清算网络进行传统方法弊端 持卡人需要访问很多网站才能完成一次完整支付 &a…

node --- [express] cookie/session 机制与 中间件的使用(路由守卫)

说明 源代码记忆、遗忘回顾使用 cookie/session 机制,让 客户端/服务器 的访问变得有状态 cookie 与 session 由于 HTTP 协议的无状态性,当一次连接断开后. 服务器并不会记录用户是否登录. 因此需要引入 cookie/session 机制 cookie cookie: 浏览器在电脑硬盘中开辟的一块空…

kprobe原理解析

参考 http://www.cnblogs.com/honpey/p/4575928.html kprobe是linux内核的一个重要特性&#xff0c;是一个轻量级的内核调试工具&#xff0c;同时它又是其他一些更高级的内核调试工具&#xff08;比如perf和systemtap&#xff09;的“基础设施”&#xff0c;4.0版本的内核中&a…

02 数据类型

转载于:https://www.cnblogs.com/theoup/p/9875293.html

css --- [学习笔记]背景图片小结 css三大特性

源代码 参考 1. 行高(line-height) 目标 理解 - 能说出行高和高度三种关系 - 能简单理解为什么行高等于单行文字会垂直居应用 使用行高实现单行文字垂直居中能会测量行高 2. CSS 背景(background) 目标 理解 - 背景的作用css 背景图片和插入图片的区别 应用 通过 css 背景…

(数据科学学习手札30)朴素贝叶斯分类器的原理详解Python与R实现

一、简介 要介绍朴素贝叶斯&#xff08;naive bayes&#xff09;分类器&#xff0c;就不得不先介绍贝叶斯决策论的相关理论&#xff1a; 贝叶斯决策论&#xff08;bayesian decision theory&#xff09;是概率框架下实施决策的基本方法。对分类任务来说&#xff0c;在所有相关概…

【技术累积】【点】【java】【29】MapUtils

内容 是Apache组织下的commons-collections包中的工具类<dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.1</version></dependency> Map操作相关的&#xff0c…

css --- [读书笔记] 盒模型(边框、内外边距)

说明 源代码学习 盒子模型(css重点) css学习三大重点: css盒子模型、 浮动、 定位 目标: 能说出盒子模型由哪四部分组成: 内容、边框、内外边距能说出内边距的作用,设置不同数值分别代表的意思: 控制内部块级元素和宽框的距离能说出块级盒子居中对齐需要的2个条件能说出外边…