流媒体弱网优化之路(mediasoup)——上行引入RED编码

流媒体弱网优化之路(mediasoup)——上行引入RED编码

——
我正在的github给大家开发一个用于做实验的项目 —— github.com/qw225967/Bifrost

目标:可以让大家熟悉各类Qos能力、带宽估计能力,提供每个环节关键参数调节接口并实现一个json全配置,提供全面的可视化算法观察能力。

欢迎大家使用
——


一、AudioRed介绍

  Red封装格式前面的文章提到了很多次,本文介绍了我的一个引入的方法,并且把AudioRed在音频抗弱网中进行了测试。下面先简单介绍一下Red封装。

1.1 AudioRed有什么用?

  之前提到视频的抗丢包弱网方式有两个:NACK、FEC。与视频需要所有丢包都完全补回的机制不同,音频可能在少数丢包的时候不会造成对话理解问题,因此音频的抗弱网可以说天生就比视频好做。当然,这只是针对于比较轻度的弱网而言,在重度弱网环境下我们仍然需要去处理丢包造成的影响。
  在WebRTC前期,视频的FEC和音频的冗余其实是做在一起的,都依赖于Red封装。音频冗余的方式很有种,我们介绍一下最简单的:
在这里插入图片描述
  每一个RED封装的数据都携带前两数据,意味着所有包都备份了2份。因此在周期性的丢包情况下,不会对整体传输效果造成影响。

1.2 协议格式

  在RFC2198草案中明确定义了RED封装的格式如下:

// 整体RTP + AudioRed 包头组成
|RTP header| RED header | RED headr | RED encoding data | RED encoding data|


// AudioRed 头
 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:1bit,表明后面是否还有其他RED Header数据,1说明后面具有其他RED Header,0说明这是最后一个RED Header.
// block PT:7bits,表明该RED Header对应的冗余编码类型,初步定义默认为102.
// timestamp offset:14bits,时间戳偏移,相当于RTP Header的时间戳偏移。
// block length:10bits,该RED Header对应编码数据块长度

二、mediasoup引入Red

  对于mediasoup来说,AudioRed的代码没有迁移过来,因此我们去对应的WebRTC把Red封装的部分的代码迁移过来。同时为了顺着mediasoup对音频的处理逻辑,我们将上下行的Nack关闭,并且只使用了透传的方式控制Red。其实mediasoup对音频采取的态度是使用opus自己的带内fec去做抗丢包能力,音频只进行转发,我们可以从几个细节看出来:
  1.RR的丢包率结合上下行最差的进行上报,意味着将SFU服务器当做不存在;
  2.音频不参与GCC,将音频当做裸流;
  3.音频无Nack,抗丢包依靠Opus内部能力。

2.1 代码迁移

  将WebRTC对应的代码迁移到mediasoup对应的模块下,同时把所有依赖都迁移过来。
在这里插入图片描述

2.2 方案

  调用上述代码中的SplitRed函数。将red包解码为原始数据包+冗余payload。
在这里插入图片描述
  调用SplitRed函数返回为一个包的list,最后一个包为该包携带的原始数据,每个数据包都会携带多个Red头,每个Red头中携带了数据采样时相对于当前封包时间戳的偏移,根据偏移量与采样率我们可以计算出相对于当前rtp头的序号大小。例如:

// 采样率:48000
// Red内部携带:3个payload
// Rtp包头序号: 4
// Rtp包头时间戳:3840

// 第一个包序号 时间戳偏移量:1920 ——> 序号偏移 (3840 - 1920)/960 = 2
// 第一个包序号为 4 - 2 = 2。

// 依次类推,偏移量为:2880 ——> 序号为:4 -  (3840 - 1920)/960 = 3

2.3 转发模块

  为了保证在最后一个环节进行Red解封装,我们设计一个新的Consumer类——AudioRedConsumer,这样我们可以通过这个将推Red格式的所有数据都解封装成无Red格式,用来支持旧版兼容。其余我们Red可以透传的可以什么都不用管,从SimpleConsumer直接转发出去。

2.4 代码

  拷贝SimpleConsumer的代码进行修改,新增一个解封装成员:webrtc::RedPayloadSplitter,并新增几个辅助函数:

void SendDecodedRtpPacket(RTC::RtpPacket* packet);
void DecodeRedPacket(RTC::RtpPacket* packet);
bool IsRed(RTC::RtpPacket* packet);
std::vector<RTC::RtpPacket::GenericExtension> GetRedPacketExtension(RTC::RtpPacket* packet);

  具体头文件展示为:

// include/RTC/AudioRedConsumer.hpp

#ifndef MS_RTC_AUDIORED_CONSUMER_HPP
#define MS_RTC_AUDIORED_CONSUMER_HPP

#include "libwebrtc/modules/audio_coding/neteq/red_payload_splitter.h"

#include "RTC/Consumer.hpp"
#include "RTC/RtpStreamSend.hpp"
#include "RTC/SeqManager.hpp"
#include "DepLibUV.hpp"


namespace RTC
{
	class AudioRedConsumer : public RTC::Consumer, public RTC::RtpStreamSend::Listener
	{
	public:
		AudioRedConsumer(
		  const std::string& id,
		  const std::string& producerId,
		  RTC::Consumer::Listener* listener,
		  json& data);
		~AudioRedConsumer() override;

	public:
		void FillJson(json& jsonObject) const override;
		void FillJsonStats(json& jsonArray) const override;
		void FillJsonScore(json& jsonObject) const override;
		void HandleRequest(Channel::Request* request) override;
		bool IsActive() const override
		{
			// clang-format off
			// return (
			// 	RTC::Consumer::IsActive() &&
			// 	this->producerRtpStream &&
			// 	this->producerRtpStream->GetScore() > 0u
			// );
			return (
				RTC::Consumer::IsActive() && this->producerRtpStream
			);
			// clang-format on
		}
		void ProducerRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override;
		void ProducerNewRtpStream(RTC::RtpStream* rtpStream, uint32_t mappedSsrc) override;
		void ProducerRtpStreamScore(RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) override;
		void ProducerRtcpSenderReport(RTC::RtpStream* rtpStream, bool first) override;
		void ProducerRtcpKeepalive() override;
		uint8_t GetBitratePriority() const override;
		uint32_t IncreaseLayer(uint32_t bitrate, bool considerLoss, bool limitT0Send) override;
		void ApplyLayers() override;
		uint32_t GetDesiredBitrate() const override;
		void SendRtpPacket(RTC::RtpPacket* packet) override;
		std::vector<RTC::RtpStreamSend*> GetRtpStreams() override
		{
			return this->rtpStreams;
		}
		void GetRtcp(RTC::RTCP::CompoundPacket* packet, RTC::RtpStreamSend* rtpStream, uint64_t nowMs) override;
		void NeedWorstRemoteFractionLost(uint32_t mappedSsrc, uint8_t& worstRemoteFractionLost) override;
		void ReceiveNack(RTC::RTCP::FeedbackRtpNackPacket* nackPacket) override;
		void ReceiveKeyFrameRequest(RTC::RTCP::FeedbackPs::MessageType messageType, uint32_t ssrc) override;
		void ReceiveRtcpReceiverReport(RTC::RTCP::ReceiverReport* report) override;
		void ReceiveRtcpExtendedReport(RTC::RTCP::ExtendedReportBlock* report, uint64_t nowMs) override;
		uint32_t GetTransmissionRate(uint64_t nowMs) override;
		float GetRtt() const override;

	private:
		// red decode
		void SendDecodedRtpPacket(RTC::RtpPacket* packet);
		void DecodeRedPacket(RTC::RtpPacket* packet);
		bool IsRed(RTC::RtpPacket* packet);
		std::vector<RTC::RtpPacket::GenericExtension> GetRedPacketExtension(RTC::RtpPacket* packet);
		// red decode
		
		void UserOnTransportConnected() override;
		void UserOnTransportDisconnected() override;
		void UserOnPaused() override;
		void UserOnResumed() override;
		void CreateRtpStream();
		void RequestKeyFrame();
		void EmitScore() const;

		/* Pure virtual methods inherited from RtpStreamSend::Listener. */
	public:
		void OnRtpStreamScore(RTC::RtpStream* rtpStream, uint8_t score, uint8_t previousScore) override;
		void OnRtpStreamRetransmitRtpPacket(RTC::RtpStreamSend* rtpStream, RTC::RtpPacket* packet) override;

	private:
		// red decoder
		std::unique_ptr<webrtc::RedPayloadSplitter> redPayloadSplitter;

		// Allocated by this.
		RTC::RtpStreamSend* rtpStream{ nullptr };
		// Others.
		std::vector<RTC::RtpStreamSend*> rtpStreams;
		RTC::RtpStream* producerRtpStream{ nullptr };
		bool keyFrameSupported{ false };
		bool syncRequired{ false };
		RTC::SeqManager<uint16_t> rtpSeqManager;
		bool managingBitrate{ false };
	};
} // namespace RTC

#endif


  cpp简略代码:

#define MS_CLASS "RTC::AudioRedConsumer"
 #define MS_LOG_DEV_LEVEL 3

#include "RTC/AudioRedConsumer.hpp"
#include "DepLibUV.hpp"
#include "Logger.hpp"
#include "MpErrors.hpp"
#include "Channel/Notifier.hpp"
#include "RTC/Codecs/Tools.hpp"
#include "RTC/RTCP/Packet.hpp"
#include "common.hpp"
#include "RTC/RTCP/Bye.hpp"

#include "libwebrtc/modules/rtp_rtcp/source/rtp_packet_received.h"
#include "libwebrtc/modules/audio_coding/neteq/packet.h"
#include "libwebrtc/modules/rtp_rtcp/source/rtp_header_extensions.h"
#include "libwebrtc/api/rtp_packet_info.h"

#include <iomanip>
#include <sstream>

namespace RTC
{
	/* Instance methods. */

	AudioRedConsumer::AudioRedConsumer(
	  const std::string& id, const std::string& producerId, RTC::Consumer::Listener* listener, json& data)
	  : RTC::Consumer::Consumer(id, producerId, listener, data, RTC::RtpParameters::Type::AUDIORED)
	{
		MS_TRACE();

		// Ensure there is a single encoding.
		if (this->consumableRtpEncodings.size() != 1u)
			MS_THROW_TYPE_ERROR("invalid consumableRtpEncodings with size != 1");

		auto& encoding         = this->rtpParameters.encodings[0];
		const auto* mediaCodec = this->rtpParameters.GetCodecForEncoding(encoding);

		this->keyFrameSupported = RTC::Codecs::Tools::CanBeKeyFrame(mediaCodec->mimeType);

		// Create RtpStreamSend instance for sending a single stream to the remote.
		CreateRtpStream();

		// 创建 red 解码
		this->redPayloadSplitter.reset(new webrtc::RedPayloadSplitter());
	}

 	...


    // 这里将原先的函数修改为我们自己的解包函数
	void AudioRedConsumer::SendRtpPacket(RTC::RtpPacket* packet)
	{
		MS_TRACE();

		this->DecodeRedPacket(packet);
	}

	bool AudioRedConsumer::IsRed(RTC::RtpPacket* packet)
	{
		return packet->GetPayloadType() == 102 ? true : false;
	}

	void AudioRedConsumer::DecodeRedPacket(RTC::RtpPacket* packet)
	{
		MS_TRACE();
		
//		if (!IsRed(packet))
//			return;

		MS_WARN_TAG(rtp, "red consumer receive a rtp packet, consumerId: %s", this->id.c_str());

		webrtc::RtpPacketReceived parsed_packet(nullptr);
		if (!parsed_packet.Parse(packet->GetData(), packet->GetSize()))
		{
			MS_WARN_TAG(rtp, "receive red packet but parses packet failed!");
			return;
		}

		webrtc::RTPHeader rtp_header;
		parsed_packet.GetHeader(&rtp_header);
		auto payload = parsed_packet.payload();

		int64_t receive_time_ms = DepLibUV::GetTimeMs();
		webrtc::PacketList packet_list;
		// Insert packet in a packet list.
		packet_list.push_back([&rtp_header, &payload, &receive_time_ms] {
			// Convert to Packet.
			webrtc::Packet packet;
			packet.payload_type = rtp_header.payloadType;
			packet.sequence_number = rtp_header.sequenceNumber;
			packet.timestamp = rtp_header.timestamp;
			packet.payload.SetData(payload.data(), payload.size());
			packet.packet_info = webrtc::RtpPacketInfo(rtp_header, receive_time_ms);
			// Waiting time will be set upon inserting the packet in the buffer.
			return packet;
		}());

		if (!redPayloadSplitter->SplitRed(&packet_list)) {
			MS_WARN_TAG(rtp, "split red packet failed!");
			return;
		}
		if (packet_list.empty()) {
			MS_WARN_TAG(rtp, "red packet list is empty!");
			return;
		}
		// 获取拓展头内容
		auto extensions = this->GetRedPacketExtension(packet);
		auto it = packet_list.begin();
		while (it != packet_list.end()) {

			webrtc::RtpPacketReceived recover_packet;
			auto recover_seq = (*it).sequence_number - ((rtp_header.timestamp - (*it).timestamp)/960);
			recover_packet.SetSequenceNumber(recover_seq);
			recover_packet.SetPayloadType((*it).payload_type);
			recover_packet.SetTimestamp((*it).timestamp);
			recover_packet.SetMarker(rtp_header.markerBit);
			recover_packet.SetSsrc(rtp_header.ssrc);

			uint8_t* payload = recover_packet.AllocatePayload((*it).payload.size());
			if (!payload)  // Too large payload buffer.
			{
				MS_WARN_TAG(rtp, "red recover packet Too large payload buffer");
				return;
			}
			memcpy(payload, (*it).payload.data(), (*it).payload.size());

			RTC::RtpPacket* recover_packet2 = RTC::RtpPacket::Parse(recover_packet.data(), recover_packet.size());
			if (!recover_packet2){
				MS_WARN_TAG(rtp, "red recover packet data is not a valid RTP packet");
				return;
			}

			// 设置拓展mid
			recover_packet2->SetExtensions(2, extensions);
			recover_packet2->SetMidExtensionId(static_cast<uint8_t>(RTC::RtpHeaderExtensionUri::Type::MID));
			const auto& mid = this->GetRtpParameters().mid;
			if (!mid.empty())
				recover_packet2->UpdateMid(mid);

			this->SendDecodedRtpPacket(recover_packet2);

			it++;
		}
	}

	// 该函数与router中需要对齐
	std::vector<RTC::RtpPacket::GenericExtension> AudioRedConsumer::GetRedPacketExtension(RTC::RtpPacket* packet) {

		uint8_t buffer[4096];
		std::vector<RTC::RtpPacket::GenericExtension> extensions;

		// This happens just once.
		if (extensions.capacity() != 24)
			extensions.reserve(24);

		extensions.clear();

		uint8_t* extenValue;
		uint8_t extenLen;
		uint8_t* bufferPtr{ buffer };

		{
			extenLen = RTC::MidMaxLength;

			extensions.emplace_back(
				static_cast<uint8_t>(RTC::RtpHeaderExtensionUri::Type::MID), extenLen, bufferPtr);

			bufferPtr += extenLen;
		}

		{
			// Proxy urn:ietf:params:rtp-hdrext:ssrc-audio-level.
			extenValue = packet->GetExtension(this->rtpHeaderExtensionIds.ssrcAudioLevel, extenLen);
			if (extenValue)
			{
				std::memcpy(bufferPtr, extenValue, extenLen);

				extensions.emplace_back(
					static_cast<uint8_t>(RTC::RtpHeaderExtensionUri::Type::SSRC_AUDIO_LEVEL),
					extenLen,
					bufferPtr);

				// Not needed since this is the latest added extension.
				// bufferPtr += extenLen;
			}
		}

		{					
			extenValue = packet->GetExtension(this->rtpHeaderExtensionIds.absCaptureTime, extenLen);

			if (extenValue)
			{
				std::memcpy(bufferPtr, extenValue, extenLen);

				extensions.emplace_back(
				static_cast<uint8_t>(RTC::RtpHeaderExtensionUri::Type::ABS_CAPTURE_TIME),
				extenLen,
				bufferPtr);

				bufferPtr += extenLen;
			}
		}

		return extensions;
	}

	void AudioRedConsumer::SendDecodedRtpPacket(RTC::RtpPacket* packet) {
		MS_TRACE();

		if (!IsActive())
			return;

		auto payloadType = packet->GetPayloadType();

		// NOTE: This may happen if this Consumer supports just some codecs of those
		// in the corresponding Producer.
		if (this->supportedCodecPayloadTypes.find(payloadType) == this->supportedCodecPayloadTypes.end())
		{
			MS_DEBUG_DEV("payload type not supported [payloadType:%" PRIu8 "]", payloadType);

			return;
		}

		// If we need to sync, support key frames and this is not a key frame, ignore
		// the packet.
		if (this->syncRequired && this->keyFrameSupported && !packet->IsKeyFrame())
			return;

		// Whether this is the first packet after re-sync.
		bool isSyncPacket = this->syncRequired;

		// Sync sequence number and timestamp if required.
		if (isSyncPacket)
		{
			if (packet->IsKeyFrame())
				MS_DEBUG_TAG(rtp, "sync key frame received");

			this->rtpSeqManager.Sync(packet->GetSequenceNumber() - 1);

			this->syncRequired = false;
		}

		// Update RTP seq number and timestamp.
		uint16_t seq;

		this->rtpSeqManager.Input(packet->GetSequenceNumber(), seq);

		// Save original packet fields.
		auto origSsrc = packet->GetSsrc();
		auto origSeq  = packet->GetSequenceNumber();

		// Rewrite packet.
		packet->SetSsrc(this->rtpParameters.encodings[0].ssrc);
		packet->SetSequenceNumber(seq);

		if (isSyncPacket)
		{
			MS_DEBUG_TAG(
			  rtp,
			  "sending sync packet [ssrc:%" PRIu32 ", seq:%" PRIu16 ", ts:%" PRIu32
			  "] from original [seq:%" PRIu16 "]",
			  packet->GetSsrc(),
			  packet->GetSequenceNumber(),
			  packet->GetTimestamp(),
			  origSeq);
		}

		MS_WARN_TAG(rtp, "red consumer send a rtp packet, consumer Id: %s", this->id.c_str());

		// Process the packet.
		if (this->rtpStream->ReceivePacket(packet))
		{
			// Send the packet.
			this->listener->OnConsumerSendRtpPacket(this, packet);

			// May emit 'trace' event.
			//EmitTraceEventRtpAndKeyFrameTypes(packet);
			MS_DEBUG_TAG(rtp,
			"consumer keyframe packet [ssrc:%" PRIu32 ", seq:%" PRIu16 ", ts:%" PRIu32
			 "] from original [seq:%" PRIu16 "]",
			  packet->GetSsrc(),
			  packet->GetSequenceNumber(),
			  packet->GetTimestamp(),
			  origSeq);
		}
		else
		{
			MS_WARN_TAG(
			  rtp,
			  "failed to send packet [ssrc:%" PRIu32 ", seq:%" PRIu16 ", ts:%" PRIu32
			  "] from original [seq:%" PRIu16 "]",
			  packet->GetSsrc(),
			  packet->GetSequenceNumber(),
			  packet->GetTimestamp(),
			  origSeq);
		}

		// Restore packet fields.
		packet->SetSsrc(origSsrc);
		packet->SetSequenceNumber(origSeq);
	}

} // namespace RTC

三、总结

3.1 效果

  在进行Red测试之前,我们验证了无Nack和无Red冗余的表现。在50%丢包情况下,整体语音传输基本已经无法理解,同时丢包严重。

上行弱网测试结果:
  1.在纯50%丢包的环境下,音频理解无异常;
  2.在50%丢包 + 100ms的弱网环境下,音频理解无异常,整体效果比Nack优秀;

码率增长:从60kbps增长至120kbps左右。

3.2 小结

  本文简单介绍了mediasoup引入Red一个透传方案,做了简单的测试。mediasoup在质量方面存在比较多缺陷,我们仍然需要不断地丰富来实现提高。对于FEC和Red,作者IBC明确说了不会支持,希望我的方案可以给大家一些启发。

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值