mediasoup simulcast实现说明

news2024/11/18 9:46:32

一. 前言

二. 空间可伸缩与时间可伸缩

三. mediasoup simulcast实现代码分析

1. 推流客户端开启 simulcast

2. mediasoup服务端接收simulcast流

3. mediasoup服务端转发流数据给消费者

a. SimulcastConsumer类声明

b. 获取预估码率,切换SimulcastConsumer的目标层

c. SimulcastConsumer过滤转发层数据

d. 发生层切换时,重写包序号与时间戳

重写包序号

重写时间戳

一. 前言

        simulcast 字面翻译称为多播,或者经常称为大小流。

        在多人音视频会议中,不同用户的下行网络质量是不同的,假设 A 发送的视频编码码率是 1.5Mbps,经过 SFU 转发时,接收者 B 的网络较好,能够正常接收播放,而接收者 C 的下行网络带宽较小,SFU 将视频流发送给 C 时会出现丢包等问题,导致 C 观看 A 的视频流很卡顿。 

        对于上述问题,一种解决方法是让 A 降低视频编码码率,使得多人音视频会议中网络最差的用户也可以正常接收视频流播放,但这样存在的问题是:A 的上行网络质量可能是好的,B 的下行网络质量也很好,但因为 C 的下行网络质量差,使得 B 也只能观看低码率的视频流。有没有一种方法,让 B 能观看高码率的视频流,对于网络较差的用户 C,则转发低码率的视频流,这样 B 和 C 都能有良好的观看体验。

        这种技术就是 simulcast,也称为大小流。发送端同时发送多个多种不同编码参数的视频流,例如第一路流的分辨率是 1080p,最大码率设置为 6Mbps,第二路流的分辨率是 720p,最大码率设置为 2Mbps,第三路流的分辨率是 360p,最大码率设置为 500kbps。发送者同时发送这三路视频流到 SFU,SFU 通过带宽探测得到不同用户的下行可用带宽,再决定给用户转发哪一路视频流,这样就可以保证不同网络质量的用户都有较好的体验。而且网络是在波动的,某个用户可能一段时间内网络很好,足以接收质量最佳的流,但过一段时间可能网络变差了,SFU 要快速探测出用户下行可用带宽的的变化,给用户切换发送合适的视频流。

        本文将介绍 mediasoup simulcast 相关的实现,包括客户端侧如何设置同时发送多路流,SFU 预估带宽后怎么进行层的选择,SFU 收到多路流如何转发给消费者,当层发生变化时需要进行什么处理等。

二. 空间可伸缩与时间可伸缩

空间层(spatialLayer):不同空间层指不同的视频流,例如 spatialLayer=0 可以指分辨率为 360p 的流,spatialLayer=1 可以指分辨率为 720p 的流,spatialLayer=2 可以指分辨率为 1080p 的流

时间层(temporalLayer):除了调整空间层来动态调整比特率,对于某一个空间层还可以通过调整不同的帧率来实现不同码率的效果

        下图所示是一个时间上不分层的帧参考关系,序号 N+1 的帧只能参考序号 N 的帧才能进行解码,对于这样序列的码流,服务器在转发时只能将收到的帧完全转发给接收者,如果某个帧不进行转发,那么之后的帧无法进行解码

        如下所示,如果我们调整帧与帧之间的参考关系并进行分层,帧 2,3,5 参考帧 1,帧 4 参考帧 3(帧 1 和 5 属于第 0 层,帧 3 属于第 1 层,帧 2 和 4 属于第 2 层),这种情况下 SFU 就可以根据用户带宽选择性转发。

        假设用户的带宽足够好,那么 SFU 将这三层的数据全部转发,用户可以看到一个帧率较高的画面,假设用户的带宽一般,那么 SFU 只选择转发第 0 层和第 1 层,可以看到帧 3 和帧 5 都是参考帧 1 的,因此用户能正常解码,只不过此时看到的帧率相较而言较低,如果用户的带宽较差,那么 SFU 只选择转发第 0 层,此时用户看到的帧率更低,但画面是正常的,这种方式又称为时间可伸缩性。

三. mediasoup simulcast实现代码分析

