Qt版本:Qt5.7.0以上,QT release下载地址http://download.qt.io/official_releases/qt/
Qt中文输入法软键盘需要重新编译qtvirtualkeyboard模块
qmake CONFIG+="lang-en_GB lang-zh_CN"
当前linux下部署版本是QT5.7.1,放在/opt/路径下,在ld.so.conf.d中使用libqt5.conf文件指定库文件目录,执行程序所在目录下使用qt.conf设置qt相关路径
ds库(director_service导播台服务的缩写)为trancoder进程下调用的一个动态库,使用QT构建界面+OpenGL渲染显示,界面布局由配置文件director_service.xml文件灵活配置生成,主要分单输出版(4G便携式导播台),多输入版(用于监控大屏,可自由切换row*col),单输出带多输入版(导播台),三个版本由ds_version.h中的条件编译宏控制,定义如下:
#ifndef DS_VERSION_H
#define DS_VERSION_H
#define VERSION 7
#define RELEASEINFO "5.7.1 @ 2018/01/01"
#define LAYOUT_TYPE_ONLY_OUTPUT 1
#define LAYOUT_TYPE_MULTI_INPUT 0
#define LAYOUT_TYPE_OUTPUT_AND_INPUT 0
#endif // DS_VERSION_H
编译哪个版本,便将对于的宏置1,其它置0即可。你在添加新的需求时,如果只是添加到对应的一个版本下,记得使用这些宏去包含,不要影响到其它版本。
几个版本的主要区别在于:
LAYOUT_TYPE_MULTI_INPUT 多输出版带一个切换row*col的主工具栏;
LAYOUT_TYPE_OUTPUT_AND_INPUT 单输出带多输入版带一个切换输出模板和调音台的主工具栏;
此外各版本的标题栏、工具栏按钮和功能可能有些差异的地方,具体依照需求而定。
先附上整体架构图
ds.conf是一个ini格式文件,记录了QT和后台进行交互的URL,表情图片路径,数据库路径等
[URL]
toolbar= http://localhost/transcoder/audio/index.html
soundmixer= http://localhost/transcoder/audio/audio.html
combinfo= http://localhost/transcoder/index.php?controller=channels&action=Dragsave
voiceinfo= http://localhost/transcoder/index.php?controller=channels&action=voiceinfo
micphone= http://localhost/transcoder/index.php?controller=channels&action=voiceover
query_overlay= http://localhost/transcoder/index.php?controller=logo&action=allinfo
add_overlay= http://localhost/transcoder/index.php?controller=logo&action=logoadd
remove_overlay= http://localhost/transcoder/index.php?controller=logo&action=ddelete
modify_overlay= http://localhost/transcoder/index.php?controller=logo&action=editpt
[PATH]
pic_usb=/usb/images/
pic_local=/var/www/transcoder/
pic_upload=/var/www/transcoder/Upload/
[SQL]
type= QMYSQL
username= root
hostname= localhost
port= 3306
dbname= videowell_db
query_models= SELECT * FROM complex_temp;
director_service.xml则是和界面配置相关的xml格式文件,程序中对此文件添加了监视器,一旦文件改变,则会重新解析此文件,做到动态更改某些配置参数,如修改debug等级,可以很方便的查看调试日志,日志记录在/var/log/ds.log文件中,通过
tail -f /var/log/ds.log
可以很方便的动态监视日志。
下面列出一些重要的配置参数:
这些属性,通过在程序中搜索关键字,可以分析得出大致的作用,你后续如果要添加其他属性,也请归类后写入此文件中。
程序中解析这个文件的函数是HDsContext类中的int parse_layout_xml(const char* xml_file);
保存这些属性的数据结构体是DsInitInfo和DsLayoutInfo
ds库导出的接口很简单,但扩展性也很强,主要参照程总那边的service.h头文件定义,我们的导出头文件为ds.h,接口如下:
extern "C" {
DSSHARED_EXPORT int libversion(void);
DSSHARED_EXPORT int libchar(void);
DSSHARED_EXPORT int libtrace(int);
DSSHARED_EXPORT int libinit(const char* xml, void* task, void** ctx);
DSSHARED_EXPORT int libstop(void* ctx);
DSSHARED_EXPORT int liboper(int media_type, int data_type, int opt, void* param, void * ctx);
}
注:extern “C”代表导出为C编译器风格的函数名
libversion是版本数字,libchar是版本的fourcc宏,libtrace是trace等级,这些接口都是历史接口,有的可能只是为了适配,如我们的trace等级有配置文件中的debug决定,并不采取libtrace设置的值
libinit用于库初始化操作,主要是new出重要的上下文内容类HDsContext,这是一个全局单例类,全局指针是g_dsCtx,然后调用parse_layout_xml函数解析上文提到的director_service.xml文件,当然也记录了图片路径,字体路径等;
需要注意是是每一路码流开始都会调用libinit,所以libinit和libstop不只是调用一次,但我们程序中实际只需调用到一次,只是为了兼容库的调用者,所以设置了调用次数引用ref,实际只会new一个HDsContext类,由srvid去区别每一路码流,由HDsContext类中的数组DsSrvItem m_srvs[DIRECTOR_MAX_SERVS];去记录每一路码流的各自信息;
liboper则是最灵活也调用最频繁的接口,media_type指示媒体类型
enum MediaType{
MediaTypeUnknown = 0,
MediaTypeAudio = 1,
MediaTypeVideo = 2,
};
data_type指示数据类型,opt指示操作类型,param是附带的参数指针,ctx指向HDsContext类;
这个接口功能是将调用者传递的控制信息和数据保存在HDsContext类中,具体实现比较复杂,建议直接去看源码,基本是
switch(media_type)
switch(data_type)
switch(opt)
三层switch-case嵌套结构
我自己写的类一般以H开头(我姓贺,首字母H,这个习惯主要是受以前公司师傅的影响,这样谁写的类出现bug谁负责哈哈),以和QT以Q开头的类区分开来
这个类在上文我们提到过了,是用来保存调用者传递过来的控制信息和数据。是一个承上启下的类。
这个类在libinit中被创建,所以是属于调用者线程的,而程序中我们需要另外创建一个线程来跑QT的消息循环,这个线程是在HDsContext类的start_gui_thread()中创建的,gui线程的入口函数是void* HDsContext::thread_gui(void* param);
核心QT线程调用流程简化如下:
QApplication app(argc, argv);//创建应用程序类
//启动画面与初始化工作
...
app.exec();//消息循环
QT的信号与槽机制为跨线程的事件通知和方法调用提供了便利性,在调用者线程和QT的gui线程间,我们不需要加锁就可以访问HDsContext类中的属性和方法。这个类中定义了一些了的信号通知,如actionChanged、videoPushed、audioPushed等,gui线程中的类可以很方便的连接这些信号,调用对应槽函数进行处理。
这个类就是我们主窗口类,在thread_gui中被创建,继承自QT的控件类,我们一律调用initUI构建UI界面,调用initConnect建立信号与槽连接。
在HMainWidget的initUI函数中,我们主要根据dssave.conf文件和director_service.xml文件来动态生成界面,即使就是创建HGeneralGLWidget(用于输入画面)和 HCombGLWidget(用于输出画面),HGeneralGLWidget和HCombGLWidget公共父类时HGLWidget,所以在HMainWidget类中,我们通过std::vector
向量来保存创建的HGLWidget类指针;
在定时渲染模式下,我们开始了一个定时器timer_repaint,定时触发onTimerRepaint函数调用
QObject::connect( &timer_repaint, SIGNAL(timeout()), this, SLOT(onTimerRepaint()) );
if (g_dsCtx->m_tInit.display_mode == DISPLAY_MODE_TIMER){
timer_repaint.start(1000 / g_dsCtx->m_tInit.fps);
}
在onTimerRepaint函数中自然就是遍历m_vecGLWdg向量,对处于播放状态的窗口进行更新操作;简化的onTimerRepaint函数如下:
for (int i = 0; i < m_vecGLWdg.size(); ++i){
HGLWidget* wdg = m_vecGLWdg[i];
if (!wdg->isResetStatus() && wdg->isVisible()){
if (g_dsCtx->pop_video(wdg->srvid) == 0)
wdg->update();
}
}
此外,HMainWidget类对鼠标和键盘事件进行了重载
virtual void keyPressEvent(QKeyEvent *e);
virtual void mousePressEvent(QMouseEvent *e);
virtual void mouseMoveEvent(QMouseEvent *e);
virtual void mouseReleaseEvent(QMouseEvent *e);
实现了输入的HGLWidget可以互换位置,输入的HGLWidget可以叠加到输出的HGLWidget上去等操作;
提供了全屏某个HGLWidget的槽函数void onFullScreen(bool);
HGLWidget的父类时QGLWidgetImpl,QGLWidgetImpl是对QT提供的QOpenGLWidget的实现,主要是实现了initializeGL和resizeGL,此外提供了顶点缓存、YUV着色器程序,drawYUV、drawTex、drawStr、drawRect等封装接口;具体实现感兴趣的可以去看源码;
HGLWidget目前子类有HGeneralGLWidget,HLmicGLWidget和HCombGLWidget,分别对应枚举类型中的GENERAL,LMIC 和 COMB
enum TYPE{
UNKOWN = 0,
GENERAL = 1, // 用于显示输入画面
COMB = 2, // 用于显示输出画面
EXTEND = 3, // 用于HDMI接扩展屏
LMIC = 4, // 用于显示连麦画面
};
HGLWidget主要是对paintGL的实现,流程函数有drawVideo、drawAudio、drawIcon、drawTitle、drawTaskInfo、drawOutline、drawDebugInfo、drawFps;子类中如有自己的特殊需求,也可以重载这些函数。
播放状态枚举
enum GLWND_STATUS{
MAJOR_STATUS_MASK = 0x00FF,
STOP = 0x0001,
PAUSE = 0x0002,
PLAYING = 0x0004,
NOSIGNAL = 0x0008,
MINOR_STATUS_MASK = 0xFF00,
PLAY_VIDEO = 0x0100,
PLAY_AUDIO = 0x0200,
};
paintGL源码
void HGLWidget::paintGL(){
calFps();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// drawVideo->drawAudio->drawTitle->drawOutline
DrawInfo di;
switch (m_status & MAJOR_STATUS_MASK) {
case STOP:
di.left = width()/2 - 50;
di.top = height()/2 - 20;
di.color = 0xFFFFFFFF;
drawStr("无视频", &di);
break;
case NOSIGNAL:
di.left = width()/2 - 50;
di.top = height()/2 - 20;
di.color = 0xFFFFFFFF;
drawStr("无信号", &di);
break;
case PAUSE:
case PLAYING:
if (m_status & PLAY_VIDEO){
drawVideo();
if (g_dsCtx->m_tInit.drawfps)
drawFps();
}
if (m_bDrawInfo && g_dsCtx->m_tInit.drawtitle){
drawTitle();
}
if (m_bDrawInfo && g_dsCtx->m_tInit.drawinfo){
drawTaskInfo();
}
if (m_status & PLAY_AUDIO){
if (m_bDrawInfo && g_dsCtx->m_tInit.drawaudio){
drawAudio();
}
}
break;
}
if (g_dsCtx->m_tInit.drawoutline){
drawOutline();
}
if (g_dsCtx->m_tInit.drawDebugInfo){
drawDebugInfo();
}
}
HGeneralGLWidget和HCombGLWidget主要区别就是标题栏和工具栏不同,此外,HCombGLWidget比较复杂的功能就是子画面和叠加物(包括文字和图片)的CRUD操作,这些操作对象大致都是一个矩形画面在输出画面上的叠加,我们统一用一个类来抽象,就是接下来的主角类HAbstractItem;
先来看看HAbstractItem的抽象接口
class HAbstractItem
{
public:
HAbstractItem();
virtual ~HAbstractItem();
virtual void add() {}
virtual void remove() {}
virtual void modify() {}
virtual bool undo() {return false;}
enum TYPE{
NONE = 0,
SCREEN = 1,
OVERLAY = 10,
PICTURE,
TEXT,
OVERLAY_END = 99,
ALL = 0xFF,
};
inline bool isOverlay(){
return type > OVERLAY && type < OVERLAY_END;
}
public:
TYPE type;
int id;
QRect rc;
};
主要子类别在枚举TYPE中给出,有画面SCREEN,图片PICTURE,文字TEXT,此外就是抽象的CRUD操作接口,还有一个撤销接口undo;
对应子类分别是HCombItem、HPictureItem和HTextItem,文字类中又分标签文本、时间、秒表、字幕
enum TEXT_TYPE{
LABEL = 1,
TIME = 2,
WATCHER = 3,
SUBTITLE = 4,
};
具体CRUD接口实现就是通过HTTP协议和后台提供的URL通信,可以通过tcpdumo去抓取http包去分析,具体通信的json格式在项目的wiki中可以找到
ds库中有几个很重要的单例类:
见项目wiki文档
我们使用环形缓冲HRingBuffer去保存音视频数据,OpenGL去渲染yuv纹理,ffmpeg去缩放yuv帧,PortAudio去播放音频;
音频自动判断卡顿发生的情况(音频缓存为空,播放的是静音帧)下暂停播放,等到帧缓存到一定数量(这个数量不好设置,目前设置为6帧,过大延时高,过小卡顿可能再次发生)再继续播放,如果发生帧溢出的情况,首先是扩大缓冲,如果情况还是发生则只能采取丢弃一部分旧的音频帧;
因为硬件性能的限制,一般是低于25fps播放视频,所以需要保证均匀的丢帧,确保不会出现较大的画面跳跃性;
对于没有播放音频的路数采取的是缓存满尾部不缓存方式;
对于有音频播放的路数因为同步机制,视频帧通常慢于音频帧,所以采取的是头部去帧方式;
音视频同步的功能,基本思路是在帧保存时附带上时间戳,根据音视频时间差去调整视频;
如果视频快,那就不从缓存区取帧(停留在上一画面,这里有个隐患,如果音频帧太慢,会造成画面卡顿);
如果慢就连续取两帧丢弃前一帧(即上面说到的头部去帧),这样就能循序渐进的达到音视频同步的目的;
但同步、延时、卡顿在恶劣环境(帧来的特别不规律,时快时慢)下,还是很难协调的,特别是视频帧缓存受限(内存不足)的情况下。
一些基本情况的处理:
首先当声音卡顿时,考虑是网络抖动太厉害造成的,应该扩大音频缓存;
当视频卡顿时,可能是音视频同步在自动调节中,如果始终不能调整好,应考虑扩大视频缓存;
延时比较大时,又需要考虑缩减缓存;
所以说卡顿和延时是很难平衡的,应尽量在保证不卡顿的情况下减少延迟,即设置合适的缓存值;
gitlab中已经新建一个install目录用于存放需要部署的文件,通过运行脚本update.sh即可安装。
部署文件有:
以调试音视频同步情况为例:
程序中关于音视频同步打印语句有:
qDebug("srvid=%d video faster audio, v_span=%ld, a_span=%ld", wdg->srvid, v_span, a_span);
qDebug("srvid=%d video slower audio, v_span=%ld, a_span=%ld", wdg->srvid, v_span, a_span);
qDebug("srvid=%d video approximate audio, v_span=%ld, a_span=%ld", wdg->srvid, v_span, a_span);
以日志打印中的某个关键词进行grep就可以看到筛选出此类信息了
tail -f /var/log/ds.log | grep v_span
这样我们就可以看到视频帧和音频帧时间戳的比较情况,一般视频帧过快faster,就会造成画面显示停顿,直到音频帧播放赶上;而视频帧过慢slower,则会造成画面跳帧播放,画面不是很连贯,approximate则表示音视频基本处于同步;
因为fps一般设置的低于25帧渲染播放,所以视频帧会时不时处于slower状态,跳帧播放,这是正常的现象;
通过打开drawdebuginfo,在对应窗口上我们可以看到窗口id和srvid的对应关系,源信息,窗口的大小,视频分辨率,显示的实际大小,音视频缓存情况;
为了跨平台,源码文件使用UTF-8编码格式保存,但VS默认以GBK编码读取源码文件,所以源码文件中的中文字符串会乱码,对于使用了QString的接口,我们声明了一个宏来应付VS的这种怪行为;
#define STR(str) QString::fromLocal8Bit(str)
维护这个库,你需要基本的QT+OpenGL+ffmpeg+PortAudio+音视频知识,如果你是这些方面的老手,看了估计会吐槽我写的烂代码(反正我也听不见了,当然你也要体谅这是三个版本不断糅合,需求不断变更的结果,数据结构,整体架构还是挺清晰的),如果你是新手,我相信理解后也会受益匪浅,从此QT得心应手,领略到音视频编解码,渲染播放,也就那么回事。