“云无界、端无边” OGeek 技术峰会 9月17日 南京不见不散! 了解详情
写点什么

Agora 教程 | 如何使用 Qt 开发视频通话应用

  • 2020 年 2 月 26 日
  • 本文字数:6590 字

    阅读完需:约 22 分钟

Agora 教程 | 如何使用 Qt 开发视频通话应用

众所周知,Qt 是一个跨平台的 C++ 图形用户界面应用程序开发框架,它具有跨平台、丰富的 API、支持 2D/3D 图形渲染、支持 OpenGL、开源等优秀的特性。很多市面上常见的应用或者游戏,例如说 VLC、WPS Office、极品飞车等,都是基于 Qt 开发。


本文将介绍如何使用 Qt 开发一个音视频通话应用。


一、使用 Qt Quick

Qt 目前有两种创建用户界面的方式:


  • Qt Widgets

  • Qt Quick

  • 其中 Qt Widgets 是传统的桌面界面库,而 Qt Quick 是新一代的高级用户界面技术,可以轻松的用于移动端、嵌入式设备等界面开发。


目前 Qt Widgets 已经基本处于维护阶段,已经非常稳定且成熟。而 Qt Quick 是未来发展的主要方向,其开发更加简捷方便,用户体验更加好。


所以本文选择 Qt Quick 作为创建用户界面的方式,开发环境如下:


  • Qt:5.12.0

  • Qt Creator:4.8.2

  • Agora Video SDK:2.4.0


二、设计交互流程

首先,我们设计一个简单的视频通话 UI 交互流程。



有 2 个主要 UI 界面:


  • JoinRoom:登录频道界面;

  • InRoom:视频通话界面;

  • 以及 3 个辅助 UI 界面:

  • Splash:欢迎界面;

  • VideoSetting:视频参数设置界面;

  • DeviceSetting:设备设置界面;

  • UI 之间的交互逻辑,已经用对应红色线框标记出来。


三、创建 Qt 项目

打开 Qt Creator,选择创建新的项目。


  1. 选择 Qt Quick Application - Empty;

  2. 输入项目名称 AgoraVideoCall,并选择项目路径;



3. 选择 qmake 编译;



4. 选择最小支持的 Qt 版本,这里默认为 Qt 5.9;



5. 选择本地 Qt 版本,这里使用 5.12.0;



6. 选择版本控制系统;



四、导入资源

导入 images 资源

我们先将准备好的图标等资源,导入到项目中。


  1. 将 images 文件夹拷贝到工程目录中;

  2. 在 Qt Creator 的项目视图中,右键点击 Resources/qml.qrc 文件;

  3. 选择添加现有路径;

  4. 选择 images 文件夹;

  5. images 文件夹下的所有资源,会自动添加到 qml.qrc 文件中;


导入 controls 资源

在 Qt Quick 中使用按钮等控件时,有两种方式:


  1. 使用 Qt Quick 定义的控件;优点是不用自己开发,可以快速集成使用。

  2. 使用用户自定义控件;优点是样式可以自己定义,且可以定义更多官方不提供的控件。

  3. 我们这里使用事先准备的一些控件,所以先按照步骤导入到项目中。

  4. 将 controls 文件夹拷贝到工程目录中;

  5. 在 Qt Creator 的项目视图中,右键点击 Resources/qml.qrc 文件;

  6. 选择添加现有路径;

  7. 选择 controls 文件夹;

  8. controls 文件夹下的所有控件,会自动添加到 qml.qrc 文件中;

  9. 需要注意的是,默认情况下控件是没有导入的,需要开发者在要使用的 UI 中导入,例如:



导入 Agora.io 音视频通话 SDK

使用音视频通话功能,需要导入 Agora.io 对应的 SDK,可以注册 Agora.io 的开发者账号,并从 SDK 下载地址中获取对应平台的 SDK。


下载后将对应的头文件拷贝到项目的 include 文件夹中,静态库拷贝到项目中的 lib 文件夹中,动态库则拷贝到项目中的 dll 文件夹中。


之后则修改 Qt 的工程文件,指定链接的动态库,打开 AgoraVideoCall.pro 文件,并添加以下内容:


INCLUDEPATH += $$PWD/libwin32: LIBS += -L$$PWD/lib/ -lagorartcsdk
复制代码


五、UI 及 UI 业务逻辑

完成项目创建和资源导入后,我们首先需要实现前面设计的 5 个 UI。


创建 UI

  1. 在项目上点击右键,并选择 Add New,选择 QtQuick UI File 模板;

  2. 输入 UI 的名称,并完成创建,会直接进入设计窗口;

  3. 根据前面的设计,通过拖拽控件以及调整位置等操作,完成 UI;



UI 业务逻辑

完成 UI 后,对应的按钮所触发的业务逻辑需要对应添加。在创建 QtQuick UI File 的时候,例如说创建 Splash UI 时,默认会创建两个 qml 文件:


  • SplashForm.ui.qml:UI 的声明描述;

  • Splash.qml:UI 对应事件的响应和部分 UI 业务逻辑;

  • 所以,例如说 Button 的点击事件、鼠标事件等,都通过对应控件的 id 进行关联处理。


例如在 SplashForm.ui.qml 中,我们期待用户如果点击任何地方,则返回到登录界面,则在 SplashForm.ui.qml 中增加鼠标事件监听区域:


MouseArea {    id: mouseArea    anchors.fill: parent}在 Splash.qml 中增加业务逻辑:
mouseArea.onClicked: main.joinRoom()
复制代码


最后在 main.qml 增加 joinRoom 的响应函数:


Loader {    id: loader    focus: true    anchors.fill: parent}
function joinRoom() { loader.setSource(Qt.resolvedUrl("JoinRoom.qml"))}
复制代码


这样就完成了一个基本的 UI 业务逻辑。其他例如打开设置窗口、登录到频道中等 UI 业务逻辑类似,就不再一一列举。


当然,实际触发的核心业务逻辑,例如说登录频道进行音视频通话、设置参数生效等可以先留空,在完成所有的 UI 交互响应后,再将该部分逻辑填充进去。


QML 与 C++ 交互

基本 UI 业务逻辑完成后,一般需要 QML 与 C++ 之间的逻辑交互。例如按下进入频道的 Join 按钮后,我们需要在 C++ 中调用 Agora 的音视频相关逻辑,来进入频道进行通话。


在 QML 中使用 C++ 的类和对象,一般有两种方式:


  1. 在 C++ 中定义一个 QObject 的子类,注册到 QML 中,在 QML 中创建该类的对象;

  2. 在 C++ 中创建对象,并将该对象设置为 QML 的上下文属性,在 QML 中使用该属性;

  3. 这里使用第二种方式,定义 MainWindow 类,用来作为核心窗体加载 main.qml,并在其构造函数中将本身设置为 QML 的上下文属性:


setWindowFlags(Qt::Window | Qt::FramelessWindowHint);resize(600, 600);
m_contentView = new QQuickWidget(this);m_contentView->rootContext()->setContextProperty("containerWindow", this);m_contentView->setResizeMode(QQuickWidget::SizeRootObjectToView);m_contentView->setSource(QUrl("qrc:///main.qml"));
QVBoxLayout *layout = new QVBoxLayout;layout->setContentsMargins(0, 0, 0, 0);layout->setSpacing(0);layout->addWidget(m_contentView);
setLayout(layout);
复制代码


六、视频渲染

Agora SDK 提供接口,使得用户可以自己定义渲染方式。接口如下:


agora::media::IExternalVideoRender *AgoraRtcEngine::createRenderInstance(    const agora::media::ExternalVideoRenerContext &context) {  if (!context.view)    return nullptr;  return new VideoRenderImpl(context);}
复制代码


VideoRenderImpl 需要继承 agora::media::IExternalVideoRender 类,并实现相关接口:


virtual void release() override {  delete this;}
virtual int initialize() override { return 0;}
virtual int deliverFrame(const agora::media::IVideoFrame &videoFrame, int rotation, bool mirrored) override { std::lock_guard<std::mutex> lock(m_mutex); if (m_view) return m_view->deliverFrame(videoFrame, rotation, mirrored); return -1;}
复制代码


我们将会使用 OpenGL 来进行渲染,定义 renderFrame :


