前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux QQ能打语音视频了!一文详解背后技术实现!

Linux QQ能打语音视频了!一文详解背后技术实现!

作者头像
腾讯云开发者
发布2024-06-18 14:07:28
2880
发布2024-06-18 14:07:28
举报

6 月 6 日,QQ For Linux 3.2.9 正式支持了音视频通话功能,这是 QQ Linux 版本的又一个里程碑事件。 2024 年,QQ 音视频正式推出 NTRTC,全平台(iOS/Android/MacOS/Windows/Linux)的支持是 NTRTC 的重要特性之一,本次 Linux 平台的适配也是这次升级过程中重要的一环。 本文作者详细记录了 QQ 音视频通话在 Linux 平台适配开发过程中的技术实现方案与一些细节,以帮助大家理解在 Linux 平台实现音视频通话能力的从 0 到 1 的过程。也欢迎大家下载最新版 Linux QQ 试用体验:im.qq.com/linuxqq

01、背景

随着新版 QQ 桌面端的上线,在网上得到了广泛的讨论,尤其 QQ For Linux 3.0 推出后,比之前的 Linux 版本有了突破性的改变。

QQ For Linux 3.1 还不支持语音、视频通话,音视频通话作为基础能力之一,适配 Linux 平台,这将是一个从0-1的过程,非常值得期待。

QQ 的音视频通话能力是基于 AVSDK,在过去3年中,我们持续对 AVSDK 进行基础架构重构,更新底层基础库, 对 AVSDK 持续优化;

在24年上半年,QQ 音视频正式推出 NTRTC,全平台(iOS/Android/MacOS/Windows/Linux)的支持是 NTRTC 的重要特性之一,本次 Linux 平台的适配也是这次升级过程中重要的一环。

Linux 平台上的适配对我们来说是一个挑战,一个全新的平台,以下思路开展:

  1. 首先我们要对 Linux 平台有个调研,包括平台信息、开发环境等;
  2. 然后针对 SDK 进行编译适配,这将涉及到所有的代码跟依赖库;
  3. 平台媒体层适配,视频、音频链路的采集、渲染、编解码等;
  4. 新增终端的通话业务适配,这包括前后端的逻辑,比如新增的终端类型,通话流控控制等;
  5. 发布部署等,如流水线搭建,版本管理;

那么我们开始!

02、Linux 平台调研

2.1 Linux 平台

Linux 内核最初只是由芬兰人林纳斯·托瓦兹(Linus Torvalds)在赫尔辛基大学上学时出于个人爱好而编写的。

Linux 是一套免费使用和自由传播的类 Unix 操作系统,是一个基于 POSIX 和 UNIX 的多用户、多任务、支持多线程和多 CPU 的操作系统。

2.2 Linux 发行版

Linux 发行版是由 Linux 内核以及各种软件和工具组成的完整操作系统。由于 Linux 的开源特性,任何人都可以创建自己的 Linux 发行版。因此,目前存在着数百种不同的 Linux 发行版,每种发行版都有其特定的目标用户和用途。以下是一些较为知名的 Linux 发行版:

图片来自 runoob.com

目前市面上较知名的发行版有:Ubuntu、RedHat、CentOS、Debian、Fedora、SuSE、OpenSUSE、Arch Linux、SolusOS、Kylin(麒麟),UOS(统信) ,还有腾讯开源的 OpenCloud OS。

每个 Linux 发行版都有其特点和优势,用户可以根据自己的需求和偏好来选择适合自己的发行版。

本次适配也就是在上述的 Linux 发行版本上开发可运行的软件。

2.3 开发之前

在做开发前,我们要了解的信息有:开发环境、用户运行环境,除了要确定 Linux 发行版(后面都统一使用 Linux 系统版本代替),还要考虑到硬件信息,比如不同 CPU 架构,GPU 信息;

2.3.1 运行环境

主流的 Linux 操作系统,如:Ubuntu、Redhat、Debian、Fedora、Kylin、UOS。

系统架构:x64、arm64、loong64、mips64el。

通过新桌面 QQ Linux 版本的分布数据,我们会优先适配 x64、arm64。

2.3.2 安装包(可执行文件)

这个很好理解,比如软件包,脚本等可运行的软件;

Linux 系统中的软件通常通过软件包的形式进行分发和安装。软件包包含了软件的可执行文件、库文件、配置文件等,以及一些元数据,如软件的版本、依赖关系等。

不同的 Linux 发行版可能使用不同的软件包管理系统,因此软件包的类型也会有所不同。以下是一些常见的 Linux 发行版和它们的软件包类型:

  1. Debian、Ubuntu、Linux Mint:这些基于 Debian 的发行版通常使用 .deb 格式的软件包,可以通过 dpkg 命令直接安装,也可以通过 apt 或 apt-get 命令进行包管理。
  2. Fedora、CentOS、Red Hat:这些发行版使用 .rpm 格式的软件包,可以通过 rpm 命令直接安装,也可以通过 yum 或 dnf 命令进行包管理。
  3. Arch Linux、Manjaro:这些发行版使用 .pkg.tar.xz 格式的软件包,可以通过 pacman 命令进行包管理。
  4. Gentoo:Gentoo 使用的是源代码包,用户可以通过 emerge 命令进行包管理。
  5. Slackware:Slackware 使用 .tgz 或 .txz 格式的软件包,可以通过 pkgtool 命令进行包管理。

