一. 前言
二. 空间可伸缩与时间可伸缩
三. 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 | 优先级 |
rid | rtp-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,确保时间戳不出现大的跳变。