int VideoRendererOpenGL::renderFrame(const agora::media::IVideoFrame &videoFrame) {  if (videoFrame.IsZeroSize())    return -1;
int r = prepare(); if (r) return r;
QOpenGLFunctions *f = renderer(); f->glClear(GL_COLOR_BUFFER_BIT);
if (m_textureWidth != (GLsizei)videoFrame.width() || m_textureHeight != (GLsizei)videoFrame.height()) { setupTextures(videoFrame); m_resetGlVert = true; }
if (m_resetGlVert) { if (!ajustVertices()) m_resetGlVert = false; }
updateTextures(videoFrame);
f->glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, g_indices); return 0;}
复制代码


具体描绘部分,在 updateTextures 中实现如下:


void VideoRendererOpenGL::updateTextures(    const agora::media::IVideoFrame &frameToRender) {  const GLsizei width = frameToRender.width();  const GLsizei height = frameToRender.height();
QOpenGLFunctions *f = renderer(); f->glActiveTexture(GL_TEXTURE0); f->glBindTexture(GL_TEXTURE_2D, m_textureIds[0]); glTexSubImage2D(width, height, frameToRender.stride(IVideoFrame::Y_PLANE), frameToRender.buffer(IVideoFrame::Y_PLANE));
f->glActiveTexture(GL_TEXTURE1); f->glBindTexture(GL_TEXTURE_2D, m_textureIds[1]); glTexSubImage2D(width / 2, height / 2, frameToRender.stride(IVideoFrame::U_PLANE), frameToRender.buffer(IVideoFrame::U_PLANE));
f->glActiveTexture(GL_TEXTURE2); f->glBindTexture(GL_TEXTURE_2D, m_textureIds[2]); glTexSubImage2D(width / 2, height / 2, frameToRender.stride(IVideoFrame::V_PLANE), frameToRender.buffer(IVideoFrame::V_PLANE));}
复制代码


这样就可以将 Agora SDK 回调中的 Frame,绘制在具体的 Widget 上了。


七、核心业务逻辑

我们需要简单封装 Agora SDK 的相关逻辑,以提供音视频通话的功能。


回调事件

Agora SDK 会提供很多事件的回调信息,例如远端用户加入频道、远端用户退出频道等,我们需要继承 agora::rtc::IRtcEngineEventHandler 事件回调类,并重写部分需要的函数,来进行事件的响应。


class AgoraRtcEngineEvent : public agora::rtc::IRtcEngineEventHandler { public:  AgoraRtcEngineEvent(AgoraRtcEngine &engine)    :m_engine(engine) {}
virtual void onVideoStopped() override { emit m_engine.videoStopped(); }
virtual void onJoinChannelSuccess(const char *channel, uid_t uid, int elapsed) override { emit m_engine.joinedChannelSuccess(channel, uid, elapsed); }
virtual void onUserJoined(uid_t uid, int elapsed) override { emit m_engine.userJoined(uid, elapsed); }
virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override { emit m_engine.userOffline(uid, reason); }
virtual void onFirstLocalVideoFrame(int width, int height, int elapsed) override { emit m_engine.firstLocalVideoFrame(width, height, elapsed); }
virtual void onFirstRemoteVideoDecoded(uid_t uid, int width, int height, int elapsed) override { emit m_engine.firstRemoteVideoDecoded(uid, width, height, elapsed); }
virtual void onFirstRemoteVideoFrame(uid_t uid, int width, int height, int elapsed) override { emit m_engine.firstRemoteVideoFrameDrawn(uid, width, height, elapsed); }
private: AgoraRtcEngine &m_engine;};
复制代码


这里我们将事件从 AgoraRtcEngine 的信号函数发出,并在 UI 中进行响应,不做复杂的处理逻辑。


资源管理

定义 AgoraRtcEngine 类,并在构造函数中,初始化音视频通话引擎: agora::rtc::IRtcEngine :


