Set and use rid/spatial layer in TrackInfo. (#3724)

* Set and use rid/spatial layer in TrackInfo.

* test
This commit is contained in:
Raja Subramanian
2025-06-12 23:22:11 -07:00
committed by GitHub
parent a9e2911645
commit 670f927ff6
11 changed files with 275 additions and 36 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ require (
github.com/jxskiss/base62 v1.1.0
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded
github.com/livekit/protocol v1.39.2-0.20250612162213-de4c760d0eeb
github.com/livekit/protocol v1.39.3-0.20250613010514-7b9c3ae9e359
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c
github.com/mackerelio/go-osstat v0.2.5
github.com/magefile/mage v1.15.0
@@ -64,7 +64,7 @@ require (
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250612022732-297b8109523d.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250612204948-4001e52a3c94.1 // indirect
buf.build/go/protovalidate v0.13.0 // indirect
buf.build/go/protoyaml v0.6.0 // indirect
cel.dev/expr v0.24.0 // indirect
+4 -8
View File
@@ -1,9 +1,5 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1 h1:uwSqFkn8DDTzNlaV9TxgSXY5OCaNdb4rH+Axd2FujkE=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250603165357-b52ab10f4468.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250612022732-297b8109523d.1 h1:AGcXSSKkdfFsRk7qvOnl5VsaifpqL2Iwp9Xhmjchvpo=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250612022732-297b8109523d.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
buf.build/go/protovalidate v0.12.0 h1:4GKJotbspQjRCcqZMGVSuC8SjwZ/FmgtSuKDpKUTZew=
buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250612204948-4001e52a3c94.1 h1:u02KLZ7wlC15LvNhDaxhOxFjYmEtS30Lri5nOaZUomk=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250612204948-4001e52a3c94.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
buf.build/go/protovalidate v0.13.0 h1:t7nC2w79q8M2KaZfFTaXmyFhnYWTPbGFtZS2rebdIQM=
buf.build/go/protovalidate v0.13.0/go.mod h1:b0ZWMqcwgx2sa1IXTFT9EpJlMp03ESY4f8t9yulcykg=
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
@@ -173,8 +169,8 @@ github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5AT
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded h1:ylZPdnlX1RW9Z15SD4mp87vT2D2shsk0hpLJwSPcq3g=
github.com/livekit/mediatransportutil v0.0.0-20250519131108-fb90f5acfded/go.mod h1:mSNtYzSf6iY9xM3UX42VEI+STHvMgHmrYzEHPcdhB8A=
github.com/livekit/protocol v1.39.2-0.20250612162213-de4c760d0eeb h1:rDeauaLLiBOnhKxQzytVVv8nLSVs5LPijrAfuA/JXG0=
github.com/livekit/protocol v1.39.2-0.20250612162213-de4c760d0eeb/go.mod h1:6HPISM0bkTXTk9RIaQTCe0IDbomBPz7Jwp+N3w5sqL0=
github.com/livekit/protocol v1.39.3-0.20250613010514-7b9c3ae9e359 h1:eDPaRl7KLKfM66cCgYEjAL7+aCwpSFIOmIBOe2eGdjY=
github.com/livekit/protocol v1.39.3-0.20250613010514-7b9c3ae9e359/go.mod h1:6HPISM0bkTXTk9RIaQTCe0IDbomBPz7Jwp+N3w5sqL0=
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c h1:WwEr0YBejYbKzk8LSaO9h8h0G9MnE7shyDu8yXQWmEc=
github.com/livekit/psrpc v0.6.1-0.20250511053145-465289d72c3c/go.mod h1:kmD+AZPkWu0MaXIMv57jhNlbiSZZ/Jx4bzlxBDVmJes=
github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o=
+11 -6
View File
@@ -81,7 +81,6 @@ type MediaTrackParams struct {
ForwardStats *sfu.ForwardStats
OnTrackEverSubscribed func(livekit.TrackID)
ShouldRegressCodec func() bool
Rids buffer.VideoLayersRid
}
func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
@@ -107,7 +106,6 @@ func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
Telemetry: params.Telemetry,
Logger: params.Logger,
RegressionTargetCodec: t.regressionTargetCodec,
Rids: params.Rids,
}, ti)
if ti.Type == livekit.TrackType_AUDIO {
@@ -135,7 +133,7 @@ func NewMediaTrack(params MediaTrackParams, ti *livekit.TrackInfo) *MediaTrack {
t.dynacastManager.NotifySubscriberMaxQuality(
subscriberID,
mimeType,
buffer.SpatialLayerToVideoQuality(layer, t.MediaTrackReceiver.TrackInfo()),
buffer.GetVideoQualityForSpatialLayer(layer, t.MediaTrackReceiver.TrackInfo()),
)
},
)
@@ -167,7 +165,7 @@ func (t *MediaTrack) OnSubscribedMaxQualityChange(
for _, q := range maxSubscribedQualities {
receiver := t.Receiver(q.CodecMime)
if receiver != nil {
receiver.SetMaxExpectedSpatialLayer(buffer.VideoQualityToSpatialLayer(q.Quality, t.MediaTrackReceiver.TrackInfo()))
receiver.SetMaxExpectedSpatialLayer(buffer.GetSpatialLayerForVideoQuality(q.Quality, t.MediaTrackReceiver.TrackInfo()))
}
}
}
@@ -265,7 +263,7 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
t.lock.Lock()
var regressCodec bool
mimeType := mime.NormalizeMimeType(track.Codec().MimeType)
layer := buffer.RidToSpatialLayer(track.RID(), ti, t.params.Rids)
layer := buffer.GetSpatialLayerForRid(track.RID(), ti)
t.params.Logger.Debugw(
"AddReceiver",
"rid", track.RID(),
@@ -273,6 +271,14 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
"ssrc", track.SSRC(),
"codec", track.Codec(),
)
logger.Infow(
"AddReceiver",
"rid", track.RID(),
"layer", layer,
"ssrc", track.SSRC(),
"codec", track.Codec(),
"trackInfo", logger.Proto(ti),
) // REMOVE
wr := t.MediaTrackReceiver.Receiver(mimeType)
if wr == nil {
priority := -1
@@ -304,7 +310,6 @@ func (t *MediaTrack) AddReceiver(receiver *webrtc.RTPReceiver, track sfu.TrackRe
receiver,
track,
ti,
t.params.Rids,
LoggerWithCodecMime(t.params.Logger, mimeType),
t.params.OnRTCP,
t.params.VideoConfig.StreamTrackerManager,
+4 -5
View File
@@ -130,7 +130,6 @@ type MediaTrackReceiverParams struct {
Telemetry telemetry.TelemetryService
Logger logger.Logger
RegressionTargetCodec mime.MimeType
Rids buffer.VideoLayersRid
}
type MediaTrackReceiver struct {
@@ -175,7 +174,7 @@ func NewMediaTrackReceiver(params MediaTrackReceiverParams, ti *livekit.TrackInf
}
func (t *MediaTrackReceiver) Restart() {
hq := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.TrackInfo())
hq := buffer.GetSpatialLayerForVideoQuality(livekit.VideoQuality_HIGH, t.TrackInfo())
for _, receiver := range t.loadReceivers() {
receiver.SetMaxExpectedSpatialLayer(hq)
@@ -670,12 +669,12 @@ func (t *MediaTrackReceiver) updateTrackInfoOfReceivers() {
func (t *MediaTrackReceiver) SetLayerSsrc(mimeType mime.MimeType, rid string, ssrc uint32) {
t.lock.Lock()
trackInfo := t.TrackInfoClone()
layer := buffer.RidToSpatialLayer(rid, trackInfo, t.params.Rids)
layer := buffer.GetSpatialLayerForRid(rid, trackInfo)
if layer == buffer.InvalidLayerSpatial {
// non-simulcast case will not have `rid`
layer = 0
}
quality := buffer.SpatialLayerToVideoQuality(layer, trackInfo)
quality := buffer.GetVideoQualityForSpatialLayer(layer, trackInfo)
// set video layer ssrc info
for i, ci := range trackInfo.Codecs {
if mime.NormalizeMimeType(ci.MimeType) != mimeType {
@@ -845,7 +844,7 @@ func (t *MediaTrackReceiver) TrackInfoClone() *livekit.TrackInfo {
func (t *MediaTrackReceiver) NotifyMaxLayerChange(maxLayer int32) {
trackInfo := t.TrackInfo()
quality := buffer.SpatialLayerToVideoQuality(maxLayer, trackInfo)
quality := buffer.GetVideoQualityForSpatialLayer(maxLayer, trackInfo)
ti := &livekit.TrackInfo{
Sid: trackInfo.Sid,
Type: trackInfo.Type,
+1 -1
View File
@@ -181,7 +181,7 @@ func (t *MediaTrackSubscriptions) AddSubscriber(sub types.LocalParticipant, wr *
if !wr.DetermineReceiver(codec) {
if t.onSubscriberMaxQualityChange != nil {
go func() {
spatial := buffer.VideoQualityToSpatialLayer(livekit.VideoQuality_HIGH, t.params.MediaTrack.ToProto())
spatial := buffer.GetSpatialLayerForVideoQuality(livekit.VideoQuality_HIGH, t.params.MediaTrack.ToProto())
t.onSubscriberMaxQualityChange(downTrack.SubscriberID(), mime.NormalizeMimeType(codec.MimeType), spatial)
}()
}
+19 -1
View File
@@ -2814,6 +2814,25 @@ func (p *ParticipantImpl) mediaTrackReceived(track sfu.TrackRemote, rtpReceiver
// only assign version on a fresh publish, i. e. avoid updating version in scenarios like migration
ti.Version = p.params.VersionGenerator.Next().ToProto()
}
if len(sdpRids) != 0 {
for _, layer := range ti.Layers {
layer.SpatialLayer = buffer.VideoQualityToSpatialLayer(layer.Quality, ti)
layer.Rid = buffer.VideoQualityToRid(layer.Quality, ti, sdpRids)
}
for _, codec := range ti.Codecs {
if !mime.IsMimeTypeStringEqual(codec.MimeType, track.Codec().MimeType) {
continue
}
for _, layer := range codec.Layers {
layer.SpatialLayer = buffer.VideoQualityToSpatialLayer(layer.Quality, ti)
layer.Rid = buffer.VideoQualityToRid(layer.Quality, ti, sdpRids)
}
}
}
mt = p.addMediaTrack(signalCid, track.ID(), ti, sdpRids)
newTrack = true
@@ -2938,7 +2957,6 @@ func (p *ParticipantImpl) addMediaTrack(signalCid string, sdpCid string, ti *liv
ShouldRegressCodec: func() bool {
return p.helper().ShouldRegressCodec()
},
Rids: sdpRids,
}, ti)
mt.OnSubscribedMaxQualityChange(p.onSubscribedMaxQualityChange)
+1 -1
View File
@@ -257,7 +257,7 @@ func (t *SubscribedTrack) applySettings() {
quality = mt.GetQualityForDimension(t.settings.Width, t.settings.Height)
}
spatial = buffer.VideoQualityToSpatialLayer(quality, mt.ToProto())
spatial = buffer.GetSpatialLayerForVideoQuality(quality, mt.ToProto())
if t.settings.Fps > 0 {
temporal = mt.GetTemporalLayerForSpatialFps(spatial, t.settings.Fps, dt.Mime())
}
+56 -3
View File
@@ -104,13 +104,13 @@ func RidToSpatialLayer(rid string, trackInfo *livekit.TrackInfo, ridSpace VideoL
return 2
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_MEDIUM]:
logger.Warnw("unexpected rid f with only two qualities, low and medium", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
logger.Warnw("unexpected rid with only two qualities, low and medium", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo), "rid", ridSpace[2])
return 1
case lp[livekit.VideoQuality_LOW] && lp[livekit.VideoQuality_HIGH]:
logger.Warnw("unexpected rid f with only two qualities, low and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
logger.Warnw("unexpected rid with only two qualities, low and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo), "rid", ridSpace[2])
return 1
case lp[livekit.VideoQuality_MEDIUM] && lp[livekit.VideoQuality_HIGH]:
logger.Warnw("unexpected rid f with only two qualities, medium and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo))
logger.Warnw("unexpected rid with only two qualities, medium and high", nil, "trackID", trackInfo.Sid, "trackInfo", logger.Proto(trackInfo), "rid", ridSpace[2])
return 1
default:
@@ -330,3 +330,56 @@ func VideoQualityToSpatialLayer(quality livekit.VideoQuality, trackInfo *livekit
return InvalidLayerSpatial
}
// SIMULCAST-CODEC-TODO: these need to be codec mime aware if and when each codec suppports different layers
func GetSpatialLayerForRid(rid string, ti *livekit.TrackInfo) int32 {
if rid == "" {
// single layer without RID
return 0
}
if ti == nil {
return InvalidLayerSpatial
}
for _, layer := range ti.Layers {
if layer.Rid == rid {
return layer.SpatialLayer
}
}
if len(ti.Layers) == 1 {
// single layer without RID
return 0
}
return InvalidLayerSpatial
}
func GetSpatialLayerForVideoQuality(quality livekit.VideoQuality, ti *livekit.TrackInfo) int32 {
if ti == nil {
return InvalidLayerSpatial
}
for _, layer := range ti.Layers {
if layer.Quality == quality {
return layer.SpatialLayer
}
}
return InvalidLayerSpatial
}
func GetVideoQualityForSpatialLayer(spatialLayer int32, ti *livekit.TrackInfo) livekit.VideoQuality {
if spatialLayer == InvalidLayerSpatial || ti == nil {
return livekit.VideoQuality_OFF
}
for _, layer := range ti.Layers {
if layer.SpatialLayer == spatialLayer {
return layer.Quality
}
}
return livekit.VideoQuality_OFF
}
+173
View File
@@ -440,3 +440,176 @@ func TestVideoQualityToRidConversion(t *testing.T) {
})
}
}
func TestGetSpatialLayerForRid(t *testing.T) {
tests := []struct {
name string
trackInfo *livekit.TrackInfo
ridToSpatialLayer map[string]int32
}{
{
"no track info",
nil,
map[string]int32{
QuarterResolution: InvalidLayerSpatial,
HalfResolution: InvalidLayerSpatial,
FullResolution: InvalidLayerSpatial,
},
},
{
"no layers",
&livekit.TrackInfo{},
map[string]int32{
QuarterResolution: InvalidLayerSpatial,
HalfResolution: InvalidLayerSpatial,
FullResolution: InvalidLayerSpatial,
},
},
{
"no rid",
&livekit.TrackInfo{},
map[string]int32{
"": 0,
},
},
{
"single layer",
&livekit.TrackInfo{
Layers: []*livekit.VideoLayer{
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0},
},
},
map[string]int32{
QuarterResolution: 0,
HalfResolution: 0,
FullResolution: 0,
},
},
{
"layers",
&livekit.TrackInfo{
Layers: []*livekit.VideoLayer{
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0, Rid: QuarterResolution},
{Quality: livekit.VideoQuality_MEDIUM, SpatialLayer: 1, Rid: HalfResolution},
},
},
map[string]int32{
QuarterResolution: 0,
HalfResolution: 1,
FullResolution: InvalidLayerSpatial,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for testRid, expectedSpatialLayer := range test.ridToSpatialLayer {
actualSpatialLayer := GetSpatialLayerForRid(testRid, test.trackInfo)
require.Equal(t, expectedSpatialLayer, actualSpatialLayer)
}
})
}
}
func TestGetSpatialLayerForVideoQuality(t *testing.T) {
tests := []struct {
name string
trackInfo *livekit.TrackInfo
videoQualityToSpatialLayer map[livekit.VideoQuality]int32
}{
{
"no track info",
nil,
map[livekit.VideoQuality]int32{
livekit.VideoQuality_LOW: InvalidLayerSpatial,
livekit.VideoQuality_MEDIUM: InvalidLayerSpatial,
livekit.VideoQuality_HIGH: InvalidLayerSpatial,
},
},
{
"no layers",
&livekit.TrackInfo{},
map[livekit.VideoQuality]int32{
livekit.VideoQuality_LOW: InvalidLayerSpatial,
livekit.VideoQuality_MEDIUM: InvalidLayerSpatial,
livekit.VideoQuality_HIGH: InvalidLayerSpatial,
},
},
{
"layers",
&livekit.TrackInfo{
Layers: []*livekit.VideoLayer{
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0, Rid: QuarterResolution},
{Quality: livekit.VideoQuality_MEDIUM, SpatialLayer: 1, Rid: HalfResolution},
},
},
map[livekit.VideoQuality]int32{
livekit.VideoQuality_LOW: 0,
livekit.VideoQuality_MEDIUM: 1,
livekit.VideoQuality_HIGH: InvalidLayerSpatial,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for testVideoQuality, expectedSpatialLayer := range test.videoQualityToSpatialLayer {
actualSpatialLayer := GetSpatialLayerForVideoQuality(testVideoQuality, test.trackInfo)
require.Equal(t, expectedSpatialLayer, actualSpatialLayer)
}
})
}
}
func TestGetVideoQualityorSpatialLayer(t *testing.T) {
tests := []struct {
name string
trackInfo *livekit.TrackInfo
spatialLayerToVideoQuality map[int32]livekit.VideoQuality
}{
{
"no track info",
nil,
map[int32]livekit.VideoQuality{
InvalidLayerSpatial: livekit.VideoQuality_OFF,
0: livekit.VideoQuality_OFF,
1: livekit.VideoQuality_OFF,
2: livekit.VideoQuality_OFF,
},
},
{
"no layers",
&livekit.TrackInfo{},
map[int32]livekit.VideoQuality{
InvalidLayerSpatial: livekit.VideoQuality_OFF,
0: livekit.VideoQuality_OFF,
1: livekit.VideoQuality_OFF,
2: livekit.VideoQuality_OFF,
},
},
{
"layers",
&livekit.TrackInfo{
Layers: []*livekit.VideoLayer{
{Quality: livekit.VideoQuality_LOW, SpatialLayer: 0, Rid: QuarterResolution},
{Quality: livekit.VideoQuality_MEDIUM, SpatialLayer: 1, Rid: HalfResolution},
},
},
map[int32]livekit.VideoQuality{
InvalidLayerSpatial: livekit.VideoQuality_OFF,
0: livekit.VideoQuality_LOW,
1: livekit.VideoQuality_MEDIUM,
2: livekit.VideoQuality_OFF,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for testSpatialLayer, expectedVideoQuality := range test.spatialLayerToVideoQuality {
actualVideoQuality := GetVideoQualityForSpatialLayer(testSpatialLayer, test.trackInfo)
require.Equal(t, expectedVideoQuality, actualVideoQuality)
}
})
}
}
+2 -6
View File
@@ -177,7 +177,6 @@ type WebRTCReceiver struct {
closed atomic.Bool
useTrackers bool
trackInfo atomic.Pointer[livekit.TrackInfo]
rids buffer.VideoLayersRid
onRTCP func([]rtcp.Packet)
@@ -252,7 +251,6 @@ func NewWebRTCReceiver(
receiver *webrtc.RTPReceiver,
track TrackRemote,
trackInfo *livekit.TrackInfo,
rids buffer.VideoLayersRid,
logger logger.Logger,
onRTCP func([]rtcp.Packet),
streamTrackerManagerConfig StreamTrackerManagerConfig,
@@ -266,7 +264,6 @@ func NewWebRTCReceiver(
codec: track.Codec(),
codecState: ReceiverCodecStateNormal,
kind: track.Kind(),
rids: rids,
onRTCP: onRTCP,
isSVC: mime.IsMimeTypeStringSVC(track.Codec().MimeType),
isRED: mime.IsMimeTypeStringRED(track.Codec().MimeType),
@@ -404,7 +401,7 @@ func (w *WebRTCReceiver) AddUpTrack(track TrackRemote, buff *buffer.Buffer) erro
layer := int32(0)
if w.Kind() == webrtc.RTPCodecTypeVideo && !w.isSVC {
layer = buffer.RidToSpatialLayer(track.RID(), w.trackInfo.Load(), w.rids)
layer = buffer.GetSpatialLayerForRid(track.RID(), w.trackInfo.Load())
}
buff.SetLogger(w.logger.WithValues("layer", layer))
buff.SetAudioLevelParams(audio.AudioLevelParams{
@@ -516,8 +513,7 @@ func (w *WebRTCReceiver) notifyMaxExpectedLayer(layer int32) {
expectedBitrate := int64(0)
for _, vl := range ti.Layers {
l := buffer.VideoQualityToSpatialLayer(vl.Quality, ti)
if l <= layer {
if vl.SpatialLayer <= layer {
expectedBitrate += int64(vl.Bitrate)
}
}
+2 -3
View File
@@ -597,9 +597,8 @@ func (s *StreamTrackerManager) maxExpectedLayerFromTrackInfoLocked() {
ti := s.trackInfo.Load()
if ti != nil {
for _, layer := range ti.Layers {
spatialLayer := buffer.VideoQualityToSpatialLayer(layer.Quality, ti)
if spatialLayer > s.maxExpectedLayer {
s.maxExpectedLayer = spatialLayer
if layer.SpatialLayer > s.maxExpectedLayer {
s.maxExpectedLayer = layer.SpatialLayer
}
}
}