Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 499, comments - 13, trackbacks - 0, articles - 1

          本文由QQ音視頻團隊賀坤分享原題“Linux QQ能打語音視頻了!一文詳解背后技術實現!”,下文進行了排版和內容優化等。

          1、引言

          2024年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 實現音視頻通話能力的過程。

           
           

          2、系列文章

          本文是系列文章中的第12 篇,本系列總目錄如下:

          IM跨平臺技術學習(一):快速了解新一代跨平臺桌面技術——Electron

          IM跨平臺技術學習(二):Electron初體驗(快速開始、跨進程通信、打包、踩坑等)

          IM跨平臺技術學習(三):vivo的Electron技術棧選型、全方位實踐總結

          IM跨平臺技術學習(四):蘑菇街基于Electron開發IM客戶端的技術實踐

          IM跨平臺技術學習(五):融云基于Electron的IM跨平臺SDK改造實踐總結

          IM跨平臺技術學習(六):網易云信基于Electron的IM消息全文檢索技術實踐

          IM跨平臺技術學習(七):得物基于Electron開發客服IM桌面端的技術實踐

          IM跨平臺技術學習(八):新QQ桌面版為何選擇Electron作為跨端框架

          IM跨平臺技術學習(九):全面解密新QQ桌面版的Electron內存占用優化

          IM跨平臺技術學習(十):快速選型跨平臺框架Electron、Flutter、Tauri、React Native等

          IM跨平臺技術學習(十一):環信基于Electron打包WebIM桌面端的技術實踐

          IM跨平臺技術學習(十二):萬字長文詳解QQ Linux端實時音視頻背后的跨平臺實踐》(* 本文

          3、技術背景

          隨著新版 QQ 桌面端的上線,在網上得到了廣泛的討論,尤其 QQ For Linux 3.0 推出后,比之前的 Linux 版本有了突破性的改變。

          QQ For Linux 3.1 還不支持語音、視頻通話,音視頻通話作為基礎能力之一,適配 Linux 平臺,這將是一個從0-1的過程,非常值得期待。

          QQ 的音視頻通話能力是基于 AVSDK,在過去3年中,我們持續對 AVSDK 進行基礎架構重構,更新底層基礎庫, 對 AVSDK 持續優化。

          在2024年上半年,QQ 音視頻正式推出 NTRTC,全平臺(iOS/Android/MacOS/Windows/Linux)的支持是 NTRTC 的重要特性之一,本次 Linux 平臺的適配也是這次升級過程中重要的一環。

          Linux 平臺上的適配對我們來說是一個挑戰。

          一個全新的平臺,從以下思路開展:

          • 1)我們要對 Linux 平臺有個調研,包括平臺信息、開發環境等;
          • 2)針對 SDK 進行編譯適配,這將涉及到所有的代碼跟依賴庫;
          • 3)平臺媒體層適配,視頻、音頻鏈路的采集、渲染、編解碼等;
          • 4)新增終端的通話業務適配,這包括前后端的邏輯,比如新增的終端類型,通話流控控制等;
          • 5)發布部署等,如流水線搭建,版本管理。

          那么我們開始!

          4、Linux平臺介紹

          Linux 內核最初只是由芬蘭人林納斯·托瓦茲(Linus Torvalds)在赫爾辛基大學上學時出于個人愛好而編寫的。

          Linux 是一套免費使用和自由傳播的類 Unix 操作系統,是一個基于 POSIX 和 UNIX 的多用戶、多任務、支持多線程和多 CPU 的操作系統。

          5、主流Linux發行版

          Linux 發行版是由 Linux 內核以及各種軟件和工具組成的完整操作系統。由于 Linux 的開源特性,任何人都可以創建自己的 Linux 發行版。因此,目前存在著數百種不同的 Linux 發行版,每種發行版都有其特定的目標用戶和用途。

          以下是一些較為知名的 Linux 發行版:

          目前市面上較知名的發行版有:Ubuntu、RedHat、CentOS、Debian、Fedora、SuSE、OpenSUSE、Arch Linux、SolusOS、Kylin(麒麟),UOS(統信) ,還有騰訊開源的 OpenCloud OS。

          每個 Linux 發行版都有其特點和優勢,用戶可以根據自己的需求和偏好來選擇適合自己的發行版。

          本次適配也就是在上述的 Linux 發行版本上開發可運行的軟件。

          6、開發之前的補課

          在做開發前,我們要了解的信息有:開發環境、用戶運行環境,除了要確定 Linux 發行版(后面都統一使用 Linux 系統版本代替),還要考慮到硬件信息,比如不同 CPU 架構,GPU 信息。

          6.1運行環境

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

          系統架構:x64、arm64、loong64、mips64el。

          通過新桌面 QQ Linux 版本的分布數據,我們會優先適配 x64、arm64。

          6.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 的軟件包格式。

          6.3靜態庫、動態庫

          在 SDK 開發中,我們交付的會根據不同平臺,App 不同的使用方式提供 SDK 產物,也就是靜態庫或者動態庫。

          例如:

          • 1)Unix:qav_ntrtc_sdk.a 的靜態庫和 qav_ntrtc_sdk.so 的動態庫;
          • 2)Windows :qav_ntrtc_sdk.lib 的靜態庫和 qav_ntrtc_sdk.dll 的動態庫;
          • 3)macOS:qav_ntrtc_sdk.a 的靜態庫和 qav_ntrtc_sdk.dylib 的動態庫。

          這些只是常見的命名約定,實際上,庫文件的命名可能會因編譯器、開發環境和開發者的選擇而有所不同。

          這個比較重要,因為作為 sdk 提供方,需要對不同交付的產物有明確的了解,sdk 也會根據使用方案提供不同的產物。

          6.4開發環境

          上面提到的不同 Linux 發行版本,這次開發申請了一臺 PC 機(x64),安裝了 TLinux(Ubuntu 20.04.6)。

          主開發機使用一臺 x64 的真機 Ubuntu20,arm64 架構則使用 M1 Pro 搭建虛擬機環境(VM ware/UTM)Ubuntu20 來輔助開發調試。

          其他驗證環境:

          6.5環境工具

          比如:

          • 1)編譯依賴:CMake、GCC、Clang(最后會切到 Clang)、ar 等;
          • 2)開發工具:VSCode、Clion、git、apt 等;
          • 3)開發環境基本上準備好了,比如 apt 安裝各個依賴的 dev 庫,編譯工具、調試環境配置等。

          6.6跨平臺開發架構

          我們在其他平臺都通過音視頻自回環 Demo 可以快速且輕量模擬音視頻通話場景,驗證功能,同樣在 Linux 平臺我們也通過建立輕量 Demo 來快速驗證該平臺的各項能力。

          我們對 Linux 平臺下可用的 GUI 開發框架做了個調研,對比了接入效率,最后選擇了 QT 開發框架。

          6.7NTRTC 自回環的 demo

          我們基于 QT6 實現了一個簡單的 Demo,通過自回環的方式,驗證音頻、視頻、傳輸通道等能力。

          開發環境:QTLinux6.6.1, Qtcreator。

          基于這個 Demo,我們可以提前在 Linux 平臺驗證音頻、視頻編解碼能力。

          從平臺知識到開發環境基本上準備差不多了,接下來先介紹下桌面端音視頻通話的的實現方案。

          6.8桌面版本 QQ 音視頻通話方案

          1)QQ(Electron) + PPAPI:

          新桌面 QQ 版本是基于 electron 進行開發的(詳見《新QQ桌面版為何選擇Electron作為跨端框架》),對于 electron 的介紹可以直接看官網。

          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 有沙箱機制(這里是重點,后面有坑);

          2)AVSDK Plugin 注冊:

          我們看下 AVSDKPlugin 的動態庫是如何注冊的:

          • 1)不同平臺區獲取對應的動態庫;
          • 2)通過 register-pepper-plugins 注冊到 electron app。

          音視頻通話相當于創建一個瀏覽器窗口,同時會拉起這個對應注冊的Plugin,具體加載 Plugin 過程這里不做過多討論,可以看這篇文章 Chromium 插件(Plugin)模塊(Module)加載過程。

          7、NTRTC-SDK For Linux 工程

          適配前,我們先看一下音視頻 AVSDKPlugin 框架。

          可以看到這個 AVSDKPlugin 實際上就是一個 PPAPI Plugin 倉庫,它集合了 NTRTC、GroupVideo、BroadCast-Core 等 SDK,通過 Wrapp 層將它們串聯起來,在包裝成 PPAPI Plugin 實例對外提供音視頻通話能力,直播能力。

          對外提供的產物:可執行文件,資源文件,內置依賴庫。

          8、工程適配

          受益于之前 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 工程化:通過修復編譯問題,或者重新編譯需要的架構版本,過程中遇到了無源碼的情況,或者找不到源碼,那么只能通過屏蔽相關能力,或者移除該能力來解決。

          9、工程編譯和Demo

          最開始編譯使用的是  gcc 11.4.0,gcc 已經滿足編譯需求。

          遇到的編譯問題:

          • 1)有源碼的,解決編譯報錯問題即可,主要體現在頭文件沒有引用,或者缺對應的實現;
          • 2)無源碼的第三方庫,也就是該平臺下沒有對應架構的庫,需要整體重新編譯即可;
          • 3)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 查看子庫的符號都是全的。

          1)不同靜態庫,相同命名的.o:

          經過排查,發現使用 ar x 命令提取文件時,如果歸檔文件中存在多個同名文件,ar 會提取找到的第一個匹配項,這里一個庫的內容出現相同的 .o 情況時,會出現覆蓋問題,這里暫時沒有好的 ar 可選項能快速解決這個問題的。

          解決方法:那就通過邏輯解決,提取時,每個庫都復制到獨立臨時目錄,待歸檔目錄內遇到重復命名的 .o 文件時,重命名這個 .o, 防止同名覆蓋。

          這個錯誤時機上是 ar 提取文件時,復制到待合并文件夾時環節出現的,是不同的靜態庫有相同命名的 .o 文件,通過重命名,還比較好解決;

          2)同一個靜態庫,相同命名的 .o:

          解決了 .o 覆蓋的問題,再次 link,還是缺失符號,通過排查還是丟了對應的符號,再次排查哪一步丟的,我們發現一個靜態庫內出現相同命名的 .o 符號段,兩個符號段在不同位置,ar x 提取時,會優先命中第一個搜索到的 .o 段,后面遇到的都會忽略,這就棘手了,是工具提取環節出現的丟失,排查了一些 ar 選項沒有解決;

          解決方法:通過修改該靜態庫內相同源文件命名解決。

          3)Demo Link & Demo Run:

          經過上面2個方面的適配,解決一系列link問題后,較順利的輸出了 x64 版本的 qav_rtc_sdk.a。

          我們通過之前提到的 qt_demo, 進行 link 驗證,也沒有問題,自回環的邏輯也正常跑起來,基于 QT 開發環境也可以正常調試,此時音頻、視頻能力可以先開始驗證。

          4)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!路通了。

          10、工程調試

          QT Demo Debug。

          首先我們通過 QT 開發環境對運行的 demo app 直接進行調試。

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

           

          通過斷點,我們可以方便的進行調試,這是基于直接運行 app 做的操作;

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

          此時有經驗的同學會想到掛載進程調試,沒錯,我們此時也可以通過掛載進程調試正在運行的音視頻進程。

          音視頻進程 AVSDKPlugin.so 調試(CLion 掛載進程調試):

          • 1)打開 CLion->Run->Attach To Process>選擇對應進程,確定;
          • 2)調試:正常在 CLion 打斷點即可;
          • 3)注意:需要是 Debug 版本的動態庫。

          demo 拉起 avsdk 各個線程:

          通過 log,我們也可以看到輸出:

          【問題】Linux 掛載進程失敗,提示沒權限:

           

          這里是 Linux 系統有個權限問題,按 GPT 給出的解決方案,修改一下重啟電腦生效。

          11、 GLIBC、GLIBCXX 運行依賴

          GLIBC 和 GLIBC++ 是兩個不同的庫,它們在 Linux 系統中扮演著重要的角色。

          1)GLIBC:

          GLIBC,全稱 GNU C Library,是 GNU 項目的 C 標準庫的實現,為系統和應用程序提供了系統調用的封裝和許多基本的程序接口。這包括輸入輸出(I/O)、字符串處理、文件操作、內存管理、數學計算等。GLIBC 是大多數基于 Linux 的系統的標準 C 庫,并且是編譯大多數 C 程序的必要組件。

          GLIBC 的版本很重要,因為不同的應用程序可能需要不同版本的 GLIBC。例如,一個用較新版本的 GLIBC 編譯的程序可能無法在只有較舊版本 GLIBC 的系統上運行。

          2)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++ 庫的版本來查詢:

          1strings /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++ 版本太高了,那就就要想辦法降級。

          3)GCC 10.5:

          我們想到的是通過降低編譯工具版本來解決,我們嘗試使用 gcc 10.5,修復了一些編譯問題,輸出的產物還是依賴較高的 GLIBC 版本,我們通過排查接口,發現是數學庫的一些相關調用。

          1./libAVSDKPlugin.so: /lib/x86_64-linux-gnu/libm.so.6: version `GLIBC_2.29' not found (required by ./libAVSDKPlugin.so)

          雖然降級了編譯工具版本,但實際上 link 的還是當前系統目錄的 libc, 或者 libm。

          一般這種情況,我們就要通過使用低版本的編譯工具鏈(使用指定的低版本的庫)。

          通用的做法就是準備好相關編譯工具鏈文件,然后通過自定義依賴庫搜索路徑來使用工具鏈的依賴庫進行編譯。

          4)構建工具鏈:buildtools & Clang:

          通過跟NTKernel的同學溝通,得知Kernel編譯使用了一套構建工具,支持x64、arm64、loong64、mips64el。

          使用的編譯器是 Clang,我們嘗試使用該構建工具,配置好 toolchan.cmake,在編譯時發現缺失了。

          5)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 即。

          6)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 版本即可。

          7)內置:

          對于缺失的依賴庫,我們可以內置到安裝目錄即可,通過 patchelf 指定搜索目錄,可以設置搜索路徑查找優先級,先搜索自定義目錄,在搜索系統路徑,如下圖所示。

          8)提示安裝:

          我們嘗試內置 OpenGL 庫解決運行環境 OpenGL 庫缺失的問題,但是通過測試下來,在不同的系統環境運行,會出現各種 OpenGL 兼容性的 crash 問題,有些情況通過運行環境安裝的默認 OpenGL 是好的。

          嘗試過通過 patchelf 配置搜索路徑優先級, 先搜索系統路徑,如:/usr/lib/x86_64-linux-gnu , 在搜索安裝目錄,來解決。

          但這也確實使用了內置 OpenGL 庫,直接 crash,整體體驗上更差,還不如早一點檢測依賴,暴露問題,引導用戶安裝。

          這個提示比較粗暴,后續會優化。

          最后針對 Linux 底層庫的支持,音視頻 GLIBC 低版本支持情況:x64 2.17+, arm64 2.29+ 。

          12、 Electron 的修改

          12.1概述

          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.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 進程不開沙盒模式,當然這里可能會有一些安全隱患,后面看下是否有更好的方案解決。

          12.2Crash  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/chro ... 1822c28c78b7115684f 這里官方的說法是重置所有權。

          實際上通過代碼排查,我們發現這個 FD owner 檢查 crash,實際上是 electron 的一個特性邏輯,我們在 content/app/content_main.cc 看到,electron app 在 Linux 平臺下是開啟了這個 FD Ownership 檢查的,那這里我們就嘗試將它關閉,是不是就可以解決了。

          通過修改 electron 源碼,重新編譯 electron,該問題得到解決。

          12.3electron 相關技巧編譯

          electron app 實際上就是 chromium 瀏覽器環境的一個 app,對于瀏覽器支持的選項大部分都支持,包括一些調試選項。

          在啟動 electron app 加啟動參數就行,實際上屬于 web 前端的技術棧,我找到一個不錯的 blog,頁面挺好看的。

          Chrome瀏覽器啟動參數大全(命令行參數):https://www.cnblogs.com/gurenyumao/p/14721035.html。

          例如開啟更多的 log 信息:參考鏈接

          #控制臺啟動qq

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

          例如使用自己編譯的 electron 版本運行 electron app:直接替換可執行文件即可,比如 electron demo、qq 等,找到 electron 的可執行文件,替換成你的就好。

          例如如何 debug electron:掛載進程方式,方法通用,跟上面調試自回環 Demo 類似。

          13、平臺媒體硬件適配

          音視頻通話、直播都離不開音頻、視頻,相關的采集、渲染、編解碼都與平臺硬件息息相關。從采集、渲染、編碼、解碼都會遇到一些問題。這里我就適配過程中,處理的一個視頻渲染降級方案做一下分享。

          13.1視頻通話渲染方案

          我們先來看一下 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 上下文綁定,如下圖所示。

          音視頻的渲染實際就是使用了 PPB_Graphics3D 的渲染方案,通過共享紋理來做夸進程渲染,在支持硬件加速的情況下。

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

          13.2PPB_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 畫布資源,我們要確定哪一步出錯。

          13.3排查過程

          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 時增加:

          1qq --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 對象,它在頭文件這么定義的。

          1std::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_=true 的, 它應該還是 false,我們接著看另一個硬件加速的邏輯。

          HardwareAccelerationEnabled中的邏輯,具體不展開了,實際上就是檢查當前環境是否啟用通過。

          https://source.chromium.org/chro ... 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 的圖片繪制,我們是如何實現的?

          13.4PPB_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 在驅動庫里,如下圖,要持續解決。

          這些問題后續會持續優化。視頻鏈路除了渲染環節,還有采集、傳輸、編解碼環節,過程中都遇到了一些問題,音頻鏈路適配也是困難重重,這些在這里不做過多敘述,后面團隊的伙伴會單獨分享。

          14、 本文小結

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

          -------過程是曲折的,有遇到難題卡了幾天無法解決,也有現在還存在一些棘手的兼容性問題,但從0-1的感覺還是很不錯的,后面我們會持續優化,遇到各種體驗問題可以直接圈我。

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

          NTRTC Linux 后續的規劃:

          • 1)支持 loongarch64、mips64el 架構;
          • 2)解決視頻相關的兼容性問題。

          在此,感謝團隊伙伴大力支持!

          15、 相關資料

          [1] 快速了解新一代跨平臺桌面技術——Electron

          [2] Electron初體驗(快速開始、跨進程通信、打包、踩坑等)

          [3] vivo的Electron技術棧選型、全方位實踐總結

          [4] 融云基于Electron的IM跨平臺SDK改造實踐總結

          [5] 閑魚IM基于Flutter的移動端跨端改造實踐

          [6] 網易云信基于Electron的IM消息全文檢索技術實踐

          [7] 即時通訊音視頻開發(十六):移動端實時音視頻開發的幾個建議

          [8] 福利貼:最全實時音視頻開發要用到的開源工程匯總

          [9] 寫給小白的實時音視頻技術入門提綱

          [10] 零基礎入門:實時音視頻技術基礎知識全面盤點

          [11] 實時音視頻面視必備:快速掌握11個視頻技術相關的基礎概念

          [12] 零基礎快速入門WebRTC:基本概念、關鍵技術、與WebSocket的區別等

          [13] 新QQ桌面版為何選擇Electron作為跨端框架

          (本文已同步發布于:http://www.52im.net/thread-4671-1-1.html



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 抚远县| 洛南县| 互助| 亚东县| 陆良县| 博客| 萨迦县| 阳谷县| 日照市| 台北市| 连云港市| 景泰县| 平陆县| 泰来县| 旺苍县| 保山市| 枣阳市| 罗城| 高雄县| 洛宁县| 鹿邑县| 临邑县| 隆化县| 焉耆| 武强县| 民勤县| 三门县| 扎兰屯市| 八宿县| 德化县| 凤翔县| 进贤县| 江华| 义马市| 万荣县| 南平市| 桐柏县| 浏阳市| 资溪县| 凤阳县| 贵阳市|