1. 推流客户端开启 simulcast

        如下所示,当使用 VP8 或者 H264 时 mediasoup-client 会默认开启 simulcast,numSimulcastStreams 默认值为 3(即空间层为 3),并且对于每一个空间层,其设置的时间层也为 3(scalabilityMode L1T3)。

        Transport produce 主要的执行逻辑是调用 this._handler.send 进行处理,_handler 是根据浏览器版本不同创建的不同的处理对象,我们以 >= Chrome 111 版本的处理逻辑为例。

         在 send 处理函数中,它调用 addTransceiver 将视频轨道 track 传入,并且添加了参数 direction, streams, sendEncodings 信息。

        sendEncodings 是一个数组,它用于设置一系列编码相关的参数,数组的每一个元素对应一个编码发送码流,例如当 sendEncodings 的长度为 3,表示我们从视频轨道采集数据后,会按照 3 个编码参数的设置编码 3 个不同的码流进行发送。

        sendEncodings 数组元素是一个对象,其包含的主要字段及含义如下。

active设置为 true 表示这组编码参数是正常工作的,设置为 false 则这组编码参数不被使用
maxBitrate编码的最大码率值
maxFramerate编码的最大帧率
priority优先级
ridrtp-stream-id,它是一个字符串,设置后可以将该信息携带在 RTP 扩展头部中,用于区分 ssrc 的层信息
scaleResolutionDownBy设置视频轨道编码的缩放因子,例如 1.0 表示按原始大小进行编码,2.0 表示宽高分别按 2 倍缩放,即分辨率为原始大小的 1/4,如果设置为 4.0 则表示宽高分别为原始大小的 1/4,分辨率为原始大小的 1/16

        了解完上述参数含义,我们可以知道,在使用 VP8 / H264 时 mediasoup-client 编码了三个不同参数的码流。第一个码流的分辨率是原始分辨率的 1/16(scaleResolutionDownBy=4),最大码率为 500kbps,时间层信息为 3;第二个码流的分辨率为原始分辨率的 1/4,最大码率为 1Mbps,时间层信息为 3;第三个码流的分辨率与原始分辨率相同,最大码率为 5Mbps,时间层信息为 3。 

2. mediasoup服务端接收simulcast流

        客户端通过 addTransceiver 添加轨道后则发起 produce 信令请求,mediasoup 服务端对应创建 Producer 对象,之后开始发送 RTP 数据。如下所示的客户端视频总共有三路码流,每一路的 rid 和 ssrc 分别如图所示。

        服务端 Producer 接收到 RTP 包之后,首先获取对应的 RtpStreamRecv*,如果不存在则创建对应的 RtpStreamRecv*。

        GetRtpStream 的逻辑如下,首先会从 mapSsrcRtpStream,mapRtxSsrcRtpStream 查找 ssrc 对应的 RtpStreamRecv* 是否存在,如果存在则返回,如果不存在则执行创建流程。

        创建 RtpStreamRecv* 逻辑如下所示,首先获取 rid 对应的 encoding 参数信息,判断当前 RTP 包的 payloadType 属于媒体包还是重传包,如果是媒体包就调用 CreateRtpStream 创建 RtpStreamRecv*,如果是重传包,则根据重传包的 rid 找到对应已经创建的 RtpStreamRecv*,调用 RtpStreamRecv::SetRtx 更新流的重传信息。

        创建后的 RtpStreamRecv 包含以下信息,这路流媒体包的 ssrc,payloadType,对应的重传信息(rtxSsrc,rtxPayloadType),这路流的 rid 信息等。

        拿到 RtpStreamRecv* 之后,再调用 RtpStreamRecv::ReceivePacket / RtpStreamRecv::ReceiveRtxPacket 进行处理(这里的处理流程不细讲),之后调用 this->listener->OnProducerRtpPacketReceived,最后调用到 Router::OnTransportProducerRtpPacketReceived。Router 会保存 producer 对应有哪些消费者 consumers,获取 consumer 调用 SendRtpPacket 发送出去。

        后续的关键逻辑在于 SFU 需要根据消费者 Transport 的可用带宽,决定转发哪一层给消费者,而并非将每一层的数据都转发给消费者,让我们接着看后续的处理。

3. mediasoup服务端转发流数据给消费者

        当有用户订阅这路 simulcast producer,该用户会创建 WebRtcTransport,并且在该 WebRtcTransport 上创建 SimulcastConsumer。

a. SimulcastConsumer类声明

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

