添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Agora 教程 | 如何使用 Qt 开发音视频通话应用 2019-04-25 05:53

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

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

1 使用 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
  • 2 设计交互流程

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

    0268_如何使用Qt开发音视频通话应用_1.png 1714×1266 206 KB

    有 2 个主要 UI 界面:

  • JoinRoom:登录频道界面;
  • InRoom:视频通话界面;
  • 以及 3 个辅助 UI 界面:

  • Splash:欢迎界面;
  • VideoSetting:视频参数设置界面;
  • DeviceSetting:设备设置界面;
  • UI 之间的交互逻辑,已经用对应红色线框标记出来。

    3 创建 Qt 项目

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

  • 选择 Qt Quick Application - Empty;
    0268_如何使用Qt开发音视频通话应用_2.png 880×542 45.9 KB
  • 输入项目名称 AgoraVideoCall,并选择项目路径;
  • 选择 qmake 编译;
  • 选择最小支持的 Qt 版本,这里默认为 Qt 5.9;
  • 选择本地 Qt 版本,这里使用 5.12.0;
  • 选择版本控制系统;

    4 导入资源

    4.1 导入 images 资源

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

  • 将 images 文件夹拷贝到工程目录中;
  • 在 Qt Creator 的项目视图中,右键点击 Resources/qml.qrc 文件;
  • 选择添加现有路径;
  • 选择 images 文件夹;
  • images 文件夹下的所有资源,会自动添加到 qml.qrc 文件中;
  • 4.2 导入 controls 资源

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

  • 使用 Qt Quick 定义的控件;优点是不用自己开发,可以快速集成使用。
  • 使用用户自定义控件;优点是样式可以自己定义,且可以定义更多官方不提供的控件。
  • 我们这里使用事先准备的一些控件,所以先按照步骤导入到项目中。

  • 将 controls 文件夹拷贝到工程目录中;
  • 在 Qt Creator 的项目视图中,右键点击 Resources/qml.qrc 文件;
  • 选择添加现有路径;
  • 选择 controls 文件夹;
  • controls 文件夹下的所有控件,会自动添加到 qml.qrc 文件中;
  • 需要注意的是,默认情况下控件是没有导入的,需要开发者在要使用的 UI 中导入,例如:

    0268_如何使用Qt开发音视频通话应用_10.png 1920×1057 189 KB

    4.3 导入 Agora.io 音视频通话 SDK

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

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

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

    INCLUDEPATH += $$PWD/lib
    win32: LIBS += -L$$PWD/lib/ -lagora_rtc_sdk

    5 UI 及 UI 业务逻辑

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

    5.1 创建 UI

  • 在项目上点击右键,并选择 Add New,选择 QtQuick UI File 模板;
    0268_如何使用Qt开发音视频通话应用_8.png 880×542 41.8 KB
  • 输入 UI 的名称,并完成创建,会直接进入设计窗口;
    0268_如何使用Qt开发音视频通话应用_9.png 1920×1057 168 KB
  • 根据前面的设计,通过拖拽控件以及调整位置等操作,完成 UI;
    0268_如何使用Qt开发音视频通话应用_11.png 1920×1057 184 KB

    5.2 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 交互响应后,再将该部分逻辑填充进去。

    5.3 QML 与 C++ 交互

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

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

  • 在 C++ 中定义一个 QObject 的子类,注册到 QML 中,在 QML 中创建该类的对象;
  • 在 C++ 中创建对象,并将该对象设置为 QML 的上下文属性,在 QML 中使用该属性;
  • 这里使用第二种方式,定义 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);
    

    6 视频渲染

    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 上了。

    7 核心业务逻辑

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

    7.1 回调事件

    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 中进行响应,不做复杂的处理逻辑。

    7.2 资源管理

    定义 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;
    

    7.3 登录频道

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

    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 中,调用 AgoraRtcEnginejoinChannel 函数,如果成功则切换到 InRoom 界面:

    function joinChannel(channel) {
        if (channel.length > 0 && agoraRtcEngine.joinChannel("", channel, 0) === 0) {
            channelName = channel
            loader.setSource(Qt.resolvedUrl("InRoom.qml"))
    

    7.4 本地流

    进入 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);
    

    7.5 远端流

    当收到 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 的开发,希望可以帮到那些,想要为自己的应用增加了音视频通话功能的场景的同学。

    4个回复
    smallzoo 回复于 2019-12-05 09:42

    在我的mac上运行为什么加入视频的时候会崩溃 日志是:/build-AgoraVideoCall-Desktop_Qt_5_12_0_clang_64bit-Debug/AgoraVideoCall.app/Contents/MacOS/AgoraVideoCall crashed.

    回复·1

    Mac 上崩溃后,会有一个详细的崩溃信息出来,问你要不要发给 Apple 进行分析,你把那个详情贴出来看看?

    okk 回复于 2020-10-15 08:25

    我使用Vs2013,Qt5.6.2打开这个Demo运行成功但是本地视频摄像头并没有打开,视频通话没反应

    博主你好,我在git上下载了这个例子,但是SDK用的3.2.0的,用手机端的官方软件进入同一个房间,我这边没法触发消息(onUserJoined),你知道怎么回事么?

    回复·0
    推荐阅读
    作者信息
    相关专栏
    SDK 教程
    164 文章
    本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。
    创建新话题
    • media-sdk
    • video
    • network
    • web-sdk
    • datahub
    • live-streaming
    • audio
    • android
    • sfu
    • websocket
    • 编解码
    • sdp
    • demo
    • official
    • swift
    • objective-c
    • java
    • javascript
    • kotlin
    • reactnative
    • 小程序
    • 工具
    • cocos
    • unity
    • linux
    • cpp
    • python
    • flutter
    • gdx
    • electron
    • c-sharp
    • release-note
    • 精华资源
    • 社区投稿
    • qt
    • 集成问题
    • 产品功能咨询
    • 文档
    • 其它
    • 深圳
    • 意见反馈
    • ios
    • macos
    • web
    • windows
    • 云录制
    • 本地服务端录制
    • stun
    • conturn
    • apicloud
    • solo
    • rtc
    • 视频会议
    • 泛行业
    • 在线教育
    • 推流
    • restful-api
    • 泛平台
    • rtm
    • 音频问题
    • 第三方功能咨询
    • 录制
    • 社交娱乐
    • 账号账单计费
    • 互动游戏
    • 质量问题
    • demo问题
    • nodejs
    • 需求及优化建议
    • 云代理
    • 文档问题
    • token
    • 定制开发服务咨询
    • ncs
    • uniapp
    • beta-program
    • rts
    • cocos-creator
    • web-ng
    • 精选文章
    • 内容共建
    • mediaplayer
    • 产品评测
    • sdk
    • 容器
    • rte
    • 实时消息
    • 开源
    • 社交游戏
    • 视频社交
    • 视频编码
    • 社交直播
    • 高清视频
    • h5
    • webrtc
    • ui
    • golang
    • 水晶球
    • vr
    • rtsa
    • 开发者吐槽会
    • voice
    • apaas
    • 官方faq
    • 小知识
    • 实时互动技术展望
    • 低延时高音质
    • 白板
    • webassembly
    • podcast
    • 编码人声
    • workshop
    • science-video
    • basictutorials
    • elementary-webrtc
    • elementary-audio
    • elementary-viau
    • intermediate-tutoria
    • intermediate-auvi
    • speech-recognition
    • player
    • video-editor
    • codec
    • intermediate-webrtc
    • intermediate-sr
    • elementary-codec
    • react
    • advanced-audio
    • intermediate-audio
    • 前端
    • 音视频
    • 架构
    • ai
    • css
    • 测试
    • metaverse
    • 问题反馈
    • science-rte
    • codectech
    • audiotech
    • videotech
    • rtetech
    • audio-practice
    • video-practice
    • rte-practice
    • 元宇宙
    • 音频
    • techheight
    • t
    • basic-agora
    • level2
    • api
    • web3
    • spring
    • 创业
    • 后端
    • rte-ng-lab
    • webrt
    • webrtc-音视频
    • rte2022编程大赛
    • 安全
    • 产品创新
    • github
    • 代码实验室

    No data

    回复