AgoraRtcEngine::AgoraRtcEngine(QObject *parent)    : QObject(parent), m_rtcEngine(createAgoraRtcEngine()),  m_eventHandler(new AgoraRtcEngineEvent(*this)) {  agora::rtc::RtcEngineContext context;  context.eventHandler = m_eventHandler.get();
// Specify your APP ID here context.appId = "";
if (*context.appId == '\0') { QMessageBox::critical(nullptr, tr("Agora QT Demo"), tr("You must specify APP ID before using the demo")); }
m_rtcEngine->initialize(context); agora::util::AutoPtr<agora::media::IMediaEngine> mediaEngine; mediaEngine.queryInterface(m_rtcEngine.get(), agora::AGORA_IID_MEDIA_ENGINE); if (mediaEngine) { mediaEngine->registerVideoRenderFactory(this); } m_rtcEngine->enableVideo();}
复制代码


注意: 有关如何获取 Agora APP ID,请参阅 Agora 官方文档。


在 App 退出时,应当在 AgoraRtcEngine 类的析构函数中,释放音视频通话引擎资源,这里我们通过指定 unique_ptr 的释放函数来自动管理:


struct RtcEngineDeleter {  void operator()(agora::rtc::IRtcEngine *engine) const {    if (engine != nullptr) engine->release();  }};
std::unique_ptr<agora::rtc::IRtcEngine, RtcEngineDeleter> m_rtcEngine;
复制代码


登录频道

大部分的逻辑基本上处理好了,接下来就是最重要的一步了。


在 MainWindow 增加 AgoraRtcEngine 的 QML 上下文属性设置:


AgoraRtcEngine *engine = m_engine.get();m_contentView->rootContext()->setContextProperty("agoraRtcEngine", engine);
复制代码


用户输入频道名,点击 Join 按钮,触发登录逻辑时,我们在 JoinRoom.qml 中增加事件处理:


btnJoin.onClicked: main.joinChannel(txtChannelName.text)在 main.qml 中,调用 AgoraRtcEngine 的 joinChannel 函数,如果成功则切换到 InRoom 界面:
function joinChannel(channel) { if (channel.length > 0 && agoraRtcEngine.joinChannel("", channel, 0) === 0) { channelName = channel loader.setSource(Qt.resolvedUrl("InRoom.qml")) }}
复制代码


本地流

进入 InRoom 界面后,需要进行本地流(一般是摄像头采集的图像)的渲染。在 InRoom.qml 的 onCompleted 中增加:


Component.onCompleted: {    inroom.views = [localVideo, remoteVideo1, remoteVideo2, remoteVideo3, remoteVideo4]
channelName.text = main.channelName agoraRtcEngine.setupLocalVideo(localVideo.videoWidget)}
复制代码


在 AgoraRtcEngine 中,将本地流渲染 Widget 设置为描绘的画布:


int AgoraRtcEngine::setupLocalVideo(QQuickItem *view) {  agora::rtc::view_t v =    reinterpret_cast<agora::rtc::view_t>(static_cast<AVideoWidget *>(view));
VideoCanvas canvas(v, RENDER_MODE_HIDDEN, 0); return m_rtcEngine->setupLocalVideo(canvas);}
复制代码


远端流

当收到 onUserJoined 和 onUserOffline 的事件时, AgoraRtcEngine 会将该事件抛出:


virtual void onUserJoined(uid_t uid, int elapsed) override {  emit m_engine.userJoined(uid, elapsed);}
复制代码


此时,在 InRoom 界面中,捕获该事件,并进行处理:


Connections {    target: agoraRtcEngine
onUserJoined: { inroom.handleUserJoined(uid) } onUserOffline: { var view = inroom.findRemoteView(uid) if (view) inroom.unbindView(uid, view) }}
function findRemoteView(uid) { for (var i in inroom.views) { var v = inroom.views[i] if (v.uid === uid && v !== localVideo) return v }}
function bindView(uid, view) { if (view.uid !== 0) return false view.uid = uid view.showVideo = true view.visible = true return true}
function unbindView(uid, view) { if (uid !== view.uid) return false view.showVideo = false view.visible = false view.uid = 0 return true}
function handleUserJoined(uid) { //check if the user is already binded var view = inroom.findRemoteView(uid)
if (view !== undefined) return
//find a free view to bind view = inroom.findRemoteView(0)
if (view && agoraRtcEngine.setupRemoteVideo(uid, view.videoWidget) === 0) { inroom.bindView(uid, view)}}
复制代码


