WebRTC 的音频数据编码及发送控制管线

WebRTC 的音频数据处理发送的概念抽象层面的完整流程如下:

-----------------------------     --------------------------     ---------------------------
|                           |     |                        | ==> | webrtc::AudioProcessing |
| webrtc::AudioDeviceModule | ==> | webrtc::AudioTransport |     ---------------------------
|                           |     |                        |          ||
-----------------------------     --------------------------          ||
                                                                      ||
                                          +=+=========================+=+
                                          | |
                                          \ /
                                           |
                    -----------------------------------------------     ---------------------
                    | webrtc::AudioSender/webrtc::AudioSendStream | ==> | webrtc::Transport |
                    -----------------------------------------------     ---------------------
                                                                                ||
                                                                                \ /
                                                 -------------------------------------------
                                                 | cricket::MediaChannel::NetworkInterface |
                                                 -------------------------------------------

AudioDeviceModule 用于控制各个操作系统平台的音频设备,主要用来做音频的采集和播放。webrtc::AudioTransport 是一个适配和胶水模块,它把 AudioDeviceModule 的音频数据采集和 webrtc::AudioProcessing 的音频数据处理及 webrtc::AudioSender/webrtc::AudioSendStream 的音频数据编码和发送控制粘起来,webrtc::AudioTransport 把采集的音频数据送给 webrtc::AudioProcessing 处理,之后再把处理后的数据给到 webrtc::AudioSender/webrtc::AudioSendStream 编码发送出去。webrtc::AudioProcessing 用于做音频数据处理,如降噪、自动增益控制和回声消除等。webrtc::AudioSender/webrtc::AudioSendStream 用于对音频数据做编码,比如 OPUS、AAC 等,RTP 打包和发送控制。webrtc::Transport 也是一个适配和胶水模块,它把 webrtc::AudioSender/webrtc::AudioSendStream 得到的 RTP 和 RTCP 包发送给后面的网络接口模块。cricket::MediaChannel::NetworkInterface 用于实现真正地把 RTP 和 RTCP 包通过底层的网络接口和协议发送,如 UDP 等,ICE 的作用即为创建一个工作良好的网络接口模块实例。

webrtc::AudioTransport 的实现位于 webrtc/audio/audio_transport_impl.h/webrtc/audio/audio_transport_impl.ccwebrtc::Transport 的实现为 cricket::WebRtcVoiceMediaChannel(位于 webrtc/media/engine/webrtc_voice_engine.h)。如果将音频数据处理发送流水线上的适配和胶水模块省掉,音频数据处理发送流水线将可简化为类似下面这样:

-----------------------------     ---------------------------     ---------------------------     -------------------------------------------
|                           |     |                         |     | webrtc::AudioSender/    |     |                                         |
| webrtc::AudioDeviceModule | ==> | webrtc::AudioProcessing | ==> | webrtc::AudioSendStream | ==> | cricket::MediaChannel::NetworkInterface |
|                           |     |                         |     |                         |     |                                         |
-----------------------------     ---------------------------     ---------------------------     -------------------------------------------
                                                 

这里来看下 webrtc::AudioSender/webrtc::AudioSendStream 中音频数据编码,RTP 包打包及发送控制的设计和实现。webrtc::AudioSender/webrtc::AudioSendStream 的实现位于 webrtc/audio/audio_send_stream.h / webrtc/audio/audio_send_stream.cc,相关的类层次结构如下图:

webrtc::AudioSender/webrtc::AudioSendStream

在 RTC 中,音频数据的编码发送不同于 RTMP 之类的推流方案的地方在于,在 RTC 中,音频编码码率需要根据探测到的网络条件,和接收端发回来的 RTCP 包,动态地调整变换;同时,由于没有 TCP 这样的传输质量保障机制,需要根据探测的网络状态,和 RTCP 反馈包,基于 RTP 对传输过程做控制,如 NACK,FEC 等。因而在 RTC 中,音频数据编码发送是一个相当复杂的过程。

webrtc::AudioSendStream 的设计与实现主要可以从两个角度来看,一是配置和控制,二是数据流。

对于配置和控制,可以对 webrtc::AudioSendStream 执行的配置和控制主要有如下这些:

  • 配置音频编码的编码器及编码参数,如最大码率、最小码率、payload type、FEC 等;
  • 配置用于把 RTP 发送到网络的 webrtc::Transport 、加密参数及静音控制等;
  • webrtc::AudioSendStream 的生命周期控制,如启动停止;
  • 设置网络传输上的额外开销大小,及动态更新的分配码率。

