WebRTC端侧的应用及三端统一架构

1. 源码获取及编译

本文没有特殊说明,都是针对 webrtc m79源码

国内网络限制,基本下面的操作都被限制,导致无法获取,因此可以通过购买一个轻量云服务器或者黑科技手段下载及编译。

具体源码及编译详情参见https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/index.md

  • Windows平台
# 首先设置环境变量
# GYP_GENERATORS msvs-ninja,ninja
# GYP_MSVS_OVERRIDE_PATH vs2017安装路径(D:\Program Files (x86)\Microsoft Visual Studio\2017\Community)
# GYP_MSVS_VERSION 2017
# DEPOT_TOOLS_WIN_TOOLCHAIN 0

$ fetch --nohooks webrtc
$ gclient sync

$ gn gen out/Debug --ide=vs2017 --args="is_debug=true target_cpu=\"x86\""
$ ninja -C out/Debug
  • Android平台

$ fetch --nohooks webrtc_android
$ gclient sync

$ cd src/
$ ./build/install-build-deps.sh
$ ./build/install-build-deps-android.sh
# To build for ARM64: use target_cpu="arm64"
# To build for 32-bit x86: use target_cpu="x86"
# To build for 64-bit x64: use target_cpu="x64"
$ gn gen out/Debug --args='target_os="android" target_cpu="arm"'
$ ninja -C out/Debug
  • iOS平台

$ fetch --nohooks webrtc_ios
$ gclient sync

$ ./tools_webrtc/ios/build_ios_libs.sh

源码下载及编译之后,就可以在不同的平台引入编译的库来进行开发。

2. WebRTC交互逻辑

WebRTC并没有提供一套完整的音视频系统架构,它只是定义了音视频会话的建立和传输方式,一个典型的WebRTC会话如下:

WebRTC端侧的应用及三端统一架构

WebRTC客户端需要通过信令服务器交换彼此的媒体描述信息(sdp),包含各自支持的音视频数据格式(如H264,vp8,opus等)以及网络地址(包括host地址,公网地址,turn服务器地址),交换之后会选择合适的音视频格式以及网络地址进行通信。WebRTC并没有定义信令服务器该如何实现,只要能达到交换sdp的功能即可,可以使用http、websocket,或者tcp私有协议都可以。

3. 基本应用

代码实现了局域网 P2P的通话的方式,信令服务器交互的逻辑先忽略。以下摘自webrtc m79版本 examples\peerconnection\client\conductor.cc

WebRTC开发通用流程如下:

  • 初始化 PeerConnectionFactoryInterface
/**
总结:
外部参数传入:
- 创建AudioDeviceModule         非必须 (WebRtcVoiceEngine内部创建)
- 创建AudioEncoderFactory        必须
- 创建AudioDecoderFactory        必须
- 创建VideoEncoderFactory        必须
- 创建VideoDecoderFactory        必须
- 创建AudioMixer                非必须    (WebRtcVoiceEngine内部创建)
- 创建AudioProcessing            非必须 (CreatePeerConnectionFactory内部创建)

CreatePeerConnectionFactory()流程:
- 创建`network_thread = network_thread`;
- 创建`worker_thread = worker_thread`;
- 创建`signaling_thread = signaling_thread`;
- 创建`MediaEngineInterface(CompositeMediaEngine)`
- 创建`BasicNetworkManager`
- 创建`BasicPacketSocketFactory`
- 创建`ChannelManager`
    - 创建`RtpDataEngine`
    - 收集支持的编码信息`std::vector<AudioCodec> send_codecs_`;
  - 收集支持的解码信息`std::vector<AudioCodec> recv_codecs_`;
  - `AudioDeviceModule`没有则创建(`AudioDeviceModule`)
  - `AudioMixer`没有则创建(`AudioMixerImpl`)
  - `AudioState`创建
  - 设置APM参数
*/
rtc::scoped_refptr<webrtc::PeerConnectionFactoryInterface> peer_connection_factory_;
peer_connection_factory_ = webrtc::CreatePeerConnectionFactory(
    nullptr /* network_thread */, 
    nullptr /* worker_thread */,
    signaling_thread.get() /* signaling_thread */, 
    nullptr /* default_adm */,
    webrtc::CreateBuiltinAudioEncoderFactory(),
    webrtc::CreateBuiltinAudioDecoderFactory(),
    webrtc::CreateBuiltinVideoEncoderFactory(),
    webrtc::CreateBuiltinVideoDecoderFactory(), 
    nullptr /* audio_mixer */,
    nullptr /* audio_processing */);
  • 初始化 PeerConnectionInterface
