1. 前言

音频模块是WebRTC非常重要的部分,音频模块中的NetEq是WebRTC的三大核心技术(NetEq/GCC/音频3A)之一,我们分七部分介绍该模块,本文是第二部分(发送端)。这个专题包括:

2. 音频采集&编码&发送

2.1. 调用堆栈

2.2. RTP报文的数据生成和发送

WebRTC支持多种平台的音频采集能力,包括Android的OpenSL ES、AAudio,也支持Linux的ALSA。OpenSL ES是Android最早商用、最为成熟的原生库,而AAudio是在 Android O 版本中引入的全新 Android C API,此API 专为低延迟的高性能音频应用而设计。音频应用,特别是在线KTV等对时延要求特别高的场景,都会优先使用AAudio。Alsa是Advanced Linux Sound Architecture的缩写,即高级Linux声音架构,Alsa在Linux操作系统上提供了对音频的支持。当前WebRTC还支持IOS、Windows、Mac。

这些音频库在采集到音频后,都会调用AudioDeviceBuffer::DeliverRecordedData(),进入到编码、发送环节。函数第11行用到的audio_transport_cb_是在WebRtcVoiceEngine::Init()中完成注册的,真实调用的是AudioTransportImpl::RecordedDataIsAvailable(),该函数最终执行encoder_queue_.PostTask()。简言之,录制线程将录制的麦克风音频数据抛上来之后,送进编码线程,后者完成音频编码和网络发送的工作。

int32_t AudioDeviceBuffer::DeliverRecordedData() {
  if (!audio_transport_cb_) {
    RTC_LOG(LS_WARNING) << "Invalid audio transport";
    return 0;
  }
  const size_t frames = rec_buffer_.size() / rec_channels_;
  const size_t bytes_per_frame = rec_channels_ * sizeof(int16_t);
  uint32_t new_mic_level_dummy = 0;
  uint32_t total_delay_ms = play_delay_ms_ + rec_delay_ms_;
  /*Call AudioTransportImpl::RecordedDataIsAvailable()*/
  int32_t res = audio_transport_cb_->RecordedDataIsAvailable(
      rec_buffer_.data(), frames, bytes_per_frame, rec_channels_,
      rec_sample_rate_, total_delay_ms, 0, 0, typing_status_,
      new_mic_level_dummy, capture_timestamp_ns_);
  if (res == -1) {
    RTC_LOG(LS_ERROR) << "RecordedDataIsAvailable() failed";
  }
  return 0;
}

void WebRtcVoiceEngine::Init() {
  ...
  // Connect the ADM to our audio path.
  adm()->RegisterAudioCallback(audio_state()->audio_transport());
  ...
}
int32_t AudioDeviceBuffer::RegisterAudioCallback(
    AudioTransport* audio_callback) {
  ...
  audio_transport_cb_ = audio_callback;
  return 0;
}

int32_t AudioTransportImpl::RecordedDataIsAvailable(
    const void* audio_data,
    size_t number_of_frames,
    size_t bytes_per_sample,
    size_t number_of_channels,
    uint32_t sample_rate,
    uint32_t audio_delay_milliseconds,
    int32_t /*clock_drift*/,
    uint32_t /*volume*/,
    bool key_pressed,
    uint32_t& /*new_mic_volume*/,
    absl::optional<int64_t>
        estimated_capture_time_ns) {  // NOLINT: to avoid changing APIs
  int send_sample_rate_hz = 0;
  size_t send_num_channels = 0;
  bool swap_stereo_channels = false;
  {
    MutexLock lock(&capture_lock_);
    send_sample_rate_hz = send_sample_rate_hz_;
    send_num_channels = send_num_channels_;
    swap_stereo_channels = swap_stereo_channels_;
  }

  std::unique_ptr<AudioFrame> audio_frame(new AudioFrame());
  InitializeCaptureFrame(sample_rate, send_sample_rate_hz, number_of_channels,
                         send_num_channels, audio_frame.get());
  voe::RemixAndResample(static_cast<const int16_t*>(audio_data),
                        number_of_frames, number_of_channels, sample_rate,
                        &capture_resampler_, audio_frame.get());
  ProcessCaptureFrame(audio_delay_milliseconds, key_pressed,
                      swap_stereo_channels, audio_processing_,
                      audio_frame.get());

  if (estimated_capture_time_ns) {
    audio_frame->set_absolute_capture_timestamp_ms(*estimated_capture_time_ns /
                                                   1000000);
  }

  if (async_audio_processing_){
    async_audio_processing_->Process(std::move(audio_frame));
  }else{
    SendProcessedData(std::move(audio_frame));
  }

  return 0;
}

void AudioTransportImpl::SendProcessedData(
    std::unique_ptr<AudioFrame> audio_frame) {
  MutexLock lock(&capture_lock_);
  if (audio_senders_.empty())
    return;

  auto it = audio_senders_.begin();
  while (++it != audio_senders_.end()) {
    auto audio_frame_copy = std::make_unique<AudioFrame>();
    audio_frame_copy->CopyFrom(*audio_frame);
    (*it)->SendAudioData(std::move(audio_frame_copy));
  }
  // Send the original frame to the first stream w/o copying.
  /*Call AudioSendStream::SendAudioData()*/
  (*audio_senders_.begin())->SendAudioData(std::move(audio_frame));
}

void AudioSendStream::SendAudioData(std::unique_ptr<AudioFrame> audio_frame) {
  ...
  /*Call ChannelSend::ProcessAndEncodeAudio()*/
  channel_send_->ProcessAndEncodeAudio(std::move(audio_frame));
}

