最近有个功能需求,界面可以直播显示某个特定摄像头的实时画面,通过对Qt本身的 QMiediaPlayer,和其他一些在网上找到的组件做了一些对比,最终选择了QtAv作为我们的组件使用。
QtAV 是一个基于 Qt 和 FFmpeg 的跨平台、高性能多媒体播放框架。
一、QtAV的编译
从 Github 上下载到的是 QtAV 的源码,或者说由于每个开发者使用的 VS 或者 Qt 版本的不同,从而迫使我们不得不自己手动对 QtAV 进行编译,而 QtAV 的编译过程相对来说是非常简单的。
-
下载编译依赖组件
因为 QtAV 是一个基于 Qt 和 FFmpeg 的跨平台多媒体播放器,所以在编译 QtAV 之前,需要我们下载一些依赖的组件。
对于 FFmpeg, 可以选择下载其源码并自己构建。
也可以选择直接下载已经构建好的组件。对于 windows 的用户,可以选择下载文件。 QtAV-depends-windows-x86+x64.7z
为了方便,我这边是选择下载已经编译好的组件版本。
-
下载QtAv源码
QtAV 的源码在 GitHub 上可以直接下载。
-
编译
编译之前,我们需要将下载下来的 QtAV 依赖的组件进行引用,因为我们需要使用 QtCreator 编译 QtAv,所以,首先需要将 QtAV-depends-windows-x86+x64.7z 解压缩之后的文件夹中的 lib 和 include 文件夹拷贝到你本地 Qt 对应的目录下。
需要注意的是,需要对应编译的版本,比如,我用的 msvc2017_64 编译,所以对应的就需要拷贝到下面的路径 D:\Qt\Qt5.12.5\5.12.5\msvc2017_64\include。也可以不进行拷贝,而是通过修改构建环境里面的INCLUDE和LIB的值进行对应的添加,这边需要注意的是,一定要注意是32位还是64位。
拷贝完成之后,直接用 QtCreator 打开 QtAV 源码中的 QtAV.pro 文件,右键构建即可,等待编译完成。
编译完成之后,在编译路径下面 lib_win_x86_64 文件夹下面会生成需要的 dll 和 lib 文件,如果以后需要方便的使用,则可以双击编译路径下面的 sdk_install.bat 文件,该文件的作用是,将编译生成的动态库、静态库已经头文件全部复制到编译 QtAV 对应的 Qt 目录下,这样,以后在使用的时候,直接在 pro 文件中引入 QtAV 模块即可。
QT += av avwidgets
如果使用 VS + QT 开发环境,也可以直接使用,或者将对应的库文件拷贝使用。
相应的 sdk_uninstall.bat 文件会删除拷贝的文件。
二、QtAv的使用
为了方便,我对多媒体播放的部分进行了简单的封装。提供了一些简单的接口,以便外部的调用,而这个程序因为比较简单,所以我用了一个按钮来控制视频的播放和暂停。并且使用了 hideEvent 来作为 stop 的信号。
cpp
class AvMediaWidget : public QWidget
{
Q_OBJECT
public:
...
void setSource(const QString& source);
void pause(const bool pause);
void stop();
void start();
protected:
virtual void enterEvent(QEvent *event) override;
virtual void leaveEvent(QEvent *event) override;
virtual void hideEvent(QHideEvent *event) override;
private:
void initPage();
private:
Ui::AvMediaWidget *ui;
QtAV::AVPlayer* m_player{ nullptr };
QtAV::WidgetRenderer* m_renderer{ nullptr };
QString m_source{ QString() };
};
首先,直接 new 一个 AVPlayer 的对象,因为我没有用到Video,所以也就用可一个简单的 WidgetRenderer
作为 player的载体,如果需要做视频播放器的话,可以使用 VideoWidget
类。
cpp
void AvMediaWidget::initPage()
{
m_player = new AVPlayer();
m_renderer = new WidgetRenderer();
Widgets::registerRenderers();
ui->verticalLayout->addWidget(m_renderer);
m_renderer->show();
m_player->setRenderer(m_renderer);
m_player->setBufferMode(QtAV::BufferBytes);
m_player->setBufferValue(1024);
...
}
紧接着,我用了一个QPushButton,这个按钮的功能主要是用来控制播放的播放、暂停。样式了,就好比是我们经常看视频时点了暂停的那样。
cpp
{
auto btn = new QPushButton(this);
auto layout = new QHBoxLayout(this);
btn->setProperty("type", "player");
layout->addWidget(btn);
layout->setContentsMargins(15, 0, 0, 0);
m_renderer->setLayout(layout);
connect(btn, &QPushButton::clicked, this, [this]()
{
if (!m_player->isPlaying())
{
start();
auto btn = static_cast<QPushButton*>(sender());
if (btn == nullptr)
{
return;
}
btn->setVisible(false);
return;
}
m_player->pause(!m_player->isPaused());
auto btn = static_cast<QPushButton*>(sender());
if (btn == nullptr)
{
return;
}
btn->setProperty("status", m_player->isPaused() ? "player-start" : "player-stop");
this->style()->unpolish(btn);
this->style()->polish(btn);
});
...
}
下面是为了在刚开始或者是停止播放之后,使得界面上会一直存在一个 start 的三角形,所以,对按钮进行了样式表的设置。
cpp
{
connect(m_player, &AVPlayer::stopped, this, [this]()
{
auto btn = m_renderer->findChild<QPushButton*>();
if (btn == nullptr)
{
return;
}
btn->setProperty("status", "player-start");
btn->setVisible(true);
this->style()->unpolish(btn);
this->style()->polish(btn);
});
}
而我们希望在播放的过程中,不能有一个按钮影响观感,所以,用了下面两个函数,来控制按钮的显示和隐藏以及根据当前播放器的状态进行样式表的设置。
cpp
void AvMediaWidget::enterEvent(QEvent *event)
{
if (!m_player->isPlaying())
{
return;
}
auto btn = m_renderer->findChild<QPushButton*>();
if (btn == nullptr)
{
return;
}
btn->setVisible(true);
btn->setProperty("status", m_player->isPaused() ? "player-start" : "player-stop");
this->style()->unpolish(btn);
this->style()->polish(btn);
}
cpp
void AvMediaWidget::leaveEvent(QEvent *event)
{
if (!m_player->isPlaying() || m_player->isPaused())
{
return;
}
auto btn = m_renderer->findChild<QPushButton*>();
if (btn == nullptr)
{
return;
}
btn->setVisible(false);
}
三、遇到的问题
1、 VS + QT 环境中报错
因为我使用的开发环境是 VS + QT 的,所以,我在使用的过程中需要将编译好的库文件拷贝到我对应的工程下面,并进行引用。
而在我的测试工程中,发现一个很有意思的事情,就是 Debug 模式下, 一切正常,但只要一切到 Release 版本,通过单步调试,发现,只要执行
cpp
m_renderer = new WidgetRenderer();
软件必崩,并且伴随报错 QWidget: Must construct a QApplication before a QWidget
这个错误消息,一般对应着两种情况:
-
就是在 main.cpp 执行 QApplication a(argc, argv); 之前就已经有 QWidget 对象被构建了,这主要体现在一下 static QWidget
-
很简单,就是 Debug 和 Release 版本对不上,也就是混用了
很明确的是我知道我的整个工程里面是没有静态的 QWidget 的,那么就只剩下第二种一种情况了,就是 Debug 和 Release 版本混用了。为了验证问题,我排查了好几遍,甚至写了有一个 QtCreator 的工程去验证了了下是不是编译库文件的问题,结果没问题。
也找了很多的信息,反正就是只要一起,就崩。debug就是没问题。
也不知道什么时候,突然想起来,既然我现在能保证我所有的 VS 的配置是正确的,但是 VS 是有 Qt 的,那么这个Qt的配置是不是正确的,检查发现,果然,我在VS的release下面配的Qt是debug的。
一个很简单的问题,却耗费了我大半个下午的时候去排查问题。而我们经常会忽略这些很小的细节,以至于多花费更多的精力和时间。
2、rtsp 服务的拉流总是会失败
测试的过程中发现,用 rtmp 服务拉流是没问题的,但是拉取 rtsp 服务的流的时候总是拉不下来,然后我用前面下载下来的 ffmpeg 单独做了一组测试。
cpp
ffplay.exe rtsp://....
ffplay.exe rtmp://ns8.indexforce.com/home/mystream
ffplay.exe -rtsp_transport tcp rtsp://....
发现,后面两种情况是可一的。那么,问题就是该找找要怎样去设置当需要拉取 rtsp 服务流的时候,使用 tcp 去拉。
后来没发现,就改了一下源码,进行重编译。
在 QtAV 的源码中找到 AVDemuxer.cpp 文件,在该文件
cpp
void checkNetwork() {
// FIXME: is there a good way to check network? now use URLContext.flags == URL_PROTOCOL_FLAG_NETWORK
// not network: concat cache pipe avdevice crypto?
if (!file.isEmpty()
&& file.contains(QLatin1String(":"))
&& (file.startsWith(QLatin1String("http")) //http, https, httpproxy
|| file.startsWith(QLatin1String("rtmp")) //rtmp{,e,s,te,ts}
|| file.startsWith(QLatin1String("mms")) //mms{,h,t}
|| file.startsWith(QLatin1String("ffrtmp")) //ffrtmpcrypt, ffrtmphttp
|| file.startsWith(QLatin1String("rtp:"))
|| file.startsWith(QLatin1String("rtsp:"))
|| file.startsWith(QLatin1String("sctp:"))
|| file.startsWith(QLatin1String("tcp:"))
|| file.startsWith(QLatin1String("tls:"))
|| file.startsWith(QLatin1String("udp:"))
|| file.startsWith(QLatin1String("gopher:"))
)) {
network = true;
}
}
方法末尾增加下面代码,告诉,如果是 rtsp 服务,则使用 TCP 方式。
cpp
if(file.startsWith(QLatin1String("rtsp:")))
options[QStringLiteral("rtsp_transport")]=QStringLiteral("tcp");
3、rtsp 测试地址太少了
基本上在网上能找到的能够正常测试的 rtsp 地址都已经不能用了,要怎么办呢?
访问 rtsp 的 官网。
拉到最下方,在 free 区域下面点击 Get Started 按钮,在接下来的界面中输入可用的邮箱,rtsp 会发送一封邮件,打开邮件中的地址,会生成两条可测试的 rtsp 流地址,并且每个月有 2G 的流量。
后续在测试的过程中还发现了其他的一些问题,比如花屏问题,也找了一些其他的库做了对比,下一节再来展开看看。