/**
CreatePeerConnection()流程:
- 创建RTCCertificateGenerator
- 创建BasicPortAllocator
- 创建RtcEventLog
- 创建Call`
- 创建PeerConnection::InitializePortAllocator_n
- 创建SctpTransportInternalFactory(cricket::SctpTransportFactory)
- 创建JsepTransportController(JsepTransportController)
- 创建WebRtcSessionDescriptionFactory(WebRtcSessionDescriptionFactory)`
*/
rtc::scoped_refptr<webrtc::PeerConnectionInterface> peer_connection_;
peer_connection_ = peer_connection_factory_->CreatePeerConnection(
      config, nullptr, nullptr, this);
  • 创建并添加 AudioTrack和 VideoTrack

rtc::scoped_refptr<webrtc::AudioTrackInterface> audio_track(
    peer_connection_factory_->CreateAudioTrack(kAudioLabel, peer_connection_factory_->CreateAudioSource(
                            cricket::AudioOptions())));
auto result_or_error = peer_connection_->AddTrack(audio_track, {kStreamId}); 

rtc::scoped_refptr<CapturerTrackSource> video_device = CapturerTrackSource::Create();
rtc::scoped_refptr<webrtc::VideoTrackInterface> video_track_(
        peer_connection_factory_->CreateVideoTrack(kVideoLabel, video_device));
result_or_error = peer_connection_->AddTrack(video_track_, {kStreamId});
  • 如果需要渲染则在 AudioTrack对象上添加 VideoSink

//this指向一个渲染类,比如负责OpenGL渲染的类
video_track_->AddOrUpdateSink(this, rtc::VideoSinkWants());
  • 请求 SDP和 Candidate,并通过信令服务器进行交互
//Client A
//发起方
peer_connection_->CreateOffer( this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());  
//CreateOffer回调里面收集的SDP信息,需要调用SetLocalDescription(),并给接收方发送sdp
peer_connection_->SetLocalDescription(DummySetSessionDescriptionObserver::Create(), desc);
//SetLocalDescription调用之后,则开始收集Candidate信息,异步收到Candidate则通过信令发送

//Client B
//当接收到Client A发送的sdp信息之后,则创建PeerConnection对象,然后调用
peer_connection_->SetRemoteDescription(DummySetSessionDescriptionObserver::Create(), session_description.release()); 
//然后调用CreateAnswer,开始收集SDP信息
peer_connection_->CreateAnswer(this, webrtc::PeerConnectionInterface::RTCOfferAnswerOptions());
//CreateAnswer回调里面收集的SDP信息,需要调用SetLocalDescription(),并给接收方发送sdp
peer_connection_->SetLocalDescription(DummySetSessionDescriptionObserver::Create(), desc);
//SetLocalDescription调用之后,则开始收集Candidate信息,异步收到Candidate则通过信令发送

//Client A接收到Client B的sdp信息
peer_connection_->SetRemoteDescription(DummySetSessionDescriptionObserver::Create(), session_description.release()); 

//Client A接收到Client B的Candidate信息 或者 Client B接收到Client A的Candidate信息 各自调用
// *****注意:这里有可能candidate比sdp先到的情况,如果先到则需要缓存处理。*****
peer_connection_->AddIceCandidate(candidate);

上述没有任何异常则媒体数据就开始传输。VideoSink的渲染类及ADM的播放模块就会接收到音视频数据,可以直接进行渲染及播放。