void ChannelSend::ProcessAndEncodeAudio(
    std::unique_ptr<AudioFrame> audio_frame) {
  ...
  encoder_queue_.PostTask(
      [this, audio_frame = std::move(audio_frame)]() mutable {
        ...
        // This call will trigger AudioPacketizationCallback::SendData if
        // encoding is done and payload is ready for packetization and
        // transmission. Otherwise, it will return without invoking the
        // callback.
        /*Call AudioCodingModuleImpl::Add10MsData()*/
        if (audio_coding_->Add10MsData(*audio_frame) < 0) {
          RTC_DLOG(LS_ERROR) << "ACM::Add10MsData() failed.";
          return;
        }
      });
}

音频编码线程执行AudioCodingModuleImpl::Add10MsData(),完成了3个阶段的工作:

  • 采用特定的音频格式实现音频原始数据(PCM数据)的编码,这部分的细节参考音频编码章节;
  • 基于RTP格式生成合适的RTP Header,并将音频编码后的数据作为Payload封装到RTP报文中,这部分工作参考RTPSenderAudio::SendAudio();
  • 将RTP报文送入网络线程,然后进入统一的QoS控制,包括BWE估计、音频带宽分配、平滑发送,这部分工作参考RTPSender::SendToNetwork()及后续流程。
// Add 10MS of raw (PCM) audio data to the encoder.
int AudioCodingModuleImpl::Add10MsData(const AudioFrame& audio_frame) {
  MutexLock lock(&acm_mutex_);
  int r = Add10MsDataInternal(audio_frame, &input_data_);
  return r < 0
             ? r
             : Encode(input_data_, audio_frame.absolute_capture_timestamp_ms());
}

int32_t AudioCodingModuleImpl::Encode(
    const InputData& input_data,
    absl::optional<int64_t> absolute_capture_timestamp_ms) {
  ...
  // Clear the buffer before reuse - encoded data will get appended.
  encode_buffer_.Clear();
  /*Call AudioEncoder::Encode()*/
  encoded_info = encoder_stack_->Encode(
      rtp_timestamp,
      rtc::ArrayView<const int16_t>(
          input_data.audio,
          input_data.audio_channel * input_data.length_per_channel),
      &encode_buffer_);
  ...
  {
    MutexLock lock(&callback_mutex_);
    if (packetization_callback_) {
      /*Call ChannelSend::SendData*/
      packetization_callback_->SendData(
          frame_type, encoded_info.payload_type, encoded_info.encoded_timestamp,
          encode_buffer_.data(), encode_buffer_.size(),
          absolute_capture_timestamp_ms.value_or(-1));
    }
  }
  previous_pltype_ = encoded_info.payload_type;
  return static_cast<int32_t>(encode_buffer_.size());
}

ChannelSend::ChannelSend(
    ...)
    : ...{
  audio_coding_ = AudioCodingModule::Create();
  ...
  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);
  ...
}

int AudioCodingModuleImpl::RegisterTransportCallback(
    AudioPacketizationCallback* transport) {
  MutexLock lock(&callback_mutex_);
  packetization_callback_ = transport;
  return 0;
}

int32_t ChannelSend::SendData(AudioFrameType frameType,
                              uint8_t payloadType,
                              uint32_t rtp_timestamp,
                              const uint8_t* payloadData,
                              size_t payloadSize,
                              int64_t absolute_capture_timestamp_ms) {
  rtc::ArrayView<const uint8_t> payload(payloadData, payloadSize);
  ...
  return SendRtpAudio(frameType, payloadType, rtp_timestamp, payload,
                      absolute_capture_timestamp_ms);
}

int32_t ChannelSend::SendRtpAudio(AudioFrameType frameType,
                                  uint8_t payloadType,
                                  uint32_t rtp_timestamp,
                                  rtc::ArrayView<const uint8_t> payload,
                                  int64_t absolute_capture_timestamp_ms) {
  ...
  // Push data from ACM to RTP/RTCP-module to deliver audio frame for
  // packetization.

  /*Call ModuleRtpRtcpImpl2::OnSendingRtpFrame()*/
  if (!rtp_rtcp_->OnSendingRtpFrame(rtp_timestamp,
                                    // Leaving the time when this frame was
                                    // received from the capture device as
                                    // undefined for voice for now.
                                    -1, payloadType,
                                    /*force_sender_report=*/false)) {
    return -1;
  }

  // This call will trigger Transport::SendPacket() from the RTP/RTCP module.
  if (!rtp_sender_audio_->SendAudio(/*Call RTPSenderAudio::SendAudio*/
          frameType, payloadType, rtp_timestamp + rtp_rtcp_->StartTimestamp(),
          payload.data(), payload.size(), absolute_capture_timestamp_ms)) {
    RTC_DLOG(LS_ERROR)
        << "ChannelSend::SendData() failed to send data to RTP/RTCP module";
    return -1;
  }

  return 0;
}

bool RTPSenderAudio::SendAudio(AudioFrameType frame_type,
                               int8_t payload_type,
                               uint32_t rtp_timestamp,
                               const uint8_t* payload_data,
                               size_t payload_size) {
  return SendAudio(frame_type, payload_type, rtp_timestamp, payload_data,
                   payload_size,
                   // TODO(bugs.webrtc.org/10739) replace once plumbed.
                   /*absolute_capture_timestamp_ms=*/-1);
}