public:
	void FillJson(json& jsonObject) const override;
	void FillJsonStats(json& jsonArray) const override;
	void FillJsonScore(json& jsonObject) const override;
	RTC::Consumer::Layers GetPreferredLayers() const override
	{
		RTC::Consumer::Layers layers;

		layers.spatial  = this->preferredSpatialLayer;
    	layers.temporal = this->preferredTemporalLayer;

		return layers;
	}
	bool IsActive() const override
	{
		// clang-format off
		return (
			RTC::Consumer::IsActive() &&
			std::any_of(
				this->producerRtpStreams.begin(),
				this->producerRtpStreams.end(),
				[](const RTC::RtpStreamRecv* rtpStream)
				{
					// If there is no RTP inactivity check do not consider the stream
					// inactive despite it has score 0.
					return (rtpStream != nullptr && (rtpStream->GetScore() > 0u || !rtpStream->HasRtpInactivityCheckEnabled()));
				}
			)
		);
		// clang-format on
	}
	void ProducerRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override;
	void ProducerNewRtpStream(RTC::RtpStreamRecv* rtpStream, uint32_t mappedSsrc) override;
	void ProducerRtpStreamScore(
	RTC::RtpStreamRecv* rtpStream, uint8_t score, uint8_t previousScore) override;
	void ProducerRtcpSenderReport(RTC::RtpStreamRecv* rtpStream, bool first) override;
	uint8_t GetBitratePriority() const override;
	uint32_t IncreaseLayer(uint32_t bitrate, bool considerLoss) override;
	void ApplyLayers() override;
	uint32_t GetDesiredBitrate() const override;
	void SendRtpPacket(RTC::RtpPacket* packet, std::shared_ptr<RTC::RtpPacket>& sharedPacket) override;
	bool GetRtcp(RTC::RTCP::CompoundPacket* packet, uint64_t nowMs) override;
	const std::vector<RTC::RtpStreamSend*>& GetRtpStreams() const override
	{
		return this->rtpStreams;
	}
	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 ReceiveRtcpXrReceiverReferenceTime(RTC::RTCP::ReceiverReferenceTime* report) override;
	uint32_t GetTransmissionRate(uint64_t nowMs) override;
	float GetRtt() const override;

	/* Methods inherited from Channel::ChannelSocket::RequestHandler. */
public:
	void HandleRequest(Channel::ChannelRequest* request) override;

private:
	void UserOnTransportConnected() override;
	void UserOnTransportDisconnected() override;
	void UserOnPaused() override;
	void UserOnResumed() override;
	void CreateRtpStream();
	void RequestKeyFrames();
	void RequestKeyFrameForTargetSpatialLayer();
	void RequestKeyFrameForCurrentSpatialLayer();
	void MayChangeLayers(bool force = false);
	bool RecalculateTargetLayers(int16_t& newTargetSpatialLayer, int16_t& newTargetTemporalLayer) const;
	void UpdateTargetLayers(int16_t newTargetSpatialLayer, int16_t newTargetTemporalLayer);
	bool CanSwitchToSpatialLayer(int16_t spatialLayer) const;
	void EmitScore() const;
	void EmitLayersChange() const;
	RTC::RtpStreamRecv* GetProducerCurrentRtpStream() const;
	RTC::RtpStreamRecv* GetProducerTargetRtpStream() const;
	RTC::RtpStreamRecv* GetProducerTsReferenceRtpStream() 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:
	// Allocated by this.
	RTC::RtpStreamSend* rtpStream{ nullptr };
	// Others.
	absl::flat_hash_map<uint32_t, int16_t> mapMappedSsrcSpatialLayer;
	std::vector<RTC::RtpStreamSend*> rtpStreams;
	std::vector<RTC::RtpStreamRecv*> producerRtpStreams; // Indexed by spatial layer.
	bool syncRequired{ false };
	int16_t spatialLayerToSync{ -1 };
	bool lastSentPacketHasMarker{ false };
	RTC::SeqManager<uint16_t> rtpSeqManager;
	int16_t preferredSpatialLayer{ -1 };
	int16_t preferredTemporalLayer{ -1 };
	int16_t provisionalTargetSpatialLayer{ -1 };
	int16_t provisionalTargetTemporalLayer{ -1 };
	int16_t targetSpatialLayer{ -1 };
	int16_t targetTemporalLayer{ -1 };
	int16_t currentSpatialLayer{ -1 };
	int16_t tsReferenceSpatialLayer{ -1 }; // Used for RTP TS sync.
	uint16_t snReferenceSpatialLayer{ 0 };
	bool checkingForOldPacketsInSpatialLayer{ false };
	std::unique_ptr<RTC::Codecs::EncodingContext> encodingContext;
	uint32_t tsOffset{ 0u }; // RTP Timestamp offset.
	bool keyFrameForTsOffsetRequested{ false };
	uint64_t lastBweDowngradeAtMs{ 0u }; // Last time we moved to lower spatial layer due to BWE.
};