以上只是一些常见的例子,实际上还有许多其他的 Linux 发行版和软件包格式。此外,一些通用的软件包格式,如 AppImage、Flatpak 和 Snap,也可以在大多数 Linux 发行版上使用。

我们以桌面版本 QQ 为例,分别打包了 deb、rpm、AppImage 的软件包格式。

2.3.3 静态库、动态库

在 SDK 开发中,我们交付的会根据不同平台,App 不同的使用方式提供 SDK 产物,也就是静态库或者动态库

例如:

Unix:`qav_ntrtc_sdk.a` 的静态库和 `qav_ntrtc_sdk.so` 的动态库;

Windows :`qav_ntrtc_sdk.lib` 的静态库和 `qav_ntrtc_sdk.dll` 的动态库。

macOS:`qav_ntrtc_sdk.a` 的静态库和 `qav_ntrtc_sdk.dylib` 的动态库。

这些只是常见的命名约定,实际上,库文件的命名可能会因编译器、开发环境和开发者的选择而有所不同。

这个比较重要,因为作为 sdk 提供方,需要对不同交付的产物有明确的了解,sdk 也会根据使用方案提供不同的产物。

2.3.4 开发环境

上面提到的不同 Linux 发行版本,这次开发申请了一台 PC 机(x64),安装了 TLinux(Ubuntu 20.04.6)。

主开发机使用一台 x64 的真机 Ubuntu20,arm64 架构则使用 M1 Pro 搭建虚拟机环境(VM ware/UTM)Ubuntu20 来辅助开发调试。

其他验证环境:

  • 虚拟机环境:Ubuntu18、Redhat、Kylin,UOS 等。
  • M1 Pro 装双系统环境:Fedora。
  • Ubuntu:https://cdimage.ubuntu.com/jammy/daily-live/current/ 下载对应版本。
  • fedora:https://asahilinux.org/
  • 其他的 iso,可以在对应的官方网站下载。

2.3.5 开发环境

  • 编译依赖:CMake、GCC、Clang(最后会切到 Clang)、ar 等。
  • 开发工具:VSCode、Clion、git、apt 等。
  • 开发环境基本上准备好了,比如 apt 安装各个依赖的 dev 库,编译工具、调试环境配置等。

2.3.6 跨平台开发架构

我们在其他平台都通过音视频自回环 Demo 可以快速且轻量模拟音视频通话场景,验证功能,同样在 Linux 平台我们也通过建立轻量 Demo 来快速验证该平台的各项能力;

我们对 Linux 平台下可用的 GUI 开发框架做了个调研,对比了接入效率,最后选择了 QT 开发框架。

图片来自在 Linux 下开发 GUI 程序的方法 _linux 系统上的 gui-CSDN 博客

2.3.7 NTRTC 自回环的 demo

我们基于 QT6 实现了一个简单的 Demo,通过自回环的方式,验证音频、视频、传输通道等能力。

开发环境:QTLinux6.6.1, Qtcreator。

基于这个 Demo,我们可以提前在 Linux 平台验证音频、视频编解码能力;

从平台知识到开发环境基本上准备差不多了,接下来先介绍下桌面端音视频通话的的实现方案。

2.3.8 桌面版本 QQ 音视频通话方案

QQ(Electron) + PPAPI

新桌面 QQ 版本是基于 electron 进行开发的,对于 electron 的介绍可以直接看官网 https://www.electronjs.org/zh/docs/latest/tutorial/process-model。

electron 内置了一个 chromium 内核,新桌面 QQ 音视频通话就是基于 Pepper Plugin(PPAPI)方案实现的,这里简单对 PPAPI 组件做个介绍。

PPAPI 组件可以通过平台动态库的形式(Windows 下为 dll 文件,Linux 下是 so 文件, Mac 下是 dyllib 文件)由浏览器直接加载,比如内置的 Flash 组件、Pdf组件,或者通过指定命令行参数 --register-pepper-plugins 来加载,比如:chrome --register-pepper-plugins="D:\\ppapi\\ppapi_example_gles2.dll;application/x-ppapi-example-gles2" D:\\ppapi\\gles2.html。可信的 PPAPI 组件以平台动态库的形式存在,所以一般 Chrome 沙箱内允许的 API(比如 CreateThread)都可以调用。

Chromium 插件(Plugin)机制:https://blog.csdn.net/Luoshengyang/article/details/52665318

通过了解 PPAPI Plugin 我们可以了解到两个关键的点。

  1. 进程是通过 IPC 进行通讯的;
  2. Plugin 有沙箱机制(这里是重点,后面有坑);

AVSDK Plugin 注册

我们看下 AVSDKPlugin 的动态库是如何注册的。

  1. 不同平台区获取对应的动态库。
  2. 通过 register-pepper-plugins 注册到 electron app。

音视频通话相当于创建一个浏览器窗口,同时会拉起这个对应注册的P lugin,具体加载 Plugin 过程这里不做过多讨论,可以看这篇文章 Chromium 插件(Plugin)模块(Module)加载过程 https://blog.csdn.net/luoshengyang/article/details/52773402。

03、NTRTC-SDK For Linux

适配前,我们先看一下音视频 AVSDKPlugin 框架。

可以看到这个 AVSDKPlugin 实际上就是一个 PPAPI Plugin 仓库,它集合了 NTRTC、GroupVideo、BroadCast-Core 等 SDK,通过 Wrapp 层将它们串联起来,在包装成 PPAPI Plugin 实例对外提供音视频通话能力,直播能力;