bool RTPSenderAudio::SendAudio(AudioFrameType frame_type,
                               int8_t payload_type,
                               uint32_t rtp_timestamp,
                               const uint8_t* payload_data,
                               size_t payload_size,
                               int64_t absolute_capture_timestamp_ms) {
  ...
  /*创建RTP报文缓冲区并设置包头字段,包括M/PT/Timestamp/SSRC/CSRC*/
  std::unique_ptr<RtpPacketToSend> packet = rtp_sender_->AllocatePacket();
  packet->SetMarker(MarkerBit(frame_type, payload_type));
  packet->SetPayloadType(payload_type);
  packet->SetTimestamp(rtp_timestamp);
  packet->set_capture_time(clock_->CurrentTime());
  // Update audio level extension, if included.
  packet->SetExtension<AudioLevel>(
      frame_type == AudioFrameType::kAudioFrameSpeech, audio_level_dbov);
  ...

  /*调整RTP报文的缓冲区大小,并将音频编码后的数据拷贝到RTP Payload指向的内存区*/
  uint8_t* payload = packet->AllocatePayload(payload_size);
  if (!payload)  // Too large payload buffer.
    return false;
  memcpy(payload, payload_data, payload_size);

  {
    MutexLock lock(&send_audio_mutex_);
    last_payload_type_ = payload_type;
  }
  packet->set_packet_type(RtpPacketMediaType::kAudio);
  packet->set_allow_retransmission(true);
  /*Call RTPSender::SendToNetwork*/
  bool send_result = rtp_sender_->SendToNetwork(std::move(packet));
  if (first_packet_sent_()) {
    RTC_LOG(LS_INFO) << "First audio RTP packet sent to pacer";
  }
  return send_result;
}

bool RTPSender::SendToNetwork(std::unique_ptr<RtpPacketToSend> packet) {
  auto packet_type = packet->packet_type();

  if (packet->capture_time() <= Timestamp::Zero()) {
    packet->set_capture_time(clock_->CurrentTime());
  }

  std::vector<std::unique_ptr<RtpPacketToSend>> packets;
  packets.emplace_back(std::move(packet));

  //call TaskQueuePacedSender::EnqueuePackets
  /*将RTP报文送入网络线程,然后进入统一的QoS控制,包括BWE估计、音频带宽分配、平滑发送*/
  paced_sender_->EnqueuePackets(std::move(packets));

  return true;
}

2.3. RTCP报文的发送

ChannelSend::SendRtpAudio()在将音频RTP报文加入到网络线程(实现统一的带宽分配和平滑发送)之前,尝试将积攒的RTCP报文汇聚在一个UDP Packet中,一次发送出去。由于UDP Packet有max packet size的限制(不能超过1500-28=1472,其中1500是以太网报文的最大长度,28是IP Header+UDP Header的总长度),如果UDP Packet的当前长度到达上限了,后面的RTCP报文会被丢弃(除非is_volatile这个Flag设置为False,这种情况下该类型的RTCP报文会在下次时机到来时继续发送)。

bool ModuleRtpRtcpImpl2::OnSendingRtpFrame(uint32_t timestamp,
                                           int64_t capture_time_ms,
                                           int payload_type,
                                           bool force_sender_report) {
  if (!Sending()) {
    return false;
  }
  // TODO(bugs.webrtc.org/12873): Migrate this method and it's users to use
  // optional Timestamps.
  absl::optional<Timestamp> capture_time;
  if (capture_time_ms > 0) {
    capture_time = Timestamp::Millis(capture_time_ms);
  }
  absl::optional<int> payload_type_optional;
  if (payload_type >= 0)
    payload_type_optional = payload_type;

  auto closure = [this, timestamp, capture_time, payload_type_optional,
                  force_sender_report] {
    rtcp_sender_.SetLastRtpTime(timestamp, capture_time, payload_type_optional);
    // Make sure an RTCP report isn't queued behind a key frame.
    if (rtcp_sender_.TimeToSendRTCPReport(force_sender_report)){
      /*Call RTCPSender::SendRTCP*/
      rtcp_sender_.SendRTCP(GetFeedbackState(), kRtcpReport);
    }
  };
  if (worker_queue_->IsCurrent()) {
    closure();
  } else {
    worker_queue_->PostTask(SafeTask(task_safety_.flag(), std::move(closure)));
  }
  return true;
}

int32_t RTCPSender::SendRTCP(const FeedbackState& feedback_state,
                             RTCPPacketType packet_type,
                             int32_t nack_size,
                             const uint16_t* nack_list) {
  int32_t error_code = -1;
  auto callback = [&](rtc::ArrayView<const uint8_t> packet) {
    /*Call TransportForMediaChannels::SendRtcp()*/
    if (transport_->SendRtcp(packet.data(), packet.size())) {
      error_code = 0;
      if (event_log_) {
        event_log_->Log(std::make_unique<RtcEventRtcpPacketOutgoing>(packet));
      }
    }
  };
  absl::optional<PacketSender> sender;
  {
    MutexLock lock(&mutex_rtcp_sender_);
    sender.emplace(callback, max_packet_size_);
    auto result = ComputeCompoundRTCPPacket(feedback_state, packet_type,
                                            nack_size, nack_list, *sender);
    if (result) {
      return *result;
    }
  }
  sender->Send();

  return error_code;
}

