// 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 }