几个重要的成员作用说明如下。

preferredSpatialLayer,preferredTemporalLayer:倾向的空间层和时间层。假设目前空间层有 3 层,当 SimulcastConsumer 中 preferredSpatialLayer 值为 0,则表示用户只倾向于订阅第 0 层的空间层,即使带宽足够,也不要切换到第 1 层或者第 2 层,时间层同理

currentSpatialLayer:当前使用的空间层

targetSpatialLayer,targetTemporalLayer:目标的空间层和时间层,即预估带宽后调整的目标层,如果预估带宽变大,目标层可能变大,如果预估带宽变小,目前层可能变小,如果 currentSpatialLayer 与 targetSpatialLayer 不相等,则需要切换到目标层

provisionalTargetSpatialLayer,provisionalTargetTemporalLayer:临时的空间层和时间层

b. 获取预估码率,切换SimulcastConsumer的目标层

        mediasoup 服务端通过 TCC 获取用户下行带宽,通过下行带宽切换 SimulcastConsumer 的目标层,代码如下所示。

        DistributeAvailableOutgoingBitrate 函数会根据通知的可用带宽,调用 SimulcastConsumer::IncreaseLayer 计算消费者可以使用的目标层(设置 provisionalTargetSpatialLayer,provisionalTargetTemporalLayer),最后通过 SimulcastConsumer::ApplyLayers 切换目标层。

	void Transport::DistributeAvailableOutgoingBitrate()
	{
		MS_TRACE();

		MS_ASSERT(this->tccClient, "no TransportCongestionClient");

		std::multimap<uint8_t, RTC::Consumer*> multimapPriorityConsumer;

		// Fill the map with Consumers and their priority (if > 0).
		for (auto& kv : this->mapConsumers)
		{
			auto* consumer = kv.second;
			auto priority  = consumer->GetBitratePriority();

			if (priority > 0u)
			{
				multimapPriorityConsumer.emplace(priority, consumer);
			}
		}

		// Nobody wants bitrate. Exit.
		if (multimapPriorityConsumer.empty())
		{
			return;
		}

		bool baseAllocation       = true;
		uint32_t availableBitrate = this->tccClient->GetAvailableBitrate();

		this->tccClient->RescheduleNextAvailableBitrateEvent();

		MS_DEBUG_DEV("before layer-by-layer iterations [availableBitrate:%" PRIu32 "]", availableBitrate);

		// Redistribute the available bitrate by allowing Consumers to increase
		// layer by layer. Initially try to spread the bitrate across all
		// consumers. Then allocate the excess bitrate to Consumers starting
		// with the highest priorty.
		while (availableBitrate > 0u)
		{
			auto previousAvailableBitrate = availableBitrate;

			for (auto it = multimapPriorityConsumer.rbegin(); it != multimapPriorityConsumer.rend(); ++it)
			{
				auto priority  = it->first;
				auto* consumer = it->second;
				auto bweType   = this->tccClient->GetBweType();

				for (uint8_t i{ 1u }; i <= (baseAllocation ? 1u : priority); ++i)
				{
					uint32_t usedBitrate{ 0u };
					const bool considerLoss = (bweType == RTC::BweType::REMB);

					usedBitrate = consumer->IncreaseLayer(availableBitrate, considerLoss);

					MS_ASSERT(usedBitrate <= availableBitrate, "Consumer used more layer bitrate than given");

					availableBitrate -= usedBitrate;

					// Exit the loop fast if used bitrate is 0.
					if (usedBitrate == 0u)
					{
						break;
					}
				}
			}

			// If no Consumer used bitrate, exit the loop.
			if (availableBitrate == previousAvailableBitrate)
			{
				break;
			}

			baseAllocation = false;
		}

		MS_DEBUG_DEV("after layer-by-layer iterations [availableBitrate:%" PRIu32 "]", availableBitrate);

		// Finally instruct Consumers to apply their computed layers.
		for (auto it = multimapPriorityConsumer.rbegin(); it != multimapPriorityConsumer.rend(); ++it)
		{
			auto* consumer = it->second;

			consumer->ApplyLayers();
		}
	}

        IncreaseLayer 逻辑如下所示,主要逻辑在于从低层级往高层级遍历,取出对应层级所需的码率,如果当前可用带宽足以消费该层级的码流,则将当前层级设置为临时层。

        如果带宽足够,经过 DistributeAvailableOutgoingBitrate 函数中 while 循环的 IncreaseLayer 调用,最终可以设置到最高层级,如果带宽不太足够,最终计算的临时层级为某个足够消费的级别。

	uint32_t SimulcastConsumer::IncreaseLayer(uint32_t bitrate, bool considerLoss)
	{
		MS_TRACE();

		MS_ASSERT(this->externallyManagedBitrate, "bitrate is not externally managed");
		MS_ASSERT(IsActive(), "should be active");

		// If already in the preferred layers, do nothing.
		// clang-format off
		if (
			this->provisionalTargetSpatialLayer == this->preferredSpatialLayer &&
			this->provisionalTargetTemporalLayer == this->preferredTemporalLayer
		)
		// clang-format on
		{
			return 0u;
		}

		uint32_t virtualBitrate;

		if (considerLoss)
		{
			// Calculate virtual available bitrate based on given bitrate and our
			// packet lost.
			auto lossPercentage = this->rtpStream->GetLossPercentage();

			if (lossPercentage < 2)
				virtualBitrate = 1.08 * bitrate;
			else if (lossPercentage > 10)
				virtualBitrate = (1 - 0.5 * (lossPercentage / 100)) * bitrate;
			else
				virtualBitrate = bitrate;
		}
		else
		{
			virtualBitrate = bitrate;
		}

		uint32_t requiredBitrate{ 0u };
		int16_t spatialLayer{ 0 };
		int16_t temporalLayer{ 0 };
		auto nowMs = DepLibUV::GetTimeMs();

		for (size_t sIdx{ 0u }; sIdx < this->producerRtpStreams.size(); ++sIdx)
		{
			spatialLayer = static_cast<int16_t>(sIdx);

			// If this is higher than current spatial layer and we moved to to current spatial
			// layer due to BWE limitations, check how much it has elapsed since then.
			if (nowMs - this->lastBweDowngradeAtMs < BweDowngradeConservativeMs)
			{
				if (this->provisionalTargetSpatialLayer > -1 && spatialLayer > this->currentSpatialLayer)
				{
					MS_DEBUG_DEV(
					  "avoid upgrading to spatial layer %" PRIi16 " due to recent BWE downgrade", spatialLayer);

					goto done;
				}
			}

			// Ignore spatial layers lower than the one we already have.
			if (spatialLayer < this->provisionalTargetSpatialLayer)
				continue;

			// This can be null.
			auto* producerRtpStream = this->producerRtpStreams.at(spatialLayer);

			// Producer stream does not exist. Ignore.
			if (!producerRtpStream)
				continue;

			// If the stream has not been active time enough and we have an active one
			// already, move to the next spatial layer.
			// clang-format off
			if (
				spatialLayer != this->provisionalTargetSpatialLayer &&
				this->provisionalTargetSpatialLayer != -1 &&
				producerRtpStream->GetActiveMs() < StreamMinActiveMs
			)
			// clang-format on
			{
				const auto* provisionalProducerRtpStream =
				  this->producerRtpStreams.at(this->provisionalTargetSpatialLayer);

				// The stream for the current provisional spatial layer has been active
				// for enough time, move to the next spatial layer.
				if (provisionalProducerRtpStream->GetActiveMs() >= StreamMinActiveMs)
					continue;
			}

			// We may not yet switch to this spatial layer.
			if (!CanSwitchToSpatialLayer(spatialLayer))
				continue;

			temporalLayer = 0;

			// Check bitrate of every temporal layer.
			for (; temporalLayer < producerRtpStream->GetTemporalLayers(); ++temporalLayer)
			{
				// Ignore temporal layers lower than the one we already have (taking into account
				// the spatial layer too).
				// clang-format off
				if (
					spatialLayer == this->provisionalTargetSpatialLayer &&
					temporalLayer <= this->provisionalTargetTemporalLayer
				)
				// clang-format on
				{
					continue;
				}

				requiredBitrate = producerRtpStream->GetLayerBitrate(nowMs, 0, temporalLayer);

				// This is simulcast so we must substract the bitrate of the current temporal
				// spatial layer if this is the temporal layer 0 of a higher spatial layer.
				//
				// clang-format off
				if (
					requiredBitrate &&
					temporalLayer == 0 &&
					this->provisionalTargetSpatialLayer > -1 &&
					spatialLayer > this->provisionalTargetSpatialLayer
				)
				// clang-format on
				{
					auto* provisionalProducerRtpStream =
					  this->producerRtpStreams.at(this->provisionalTargetSpatialLayer);
					auto provisionalRequiredBitrate =
					  provisionalProducerRtpStream->GetBitrate(nowMs, 0, this->provisionalTargetTemporalLayer);

					if (requiredBitrate > provisionalRequiredBitrate)
						requiredBitrate -= provisionalRequiredBitrate;
					else
						requiredBitrate = 1u; // Don't set 0 since it would be ignored.
				}

				MS_DEBUG_DEV(
				  "testing layers %" PRIi16 ":%" PRIi16 " [virtual bitrate:%" PRIu32
				  ", required bitrate:%" PRIu32 "]",
				  spatialLayer,
				  temporalLayer,
				  virtualBitrate,
				  requiredBitrate);

				// If active layer, end iterations here. Otherwise move to next spatial layer.
				if (requiredBitrate)
					goto done;
				else
					break;
			}

			// If this is the preferred or higher spatial layer, take it and exit.
			if (spatialLayer >= this->preferredSpatialLayer)
				break;
		}

	done:

		// No higher active layers found.
		if (!requiredBitrate)
			return 0u;

		// No luck.
		if (requiredBitrate > virtualBitrate)
			return 0u;

		// Set provisional layers.
		this->provisionalTargetSpatialLayer  = spatialLayer;
		this->provisionalTargetTemporalLayer = temporalLayer;

		MS_DEBUG_DEV(
		  "setting provisional layers to %" PRIi16 ":%" PRIi16 " [virtual bitrate:%" PRIu32
		  ", required bitrate:%" PRIu32 "]",
		  this->provisionalTargetSpatialLayer,
		  this->provisionalTargetTemporalLayer,
		  virtualBitrate,
		  requiredBitrate);

		if (requiredBitrate <= bitrate)
			return requiredBitrate;
		else if (requiredBitrate <= virtualBitrate)
			return bitrate;
		else
			return requiredBitrate; // NOTE: This cannot happen.
	}

        在 ApplyLayers 函数中,将临时层与目标层比较,如果不相等,则通过 UpdateTargetLayers 更新目标层。