我们看下RTCPSender::ComputeCompoundRTCPPacket()的实现细节。函数开头通过SetFlag()将RTCP报文类型设置到了report_flags_。除了该函数,其他地方也会调用SetFlag(),所以在这里统一做了处理,对report_flags_做了遍历,为每个类型的RTCP报文找到对应的序列化方案并执行。注意3个细节:

  • 执行序列化时,由于是尝试将所有的RTCP报文放在一个UDP Packet中,而后者是有Size上限的,在达到上限后,序列化会失败,但这里并没有判断这个失败,所以这些RTCP报文被默默地丢弃;
  • 在ModuleRtpRtcpImpl2::OnSendingRtpFrame()设置的kRtcpReport本身不属于任何类型的RTCP报文,其作用是触发所有已在report_flags_登记的RTCP报文尽快创建和发送。
  • 所有可以被序列化的RTCP报文的create方法都在RTCPSender::RTCPSender()中指定了,例如:

builders_[kRtcpPli] = &RTCPSender::BuildPLI;

builders_[kRtcpFir] = &RTCPSender::BuildFIR;

builders_[kRtcpNack] = &RTCPSender::BuildNACK;

absl::optional<int32_t> RTCPSender::ComputeCompoundRTCPPacket(
    const FeedbackState& feedback_state,
    RTCPPacketType packet_type,
    int32_t nack_size,
    const uint16_t* nack_list,
    PacketSender& sender) {
  if (method_ == RtcpMode::kOff) {
    RTC_LOG(LS_WARNING) << "Can't send rtcp if it is disabled.";
    return -1;
  }
  // Add the flag as volatile. Non volatile entries will not be overwritten.
  // The new volatile flag will be consumed by the end of this call.
  SetFlag(packet_type, true);
  ...
  auto it = report_flags_.begin();
  while (it != report_flags_.end()) {
    uint32_t rtcp_packet_type = it->type;

    if (it->is_volatile) {
      report_flags_.erase(it++);
    } else {
      ++it;
    }

    // If there is a BYE, don't append now - save it and append it
    // at the end later.
    if (rtcp_packet_type == kRtcpBye) {
      create_bye = true;
      continue;
    }
    auto builder_it = builders_.find(rtcp_packet_type);
    if (builder_it == builders_.end()) {
      RTC_DCHECK_NOTREACHED()
          << "Could not find builder for packet type " << rtcp_packet_type;
    } else {
      BuilderFunc func = builder_it->second;
      (this->*func)(context, sender);
    }
  }
  ...
  return absl::nullopt;
}

void RTCPSender::SetFlag(uint32_t type, bool is_volatile) {
  if (type & kRtcpAnyExtendedReports) {
    report_flags_.insert(ReportFlag(kRtcpAnyExtendedReports, is_volatile));
  } else {
    report_flags_.insert(ReportFlag(type, is_volatile));
  }
}

RTCPSender::RTCPSender(Configuration config)
    : ... {
  builders_[kRtcpSr] = &RTCPSender::BuildSR;
  builders_[kRtcpRr] = &RTCPSender::BuildRR;
  builders_[kRtcpSdes] = &RTCPSender::BuildSDES;
  builders_[kRtcpPli] = &RTCPSender::BuildPLI;
  builders_[kRtcpFir] = &RTCPSender::BuildFIR;
  builders_[kRtcpRemb] = &RTCPSender::BuildREMB;
  builders_[kRtcpBye] = &RTCPSender::BuildBYE;
  builders_[kRtcpLossNotification] = &RTCPSender::BuildLossNotification;
  builders_[kRtcpTmmbr] = &RTCPSender::BuildTMMBR;
  builders_[kRtcpTmmbn] = &RTCPSender::BuildTMMBN;
  builders_[kRtcpNack] = &RTCPSender::BuildNACK;
  builders_[kRtcpAnyExtendedReports] = &RTCPSender::BuildExtendedReports;
}

2.4. 音频编码

2.4.1. 预备知识

2.4.1.1. 主流音频编码介绍

WebRTC支持G.722、iLBC、Opus三种音频编码格式。

G.722.1是由Polycom提出的一套低码率低复杂度的宽带语音编码算法,可以对语音(300~4000Hz)和7kHz以内的音乐进行编码,采样率为16kHz。由于算法复杂度低,非常适合CPU性能不高的嵌入式平台。

iLBC (internet Low Bitrate Codec)是由GIPS开发的基于低带宽的语音编码(speech),码率为13.3 kb/s (每帧30ms)和15.2 kb/s (每帧20ms), 采样率为8K。iLBC的语音质量比G.711、G.722等都要好,更难得的是,丢包率越高,iLBC在语音质量上的优势就越明显。抗丢包能力强是iLBC最大的特点,解码算法支持PLC(Packet loss concealment,网络丢包消除),通过该技术,iLBC可忍受15%的丢包损失。

下面介绍下PLC的原理。在网络正常情况下,iLBC会记录下当前数据的相关参数和激励信号,以便在之后的数据丢失的情况下进行处理;在当前数据接收正常而之前数据包丢失的情况下,iLBC会对当前解码出的语音和之前模拟生成的语音进行平滑处理,以消除不连贯的感觉;在当前数据包丢失的情况下,iLBC会对之前记录下来的激励信号作相关处理并与随机信号进行混合,以得到模拟的激励信号,从而得到替代丢失语音的模拟语音。

