Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计
与解码相关的主要代码在上一篇博客中已经做了介绍,本篇我们会先讨论一下如何控制解码速度再提供一个我个人的封装思路。最后回归到界面设计环节重点看一下如何保证播放器界面在缩放和拖动的过程中保证视频画面的宽高比例。
一、解码速度
播放器播放媒体文件的时候播放进度需要我们自己控制。基本的控制方法有两种:
- 根据FPS控制视频的播放帧率,让音频跟随。
- 控制音频的播放解码速度,让视频跟随。
媒体文件在编码的时候,正常情况下视频数据和音频输出是交替写入的。换句话说,解码每一帧视频数据伴随需要播放的音频数据也应该被解码。所以,方案一的实现就比较简单和直接。但是在有些情况下也可能会出现音视频编码不同步的问题,大部分情况是视频提前于音频。万一遇到这样的情况,如果需要让我们的播放器带有一定纠错功能就必须采用第二种方案。方案二的设计思路是当遇到音频数据时正常播放,遇到视频数据时先缓冲起来,再根据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 = / fps - el > ? / fps - el : ;
QThread::msleep(interval);
}
else if (pkt->stream_index == audioIndex) { // 当前为音频帧,直接让Qt的音频播放器播放
codecPacket(pkt);
char data[] = { };
int len = toPCM(data);
ioDevice->write(data, len);
}
方案二
AVPacket *pkt = readPacket(); if (pkt->stream_index == audioIndex) {
codecPacket(pkt);
char data[AUDIO_IODEVICE_WRITE_SIZE] = { };
int len = toPCM(data);
ioDevice->write(data, len);
}
else if (pkt->stream_index == videoIndex) {
videoPacketList.push_back(pkt);
} while (videoPacketList.size() > && 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或PCM
int toRGB(char *outData, int outWidth, int outHeight);
int toPCM(char *outData); int durationMsec; // 文件时长
int fps; // 视频FPS
int 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 = ; // 采样率
int dstSampleSize = ; // 采样大小
int dstChannels = ; // 通道数
// 线程启动的代理方法
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 = ;
int videoHeight = ;
int m_interval = ;
/************************************************************************/
/* default: CD音质(16bit 44100Hz stereo) */
/************************************************************************/
int sampleRate = ; // 采样率
int sampleSize = ; // 采样大小
int channels = ; // 通道数
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
Qt与FFmpeg联合开发指南(二)——解码(2):封装和界面设计的更多相关文章
- Qt与FFmpeg联合开发指南(一)——解码(1):功能实现
前言:对于从未接触过音视频编解码的同学来说,使用FFmpeg的学习曲线恐怕略显陡峭.本人由于工作需要,正好需要在项目中使用.因此特地将开发过程总结下来.只当提供给有兴趣的同学参考和学习. 由于FFmp ...
- Qt与FFmpeg联合开发指南(四)——编码(2):完善功能和基础封装
上一章我用一个demo函数演示了基于Qt的音视频采集到编码的完整流程,最后经过测试我们也发现了代码中存在的问题.本章我们就先处理几个遗留问题,再对代码进行完善,最后把编码功能做基础封装. 一.遗留问题 ...
- Qt与FFmpeg联合开发指南(三)——编码(1):代码流程演示
前两讲演示了基本的解码流程和简单功能封装,今天我们开始学习编码.编码就是封装音视频流的过程,在整个编码教程中,我会首先在一个函数中演示完成的编码流程,再解释其中存在的问题.下一讲我们会将编码功能进行封 ...
- 循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码
VUE+Element 前端应用,比较不错的一点就是界面组件化,我们可以根据重用的指导方针,把界面内容拆分为各个不同的组合,每一个模块可以是一个组件,也可以是多个组件的综合体,而且这一个过程非常方便. ...
- 三十二、Java图形化界面设计——布局管理器之CardLayout(卡片布局)
摘自 http://blog.csdn.net/liujun13579/article/details/7773945 三十二.Java图形化界面设计--布局管理器之CardLayout(卡片布局) ...
- Spark Streaming和Kafka整合开发指南(二)
在本博客的<Spark Streaming和Kafka整合开发指南(一)>文章中介绍了如何使用基于Receiver的方法使用Spark Streaming从Kafka中接收数据.本文将介绍 ...
- Ext JS 6开发实例(三) :主界面设计
在上文中,已经将CMD创建的应用程序导入到项目里了,而且也看到默认的主界面了,今天的主要工作就是修改这个主界面,以符合项目的需要.除了设计主界面,还有一些其他的东西需要配置一下. 添加本地化包 打开a ...
- 【转】Polymer API开发指南 (二)(翻译)
原文转自:http://segmentfault.com/blog/windwhinny/1190000000596258 公开 property 当你公开一个 Polymer 元素的 propert ...
- Qt版本中国象棋开发(二)
实现功能:棋盘绘制 核心函数: void paintEvent(QPaintEvent *); //QWidget自带的虚函数,重写后使用 QPainter 类来绘制图形 QPainter paint ...
随机推荐
- Spark:将RDD[List[String,List[Person]]]中的List[Person]通过spark api保存为hdfs文件时一直出现not serializable task,没办法找到"spark自定义Kryo序列化输入输出API"
声明:本文转自<在Spark中自定义Kryo序列化输入输出API> 在Spark中内置支持两种系列化格式:(1).Java serialization:(2).Kryo seriali ...
- Struts(十九):类型转换、类型转换错误消息及显示
类型转换概念 1.从html表单页面到一个Action对象,类型转化是从字符串到一个非字符串:html并没有“类型”的概念,每个表单输入的信息都只可能是一个字符串或者一个字符串数组,但是在服务器端,必 ...
- Centos:如何查找安装的jdk的目录
使用$JAVA_HOME的话能定位JDK的安装路径的前提是配置了环境变量$JAVA_HOME,否则如下所示,根本定位不到JDK的安装路径. 正确的方式是通过 which java: [tt@vddd ...
- hdu-1237 简单计算器---中缀表达式转后缀表达式
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1237 题目大意: 读入一个只包含 +, -, *, / 的非负整数计算表达式,计算该表达式的值. 思路 ...
- append()/extend()/insert()/remove()/del/pop()/slice列表分片
member = ['小甲鱼', 88, '黑夜', 90, '迷途', 85, '怡静', 90, '秋舞斜阳', 88] member.append('字符串')#在列表结尾处增加字符串 memb ...
- Python进阶_mysql(1)
什么是数据库? 数据库(Database)是按照数据结构来组织.存储和管理数据的仓库,每个数据库都有一个或多个不同的API用于创建,访问,管理,搜索和复制所保存的数据. 进入mysql (linux ...
- Java 异常基础详解
目录 1. Java 中的异常 1.1 什么是异常? 1.2 什么是异常处理? 1.2.1 异常处理的优势 1.3 Java 异常类的层次结构 1.4 异常类型 1.5 检查和未检查异常之间的区别 1 ...
- [LeetCode] Binary Number with Alternating Bits 有交替位的二进制数
Given a positive integer, check whether it has alternating bits: namely, if two adjacent bits will a ...
- Python学习【第26篇】:Python系列- 多线程(threading)
线程的调用方式:threanding模块 import threading import time def sayhi(num): #定义每个线程要运行的函数 print("running ...
- HttpClient入门二
通过上一节我们已经可以实现对一个网站源码的抓取. 但是,有一些网站,在爬取的时候会出现如下的错误: HTTP/1.1 503 Service Unavailable <!DOCTYPE html ...