Files
livekit/pkg/sfu/forwarder.go
T

1696 lines
48 KiB
Go

package sfu
import (
"fmt"
"math"
"strings"
"sync"
"time"
"github.com/pion/webrtc/v3"
"github.com/livekit/protocol/logger"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
dd "github.com/livekit/livekit-server/pkg/sfu/dependencydescriptor"
)
// Forwarder
const (
FlagPauseOnDowngrade = true
FlagFilterRTX = true
TransitionCostSpatial = 10
ParkedLayersWaitDuration = 2 * time.Second
)
// -------------------------------------------------------------------
type VideoPauseReason int
const (
VideoPauseReasonNone VideoPauseReason = iota
VideoPauseReasonMuted
VideoPauseReasonPubMuted
VideoPauseReasonFeedDry
VideoPauseReasonBandwidth
)
func (v VideoPauseReason) String() string {
switch v {
case VideoPauseReasonNone:
return "NONE"
case VideoPauseReasonMuted:
return "MUTED"
case VideoPauseReasonPubMuted:
return "PUB_MUTED"
case VideoPauseReasonFeedDry:
return "FEED_DRY"
case VideoPauseReasonBandwidth:
return "BANDWIDTH"
default:
return fmt.Sprintf("%d", int(v))
}
}
// -------------------------------------------------------------------
type VideoAllocation struct {
pauseReason VideoPauseReason
isDeficient bool
bandwidthRequested int64
bandwidthDelta int64
bitrates Bitrates
targetLayers VideoLayers
maxLayers VideoLayers
distanceToDesired int32
}
func (v VideoAllocation) String() string {
return fmt.Sprintf("VideoAllocation{pause: %s, def: %+v, bw: %d, del: %d, rates: %+v, target: %s, max: %s, dist: %d}",
v.pauseReason, v.isDeficient, v.bandwidthRequested, v.bandwidthDelta, v.bitrates, v.targetLayers, v.maxLayers, v.distanceToDesired)
}
var (
VideoAllocationDefault = VideoAllocation{
pauseReason: VideoPauseReasonFeedDry, // start with no feed till feed is seen
targetLayers: InvalidLayers,
maxLayers: InvalidLayers,
}
)
// -------------------------------------------------------------------
type VideoAllocationProvisional struct {
muted bool
pubMuted bool
bitrates Bitrates
maxLayers VideoLayers
currentLayers VideoLayers
parkedLayers VideoLayers
allocatedLayers VideoLayers
}
// -------------------------------------------------------------------
type VideoTransition struct {
from VideoLayers
to VideoLayers
bandwidthDelta int64
}
func (v VideoTransition) String() string {
return fmt.Sprintf("VideoTransition{from: %s, to: %s, del: %d}", v.from, v.to, v.bandwidthDelta)
}
// -------------------------------------------------------------------
type TranslationParams struct {
shouldDrop bool
isDroppingRelevant bool
isSwitchingToMaxLayer bool
rtp *TranslationParamsRTP
vp8 *TranslationParamsVP8
ddExtension *dd.DependencyDescriptorExtension
marker bool
// indicates this frame has 'Switch' decode indication for target layer
// TODO : in theory, we need check frame chain is not broken for the target
// but we don't have frame queue now, so just use decode target indication
isSwitchingToTargetLayer bool
}
// -------------------------------------------------------------------
type VideoLayers = buffer.VideoLayer
const (
InvalidLayerSpatial = buffer.InvalidLayerSpatial
InvalidLayerTemporal = buffer.InvalidLayerTemporal
DefaultMaxLayerSpatial = buffer.DefaultMaxLayerSpatial
DefaultMaxLayerTemporal = buffer.DefaultMaxLayerTemporal
)
var (
InvalidLayers = buffer.InvalidLayers
DefaultMaxLayers = VideoLayers{
Spatial: DefaultMaxLayerSpatial,
Temporal: DefaultMaxLayerTemporal,
}
)
// -------------------------------------------------------------------
type ForwarderState struct {
Started bool
RTP RTPMungerState
VP8 VP8MungerState
}
func (f ForwarderState) String() string {
return fmt.Sprintf("ForwarderState{started: %v, rtp: %s, vp8: %s}", f.Started, f.RTP.String(), f.VP8.String())
}
// -------------------------------------------------------------------
type Forwarder struct {
lock sync.RWMutex
codec webrtc.RTPCodecCapability
kind webrtc.RTPCodecType
logger logger.Logger
getReferenceLayerRTPTimestamp func(ts uint32, layer int32, referenceLayer int32) (uint32, error)
numAdvertisedLayers int32
muted bool
pubMuted bool
started bool
lastSSRC uint32
referenceLayerSpatial int32
maxLayers VideoLayers
currentLayers VideoLayers
targetLayers VideoLayers
parkedLayers VideoLayers // layers that can resume without key frame
parkedLayersTimer *time.Timer
provisional *VideoAllocationProvisional
lastAllocation VideoAllocation
rtpMunger *RTPMunger
vp8Munger *VP8Munger
isTemporalSupported bool
ddLayerSelector *DDVideoLayerSelector
onParkedLayersExpired func()
}
func NewForwarder(
kind webrtc.RTPCodecType,
logger logger.Logger,
getReferenceLayerRTPTimestamp func(ts uint32, layer int32, referenceLayer int32) (uint32, error),
) *Forwarder {
f := &Forwarder{
kind: kind,
logger: logger,
getReferenceLayerRTPTimestamp: getReferenceLayerRTPTimestamp,
referenceLayerSpatial: InvalidLayerSpatial,
// start off with nothing, let streamallocator/opportunistic forwarder set the target
currentLayers: InvalidLayers,
targetLayers: InvalidLayers,
parkedLayers: InvalidLayers,
lastAllocation: VideoAllocationDefault,
rtpMunger: NewRTPMunger(logger),
}
if f.kind == webrtc.RTPCodecTypeVideo {
f.maxLayers = VideoLayers{Spatial: InvalidLayerSpatial, Temporal: DefaultMaxLayerTemporal}
} else {
f.maxLayers = InvalidLayers
}
return f
}
func (f *Forwarder) SetNumAdvertisedLayers(numAdvertisedLayers int32) {
f.lock.Lock()
defer f.lock.Unlock()
f.numAdvertisedLayers = numAdvertisedLayers
}
func (f *Forwarder) OnParkedLayersExpired(fn func()) {
f.lock.Lock()
defer f.lock.Unlock()
f.onParkedLayersExpired = fn
}
func (f *Forwarder) getOnParkedLayersExpired() func() {
f.lock.RLock()
defer f.lock.RUnlock()
return f.onParkedLayersExpired
}
func (f *Forwarder) DetermineCodec(codec webrtc.RTPCodecCapability) {
f.lock.Lock()
defer f.lock.Unlock()
if f.codec.MimeType != "" {
return
}
f.codec = codec
switch strings.ToLower(codec.MimeType) {
case "video/vp8":
f.isTemporalSupported = true
f.vp8Munger = NewVP8Munger(f.logger)
case "video/av1":
// TODO : we only enable dd layer selector for av1 now, at future we can
// enable it for vp8 too
f.ddLayerSelector = NewDDVideoLayerSelector(f.logger)
}
}
func (f *Forwarder) GetState() ForwarderState {
f.lock.RLock()
defer f.lock.RUnlock()
if !f.started {
return ForwarderState{}
}
state := ForwarderState{
Started: f.started,
RTP: f.rtpMunger.GetLast(),
}
if f.vp8Munger != nil {
state.VP8 = f.vp8Munger.GetLast()
}
return state
}
func (f *Forwarder) SeedState(state ForwarderState) {
if !state.Started {
return
}
f.lock.Lock()
defer f.lock.Unlock()
f.rtpMunger.SeedLast(state.RTP)
if f.vp8Munger != nil {
f.vp8Munger.SeedLast(state.VP8)
}
f.started = true
}
func (f *Forwarder) Mute(muted bool) (bool, VideoLayers) {
f.lock.Lock()
defer f.lock.Unlock()
if f.muted == muted {
return false, f.maxLayers
}
f.logger.Debugw("setting forwarder mute", "muted", muted)
f.muted = muted
// resync when muted so that sequence numbers do not jump on unmute
if muted {
f.resyncLocked()
}
return true, f.maxLayers
}
func (f *Forwarder) IsMuted() bool {
f.lock.RLock()
defer f.lock.RUnlock()
return f.muted
}
func (f *Forwarder) PubMute(pubMuted bool) (bool, VideoLayers) {
f.lock.Lock()
defer f.lock.Unlock()
if f.pubMuted == pubMuted {
return false, f.maxLayers
}
f.logger.Debugw("setting forwarder pub mute", "pubMuted", pubMuted)
f.pubMuted = pubMuted
// Do not resync on publisher mute as forwarding can continue on unmute using same layers.
// On unmute, park current layers as streaming can continue without a key frame when publisher starts the stream.
if !pubMuted && f.targetLayers.IsValid() && f.currentLayers.Spatial == f.targetLayers.Spatial {
f.setupParkedLayers(f.targetLayers)
f.currentLayers = InvalidLayers
}
return true, f.maxLayers
}
func (f *Forwarder) IsPubMuted() bool {
f.lock.RLock()
defer f.lock.RUnlock()
return f.pubMuted
}
func (f *Forwarder) IsAnyMuted() bool {
f.lock.RLock()
defer f.lock.RUnlock()
return f.muted || f.pubMuted
}
func (f *Forwarder) SetMaxSpatialLayer(spatialLayer int32) (bool, VideoLayers, VideoLayers) {
f.lock.Lock()
defer f.lock.Unlock()
if f.kind == webrtc.RTPCodecTypeAudio || spatialLayer == f.maxLayers.Spatial {
return false, f.maxLayers, f.currentLayers
}
f.logger.Infow("setting max spatial layer", "layer", spatialLayer)
f.maxLayers.Spatial = spatialLayer
f.clearParkedLayers()
return true, f.maxLayers, f.currentLayers
}
func (f *Forwarder) SetMaxTemporalLayer(temporalLayer int32) (bool, VideoLayers, VideoLayers) {
f.lock.Lock()
defer f.lock.Unlock()
if f.kind == webrtc.RTPCodecTypeAudio || temporalLayer == f.maxLayers.Temporal {
return false, f.maxLayers, f.currentLayers
}
f.logger.Infow("setting max temporal layer", "layer", temporalLayer)
f.maxLayers.Temporal = temporalLayer
f.clearParkedLayers()
return true, f.maxLayers, f.currentLayers
}
func (f *Forwarder) MaxLayers() VideoLayers {
f.lock.RLock()
defer f.lock.RUnlock()
return f.maxLayers
}
func (f *Forwarder) CurrentLayers() VideoLayers {
f.lock.RLock()
defer f.lock.RUnlock()
return f.currentLayers
}
func (f *Forwarder) TargetLayers() VideoLayers {
f.lock.RLock()
defer f.lock.RUnlock()
return f.targetLayers
}
func (f *Forwarder) GetReferenceLayerSpatial() int32 {
f.lock.RLock()
defer f.lock.RUnlock()
return f.referenceLayerSpatial
}
func (f *Forwarder) IsReducedQuality() (int32, bool) {
f.lock.RLock()
defer f.lock.RUnlock()
if f.muted || f.pubMuted || f.lastAllocation.pauseReason == VideoPauseReasonFeedDry || f.targetLayers.Spatial == InvalidLayerSpatial {
return 0, false
}
if f.currentLayers.Spatial != f.targetLayers.Spatial {
//
// Waiting for layer lock, do not declare reduced quality.
// Note the target might actually be a lower layer than current.
//
return 0, false
}
distance := f.maxLayers.Spatial - f.currentLayers.Spatial
if distance < 0 {
distance = 0
}
return distance, f.isDeficientLocked()
}
func (f *Forwarder) getOptimalBandwidthNeeded(brs Bitrates, maxLayers VideoLayers) int64 {
if f.muted || f.pubMuted {
return 0
}
for i := maxLayers.Spatial; i >= 0; i-- {
for j := maxLayers.Temporal; j >= 0; j-- {
if brs[i][j] == 0 {
continue
}
return brs[i][j]
}
}
// could be 0 due to either
// 1. publisher has stopped all layers ==> feed dry.
// 2. stream tracker has declared all layers stopped, functionally same as above.
// But, listed differently as this could be a mis-detection.
// 3. Bitrate measurement is pending.
return 0
}
func (f *Forwarder) getLayerChanges(brs Bitrates) (addedLayers []int32, removedLayers []int32) {
lastAllocationLayers := f.lastAllocation.bitrates.GetLayers()
nowLayers := brs.GetLayers()
for _, ln := range nowLayers {
found := false
for _, lla := range lastAllocationLayers {
if lla == ln {
found = true
break
}
}
if !found {
addedLayers = append(addedLayers, ln)
}
}
for _, lla := range lastAllocationLayers {
found := false
for _, ln := range nowLayers {
if ln == lla {
found = true
break
}
}
if !found {
removedLayers = append(removedLayers, lla)
}
}
return
}
func (f *Forwarder) getDistanceToDesired(brs Bitrates, targetLayers VideoLayers, maxLayers VideoLayers) int32 {
if f.muted || f.pubMuted {
return 0
}
found := false
distance := int32(0)
for s := maxLayers.Spatial; s >= 0; s-- {
for t := maxLayers.Temporal; t >= 0; t-- {
if brs[s][t] == 0 {
continue
}
if s == targetLayers.Spatial && t == targetLayers.Temporal {
found = true
break
}
distance++
}
if found {
break
}
}
// maybe overshooting
if !found && targetLayers.IsValid() {
distance = 0
for s := targetLayers.Spatial; s > f.maxLayers.Spatial; s-- {
for t := f.maxLayers.Temporal; t >= 0; t-- {
if targetLayers.Temporal < t || brs[s][t] == 0 {
continue
}
distance--
}
}
}
return distance
}
func (f *Forwarder) isDeficientLocked() bool {
return f.lastAllocation.isDeficient
}
func (f *Forwarder) IsDeficient() bool {
f.lock.RLock()
defer f.lock.RUnlock()
return f.isDeficientLocked()
}
func (f *Forwarder) BandwidthRequested(brs Bitrates) int64 {
f.lock.RLock()
defer f.lock.RUnlock()
if !f.targetLayers.IsValid() {
if f.targetLayers != InvalidLayers {
f.logger.Warnw(
"unexpected target layers", nil,
"target", f.targetLayers,
"current", f.currentLayers,
"parked", f.parkedLayers,
"max", f.maxLayers,
"lastAllocation", f.lastAllocation,
)
}
return 0
}
return brs[f.targetLayers.Spatial][f.targetLayers.Temporal]
}
func (f *Forwarder) DistanceToDesired() int32 {
f.lock.RLock()
defer f.lock.RUnlock()
return f.lastAllocation.distanceToDesired
}
func (f *Forwarder) AllocateOptimal(brs Bitrates, allowOvershoot bool) VideoAllocation {
f.lock.Lock()
defer f.lock.Unlock()
if f.kind == webrtc.RTPCodecTypeAudio {
return f.lastAllocation
}
alloc := VideoAllocation{
pauseReason: VideoPauseReasonNone,
bitrates: brs,
targetLayers: InvalidLayers,
maxLayers: f.maxLayers,
}
if f.getOptimalBandwidthNeeded(brs, f.maxLayers) == 0 {
alloc.pauseReason = VideoPauseReasonFeedDry
}
switch {
case f.muted:
alloc.pauseReason = VideoPauseReasonMuted
case f.pubMuted:
alloc.pauseReason = VideoPauseReasonPubMuted
// leave it at current layers for opportunistic resume
alloc.targetLayers = f.currentLayers
case f.parkedLayers.IsValid():
// if parked on a layer, let it continue
alloc.targetLayers = f.parkedLayers
case !f.targetLayers.IsValid():
if f.maxLayers.IsValid() {
if allowOvershoot {
alloc.targetLayers = VideoLayers{
Spatial: int32(math.Max(0, float64(f.numAdvertisedLayers-1))),
Temporal: DefaultMaxLayerTemporal,
}
} else {
alloc.targetLayers = f.maxLayers
}
}
case f.lastAllocation.maxLayers != f.maxLayers:
alloc.targetLayers = f.maxLayers
default:
doAlloc := false
added, removed := f.getLayerChanges(brs)
// check for an added higher than current target
for _, l := range added {
if l > f.targetLayers.Spatial {
doAlloc = true
break
}
}
// check for current target being removed
if !doAlloc {
for _, l := range removed {
if l == f.targetLayers.Spatial {
doAlloc = true
break
}
}
}
if !doAlloc {
// no layer changes, leave target as is
alloc.targetLayers = f.targetLayers
alloc.bandwidthRequested = brs[alloc.targetLayers.Spatial][alloc.targetLayers.Temporal]
} else {
// allocate best layer available
for s := f.maxLayers.Spatial; s >= 0; s-- {
for t := f.maxLayers.Temporal; t >= 0; t-- {
if brs[s][t] == 0 {
continue
}
alloc.targetLayers = VideoLayers{
Spatial: s,
Temporal: t,
}
alloc.bandwidthRequested = brs[s][t]
break
}
if alloc.bandwidthRequested != 0 {
break
}
}
if alloc.bandwidthRequested == 0 && f.maxLayers.IsValid() && allowOvershoot {
// if we cannot allocate anything below max layer,
// look for a layer above. It is okay to overshoot
// in optimal allocation (i.e. no bandwidth restrictions).
// It is possible that clients send only a higher layer.
// To accommodate cases like that, try finding a layer
// above the requested maximum to ensure streaming
for s := f.maxLayers.Spatial + 1; s <= DefaultMaxLayerSpatial; s++ {
for t := int32(0); t <= DefaultMaxLayerTemporal; t++ {
if brs[s][t] == 0 {
continue
}
alloc.targetLayers = VideoLayers{
Spatial: s,
Temporal: t,
}
alloc.bandwidthRequested = brs[s][t]
alloc.pauseReason = VideoPauseReasonNone
f.logger.Infow("allowing overshoot", "maxLayer", f.maxLayers, "targetLayers", alloc.targetLayers)
break
}
if alloc.bandwidthRequested != 0 {
break
}
}
}
// feed may be dry, leave target at current if already started for opportunistic resume
if alloc.bandwidthRequested == 0 && f.maxLayers.IsValid() {
if f.started && f.currentLayers.IsValid() {
alloc.targetLayers = f.currentLayers
} else {
// opportunisitically latch on to anything
alloc.targetLayers = VideoLayers{
Spatial: int32(math.Max(0, float64(f.numAdvertisedLayers-1))),
Temporal: DefaultMaxLayerTemporal,
}
}
}
}
}
if !alloc.targetLayers.IsValid() {
alloc.targetLayers = InvalidLayers
}
alloc.bandwidthDelta = alloc.bandwidthRequested - f.lastAllocation.bandwidthRequested
alloc.distanceToDesired = f.getDistanceToDesired(brs, alloc.targetLayers, f.maxLayers)
return f.updateAllocation(alloc, "optimal")
}
func (f *Forwarder) ProvisionalAllocatePrepare(bitrates Bitrates) {
f.lock.Lock()
defer f.lock.Unlock()
f.provisional = &VideoAllocationProvisional{
allocatedLayers: InvalidLayers,
muted: f.muted,
pubMuted: f.pubMuted,
bitrates: bitrates,
maxLayers: f.maxLayers,
currentLayers: f.currentLayers,
parkedLayers: f.parkedLayers,
}
}
func (f *Forwarder) ProvisionalAllocate(availableChannelCapacity int64, layers VideoLayers, allowPause bool, allowOvershoot bool) int64 {
f.lock.Lock()
defer f.lock.Unlock()
if f.provisional.muted || f.provisional.pubMuted || !f.provisional.maxLayers.IsValid() || (!allowOvershoot && layers.GreaterThan(f.provisional.maxLayers)) {
return 0
}
requiredBitrate := f.provisional.bitrates[layers.Spatial][layers.Temporal]
if requiredBitrate == 0 {
return 0
}
alreadyAllocatedBitrate := int64(0)
if f.provisional.allocatedLayers.IsValid() {
alreadyAllocatedBitrate = f.provisional.bitrates[f.provisional.allocatedLayers.Spatial][f.provisional.allocatedLayers.Temporal]
}
// a layer under maximum fits, take it
if !layers.GreaterThan(f.provisional.maxLayers) && requiredBitrate <= (availableChannelCapacity+alreadyAllocatedBitrate) {
f.provisional.allocatedLayers = layers
return requiredBitrate - alreadyAllocatedBitrate
}
//
// Given layer does not fit. But overshoot is allowed.
// Could be one of
// 1. a layer below maximum that does not fit
// 2. a layer above maximum which may or may not fit.
// In any of those cases, take the lowest possible layer if pause is not allowed
//
if !allowPause && (!f.provisional.allocatedLayers.IsValid() || !layers.GreaterThan(f.provisional.allocatedLayers)) {
f.provisional.allocatedLayers = layers
return requiredBitrate - alreadyAllocatedBitrate
}
return 0
}
func (f *Forwarder) ProvisionalAllocateGetCooperativeTransition(allowOvershoot bool) VideoTransition {
//
// This is called when a track needs a change (could be mute/unmute, subscribed layers changed, published layers changed)
// when channel is congested.
//
// The goal is to provide a co-operative transition. Co-operative stream allocation aims to keep all the streams active
// as much as possible.
//
// When channel is congested, effecting a transition which will consume more bits will lead to more congestion.
// So, this routine does the following
// 1. When muting, it is not going to increase consumption.
// 2. If the stream is currently active and the transition needs more bits (higher layers = more bits), do not make the up move.
// The higher layer requirement could be due to a new published layer becoming available or subscribed layers changing.
// 3. If the new target layers are lower than current target, take the move down and save bits.
// 4. If not currently streaming, find the minimum layers that can unpause the stream.
//
// To summarize, co-operative streaming means
// - Try to keep tracks streaming, i.e. no pauses at the expense of some streams not being at optimal layers
// - Do not make an upgrade as it could affect other tracks
//
f.lock.Lock()
defer f.lock.Unlock()
if f.provisional.muted || f.provisional.pubMuted {
f.provisional.allocatedLayers = InvalidLayers
if f.provisional.pubMuted {
// leave it at current for opportunistic forwarding, there is still bandwidth saving with publisher mute
f.provisional.allocatedLayers = f.provisional.currentLayers
}
return VideoTransition{
from: f.targetLayers,
to: f.provisional.allocatedLayers,
bandwidthDelta: 0 - f.lastAllocation.bandwidthRequested,
}
}
// check if we should preserve current target
if f.targetLayers.IsValid() {
// what is the highest that is available
maximalLayers := InvalidLayers
maximalBandwidthRequired := int64(0)
for s := f.provisional.maxLayers.Spatial; s >= 0; s-- {
for t := f.provisional.maxLayers.Temporal; t >= 0; t-- {
if f.provisional.bitrates[s][t] != 0 {
maximalLayers = VideoLayers{Spatial: s, Temporal: t}
maximalBandwidthRequired = f.provisional.bitrates[s][t]
break
}
}
if maximalBandwidthRequired != 0 {
break
}
}
if maximalLayers.IsValid() {
if !f.targetLayers.GreaterThan(maximalLayers) && f.provisional.bitrates[f.targetLayers.Spatial][f.targetLayers.Temporal] != 0 {
// currently streaming and maybe wanting an upgrade (f.targetLayers <= maximalLayers),
// just preserve current target in the cooperative scheme of things
f.provisional.allocatedLayers = f.targetLayers
return VideoTransition{
from: f.targetLayers,
to: f.targetLayers,
bandwidthDelta: 0,
}
}
if f.targetLayers.GreaterThan(maximalLayers) {
// maximalLayers < f.targetLayers, make the down move
f.provisional.allocatedLayers = maximalLayers
return VideoTransition{
from: f.targetLayers,
to: maximalLayers,
bandwidthDelta: maximalBandwidthRequired - f.lastAllocation.bandwidthRequested,
}
}
}
}
findNextLayer := func(
minSpatial, maxSpatial int32,
minTemporal, maxTemporal int32,
) (VideoLayers, int64) {
layers := InvalidLayers
bw := int64(0)
for s := minSpatial; s <= maxSpatial; s++ {
for t := minTemporal; t <= maxTemporal; t++ {
if f.provisional.bitrates[s][t] != 0 {
layers = VideoLayers{Spatial: s, Temporal: t}
bw = f.provisional.bitrates[s][t]
break
}
}
if bw != 0 {
break
}
}
return layers, bw
}
targetLayers := f.targetLayers
bandwidthRequired := int64(0)
if !targetLayers.IsValid() {
// currently not streaming, find minimal
// NOTE: a layer in feed could have paused and there could be other options than going back to minimal,
// but the cooperative scheme knocks things back to minimal
targetLayers, bandwidthRequired = findNextLayer(
0, f.provisional.maxLayers.Spatial,
0, f.provisional.maxLayers.Temporal,
)
// could not find a minimal layer, overshoot if allowed
if bandwidthRequired == 0 && f.provisional.maxLayers.IsValid() && allowOvershoot {
targetLayers, bandwidthRequired = findNextLayer(
f.provisional.maxLayers.Spatial+1, DefaultMaxLayerSpatial,
0, DefaultMaxLayerTemporal,
)
}
}
// if nothing available, just leave target at current to enable opportunistic forwarding in case current resumes
if !targetLayers.IsValid() {
if f.provisional.parkedLayers.IsValid() {
targetLayers = f.provisional.parkedLayers
} else {
targetLayers = f.provisional.currentLayers
}
}
f.provisional.allocatedLayers = targetLayers
return VideoTransition{
from: f.targetLayers,
to: targetLayers,
bandwidthDelta: bandwidthRequired - f.lastAllocation.bandwidthRequested,
}
}
func (f *Forwarder) ProvisionalAllocateGetBestWeightedTransition() VideoTransition {
//
// This is called when a track needs a change (could be mute/unmute, subscribed layers changed, published layers changed)
// when channel is congested. This is called on tracks other than the one needing the change. When the track
// needing the change requires bits, this is called to check if this track can contribute some bits to the pool.
//
// The goal is to keep all tracks streaming as much as possible. So, the track that needs a change needs bandwidth to be unpaused.
//
// This tries to figure out how much this track can contribute back to the pool to enable the track that needs to be unpaused.
// 1. Track muted OR feed dry - can contribute everything back in case it was using bandwidth.
// 2. Look at all possible down transitions from current target and find the best offer.
// Best offer is calculated as bandwidth saved moving to a down layer divided by cost.
// Cost has two components
// a. Transition cost: Spatial layer switch is expensive due to key frame requirement, but temporal layer switch is free.
// b. Quality cost: The farther away from desired layers, the higher the quality cost.
//
f.lock.Lock()
defer f.lock.Unlock()
if f.provisional.muted || f.provisional.pubMuted {
f.provisional.allocatedLayers = InvalidLayers
if f.provisional.pubMuted {
// leave it at current for opportunistic forwarding, there is still bandwidth saving with publisher mute
f.provisional.allocatedLayers = f.provisional.currentLayers
}
return VideoTransition{
from: f.targetLayers,
to: f.provisional.allocatedLayers,
bandwidthDelta: 0 - f.lastAllocation.bandwidthRequested,
}
}
maxReachableLayerTemporal := InvalidLayerTemporal
for t := f.provisional.maxLayers.Temporal; t >= 0; t-- {
for s := f.provisional.maxLayers.Spatial; s >= 0; s-- {
if f.provisional.bitrates[s][t] != 0 {
maxReachableLayerTemporal = t
break
}
}
if maxReachableLayerTemporal != InvalidLayerTemporal {
break
}
}
if maxReachableLayerTemporal == InvalidLayerTemporal {
// feed has gone dry, just leave target at current to enable opportunistic forwarding in case current resumes.
// Note that this is giving back bits and opportunistic forwarding resuming might trigger congestion again,
// but that should be handled by stream allocator.
if f.provisional.parkedLayers.IsValid() {
f.provisional.allocatedLayers = f.provisional.parkedLayers
} else {
f.provisional.allocatedLayers = f.provisional.currentLayers
}
return VideoTransition{
from: f.targetLayers,
to: f.provisional.allocatedLayers,
bandwidthDelta: 0 - f.lastAllocation.bandwidthRequested,
}
}
// starting from minimum to target, find transition which gives the best
// transition taking into account bits saved vs cost of such a transition
bestLayers := InvalidLayers
bestBandwidthDelta := int64(0)
bestValue := float32(0)
for s := int32(0); s <= f.targetLayers.Spatial; s++ {
for t := int32(0); t <= f.targetLayers.Temporal; t++ {
if s == f.targetLayers.Spatial && t == f.targetLayers.Temporal {
break
}
bandwidthDelta := int64(math.Max(float64(0), float64(f.lastAllocation.bandwidthRequested-f.provisional.bitrates[s][t])))
transitionCost := int32(0)
if f.targetLayers.Spatial != s {
transitionCost = TransitionCostSpatial
}
qualityCost := (maxReachableLayerTemporal+1)*(f.targetLayers.Spatial-s) + (f.targetLayers.Temporal - t)
value := float32(0)
if (transitionCost + qualityCost) != 0 {
value = float32(bandwidthDelta) / float32(transitionCost+qualityCost)
}
if value > bestValue || (value == bestValue && bandwidthDelta > bestBandwidthDelta) {
bestValue = value
bestBandwidthDelta = bandwidthDelta
bestLayers = VideoLayers{Spatial: s, Temporal: t}
}
}
}
f.provisional.allocatedLayers = bestLayers
return VideoTransition{
from: f.targetLayers,
to: bestLayers,
bandwidthDelta: bestBandwidthDelta,
}
}
func (f *Forwarder) ProvisionalAllocateCommit() VideoAllocation {
f.lock.Lock()
defer f.lock.Unlock()
alloc := VideoAllocation{
bandwidthRequested: 0,
bandwidthDelta: -f.lastAllocation.bandwidthRequested,
bitrates: f.provisional.bitrates,
targetLayers: f.provisional.allocatedLayers,
maxLayers: f.provisional.maxLayers,
distanceToDesired: f.getDistanceToDesired(f.provisional.bitrates, f.provisional.allocatedLayers, f.provisional.maxLayers),
}
switch {
case f.provisional.muted:
alloc.pauseReason = VideoPauseReasonMuted
case f.provisional.pubMuted:
alloc.pauseReason = VideoPauseReasonPubMuted
case f.getOptimalBandwidthNeeded(f.provisional.bitrates, f.provisional.maxLayers) == 0:
if f.provisional.allocatedLayers.IsValid() {
// overshoot
alloc.bandwidthRequested = f.provisional.bitrates[f.provisional.allocatedLayers.Spatial][f.provisional.allocatedLayers.Temporal]
alloc.bandwidthDelta = alloc.bandwidthRequested - f.lastAllocation.bandwidthRequested
} else {
alloc.pauseReason = VideoPauseReasonFeedDry
// leave target at current for opportunistic forwarding
if f.provisional.currentLayers.IsValid() && f.provisional.currentLayers.Spatial <= f.provisional.maxLayers.Spatial {
f.provisional.allocatedLayers = f.provisional.currentLayers
alloc.targetLayers = f.provisional.allocatedLayers
}
}
default:
if f.provisional.allocatedLayers.IsValid() {
alloc.bandwidthRequested = f.provisional.bitrates[f.provisional.allocatedLayers.Spatial][f.provisional.allocatedLayers.Temporal]
}
alloc.bandwidthDelta = alloc.bandwidthRequested - f.lastAllocation.bandwidthRequested
if f.provisional.allocatedLayers.GreaterThan(f.provisional.maxLayers) ||
alloc.bandwidthRequested >= f.getOptimalBandwidthNeeded(f.provisional.bitrates, f.provisional.maxLayers) {
// could be greater than optimal if overshooting
alloc.isDeficient = false
} else {
alloc.isDeficient = true
if !f.provisional.allocatedLayers.IsValid() {
alloc.pauseReason = VideoPauseReasonBandwidth
}
}
}
f.clearParkedLayers()
return f.updateAllocation(alloc, "cooperative")
}
func (f *Forwarder) AllocateNextHigher(availableChannelCapacity int64, brs Bitrates, allowOvershoot bool) (VideoAllocation, bool) {
f.lock.Lock()
defer f.lock.Unlock()
if f.kind == webrtc.RTPCodecTypeAudio {
return f.lastAllocation, false
}
// if not deficient, nothing to do
if !f.isDeficientLocked() {
return f.lastAllocation, false
}
// if targets are still pending, don't increase
if f.targetLayers.IsValid() && f.targetLayers != f.currentLayers {
return f.lastAllocation, false
}
optimalBandwidthNeeded := f.getOptimalBandwidthNeeded(brs, f.maxLayers)
alreadyAllocated := int64(0)
if f.targetLayers.IsValid() {
alreadyAllocated = brs[f.targetLayers.Spatial][f.targetLayers.Temporal]
}
doAllocation := func(
minSpatial, maxSpatial int32,
minTemporal, maxTemporal int32,
) (bool, VideoAllocation, bool) {
for s := minSpatial; s <= maxSpatial; s++ {
for t := minTemporal; t <= maxTemporal; t++ {
bandwidthRequested := brs[s][t]
if bandwidthRequested == 0 {
continue
}
if !allowOvershoot && bandwidthRequested-alreadyAllocated > availableChannelCapacity {
// next higher available layer does not fit, return
return true, f.lastAllocation, false
}
targetLayers := VideoLayers{Spatial: s, Temporal: t}
alloc := VideoAllocation{
isDeficient: true,
bandwidthRequested: bandwidthRequested,
bandwidthDelta: bandwidthRequested - alreadyAllocated,
bitrates: brs,
targetLayers: targetLayers,
maxLayers: f.maxLayers,
distanceToDesired: f.getDistanceToDesired(brs, targetLayers, f.maxLayers),
}
if targetLayers.GreaterThan(f.maxLayers) || bandwidthRequested >= optimalBandwidthNeeded {
alloc.isDeficient = false
}
return true, f.updateAllocation(alloc, "next-higher"), true
}
}
return false, VideoAllocation{}, false
}
done := false
var allocation VideoAllocation
boosted := false
// try moving temporal layer up in currently streaming spatial layer
if f.targetLayers.IsValid() {
done, allocation, boosted = doAllocation(
f.targetLayers.Spatial, f.targetLayers.Spatial,
f.targetLayers.Temporal+1, f.maxLayers.Temporal,
)
if done {
return allocation, boosted
}
}
// try moving spatial layer up if temporal layer move up is not available
done, allocation, boosted = doAllocation(
f.targetLayers.Spatial+1, f.maxLayers.Spatial,
0, f.maxLayers.Temporal,
)
if done {
return allocation, boosted
}
if allowOvershoot && f.maxLayers.IsValid() {
done, allocation, boosted = doAllocation(
f.maxLayers.Spatial+1, DefaultMaxLayerSpatial,
0, DefaultMaxLayerTemporal,
)
if done {
return allocation, boosted
}
}
return f.lastAllocation, false
}
func (f *Forwarder) GetNextHigherTransition(brs Bitrates, allowOvershoot bool) (VideoTransition, bool) {
f.lock.Lock()
defer f.lock.Unlock()
if f.kind == webrtc.RTPCodecTypeAudio {
return VideoTransition{}, false
}
// if not deficient, nothing to do
if !f.isDeficientLocked() {
return VideoTransition{}, false
}
// if targets are still pending, don't increase
if f.targetLayers.IsValid() && f.targetLayers != f.currentLayers {
return VideoTransition{}, false
}
alreadyAllocated := int64(0)
if f.targetLayers.IsValid() {
alreadyAllocated = brs[f.targetLayers.Spatial][f.targetLayers.Temporal]
}
findNextHigher := func(
minSpatial, maxSpatial int32,
minTemporal, maxTemporal int32,
) (bool, VideoTransition, bool) {
for s := minSpatial; s <= maxSpatial; s++ {
for t := minTemporal; t <= maxTemporal; t++ {
bandwidthRequested := brs[s][t]
if bandwidthRequested == 0 {
continue
}
transition := VideoTransition{
from: f.targetLayers,
to: VideoLayers{Spatial: s, Temporal: t},
bandwidthDelta: bandwidthRequested - alreadyAllocated,
}
return true, transition, true
}
}
return false, VideoTransition{}, false
}
done := false
var transition VideoTransition
isAvailable := false
// try moving temporal layer up in currently streaming spatial layer
if f.targetLayers.IsValid() {
done, transition, isAvailable = findNextHigher(
f.targetLayers.Spatial, f.targetLayers.Spatial,
f.targetLayers.Temporal+1, f.maxLayers.Temporal,
)
if done {
return transition, isAvailable
}
}
// try moving spatial layer up if temporal layer move up is not available
done, transition, isAvailable = findNextHigher(
f.targetLayers.Spatial+1, f.maxLayers.Spatial,
0, f.maxLayers.Temporal,
)
if done {
return transition, isAvailable
}
if allowOvershoot && f.maxLayers.IsValid() {
done, transition, isAvailable = findNextHigher(
f.maxLayers.Spatial+1, DefaultMaxLayerSpatial,
0, DefaultMaxLayerTemporal,
)
if done {
return transition, isAvailable
}
}
return VideoTransition{}, false
}
func (f *Forwarder) Pause(brs Bitrates) VideoAllocation {
f.lock.Lock()
defer f.lock.Unlock()
alloc := VideoAllocation{
bandwidthRequested: 0,
bandwidthDelta: 0 - f.lastAllocation.bandwidthRequested,
bitrates: brs,
targetLayers: InvalidLayers,
maxLayers: f.maxLayers,
distanceToDesired: f.getDistanceToDesired(brs, InvalidLayers, f.maxLayers),
}
switch {
case f.muted:
alloc.pauseReason = VideoPauseReasonMuted
case f.pubMuted:
alloc.pauseReason = VideoPauseReasonPubMuted
case f.getOptimalBandwidthNeeded(brs, f.maxLayers) == 0:
alloc.pauseReason = VideoPauseReasonFeedDry
default:
// pausing due to lack of bandwidth
alloc.isDeficient = true
alloc.pauseReason = VideoPauseReasonBandwidth
}
f.clearParkedLayers()
return f.updateAllocation(alloc, "pause")
}
func (f *Forwarder) updateAllocation(alloc VideoAllocation, reason string) VideoAllocation {
if alloc.isDeficient != f.lastAllocation.isDeficient ||
alloc.pauseReason != f.lastAllocation.pauseReason ||
alloc.targetLayers != f.lastAllocation.targetLayers {
f.logger.Infow(fmt.Sprintf("stream allocation: %s", reason), "allocation", alloc)
}
f.lastAllocation = alloc
f.setTargetLayers(f.lastAllocation.targetLayers)
if !f.targetLayers.IsValid() {
f.resyncLocked()
}
return f.lastAllocation
}
func (f *Forwarder) setTargetLayers(targetLayers VideoLayers) {
f.targetLayers = targetLayers
if f.ddLayerSelector != nil {
f.ddLayerSelector.SelectLayer(targetLayers)
}
}
func (f *Forwarder) Resync() {
f.lock.Lock()
defer f.lock.Unlock()
f.resyncLocked()
}
func (f *Forwarder) resyncLocked() {
f.currentLayers = InvalidLayers
f.lastSSRC = 0
f.clearParkedLayers()
}
func (f *Forwarder) clearParkedLayers() {
f.parkedLayers = InvalidLayers
if f.parkedLayersTimer != nil {
f.parkedLayersTimer.Stop()
f.parkedLayersTimer = nil
}
}
func (f *Forwarder) setupParkedLayers(parkedLayers VideoLayers) {
f.clearParkedLayers()
f.parkedLayers = parkedLayers
f.parkedLayersTimer = time.AfterFunc(ParkedLayersWaitDuration, func() {
f.lock.Lock()
f.clearParkedLayers()
f.lock.Unlock()
if onParkedLayersExpired := f.getOnParkedLayersExpired(); onParkedLayersExpired != nil {
onParkedLayersExpired()
}
})
}
func (f *Forwarder) CheckSync() (locked bool, layer int32) {
f.lock.RLock()
defer f.lock.RUnlock()
layer = f.targetLayers.Spatial
locked = f.targetLayers.Spatial == f.currentLayers.Spatial || f.parkedLayers.IsValid()
return
}
func (f *Forwarder) FilterRTX(nacks []uint16) (filtered []uint16, disallowedLayers [DefaultMaxLayerSpatial + 1]bool) {
if !FlagFilterRTX {
filtered = nacks
return
}
f.lock.RLock()
defer f.lock.RUnlock()
filtered = f.rtpMunger.FilterRTX(nacks)
//
// Curb RTX when deficient for two cases
// 1. Target layer is lower than current layer. When current hits target, a key frame should flush the decoder.
// 2. Requested layer is higher than current. Current layer's key frame should have flushed encoder.
// Remote might ask for older layer because of its jitter buffer, but let it starve as channel is already congested.
//
// Without the curb, when congestion hits, RTX rate could be so high that it further congests the channel.
//
for layer := int32(0); layer < DefaultMaxLayerSpatial+1; layer++ {
if f.isDeficientLocked() && (f.targetLayers.Spatial < f.currentLayers.Spatial || layer > f.currentLayers.Spatial) {
disallowedLayers[layer] = true
}
}
return
}
func (f *Forwarder) GetTranslationParams(extPkt *buffer.ExtPacket, layer int32) (*TranslationParams, error) {
f.lock.Lock()
defer f.lock.Unlock()
// Video: Do not drop on publisher mute to enable resume on publisher unmute without a key frame.
if f.muted {
return &TranslationParams{
shouldDrop: true,
}, nil
}
switch f.kind {
case webrtc.RTPCodecTypeAudio:
// Audio: Blank frames are injected on publisher mute to ensure decoder does not get stuck at a noise frame. So, do not forward.
if f.pubMuted {
return &TranslationParams{
shouldDrop: true,
}, nil
}
return f.getTranslationParamsAudio(extPkt, layer)
case webrtc.RTPCodecTypeVideo:
return f.getTranslationParamsVideo(extPkt, layer)
}
return nil, ErrUnknownKind
}
// should be called with lock held
func (f *Forwarder) getTranslationParamsCommon(extPkt *buffer.ExtPacket, layer int32, tp *TranslationParams) (*TranslationParams, error) {
if f.lastSSRC != extPkt.Packet.SSRC {
if !f.started {
f.started = true
f.referenceLayerSpatial = layer
f.rtpMunger.SetLastSnTs(extPkt)
if f.vp8Munger != nil {
f.vp8Munger.SetLast(extPkt)
}
} else {
if f.referenceLayerSpatial == InvalidLayerSpatial {
// on a resume, reference layer may not be set, so only set when it is invalid
f.referenceLayerSpatial = layer
}
// Compute how much time passed between the old RTP extPkt
// and the current packet, and fix timestamp on source change
td := uint32(1)
if f.getReferenceLayerRTPTimestamp != nil {
refTS, err := f.getReferenceLayerRTPTimestamp(extPkt.Packet.Timestamp, layer, f.referenceLayerSpatial)
if err == nil {
last := f.rtpMunger.GetLast()
td = refTS - last.LastTS
if td == 0 || td > (1<<31) {
f.logger.Infow("reference timestamp out-of-order, using default", "lastTS", last.LastTS, "refTS", refTS, "td", int32(td))
td = 1
}
}
} else {
f.logger.Infow("reference timestamp not available, using default")
}
f.rtpMunger.UpdateSnTsOffsets(extPkt, 1, td)
if f.vp8Munger != nil {
f.vp8Munger.UpdateOffsets(extPkt)
}
}
f.logger.Debugw("switching feed", "from", f.lastSSRC, "to", extPkt.Packet.SSRC)
f.lastSSRC = extPkt.Packet.SSRC
}
if tp == nil {
tp = &TranslationParams{}
}
tpRTP, err := f.rtpMunger.UpdateAndGetSnTs(extPkt)
if err != nil {
tp.shouldDrop = true
if err == ErrPaddingOnlyPacket || err == ErrDuplicatePacket || err == ErrOutOfOrderSequenceNumberCacheMiss {
if err == ErrOutOfOrderSequenceNumberCacheMiss {
tp.isDroppingRelevant = true
}
return tp, nil
}
tp.isDroppingRelevant = true
return tp, err
}
tp.rtp = tpRTP
return tp, nil
}
// should be called with lock held
func (f *Forwarder) getTranslationParamsAudio(extPkt *buffer.ExtPacket, layer int32) (*TranslationParams, error) {
return f.getTranslationParamsCommon(extPkt, layer, nil)
}
// should be called with lock held
func (f *Forwarder) getTranslationParamsVideo(extPkt *buffer.ExtPacket, layer int32) (*TranslationParams, error) {
tp := &TranslationParams{}
if !f.targetLayers.IsValid() {
// stream is paused by streamallocator
tp.shouldDrop = true
return tp, nil
}
if f.ddLayerSelector != nil {
if selected := f.ddLayerSelector.Select(extPkt, tp); !selected {
tp.shouldDrop = true
f.rtpMunger.PacketDropped(extPkt)
return tp, nil
} else if tp.isSwitchingToTargetLayer {
// lock to target layer
f.logger.Infow("locking to target layer", "current", f.currentLayers, "target", f.targetLayers, "feed", extPkt.Packet.SSRC)
f.currentLayers.Spatial = f.targetLayers.Spatial
if !f.isTemporalSupported {
f.currentLayers.Temporal = f.targetLayers.Temporal
}
// TODO : we switch to target layer immediately now since we assume all frame chain is integrity
// if we have frame chain check, should switch only if target chain is not broken and decodable
// if f.ddLayerSelector != nil {
// f.ddLayerSelector.SelectLayer(f.currentLayers)
// }
if f.currentLayers.Spatial >= f.maxLayers.Spatial || f.currentLayers.Spatial == (f.numAdvertisedLayers-1) {
tp.isSwitchingToMaxLayer = true
}
}
} else {
if f.currentLayers.Spatial != f.targetLayers.Spatial {
// Three things to check when not locked to target
// 1. Resumable layer - don't need a key frame
// 2. Opportunistic layer upgrade - needs a key frame
// 3. Need to downgrade - needs a key frame
found := false
if f.parkedLayers.IsValid() {
if f.parkedLayers.Spatial == layer {
f.logger.Infow("resuming at parked layer", "current", f.currentLayers, "target", f.targetLayers, "parked", f.parkedLayers, "feed", extPkt.Packet.SSRC)
f.currentLayers = f.parkedLayers
found = true
}
} else {
if extPkt.KeyFrame {
if layer > f.currentLayers.Spatial && layer <= f.targetLayers.Spatial {
f.logger.Infow("upgrading layer", "current", f.currentLayers, "target", f.targetLayers, "layer", layer, "feed", extPkt.Packet.SSRC)
found = true
}
if layer < f.currentLayers.Spatial && layer >= f.targetLayers.Spatial {
f.logger.Infow("downgrading layer", "current", f.currentLayers, "target", f.targetLayers, "layer", layer, "feed", extPkt.Packet.SSRC)
found = true
}
if found {
f.currentLayers.Spatial = layer
if !f.isTemporalSupported {
f.currentLayers.Temporal = extPkt.Temporal
}
}
}
}
if found {
tp.isSwitchingToTargetLayer = true
f.clearParkedLayers()
if f.currentLayers.Spatial >= f.maxLayers.Spatial || f.currentLayers.Spatial == (f.numAdvertisedLayers-1) {
tp.isSwitchingToMaxLayer = true
// if maximum is attained, adjust target to enable fast path layer check in per-packet path
f.logger.Infow(
"reached max layer",
"current", f.currentLayers,
"target", f.targetLayers,
"max", f.maxLayers,
"advertised", f.numAdvertisedLayers,
"feed", extPkt.Packet.SSRC,
)
f.targetLayers.Spatial = f.currentLayers.Spatial
}
}
} else {
// if locked to higher than max layer due to overshoot, check if it can be dialed back
if f.targetLayers.Spatial > f.maxLayers.Spatial {
if layer <= f.maxLayers.Spatial && extPkt.KeyFrame {
f.logger.Infow("adjusting overshoot", "current", f.currentLayers, "target", f.targetLayers, "layer", layer, "feed", extPkt.Packet.SSRC)
f.currentLayers.Spatial = layer
f.targetLayers.Spatial = layer
if f.currentLayers.Spatial >= f.maxLayers.Spatial || f.currentLayers.Spatial == (f.numAdvertisedLayers-1) {
tp.isSwitchingToMaxLayer = true
}
}
}
}
}
if f.currentLayers.Spatial != layer {
tp.shouldDrop = true
return tp, nil
}
if FlagPauseOnDowngrade && f.targetLayers.Spatial < f.currentLayers.Spatial && f.isDeficientLocked() {
//
// If target layer is lower than both the current and
// maximum subscribed layer, it is due to bandwidth
// constraints that the target layer has been switched down.
// Continuing to send higher layer will only exacerbate the
// situation by putting more stress on the channel. So, drop it.
//
// In the other direction, it is okay to keep forwarding till
// switch point to get a smoother stream till the higher
// layer key frame arrives.
//
// Note that it is possible for client subscription layer restriction
// to coincide with server restriction due to bandwidth limitation,
// In the case of subscription change, higher should continue streaming
// to ensure smooth transition.
//
// To differentiate between the two cases, drop only when in DEFICIENT state.
//
tp.shouldDrop = true
tp.isDroppingRelevant = true
return tp, nil
}
_, err := f.getTranslationParamsCommon(extPkt, layer, tp)
if tp.shouldDrop || f.vp8Munger == nil || len(extPkt.Packet.Payload) == 0 {
return tp, err
}
// catch up temporal layer if necessary
if f.currentLayers.Temporal != f.targetLayers.Temporal {
incomingVP8, ok := extPkt.Payload.(buffer.VP8)
if ok {
if incomingVP8.TIDPresent == 0 || incomingVP8.TID <= uint8(f.targetLayers.Temporal) {
f.currentLayers.Temporal = f.targetLayers.Temporal
}
}
}
tpVP8, err := f.vp8Munger.UpdateAndGet(extPkt, tp.rtp.snOrdering, f.currentLayers.Temporal)
if err != nil {
tp.rtp = nil
tp.shouldDrop = true
if err == ErrFilteredVP8TemporalLayer || err == ErrOutOfOrderVP8PictureIdCacheMiss {
if err == ErrFilteredVP8TemporalLayer {
// filtered temporal layer, update sequence number offset to prevent holes
f.rtpMunger.PacketDropped(extPkt)
}
if err == ErrOutOfOrderVP8PictureIdCacheMiss {
tp.isDroppingRelevant = true
}
return tp, nil
}
tp.isDroppingRelevant = true
return tp, err
}
tp.vp8 = tpVP8
return tp, nil
}
func (f *Forwarder) GetSnTsForPadding(num int) ([]SnTs, error) {
f.lock.Lock()
defer f.lock.Unlock()
// padding is used for probing. Padding packets should be
// at only the frame boundaries to ensure decoder sequencer does
// not get out-of-sync. But, when a stream is paused,
// force a frame marker as a restart of the stream will
// start with a key frame which will reset the decoder.
forceMarker := false
if !f.targetLayers.IsValid() {
forceMarker = true
}
return f.rtpMunger.UpdateAndGetPaddingSnTs(num, 0, 0, forceMarker)
}
func (f *Forwarder) GetSnTsForBlankFrames(frameRate uint32, numPackets int) ([]SnTs, bool, error) {
f.lock.Lock()
defer f.lock.Unlock()
frameEndNeeded := !f.rtpMunger.IsOnFrameBoundary()
if frameEndNeeded {
numPackets++
}
snts, err := f.rtpMunger.UpdateAndGetPaddingSnTs(numPackets, f.codec.ClockRate, frameRate, frameEndNeeded)
return snts, frameEndNeeded, err
}
func (f *Forwarder) GetPaddingVP8(frameEndNeeded bool) *buffer.VP8 {
f.lock.Lock()
defer f.lock.Unlock()
return f.vp8Munger.UpdateAndGetPadding(!frameEndNeeded)
}
func (f *Forwarder) GetRTPMungerParams() RTPMungerParams {
f.lock.RLock()
defer f.lock.RUnlock()
return f.rtpMunger.GetParams()
}