对外提供的产物:可执行文件,资源文件,内置依赖库。

3.1 工程适配

受益于之前 CMake 的统一构建, QQ NT 的跨平台重构之旅-音视频全平台构建统一 本次对 Linux 平台的编译适配工作也顺利很多,主要处理下面几个事项。

  1. CMake 相关针对 Linux 平台增加一些平台逻辑,比如关闭某些编译特性,或者平台文件仅在 Linux 环境下编译;
  2. 业务逻辑适配,比如新增的平台 Type 兼容,平台基本信息等;
  3. 缺失的一些实现;
  4. Linux 平台下,各个第三方依赖库的编译,如视频编解码;

例:CMake 平台宏差异,可以增加不通的特性选项:

代码语言:javascript
复制
if(WIN32)
  # 设置 Windows 平台的特定选项
elseif(UNIX AND NOT APPLE AND NOT ANDROID)
  # 设置 Linux 平台的特定选项
elseif(APPLE)
  # 设置 macOS 平台的特定选项
endif()

BroadCast-Core 等其他依赖库

CMake 工程化

通过修复编译问题,或者重新编译需要的架构版本,过程中遇到了无源码的情况,或者找不到源码,那么只能通过屏蔽相关能力,或者移除该能力来解决。

3.2 编译&Demo

最开始编译使用的是 gcc 11.4.0,gcc 已经满足编译需求。

遇到的编译问题:

  • 有源码的,解决编译报错问题即可,主要体现在头文件没有引用,或者缺对应的实现;
  • 无源码的第三方库,也就是该平台下没有对应架构的库,需要整体重新编译即可;
  • fPIC 问题,编解码库 link 到动态库时出现 fPIC 错误。
代码语言:javascript
复制
/usr/bin/ld: ../../../qav_rtc_sdk/av_engine/android_ios_mac/Lib/Linux/x86_64/libTcH264Enc.a(cabac-a.asm.o): relocation R_X86_64_PC32 against symbol `g_kuiCabacRangeLps' can not be used when making a shared object; recompile with -fPIC

H264 编码和解码库在链接时报 fPIC 的问题,增加 -Bsymbolic 链接,关闭动态库 so 中默认的符号抢占方式,来绕过 fPIC 的检查。

  • 合并净态库

在输出 avsdk 静态库时,一般都会将各个子库进行合并,生成一个最终 qav_rtc_sdk.a,在 Linux 下没有类似 libtool、libexe 等工具,不过有个 ar 工具,可以达到合并的效果:

  1. 通过 ar x 提取静态库的所有.o文件。
  2. 在通过 ar crs 合并所有的.o 文件。
  3. 通过 ranlib 生成新的静态库索引。

但是合并后出现了问题,合并后,link 到 demo 时报错,符号缺失?符号丢了!但是通过 nm 查看子库的符号都是全的;

不同静态库,相同命名的.o

经过排查,发现使用 ar x 命令提取文件时,如果归档文件中存在多个同名文件,ar 会提取找到的第一个匹配项,这里一个库的内容出现相同的 .o 情况时,会出现覆盖问题,这里暂时没有好的 ar 可选项能快速解决这个问题的。

解决方法:那就通过逻辑解决,提取时,每个库都复制到独立临时目录,待归档目录内遇到重复命名的 .o 文件时,重命名这个 .o, 防止同名覆盖。

这个错误时机上是 ar 提取文件时,复制到待合并文件夹时环节出现的,是不同的静态库有相同命名的 .o 文件,通过重命名,还比较好解决;

同一个静态库,相同命名的 .o

解决了 .o 覆盖的问题,再次 link,还是缺失符号,通过排查还是丢了对应的符号,再次排查哪一步丢的,我们发现一个静态库内出现相同命名的 .o 符号段,两个符号段在不同位置,ar x 提取时,会优先命中第一个搜索到的 .o 段,后面遇到的都会忽略,这就棘手了,是工具提取环节出现的丢失,排查了一些 ar 选项没有解决;

解决方法:通过修改该静态库内相同源文件命名解决;

Demo Link & Demo Run

经过上面2个方面的适配,解决一系列link问题后,较顺利的输出了 x64 版本的 qav_rtc_sdk.a。

我们通过之前提到的 qt_demo, 进行 link 验证,也没有问题,自回环的逻辑也正常跑起来,基于 QT 开发环境也可以正常调试,此时音频、视频能力可以先开始验证。

AVSDKPlugin & electron demo

我们输出了 x64 版本的 AVSDKPlugin.so,搭建了一个 electron demo,用于验证我们的动态库是否可以正常运行;

这里需要在 Linux 安装 electron 环境,具体看 Electron Quick Start。

然后我们遇到了第一个问题:动态库拉不起来!

错误信息:182204.991288: ERROR:ppapi_thread.cc(269) Failed to load Pepper module from ~/robert/AVSDKPluginDemo/app/avsdk/libAVSDKPlugin.so(error cannot open shared object file: Operation not permitted)

通过错误信息,我们大致能看出来是权限问题,首先通过确认,排除了 so 文件路径错误的问题,那就是权限问题。

还记得上面介绍 pepper plugin 时的沙箱问题吗,没错,就是这个,electron app 默认是开启沙箱模式的,也就是说 app 住进程是开启沙箱的,住进程通过 fork 方式拉起的进程都会带沙箱模式;

既然知道了什么原因,我们暂时先关闭 electron app 的沙箱模式,后面这个问题通过修改 electron 源码来解决;

运行 Electron Demo,Electron 新创建了一个浏览器窗口,并且通过 Pepper Plugin 方式,拉起音视频进程加载了 AVSDKPlugin.so,well done!路通了。

3.3 调试

QT Demo Debug

首先我们通过 QT 开发环境对运行的 demo app 直接进行调试。

demo link 了 qav_ntrtc_sdk.a , 使用了 xpstl::list 做了一些操作。

通过断点,我们可以方便的进行调试,这是基于直接运行 app 做的操作;

那么如何调试 electron app + plugin 拉起的 AVSDKPlugin.so 呢?

此时有经验的同学会想到挂载进程调试,没错,我们此时也可以通过挂载进程调试正在运行的音视频进程。

音视频进程 AVSDKPlugin.so 调试(CLion 挂载进程调试)。

打开 CLion->Run->Attach To Process>选择对应进程,确定。

调试:正常在 CLion 打断点即可。

注意:需要是 Debug 版本的动态库。

demo 拉起 avsdk 各个线程。

通过 log,我们也可以看到输出。

【问题】Linux 挂载进程失败,提示没权限。

这里是 Linux 系统有个权限问题,按 GPT 给出的解决方案,修改一下重启电脑生效。

3.4 GLIBC、GLIBCXX 运行依赖

GLIBC 和 GLIBC++ 是两个不同的库,它们在 Linux 系统中扮演着重要的角色。

GLIBC

GLIBC,全称 GNU C Library,是 GNU 项目的 C 标准库的实现,为系统和应用程序提供了系统调用的封装和许多基本的程序接口。这包括输入输出(I/O)、字符串处理、文件操作、内存管理、数学计算等。GLIBC 是大多数基于 Linux 的系统的标准 C 库,并且是编译大多数 C 程序的必要组件。

GLIBC 的版本很重要,因为不同的应用程序可能需要不同版本的 GLIBC。例如,一个用较新版本的 GLIBC 编译的程序可能无法在只有较旧版本 GLIBC 的系统上运行。

GLIBC++

GLIBC++ 是 GNU libstdc++ 库的常见称呼,它是 C++ 标准库的 GNU 实现。它提供了 C++ 程序所需的标准功能,包括输入输出流(iostream)、数据结构(如 STL 容器)、算法、字符串处理等。当你编译 C++ 程序时,通常需要链接到 libstdc++ 库。

与 GLIBC 类似,不同版本的 GNU libstdc++ 支持不同版本的 C++ 标准。例如,较新版本的 libstdc++ 支持 C++11、C++14、C++17 和 C++20 的新特性。

### 版本查询和兼容性,在 Linux 系统中,你可以通过运行以下命令来查询 GLIBC 和 GLIBC++ 的版本:

  • 对于 GLIBC,可以使用 `ldd --version` 或 `libc.so.6` 文件来查询:
代码语言:javascript
复制
ldd --version
# 或者
/lib/x86_64-linux-gnu/libc.so.6
  • 对于 GLIBC++,可以通过检查 libstdc++ 库的版本来查询:
代码语言:javascript
复制
strings /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep GLIBCXX

兼容性通常是向后的,这意味着用旧版本的 GLIBC 或 GLIBC++ 编译的程序应该能在有较新版本库的系统上运行。然而,反过来通常不行,因为旧版本的库不包含新版本中引入的符号和功能。

在输出我们编译好的 AVSDKPlugin 后,在 Ubuntu20、22上正常运行起来,但是我们发现。

AVSDKPlugin.so 放到不同 Linux 版本上运行时,比如 Ubuntu 18、Fedora 23、Qlin 等系统上,发现音视频拉不起来??

通过ldd AVSDKPlugin.so 我们发现出现一些依赖库 no found, 或者 GLIBC need 2.29等错误信息。

这个是 Ubuntu 18(x64)的报错:

代码语言:javascript
复制
robert@ubuntu:~/.config/QQ/global/ext_lib/avsdk$ ldd libAVSDKPlugin.so 
./libAVSDKPlugin.so: /lib/x86_64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.29' not found (required by ./libAVSDKPlugin.so)
./libAVSDKPlugin.so: /lib/x86_64-linux-gnu/libstdc++.so.6: version `CXXABI_1.3.13' not found (required by ./libAVSDKPlugin.so)

在 KylinOS(麒麟) arm64 系统错误信息。

这表明我们依赖的库使用了较高版本的 GLIBC 编译,在低 GLIBC 版本的系统上无法运行!

我们要确定两个信息:

  1. 编译时使用的 GUN C Library(libc.so) 支持的 GLIBC 版本;
  2. 运行环境的 libc.so 支持的 GLIBC 版本;

要满足 编译输出的产物依赖的 GLIBC 版本,小于运行环境的 libc 支持的 GLIBC 版本,才能正常运行;

查看一下我们依赖的 GLIBC 版本,终端输入:

代码语言:javascript
复制
strings libAVSDKPlugin.so | grep GLIBC
代码语言:javascript
复制
GLIBC_2.3
GLIBC_2.3.3
GLIBC_2.27
GLIBC_2.29
GLIBC_2.2.5
... 省略
GLIBC_2.17
GLIBC_2.4
GLIBC_2.3.2
GLIBC_2.7
GLIBC_2.12