Opus编码是是有损音频编码中的后进者,但表现不俗:从Opus官网的报告(https://opus-codec.org/comparison/)可以看出,Opus在各种码率/频宽下的音质表现均好于同类产品,同时在时延上也有明显优势;另一方面,Opus内部自带了FEC和PLC能力,抗丢包能力不逊于iLBC。再加上Opus是一个开源项目,没有任何专利问题,所以从诞生后很快成为了音频有损压缩领域的王者,被广泛应用于VoIP、视频会议、游戏喊麦等场景。

Opus支持如下特性:

  1. 支持从8KHz(窄带)到48KHz(全频段)的采样率;
  2. 支持从6Kbps到510Kbps的码率;
  3. 单帧编码周期从2.5ms到60ms;
  4. 支持演讲和音乐模式;
  5. 支持单声道、立体声;
  6. 支持带内FEC和丢包隐藏算法(PLC);
  7. 支持恒定码率(CBR)和可变码率(VBR)。
2.4.1.2. Opus编码和RTP封装

在RFC 6716 “Frame Duration”(https://datatracker.ietf.org/doc/html/rfc6716#section-2.1.4)一节介绍了一个RTP报文可以承载的音频数据量:

  • 一个RTP报文可以承载多个Opus Frame;
  • 一个Opus Frame的编码周期包括:2.5, 5, 10, 20, 40, or 60 ms。

编码周期越长,单个Opus Frame纳入的采样点越多,1秒内需要的RTP报文个数越少,IP(20字节)+UDP(8字节)+RTP(12字节)头部的累计开销越少,同时编码压缩率越高(当编码周期超过20ms时,编码收益变得很小),带宽利用率也越高;另一方面,编码周期会影响时延,例如60ms的编码周期比10ms的编码周期在时延上要多出50ms,而一个RTP报文承载的Opus Frame越多,端到端的延迟越大(报文需要累积一段时间后才能发送)。

假设Opus的码率是48Kbps,20ms的编码周期下一共产生48*20=960字节的数据,外加至少40字节的头部开销,一共需要1000字节。假设Opus的码率为64K,20ms的编码周期下一共产生64*20=1280字节,此时以及超出UDP报文1200字节的总长度限制,此时需要将Opus数据存入多个RTP报文中。

通过RTP分片机制(RFC 7587),一个Opus Frame可以拆分为多个RTP分片单元(Fragmentation Units,FUs),此时第一个分片包含原始RTP头部和部分载荷,后续分片仅包含分片头(4 字节)和剩余载荷。

基于以上因素考虑,我们会在时延、弱网对抗能力和带宽利用率之间平衡,一般的选择是:

  • 一个RTP报文里面存放一个Opus Frame;
  • 一个Opus Frame的编码周期为20ms;
  • 若Opus的码率过大,则通过RTP分片机制,以多个RTP报文发送一帧音频数据。

2.4.2. 代码解析

2.4.2.1. 编码器设置

在创建AudioSendStream时,会根据SDP中协商的编码器类型创建Audio Encoder,WebRTC在具体实现时采用了工厂模式,如果指定的是Opus,则会调用AudioEncoderOpusImpl::MakeAudioEncoder()创建AudioEncoderOpusImpl实例,该实例负责Opus Encoding的工作。

创建了Audio Encoder之后,经过逐层调用,最终将AudioCodingModuleImpl::encoder_stack_设置为该编码器。

如果配置中指定了RED(Redundant Encoding),则会创建AudioEncoderCopyRed实例,该实例通过上面创建的Audio Encoder实现对PCM数据的编码,得到Primary Data,再根据RED协议创建Redundant Data。从这一点上我们可以看出WebRTC在RED实现上的另一个问题:RED是通过SDP协商的,一旦协商好之后,RED就是无条件触发的,也就是在任何时候都会按相同的冗余度发送冗余数据,这一点和FEC的实现不同,后者可以实时监测丢包率情况,根据丢包率大小来选择是否开启FEC以及开启后的冗余度。

// Apply current codec settings to a single voe::Channel used for sending.
bool AudioSendStream::SetupSendCodec(const Config& new_config) {
  const auto& spec = *new_config.send_codec_spec;
  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);
  }
  ...

  // 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),
                                                    field_trials_);
  }
  ...

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

  return true;
}

template <typename... Ts>
class AudioEncoderFactoryT : public AudioEncoderFactory {
 public:
  explicit AudioEncoderFactoryT(const FieldTrialsView* field_trials) {
    field_trials_ = field_trials;
  }
  /*...*/
  std::unique_ptr<AudioEncoder> MakeAudioEncoder(
      int payload_type,
      const SdpAudioFormat& format,
      absl::optional<AudioCodecPairId> codec_pair_id) override {
    return Helper<Ts...>::MakeAudioEncoder(payload_type, format, codec_pair_id,
                                           field_trials_);
  }

  const FieldTrialsView* field_trials_;
};

std::unique_ptr<AudioEncoder> AudioEncoderOpusImpl::MakeAudioEncoder(
    const AudioEncoderOpusConfig& config,
    int payload_type) {
  if (!config.IsOk()) {
    RTC_DCHECK_NOTREACHED();
    return nullptr;
  }
  return std::make_unique<AudioEncoderOpusImpl>(config, payload_type);
}

2.4.2.2. 音频编码

音频编码由AudioEncoder及其继承类完成,继承类包括:AudioEncoderG722Impl&AudioEncoderIlbcImpl&AudioEncoderOpusImpl,同时也包括AudioEncoderCopyRed。

Red编码本质上属于网络层面的冗余编码,和G722/iLBC/Opus有本质不同,前者将编码后的数据做简单地复制,后者是将PCM原始音频数据做有损压缩编码;同时两者的调用流程也不一样。这里先不考虑RED,只关注音频的有损压缩编码。

AudioEncoder::EncodedInfo AudioEncoder::Encode(
    uint32_t rtp_timestamp,
    rtc::ArrayView<const int16_t> audio,
    rtc::Buffer* encoded) {
  ...
  EncodedInfo info = EncodeImpl(rtp_timestamp, audio, encoded);
  ...
  return info;
}

AudioEncoderG722Impl&AudioEncoderIlbcImpl&AudioEncoderOpusImpl都实现了EncodeImpl()虚函数,其中AudioEncoderOpusImpl::EncodeImpl()调用了WebRtcOpus_Encode(),在该函数内部实现了Opus编码,并在rtc::Buffer* encoded指向的内存块中存入编码后的数据。

AudioEncoder::EncodedInfo AudioEncoderOpusImpl::EncodeImpl(
    uint32_t rtp_timestamp,
    rtc::ArrayView<const int16_t> audio,
    rtc::Buffer* encoded) {
  MaybeUpdateUplinkBandwidth();

  if (input_buffer_.empty())
    first_timestamp_in_buffer_ = rtp_timestamp;

  input_buffer_.insert(input_buffer_.end(), audio.cbegin(), audio.cend());
  if (input_buffer_.size() <
      (Num10msFramesPerPacket() * SamplesPer10msFrame())) {
    return EncodedInfo();
  }

  const size_t max_encoded_bytes = SufficientOutputBufferSize();
  EncodedInfo info;
  info.encoded_bytes = encoded->AppendData(
      max_encoded_bytes, [&](rtc::ArrayView<uint8_t> encoded) {
        int status = WebRtcOpus_Encode(
            inst_, &input_buffer_[0],
            rtc::CheckedDivExact(input_buffer_.size(), config_.num_channels),
            rtc::saturated_cast<int16_t>(max_encoded_bytes), encoded.data());
        return static_cast<size_t>(status);
      });
  input_buffer_.clear()
  ...
  return info;
}

2.5. RED编码

在3.1章节给出的堆栈中,我们注意到进入音频编码环节时,WebRTC调用了AudioEncoder::Encode(),该函数会调用EncodeImpl(),这是一个纯虚函数,继承类AudioEncoderCopyRed实现了该函数。AudioEncoderCopyRed::EncodeImpl()通过调用真实的编码器AudioEncoderOpusImpl(或AudioEncoderG722Impl)实现对PCM数据的编码,还基于该编码数据实现了RED冗余。本章节讨论RED协议和编码。

2.5.1. RED的价值、实现原理及缺陷

2.5.1.1. RED的价值

在WebRTC中,RED(Redundant Encoding,冗余编码)是一种前向纠错(FEC, Forward Error Correction)技术,其核心作用是通过在音视频数据包中添加冗余信息,提升网络传输的可靠性和抗丢包能力。对于RED,我们关注两个核心技术点:

  • 冗余数据保护:RED通过将编码后的音频报文与冗余数据包打包在一起,放在同一个RTP报文中。当网络丢包发生时,接收端可以利用冗余数据恢复丢失的原始数据,无需依赖重传机制(NACK)。
  • 降低对重传的依赖:传统NACK机制需要接收端请求丢失数据的重传,但重传会引入额外延迟(尤其在高丢包率且高RTT的场景)。RED通过本地冗余数据恢复,避免了重传的延迟,适合对实时性要求高的场景(如语音通话、视频会议)。
2.5.1.2. RED的实现原理

WebRTC用一个冗余报文队列存储需要发送的Encoded Redundant Data,新来的冗余数据插入到该队列的头部,队列的长度等于设定的RED冗余度。当队列满的时候,要先将队列中已有元素全部向后移动一格,在淘汰掉最老的数据的同时腾出队列头部来存放新数据。

另一方面,RED报文会将Encoded Primary Data和Encoded Redundant Data放在一起作为RTP Payload发送,只要包括IP Header+UDP Header+RTP Header+RTP Payload小于UDP的MTU(WebRTC设置为1200)就可以。所以当Encoded Primary Data足够小的时候,可以将冗余报文队列中的所有数据和原始数据放在一个RTP报文中直接发送出去。我们就先讨论这种情况,我们假定RED冗余度为3。

当冗余报文队列为空时,新来Primary Data 1,此时发送的RED报文中只有1,同时1被插入冗余队列头部;

新来Primary Data 2后,发送的RED报文中包含1和2,同时2被插入到冗余队列头部;

新来Primary Data 3后,发送的RED报文中包含1/2/3,同时3被插入到冗余队列头部;

新来Primary Data 4后,发送的RED报文中讨论1/2/3/4,同时1被淘汰出冗余队列,4插入到队列头部;

后续新来的Primary Data 5,6等,都会包含在RED报文中,且都会引起冗余队列的右移和淘汰机制。

每个Primary Data在刚插入到冗余队列时,位于队列头部,在被淘汰之前位于队列尾部,被送到RED报文中的次数有1+N次,N=RED冗余度。

当Primary Data很小的时候,以上逻辑成立。例如当我们将Opus的码率设置为16Kbps,同时将编码周期设置为20ms时,Primary Data=16*20/8=320/8=40字节,RTP Payload Length = RED Header Length*4+1+Primary Data Length + Redundant Data Length = 4*4+1+40+40*3=177, RTP Packet Size = IP Header Length + UDP Header Legnth+ RTP Header Length + RTP Payload Length = 10+8+12+177 = 207 < 1200。

当Primary Data很大的时候,以上逻辑可能就不成立了。例如当Opus的码率被设置为128Kbps时,20ms编码周期下的Primary Data = 128*20/8=320字节,此时RTP Payload Length = RED Header Length*4+1+Primary Data Length + Redundant Data Length = 4*4+1+320+320*3=1297 > 1200了。在这种情况下冗余队列中的3份冗余数据就无法一次存入同一个RTP报文了。此时虽然设定的冗余度是3,但实际上每份音频编码数据作为Redundant Data只发送了2次(还有一次是作为原始数据发送),具体行为如下图:

2.5.1.3. RED的缺陷

RED作为一种前向纠错机制,在冗余度的设置上只允许设为100%的整数倍,如1倍冗余、2倍冗余等。对比FEC可以设置30%、50%的冗余度,RED的冗余度显得非常的粗糙。而且,根据上述讨论,受限于WebRTC将冗余数据和原始数据放到同一个RTP报文的实际约束,填充报文时实际的冗余度可能要小于设定值。

RED的开启是通过SDP协商的,一旦协商通过后就会无条件开启,在任何时候都会按照相同的冗余度发送冗余数据,这一点又区别于FEC。FEC可以实时监测丢包率,根据丢包率大小选择开启或关闭FEC,还可以设置FEC的冗余度,所以FEC在效果上要远好于RED。在实际商用时,很多RTC厂商都会尝试修改该约束,让RED冗余度随着丢包率动态调整,以节约带宽(笔者曾在一家RTC厂商做商业推广,曾有语聊App的客户抱怨实际收费的带宽远高于设定的音频码率)。

最后,RED在实现原理上只是简单地将音频数据多次发送,不涉及复杂的数学运算。对比RSFEC,M个原始报文+N个冗余报文发送出去之后,只要收到的报文总数(包括原始报文和冗余报文)不小于M,就可以将M个原始报文全部恢复回来,丢包对抗能力远高于RED,原因就在于原始报文和冗余报文之间存在着数学约束(矩阵运算)。

2.5.2. SDP协商

在使用RED时,我们需要为RED指定一个动态的Payload Type,为此我们用到了SDP的协商机制,通过关键字rtpmap,我们为一种动态的Payload Type指定编码器类型、采样率和声道数。例如:

m=audio 12345 RTP/AVP 121 0 5
a=rtpmap:121 red/8000/1
a=fmtp:121 0/5

以上SDP协商指定了当前Audio Stream使用的Payload Type包括121 (a dynamic payload type), 0 (PCM,脉冲编码调制) and 5 (DVI,自适应差分脉冲编码调制),rtpmap将121的Payload Type和RED绑定在一起,标识这种类型的报文为冗余报文(在RED报文的RTP Header的PT字段中设置)。fmtp则标识冗余报文首选的编码对象为PCM数据,其次为DVI数据(在RED Header的block PT中设置)。

2.5.3. RED协议

