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 的一套代码全部挪用到各个平台。
有些内容,是做着做着就变清晰了的,以上的分析思路,还需要更多的代码调试、框架分析、技术预研等做支撑。