diff --git a/pkg/rtc/dynacastquality.go b/pkg/rtc/dynacastquality.go index bba9d8f74..5b55e1ae8 100644 --- a/pkg/rtc/dynacastquality.go +++ b/pkg/rtc/dynacastquality.go @@ -58,7 +58,13 @@ func (d *DynacastQuality) OnSubscribedMaxQualityChange(f func(maxSubscribedQuali } func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) { - d.params.Logger.Infow("setting subscriber max quality", "mime", d.params.MimeType, "subscriberID", subscriberID, "quality", quality.String()) + d.params.Logger.Infow( + "setting subscriber max quality", + "mime", d.params.MimeType, + "subscriberID", subscriberID, + "quality", quality.String(), + ) + d.lock.Lock() if quality == livekit.VideoQuality_OFF { delete(d.maxSubscriberQuality, subscriberID) @@ -71,7 +77,13 @@ func (d *DynacastQuality) NotifySubscriberMaxQuality(subscriberID livekit.Partic } func (d *DynacastQuality) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) { - d.params.Logger.Infow("setting subscriber node max quality", "mime", d.params.MimeType, "subscriberNodeID", nodeID, "quality", quality.String()) + d.params.Logger.Infow( + "setting subscriber node max quality", + "mime", d.params.MimeType, + "subscriberNodeID", nodeID, + "quality", quality.String(), + ) + d.lock.Lock() if quality == livekit.VideoQuality_OFF { delete(d.maxSubscriberNodeQuality, nodeID) diff --git a/pkg/rtc/mediatrack.go b/pkg/rtc/mediatrack.go index b1f6bd9ee..e0f5f2b22 100644 --- a/pkg/rtc/mediatrack.go +++ b/pkg/rtc/mediatrack.go @@ -19,7 +19,6 @@ import ( "github.com/livekit/livekit-server/pkg/sfu/buffer" "github.com/livekit/livekit-server/pkg/sfu/twcc" "github.com/livekit/livekit-server/pkg/telemetry" - "github.com/livekit/livekit-server/pkg/utils" ) // MediaTrack represents a WebRTC track that needs to be forwarded @@ -106,28 +105,45 @@ func NewMediaTrack(params MediaTrackParams) *MediaTrack { t.MediaTrackReceiver.OnSetupReceiver(func(mime string) { t.dynacastManager.AddCodec(mime) }) - t.MediaTrackReceiver.OnSubscriberMaxQualityChange(func(subscriberID livekit.ParticipantID, codec webrtc.RTPCodecCapability, layer int32) { - t.dynacastManager.NotifySubscriberMaxQuality(subscriberID, codec.MimeType, utils.QualityForSpatialLayer(layer)) - }) + t.MediaTrackReceiver.OnSubscriberMaxQualityChange( + func(subscriberID livekit.ParticipantID, codec webrtc.RTPCodecCapability, layer int32) { + t.dynacastManager.NotifySubscriberMaxQuality( + subscriberID, + codec.MimeType, + buffer.SpatialLayerToVideoQuality(layer, t.params.TrackInfo), + ) + }, + ) } return t } -func (t *MediaTrack) OnSubscribedMaxQualityChange(f func(trackID livekit.TrackID, subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality) error) { - if t.dynacastManager != nil { - t.dynacastManager.OnSubscribedMaxQualityChange(func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality) { - if f != nil && !t.IsMuted() { - _ = f(t.ID(), subscribedQualities, maxSubscribedQualities) - } - for _, q := range maxSubscribedQualities { - receiver := t.Receiver(q.CodecMime) - if receiver != nil { - receiver.SetMaxExpectedSpatialLayer(utils.SpatialLayerForQuality(q.Quality)) - } - } - }) +func (t *MediaTrack) OnSubscribedMaxQualityChange( + f func( + trackID livekit.TrackID, + subscribedQualities []*livekit.SubscribedCodec, + maxSubscribedQualities []types.SubscribedCodecQuality, + ) error, +) { + if t.dynacastManager == nil { + return } + + handler := func(subscribedQualities []*livekit.SubscribedCodec, maxSubscribedQualities []types.SubscribedCodecQuality) { + if f != nil && !t.IsMuted() { + _ = f(t.ID(), subscribedQualities, maxSubscribedQualities) + } + + for _, q := range maxSubscribedQualities { + receiver := t.Receiver(q.CodecMime) + if receiver != nil { + receiver.SetMaxExpectedSpatialLayer(buffer.VideoQualityToSpatialLayer(q.Quality, t.params.TrackInfo)) + } + } + } + + t.dynacastManager.OnSubscribedMaxQualityChange(handler) } func (t *MediaTrack) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, qualities []types.SubscribedCodecQuality) { @@ -267,7 +283,7 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track *webrtc.Tra ) } - newWR.OnMaxLayerChange(t.OnMaxLayerChange) + newWR.OnMaxLayerChange(t.onMaxLayerChange) t.buffer = buff @@ -315,7 +331,7 @@ func (t *MediaTrack) HasPendingCodec() bool { return t.MediaTrackReceiver.PrimaryReceiver() == nil } -func (t *MediaTrack) OnMaxLayerChange(maxLayer int32) { +func (t *MediaTrack) onMaxLayerChange(maxLayer int32) { ti := &livekit.TrackInfo{ Sid: t.trackInfo.Sid, Type: t.trackInfo.Type, diff --git a/pkg/rtc/mediatrackreceiver.go b/pkg/rtc/mediatrackreceiver.go index 510b82b6b..b0af43889 100644 --- a/pkg/rtc/mediatrackreceiver.go +++ b/pkg/rtc/mediatrackreceiver.go @@ -18,7 +18,6 @@ import ( "github.com/livekit/livekit-server/pkg/sfu" "github.com/livekit/livekit-server/pkg/sfu/buffer" "github.com/livekit/livekit-server/pkg/telemetry" - "github.com/livekit/livekit-server/pkg/utils" ) const ( @@ -116,7 +115,7 @@ func (t *MediaTrackReceiver) Restart() { t.lock.Unlock() for _, receiver := range receivers { - receiver.SetMaxExpectedSpatialLayer(utils.SpatialLayerForQuality(livekit.VideoQuality_HIGH)) + receiver.SetMaxExpectedSpatialLayer(buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.params.TrackInfo)) } } @@ -219,7 +218,7 @@ func (t *MediaTrackReceiver) SetLayerSsrc(mime string, rid string, ssrc uint32) t.lock.Lock() defer t.lock.Unlock() - layer := sfu.RidToLayer(rid) + layer := buffer.RidToSpatialLayer(rid, t.params.TrackInfo) if layer == sfu.InvalidLayerSpatial { // non-simulcast case will not have `rid` layer = 0 @@ -774,14 +773,3 @@ func (t *MediaTrackReceiver) SetRTT(rtt uint32) { } // --------------------------- - -func VideoQualityToRID(q livekit.VideoQuality) string { - switch q { - case livekit.VideoQuality_HIGH: - return sfu.FullResolution - case livekit.VideoQuality_MEDIUM: - return sfu.HalfResolution - default: - return sfu.QuarterResolution - } -} diff --git a/pkg/rtc/participant.go b/pkg/rtc/participant.go index 3ea713f06..36bac1694 100644 --- a/pkg/rtc/participant.go +++ b/pkg/rtc/participant.go @@ -1226,13 +1226,15 @@ func (p *ParticipantImpl) onMediaTrack(track *webrtc.TrackRemote, rtpReceiver *w "kind", track.Kind().String(), "trackID", publishedTrack.ID(), "rid", track.RID(), - "SSRC", track.SSRC()) + "SSRC", track.SSRC(), + "mime", track.Codec().MimeType) } else { p.params.Logger.Warnw("webrtc Track published but can't find MediaTrack", nil, "kind", track.Kind().String(), "webrtcTrackID", track.ID(), "rid", track.RID(), - "SSRC", track.SSRC()) + "SSRC", track.SSRC(), + "mime", track.Codec().MimeType) } if !isNewTrack && publishedTrack != nil && !publishedTrack.HasPendingCodec() && p.IsReady() { diff --git a/pkg/rtc/subscribedtrack.go b/pkg/rtc/subscribedtrack.go index 1367a2931..3a457cf24 100644 --- a/pkg/rtc/subscribedtrack.go +++ b/pkg/rtc/subscribedtrack.go @@ -11,7 +11,7 @@ import ( "github.com/livekit/livekit-server/pkg/rtc/types" "github.com/livekit/livekit-server/pkg/sfu" - "github.com/livekit/livekit-server/pkg/utils" + "github.com/livekit/livekit-server/pkg/sfu/buffer" ) const ( @@ -58,7 +58,9 @@ func (t *SubscribedTrack) OnBind(f func()) { func (t *SubscribedTrack) Bound() { t.bound.Store(true) if !t.params.AdaptiveStream { - t.params.DownTrack.SetMaxSpatialLayer(utils.SpatialLayerForQuality(livekit.VideoQuality_HIGH)) + t.params.DownTrack.SetMaxSpatialLayer( + buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.params.MediaTrack.ToProto()), + ) } t.maybeOnBind() } @@ -141,7 +143,7 @@ func (t *SubscribedTrack) UpdateVideoLayer() { if settings.Width > 0 { quality = t.MediaTrack().GetQualityForDimension(settings.Width, settings.Height) } - t.DownTrack().SetMaxSpatialLayer(utils.SpatialLayerForQuality(quality)) + t.DownTrack().SetMaxSpatialLayer(buffer.VideoQualityToSpatialLayer(quality, t.params.MediaTrack.ToProto())) } func (t *SubscribedTrack) updateDownTrackMute() { diff --git a/pkg/sfu/buffer/buffer.go b/pkg/sfu/buffer/buffer.go index 2527e36ea..44dc8f63e 100644 --- a/pkg/sfu/buffer/buffer.go +++ b/pkg/sfu/buffer/buffer.go @@ -476,8 +476,6 @@ func (b *Buffer) getExtPacket(rawPacket []byte, rtpPacket *rtp.Packet, arrivalTi } case "video/h264": ep.KeyFrame = IsH264Keyframe(rtpPacket.Payload) - case "video/vp9": - ep.KeyFrame = IsVp9Keyframe(rtpPacket.Payload) case "video/av1": ep.KeyFrame = IsAV1Keyframe(rtpPacket.Payload) } diff --git a/pkg/sfu/buffer/helpers.go b/pkg/sfu/buffer/helpers.go index 398edbf9e..b55724420 100644 --- a/pkg/sfu/buffer/helpers.go +++ b/pkg/sfu/buffer/helpers.go @@ -5,8 +5,6 @@ import ( "errors" "time" - "github.com/pion/rtp/codecs" - "github.com/livekit/protocol/logger" ) @@ -279,30 +277,6 @@ func IsH264Keyframe(payload []byte) bool { return false } -// IsAV1Keyframe detects if vp9 payload is a keyframe -// taken from https://github.com/jech/galene/blob/master/codecs/codecs.go -// all credits belongs to Juliusz Chroboczek @jech and the awesome Galene SFU -func IsVp9Keyframe(payload []byte) bool { - var vp9 codecs.VP9Packet - _, err := vp9.Unmarshal(payload) - if err != nil || len(vp9.Payload) < 1 { - return false - } - if !vp9.B { - return false - } - - if (vp9.Payload[0] & 0xc0) != 0x80 { - return false - } - - profile := (vp9.Payload[0] >> 4) & 0x3 - if profile != 3 { - return (vp9.Payload[0] & 0xC) == 0 - } - return (vp9.Payload[0] & 0x6) == 0 -} - // IsAV1Keyframe detects if av1 payload is a keyframe // taken from https://github.com/jech/galene/blob/master/codecs/codecs.go // all credits belongs to Juliusz Chroboczek @jech and the awesome Galene SFU diff --git a/pkg/sfu/buffer/videolayerutils.go b/pkg/sfu/buffer/videolayerutils.go new file mode 100644 index 000000000..b2bc54f23 --- /dev/null +++ b/pkg/sfu/buffer/videolayerutils.go @@ -0,0 +1,306 @@ +package buffer + +import ( + "github.com/livekit/protocol/livekit" + "github.com/livekit/protocol/logger" +) + +const ( + QuarterResolution = "q" + HalfResolution = "h" + FullResolution = "f" +) + +func LayerPresenceFromTrackInfo(trackInfo *livekit.TrackInfo) *[livekit.VideoQuality_HIGH + 1]bool { + if trackInfo == nil || len(trackInfo.Layers) == 0 { + return nil + } + + var layerPresence [livekit.VideoQuality_HIGH + 1]bool + for _, layer := range trackInfo.Layers { + layerPresence[layer.Quality] = true + } + + return &layerPresence +} + +func RidToSpatialLayer(rid string, trackInfo *livekit.TrackInfo) int32 { + lp := LayerPresenceFromTrackInfo(trackInfo) + if lp == nil { + switch rid { + case QuarterResolution: + return 0 + case HalfResolution: + return 1 + case FullResolution: + return 2 + default: + return 0 + } + } + + switch rid { + case QuarterResolution: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return 0 + + default: + // only one quality published, could be any + return 0 + } + + case HalfResolution: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return 1 + + default: + // only one quality published, could be any + return 0 + } + + case FullResolution: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return 2 + + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + logger.Warnw("unexpected rid f with only two qualities, low and medium", nil) + return 1 + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + logger.Warnw("unexpected rid f with only two qualities, low and high", nil) + return 1 + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + logger.Warnw("unexpected rid f with only two qualities, medum and high", nil) + return 1 + + default: + // only one quality published, could be any + return 0 + } + + default: + // no rid, should be single layer + return 0 + } +} + +func SpatialLayerToRid(layer int32, trackInfo *livekit.TrackInfo) string { + lp := LayerPresenceFromTrackInfo(trackInfo) + if lp == nil { + switch layer { + case 0: + return QuarterResolution + case 1: + return HalfResolution + case 2: + return FullResolution + default: + return QuarterResolution + } + } + + switch layer { + case 0: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return QuarterResolution + + default: + return QuarterResolution + } + + case 1: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return HalfResolution + + default: + return QuarterResolution + } + + case 2: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return FullResolution + + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + logger.Warnw("unexpected layer 2 with only two qualities, low and medium", nil) + return HalfResolution + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + logger.Warnw("unexpected layer 2 with only two qualities, low and high", nil) + return HalfResolution + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + logger.Warnw("unexpected layer 2 with only two qualities, medum and high", nil) + return HalfResolution + + default: + return QuarterResolution + } + + default: + return QuarterResolution + } +} + +func VideoQualityToRid(quality livekit.VideoQuality, trackInfo *livekit.TrackInfo) string { + return SpatialLayerToRid(VideoQualityToSpatialLayer(quality, trackInfo), trackInfo) +} + +func SpatialLayerToVideoQuality(layer int32, trackInfo *livekit.TrackInfo) livekit.VideoQuality { + lp := LayerPresenceFromTrackInfo(trackInfo) + if lp == nil { + switch layer { + case 0: + return livekit.VideoQuality_LOW + case 1: + return livekit.VideoQuality_MEDIUM + case 2: + return livekit.VideoQuality_HIGH + default: + return livekit.VideoQuality_OFF + } + } + + switch layer { + case 0: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW]: + return livekit.VideoQuality_LOW + + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_MEDIUM]: + return livekit.VideoQuality_MEDIUM + + default: + return livekit.VideoQuality_HIGH + } + + case 1: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + return livekit.VideoQuality_MEDIUM + + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return livekit.VideoQuality_HIGH + + default: + logger.Errorw("invalid layer", nil, "layer", layer, "trackInfo", trackInfo) + return livekit.VideoQuality_HIGH + } + + case 2: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return livekit.VideoQuality_HIGH + + default: + logger.Errorw("invalid layer", nil, "layer", layer, "trackInfo", trackInfo) + return livekit.VideoQuality_HIGH + } + } + + return livekit.VideoQuality_OFF +} + +func VideoQualityToSpatialLayer(quality livekit.VideoQuality, trackInfo *livekit.TrackInfo) int32 { + lp := LayerPresenceFromTrackInfo(trackInfo) + if lp == nil { + switch quality { + case livekit.VideoQuality_LOW: + return 0 + case livekit.VideoQuality_MEDIUM: + return 1 + case livekit.VideoQuality_HIGH: + return 2 + default: + return InvalidLayerSpatial + } + } + + switch quality { + case livekit.VideoQuality_LOW: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + default: // only one quality published, could be any + return 0 + } + + case livekit.VideoQuality_MEDIUM: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + return 1 + + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return 0 + + default: // only one quality published, could be any + return 0 + } + + case livekit.VideoQuality_HIGH: + switch { + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return 2 + + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]: + fallthrough + case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]: + fallthrough + case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]: + return 1 + + default: // only one quality published, could be any + return 0 + } + } + + return InvalidLayerSpatial +} diff --git a/pkg/sfu/buffer/videolayerutils_test.go b/pkg/sfu/buffer/videolayerutils_test.go new file mode 100644 index 000000000..7e3b08792 --- /dev/null +++ b/pkg/sfu/buffer/videolayerutils_test.go @@ -0,0 +1,427 @@ +package buffer + +import ( + "testing" + + "github.com/livekit/protocol/livekit" + "github.com/stretchr/testify/require" +) + +func TestRidConversion(t *testing.T) { + type RidAndLayer struct { + rid string + layer int32 + } + tests := []struct { + name string + trackInfo *livekit.TrackInfo + ridToLayer map[string]RidAndLayer + }{ + { + "no track info", + nil, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + FullResolution: RidAndLayer{rid: FullResolution, layer: 2}, + }, + }, + { + "no layers", + &livekit.TrackInfo{}, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + FullResolution: RidAndLayer{rid: FullResolution, layer: 2}, + }, + }, + { + "single layer, low", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + }, + }, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + FullResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + }, + }, + { + "single layer, medium", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + }, + }, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + FullResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + }, + }, + { + "single layer, high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + FullResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + }, + }, + { + "two layers, low and medium", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + }, + }, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + FullResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + }, + }, + { + "two layers, low and high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + FullResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + }, + }, + { + "two layers, medium and high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + FullResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + }, + }, + { + "three layers", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[string]RidAndLayer{ + "": RidAndLayer{rid: QuarterResolution, layer: 0}, + QuarterResolution: RidAndLayer{rid: QuarterResolution, layer: 0}, + HalfResolution: RidAndLayer{rid: HalfResolution, layer: 1}, + FullResolution: RidAndLayer{rid: FullResolution, layer: 2}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for testRid, expectedResult := range test.ridToLayer { + actualLayer := RidToSpatialLayer(testRid, test.trackInfo) + require.Equal(t, expectedResult.layer, actualLayer) + + actualRid := SpatialLayerToRid(actualLayer, test.trackInfo) + require.Equal(t, expectedResult.rid, actualRid) + } + }) + } +} + +func TestQualityConversion(t *testing.T) { + type QualityAndLayer struct { + quality livekit.VideoQuality + layer int32 + } + tests := []struct { + name string + trackInfo *livekit.TrackInfo + qualityToLayer map[livekit.VideoQuality]QualityAndLayer + }{ + { + "no track info", + nil, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_LOW, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 1}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 2}, + }, + }, + { + "no layers", + &livekit.TrackInfo{}, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_LOW, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 1}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 2}, + }, + }, + { + "single layer, low", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + }, + }, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_LOW, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_LOW, layer: 0}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_LOW, layer: 0}, + }, + }, + { + "single layer, medium", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + }, + }, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 0}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 0}, + }, + }, + { + "single layer, high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 0}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 0}, + }, + }, + { + "two layers, low and medium", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + }, + }, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_LOW, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 1}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 1}, + }, + }, + { + "two layers, low and high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_LOW, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 1}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 1}, + }, + }, + { + "two layers, medium and high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 0}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 1}, + }, + }, + { + "three layers", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[livekit.VideoQuality]QualityAndLayer{ + livekit.VideoQuality_LOW: QualityAndLayer{quality: livekit.VideoQuality_LOW, layer: 0}, + livekit.VideoQuality_MEDIUM: QualityAndLayer{quality: livekit.VideoQuality_MEDIUM, layer: 1}, + livekit.VideoQuality_HIGH: QualityAndLayer{quality: livekit.VideoQuality_HIGH, layer: 2}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for testQuality, expectedResult := range test.qualityToLayer { + actualLayer := VideoQualityToSpatialLayer(testQuality, test.trackInfo) + require.Equal(t, expectedResult.layer, actualLayer) + + actualQuality := SpatialLayerToVideoQuality(actualLayer, test.trackInfo) + require.Equal(t, expectedResult.quality, actualQuality) + } + }) + } +} + +func TestVideoQualityToRidConversion(t *testing.T) { + tests := []struct { + name string + trackInfo *livekit.TrackInfo + qualityToRid map[livekit.VideoQuality]string + }{ + { + "no track info", + nil, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: HalfResolution, + livekit.VideoQuality_HIGH: FullResolution, + }, + }, + { + "no layers", + &livekit.TrackInfo{}, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: HalfResolution, + livekit.VideoQuality_HIGH: FullResolution, + }, + }, + { + "single layer, low", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + }, + }, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: QuarterResolution, + livekit.VideoQuality_HIGH: QuarterResolution, + }, + }, + { + "single layer, medium", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + }, + }, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: QuarterResolution, + livekit.VideoQuality_HIGH: QuarterResolution, + }, + }, + { + "single layer, high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: QuarterResolution, + livekit.VideoQuality_HIGH: QuarterResolution, + }, + }, + { + "two layers, low and medium", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + }, + }, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: HalfResolution, + livekit.VideoQuality_HIGH: HalfResolution, + }, + }, + { + "two layers, low and high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: HalfResolution, + livekit.VideoQuality_HIGH: HalfResolution, + }, + }, + { + "two layers, medium and high", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: QuarterResolution, + livekit.VideoQuality_HIGH: HalfResolution, + }, + }, + { + "three layers", + &livekit.TrackInfo{ + Layers: []*livekit.VideoLayer{ + &livekit.VideoLayer{Quality: livekit.VideoQuality_LOW}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_MEDIUM}, + &livekit.VideoLayer{Quality: livekit.VideoQuality_HIGH}, + }, + }, + map[livekit.VideoQuality]string{ + livekit.VideoQuality_LOW: QuarterResolution, + livekit.VideoQuality_MEDIUM: HalfResolution, + livekit.VideoQuality_HIGH: FullResolution, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for testQuality, expectedRid := range test.qualityToRid { + actualRid := VideoQualityToRid(testQuality, test.trackInfo) + require.Equal(t, expectedRid, actualRid) + } + }) + } +} diff --git a/pkg/sfu/connectionquality/connectionstats.go b/pkg/sfu/connectionquality/connectionstats.go index 2d140092f..42f141d24 100644 --- a/pkg/sfu/connectionquality/connectionstats.go +++ b/pkg/sfu/connectionquality/connectionstats.go @@ -11,7 +11,6 @@ import ( "github.com/livekit/protocol/logger" "github.com/livekit/livekit-server/pkg/sfu/buffer" - "github.com/livekit/livekit-server/pkg/utils" ) const ( @@ -82,7 +81,7 @@ func (cs *ConnectionStats) Start(trackInfo *livekit.TrackInfo) { cs.normFactors[0] = MaxScore / AudioTrackScore(params, 1) } else { for _, layer := range cs.trackInfo.Layers { - spatial := utils.SpatialLayerForQuality(layer.Quality) + spatial := buffer.VideoQualityToSpatialLayer(layer.Quality, cs.trackInfo) // LK-TODO: would be good to have expected frame rate in Trackinfo frameRate := uint32(30) switch spatial { @@ -99,7 +98,7 @@ func (cs *ConnectionStats) Start(trackInfo *livekit.TrackInfo) { Height: layer.Height, Frames: frameRate, } - cs.normFactors[utils.SpatialLayerForQuality(layer.Quality)] = MaxScore / VideoTrackScore(params, 1) + cs.normFactors[spatial] = MaxScore / VideoTrackScore(params, 1) } } cs.lock.Unlock() @@ -132,7 +131,7 @@ func (cs *ConnectionStats) getLayerDimensions(layer int32) (uint32, uint32) { } for _, l := range cs.trackInfo.Layers { - if layer == utils.SpatialLayerForQuality(l.Quality) { + if layer == buffer.VideoQualityToSpatialLayer(l.Quality, cs.trackInfo) { return l.Width, l.Height } } diff --git a/pkg/sfu/helpers.go b/pkg/sfu/helpers.go index cc54c824e..53292f030 100644 --- a/pkg/sfu/helpers.go +++ b/pkg/sfu/helpers.go @@ -11,12 +11,6 @@ import ( "github.com/livekit/livekit-server/pkg/sfu/buffer" ) -const ( - QuarterResolution = "q" - HalfResolution = "h" - FullResolution = "f" -) - // Do a fuzzy find for a codec in the list of codecs // Used for lookup up a codec in an existing list to find a match func codecParametersFuzzySearch(needle webrtc.RTPCodecParameters, haystack []webrtc.RTPCodecParameters) (webrtc.RTPCodecParameters, error) { diff --git a/pkg/sfu/receiver.go b/pkg/sfu/receiver.go index 53518dae6..db04bb3f6 100644 --- a/pkg/sfu/receiver.go +++ b/pkg/sfu/receiver.go @@ -12,7 +12,6 @@ import ( "github.com/pion/webrtc/v3" "go.uber.org/atomic" - "github.com/livekit/livekit-server/pkg/utils" "github.com/livekit/protocol/livekit" "github.com/livekit/protocol/logger" @@ -109,19 +108,6 @@ type WebRTCReceiver struct { primaryReceiver atomic.Value // *RedPrimaryReceiver } -func RidToLayer(rid string) int32 { - switch rid { - case FullResolution: - return 2 - case HalfResolution: - return 1 - case QuarterResolution: - return 0 - default: - return InvalidLayerSpatial - } -} - func IsSvcCodec(mime string) bool { switch strings.ToLower(mime) { case "video/av1": @@ -312,14 +298,9 @@ func (w *WebRTCReceiver) AddUpTrack(track *webrtc.TrackRemote, buff *buffer.Buff return } - layer := RidToLayer(track.RID()) - if layer == InvalidLayerSpatial { - // check if there is only one layer and if so, assign it to that - if len(w.trackInfo.Layers) == 1 { - layer = utils.SpatialLayerForQuality(w.trackInfo.Layers[0].Quality) - } else { - layer = 0 - } + layer := int32(0) + if w.Kind() == webrtc.RTPCodecTypeVideo { + layer = buffer.RidToSpatialLayer(track.RID(), w.trackInfo) } buff.SetLogger(logger.Logger(logr.Logger(w.logger).WithValues("layer", layer))) buff.SetTWCC(w.twcc) @@ -332,12 +313,12 @@ func (w *WebRTCReceiver) AddUpTrack(track *webrtc.TrackRemote, buff *buffer.Buff buff.OnRtcpFeedback(w.sendRTCP) var duration time.Duration - switch track.RID() { - case FullResolution: + switch layer { + case 2: duration = w.pliThrottleConfig.HighQuality - case HalfResolution: + case 1: duration = w.pliThrottleConfig.MidQuality - case QuarterResolution: + case 0: duration = w.pliThrottleConfig.LowQuality default: duration = w.pliThrottleConfig.MidQuality @@ -549,6 +530,9 @@ func (w *WebRTCReceiver) forwardRTP(layer int32) { }) w.streamTrackerManager.RemoveTracker(layer) + if w.isSVC { + w.streamTrackerManager.RemoveAllTrackers() + } }() for { diff --git a/pkg/sfu/streamtrackermanager.go b/pkg/sfu/streamtrackermanager.go index 5f6f40f7b..cd141bfb0 100644 --- a/pkg/sfu/streamtrackermanager.go +++ b/pkg/sfu/streamtrackermanager.go @@ -7,7 +7,7 @@ import ( "github.com/go-logr/logr" - "github.com/livekit/livekit-server/pkg/utils" + "github.com/livekit/livekit-server/pkg/sfu/buffer" "github.com/livekit/protocol/livekit" "github.com/livekit/protocol/logger" ) @@ -89,7 +89,7 @@ func NewStreamTrackerManager(logger logger.Logger, trackInfo *livekit.TrackInfo) } for _, layer := range s.trackInfo.Layers { - spatialLayer := utils.SpatialLayerForQuality(layer.Quality) + spatialLayer := buffer.VideoQualityToSpatialLayer(layer.Quality, trackInfo) if spatialLayer > s.maxPublishedLayer { s.maxPublishedLayer = spatialLayer } @@ -263,7 +263,7 @@ func (s *StreamTrackerManager) GetLayerDimension(layer int32) (uint32, uint32) { height := uint32(0) width := uint32(0) if len(s.trackInfo.Layers) > 0 { - quality := utils.QualityForSpatialLayer(layer) + quality := buffer.SpatialLayerToVideoQuality(layer, s.trackInfo) for _, layer := range s.trackInfo.Layers { if layer.Quality == quality { height = layer.Height @@ -376,7 +376,12 @@ func (s *StreamTrackerManager) addAvailableLayer(layer int32) { exemptedLayers = append(exemptedLayers, s.exemptedLayers...) s.lock.Unlock() - s.logger.Debugw("available layers changed - layer seen", "added", layer, "availableLayers", availableLayers, "exemptedLayers", exemptedLayers) + s.logger.Debugw( + "available layers changed - layer seen", + "added", layer, + "availableLayers", availableLayers, + "exemptedLayers", exemptedLayers, + ) if s.onAvailableLayersChanged != nil { s.onAvailableLayersChanged(availableLayers, exemptedLayers) @@ -430,16 +435,18 @@ func (s *StreamTrackerManager) removeAvailableLayer(layer int32) { // // add to exempt if not already present // - found := false - for _, el := range s.exemptedLayers { - if el == layer { - found = true - break + if exempt { + found := false + for _, el := range s.exemptedLayers { + if el == layer { + found = true + break + } + } + if !found { + s.exemptedLayers = append(s.exemptedLayers, layer) + sort.Slice(s.exemptedLayers, func(i, j int) bool { return s.exemptedLayers[i] < s.exemptedLayers[j] }) } - } - if !found && exempt { - s.exemptedLayers = append(s.exemptedLayers, layer) - sort.Slice(s.exemptedLayers, func(i, j int) bool { return s.exemptedLayers[i] < s.exemptedLayers[j] }) } var exemptedLayers []int32 @@ -451,7 +458,12 @@ func (s *StreamTrackerManager) removeAvailableLayer(layer int32) { } s.lock.Unlock() - s.logger.Debugw("available layers changed - layer gone", "removed", layer, "layers", newLayers) + s.logger.Debugw( + "available layers changed - layer gone", + "removed", layer, + "availableLayers", newLayers, + "exeptedLayers", exemptedLayers, + ) // need to immediately switch off unavailable layers if s.onAvailableLayersChanged != nil { diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go deleted file mode 100644 index 6bba5b1cd..000000000 --- a/pkg/utils/helpers.go +++ /dev/null @@ -1,33 +0,0 @@ -package utils - -import ( - "github.com/livekit/protocol/livekit" -) - -func SpatialLayerForQuality(quality livekit.VideoQuality) int32 { - switch quality { - case livekit.VideoQuality_LOW: - return 0 - case livekit.VideoQuality_MEDIUM: - return 1 - case livekit.VideoQuality_HIGH: - return 2 - case livekit.VideoQuality_OFF: - return -1 - default: - return -1 - } -} - -func QualityForSpatialLayer(layer int32) livekit.VideoQuality { - switch layer { - case 0: - return livekit.VideoQuality_LOW - case 1: - return livekit.VideoQuality_MEDIUM - case 2: - return livekit.VideoQuality_HIGH - default: - return livekit.VideoQuality_OFF - } -}