RED协议在RFC 2198(https://datatracker.ietf.org/doc/html/rfc2198)中定义,RED Header的格式如下:

    0                   1                    2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3  4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |F|   block PT  |  timestamp offset         |   block length    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

各字段含义如下:

F:标识后面是否还有新的头部,1表示还有新的头部,0表示这是最后一个头部。

block PT:标识RTP Payload Type,这个标识和RTP Header的PT字段不同,前者标识原始音频数据的Payload Type,如0和5(分别对应PCM和DVI),后者对于RED报文而言,就是SDP协商出来的RED对应的Payload Type(如上文中协商出来的121)。

timestamp offset:冗余数据相对于RTP Header中给出的时间戳的偏移量,是一个14bite的无符号整数。这就要求冗余数据必须晚于原始数据发出。

block length:冗余数据块的长度,不包括头部长度。

当F为0时,RED Header精简为只有F和block PT字段:

 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|0|   block PT  |
+-+-+-+-+-+-+-+-+

以下是一个具体的实例,从这个实例我们看到:在报文长度允许的情况下,RED报文其实是将原始报文和冗余报文放在一个RTP Payload中的。如果原始报文超过了1200字节,则一个RED报文就只能存放原始报文或冗余报文了。

    0                   1                    2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3  4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |V=2|P|X| CC=0  |M|      PT     |   sequence number of primary  |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |              timestamp  of primary encoding                   |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |           synchronization source (SSRC) identifier            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |1| block PT=121|  timestamp offset         |   block length    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |0| block PT=121|                                               |
   +-+-+-+-+-+-+-+-+                                               +
   |          encoded redundant data (PT=0)                        |
   +                                                               +
   +                                                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+     
   |                                                               |
   +                encoded primary data(PT=0)                     +
   /                                                               /
   +                                                               +
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

2.5.4. 代码解析

AudioEncoderCopyRed::EncodeImpl()是实际执行RED编码的函数,这个函数首先调用了一次AudioEncoder::Encode(),启动编码器(如Opus)对PCM数据进行编码,得到Primary Data,然后将redundant_encodings_中包含的所有冗余数据和primary_encoded_所包含的原始数据按照RED协议规范写入RTP Payload,再调整冗余数据队列,将primary_encoded_插入到redundant_encodings_指向的队列头部并淘汰队列尾部数据。

大家注意该函数第106~108行代码,音频编码后的原始数据被纳入冗余数据队列,由此可知该队列元素的payload_type就是真实编码时设置的Payload Type(例如PCM数据对应的Payload Type为0)。再看第70行代码,我们就可以知道RED Header的block PT其实就是真实编码时设置的Payload Type。

大家先看第99~113行代码,原始数据primary_encoded_在插入到冗余数据队列redundant_encodings_的头部之后,Payload Type被更新为red_payload_type_。所以我们回过头看第70~73行代码:

encoded->data()[header_offset] = it->first.payload_type | 0x80;

rtc::SetBE16(static_cast<uint8_t*>(encoded->data()) + header_offset + 1,

(timestamp_delta << 2) | (it->first.encoded_bytes >> 8));

encoded->data()[header_offset + 3] = it->first.encoded_bytes & 0xff;

可以看到,所有RED Header的F字段被设置为1,block PT被设置为red_payload_type_,同时还设置了timestamp offset和block length。

再看第92~93行代码:

encoded->AppendData(primary_encoded_);

encoded->data()[header_offset] = info.payload_type;

这段代码将原始报文追加到冗余数据的后面,共同作为RTP Payload的一部分。由于这是RED报文的最后一个Block,所以RED Header简化为1个字节且F字段为0(info.payload_type的最高比特位为0)。

AudioEncoder::EncodedInfo AudioEncoderCopyRed::EncodeImpl(
    uint32_t rtp_timestamp,
    rtc::ArrayView<const int16_t> audio,
    rtc::Buffer* encoded) {
  primary_encoded_.Clear();

  /*call AudioEncoder::Encode()
   *在编出RED报文之前,先对原始报文执行编码,编码后的内容存入primary_encoded_中
   */
  EncodedInfo info =
      speech_encoder_->Encode(rtp_timestamp, audio, &primary_encoded_);

  if (info.encoded_bytes == 0 || info.encoded_bytes >= kRedMaxPacketSize/*1024*/) {
    return info;
  }

  /*不管RED报文有几份,始终有一个Header标识这是最后一个RED Header*/
  size_t header_length_bytes = kRedLastHeaderLength/*1*/;

  /*计算在RED报文里面可以存放多少报文(一个原始报文+N个冗余报文),进而计算得到RED Header Length
   *判断依据:1).总大小不能超过一个UDP报文的最大大小1200
   *        2).每个冗余报文的时间戳和RTP Header中记录的时间戳的差值不能超过2^14
   */
  size_t bytes_available = max_packet_length_ - info.encoded_bytes;

  /*std::list<std::pair<EncodedInfo, rtc::Buffer>> redundant_encodings_;*/
  auto it = redundant_encodings_.begin();

  // Determine how much redundancy we can fit into our packet by
  // iterating forward. This is determined both by the length as well
  // as the timestamp difference. The latter can occur with opus DTX which
  // has timestamp gaps of 400ms which exceeds REDs timestamp delta field size.
  for (; it != redundant_encodings_.end(); it++) {
    if (bytes_available < kRedHeaderLength/*4*/ + it->first.encoded_bytes) {
      break;
    }
    if (it->first.encoded_bytes == 0) {/*如果冗余报文队列还未插满,则break*/
      break;
    }
    if (rtp_timestamp - it->first.encoded_timestamp >= kRedMaxTimestampDelta/*2^14*/) {
      /*red header的timestamp字段是14比特的无符号整形数,
       *所以当前报文和原始报文之间的时间戳的差值不能超过2^14
       */
      break;
    }
    bytes_available -= kRedHeaderLength/*4*/ + it->first.encoded_bytes;
    header_length_bytes += kRedHeaderLength/*4*/;
  }

  // Allocate room for RFC 2198 header.
  encoded->SetSize(header_length_bytes);

  // Iterate backwards and append the data.
  /*按RED协议将冗余报文存入RTP Payload中*/

  /*RED协议格式(RFC 2198)
    0                   1                    2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3  4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |F|   block PT  |  timestamp offset         |   block length    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  */

  size_t header_offset = 0;
  while (it-- != redundant_encodings_.begin()) {
    encoded->AppendData(it->second);

    const uint32_t timestamp_delta =
        info.encoded_timestamp - it->first.encoded_timestamp;
    encoded->data()[header_offset] = it->first.payload_type | 0x80;
    rtc::SetBE16(static_cast<uint8_t*>(encoded->data()) + header_offset + 1,
                 (timestamp_delta << 2) | (it->first.encoded_bytes >> 8));
    encoded->data()[header_offset + 3] = it->first.encoded_bytes & 0xff;
    header_offset += kRedHeaderLength/*4*/;
    info.redundant.push_back(it->first);
  }

  // `info` will be implicitly cast to an EncodedInfoLeaf struct, effectively
  // discarding the (empty) vector of redundant information. This is
  // intentional.
  if (header_length_bytes > kRedHeaderLength/*4*/) {
    info.redundant.push_back(info);
  }
  
  /*将原始报文追加到RTP Payload中,由于这是RED报文的最后一个Block,
   *所以RED Header简化为1个字节且F字段为0
    0 1 2 3 4 5 6 7
   +-+-+-+-+-+-+-+-+
   |0|   Block PT  |
   +-+-+-+-+-+-+-+-+
   */
  encoded->AppendData(primary_encoded_);
  encoded->data()[header_offset] = info.payload_type;

  // Shift the redundant encodings.
  /*将链表中的元素向后移动一格,淘汰掉最后一个冗余报文
   *腾出链表头部并在头部存入原始报文作为新的冗余报文
   */
  auto rit = redundant_encodings_.rbegin();
  for (auto next = std::next(rit); next != redundant_encodings_.rend();
       rit++, next = std::next(rit)) {
    rit->first = next->first;
    rit->second.SetData(next->second);
  }
  it = redundant_encodings_.begin();
  if (it != redundant_encodings_.end()) {
    it->first = info;
    it->second.SetData(primary_encoded_);
  }

  // Update main EncodedInfo.
  info.payload_type = red_payload_type_;
  info.encoded_bytes = encoded->size();
  return info;
}

The End.

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