WebRTC C++ native 開發 Linux 與 MacOS 開發


1. 項目說明

該項目基於Qt5.9,使用舊版的WebRTC進行開發。將WebRTC編譯成靜態庫,提取頭文件整合至Qt工程中,然后編寫代碼,調用WebRTC的接口完成和Janus的sdp、offer等的信息交換,從而建立一個SFU的架構,完成多人音視頻通話的功能。

對於 MacOS 以及 Java 平台,WebRTC 同時會編譯出相關平台的 framework,該項目不是基於這些二次封裝的 framework,而是基於原生C++接口的純C++的native開發。20年8月初的時候就有一本書是寫 WebRTC native 開發的,實際上是基於 framework 的 native 開發。

  該項目最終目的不是構建一個可用的開源產品,而是通過利用各種現有資源,實現工程化的快速模型,學習它的工作流程。最終也達到了目的,通過已有的代碼跑通一個快速模型,實現了和 Janus 網頁端的音視頻交互。另外,熟悉了WebRTC的項目結構,為以后的深入學習打下了基礎。

  搭建Janus服務器,經過多次搭建,已經寫成了腳本化的執行流程。不作為本文的主要內容。

  該項目分兩部分:一部分是Linux平台的音視頻通話,另一部分就是移植到Mac平台。后來發現,移植到Mac平台的操作可能有些不必要,詳細的原因后面再解釋。

2. 要點

代碼的下載、編譯等不作為重點講解,此處乃是八仙過海,各顯神通的地方,每個人有每個人自己的方法。

同時,該項目主要是解決一系列的工程問題,例如環境搭建、Qt的項目整合、軟件框架的整體認識、WebRTC開發環境的熟悉。萬里之行,始於快速模型。

  截止文章記錄之時,Linux的環境已不方便使用,下面就以Mac的環境來做記錄,代碼結構如下所示:

    

 

當使用最新版時,主要遇到的問題是API不匹配,且在Mac上跑不起來,以及在Linux上和Qt工程如何整合,在Mac上又該如何整合進Qt工程里面,這些問題,在網上的答案都不太靠譜,包括google官方討論組里關於如何整合進Qt也未能給出令人滿意的答案(Linux上還算簡單,主要的問題在於MacOS上和Qt工程的整合),后來經過自己大量的實踐,以及經驗積累,總算攻克,其中既需要一時的靈感,更需要幾分運氣的成分。

對於這樣大型的項目來說,如果這些最基本的工程問題得不到解決,更遑論代碼水平有多高,都無法發揮出來。本文不准備去討論如何去解決這些工程問題。

基於2020年中新版的編譯:

  下面就是webrtc整合進Qt編譯成功的一瞬間。由於新版的webrtc代碼移除了mac平台的視屏捕獲,就連DeviceList都獲取不到,在這份代碼里面注釋掉了打開攝像頭相關的少部分代碼,使得工程編譯成功,雖然不能正常使用,但是證明了webrtc的庫的鏈接,工程整合都是沒有問題的。對於Mac上的Native的webrtc開發,根據搜集的資料來看,谷歌在15年左右很長一段時間內是支持Mac平台的native開發的,后來就移除了相關代碼,搞出了一套framework,分別為Android、IOS/Mac平台做了相應的適配,底層還是原來的主體框架,但是上層已經做了大量的修改。以后谷歌是否會重新支持Mac上的Native開發,還不得而知。

  總之,隨着MacOS適配到ARM平台,各種開發框架更是和 IOS 整合,Mac上想要實現做很少得改動就能和Linux上共用同一套代碼可能就會變得非常困難。

  而且經過大量代碼梳理,得出了這樣得結論:sdk文件夾下得objc相關的代碼接口,主要是將webrtc核心的c++框架適配到oc這一套體系中去,想要反其道而行之使用oc這一套東西作為底層模塊完成音視頻捕獲,從而適配上層的主體框架,是有很大風險且很難實現的。對於Android平台,也是一樣的道理。

  

  因此,純native的開發,主要就集中在Linux、Windows平台下,只需要向里面的工廠模式的代碼中添加相關的類,就能實現一些擴展的功能,如:使用硬件加速渲染視頻等。

  IOS、MacOS的開發,就主要以framework為主,就是oc的這一套東西,Android也是相應的framework。

    