c. SimulcastConsumer过滤转发层数据

        对于 SimulcastConsumer,它只创建一个发送的 RtpStreamSend 对象,相当于对于接收者而言,只会有一路 ssrc 的流。mediasoup server 接收发布者的多路 simulcast 流,但只需要将特定层的流数据转发给消费者即可。

        SimulcastConsumer::SendRtpPacket 只发送特定空间层数据的逻辑如下所示,如果不属于当前空间层的数据不做后续发送处理,如果当前空间层与目标空间层不一致,并且收到了目标空间层的关键帧数据,则将当前空间层切换成目标空间层。

        注意在 UpdateTargetLayers 时会请求目标空间层的关键帧数据,避免虽然重新计算了目标空间层,但是由于目标空间层一直没有关键帧数据导致无法切换 currentSpatialLayer(不同空间层属于不同的码流,不能说计算好目标空间层之后就直接切换发送,必须等到目标空间层有关键帧才能进行切换,否则解码会发生错误)。

        对于时间层的过滤,假设目标时间层是 2,应该在转发的时候将 0,1,2 三层数据全部转发,如果目标时间层是 1,则只转发 0,1 两层,与 simulcast 空间层只转发特定层不同,因为时间层高层依赖于低层才能正常解码。

        以 H264 的时间层过滤逻辑为例,它依赖于 RTP 扩展头部 framemarking 中的 TID 信息(Temporal ID),根据 TID 与目标时间层进行比较来确定是否转发。对于 VP8 则是通过 VP8 本身的负载描述进行判断,此处不详细描述。