以上的流程以Windows平台为例,但是对于Android/iOS/Web也是通用的,基本流程不变。

4. 高级应用

以下分享2个 WebRTC在端侧的实际应用案例。

4.1 支持Video视频自采集

案例说明:美颜/美型等特效对于直播来说是至关重要的。通常情况下直播SDK都会支持美颜/美型等特效功能,但是 WebRTC源码中没有任何相关功能。如果针对源码扩展的话,那么就费时费力费头发。如何将直播SDK经过美颜/美型等特效处理之后的纹理/RGBA数据,直接传递给 WebRTC库中传输呢?想要实现扩展功能,则需要先了解一下 WebRTC视频采集流水线。

4.1.1 了解WebRTC视频采集流水线
WebRTC端侧的应用及三端统一架构
  • 视频采集到VideoTrack流程
  • VideoSourceInterface:视频数据源接口,支持 add及 remove方法;
  • VideoSinkInterface:视频数据输出统一接口;
  • VideoTrack : 内部维护 VideoSourceInterface,渲染对象直接添加到 VideoTrack,而编码器关联到 VideoTrack则需要如下流程
    • 函数调用 AddTrack()-->CreateOffer()-->SetLocalDescriptio()最终关联;
    • 具体对象传递流程 VideoTrack-->VideoRtpSender-->WebRtcVideoVChannel-->WebRtcVideoSendStream-->VideoSendStream-->VideoStreamEncoder
  • VideoBroadcaster:同时实现 VideoSourceInterface及 VideoSinkInterface接口,内部维护容器 vector<VideoSinkInterface>;
  • CapturerTrackSource : 数据获取实现类,实现 VideoSourceInterface接口。内部须维护一个 VideoBroadcaster类,视频数据返回之后通过调用 VideoSinkInterface的 onFrame接口,然后作用到 vector<VideoSinkInterface>每一个对象上;

通过上面源码类图,有了大致的了解,为了更好的理解 WebRTC的视频采集,将上面类简化并抽离出主要方法。

WebRTC端侧的应用及三端统一架构
  • VideoTrackInterface可以认为是 VideoSourceInterface的代理类,根据不同的 VideoSourceInterface实现不同的数据源(Camera,文件,网络流等);
  • VideoSourceInterface和 VideoSinkInterface采用观察者模式, VideoSourceInterface为被观察者, VideoSinkInterface为观察者。 VideoBroadcaster是 VideoSourceInterface的具体操作类。负责 VideoSinkInterface的增/删/分发操作。
  • VideoSinkInterface为输出接口,可以被渲染类/编码类/本地文件类等实现。
4.1.2 解决方案
/*VideoSourceImpl.h*/
class VideoSourceImpl :
    public VideoSourceInterface<VideoFrame>
{
public:
    virtual ~VideoSourceImpl();
    static VideoSourceImpl* Create(size_t width,
        size_t height,
        size_t target_fps,
        size_t capture_device_index);

    void AddOrUpdateSink(VideoSinkInterface<VideoFrame>* sink,
        const VideoSinkWants& wants) override;
    void RemoveSink(VideoSinkInterface<VideoFrame>* sink) override;
    void OnFrame(VideoFrame& frame) override;
private:
    VideoSourceImpl();
private:
    VideoBroadcaster broadcaster_;
};
/*VideoSourceImpl.c*/
VideoSourceImpl* VideoSourceImpl::Create(size_t width,
    size_t height,
    size_t target_fps,
    size_t capture_device_index) {
    std::unique_ptr<VideoSourceImpl> vcm_capturer(new VideoSourceImpl());
    return vcm_capturer.release();
}
VideoSourceImpl::VideoSourceImpl() {}
VideoSourceImpl::~VideoSourceImpl() {}

void VideoSourceImpl::AddOrUpdateSink(VideoSinkInterface<VideoFrame>* sink,
    const VideoSinkWants& wants) {
    broadcaster_.AddOrUpdateSink(sink, wants);
}