2.1 Linux下的 WebRTC Native 開發 

在Llinux下,可以通過以上的界面與Janus進行視頻通話。寫這篇文章的時候,阿里雲的環境已被銷毀掉,相關的開發環境已經不方便短時間內復現,時間倉促,僅作記錄

Qt用來開發這套程序的好處就是,在可能的情況下,做很少的適配就能搞出跨平台的代碼,且因為已經封裝好了成熟的websocket、json等各種庫和框架,十分方便開發客戶端,節省了大量的時間。

新版的已經沒有了CreateVideoTrack接口,該接口就是通過WebRtc的框架與底層的AVCaptureSession打交道,在linux上記得是 v4l 的那一套完成視頻捕獲,新版中,Mac 和 Linux 都被移除了該接口。所以說新版的只能編譯通過,和Qt進行整合,做技術預研,Linux平台舊版的才是真正能跑起來的代碼。

3. 通過sdk接口學習webrtc框架

  主要是對MacOS平台的AppRtc進行分析,這里面內容比較多,就把當時的分析筆記在這里記錄一下。

  只用到了AppRtc的文件夾和sdk文件夾的內容,對視頻采集的調用層級進行分析,試圖找出為webrtc調用已有的接口添加Mac的視頻捕獲類的方法。事實證明,經過了sdk中這一層的封裝,OC代碼已經和主體框架緊密耦合,貌似不是簡單添加個類就能解決Mac上原生框架視頻捕獲的問題

最終,native流程參考AppRtc和Android源碼,apprtc的關鍵詞:videoTrackWithSource
結論: videoSource其實是從原生camera獲取數據的代理類的包裝。暫時沒看到適配器之類的。
    factory 中加 Video source(從原生camera抽象出來並拿到數據), video source中有數據回調。
    738   RTC_OBJC_TYPE(RTCVideoSource) *source = [_factory videoSource]; // 下面 api/peerconnection/RTCPeerConnectionFactory.mm

    746     RTC_OBJC_TYPE(RTCCameraVideoCapturer) *capturer =
    747         [[RTC_OBJC_TYPE(RTCCameraVideoCapturer) alloc] initWithDelegate:source];

    748     [_delegate appClient:self didCreateLocalCapturer:capturer];

    760   return [_factory videoTrackWithSource:source trackId:kARDVideoTrackId];
    要想弄明白以上738行就要把api/peerconnection/RTCPeerConnectionFactory.h的代理弄的很清了。
    grep -rn "RTCPeerConnectionDelegate" *
--------------------------------------------------------------------------------------


為什么從RTCCameraVideoCapturer(原生camera)研究?為了弄清楚ObjcTo系列函數的調用邏輯是在哪一層。
以便想辦法單獨拿出來用到C++中

vim ARDExternalSampleCapturer.m             # 這個也是接口,沒幾行代碼
    18 - (instancetype)initWithDelegate:(__weak id<RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>)delegate {
    19   return [super initWithDelegate:delegate];
    20 }