对于数据流,一是采集处理的音频数据被送进 webrtc::AudioSendStream 以做编码和发送处理;二是网络传回的 RTCP 包,以对编码发送过程产生影响。

传回的 RTCP 包和 webrtc::AudioSendStream 的控制接口,共同构成音频数据编码及发送控制过程的信息来源。

webrtc::AudioSendStream 的实现中,最主要的数据处理流程 —— 音频数据编码、发送过程,及相关模块如下图:

webrtc::AudioSendStream Send Audio

这个图中的箭头表示数据流动的方向,数据在各个模块中处理的先后顺序为自左向右。

webrtc::AudioSendStream 的实现的数据处理流程中,输入数据为 PCM,来自于 webrtc::AudioTransport,输出则为 RTP 包,被送给 webrtc::Transport 发出去。

webrtc::AudioSendStream 的实现内部,数据首先会经过 ACM 模块 来做编码,随后经过编码的音频帧送进 rtp_rtcp 模块 打成 RTP 包,然后 RTP 包被送进 pacing 模块 做平滑发送控制,最后再在 rtp_rtcp 模块 中被给到webrtc::Transport 发出去。

站在 webrtc::AudioSendStream 的视角,基于抽象的模块及接口,搭建数据处理流水线是这个组件的机制设计,各个模块及接口的具体实现是机制下的策略。这里先来看一下,关于 webrtc::AudioSendStream 的实现的这个数据处理流水线的搭建过程。

webrtc::AudioSendStream 的实现加进 webrtc::AudioTransport

webrtc::AudioSendStream 的音频 PCM 数据来源于 webrtc::AudioTransportwebrtc::AudioSendStream 的实现被作为 webrtc::AudioTransport 的一个 webrtc::AudioSender 加进 webrtc::AudioTransport,在 webrtc::AudioSendStream 的生命周期函数 Start() 被调用时执行,这个添加的过程大体如下:

#0  webrtc::AudioTransportImpl::UpdateAudioSenders(std::vector<webrtc::AudioSender*, std::allocator<webrtc::AudioSender*> >, int, unsigned long) ()
    at webrtc/audio/audio_transport_impl.cc:262
#1  webrtc::internal::AudioState::UpdateAudioTransportWithSendingStreams() () at webrtc/audio/audio_state.cc:172
#2  webrtc::internal::AudioState::AddSendingStream(webrtc::AudioSendStream*, int, unsigned long) ()
    at webrtc/audio/audio_state.cc:100
#3  webrtc::internal::AudioSendStream::Start() () at webrtc/audio/audio_send_stream.cc:370

webrtc::AudioSendStream 把它自己加进 webrtc::AudioStatewebrtc::AudioState 把新加的 webrtc::AudioSendStream 和之前已经添加的 webrtc::AudioSendStream 一起更新进 webrtc::AudioTransport。这个过程有几个值得关注的地方:

  • webrtc::AudioTransport 支持把录制获得的同一份数据同时发送给多个 webrtc::AudioSender/webrtc::AudioSendStreamwebrtc::AudioSendStream 用于管理音频数据的编码和编码数据的发送控制,这也就意味着,WebRTC 的音频数据处理管线,支持同时把录制获得的音频数据,以不同的编码方式和编码数据发送控制机制及策略发送到不同的网络,比如一路发送到基于 UDP 传输的 RTC 网络,另一路发送到基于 TCP 传输的 RTMP 网络。

  • 如果添加的 webrtc::AudioSendStream 是第一个 webrtc::AudioSendStreamwebrtc::AudioState 还会自动地初始化并启动录音。

webrtc::AudioSendStream 实现内部的数据处理管线搭建

webrtc::AudioSendStream 实现内部的数据处理管线是分步骤搭建的完成的。我们围绕着上面的 webrtc::AudioSendStream Send Audio 图 来看这个过程。

webrtc::AudioSendStream 对象创建,也就是 webrtc::voe::(anonymous namespace)::ChannelSend 对象创建时,会创建一些关键对象,并建立部分各个对象之间的联系,这个调用过程如下:

#0  webrtc::voe::(anonymous namespace)::ChannelSend::ChannelSend(webrtc::Clock*, webrtc::TaskQueueFactory*, webrtc::Transport*, webrtc::RtcpRttStats*, webrtc::RtcEventLog*, webrtc::FrameEncryptorInterface*, webrtc::CryptoOptions const&, bool, int, unsigned int, rtc::scoped_refptr<webrtc::FrameTransformerInterface>, webrtc::TransportFeedbackObserver*) () at webrtc/audio/channel_send.cc:450
#2  webrtc::voe::CreateChannelSend(webrtc::Clock*, webrtc::TaskQueueFactory*, webrtc::Transport*, webrtc::RtcpRttStats*, webrtc::RtcEventLog*, webrtc::FrameEncryptorInterface*, webrtc::CryptoOptions const&, bool, int, unsigned int, rtc::scoped_refptr<webrtc::FrameTransformerInterface>, webrtc::TransportFeedbackObserver*) () at webrtc/audio/channel_send.cc:953
#3  webrtc::internal::AudioSendStream::AudioSendStream(webrtc::Clock*, webrtc::AudioSendStream::Config const&, rtc::scoped_refptr<webrtc::AudioState> const&, webrtc::TaskQueueFactory*, webrtc::RtpTransportControllerSendInterface*, webrtc::BitrateAllocatorInterface*, webrtc::RtcEventLog*, webrtc::RtcpRttStats*, absl::optional<webrtc::RtpState> const&) () at webrtc/audio/audio_send_stream.cc:118
#4  webrtc::internal::Call::CreateAudioSendStream(webrtc::AudioSendStream::Config const&) () at webrtc/call/call.cc:897

webrtc::AudioSendStream 通过 webrtc::Call 创建,传入了 webrtc::AudioSendStream::Config 包含与编解码、RTP、加密相关,及 webrtc::Transport 等各种配置。

webrtc::voe::(anonymous namespace)::ChannelSend 对象的构造函数如下:

ChannelSend::ChannelSend(
    Clock* clock,
    TaskQueueFactory* task_queue_factory,
    Transport* rtp_transport,
    RtcpRttStats* rtcp_rtt_stats,
    RtcEventLog* rtc_event_log,
    FrameEncryptorInterface* frame_encryptor,
    const webrtc::CryptoOptions& crypto_options,
    bool extmap_allow_mixed,
    int rtcp_report_interval_ms,
    uint32_t ssrc,
    rtc::scoped_refptr<FrameTransformerInterface> frame_transformer,
    TransportFeedbackObserver* feedback_observer)
    : ssrc_(ssrc),
      event_log_(rtc_event_log),
      _timeStamp(0),  // This is just an offset, RTP module will add it's own
                      // random offset
      input_mute_(false),
      previous_frame_muted_(false),
      _includeAudioLevelIndication(false),
      rtcp_observer_(new VoERtcpObserver(this)),
      feedback_observer_(feedback_observer),
      rtp_packet_pacer_proxy_(new RtpPacketSenderProxy()),
      retransmission_rate_limiter_(
          new RateLimiter(clock, kMaxRetransmissionWindowMs)),
      frame_encryptor_(frame_encryptor),
      crypto_options_(crypto_options),
      encoder_queue_(task_queue_factory->CreateTaskQueue(
          "AudioEncoder",
          TaskQueueFactory::Priority::NORMAL)),
      fixing_timestamp_stall_(
          !field_trial::IsDisabled("WebRTC-Audio-FixTimestampStall")) {
  audio_coding_.reset(AudioCodingModule::Create(AudioCodingModule::Config()));

  RtpRtcpInterface::Configuration configuration;
  configuration.bandwidth_callback = rtcp_observer_.get();
  configuration.transport_feedback_callback = feedback_observer_;
  configuration.clock = (clock ? clock : Clock::GetRealTimeClock());
  configuration.audio = true;
  configuration.outgoing_transport = rtp_transport;

  configuration.paced_sender = rtp_packet_pacer_proxy_.get();

  configuration.event_log = event_log_;
  configuration.rtt_stats = rtcp_rtt_stats;
  configuration.retransmission_rate_limiter =
      retransmission_rate_limiter_.get();
  configuration.extmap_allow_mixed = extmap_allow_mixed;
  configuration.rtcp_report_interval_ms = rtcp_report_interval_ms;
  configuration.rtcp_packet_type_counter_observer = this;

  configuration.local_media_ssrc = ssrc;

  rtp_rtcp_ = ModuleRtpRtcpImpl2::Create(configuration);
  rtp_rtcp_->SetSendingMediaStatus(false);

  rtp_sender_audio_ = std::make_unique<RTPSenderAudio>(configuration.clock,
                                                       rtp_rtcp_->RtpSender());

  // Ensure that RTCP is enabled by default for the created channel.
  rtp_rtcp_->SetRTCPStatus(RtcpMode::kCompound);

  int error = audio_coding_->RegisterTransportCallback(this);
  RTC_DCHECK_EQ(0, error);
  if (frame_transformer)
    InitFrameTransformerDelegate(std::move(frame_transformer));
}