我们在 UI 中设计最多只能显示 4 个远端流,所以超过 4 个时,就不再进行 bindView 处理。


在 AgoraRtcEngine 中,将远端流渲染 Widget 设置为描绘的画布:


int AgoraRtcEngine::setupRemoteVideo(unsigned int uid, QQuickItem* view) {  agora::rtc::view_t v =      reinterpret_cast<agora::rtc::view_t>(static_cast<AVideoWidget *>(view));  VideoCanvas canvas(v, RENDER_MODE_HIDDEN, uid);  return m_rtcEngine->setupRemoteVideo(canvas);}
复制代码


至此,基本的核心业务逻辑完成,通话效果如下:


八、总结

Qt 作为一个很成熟的图形界面库,使用起来非常简单,并且具备大量的文档和解决方案,个人认为是桌面下开发图形界面库首选的方案之一。这个 Demo 的开发,希望可以帮到那些,想要为自己的应用增加了音视频通话功能的场景的同学。



本文转载自 声网 Agora 公众号。


原文链接:https://mp.weixin.qq.com/s/Y-wAl67kiu9Z4solhCfrjQ


2020 年 2 月 26 日 15:48739

评论

发布
暂无评论
发现更多内容

和12岁小同志搞创客开发:手撕代码,做一款亮度可调节灯

不脱发的程序猿

少儿编程 DIY 智能硬件 创客开发 Arduino

DDD是软件设计思维方式的转变

Bruce Talk

领域驱动设计 DDD

Vue进阶(贰零壹):JS合并两个数组方法详解

No Silver Bullet

Vue 数组 11月日更

模块四作业

double蠢

「架构实战营」

.NET6新东西--struct优化

喵叔

11月日更

架构训练营 - 模块 4 作业

焦龙

架构实战营

Redis 实现限流的三种方式

大数据技术指南

11月日更

模块四作业:千万级学生管理系统的考试试卷存储方案

dean

架构实战营

十分钟搞懂WebAssembly

俞凡

Wasm

kafka常用命令

williamcai

kafka

一些关于原宇宙的思考

Simon

元宇宙 Metaverse

CSS架构揭秘之Ant design

Augus

CSS 11月日更

基于海思Hi3559A或者Atlas_200模块,Hi3559A(主)+Atlas_200(从)开发AI加速边缘计算主板的三种模式

Todd Wong

人工智能 深度学习

自定义View:如何手写ViewGroup实现ListView效果

Changing Lin

11月日更

CentOS环境下Redis的安装和配置

Empty

redis

通过USB接入双目UVC协议外接人脸比对相机实现1:1比对开发

Todd Wong

数字化办公

Hive数据抽样与存储格式详解

五分钟学大数据

11月日更

【解析】通证经济的分类及用途

CECBC

区块链是什么

Rayjun

区块链

Apache Pulsar 与 Kafka 性能比较:延迟性(测试方法)

Apache Pulsar

大数据 kafka 分布式 云原生 Apache Pulsar

Go语言学习查缺补漏ing Day2

Regan Yue

Go 语言 11月日更

财经大课:通货膨胀的逻辑

石云升

学习笔记 财经思维 11月日更

模块五作业

沐风

区块链新闻编辑部成立,看区块链如何助力新闻传播?

CECBC

【架构实战营】模块四

Henry | 衣谷

架构实战营

面试必备(背)--Go语言八股文系列!

微客鸟窝

Go 语言 八股文 11月日更

区块链,不是元宇宙的全部

CECBC

工作三原则

ok绷

复习第三天

IT蜗壳-Tango

11月日更

requests-html库初识 + 无资料解BUG之 I/O error : encoder error,Python爬虫第30例

梦想橡皮擦

11月日更

30分钟学习go语言

坚果

Go 语言 11月日更

首届腾讯云大数据峰会暨Techo TVP开发者峰会

首届腾讯云大数据峰会暨Techo TVP开发者峰会

Agora 教程 | 如何使用 Qt 开发视频通话应用_新基建_声网_InfoQ精选文章