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客户端需要通过信令服务器交换彼此的媒体描述信息(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
视频采集流水线
- 视频采集到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
的视频采集,将上面类简化并抽离出主要方法。
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
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
接口,通过AudioDeviceModule
的 int32_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平台依赖 SDK
及 NDK
, Windows
和 iOS
就不需要。
于是我们就可以通过4个git仓库来管理和维护。
仓库名 | 地址 | 描述 |
---|---|---|
webrtcm79core | https://xxxxx.com/webrtcm79core.git | 除了third_party及测试相关目录之外其他目录 |
webrtcm79thirdpartyandroid | https://xxxxx.com/webrtcm79thirdpartyandroid.git | Android的第三方库,基本不会更新 |
webrtcm79thirdpartyiOS | https://xxxxx.com/webrtcm79thirdpartyiOS.git | iOS的第三方库,基本不会更新 |
webrtcm79thirdpartywindows | https://xxxxx.com/webrtcm79thirdpartywindows.git | Windows的第三方库,基本不会更新 |
通过上面的处理我们就可以将WebRTC的源码进行统一,针对不同的平台拉取不同的代码分支编译即可。其中 webrtc_m79_third_party_android
、 webrtc_m79_third_party_iOS
、 webrtc_m79_third_party_windows
这些基本不会更新。针对 webrtc_m79_core
进行二次开发即可。
6. 业务逻辑统一
6.1 系统架构
- 核心层:
- 对外提供接口;
- 集成不同架构webrtc及信令SDK;
- WebRTC业务逻辑管理;
- 信令交互业务逻辑管理;
- 日志管理;
- 适配层:
- Android平台通过JNI实现API层与核心层逻辑交互;
- iOS平台通过OC实现API层与核心层逻辑交互;
- Windows平台API层和核心层直接交互;
- API层
- 对外提供接口;
- 针对不同架构平台实现ADM、硬编解码等功能;
6.2 代码架构
- 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-v8a
,armeabi-v7a
,x86
,x86_64
iOS
目录:windows
目录
android
目录:arm64-v8a
,armeabi-v7a
,x86
,x86_64
iOS
目录windows
目录include
目录
libsignal
目录: 信令SDK不同平台库。libwebrtc
目录: WebRTC不同平台库。- 等等其他依赖库
通过上面源码及业务逻辑的统一之后,可以大大降低后期WebRTC的维护成本。
作者:潘超
来源:斗鱼流媒体
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。