void VideoSourceImpl::RemoveSink(VideoSinkInterface<VideoFrame>* sink) {
    broadcaster_.RemoveSink(sink);
}

void VideoSourceImpl::OnFrame(VideoFrame& frame) {
    broadcaster_.OnFrame(frame);
}

VideoSourceImpl对象作为参数传递给 VideoTrack,然后外部纹理/RGBA/NV12等数据直接通过 VideoSourceImpl::OnFrame传递即可。

4.2 支持Audio本地音量数据回调

案例说明:多人语音连麦场景下,产品想在界面上面动态的展示本地音量的波动。但是 WebRTC未支持这个功能。

4.2.1 WebRTC音频采集流水线及ADM
WebRTC端侧的应用及三端统一架构
  • AudioDeviceModule:负责音频数据的采集和播放,不同平台不同实现。实现类通常内部维护 AudioDeviceBuffer对象;
  • AudioDeviceBuffer:负责管理音频数据的缓冲区,内部维护 AudioTransport对象;
  • AudioTransport:关键的对外接口,负责音频数据的传入(调用NeedMorePlayData方法,供Playout使用)和输出(调用RecordedDataIsAvailable方法,数据由Record采集操作产生)
  • AudioProcessing:负责对录制的音频数据做初始处理,典型的3A算法。
  • AudioMixer:负责多路远端发送过来的音频数据做混音。
  • AudioState:负责以上各个类的调度;

以上几个关键的接口重要函数定义如下:

  • AudioDeviceModule接口,各种平台相关逻辑的抽象

class AudioDeviceModule : public rtc::RefCountInterface {
 public:

 public:
  // Creates a default ADM for usage in production code.
  static rtc::scoped_refptr<AudioDeviceModule> Create(
      AudioLayer audio_layer,
      TaskQueueFactory* task_queue_factory);

  // Full-duplex transportation of PCM audio
  virtual int32_t RegisterAudioCallback(AudioTransport* audioCallback) = 0;

  //.............................................
  // Audio transport control
  virtual int32_t StartPlayout() = 0;
  virtual int32_t StopPlayout() = 0;
  virtual bool Playing() const = 0;
  virtual int32_t StartRecording() = 0;
  virtual int32_t StopRecording() = 0;
  virtual bool Recording() const = 0;

  //.............................................

 protected:
  ~AudioDeviceModule() override {}
};
  • AudioDeviceBuffer接口

class AudioDeviceBuffer {
  //.................
  AudioTransport* audio_transport_cb_;
  //.................
}

int32_t AudioDeviceBuffer::RegisterAudioCallback(
    AudioTransport* audio_callback) {
  RTC_DCHECK_RUN_ON(&main_thread_checker_);
  if (playing_ || recording_) {
    RTC_LOG(LS_ERROR) << "Failed to set audio transport since media was active";
    return -1;
  }
  audio_transport_cb_ = audio_callback;
  return 0;
}
  • AudioTransport接口
class AudioTransport {
 public:
  //录制的数据发送出去
  virtual int32_t RecordedDataIsAvailable(const void* audioSamples,
                                          const size_t nSamples,
                                          const size_t nBytesPerSample,
                                          const size_t nChannels,
                                          const uint32_t samplesPerSec,
                                          const uint32_t totalDelayMS,
                                          const int32_t clockDrift,
                                          const uint32_t currentMicLevel,
                                          const bool keyPressed,
                                          uint32_t& newMicLevel) = 0;  // NOLINT

  // Implementation has to setup safe values for all specified out parameters.
  //获取用于播放的音频数据
  virtual int32_t NeedMorePlayData(const size_t nSamples,
                                   const size_t nBytesPerSample,
                                   const size_t nChannels,
                                   const uint32_t samplesPerSec,
                                   void* audioSamples,
                                   size_t& nSamplesOut,  // NOLINT
                                   int64_t* elapsed_time_ms,
                                   int64_t* ntp_time_ms) = 0;  // NOLINT

  // Method to pull mixed render audio data from all active VoE channels.
  // The data will not be passed as reference for audio processing internally.
  //获取用于播放的音频数据
  virtual void PullRenderData(int bits_per_sample,
                              int sample_rate,
                              size_t number_of_channels,
                              size_t number_of_frames,
                              void* audio_data,
                              int64_t* elapsed_time_ms,
                              int64_t* ntp_time_ms) = 0;

 protected:
  virtual ~AudioTransport() {}
};

那么这些对象是怎么关联的呢?

通常AudioDeviceModule实现类则必然拥有AudioDeviceBuffer对象。而AudioDeviceBuffer对象内部拥有AudioTransport对象。AudioDeviceModule通过AudioDeviceBuffer对象的RegisterAudioCallback方法,将三者联系起来。分析 Windows/Android/iOS的实现类都是这个流程。

以下列举不同平台的AudioDeviceModule实现

  • Windows平台
    • std::unique_ptr<AudioDeviceGeneric>audio_device_;
    • 实现类 AudioDeviceWindowsCore 继承 AudioDeviceGeneric
    • AudioDeviceWindowsCore为 Windows平台硬件麦相关API的实现。
    • AudioDeviceBufferaudio_device_buffer_;
    • 路径: modules\audio_device\audio_device_impl.cc
    • 继承关系: AudioDeviceModuleImpl-->AudioDeviceModuleForTest-->AudioDeviceModule
    • 核心属性:
  • Android平台
    • conststd::unique_ptr<AudioInput>input_;
    • conststd::unique_ptr<AudioOutput>output_;
    • std::unique_ptr<AudioDeviceBuffer>audio_device_buffer_;
    • 路径: sdk/android/src/jni/audio_device/audio_device_module.cc
    • 继承关系: AndroidAudioDeviceModule-->AudioDeviceModule
    • 核心属性:
  • iOS平台
    • 实现类 AudioDeviceIOS 继承 AudioDeviceGeneric
    • AudioDeviceIOS为 iOS平台硬件麦相关API的实现。
    • 路径: sdk/objc/native/src/audio/audio_device_module_ios.mm
    • 继承关系: AudioDeviceModuleIOS-->AudioDeviceModule
    • 核心属性:
    • std::unique_ptr<AudioDeviceIOS>audio_device_;
    • std::unique_ptr<AudioDeviceBuffer>audio_device_buffer_;
4.2.2 解决方案

了解了上面的知识之后,其实方案有多种可以选择,这里仅给出一种实现方式。
根据音频采集流水线我们知道AudioTransport接口是音视频出入的大门,我们可以自定义AudioDeviceModule并实现AudioTransport接口,通过AudioDeviceModuleint32_tRegisterAudioCallback(webrtc::AudioTransport*audio_callback)方法建立关联。然后重写RecordedDataIsAvailable接口,内部计算音量值并通过接口返回即可。


//CustomADMWrapper.h
class CustomADMWrapper : public webrtc::AudioDeviceModule, public webrtc::AudioTransport {
public:
    // AudioTransport methods overrides.
    int32_t RecordedDataIsAvailable(const void* audioSamples,
        const size_t nSamples,
        const size_t nBytesPerSample,
        const size_t nChannels,
        const uint32_t samples_per_sec,
        const uint32_t total_delay_ms,
        const int32_t clockDrift,
        const uint32_t currentMicLevel,
        const bool keyPressed,
        uint32_t& newMicLevel)override;
}

//CustomADMWrapper.cc
int32_t CustomADMWrapper::RecordedDataIsAvailable(const void* audioSamples,
    const size_t nSamples,
    const size_t nBytesPerSample,
    const size_t nChannels,
    const uint32_t samples_per_sec,
    const uint32_t total_delay_ms,
    const int32_t clockDrift,
    const uint32_t currentMicLevel,
    const bool keyPressed,
    uint32_t& newMicLevel) {
    /*计算音量并通过接口回调音量给上层。*/
    return res;
}

5. 源码统一

有过WebRTC开发经验的人应该都知道,WebRTC针对不同的平台有不同的分支,不同的编译脚本。如果我们需要修改WebRTC某一处源码,则就需要切换分支修改代码,那么我们可以使用一套代码来管理吗?下面我介绍一下我们的方案。

查看以下源码目录我们不难发现,Android/iOS/Windows几乎都一致,只有 third_party目录存在差异,这里不难理解,都是在编译时的依赖库和工具,比如Android平台依赖 SDKNDKWindowsiOS就不需要。

WebRTC端侧的应用及三端统一架构

于是我们就可以通过4个git仓库来管理和维护。

仓库名地址描述
webrtcm79corehttps://xxxxx.com/webrtcm79core.git除了third_party及测试相关目录之外其他目录
webrtcm79thirdpartyandroidhttps://xxxxx.com/webrtcm79thirdpartyandroid.gitAndroid的第三方库,基本不会更新
webrtcm79thirdpartyiOShttps://xxxxx.com/webrtcm79thirdpartyiOS.gitiOS的第三方库,基本不会更新
webrtcm79thirdpartywindowshttps://xxxxx.com/webrtcm79thirdpartywindows.gitWindows的第三方库,基本不会更新

通过上面的处理我们就可以将WebRTC的源码进行统一,针对不同的平台拉取不同的代码分支编译即可。其中 webrtc_m79_third_party_androidwebrtc_m79_third_party_iOSwebrtc_m79_third_party_windows这些基本不会更新。针对 webrtc_m79_core进行二次开发即可。

6. 业务逻辑统一

6.1 系统架构

WebRTC端侧的应用及三端统一架构
  • 核心层:
    • 对外提供接口;
    • 集成不同架构webrtc及信令SDK;
    • WebRTC业务逻辑管理;
    • 信令交互业务逻辑管理;
    • 日志管理;
  • 适配层:
    • Android平台通过JNI实现API层与核心层逻辑交互;
    • iOS平台通过OC实现API层与核心层逻辑交互;
    • Windows平台API层和核心层直接交互;
  • API层
    • 对外提供接口;
    • 针对不同架构平台实现ADM、硬编解码等功能;

6.2 代码架构

WebRTC端侧的应用及三端统一架构
  • Core库 :封装对外接口,区分不同编译平台;
  • DYRtcClien模块:集成信令 SDK/WebRTC/日志SDK等等、各个业务逻辑的封装及处理。C++实现;
  • DYRTCEngineKit_Windows模块:集成Windows平台采集/编码/ADM等逻辑;对外接口;
  • DYRTCEngineKit_Android模块:集成Android平台采集/编码/ADM等逻辑; JNI接口实现;Java对外接口;
  • DYRTCEngineKit_iOS模块:集成iOS平台采集/编码/ADM等逻辑;OC对外接口;
  • 应用层:
    • 对外接口需根据不同平台接入不同的库;

6.3 工程管理

整个工程目录结构如下:

  • core目录: DYRtcClien模块目录
  • platform目录:
    • DYRTCEngineKit_Windows编译工程及Demo工程
    • DYRTCEngineKit_iOS编译工程及Demo工程
    • DYRTCEngineKit_Android编译工程及Demo工程
    • android目录:
    • iOS目录:
    • windows目录:
  • third_party目录: 信令SDK、日志SDK、WebRTC等不同平台的库;
    • android目录: arm64-v8aarmeabi-v7a , x86x86_64
    • iOS目录:
    • windows目录
    • android目录: arm64-v8aarmeabi-v7a , x86x86_64
    • iOS目录
    • windows目录
    • include目录
    • libsignal目录: 信令SDK不同平台库。
    • libwebrtc目录: WebRTC不同平台库。
    • 等等其他依赖库

通过上面源码及业务逻辑的统一之后,可以大大降低后期WebRTC的维护成本。

作者:潘超
来源:斗鱼流媒体

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论