通过输出的信息,我们知道我们在 Ubuntu 20,x64 环境,使用 GCC 10.5 编译输出的产物,最低支持 GLIBC2.29, 也就是运行环境需要有 GLIBC 2.29,但上面 Ubuntu18、跟 KylinOS 环境的 GLIBC 版本都太低了,无法运行我们的动态库,那怎么办呢?

上面提到了,avsdk、avsdkplugin 都是使用 gcc11.4 进行编译的,使用的系统是 Ubuntu20。

我们通过 strings /usr/lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC 来查看 GLIBC 的版本信息。

代码语言:javascript
复制
robert@robert-LC0:~$ strings /usr/lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC
GLIBC_2.2.5
GLIBC_2.2.6
GLIBC_2.3
...省略
GLIBC_2.17
...省略
GLIBC_2.27
GLIBC_2.28
GLIBC_2.29
GLIBC_2.30
GLIBC_PRIVATE
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.14) stable release version 2.31.

而上面运行环境没有达到 AVSDKPlugin 依赖的 GLIBC 需要支持2.29,我们编译使用的 libc++ 版本太高了,那就就要想办法降级。

GCC 10.5

我们想到的是通过降低编译工具版本来解决,我们尝试使用 gcc 10.5,修复了一些编译问题,输出的产物还是依赖较高的 GLIBC 版本,我们通过排查接口,发现是数学库的一些相关调用.

代码语言:javascript
复制
./libAVSDKPlugin.so: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found (required by ./libAVSDKPlugin.so)

虽然降级了编译工具版本,但实际上 link 的还是当前系统目录的 libc, 或者 libm。

一般这种情况,我们就要通过使用低版本的编译工具链(使用指定的低版本的库)。

通用的做法就是准备好相关编译工具链文件,然后通过自定义依赖库搜索路径来使用工具链的依赖库进行编译。

构建工具链:buildtools & Clang

通过跟NTKernel的同学沟通,得知Kernel编译使用了一套构建工具,支持x64、arm64、loong64、mips64el。

使用的编译器是 Clang,我们尝试使用该构建工具,配置好 toolchan.cmake, 在编译时发现缺失了。

experiemental::coroutine undefine

代码语言:javascript
复制
xplatform-ng/xpng/task/coroutine/task.h:31:30: fatal error: use of undeclared identifier 'experimental'
    using coroutine_handle = experimental::coroutine_handle<T>;

这里 coroutine 是 c++20 的特性,cmake配置下从 c++17 升级到 c++20 即。

filesystem 相关符号缺失

代码语言:javascript
复制
ld.lld: error: undefined symbol: std::Cr::__fs::filesystem::__file_size(std::Cr::__fs::filesystem::path const&, std::Cr::error_code*)
>>> referenced by operations.h:108 (/home/robert/buildtools/toolchain/../libcxx/include/__filesystem/operations.h:108)

我们发现 std::Cr::__fs::filesystem, 发现是构建工具链中没有相关实现。

需要构建工具链内的 libc++.a 增加 systemfile 的实现编译。

可以参考 https://libcxx.llvm.org/BuildingLibcxx.html, 编译出对应的 filesystem 版本即可。

1、内置

对于缺失的依赖库,我们可以内置到安装目录即可,通过 patchelf 指定搜索目录,可以设置搜索路径查找优先级,先搜索自定义目录,在搜索系统路径,如图:

2、提示安装

我们尝试内置 OpenGL 库解决运行环境 OpenGL 库缺失的问题,但是通过测试下来,在不同的系统环境运行,会出现各种 OpenGL 兼容性的 crash 问题,有些情况通过运行环境安装的默认 OpenGL 是好的。

尝试过通过 patchelf 配置搜索路径优先级, 先搜索系统路径,如:/usr/lib/x86_64-linux-gnu , 在搜索安装目录,来解决。

但这也确实使用了内置 OpenGL 库,直接 crash,整体体验上更差,还不如早一点检测依赖,暴露问题,引导用户安装。

这个提示比较粗暴,后续会优化。

最后针对 Linux 底层库的支持,音视频 GLIBC 低版本支持情况:x64 2.17+, arm64 2.29+

3.5 Electron 的修改

electron 的相关介绍可以去官网看下 Electron。

electron的是一个开源项目,可以自行编译 electron 版本来满足自己产品的需求。

构建可以参考这个 构建 electron。

对于 electron,qq 桌面端的 electron 实际上自己编译的,也做了一些优化跟定制,本次 Linux 适配我们也做了一些修改。

沙盒问题

chromium 有它自己管理的一套沙盒机制,在前面我们有提过。

QQ Electron App 的主进程是开启沙盒的,那么通过主进程 fork 方式拉起来的进程都会继承主进程的配置。

例:

代码语言:javascript
复制
/opt/QQ/qq --type=renderer --crashpad-handler-pid=5273 --enable-crash-reporter=bc2ad366-d1b0-4f89-8bb4-e34227773324,no_channel --user-data-dir=/home/haier/.config/QQ --standard-schemes=app --secure-schemes=app --bypasscsp-schemes --cors-schemes --fetch-schemes=app --service-worker-schemes --streaming-schemes --app-path=/opt/QQ/resources/app --enable-sandbox --allow-command-line-plugins --force-color-profile=srgb --register-pepper-plugins=/opt/QQ/resources/app/avsdk/libAVSDKPlugin.so;application/x-ppapi-avSDK --js-flags=--expose-gc --disable-gpu-compositing --lang=zh-CN --num-raster-threads=4 --enable-main-frame-before-activation --renderer-client-id=7 --time-ticks-at-unix-epoch=-1713183163571621 --launch-time-ticks=66654460 --shared-files=v8_context_snapshot_data:100 --field-trial-handle=0,i,12981793670346963750,3504886440467676680,262144 --enable-features=kWebSQLAccess --disable-features=SpareRendererForSitePerProcess --variations-seed-version

