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

共 32356字,需浏览 65分钟

 ·

2024-06-18 08:43


👉目录


1 背景

2 Linux 平台调研

3 NTRTC-SDK For Linux

4 总结




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 平台宏差异,可以增加不通的特性选项:

    
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 错误。

   
/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` 文件来查询:

    
ldd --version# 或者/lib/x86_64-linux-gnu/libc.so.6


  • 对于 GLIBC++,可以通过检查 libstdc++ 库的版本来查询:
    
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)的报错:

    
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 版本,终端输入:

   
strings libAVSDKPlugin.so | grep GLIBC
   
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 的版本信息。

   
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 版本,我们通过排查接口,发现是数学库的一些相关调用.

   
./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

    
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 相关符号缺失

   
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 方式拉起来的进程都会继承主进程的配置。

例:

   
/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

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

    
/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.ccbool 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

   
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 信息。
   
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,我们发现以下信息。


具体对应到代码:
    
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 信息,跟显卡驱动相关

    
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 时增加。

    
qq --enable-logging=stderr --v=1

又多出一些信息

    
[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 是如何添加到启动选项中的?

    
// Prevent the compositor from using its GPU implementation.const char kDisableGpuCompositing[] = "disable-gpu-compositing";

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

    
#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().

    
bool GpuDataManagerImpl::IsGpuCompositingDisabledForHardwareGpu() const { base::AutoLock auto_lock(lock_); return private_->IsGpuCompositingDisabledForHardwareGpu();}

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

    
std::unique_ptr<GpuDataManagerImplPrivate> private_ GUARDED_BY(lock_)


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

    
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 加速。

    
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 的方式验证我们的猜想。


    
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-
原创作者|贺坤


大家对 Linux QQ 有什么体验心得?欢迎评论留言。我们将选取1则评论,送出腾讯云开发者定制眼罩1个(见下图)。6月25日中午12点开奖。

📢📢欢迎加入腾讯云开发者社群,享前沿资讯、大咖干货,找兴趣搭子,交同城好友,更有鹅厂招聘机会、限量周边好礼等你来~


(长按图片立即扫码)




浏览 547
7点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
7点赞
评论
收藏
分享

手机扫一扫分享

分享
举报