snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
Some checks failed
Release to Docker / docker (push) Failing after 2s
Release Test Server to Docker / docker (push) Failing after 3s
Test / test (push) Failing after 16s

This commit is contained in:
upstream-mirror
2026-09-15 16:14:22 +05:00
commit 78704103fc
422 changed files with 154805 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,310 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sendsidebwe
import (
"errors"
"time"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils"
"go.uber.org/zap/zapcore"
)
// -------------------------------------------------------------
var (
errGroupFinalized = errors.New("packet group is finalized")
errOldPacket = errors.New("packet is older than packet group start")
)
// -------------------------------------------------------------
type PacketGroupConfig struct {
MinPackets int `yaml:"min_packets,omitempty"`
MaxWindowDuration time.Duration `yaml:"max_window_duration,omitempty"`
}
var (
defaultPacketGroupConfig = PacketGroupConfig{
MinPackets: 30,
MaxWindowDuration: 500 * time.Millisecond,
}
)
// -------------------------------------------------------------
type stat struct {
numPackets int
numBytes int
}
func (s *stat) add(size int) {
s.numPackets++
s.numBytes += size
}
func (s *stat) remove(size int) {
s.numPackets--
s.numBytes -= size
}
func (s *stat) getNumPackets() int {
return s.numPackets
}
func (s *stat) getNumBytes() int {
return s.numBytes
}
func (s stat) MarshalLogObject(e zapcore.ObjectEncoder) error {
e.AddInt("numPackets", s.numPackets)
e.AddInt("numBytes", s.numBytes)
return nil
}
// -------------------------------------------------------------
type classStat struct {
primary stat
rtx stat
probe stat
}
func (c *classStat) add(size int, isRTX bool, isProbe bool) {
if isRTX {
c.rtx.add(size)
} else if isProbe {
c.probe.add(size)
} else {
c.primary.add(size)
}
}
func (c *classStat) remove(size int, isRTX bool, isProbe bool) {
if isRTX {
c.rtx.remove(size)
} else if isProbe {
c.probe.remove(size)
} else {
c.primary.remove(size)
}
}
func (c *classStat) numPackets() int {
return c.primary.getNumPackets() + c.rtx.getNumPackets() + c.probe.getNumPackets()
}
func (c *classStat) numBytes() int {
return c.primary.getNumBytes() + c.rtx.getNumBytes() + c.probe.getNumBytes()
}
func (c classStat) MarshalLogObject(e zapcore.ObjectEncoder) error {
e.AddObject("primary", c.primary)
e.AddObject("rtx", c.rtx)
e.AddObject("probe", c.probe)
return nil
}
// -------------------------------------------------------------
type packetGroupParams struct {
Config PacketGroupConfig
WeightedLoss WeightedLossConfig
Logger logger.Logger
}
type packetGroup struct {
params packetGroupParams
minSequenceNumber uint64
maxSequenceNumber uint64
minSendTime int64
maxSendTime int64
minRecvTime int64 // for information only
maxRecvTime int64 // for information only
acked classStat
lost classStat
snBitmap *utils.Bitmap[uint64]
aggregateSendDelta int64
aggregateRecvDelta int64
inheritedQueuingDelay int64
isFinalized bool
}
func newPacketGroup(params packetGroupParams, inheritedQueuingDelay int64) *packetGroup {
return &packetGroup{
params: params,
inheritedQueuingDelay: inheritedQueuingDelay,
snBitmap: utils.NewBitmap[uint64](params.Config.MinPackets),
}
}
func (p *packetGroup) Add(pi *packetInfo, sendDelta, recvDelta int64, isLost bool) error {
if isLost {
return p.lostPacket(pi)
}
if err := p.inGroup(pi.sequenceNumber); err != nil {
return err
}
if p.minSequenceNumber == 0 || pi.sequenceNumber < p.minSequenceNumber {
p.minSequenceNumber = pi.sequenceNumber
}
p.maxSequenceNumber = max(p.maxSequenceNumber, pi.sequenceNumber)
if p.minSendTime == 0 || (pi.sendTime-sendDelta) < p.minSendTime {
p.minSendTime = pi.sendTime - sendDelta
}
p.maxSendTime = max(p.maxSendTime, pi.sendTime)
if p.minRecvTime == 0 || (pi.recvTime-recvDelta) < p.minRecvTime {
p.minRecvTime = pi.recvTime - recvDelta
}
p.maxRecvTime = max(p.maxRecvTime, pi.recvTime)
p.acked.add(int(pi.size), pi.isRTX, pi.isProbe)
if int(pi.sequenceNumber-p.minSequenceNumber) < p.snBitmap.Len() && p.snBitmap.IsSet(pi.sequenceNumber-p.minSequenceNumber) {
// an earlier packet reported as lost has been received
p.snBitmap.Clear(pi.sequenceNumber - p.minSequenceNumber)
p.lost.remove(int(pi.size), pi.isRTX, pi.isProbe)
}
// note that out-of-order deliveries will amplify the queueing delay.
// for e.g. a, b, c getting delivered as a, c, b.
// let us say packets are delivered with interval of `x`
// send delta aggregate will go up by x((a, c) = 2x + (c, b) -1x)
// recv delta aggregate will go up by 3x((a, c) = 2x + (c, b) 1x)
p.aggregateSendDelta += sendDelta
p.aggregateRecvDelta += recvDelta
if p.acked.numPackets() == p.params.Config.MinPackets || (pi.sendTime-p.minSendTime) > p.params.Config.MaxWindowDuration.Microseconds() {
p.isFinalized = true
}
return nil
}
func (p *packetGroup) lostPacket(pi *packetInfo) error {
if pi.recvTime != 0 {
// previously received packet, so not lost
return nil
}
if err := p.inGroup(pi.sequenceNumber); err != nil {
return err
}
if p.minSequenceNumber == 0 || pi.sequenceNumber < p.minSequenceNumber {
p.minSequenceNumber = pi.sequenceNumber
}
p.maxSequenceNumber = max(p.maxSequenceNumber, pi.sequenceNumber)
p.snBitmap.Set(pi.sequenceNumber - p.minSequenceNumber)
p.lost.add(int(pi.size), pi.isRTX, pi.isProbe)
return nil
}
func (p *packetGroup) MinSendTime() int64 {
return p.minSendTime
}
func (p *packetGroup) SendWindow() (int64, int64) {
return p.minSendTime, p.maxSendTime
}
func (p *packetGroup) PropagatedQueuingDelay() int64 {
if p.inheritedQueuingDelay+p.aggregateRecvDelta-p.aggregateSendDelta > 0 {
return p.inheritedQueuingDelay + p.aggregateRecvDelta - p.aggregateSendDelta
}
return max(0, p.aggregateRecvDelta-p.aggregateSendDelta)
}
func (p *packetGroup) FinalizedPropagatedQueuingDelay() (int64, bool) {
if !p.isFinalized {
return 0, false
}
return p.PropagatedQueuingDelay(), true
}
func (p *packetGroup) IsFinalized() bool {
return p.isFinalized
}
func (p *packetGroup) Traffic() *trafficStats {
return &trafficStats{
minSendTime: p.minSendTime,
maxSendTime: p.maxSendTime,
sendDelta: p.aggregateSendDelta,
recvDelta: p.aggregateRecvDelta,
ackedPackets: p.acked.numPackets(),
ackedBytes: p.acked.numBytes(),
lostPackets: p.lost.numPackets(),
lostBytes: p.lost.numBytes(),
}
}
func (p *packetGroup) MarshalLogObject(e zapcore.ObjectEncoder) error {
if p == nil {
return nil
}
e.AddUint64("minSequenceNumber", p.minSequenceNumber)
e.AddUint64("maxSequenceNumber", p.maxSequenceNumber)
e.AddObject("acked", p.acked)
e.AddObject("lost", p.lost)
e.AddInt64("minRecvTime", p.minRecvTime)
e.AddInt64("maxRecvTime", p.maxRecvTime)
recvDuration := time.Duration((p.maxRecvTime - p.minRecvTime) * 1000)
e.AddDuration("recvDuration", recvDuration)
recvBitrate := float64(0)
if recvDuration != 0 {
recvBitrate = float64(p.acked.numBytes()*8) / recvDuration.Seconds()
e.AddFloat64("recvBitrate", recvBitrate)
}
ts := newTrafficStats(trafficStatsParams{
Config: p.params.WeightedLoss,
Logger: p.params.Logger,
})
ts.Merge(p.Traffic())
e.AddObject("trafficStats", ts)
e.AddInt64("inheritedQueuingDelay", p.inheritedQueuingDelay)
e.AddInt64("propagatedQueuingDelay", p.PropagatedQueuingDelay())
e.AddBool("isFinalized", p.isFinalized)
return nil
}
func (p *packetGroup) inGroup(sequenceNumber uint64) error {
if p.isFinalized && sequenceNumber > p.maxSequenceNumber {
return errGroupFinalized
}
if sequenceNumber < p.minSequenceNumber {
return errOldPacket
}
return nil
}

View File

@@ -0,0 +1,45 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sendsidebwe
import (
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
"go.uber.org/zap/zapcore"
)
type packetInfo struct {
sequenceNumber uint64
sendTime int64
recvTime int64
probeClusterId ccutils.ProbeClusterId
size uint16
isRTX bool
isProbe bool
}
func (pi *packetInfo) MarshalLogObject(e zapcore.ObjectEncoder) error {
if pi == nil {
return nil
}
e.AddUint64("sequenceNumber", pi.sequenceNumber)
e.AddInt64("sendTime", pi.sendTime)
e.AddInt64("recvTime", pi.recvTime)
e.AddUint32("probeClusterId", uint32(pi.probeClusterId))
e.AddUint16("size", pi.size)
e.AddBool("isRTX", pi.isRTX)
e.AddBool("isProbe", pi.isProbe)
return nil
}

View File

@@ -0,0 +1,168 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sendsidebwe
import (
"math/rand"
"sync"
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
)
// -------------------------------------------------------------------------------
type packetTrackerParams struct {
Logger logger.Logger
}
type packetTracker struct {
params packetTrackerParams
lock sync.Mutex
sequenceNumber uint64
baseSendTime int64
packetInfos [2048]packetInfo
baseRecvTime int64
piLastRecv *packetInfo
probeClusterId ccutils.ProbeClusterId
probeMaxSequenceNumber uint64
}
func newPacketTracker(params packetTrackerParams) *packetTracker {
return &packetTracker{
params: params,
sequenceNumber: uint64(rand.Intn(1<<14)) + uint64(1<<15), // a random number in third quartile of sequence number space
}
}
func (p *packetTracker) RecordPacketSendAndGetSequenceNumber(
atMicro int64,
size int,
isRTX bool,
probeClusterId ccutils.ProbeClusterId,
isProbe bool,
) uint16 {
p.lock.Lock()
defer p.lock.Unlock()
if p.baseSendTime == 0 {
p.baseSendTime = atMicro
}
pi := p.getPacketInfo(uint16(p.sequenceNumber))
*pi = packetInfo{
sequenceNumber: p.sequenceNumber,
sendTime: atMicro - p.baseSendTime,
size: uint16(size),
isRTX: isRTX,
probeClusterId: probeClusterId,
isProbe: isProbe,
}
p.sequenceNumber++
// extreme case of wrap around before receiving any feedback
if pi == p.piLastRecv {
p.piLastRecv = nil
}
if p.probeClusterId != ccutils.ProbeClusterIdInvalid && p.probeClusterId == pi.probeClusterId && pi.sequenceNumber > p.probeMaxSequenceNumber {
p.probeMaxSequenceNumber = pi.sequenceNumber
}
return uint16(pi.sequenceNumber)
}
func (p *packetTracker) BaseSendTimeThreshold(threshold int64) (int64, bool) {
p.lock.Lock()
defer p.lock.Unlock()
if p.baseSendTime == 0 {
return 0, false
}
return mono.UnixMicro() - p.baseSendTime - threshold, true
}
func (p *packetTracker) RecordPacketIndicationFromRemote(sn uint16, recvTime int64) (piRecv packetInfo, sendDelta, recvDelta int64) {
p.lock.Lock()
defer p.lock.Unlock()
pi := p.getPacketInfoExisting(sn)
if pi == nil {
return
}
if recvTime == 0 {
// maybe lost OR already received but reported lost in a later report
piRecv = *pi
return
}
if p.baseRecvTime == 0 {
p.baseRecvTime = recvTime
p.piLastRecv = pi
}
pi.recvTime = recvTime - p.baseRecvTime
piRecv = *pi
if p.piLastRecv != nil {
sendDelta, recvDelta = pi.sendTime-p.piLastRecv.sendTime, pi.recvTime-p.piLastRecv.recvTime
}
p.piLastRecv = pi
return
}
func (p *packetTracker) getPacketInfo(sn uint16) *packetInfo {
return &p.packetInfos[int(sn)%len(p.packetInfos)]
}
func (p *packetTracker) getPacketInfoExisting(sn uint16) *packetInfo {
pi := &p.packetInfos[int(sn)%len(p.packetInfos)]
if uint16(pi.sequenceNumber) == sn {
return pi
}
return nil
}
func (p *packetTracker) ProbeClusterStarting(probeClusterId ccutils.ProbeClusterId) {
p.lock.Lock()
defer p.lock.Unlock()
p.probeClusterId = probeClusterId
}
func (p *packetTracker) ProbeClusterDone(probeClusterId ccutils.ProbeClusterId) {
p.lock.Lock()
defer p.lock.Unlock()
if p.probeClusterId == probeClusterId {
p.probeClusterId = ccutils.ProbeClusterIdInvalid
}
}
func (p *packetTracker) ProbeMaxSequenceNumber() uint64 {
p.lock.Lock()
defer p.lock.Unlock()
return p.probeMaxSequenceNumber
}

View File

@@ -0,0 +1,137 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sendsidebwe
import (
"time"
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
"github.com/livekit/protocol/logger"
"github.com/livekit/protocol/utils/mono"
"go.uber.org/zap/zapcore"
)
// -------------------------------------------------------------
type ProbePacketGroupConfig struct {
PacketGroup PacketGroupConfig `yaml:"packet_group,omitempty"`
SettleWaitNumRTT uint32 `yaml:"settle_wait_num_rtt,omitempty"`
SettleWaitMin time.Duration `yaml:"settle_wait_min,omitempty"`
SettleWaitMax time.Duration `yaml:"settle_wait_max,omitempty"`
}
var (
// large numbers to treat a probe packet group as one
defaultProbePacketGroupConfig = ProbePacketGroupConfig{
PacketGroup: PacketGroupConfig{
MinPackets: 16384,
MaxWindowDuration: time.Minute,
},
SettleWaitNumRTT: 5,
SettleWaitMin: 250 * time.Millisecond,
SettleWaitMax: 5 * time.Second,
}
)
// -------------------------------------------------------------
type probePacketGroupParams struct {
Config ProbePacketGroupConfig
WeightedLoss WeightedLossConfig
Logger logger.Logger
}
type probePacketGroup struct {
params probePacketGroupParams
pci ccutils.ProbeClusterInfo
*packetGroup
maxSequenceNumber uint64
doneAt time.Time
}
func newProbePacketGroup(params probePacketGroupParams, pci ccutils.ProbeClusterInfo) *probePacketGroup {
return &probePacketGroup{
params: params,
pci: pci,
packetGroup: newPacketGroup(
packetGroupParams{
Config: params.Config.PacketGroup,
WeightedLoss: params.WeightedLoss,
Logger: params.Logger,
},
0,
),
}
}
func (p *probePacketGroup) ProbeClusterDone(pci ccutils.ProbeClusterInfo) {
if p.pci.Id != pci.Id {
return
}
p.pci.Result = pci.Result
p.doneAt = mono.Now()
}
func (p *probePacketGroup) ProbeClusterInfo() ccutils.ProbeClusterInfo {
return p.pci
}
func (p *probePacketGroup) MaybeFinalizeProbe(maxSequenceNumber uint64, rtt float64) (ccutils.ProbeClusterInfo, bool) {
if p.doneAt.IsZero() {
return ccutils.ProbeClusterInfoInvalid, false
}
if maxSequenceNumber != 0 && p.maxSequenceNumber >= maxSequenceNumber {
return p.pci, true
}
settleWait := time.Duration(float64(p.params.Config.SettleWaitNumRTT) * rtt * float64(time.Second))
if settleWait < p.params.Config.SettleWaitMin {
settleWait = p.params.Config.SettleWaitMin
}
if settleWait > p.params.Config.SettleWaitMax {
settleWait = p.params.Config.SettleWaitMax
}
if time.Since(p.doneAt) < settleWait {
return ccutils.ProbeClusterInfoInvalid, false
}
return p.pci, true
}
func (p *probePacketGroup) Add(pi *packetInfo, sendDelta, recvDelta int64, isLost bool) error {
if pi.probeClusterId != p.pci.Id {
return nil
}
p.maxSequenceNumber = max(p.maxSequenceNumber, pi.sequenceNumber)
return p.packetGroup.Add(pi, sendDelta, recvDelta, isLost)
}
func (p *probePacketGroup) MarshalLogObject(e zapcore.ObjectEncoder) error {
if p == nil {
return nil
}
e.AddObject("pci", p.pci)
e.AddObject("packetGroup", p.packetGroup)
e.AddUint64("maxSequenceNumber", p.maxSequenceNumber)
e.AddTime("doneAt", p.doneAt)
return nil
}

View File

@@ -0,0 +1,151 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sendsidebwe
import (
"time"
"github.com/livekit/livekit-server/pkg/sfu/bwe"
"github.com/livekit/livekit-server/pkg/sfu/ccutils"
"github.com/livekit/protocol/logger"
"github.com/pion/rtcp"
)
var _ bwe.BWE = (*SendSideBWE)(nil)
//
// Based on a simplified/modified version of JitterPath paper
// (https://homepage.iis.sinica.edu.tw/papers/lcs/2114-F.pdf)
//
// TWCC feedback is used to calculate delta one-way-delay.
// It is accumulated/propagated to determine in which region
// groups of packets are operating in.
//
// In simplified terms,
// o JQR (Join Queuing Region) is when channel is congested.
// o DQR (Disjoint Queuing Region) is when channel is not.
//
// Packets are grouped and thresholds applied to smooth over
// small variations. For example, in the paper,
// if propagated_queuing_delay + delta_one_way_delay > 0 {
// possibly_operating_in_jqr
// }
// But, in this implementation it is checked at packet group level,
// i. e. using queuing delay and aggregated delta one-way-delay of
// the group and a minimum value threshold is applied before declaring
// that a group is in JQR.
//
// There is also hysteresis to make transisitons smoother, i.e. if the
// metric is above a certain threshold, it is JQR and it is DQR only if it
// is below a certain value and the gap in between those two thresholds
// are treated as interdeterminate groups.
//
// ---------------------------------------------------------------------------
type SendSideBWEConfig struct {
CongestionDetector CongestionDetectorConfig `yaml:"congestion_detector,omitempty"`
}
var (
DefaultSendSideBWEConfig = SendSideBWEConfig{
CongestionDetector: defaultCongestionDetectorConfig,
}
)
// ---------------------------------------------------------------------------
type SendSideBWEParams struct {
Config SendSideBWEConfig
Logger logger.Logger
}
type SendSideBWE struct {
bwe.NullBWE
params SendSideBWEParams
*congestionDetector
}
func NewSendSideBWE(params SendSideBWEParams) *SendSideBWE {
return &SendSideBWE{
params: params,
congestionDetector: newCongestionDetector(congestionDetectorParams{
Config: params.Config.CongestionDetector,
Logger: params.Logger,
}),
}
}
func (s *SendSideBWE) Type() bwe.BWEType {
return bwe.BWETypeSendSide
}
func (s *SendSideBWE) SetBWEListener(bweListener bwe.BWEListener) {
s.congestionDetector.SetBWEListener(bweListener)
}
func (s *SendSideBWE) Reset() {
s.congestionDetector.Reset()
}
func (s *SendSideBWE) RecordPacketSendAndGetSequenceNumber(
atMicro int64,
size int,
isRTX bool,
probeClusterId ccutils.ProbeClusterId,
isProbe bool,
) uint16 {
return s.congestionDetector.RecordPacketSendAndGetSequenceNumber(atMicro, size, isRTX, probeClusterId, isProbe)
}
func (s *SendSideBWE) HandleTWCCFeedback(report *rtcp.TransportLayerCC) {
s.congestionDetector.HandleTWCCFeedback(report)
}
func (s *SendSideBWE) UpdateRTT(rtt float64) {
s.congestionDetector.UpdateRTT(rtt)
}
func (s *SendSideBWE) CongestionState() bwe.CongestionState {
return s.congestionDetector.CongestionState()
}
func (s *SendSideBWE) CanProbe() bool {
return s.congestionDetector.CanProbe()
}
func (s *SendSideBWE) ProbeDuration() time.Duration {
return s.congestionDetector.ProbeDuration()
}
func (s *SendSideBWE) ProbeClusterStarting(pci ccutils.ProbeClusterInfo) {
s.congestionDetector.ProbeClusterStarting(pci)
}
func (s *SendSideBWE) ProbeClusterDone(pci ccutils.ProbeClusterInfo) {
s.congestionDetector.ProbeClusterDone(pci)
}
func (s *SendSideBWE) ProbeClusterIsGoalReached() bool {
return s.congestionDetector.ProbeClusterIsGoalReached()
}
func (s *SendSideBWE) ProbeClusterFinalize() (ccutils.ProbeSignal, int64, bool) {
return s.congestionDetector.ProbeClusterFinalize()
}
// ------------------------------------------------

View File

@@ -0,0 +1,191 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sendsidebwe
import (
"math"
"time"
"github.com/livekit/protocol/logger"
"go.uber.org/zap/zapcore"
)
// -----------------------------------------------------------
type WeightedLossConfig struct {
MinDurationForLossValidity time.Duration `yaml:"min_duration_for_loss_validity,omitempty"`
BaseDuration time.Duration `yaml:"base_duration,omitempty"`
BasePPS int `yaml:"base_pps,omitempty"`
LossPenaltyFactor float64 `yaml:"loss_penalty_factor,omitempty"`
}
var (
defaultWeightedLossConfig = WeightedLossConfig{
MinDurationForLossValidity: 100 * time.Millisecond,
BaseDuration: 500 * time.Millisecond,
BasePPS: 30,
LossPenaltyFactor: 0.25,
}
)
// -----------------------------------------------------------
type trafficStatsParams struct {
Config WeightedLossConfig
Logger logger.Logger
}
type trafficStats struct {
params trafficStatsParams
minSendTime int64
maxSendTime int64
sendDelta int64
recvDelta int64
ackedPackets int
ackedBytes int
lostPackets int
lostBytes int
}
func newTrafficStats(params trafficStatsParams) *trafficStats {
return &trafficStats{
params: params,
}
}
func (ts *trafficStats) Merge(rhs *trafficStats) {
if ts.minSendTime == 0 || rhs.minSendTime < ts.minSendTime {
ts.minSendTime = rhs.minSendTime
}
if rhs.maxSendTime > ts.maxSendTime {
ts.maxSendTime = rhs.maxSendTime
}
ts.sendDelta += rhs.sendDelta
ts.recvDelta += rhs.recvDelta
ts.ackedPackets += rhs.ackedPackets
ts.ackedBytes += rhs.ackedBytes
ts.lostPackets += rhs.lostPackets
ts.lostBytes += rhs.lostBytes
}
func (ts *trafficStats) NumBytes() int {
return ts.ackedBytes + ts.lostBytes
}
func (ts *trafficStats) Duration() int64 {
return ts.maxSendTime - ts.minSendTime
}
func (ts *trafficStats) AcknowledgedBitrate() int64 {
duration := ts.Duration()
if duration == 0 {
return 0
}
ackedBitrate := float64(ts.ackedBytes) * 8 * 1e6 / float64(ts.Duration())
return int64(ackedBitrate * ts.CapturedTrafficRatio())
}
func (ts *trafficStats) CapturedTrafficRatio() float64 {
if ts.recvDelta == 0 {
return 0.0
}
// apply a penalty for lost packets,
// the rationale being packet dropping is a strategy to relieve congestion
// and if they were not dropped, they would have increased queuing delay,
// as it is not possible to know the reason for the losses,
// apply a small penalty to receive delta aggregate to simulate those packets
// building up queuing delay.
return min(1.0, float64(ts.sendDelta)/float64(ts.recvDelta+ts.lossPenalty()))
}
func (ts *trafficStats) WeightedLoss() float64 {
durationMicro := ts.Duration()
if time.Duration(durationMicro*1000) < ts.params.Config.MinDurationForLossValidity {
return 0.0
}
totalPackets := float64(ts.lostPackets + ts.ackedPackets)
pps := totalPackets * 1e6 / float64(durationMicro)
// longer duration, i. e. more time resolution, lower pps is acceptable as the measurement is more stable
deltaDuration := time.Duration(durationMicro*1000) - ts.params.Config.BaseDuration
if deltaDuration < 0 {
deltaDuration = 0
}
threshold := math.Exp(-deltaDuration.Seconds()) * float64(ts.params.Config.BasePPS)
if pps < threshold {
return 0.0
}
lossRatio := float64(0.0)
if totalPackets != 0 {
lossRatio = float64(ts.lostPackets) / totalPackets
}
// Log10 is used to give higher weight for the same loss ratio at higher packet rates,
// for e.g.
// - 10% loss at 20 pps = 0.1 * log10(20) = 0.130
// - 10% loss at 100 pps = 0.1 * log10(100) = 0.2
// - 10% loss at 1000 pps = 0.1 * log10(1000) = 0.3
return lossRatio * math.Log10(pps)
}
func (ts *trafficStats) lossPenalty() int64 {
return int64(float64(ts.recvDelta) * ts.WeightedLoss() * ts.params.Config.LossPenaltyFactor)
}
func (ts *trafficStats) MarshalLogObject(e zapcore.ObjectEncoder) error {
if ts == nil {
return nil
}
e.AddInt64("minSendTime", ts.minSendTime)
e.AddInt64("maxSendTime", ts.maxSendTime)
duration := time.Duration(ts.Duration() * 1000)
e.AddDuration("duration", duration)
e.AddInt("ackedPackets", ts.ackedPackets)
e.AddInt("ackedBytes", ts.ackedBytes)
e.AddInt("lostPackets", ts.lostPackets)
e.AddInt("lostBytes", ts.lostBytes)
bitrate := float64(0)
if duration != 0 {
bitrate = float64(ts.ackedBytes*8) / duration.Seconds()
e.AddFloat64("bitrate", bitrate)
}
e.AddInt64("sendDelta", ts.sendDelta)
e.AddInt64("recvDelta", ts.recvDelta)
e.AddInt64("groupDelay", ts.recvDelta-ts.sendDelta)
totalPackets := ts.lostPackets + ts.ackedPackets
if duration != 0 {
e.AddFloat64("pps", float64(totalPackets)/duration.Seconds())
}
if (totalPackets) != 0 {
e.AddFloat64("rawLoss", float64(ts.lostPackets)/float64(totalPackets))
}
e.AddFloat64("weightedLoss", ts.WeightedLoss())
e.AddInt64("lossPenalty", ts.lossPenalty())
capturedTrafficRatio := ts.CapturedTrafficRatio()
e.AddFloat64("capturedTrafficRatio", capturedTrafficRatio)
e.AddFloat64("estimatedAvailableChannelCapacity", bitrate*capturedTrafficRatio)
return nil
}

View File

@@ -0,0 +1,127 @@
// Copyright 2023 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package sendsidebwe
import (
"time"
"github.com/livekit/protocol/logger"
"github.com/pion/rtcp"
"go.uber.org/zap/zapcore"
)
// ------------------------------------------------------
const (
cOutlierReportFactor = 3
cEstimatedFeedbackIntervalAlpha = float64(0.9)
cReferenceTimeMask = (1 << 24) - 1
cReferenceTimeResolution = 64 // 64 ms
)
// ------------------------------------------------------
type twccFeedbackParams struct {
Logger logger.Logger
}
type twccFeedback struct {
params twccFeedbackParams
lastFeedbackTime time.Time
estimatedFeedbackInterval time.Duration
numReports int
numReportsOutOfOrder int
highestFeedbackCount uint8
cycles int64
highestReferenceTime uint32
}
func newTWCCFeedback(params twccFeedbackParams) *twccFeedback {
return &twccFeedback{
params: params,
}
}
func (t *twccFeedback) ProcessReport(report *rtcp.TransportLayerCC, at time.Time) (int64, bool) {
t.numReports++
if t.lastFeedbackTime.IsZero() {
t.lastFeedbackTime = at
t.highestReferenceTime = report.ReferenceTime
t.highestFeedbackCount = report.FbPktCount
return (t.cycles + int64(report.ReferenceTime)) * cReferenceTimeResolution * 1000, false
}
isOutOfOrder := false
if (report.FbPktCount - t.highestFeedbackCount) > (1 << 7) {
t.numReportsOutOfOrder++
isOutOfOrder = true
t.params.Logger.Infow(
"send side bwe: received out-of-order feedback report",
"highestFeedbackCount", t.highestFeedbackCount,
"fbPktCount", report.FbPktCount,
"numReportsOutOfOrder", t.numReportsOutOfOrder,
)
}
// reference time wrap around handling
var referenceTime int64
if (report.ReferenceTime-t.highestReferenceTime)&cReferenceTimeMask < (1 << 23) {
if report.ReferenceTime < t.highestReferenceTime {
t.cycles += (1 << 24)
}
t.highestReferenceTime = report.ReferenceTime
referenceTime = t.cycles + int64(report.ReferenceTime)
} else {
cycles := t.cycles
if report.ReferenceTime > t.highestReferenceTime && cycles >= (1<<24) {
cycles -= (1 << 24)
}
referenceTime = cycles + int64(report.ReferenceTime)
}
if !isOutOfOrder {
sinceLast := at.Sub(t.lastFeedbackTime)
if t.estimatedFeedbackInterval == 0 {
t.estimatedFeedbackInterval = sinceLast
} else {
// filter out outliers from estimate
if sinceLast > t.estimatedFeedbackInterval/cOutlierReportFactor && sinceLast < cOutlierReportFactor*t.estimatedFeedbackInterval {
// smoothed version of inter feedback interval
t.estimatedFeedbackInterval = time.Duration(cEstimatedFeedbackIntervalAlpha*float64(t.estimatedFeedbackInterval) + (1.0-cEstimatedFeedbackIntervalAlpha)*float64(sinceLast))
}
}
t.lastFeedbackTime = at
t.highestFeedbackCount = report.FbPktCount
}
return referenceTime * cReferenceTimeResolution * 1000, isOutOfOrder
}
func (t *twccFeedback) MarshalLogObject(e zapcore.ObjectEncoder) error {
if t == nil {
return nil
}
e.AddTime("lastFeedbackTime", t.lastFeedbackTime)
e.AddDuration("estimatedFeedbackInterval", t.estimatedFeedbackInterval)
e.AddInt("numReports", t.numReports)
e.AddInt("numReportsOutOfOrder", t.numReportsOutOfOrder)
e.AddInt64("cycles", t.cycles/(1<<24))
return nil
}