那么我们要在拉起子进程时不开启沙盒如何做呢?

代码语言:javascript
复制
/content/browser/child_process_launcher_helper.cc 
void ChildProcessLauncherHelper::LaunchOnLauncherThread() {
   int launch_result = LAUNCH_RESULT_FAILURE;
   absl::optional<base::LaunchOptions> options;
   base::LaunchOptions* options_ptr = nullptr;
   if (IsUsingLaunchOptions() || GetProcessType() == switches::kPpapiPluginProcess) {
     options.emplace();
     options_ptr = &*options;
   }

/content/browser/child_process_launcher_helper_linux.cc
bool ChildProcessLauncherHelper::BeforeLaunchOnLauncherThread(
     PosixFileDescriptorInfo& files_to_register,
     base::LaunchOptions* options) {
   if (options) {
     DCHECK(!GetZygoteForLaunch() || GetProcessType() == switches::kPpapiPluginProcess);
     // Convert FD mapping to FileHandleMappingVector
     options->fds_to_remap = files_to_register.GetMappingWithIDAdjustment(
         base::GlobalDescriptors::kBaseDescriptor);

ChildProcessLauncherHelper::LaunchProcessOnLauncherThread(
   *is_synchronous_launch = true;
   Process process;
   ZygoteCommunication* zygote_handle = GetZygoteForLaunch();
   if (zygote_handle && GetProcessType() != switches::kPpapiPluginProcess) {
     // TODO(crbug.com/569191): If chrome supported multiple zygotes they could
     // be created lazily here, or in the delegate GetZygote() implementations.
     // Additionally, the delegate could provide a UseGenericZygote() method.

我们针对 ppapi 进程修改,来关闭ppapi进程的沙盒模式选项,让 ppapi 进程不开沙盒模式,当然这里可能会有一些安全隐患,后面看下是否有更好的方案解决。

Crash due to FD ownership

代码语言:javascript
复制
Crashing due to FD ownership violation:
#1 0x5595aafa4eec <unknown>
#0 0x5595aafabe73 <unknown>
#2 0x5595aafa4ea7 close
#3 0x7fc8275dc27b <unknown>
#4 0x7fc82a8e6615 <unknown>

在测试过程中,我们发现通过 electron 拉起的 ppapi plugin 进程时,经常出现这个 crash,导致音视频功能经常不可用,通过报错信息,搜索到一些相关信息。

https://github.com/electron/electron/pull/40677 具体看算是 electron 的bug,找到推荐修改方式,https://source.chromium.org/chromium/chromium/src/+/main:base/files/scoped_file.h;l=66-82;drc=e4622aaeccea84652488d1822c28c78b7115684f 这里官方的说法是重置所有权。

实际上通过代码排查,我们发现这个 FD owner 检查 crash,实际上是 electron 的一个特性逻辑,我们在 content/app/content_main.cc 看到,electron app 在 Linux 平台下是开启了这个 FD Ownership 检查的,那这里我们就尝试将它关闭,是不是就可以解决了。

通过修改 electron 源码,重新编译 electron,该问题得到解决;

electron 相关技巧编译

electron app 实际上就是 chromium 浏览器环境的一个 app,对于浏览器支持的选项大部分都支持,包括一些调试选项。

在启动 electron app 加启动参数就行,实际上属于 web 前端的技术栈,我找到一个不错的 blog,页面挺好看的。

Chrome浏览器启动参数大全(命令行参数):https://www.cnblogs.com/gurenyumao/p/14721035.html

例:开启更多的 log 信息。

代码语言:javascript
复制
https://www.chromium.org/for-testers/enable-logging/
#控制台启动qq
qq --enable-logging=stderr --v=1

例:使用自己编译的 electron 版本运行 electron app。

直接替换可执行文件即可,比如 electron demo、qq 等,找到 electron 的可执行文件,替换成你的就好。

例:如何 debug electron。

挂载进程方式,方法通用,跟上面调试自回环 Demo 类似。

3.6 平台媒体硬件适配

音视频通话、直播都离不开音频、视频,相关的采集、渲染、编解码都与平台硬件息息相关。

从采集、渲染、编码、解码都会遇到一些问题;这里我就适配过程中,处理的一个视频渲染降级方案做一下分享。

视频通话渲染方案

我们先来看一下 Chromium Plugin 执行 3D 渲染的过程 的渲染过程。

在 Plugin 进程中,OpenGL 上下文通过 Graphics3D 类描述。因此,创建 OpenGL 上下文意味着是创建一个 Graphics3D 对象。这个 Graphics3D 对象在创建的过程中,会调用 PPB_GRAPHICS_3D_INTERFACE_1_0 接口提供的一个函数 Create。该函数又会通过一个 APP_ID_RESOURCE_CREATION 接口向 Render 进程发送一个类型为 PpapiHostMsg_PPBGraphics3D_Create 的 IPC 消息。在 Plugin 进程中,APP_ID_RESOURCE_CREATION 接口是通过一个 ResourceCreationProxy 对象实现的,因此,Plugin 进程实际上是通过 ResourceCreationProxy 类向 Render 进程发送一个类型为 PpapiHostMsg_PPBGraphics3D_Create 的 IPC 消息的。

Plugin 在初始化 OpenGL 环境的过程中做的第二件事情就是将刚刚创建出来的 OpenGL 上下文指定为当前要使用的 OpenGL 上下文。这个过程称为 OpenGL 上下文绑定,如图所示:

Chromium 插件(Plugin)执行 3D 渲染的过程分析 _plugin for 3d manipulation-CSDN 博客

音视频的渲染实际就是使用了 PPB_Graphics3D 的渲染方案,通过共享纹理来做夸进程渲染,在支持硬件加速的情况下。

Win 使用了 ID3D11Device、MacOS 使用了 Metal。

PPB_Graphics3D->Create 失败问题

在开发过程中,我们在一些虚拟机的 Linux 系统上发现视频渲染黑屏,通过排查 Log,我们发现以下信息。

具体对应到代码:

代码语言:javascript
复制
PP_Resource graphics =
      g_graphics_3d_interface->Create(g_pp_instance, 0, attributes);
if (!graphics){
    log = "avsdk output(wrapper): PP_Resource Create fail";
}

发现这个 PP_Resource(PPB_Graphics3D) 初始化失败了,这会导致视频无法渲染。

我们知道 Plugin 是通过 ppapi 跟 render 进程交互的, 这个创建过程实际就是发送一个创建资源 message 到 render 进程创建 3D 画布资源,我们要确定哪一步出错。

排查过程

1、确认环境、显卡驱动,我们发现在启动 QQ 后,有问题的环境会输出一些 warning 信息,跟显卡驱动相关

代码语言:javascript
复制
Warning: vkCreateInstance: Found no drivers!
Warning: vkCreateInstance failed with VK_ERROR_INCOMPATIBLE_DRIVER
    at CheckVkSuccessImpl (../../third_party/dawn/src/dawn/native/vulkan/VulkanError.cpp:101)

此时我们通过启动 qq 时增加。

代码语言:javascript
复制
qq --enable-logging=stderr --v=1

又多出一些信息。

代码语言:javascript
复制
[9364:0415/181411.892176:ERROR:gl_utils.cc(412)] [.WebGL-0x200029f800]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels
[9364:0415/181411.949701:ERROR:gl_utils.cc(412)] [.WebGL-0x200029f800]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels
[9364:0415/181411.976514:ERROR:gl_utils.cc(412)] [.WebGL-0x200029f800]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels
[9364:0415/181412.027489:ERROR:gl_utils.cc(412)] [.WebGL-0x200029f800]GL Driver Message (OpenGL, Performance, GL_CLOSE_PATH_NV, High): GPU stall due to ReadPixels (this message will no longer repeat)

可以看到在驱动出了一些警告,或者错误。

2、进程启动选项多出的 --disable-gpu-compositing 参数

我们发现在有问题的环境,在音视频进程启动时多了一个启动选项--disable-gpu-compositing。

通过排查这个不是我们业务增加的,也就是他是 chromium 通过当前系统环境自己加的选项,这个参数的作用是禁用 GPU(图形处理单元)合成,也就是它直接导致了 PPB_Graphics3D->Create 失败。

3、electron 源码分析

那么--disable-gpu-compositing 是如何添加到启动选项中的?

代码语言:javascript
复制
// Prevent the compositor from using its GPU implementation.
const char kDisableGpuCompositing[]         = "disable-gpu-compositing";

可以看到 gpu_data_manager_impl_private.cc 里面的实现,在 IsGpuCompositingDisabledS 时加了这个 disable-gpu-compositing.

代码语言:javascript
复制
#if BUILDFLAG(IS_ANDROID)
  if (browser_cmd.HasSwitch(switches::kDisableGpuCompositing)) {
    renderer_cmd->AppendSwitch(switches::kDisableGpuCompositing);
  }
#elif !BUILDFLAG(IS_CHROMEOS_ASH)
  // If gpu compositing is not being used, tell the renderer at startup. This
  // is inherently racey, as it may change while the renderer is being
  // launched, but the renderer will hear about the correct state eventually.
  // This optimizes the common case to avoid wasted work.
  if (GpuDataManagerImpl::GetInstance()->IsGpuCompositingDisabled())
    renderer_cmd->AppendSwitch(switches::kDisableGpuCompositing);
#endif  // BUILDFLAG(IS_ANDROID)

位于content/brower/gpu/gpu_data_manager_impl.h/.cc GpuDataManagerImpl::GetInstance->IsGpuCompositingDisabled().

代码语言:javascript
复制
bool GpuDataManagerImpl::IsGpuCompositingDisabledForHardwareGpu() const {
  base::AutoLock auto_lock(lock_);
  return private_->IsGpuCompositingDisabledForHardwareGpu();
}

可以看到实际访问了一个 private 对象,它在头文件这么定义的。

代码语言:javascript
复制
std::unique_ptr<GpuDataManagerImplPrivate> private_ GUARDED_BY(lock_)

GpuDataManagerImplPrivate位于content/brower/gpu/gpu_data_manager_impl_private.h/.cc

代码语言:javascript
复制
bool GpuDataManagerImplPrivate::IsGpuCompositingDisabled() const {
  return disable_gpu_compositing_ || !HardwareAccelerationEnabled();
}

这里看到它通过两个变量来决定是否关闭了 gpu 加速 disable_gpu_compositing_ 与 HardwareAccelerationEnabled() 变量不开启 gpu 加速 或者 硬件不支持 gpu 加速, 这里都返回 false,启动插件进程的cmd就会加上--disable-gpu-compositing。

那么 disable_gpu_compositing_逻辑 ,默认是 false, 默认会开启 gpu 加速。

看到唯一修改该变量值的就是 SetGpuCompositingDisabled 调用。

它调用了 IsGpuCompositingDisabled 逻辑,判断已经开启 gpu 加速的情况下,再去设置这个变量为 true,关闭 gpu 加速。

代码语言:javascript
复制
void GpuDataManagerImplPrivate::SetGpuCompositingDisabled() {
  if (!IsGpuCompositingDisabled()) {
    disable_gpu_compositing_ = true;
    if (gpu_feature_info_.IsInitialized())
      NotifyGpuInfoUpdate();
  }
}

我们看到它只有两处调用,一个是初始化 gpu_data_manager_impl_private,它判断了当前命令行是否加了--disable-gpu-compositing,如果加了,则调用 SetGpuCompositingDisabled。

这里我们确认主进程拉起来时不会带这个命令,子进程启动时也没有加,所以不是外部将这个 disable_gpu_compositing_=t rue 的, 它应该还是 false,我们接着看另一个硬件加速的逻辑。

HardwareAccelerationEnabled 中的逻辑,具体不展开了,实际上就是检查当前环境是否启用通过。

https://source.chromium.org/chromium/chromium/src/+/main:gpu/config/software_rendering_list.json 这个文件的白名单列表确定的。

我们通过修改 electron,增加 debug log 的方式验证我们的猜想。

代码语言:javascript
复制
VERBOSE1:gpu_control_list.cc(296)] Control list match for rule #3 in gpu_blocklist.
VERBOSE1:gpu_control_list.cc(296)] Control list match for rule #27 in gpu_blocklist.
VERBOSE1:gpu_control_list.cc(296)] Control list match for rule #28 in gpu_blocklist.
VERBOSE1:gpu_control_list.cc(296)] Control list match for rule #29 in gpu_blocklist.
VERBOSE1:gpu_control_list.cc(296)] Control list match for rule #50 in gpu_blocklist.
VERBOSE1:gpu_control_list.cc(296)] Control list match for rule #134 in gpu_blocklist.
VERBOSE1:gpu_control_list.cc(296)] Control list match for rule #176 in gpu_blocklist.

