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