d. 发生层切换时,重写包序号与时间戳

        Simulcast 发布者的不同流是互相独立的,也就是说不同的流的起始序号,起始 RTP timestamp 是不同的。对于接收者而言,它只认为是一路流,因此它期望接收的是序列号正常按一递增,RTP 时间戳按采样率/帧率(理想情况)递增的码流,因为序号可以指示包的丢失情况,如果出现序号空隙会触发 NACK 要求重传,而 RTP timestamp 指示了帧的播放时间,如果时间戳异常可能导致播放异常。

        如果上行只有一个码流,转发给消费者也是这一路码流的话,SFU 不需要重写包序号和时间戳,但是对于 Simulcast 场景,由于不同层互相独立,因此需要由 mediasoup 重写包序号和时间戳。

重写包序号

        如上所示,假设层 0 seq=[0, 2],层 1 seq=[50, 53],层 2 seq=[100,105] 的包属于同一时间不同空间层的帧的包。目前我们使用的是第 0 层,我们已经发送了 seq=0,1, 2 的包,接下来我们要从第 0 层切换到第 2 层,而第 2 层之后需要转发的是 seq=106 的 RTP 包,此时我们则需要将 105 与 2 进行同步,进行同步的意思相当于告诉序号管理器 105 对应的是 2,后面输入 106 的话就输出 3,输入 107 的话就输出 4,这样就保证了输出序号的连续性。

重写时间戳

        RTP timestamp 与 NTP 关系是 NTP 时间增加 1s,RTP 时间戳增加一个采样率(90K),因此 NTP 与 RTP timestamp 之间的关系可以看作是一条 y=kx+b 的曲线,k 固定,b 不固定(因为 RTP timestamp 开始时间是不固定的)。

        对于层 0,ntp1 对应的 RTP timestamp 为 ts0,而层 2,ntp1 对应的 RTP timestamp 为 ts2,当我们从层 0 切换到层 2 时,如果不改写时间戳,timestamp 会突然变大了 ts2-ts0,因此我们需要将时间戳减去 ts2-ts0,确保时间戳不出现大的跳变。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1955655.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

大脑自组织神经网络通俗讲解

大脑自组织神经网络的核心概念 大脑自组织神经网络&#xff0c;是指大脑中的神经元通过自组织的方式形成复杂的网络结构&#xff0c;从而实现信息的处理和存储。这一过程涉及到神经元的生长、连接和重塑&#xff0c;是大脑学习和记忆的基础。其核心公式涉及神经网络的权重更新…

优化算法:2.粒子群算法(PSO)及Python实现

一、定义 粒子群算法&#xff08;Particle Swarm Optimization&#xff0c;PSO&#xff09;是一种模拟鸟群觅食行为的优化算法。想象一群鸟在寻找食物&#xff0c;每只鸟都在尝试找到食物最多的位置。它们通过互相交流信息&#xff0c;逐渐向食物最多的地方聚集。PSO就是基于这…

探索HTTPx:Python中的HTTP客户端新选择

文章目录 探索HTTPx&#xff1a;Python中的HTTP客户端新选择背景什么是HTTPx&#xff1f;安装HTTPx简单的库函数使用方法发送GET请求发送POST请求设置超时使用代理处理Cookies 应用场景异步请求连接池管理重试机制 常见问题与解决方案问题1&#xff1a;超时错误问题2&#xff1…

ROS getting started

文章目录 前言一、认识ROS提供的命令行工具nodestopicsservicesparametersactionsrqt_console, rqt_graph批量启动多个节点recorde and playc基础pub-sub 1.5 ROS2和fastdds1 改变订阅模式2 xml配置3 指定xml位置4 talker/listener通过发现服务器发送topic5 ros2 检视6 远程fas…

Natutre Methods|单细胞+空间转录,值得去复现的开源单细胞分析pipeline

肺癌是全球第二大最常见的癌症&#xff0c;也是癌症相关死亡的主要原因。肿瘤生态系统具有多种免疫细胞类型。尤其是髓系细胞&#xff0c;髓系细胞普遍存在&#xff0c;并且在促进疾病方面发挥着众所周知的作用。该篇通过单细胞和空间转录组学分析了 25 名未经治疗的腺癌和鳞状…

58页PPT智慧工地整体解决方案(精华版)

智慧工地的核心技术主要包括以下几个方面&#xff1a; 本文篇幅限制&#xff0c;只分享部分内容&#xff0c;喜欢文章请点赞转发评论&#xff0c;下载完整版PPT可以查看文章中图片右下角信息 1. 物联网&#xff08;IoT&#xff09;技术 核心作用&#xff1a;物联网技术是智慧…

贪心+背包

这道题比较坑的就是我们的对于相同截止时间的需要排个序&#xff0c;因为我们这个工作是有时间前后顺序的&#xff0c;我们如果不排序的话我们一些截止时间晚的工作就无法得到最优报酬 #include<bits/stdc.h> using namespace std;#define int long long int t; int n; c…

数据结构:二叉树(堆)的顺序存储

文章目录 1. 树1.1 树的概念和结构1.2 树的相关术语 2. 二叉树2.1 二叉树的概念和结构2.2 二叉树的特点2.3 特殊的二叉树2.3.1 满二叉树2.3.2 完全二叉树 2.4 二叉树的性质 3. 实现顺序结构二叉树3.1 堆的概念和结构3.2 初始化3.3 销毁3.4 插入数据3.5 向上调整算法3.6 删除数据…

Java语言程序设计——篇九(2)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 枚举类型 枚举类型的定义枚举类型的方法实战演练 枚举在switch中的应用实战演练 枚举类的构造方法实战演练 枚举类型的定义 [修饰符] enum 枚举…

自动控制:带死区的PID控制算法

带死区的PID控制算法 在计算机控制系统中&#xff0c;为了避免控制动作过于频繁&#xff0c;消除因频繁动作所引起的振荡&#xff0c;可采用带死区的PID控制。带死区的PID控制通过引入一个死区&#xff0c;使得在误差较小的范围内不进行控制动作&#xff0c;从而减少控制系统的…

深入源码:解析SpotBugs(1)静态代码分析框架

文章目录 引言SpotBugs概述启动附录 引言 SpotBugs是一个开源的Java静态分析工具&#xff0c;旨在帮助开发人员检测Java代码中的潜在缺陷和漏洞。以下是对SpotBugs的详细解释&#xff1a; SpotBugs概述 定义与功能&#xff1a;SpotBugs是FindBugs的继任者。FindBugs是一个广受…

LInux的基础用法

Linux学习1&#xff1a;LInux的基本功能 读写的权限 读写的权限可以写为&#xff1a;r,w,x 九个权限可以分成三组&#xff1a; user&#xff1a;当前文件所属用户的权限 。 group&#xff1a;与当前文件所属用户同一组的用户权限 。 others&#xff1a;其他用户的权限。 使用…

免费【2024】springboot 编程语言在线学习平台的设计与实现

博主介绍&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HTML、Jsp、PHP、Nodejs、Python、爬虫、数据可视化…

昇思MindSpore 应用学习-RNN实现情感分类-CSDN

RNN实现情感分类 AI代码解析 概述 情感分类是自然语言处理中的经典任务&#xff0c;是典型的分类问题。本节使用MindSpore实现一个基于RNN网络的情感分类模型&#xff0c;实现如下的效果&#xff1a; 输入: This film is terrible 正确标签: Negative 预测标签: Negative输入…

深入分析 Android ContentProvider (七)

文章目录 深入分析 Android ContentProvider (七)ContentProvider 的高级使用和最佳实践1. 高级使用场景1.1. 跨应用数据共享示例&#xff1a;跨应用数据共享 1.2. 动态授权示例&#xff1a;动态授权 1.3. 数据观察与通知示例&#xff1a;内容观察者 2. 最佳实践2.1. 设计合理的…

Linux(虚拟机)的介绍

Linux介绍 常见的操作系统 Windows&#xff1a;微软公司开发的一款桌面操作系统&#xff08;闭源系统&#xff09;。版本有dos&#xff0c;win98&#xff0c;win NT&#xff0c;win XP , win7, win vista. win8, win10&#xff0c;win11。服务器操作系统&#xff1a;winserve…

大模型争锋:左手“世界最强” 右手“高性价比”

2020年&#xff0c;OpenAI团队发表论文&#xff0c;正式提出了大模型开发的经验法则Scaling Law&#xff0c;目前它并没有统一的中文名称&#xff0c;大致可以理解为“规模法则”&#xff0c;更通俗地说是“大力出奇迹”。2022年年底&#xff0c;ChatGPT的横空出世验证了“规模…

C++——类和对象(中)

目录 一、类的默认成员函数 二、构造函数 三、析构函数 四、拷贝构造函数 五、运算符重载 1.基本知识 2.赋值运算符重载 3.取地址运算符重载 a.const成员函数 b.取地址运算符重载 一、类的默认成员函数 默认成员函数就是用户没有显式实现&#xff0c;编译器会自动生成…

[ECharts] There is a chart instance already initialized on the dom. 已存在图表,渲染重复

报错&#xff1a;已存在图表&#xff0c;渲染重复 解决: 在合适的时机执行 dispose 方法即可 // echarts 全局存入 实例 let myChart: any;// 在你的 initChart 初始化 Echarts 方法中 先执行清理方法 const initChart () > {// 执行清理方法然后初始化if(myChart){cons…

Linux中进程之间的通信

IPC的概念 即进程间的通信 常用方式&#xff1a; 1&#xff0c;管道通信&#xff1a;有名管道&#xff0c;无名管道 2&#xff0c;信号- 系统开销小 3&#xff0c;消息队列-内核的链表 4&#xff0c;信号量-计数器 5&#xff0c;共享内存 6&#xff0c;内存映射 7&…