确实命中了,那么有什么办法可以绕过去这个检查呢?

4、qq electron 启动时增加选项 --ignore-gpu-blocklist

通过启动参数--ignore-gpu-blocklist 跳过检查逻辑,渲染与采集可以正常显示画面,但有以下几个问题。

  1. QQ 主 render 进程会花屏,或者显示异常;
  2. 音视频通话 render 进程,也会有花屏、绿屏、闪屏(在开启摄像头采集的情况);
  3. 某些系统开启摄像头采集过一段时间会 crash,目前怀疑是驱动问题;

QQ 主进程受到影响是我们不可接受的,它直接影响了用户在使用 QQ 的体验;

经过讨论,在不影响主进程的情况下,还要保证渲染正常,不能使用PPB_Graphics3D方案,降级到PPB_Graphics2D方案来代替,那么PPB_Graphics2D 实际上就是 RGB 的图片绘制,我们是如何实现的?

PPB_Graphics2D 渲染方案

考虑到 PPB_Graphics3D 渲染方案在 Linux 兼容性问题,目前很难解决。

讨论后,在有问题的环境下降级到 PPB_Graphics2D 方案。

  1. 音视频进程增加独立 OpenGL 上下文,新增离屏渲染流程,绘制后,复制出 rgba 数据给到 PPB_Graphics2D 上下文
  2. 使用 PPB_Graphics2D 进行渲染上屏;

流程图如下:

这套方案实际上是兜底方案,会在 PPB_Graphics3D 初始化失败的情况在降级到 PPB_Graphics2D。

存在的缺点:

  1. 增加了离屏渲染过程,会有内存、cpu 的增长;
  2. 2D 方案,是通过图片传递到 render 进程的,画布尺寸拉的越大,会有卡顿情况;
  3. 兼容性问题,一些渲染操作直接 crash 在驱动库里,如下图,要持续解决;

这些问题后续会持续优化。视频链路除了渲染环节,还有采集、传输、编解码环节,过程中都遇到了一些问题,音频链路适配也是困难重重,这些在这里不做过多叙述,后面团队的伙伴会单独分享。

04、总结

最后看一下 Linux 端通话效果:

过程是曲折的,有遇到难题卡了几天无法解决,也有现在还存在一些棘手的兼容性问题,但从0-1的感觉还是很不错的,后面我们会持续优化,遇到各种体验问题可以直接圈我。

Linux QQ 下载地址 https://im.qq.com/linuxqq/index.shtml

NTRTC Linux 后续的规划:

1、支持 loongarch64、mips64el 架构。

2、解决视频相关的兼容性问题。

在此,感谢团队伙伴大力支持!

-End-

原创作者|贺坤

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01、背景
  • 02、Linux 平台调研
  • 03、NTRTC-SDK For Linux
  • 04、总结
相关产品与服务
实时音视频
实时音视频(Tencent RTC)基于腾讯21年来在网络与音视频技术上的深度积累,以多人音视频通话和低延时互动直播两大场景化方案,通过腾讯云服务向开发者开放,致力于帮助开发者快速搭建低成本、低延时、高品质的音视频互动解决方案。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档