webrtc::voe::(anonymous namespace)::ChannelSend 对象的构造函数的执行过程如下:

  • 创建了一个 RtpPacketSenderProxy 对象;
  • 创建了一个 webrtc::AudioCodingModule 对象,建立起了下图中标号为 1 的这条连接;
  • 创建了一个 webrtc::ModuleRtpRtcpImpl2 对象,在创建这个对象时传入的 configuration 参数的 outgoing_transport 配置项指向了传入的 webrtc::Transport,建立起了下图中标号为 10 的这条连接,configuration 参数的 paced_sender 配置项指向了前面创建的 RtpPacketSenderProxy 对象,建立起了下图中标号为 7 的这条连接;
  • 创建了一个 webrtc::RTPSenderAudio 对象,通过 rtp_sender_audio_configrtp_sender 配置项传入了从 webrtc::ModuleRtpRtcpImpl2 对象获得的 webrtc::RTPSender 对象,建立起了下图中标号为 56 的这条连接;
  • this 作为 webrtc::AudioPacketizationCallback 注册给了 webrtc::AudioCodingModule 对象,这样就建立起了下图中标号为 2 的这条连接。
ChannelSend Pipeline

图中标为绿色的模块为这个阶段已经接入 webrtc::voe::(anonymous namespace)::ChannelSend 的模块,标为黄色的则为那些还没有接进来的模块;实现箭头表示这个阶段已经建立的连接,虚线箭头则表示还没有建立的连接。

ChannelSendRegisterSenderCongestionControlObjects() 函数中,PacedSender 发送控制相关的模块被接进来:

#0  webrtc::voe::(anonymous namespace)::ChannelSend::RegisterSenderCongestionControlObjects(webrtc::RtpTransportControllerSendInterface*, webrtc::RtcpBandwidthObserver*) () at webrtc/audio/channel_send.cc:713
#1  webrtc::internal::AudioSendStream::ConfigureStream(webrtc::AudioSendStream::Config const&, bool) ()
    at webrtc/audio/audio_send_stream.cc:303
#2  webrtc::internal::AudioSendStream::AudioSendStream(webrtc::Clock*, webrtc::AudioSendStream::Config const&, rtc::scoped_refptr<webrtc::AudioState> const&, webrtc::TaskQueueFactory*, webrtc::RtpTransportControllerSendInterface*, webrtc::BitrateAllocatorInterface*, webrtc::RtcEventLog*, absl::optional<webrtc::RtpState> const&, std::unique_ptr<webrtc::voe::ChannelSendInterface, std::default_delete<webrtc::voe::ChannelSendInterface> >) ()
    at webrtc/audio/audio_send_stream.cc:170
#3  webrtc::internal::AudioSendStream::AudioSendStream(webrtc::Clock*, webrtc::AudioSendStream::Config const&, rtc::scoped_refptr<webrtc::AudioState> const&, webrtc::TaskQueueFactory*, webrtc::RtpTransportControllerSendInterface*, webrtc::BitrateAllocatorInterface*, webrtc::RtcEventLog*, webrtc::RtcpRttStats*, absl::optional<webrtc::RtpState> const&) () at webrtc/audio/audio_send_stream.cc:110

这个操作也发生在 webrtc::AudioSendStream 对象的创建期间。ChannelSendRegisterSenderCongestionControlObjects() 函数的实现如下:

void ChannelSend::RegisterSenderCongestionControlObjects(
    RtpTransportControllerSendInterface* transport,
    RtcpBandwidthObserver* bandwidth_observer) {
  RTC_DCHECK_RUN_ON(&worker_thread_checker_);
  RtpPacketSender* rtp_packet_pacer = transport->packet_sender();
  PacketRouter* packet_router = transport->packet_router();

  RTC_DCHECK(rtp_packet_pacer);
  RTC_DCHECK(packet_router);
  RTC_DCHECK(!packet_router_);
  rtcp_observer_->SetBandwidthObserver(bandwidth_observer);
  rtp_packet_pacer_proxy_->SetPacketPacer(rtp_packet_pacer);
  rtp_rtcp_->SetStorePacketsStatus(true, 600);
  packet_router_ = packet_router;
}

ChannelSend 从传入的 webrtc::RtpTransportControllerSendInterface 对象中获得 webrtc::RtpPacketSenderwebrtc::PacketRouterwebrtc::RtpPacketSender 被设置给前面创建的 RtpPacketSenderProxy 对象,这样就建立起了前面的图中标号为 8 的这条连接,获得的 webrtc::PacketRouter 保存起来备用。

这样 webrtc::AudioSendStream 内部的数据处理管线的状态变为如下图所示:

ChannelSend Pipeline 2

音频编码器在 webrtc::AudioSendStream 的配置接口 Reconfigure() 被调用时创建,并被注入进 webrtc::AudioCodingModule。这个调用过程如下:

#0  webrtc::(anonymous namespace)::AudioCodingModuleImpl::ModifyEncoder(rtc::FunctionView<void (std::unique_ptr<webrtc::AudioEncoder, std::default_delete<webrtc::AudioEncoder> >*)>) () at webrtc/modules/audio_coding/acm2/audio_coding_module.cc:320
#1  webrtc::AudioCodingModule::SetEncoder(std::unique_ptr<webrtc::AudioEncoder, std::default_delete<webrtc::AudioEncoder> >) ()
    at webrtc/modules/audio_coding/include/audio_coding_module.h:96
#2  webrtc::voe::(anonymous namespace)::ChannelSend::SetEncoder(int, std::unique_ptr<webrtc::AudioEncoder, std::default_delete<webrtc::AudioEncoder> >) () at webrtc/audio/channel_send.cc:588
#3  webrtc::internal::AudioSendStream::SetupSendCodec(webrtc::AudioSendStream::Config const&) ()
    at webrtc/audio/audio_send_stream.cc:657
#4  webrtc::internal::AudioSendStream::ReconfigureSendCodec(webrtc::AudioSendStream::Config const&) ()
    at webrtc/audio/audio_send_stream.cc:688
#5  webrtc::internal::AudioSendStream::ConfigureStream(webrtc::AudioSendStream::Config const&, bool) ()
    at webrtc/audio/audio_send_stream.cc:323
#6  webrtc::internal::AudioSendStream::Reconfigure(webrtc::AudioSendStream::Config const&) ()
    at webrtc/audio/audio_send_stream.cc:195
#7  cricket::WebRtcVoiceMediaChannel::WebRtcAudioSendStream::ReconfigureAudioSendStream() ()
    at webrtc/media/engine/webrtc_voice_engine.cc:1181

来看一下 webrtc::AudioSendStream 中创建音频编码器的过程:

// Apply current codec settings to a single voe::Channel used for sending.
bool AudioSendStream::SetupSendCodec(const Config& new_config) {
  RTC_DCHECK(new_config.send_codec_spec);
  const auto& spec = *new_config.send_codec_spec;

  RTC_DCHECK(new_config.encoder_factory);
  std::unique_ptr<AudioEncoder> encoder =
      new_config.encoder_factory->MakeAudioEncoder(
          spec.payload_type, spec.format, new_config.codec_pair_id);

  if (!encoder) {
    RTC_DLOG(LS_ERROR) << "Unable to create encoder for "
                       << rtc::ToString(spec.format);
    return false;
  }

  // If a bitrate has been specified for the codec, use it over the
  // codec's default.
  if (spec.target_bitrate_bps) {
    encoder->OnReceivedTargetAudioBitrate(*spec.target_bitrate_bps);
  }

  // Enable ANA if configured (currently only used by Opus).
  if (new_config.audio_network_adaptor_config) {
    if (encoder->EnableAudioNetworkAdaptor(
            *new_config.audio_network_adaptor_config, event_log_)) {
      RTC_LOG(LS_INFO) << "Audio network adaptor enabled on SSRC "
                       << new_config.rtp.ssrc;
    } else {
      RTC_LOG(LS_INFO) << "Failed to enable Audio network adaptor on SSRC "
                       << new_config.rtp.ssrc;
    }
  }

  // Wrap the encoder in an AudioEncoderCNG, if VAD is enabled.
  if (spec.cng_payload_type) {
    AudioEncoderCngConfig cng_config;
    cng_config.num_channels = encoder->NumChannels();
    cng_config.payload_type = *spec.cng_payload_type;
    cng_config.speech_encoder = std::move(encoder);
    cng_config.vad_mode = Vad::kVadNormal;
    encoder = CreateComfortNoiseEncoder(std::move(cng_config));

    RegisterCngPayloadType(*spec.cng_payload_type,
                           new_config.send_codec_spec->format.clockrate_hz);
  }

  // Wrap the encoder in a RED encoder, if RED is enabled.
  if (spec.red_payload_type) {
    AudioEncoderCopyRed::Config red_config;
    red_config.payload_type = *spec.red_payload_type;
    red_config.speech_encoder = std::move(encoder);
    encoder = std::make_unique<AudioEncoderCopyRed>(std::move(red_config));
  }

  // Set currently known overhead (used in ANA, opus only).
  // If overhead changes later, it will be updated in UpdateOverheadForEncoder.
  {
    MutexLock lock(&overhead_per_packet_lock_);
    size_t overhead = GetPerPacketOverheadBytes();
    if (overhead > 0) {
      encoder->OnReceivedOverhead(overhead);
    }
  }

  StoreEncoderProperties(encoder->SampleRateHz(), encoder->NumChannels());
  channel_send_->SetEncoder(new_config.send_codec_spec->payload_type,
                            std::move(encoder));

  return true;
}

RTC 的音频编码比较特别的地方,一是编码的数据主要是语音,二是编码策略总是与弱网对抗紧密相关。从 AudioSendStream::SetupSendCodec() 函数创建音频编码器的过程也能看出来。在实际的音频数据编码器如 OPUS,AAC 等外面,还可以套上舒适噪音编码器,或可以生成冗余帧的 RED 编码器。

音频编码器创建完成之后,前面的图中标号为 34 的两条连接就建立起来的。

这样 webrtc::AudioSendStream 内部的数据处理管线的状态变为如下图所示:

ChannelSend Pipeline 3

webrtc::AudioSendStream 的生命周期函数 Start() 被调用时,webrtc::PacketRouterwebrtc::ModuleRtpRtcpImpl2 被连接了起来,这个调用过程如下:

#0  webrtc::voe::(anonymous namespace)::ChannelSend::StartSend() () at webrtc/audio/channel_send.cc:531
#1  webrtc::internal::AudioSendStream::Start() () at webrtc/audio/audio_send_stream.cc:368

ChannelSend::StartSend() 函数实现如下:

void ChannelSend::StartSend() {
  RTC_DCHECK_RUN_ON(&worker_thread_checker_);
  RTC_DCHECK(!sending_);
  sending_ = true;

  RTC_DCHECK(packet_router_);
  packet_router_->AddSendRtpModule(rtp_rtcp_.get(), /*remb_candidate=*/false);
  rtp_rtcp_->SetSendingMediaStatus(true);
  int ret = rtp_rtcp_->SetSendingStatus(true);
  RTC_DCHECK_EQ(0, ret);

  // It is now OK to start processing on the encoder task queue.
  encoder_queue_.PostTask([this] {
    RTC_DCHECK_RUN_ON(&encoder_queue_);
    encoder_queue_is_active_ = true;
  });
}

这样前面的图中标号为 9 的这条连接也建立起了。 webrtc::AudioSendStream 内部的数据处理管线最终建立完成。

将上面图中各箭头旁边的标号按照建立的次序来标,则如下图所示:

ChannelSend Pipeline 4

总结下来,webrtc::AudioSendStream 内部的数据处理管线分 4 步完成了建立。

本文的分析中,含有一些函数调用栈的信息,函数调用栈的信息中甚至包含了代码所在的源文件及行号。这里的分析基于 OpenRTCClient 中的 WebRTC M98 的源码进行。

后面再分模块深入研究音频数据编码及发送控制的过程。

Done.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容