grep -rn "ARDExternalSampleCapturer" *      # 這個獲取到數據就被放到RTCVideoFrame里面去了。
    ARDAppClient.h:26:@class ARDExternalSampleCapturer;
    ARDAppClient.h:57:    didCreateLocalExternalSampleCapturer:(ARDExternalSampleCapturer *)externalSampleCapturer;
    ARDAppClient.m:32:#import "ARDExternalSampleCapturer.h"
    ARDAppClient.m:742:    ARDExternalSampleCapturer *capturer =
    ARDAppClient.m:743:        [[ARDExternalSampleCapturer alloc] initWithDelegate:source];
    ARDExternalSampleCapturer.h:17:@interface ARDExternalSampleCapturer : RTC_OBJC_TYPE
    ARDExternalSampleCapturer.m:11:#import "ARDExternalSampleCapturer.h"
    ARDExternalSampleCapturer.m:16:@implementation ARDExternalSampleCapturer
    ios/broadcast_extension/ARDBroadcastSampleHandler.m:15:#import "ARDExternalSampleCapturer.h"
    ios/broadcast_extension/ARDBroadcastSampleHandler.m:111:    didCreateLocalExternalSampleCapturer:(ARDExternalSampleCapturer *)externalSampleCapturer {



grep -rn "RTCCameraVideoCapturer" *
    ARDAppClient.h:28:@class RTC_OBJC_TYPE(RTCCameraVideoCapturer);
    ARDAppClient.h:40:    didCreateLocalCapturer:(RTC_OBJC_TYPE(RTCCameraVideoCapturer) *)localCapturer;
    ARDAppClient.m:14:#import <WebRTC/RTCCameraVideoCapturer.h>
    ARDAppClient.m:746:    RTC_OBJC_TYPE(RTCCameraVideoCapturer) *capturer =
    ARDAppClient.m:747:        [[RTC_OBJC_TYPE(RTCCameraVideoCapturer) alloc] initWithDelegate:source];
    這里面的都很重要,主要在於746行。結合下面initWith、繼承關系、以及原生camera實現的函數,發現在這個文件多了很多直接控制camera、獲取camera信息的調用。
    746行就是代理的使用,追蹤其數據回調流程。里面涉及到操作factory,猜想對於source、capturer(接口)等的添加、創建和硬件關系不大。
    回頭研究videosink、以及從factory獲取video source的邏輯

vim ARDCaptureController.h          # 這個頭文件沒幾行,實現文件里對capture做了控制,start、stop並initWithCapturer
    18 - (instancetype)initWithCapturer:(RTC_OBJC_TYPE(RTCCameraVideoCapturer) *)capturer
    19                         settings:(ARDSettingsModel *)settings;
vim ARDCaptureController.m
    25 - (instancetype)initWithCapturer:(RTC_OBJC_TYPE(RTCCameraVideoCapturer) *)capturer

AppRTCMobile % grep -rn "RTCCameraVideoCapturer.h" *
    ARDAppClient.m:14:#import <WebRTC/RTCCameraVideoCapturer.h>
    ARDCaptureController.h:11:#import <WebRTC/RTCCameraVideoCapturer.h>
    ARDSettingsModel.m:14:#import <WebRTC/RTCCameraVideoCapturer.h>
    ios/ARDVideoCallViewController.m:14:#import <WebRTC/RTCCameraVideoCapturer.h>
    可見AppRTC調用了framework里的方法,就去sdk/objc下繼續追蹤。

下面結合sdk/objc進行分析-------------------------------------------------------------------------------------------------

api/peerconnection/RTCPeerConnectionFactory.mm
     - (RTC_OBJC_TYPE(RTCVideoSource) *)videoSource {      # 被 ARDCaptureController.m 調用。

grep -rn "RTCVideoSource" * | grep RTCVideoCapturerDelegate
    api/peerconnection/RTCVideoSource.h:21:@interface RTC_OBJC_TYPE (RTCVideoSource) : RTC_OBJC_TYPE(RTCMediaSource) <RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>
    components/capturer/RTCCameraVideoCapturer.h:21:// RTCVideoCapturerDelegate (usually RTCVideoSource).
        20 // Camera capture that implements RTCVideoCapturer. Delivers frames to a
        21 // RTCVideoCapturerDelegate (usually RTCVideoSource).
        22 NS_EXTENSION_UNAVAILABLE_IOS("Camera not available in app extensions.")
        23 @interface RTC_OBJC_TYPE (RTCCameraVideoCapturer) : RTC_OBJC_TYPE(RTCVideoCapturer)
        #發現RTCCameraVideoCapturer繼承自RTCVideoCapturer,RTCVideoCapturerDelegate 協議在下面定義,在多處使用。該文件有camera詳細的控制函數

grep -rn "RTCVideoCapturer\b" *
    base/RTCVideoCapturer.h:29:@property(nonatomic, weak) id<RTC_OBJC_TYPE(RTCVideoCapturerDelegate)> delegate;
    Framework/Headers/WebRTC/RTCVideoCapturer.h:11:#import "base/RTCVideoCapturer.h"
    api/peerconnection/RTCVideoSource.mm:67:- (void)capturer:(RTC_OBJC_TYPE(RTCVideoCapturer) *)capturer
    base/RTCVideoCapturer.m:13:@implementation RTC_OBJC_TYPE (RTCVideoCapturer)

base/RTCVideoCapturer.h +17      # 該文件僅僅是個代理,沒幾行代碼。比較關心Camera實際控制處理的邏輯,代理在哪里被使用、適配器怎么和factory等打交道是重點。
      19 RTC_OBJC_EXPORT
      20 @protocol RTC_OBJC_TYPE(RTCVideoCapturerDelegate) <NSObject>
      21 - (void) capturer : (RTC_OBJC_TYPE(RTCVideoCapturer) *)capturer
      22 didCaptureVideoFrame : (RTC_OBJC_TYPE(RTCVideoFrame) *)frame;
      23 @end
      這下搜到很多信息,思路漸漸清晰。

grep -rn "super initWith" *            # 后面詳細研究一下繼承關系
    重點關注這幾個:
    api/peerconnection/RTCAudioTrack.mm:46:  return [super initWithFactory:factory nativeTrack:nativeTrack type:type];
    api/peerconnection/RTCAudioSource.mm:27:  if (self = [super initWithFactory:factory
    api/peerconnection/RTCVideoTrack.mm:48:  if (self = [super initWithFactory:factory nativeTrack:nativeMediaTrack type:type]) {
    api/peerconnection/RTCVideoSource.mm:36:  if (self = [super initWithFactory:factory
    components/capturer/RTCCameraVideoCapturer.m:68:  if (self = [super initWithDelegate:delegate]) {
    這些信息很重要,要弄清楚它們從誰繼承過來,以及factory、track、source等的關系。
    類似於C++的初始化參數列表里面構造父類,同樣的參數,通過構造子類,在父類中也是可見的。
繼承關系:
    components/capturer/RTCCameraVideoCapturer.h
        23 @interface RTC_OBJC_TYPE (RTCCameraVideoCapturer) : RTC_OBJC_TYPE(RTCVideoCapturer)


grep -rn "AVCaptureVideoDataOutputSampleBufferDelegate" *
    components/capturer/RTCCameraVideoCapturer.m:29:()<AVCaptureVideoDataOutputSampleBufferDelegate> @property(nonatomic,
    components/capturer/RTCCameraVideoCapturer.m:236:#pragma mark AVCaptureVideoDataOutputSampleBufferDelegate
    此處就是原生的ios/mac camera控制程序了。看看頭文件:
        23 @interface RTC_OBJC_TYPE (RTCCameraVideoCapturer) : RTC_OBJC_TYPE(RTCVideoCapturer)  # 這個繼承關系很重要。
        26 @property(readonly, nonatomic) AVCaptureSession *captureSession;                     # 有沒有供外部使用以設置一些參數???
        29 + (NSArray<AVCaptureDevice *> *)captureDevices;                                      # 獲得所有的設備信息的關鍵。
        30 // Returns list of formats that are supported by this class for this device.
        31 + (NSArray<AVCaptureDeviceFormat *> *)supportedFormatsForDevice:(AVCaptureDevice *)device;
        34 - (FourCharCode)preferredOutputPixelFormat;
        40 - (void)startCaptureWithDevice:(AVCaptureDevice *)device    # 注意區分capturer和capture,這里有兩個 staCap 函數,都是異步執行的。
    只對外提供了兩個函數,有沒有使用initWith然后供父類使用的?最有可能還是通過代理的方式,上面initWith:components/capturer/RTCCameraVideoCapturer.m:68
    RTCVideoCapturer就是個純接口,作為原生camera的父類,對外提供的數據,由RTCCameraVideoCapturer的 super initWith 完成構造。
    知道數據怎么提供出去了,那么數據是怎么被對方獲取的?
    base/RTCVideoCapturer.m +17
        17 - (instancetype)initWithDelegate:(id<RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>)delegate {
        傳入的這個參數,是符合該協議的代理,在實際的Camera:RTCCameraVideoCapturer被構造的時候傳入。在哪被調用?ut里面,沒有了。。。。
        也就是說該類沒有被進一步封裝成C++了。回到AppRTC里面去搜索。

grep -rn "RTCCameraVideoCapturer" * | grep initWith
    unittests/RTCCameraVideoCapturerTests.mm:88:      [[RTC_OBJC_TYPE(RTCCameraVideoCapturer) alloc] initWithDelegate:self.delegateMock];
    unittests/RTCCameraVideoCapturerTests.mm:103:      [[RTC_OBJC_TYPE(RTCCameraVideoCapturer) alloc] initWithDelegate:self.delegateMock


結論:RTCVideoCapturer只是個設置代理的接口。真正的camera數據被傳到該代理中,該代理中一定實現了某些代理方法。弄清楚是哪幾個。



繼續分析: grep -rn "VideoSinkInterface" * --color
#endif

#include "modules/video_capture/video_capture_factory.h"
#include "modules/video_capture/video_capture.h"
void getDevices()
{
    std::unique_ptr<webrtc::VideoCaptureModule::DeviceInfo> device_info(webrtc::VideoCaptureFactory::CreateDeviceInfo());

    qDebug() << device_info->NumberOfDevices();
    char device_name[256];
    char unique_name[256];
    uint32_t capture_device_index = 0;
    /*device_info->GetDeviceName(static_cast<uint32_t>(capture_device_index),
                                     device_name, sizeof(device_name), unique_name,
                                     sizeof(unique_name));*/
    // qDebug() << device_name << " ----- " << unique_name;
    /*
     * grep -rn "^#.*sdk/objc" * 發現module里面是有oc的東西的。
     * grep -rn "^#import.*" *  發現base/mac、base/ios里面是有平台特定的東西的。
     * 總之,sdk下面的基本都沒被webrtc主框架使用,那么到底支不支持呢?
     * grep -rn "^#include*.base/native_library.h" *   使用oc的也只有這個庫,但基本沒被其他地方所使用。
     *
     * 結論:Mac的這套東西平台特定的內容太多了,所以單獨成framework,供mac/ios使用,主框架里面代碼耦合性較高、封裝很完善,不能和framework強耦合。
     *
     VcmCapturer: _Create、virtual _OnFrame
     this 繼承自 rtc::VideoSinkInterface<VideoFrame>, vcm_ 是 rtc::scoped_refptr<VideoCaptureModule>
     vcm_->RegisterCaptureDataCallback(this);
     vcm_ = webrtc::VideoCaptureFactory::Create(unique_name); // 僅僅是創建。。。
     set capability_ && vcm_->StartCapture(capability_)
    */
}

4. MacOS 視頻采集的可行性

  在了解到新版webrtc可以使用vcm進行自定義視頻源的時候,一切好像又有了轉折。以下的內容只做分析,再進一步深入學習WebRTC之后再回過頭來解決這個問題。

  雖然當時沒有找到使用vmc進行視頻傳輸的相關示例,但是找到了使用硬件加速的示例:

    

  深入分析該程序,應該可以找到答案。這樣一來,最理想的情況,新版的webrtc,都能在Linux和Mac上使用同一套架構的代碼進行native 程序開發了。

  更深入的內容,往后再一點點的補充。

5. 更新

上面的文章,完成了一個基於Linux的舊版webrtc向Janus服務器推流的應用程序,不管框架怎么變,代碼怎么更新,其內部還是使用標准的webrtc協議。

經過分析,webrtc的整個框架的核心,無論是在哪個平台上,都是一樣的,只不過從框架中抽離出來一些模塊單獨做了相應平台的封裝,使之適配相應平台的軟件運行環境和開發語言。

其中和平台相關最重要的部分在modules下面,里面包含了相應平台的視頻、語音模塊,如果能做對應的適配,理論上是可以在所有平台上共用一份源碼的。

另外,關於最新版webrtc的開發,可以參考Git上已有的推流項目來完成,然后就能對比出和舊版本的差異,從而完成項目的升級迭代。

其它關於UI的部分,則是一小塊需要適配的部分。核心的地方還是在於音視頻的硬件采集的平台差異。

如果能完成如上工作,則可以開發出各種SDK,適用於各種平台。很顯然,這些工作需要較大的工作量。

基於 sdk 或 framework 的 ios、android 的 API 代碼,則可以看作是將 webrtc core 的軟件架構單獨拿出來使用的案例,同時對比 Jni 層以及 C++ 轉 OC 所封裝的 API 的架構差異之處,又是研究 webrtc 整體框架的使用的一個突破口。不過這個思路有點舍近求遠。

最簡單的途徑是研究 modules 模塊,添加音視頻采集的工廠方法,以及 linux 和 windows 平台上的 peerconnection 示例代碼,就可以把 native 的一套代碼全部挪用到各個平台。

有些內容,是做着做着就變清晰了的,以上的分析思路,還需要更多的代碼調試、框架分析、技術預研等做支撐。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM