mirror of
https://github.com/livekit/livekit.git
synced 2026-10-23 00:02:53 +00:00
Experimental flag to try time stamp adjustment to control drift. (#1687)
* Experimental flag to try time stamp adjustment to control drift. There is a config to enable this. Using a PID controller to try and keep the sample rate at expected value. Need to be seen if this works well. Adjustment are limited to 25 ms max at a time to ensure there are no large jumps. And it is applied when doing RTCP sender report which happens once in 5 seconds currently for both audio and video tracks. A nice introduction to PID controllers - https://alphaville.github.io/qub/pid-101/#/ Implementation borrowed from - https://github.com/pms67/PID A few things TODO 1. PID controller tuning is a process. Have picked values from test from that implementation above. May not be the best. Need to try. 2. Can potentially run this more often. Rather than running it only when running RTCP sender report (which is once in 5 seconds now), can potentially run it every second and limit the amount of change to something like 10 ms max. * remove unused variable * debug log a bit more
This commit is contained in:
1 parent
ae5d2c3261
commit
3fb93135f5
11 files changed
+298
-49
No files matched your search
+147
-38
@@ -174,21 +174,28 @@ type RTPStats struct {
|
||||
rtt uint32
|
||||
maxRtt uint32
|
||||
|
||||
srData *RTCPSenderReportData
|
||||
lastSRTime time.Time
|
||||
lastSRNTP mediatransportutil.NtpTime
|
||||
srData *RTCPSenderReportData
|
||||
lastSRTime time.Time
|
||||
lastSRNTP mediatransportutil.NtpTime
|
||||
pidController *PIDController
|
||||
|
||||
nextSnapshotId uint32
|
||||
snapshots map[uint32]*Snapshot
|
||||
}
|
||||
|
||||
func NewRTPStats(params RTPStatsParams) *RTPStats {
|
||||
return &RTPStats{
|
||||
r := &RTPStats{
|
||||
params: params,
|
||||
logger: params.Logger,
|
||||
nextSnapshotId: FirstSnapshotId,
|
||||
snapshots: make(map[uint32]*Snapshot),
|
||||
pidController: NewPIDController(),
|
||||
}
|
||||
|
||||
r.pidController.SetGains(2.0, 0.5, 0.25)
|
||||
r.pidController.SetDerivativeLPF(0.02)
|
||||
r.pidController.SetOutputLimits(-0.025*float64(params.ClockRate), 0.025*float64(params.ClockRate))
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *RTPStats) Seed(from *RTPStats) {
|
||||
@@ -731,37 +738,35 @@ func (r *RTPStats) SetRtcpSenderReportData(srData *RTCPSenderReportData) {
|
||||
}
|
||||
|
||||
// TODO-REMOVE-AFTER-DEBUG-START
|
||||
if r.params.ClockRate != 90000 { // log only for audio as it is less frequent
|
||||
ntpTime := srData.NTPTimestamp.Time()
|
||||
ntpTime := srData.NTPTimestamp.Time()
|
||||
|
||||
var ntpDiffSinceLast, arrivalDiffSinceLast time.Duration
|
||||
var rtpDiffSinceLast uint32
|
||||
if r.srData != nil {
|
||||
ntpDiffSinceLast = ntpTime.Sub(r.srData.NTPTimestamp.Time())
|
||||
rtpDiffSinceLast = srData.RTPTimestamp - r.srData.RTPTimestamp
|
||||
arrivalDiffSinceLast = srData.ArrivalTime.Sub(r.srData.ArrivalTime)
|
||||
}
|
||||
|
||||
timeSinceFirst := srData.NTPTimestamp.Time().Sub(r.firstTime)
|
||||
rtpDiffSinceFirst := getExtTS(srData.RTPTimestamp, r.tsCycles) - r.extStartTS
|
||||
drift := int64(uint64(timeSinceFirst.Nanoseconds()*int64(r.params.ClockRate)/1e9) - rtpDiffSinceFirst)
|
||||
driftMs := (float64(drift) * 1000) / float64(r.params.ClockRate)
|
||||
|
||||
r.logger.Debugw(
|
||||
"received sender report",
|
||||
"ntp", ntpTime,
|
||||
"rtp", srData.RTPTimestamp,
|
||||
"arrival", srData.ArrivalTime,
|
||||
"ntpDiff", ntpDiffSinceLast,
|
||||
"rtpDiff", rtpDiffSinceLast,
|
||||
"arrivalDiff", arrivalDiffSinceLast,
|
||||
"expectedTimeDiff", float64(rtpDiffSinceLast)/float64(r.params.ClockRate),
|
||||
"timeSinceFirst", timeSinceFirst,
|
||||
"rtpDiffSinceFirst", rtpDiffSinceFirst,
|
||||
"drift", drift,
|
||||
"driftMs", driftMs,
|
||||
)
|
||||
var ntpDiffSinceLast, arrivalDiffSinceLast time.Duration
|
||||
var rtpDiffSinceLast uint32
|
||||
if r.srData != nil {
|
||||
ntpDiffSinceLast = ntpTime.Sub(r.srData.NTPTimestamp.Time())
|
||||
rtpDiffSinceLast = srData.RTPTimestamp - r.srData.RTPTimestamp
|
||||
arrivalDiffSinceLast = srData.ArrivalTime.Sub(r.srData.ArrivalTime)
|
||||
}
|
||||
|
||||
timeSinceFirst := time.Since(r.firstTime) // ideally should use NTP time from SR, but that is a different time base, now is a resonable approximation
|
||||
rtpDiffSinceFirst := getExtTS(srData.RTPTimestamp, r.tsCycles) - r.extStartTS
|
||||
drift := int64(uint64(timeSinceFirst.Nanoseconds()*int64(r.params.ClockRate)/1e9) - rtpDiffSinceFirst)
|
||||
driftMs := (float64(drift) * 1000) / float64(r.params.ClockRate)
|
||||
|
||||
r.logger.Debugw(
|
||||
"received sender report",
|
||||
"ntp", ntpTime,
|
||||
"rtp", srData.RTPTimestamp,
|
||||
"arrival", srData.ArrivalTime,
|
||||
"ntpDiff", ntpDiffSinceLast,
|
||||
"rtpDiff", rtpDiffSinceLast,
|
||||
"arrivalDiff", arrivalDiffSinceLast,
|
||||
"expectedTimeDiff", float64(rtpDiffSinceLast)/float64(r.params.ClockRate),
|
||||
"timeSinceFirst", timeSinceFirst,
|
||||
"rtpDiffSinceFirst", rtpDiffSinceFirst,
|
||||
"drift", drift,
|
||||
"driftMs", driftMs,
|
||||
)
|
||||
// TODO-REMOVE-AFTER-DEBUG-END
|
||||
|
||||
srDataCopy := *srData
|
||||
@@ -806,12 +811,12 @@ func (r *RTPStats) GetExpectedRTPTimestamp(at time.Time) (uint32, error) {
|
||||
return uint32(expectedExtRTP), nil
|
||||
}
|
||||
|
||||
func (r *RTPStats) GetRtcpSenderReport(ssrc uint32) *rtcp.SenderReport {
|
||||
func (r *RTPStats) GetRtcpSenderReport(ssrc uint32) (*rtcp.SenderReport, float64) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
|
||||
if !r.initialized {
|
||||
return nil
|
||||
return nil, 0.0
|
||||
}
|
||||
|
||||
// construct current time based on monotonic clock
|
||||
@@ -836,6 +841,23 @@ func (r *RTPStats) GetRtcpSenderReport(ssrc uint32) *rtcp.SenderReport {
|
||||
timeSinceHighest := time.Since(r.highestTime)
|
||||
nowRTP := r.highestTS + uint32(timeSinceHighest.Nanoseconds()*int64(r.params.ClockRate)/1e9)
|
||||
|
||||
// TODO-REMOVE-AFTER-DEBUG-START
|
||||
timeSinceFirst = nowNTP.Time().Sub(r.firstTime)
|
||||
rtpDiffSinceFirst := getExtTS(nowRTP, r.tsCycles) - r.extStartTS
|
||||
measurement := float64(rtpDiffSinceFirst) / timeSinceFirst.Seconds()
|
||||
pidOutput := r.pidController.Update(
|
||||
float64(r.params.ClockRate),
|
||||
measurement,
|
||||
now,
|
||||
)
|
||||
r.logger.Debugw(
|
||||
"pid controller output",
|
||||
"measurement", measurement,
|
||||
"errorTerm", float64(r.params.ClockRate)-measurement,
|
||||
"pidOutput", pidOutput,
|
||||
)
|
||||
// TODO-REMOVE-AFTER-DEBUG-STOP
|
||||
|
||||
// TODO-REMOVE-AFTER-DEBUG-START
|
||||
ntpTime := nowNTP.Time()
|
||||
|
||||
@@ -843,8 +865,6 @@ func (r *RTPStats) GetRtcpSenderReport(ssrc uint32) *rtcp.SenderReport {
|
||||
rtpDiffLocal := int32(nowRTP - r.highestTS)
|
||||
rtpOffsetLocal := int32(nowRTP - r.highestTS - uint32(ntpDiffLocal.Nanoseconds()*int64(r.params.ClockRate)/1e9))
|
||||
|
||||
timeSinceFirst = nowNTP.Time().Sub(r.firstTime)
|
||||
rtpDiffSinceFirst := getExtTS(nowRTP, r.tsCycles) - r.extStartTS
|
||||
drift := int64(uint64(timeSinceFirst.Nanoseconds()*int64(r.params.ClockRate)/1e9) - rtpDiffSinceFirst)
|
||||
driftMs := (float64(drift) * 1000) / float64(r.params.ClockRate)
|
||||
|
||||
@@ -853,6 +873,7 @@ func (r *RTPStats) GetRtcpSenderReport(ssrc uint32) *rtcp.SenderReport {
|
||||
"highestTS", r.highestTS,
|
||||
"highestTime", r.highestTime.String(),
|
||||
"reportTS", nowRTP,
|
||||
"expectedTS", uint32(expectedExtRTP),
|
||||
"reportTime", ntpTime.String(),
|
||||
"rtpDiffLocal", rtpDiffLocal,
|
||||
"ntpDiffLocal", ntpDiffLocal,
|
||||
@@ -861,6 +882,7 @@ func (r *RTPStats) GetRtcpSenderReport(ssrc uint32) *rtcp.SenderReport {
|
||||
"rtpDiffSinceFirst", rtpDiffSinceFirst,
|
||||
"drift", drift,
|
||||
"driftMs", driftMs,
|
||||
"rate", measurement,
|
||||
)
|
||||
// TODO-REMOVE-AFTER-DEBUG-END
|
||||
|
||||
@@ -873,7 +895,7 @@ func (r *RTPStats) GetRtcpSenderReport(ssrc uint32) *rtcp.SenderReport {
|
||||
RTPTime: nowRTP,
|
||||
PacketCount: r.getTotalPacketsPrimary() + r.packetsDuplicate + r.packetsPadding,
|
||||
OctetCount: uint32(r.bytes + r.bytesDuplicate + r.bytesPadding),
|
||||
}
|
||||
}, pidOutput
|
||||
}
|
||||
|
||||
func (r *RTPStats) SnapshotRtcpReceptionReport(ssrc uint32, proxyFracLost uint8, snapshotId uint32) *rtcp.ReceptionReport {
|
||||
@@ -1747,3 +1769,90 @@ func AggregateRTPDeltaInfo(deltaInfoList []*RTPDeltaInfo) *RTPDeltaInfo {
|
||||
Firs: firs,
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type PIDController struct {
|
||||
kp, ki, kd float64
|
||||
|
||||
tau float64 // low pass filter of D, time constant
|
||||
|
||||
outMin, outMax float64
|
||||
isOutLimitsSet bool
|
||||
|
||||
iMin, iMax float64
|
||||
isILimitsSet bool
|
||||
|
||||
iVal, dVal float64
|
||||
|
||||
prevError, prevMeasurement float64
|
||||
prevMeasurementTime time.Time
|
||||
}
|
||||
|
||||
func NewPIDController() *PIDController {
|
||||
return &PIDController{}
|
||||
}
|
||||
|
||||
func (p *PIDController) SetGains(kp, ki, kd float64) {
|
||||
p.kp = kp
|
||||
p.ki = ki
|
||||
p.kd = kd
|
||||
}
|
||||
|
||||
func (p *PIDController) SetDerivativeLPF(tau float64) {
|
||||
p.tau = tau
|
||||
}
|
||||
|
||||
func (p *PIDController) SetOutputLimits(min, max float64) {
|
||||
p.outMin = min
|
||||
p.outMax = max
|
||||
p.isOutLimitsSet = true
|
||||
}
|
||||
|
||||
func (p *PIDController) SetIntegralLimits(min, max float64) {
|
||||
p.iMin = min
|
||||
p.iMax = max
|
||||
p.isILimitsSet = true
|
||||
}
|
||||
|
||||
func (p *PIDController) Update(setpoint, measurement float64, at time.Time) float64 {
|
||||
diff := setpoint - measurement
|
||||
if p.prevMeasurementTime.IsZero() {
|
||||
p.prevError = diff
|
||||
p.prevMeasurement = measurement
|
||||
p.prevMeasurementTime = at
|
||||
return 0
|
||||
}
|
||||
|
||||
proportional := p.kp * diff
|
||||
|
||||
duration := at.Sub(p.prevMeasurementTime).Seconds()
|
||||
p.iVal = p.iVal + (0.5 * p.ki * duration * (diff + p.prevError))
|
||||
if p.isILimitsSet {
|
||||
if p.iVal > p.iMax {
|
||||
p.iVal = p.iMax
|
||||
}
|
||||
if p.iVal < p.iMin {
|
||||
p.iVal = p.iMin
|
||||
}
|
||||
}
|
||||
|
||||
p.dVal = (-2.0 * p.kd * (measurement - p.prevMeasurement)) + (((2.0*p.tau - duration) * p.dVal) / (2.0*p.tau + duration))
|
||||
|
||||
output := proportional + p.iVal + p.dVal
|
||||
if p.isOutLimitsSet {
|
||||
if output > p.outMax {
|
||||
output = p.outMax
|
||||
}
|
||||
if output < p.outMin {
|
||||
output = p.outMin
|
||||
}
|
||||
}
|
||||
|
||||
p.prevError = diff
|
||||
p.prevMeasurement = measurement
|
||||
p.prevMeasurementTime = at
|
||||
return output
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
Reference in new issue
Block a user