snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
This commit is contained in:
523
pkg/rtc/dynacast/dynacastmanager_test.go
Normal file
523
pkg/rtc/dynacast/dynacastmanager_test.go
Normal file
@@ -0,0 +1,523 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
)
|
||||
|
||||
type testDynacastManagerListener struct {
|
||||
onSubscribedMaxQualityChange func(subscribedQualties []*livekit.SubscribedCodec)
|
||||
onSubscribedAudioCodecChange func(subscribedCodecs []*livekit.SubscribedAudioCodec)
|
||||
}
|
||||
|
||||
func (t *testDynacastManagerListener) OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
_maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
t.onSubscribedMaxQualityChange(subscribedQualities)
|
||||
}
|
||||
|
||||
func (t *testDynacastManagerListener) OnDynacastSubscribedAudioCodecChange(
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
t.onSubscribedAudioCodecChange(codecs)
|
||||
}
|
||||
|
||||
func TestSubscribedMaxQuality(t *testing.T) {
|
||||
t.Run("subscribers muted", func(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
|
||||
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
|
||||
|
||||
// mute all subscribers of vp8
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
|
||||
|
||||
expectedSubscribedQualities := []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
|
||||
t.Run("subscribers max quality", func(t *testing.T) {
|
||||
lock := sync.RWMutex{}
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.(*dynacastManagerVideo).maxSubscribedQuality = map[mime.MimeType]livekit.VideoQuality{
|
||||
mime.MimeTypeVP8: livekit.VideoQuality_LOW,
|
||||
mime.MimeTypeAV1: livekit.VideoQuality_LOW,
|
||||
}
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_HIGH)
|
||||
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_MEDIUM)
|
||||
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
|
||||
|
||||
expectedSubscribedQualities := []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// "s1" dropping to MEDIUM should disable HIGH layer
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_MEDIUM)
|
||||
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// "s1" , "s2" , "s3" dropping to LOW should disable HIGH & MEDIUM
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
|
||||
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
|
||||
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_LOW)
|
||||
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// muting "s2" only should not disable all qualities of vp8, no change of expected qualities
|
||||
dm.NotifySubscriberMaxQuality("s2", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// muting "s1" and s3 also should disable all qualities
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_OFF)
|
||||
dm.NotifySubscriberMaxQuality("s3", mime.MimeTypeAV1, livekit.VideoQuality_OFF)
|
||||
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// unmuting "s1" should enable vp8 previously set max quality
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeVP8, livekit.VideoQuality_LOW)
|
||||
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// a higher quality from a different node should trigger that quality
|
||||
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
|
||||
{CodecMime: mime.MimeTypeVP8, Quality: livekit.VideoQuality_HIGH},
|
||||
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_MEDIUM},
|
||||
})
|
||||
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCodecRegression(t *testing.T) {
|
||||
t.Run("codec regression video", func(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
actualSubscribedQualities := make([]*livekit.SubscribedCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerVideo(DynacastManagerVideoParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedMaxQualityChange: func(subscribedQualities []*livekit.SubscribedCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedQualities = subscribedQualities
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_HIGH)
|
||||
|
||||
expectedSubscribedQualities := []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
dm.HandleCodecRegression(mime.MimeTypeAV1, mime.MimeTypeVP8)
|
||||
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// av1 quality change should be forwarded to vp8
|
||||
// av1 quality change of node should be ignored
|
||||
dm.NotifySubscriberMaxQuality("s1", mime.MimeTypeAV1, livekit.VideoQuality_MEDIUM)
|
||||
dm.NotifySubscriberNodeMaxQuality("n1", []types.SubscribedCodecQuality{
|
||||
{CodecMime: mime.MimeTypeAV1, Quality: livekit.VideoQuality_HIGH},
|
||||
})
|
||||
expectedSubscribedQualities = []*livekit.SubscribedCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeAV1.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeVP8.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: true},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedCodecsAsString(expectedSubscribedQualities) == subscribedCodecsAsString(actualSubscribedQualities)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
|
||||
t.Run("codec regression audio", func(t *testing.T) {
|
||||
var lock sync.Mutex
|
||||
actualSubscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0)
|
||||
|
||||
dm := NewDynacastManagerAudio(DynacastManagerAudioParams{
|
||||
Listener: &testDynacastManagerListener{
|
||||
onSubscribedAudioCodecChange: func(subscribedCodecs []*livekit.SubscribedAudioCodec) {
|
||||
lock.Lock()
|
||||
actualSubscribedCodecs = subscribedCodecs
|
||||
lock.Unlock()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dm.NotifySubscription("s1", mime.MimeTypeRED, true)
|
||||
|
||||
expectedSubscribedCodecs := []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
dm.HandleCodecRegression(mime.MimeTypeRED, mime.MimeTypeOpus)
|
||||
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// RED disable as subscriber or subscriber node should be ignored as it has been regressed
|
||||
dm.NotifySubscription("s1", mime.MimeTypeRED, false)
|
||||
dm.NotifySubscriptionNode("n1", []*livekit.SubscribedAudioCodec{
|
||||
{Codec: mime.MimeTypeRED.String(), Enabled: false},
|
||||
})
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// `s1` unsubscription should turn off `opus`
|
||||
dm.NotifySubscription("s1", mime.MimeTypeOpus, false)
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
// a node subscription should turn `opus` back on
|
||||
dm.NotifySubscriptionNode("n1", []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
})
|
||||
expectedSubscribedCodecs = []*livekit.SubscribedAudioCodec{
|
||||
{
|
||||
Codec: mime.MimeTypeRED.String(),
|
||||
Enabled: false,
|
||||
},
|
||||
{
|
||||
Codec: mime.MimeTypeOpus.String(),
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
require.Eventually(t, func() bool {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
return subscribedAudioCodecsAsString(expectedSubscribedCodecs) == subscribedAudioCodecsAsString(actualSubscribedCodecs)
|
||||
}, 10*time.Second, 100*time.Millisecond)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func subscribedCodecsAsString(c1 []*livekit.SubscribedCodec) string {
|
||||
slices.SortFunc(c1, func(a, b *livekit.SubscribedCodec) int {
|
||||
return strings.Compare(a.Codec, b.Codec)
|
||||
})
|
||||
|
||||
var s1 strings.Builder
|
||||
for _, c := range c1 {
|
||||
s1.WriteString(c.String())
|
||||
}
|
||||
return s1.String()
|
||||
}
|
||||
|
||||
func subscribedAudioCodecsAsString(c1 []*livekit.SubscribedAudioCodec) string {
|
||||
slices.SortFunc(c1, func(a, b *livekit.SubscribedAudioCodec) int {
|
||||
return strings.Compare(a.Codec, b.Codec)
|
||||
})
|
||||
|
||||
var s1 strings.Builder
|
||||
for _, c := range c1 {
|
||||
s1.WriteString(c.String())
|
||||
}
|
||||
return s1.String()
|
||||
}
|
||||
198
pkg/rtc/dynacast/dynacastmanageraudio.go
Normal file
198
pkg/rtc/dynacast/dynacastmanageraudio.go
Normal file
@@ -0,0 +1,198 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
)
|
||||
|
||||
var _ DynacastManager = (*dynacastManagerAudio)(nil)
|
||||
var _ dynacastQualityListener = (*dynacastManagerAudio)(nil)
|
||||
|
||||
type DynacastManagerAudioParams struct {
|
||||
Listener DynacastManagerListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type dynacastManagerAudio struct {
|
||||
params DynacastManagerAudioParams
|
||||
|
||||
subscribedCodecs map[mime.MimeType]bool
|
||||
committedSubscribedCodecs map[mime.MimeType]bool
|
||||
|
||||
isClosed bool
|
||||
|
||||
*dynacastManagerBase
|
||||
}
|
||||
|
||||
func NewDynacastManagerAudio(params DynacastManagerAudioParams) DynacastManager {
|
||||
if params.Logger == nil {
|
||||
params.Logger = logger.GetLogger()
|
||||
}
|
||||
d := &dynacastManagerAudio{
|
||||
params: params,
|
||||
subscribedCodecs: make(map[mime.MimeType]bool),
|
||||
committedSubscribedCodecs: make(map[mime.MimeType]bool),
|
||||
}
|
||||
d.dynacastManagerBase = newDynacastManagerBase(dynacastManagerBaseParams{
|
||||
Logger: params.Logger,
|
||||
OpsQueueDepth: 4,
|
||||
OnRestart: func() {
|
||||
d.committedSubscribedCodecs = make(map[mime.MimeType]bool)
|
||||
},
|
||||
OnDynacastQualityCreate: func(mimeType mime.MimeType) dynacastQuality {
|
||||
dq := newDynacastQualityAudio(dynacastQualityAudioParams{
|
||||
MimeType: mimeType,
|
||||
Listener: d,
|
||||
Logger: d.params.Logger,
|
||||
})
|
||||
return dq
|
||||
},
|
||||
OnRegressCodec: func(fromMime, toMime mime.MimeType) {
|
||||
d.subscribedCodecs[fromMime] = false
|
||||
|
||||
// if the new codec is not added, notify the publisher to start publishing
|
||||
if _, ok := d.subscribedCodecs[toMime]; !ok {
|
||||
d.subscribedCodecs[toMime] = true
|
||||
}
|
||||
},
|
||||
OnUpdateNeeded: d.update,
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
// It is possible for tracks to be in pending close state. When track
|
||||
// is waiting to be closed, a node is not streaming a track. This can
|
||||
// be used to force an update announcing that subscribed codec is disabled,
|
||||
// i.e. indicating not pulling track any more.
|
||||
func (d *dynacastManagerAudio) ForceEnable(enabled bool) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
for mime := range d.committedSubscribedCodecs {
|
||||
d.committedSubscribedCodecs[mime] = enabled
|
||||
}
|
||||
|
||||
d.enqueueSubscribedChange()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
dq := d.getOrCreateDynacastQuality(mime)
|
||||
if dq != nil {
|
||||
dq.NotifySubscription(subscriberID, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
for _, codec := range codecs {
|
||||
dq := d.getOrCreateDynacastQuality(mime.NormalizeMimeType(codec.Codec))
|
||||
if dq != nil {
|
||||
dq.NotifySubscriptionNode(nodeID, codec.Enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) OnUpdateAudioCodecForMime(mime mime.MimeType, enabled bool) {
|
||||
d.lock.Lock()
|
||||
if _, ok := d.regressedCodec[mime]; !ok {
|
||||
d.subscribedCodecs[mime] = enabled
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.update(false)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) update(force bool) {
|
||||
d.lock.Lock()
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"processing subscribed codec change",
|
||||
"force", force,
|
||||
"committedSubscribedCodecs", d.committedSubscribedCodecs,
|
||||
"subscribedCodecs", d.subscribedCodecs,
|
||||
)
|
||||
|
||||
if len(d.subscribedCodecs) == 0 {
|
||||
// no mime has been added, nothing to update
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// add or remove of a mime triggers an update
|
||||
changed := len(d.subscribedCodecs) != len(d.committedSubscribedCodecs)
|
||||
if !changed {
|
||||
for mime, enabled := range d.subscribedCodecs {
|
||||
if ce, ok := d.committedSubscribedCodecs[mime]; ok {
|
||||
if ce != enabled {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !force && !changed {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"committing subscribed codec change",
|
||||
"force", force,
|
||||
"committedSubscribedCoecs", d.committedSubscribedCodecs,
|
||||
"subscribedcodecs", d.subscribedCodecs,
|
||||
)
|
||||
|
||||
// commit change
|
||||
d.committedSubscribedCodecs = make(map[mime.MimeType]bool, len(d.subscribedCodecs))
|
||||
for mime, enabled := range d.subscribedCodecs {
|
||||
d.committedSubscribedCodecs[mime] = enabled
|
||||
}
|
||||
|
||||
d.enqueueSubscribedChange()
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerAudio) enqueueSubscribedChange() {
|
||||
if d.isClosed || d.params.Listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
subscribedCodecs := make([]*livekit.SubscribedAudioCodec, 0, len(d.committedSubscribedCodecs))
|
||||
for mime, enabled := range d.committedSubscribedCodecs {
|
||||
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedAudioCodec{
|
||||
Codec: mime.String(),
|
||||
Enabled: enabled,
|
||||
})
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"subscribedAudioCodecChange",
|
||||
"subscribedCodecs", logger.ProtoSlice(subscribedCodecs),
|
||||
)
|
||||
d.notifyOpsQueue.Enqueue(func() {
|
||||
d.params.Listener.OnDynacastSubscribedAudioCodecChange(subscribedCodecs)
|
||||
})
|
||||
}
|
||||
165
pkg/rtc/dynacast/dynacastmanagerbase.go
Normal file
165
pkg/rtc/dynacast/dynacastmanagerbase.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/utils"
|
||||
)
|
||||
|
||||
type dynacastManagerBaseParams struct {
|
||||
Logger logger.Logger
|
||||
OpsQueueDepth uint
|
||||
OnRestart func()
|
||||
OnDynacastQualityCreate func(mimeType mime.MimeType) dynacastQuality
|
||||
OnRegressCodec func(fromMime, toMime mime.MimeType)
|
||||
OnUpdateNeeded func(force bool)
|
||||
}
|
||||
|
||||
type dynacastManagerBase struct {
|
||||
params dynacastManagerBaseParams
|
||||
|
||||
lock sync.RWMutex
|
||||
regressedCodec map[mime.MimeType]struct{}
|
||||
dynacastQuality map[mime.MimeType]dynacastQuality
|
||||
|
||||
notifyOpsQueue *utils.OpsQueue
|
||||
|
||||
isClosed bool
|
||||
|
||||
dynacastManagerNull
|
||||
dynacastQualityListenerNull
|
||||
}
|
||||
|
||||
func newDynacastManagerBase(params dynacastManagerBaseParams) *dynacastManagerBase {
|
||||
if params.OpsQueueDepth == 0 {
|
||||
params.OpsQueueDepth = 4
|
||||
}
|
||||
d := &dynacastManagerBase{
|
||||
params: params,
|
||||
regressedCodec: make(map[mime.MimeType]struct{}),
|
||||
dynacastQuality: make(map[mime.MimeType]dynacastQuality),
|
||||
notifyOpsQueue: utils.NewOpsQueue(utils.OpsQueueParams{
|
||||
Name: "dynacast-notify",
|
||||
MinSize: params.OpsQueueDepth,
|
||||
FlushOnStop: true,
|
||||
Logger: params.Logger,
|
||||
}),
|
||||
}
|
||||
d.notifyOpsQueue.Start()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) AddCodec(mime mime.MimeType) {
|
||||
d.getOrCreateDynacastQuality(mime)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) HandleCodecRegression(fromMime, toMime mime.MimeType) {
|
||||
fromDq := d.getOrCreateDynacastQuality(fromMime)
|
||||
|
||||
d.lock.Lock()
|
||||
if d.isClosed {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if fromDq == nil {
|
||||
// should not happen as we have added the codec on setup receiver
|
||||
d.params.Logger.Warnw("regression from codec not found", nil, "mime", fromMime, "toMime", toMime)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
d.regressedCodec[fromMime] = struct{}{}
|
||||
d.params.OnRegressCodec(fromMime, toMime)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.params.OnUpdateNeeded(false)
|
||||
|
||||
fromDq.Stop()
|
||||
fromDq.RegressTo(d.getOrCreateDynacastQuality(toMime))
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) Restart() {
|
||||
d.lock.Lock()
|
||||
d.params.OnRestart()
|
||||
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.lock.Unlock()
|
||||
|
||||
for _, dq := range dqs {
|
||||
dq.Restart()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) Close() {
|
||||
d.notifyOpsQueue.Stop()
|
||||
|
||||
d.lock.Lock()
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.dynacastQuality = make(map[mime.MimeType]dynacastQuality)
|
||||
|
||||
d.isClosed = true
|
||||
d.lock.Unlock()
|
||||
|
||||
for _, dq := range dqs {
|
||||
dq.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// There are situations like track unmute or streaming from a different node
|
||||
// where subscription changes needs to sent to the provider immediately.
|
||||
// This bypasses any debouncing and forces a subscription change update
|
||||
// with immediate effect.
|
||||
func (d *dynacastManagerBase) ForceUpdate() {
|
||||
d.params.OnUpdateNeeded(true)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
dqs := d.getDynacastQualitiesLocked()
|
||||
d.lock.Unlock()
|
||||
for _, dq := range dqs {
|
||||
dq.ClearSubscriberNodes()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) getOrCreateDynacastQuality(mimeType mime.MimeType) dynacastQuality {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if d.isClosed || mimeType == mime.MimeTypeUnknown {
|
||||
return nil
|
||||
}
|
||||
|
||||
if dq := d.dynacastQuality[mimeType]; dq != nil {
|
||||
return dq
|
||||
}
|
||||
|
||||
dq := d.params.OnDynacastQualityCreate(mimeType)
|
||||
dq.Start()
|
||||
|
||||
d.dynacastQuality[mimeType] = dq
|
||||
return dq
|
||||
}
|
||||
|
||||
func (d *dynacastManagerBase) getDynacastQualitiesLocked() []dynacastQuality {
|
||||
return slices.Collect(maps.Values(d.dynacastQuality))
|
||||
}
|
||||
271
pkg/rtc/dynacast/dynacastmanagervideo.go
Normal file
271
pkg/rtc/dynacast/dynacastmanagervideo.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"time"
|
||||
|
||||
"github.com/bep/debounce"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
var _ DynacastManager = (*dynacastManagerVideo)(nil)
|
||||
var _ dynacastQualityListener = (*dynacastManagerVideo)(nil)
|
||||
|
||||
type DynacastManagerVideoParams struct {
|
||||
DynacastPauseDelay time.Duration
|
||||
Listener DynacastManagerListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
type dynacastManagerVideo struct {
|
||||
params DynacastManagerVideoParams
|
||||
|
||||
maxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
|
||||
committedMaxSubscribedQuality map[mime.MimeType]livekit.VideoQuality
|
||||
|
||||
maxSubscribedQualityDebounce func(func())
|
||||
maxSubscribedQualityDebouncePending bool
|
||||
|
||||
isClosed bool
|
||||
|
||||
*dynacastManagerBase
|
||||
}
|
||||
|
||||
func NewDynacastManagerVideo(params DynacastManagerVideoParams) DynacastManager {
|
||||
if params.Logger == nil {
|
||||
params.Logger = logger.GetLogger()
|
||||
}
|
||||
d := &dynacastManagerVideo{
|
||||
params: params,
|
||||
maxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
|
||||
committedMaxSubscribedQuality: make(map[mime.MimeType]livekit.VideoQuality),
|
||||
}
|
||||
if params.DynacastPauseDelay > 0 {
|
||||
d.maxSubscribedQualityDebounce = debounce.New(params.DynacastPauseDelay)
|
||||
}
|
||||
d.dynacastManagerBase = newDynacastManagerBase(dynacastManagerBaseParams{
|
||||
Logger: params.Logger,
|
||||
OpsQueueDepth: 64,
|
||||
OnRestart: func() {
|
||||
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality)
|
||||
},
|
||||
OnDynacastQualityCreate: func(mimeType mime.MimeType) dynacastQuality {
|
||||
dq := newDynacastQualityVideo(dynacastQualityVideoParams{
|
||||
MimeType: mimeType,
|
||||
Listener: d,
|
||||
Logger: d.params.Logger,
|
||||
})
|
||||
return dq
|
||||
},
|
||||
OnRegressCodec: func(fromMime, toMime mime.MimeType) {
|
||||
d.maxSubscribedQuality[fromMime] = livekit.VideoQuality_OFF
|
||||
|
||||
// if the new codec is not added, notify the publisher to start publishing
|
||||
if _, ok := d.maxSubscribedQuality[toMime]; !ok {
|
||||
d.maxSubscribedQuality[toMime] = livekit.VideoQuality_HIGH
|
||||
}
|
||||
},
|
||||
OnUpdateNeeded: d.update,
|
||||
})
|
||||
return d
|
||||
}
|
||||
|
||||
// It is possible for tracks to be in pending close state. When track
|
||||
// is waiting to be closed, a node is not streaming a track. This can
|
||||
// be used to force an update announcing that subscribed quality is OFF,
|
||||
// i.e. indicating not pulling track any more.
|
||||
func (d *dynacastManagerVideo) ForceQuality(quality livekit.VideoQuality) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
for mime := range d.committedMaxSubscribedQuality {
|
||||
d.committedMaxSubscribedQuality[mime] = quality
|
||||
}
|
||||
|
||||
d.enqueueSubscribedQualityChange()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerVideo) NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
) {
|
||||
dq := d.getOrCreateDynacastQuality(mime)
|
||||
if dq != nil {
|
||||
dq.NotifySubscriberMaxQuality(subscriberID, quality)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerVideo) NotifySubscriberNodeMaxQuality(
|
||||
nodeID livekit.NodeID,
|
||||
qualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
for _, quality := range qualities {
|
||||
dq := d.getOrCreateDynacastQuality(quality.CodecMime)
|
||||
if dq != nil {
|
||||
dq.NotifySubscriberNodeMaxQuality(nodeID, quality.Quality)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastManagerVideo) OnUpdateMaxQualityForMime(
|
||||
mime mime.MimeType,
|
||||
maxQuality livekit.VideoQuality,
|
||||
) {
|
||||
d.lock.Lock()
|
||||
if _, ok := d.regressedCodec[mime]; !ok {
|
||||
d.maxSubscribedQuality[mime] = maxQuality
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.update(false)
|
||||
}
|
||||
|
||||
func (d *dynacastManagerVideo) update(force bool) {
|
||||
d.lock.Lock()
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"processing quality change",
|
||||
"force", force,
|
||||
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
)
|
||||
|
||||
if len(d.maxSubscribedQuality) == 0 {
|
||||
// no mime has been added, nothing to update
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// add or remove of a mime triggers an update
|
||||
changed := len(d.maxSubscribedQuality) != len(d.committedMaxSubscribedQuality)
|
||||
downgradesOnly := !changed
|
||||
if !changed {
|
||||
for mime, quality := range d.maxSubscribedQuality {
|
||||
if cq, ok := d.committedMaxSubscribedQuality[mime]; ok {
|
||||
if cq != quality {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if (cq == livekit.VideoQuality_OFF && quality != livekit.VideoQuality_OFF) || (cq != livekit.VideoQuality_OFF && quality != livekit.VideoQuality_OFF && cq < quality) {
|
||||
downgradesOnly = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !force {
|
||||
if !changed {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if downgradesOnly && d.maxSubscribedQualityDebounce != nil {
|
||||
if !d.maxSubscribedQualityDebouncePending {
|
||||
d.params.Logger.Debugw(
|
||||
"debouncing quality downgrade",
|
||||
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
)
|
||||
d.maxSubscribedQualityDebounce(func() {
|
||||
d.update(true)
|
||||
})
|
||||
d.maxSubscribedQualityDebouncePending = true
|
||||
} else {
|
||||
d.params.Logger.Debugw(
|
||||
"quality downgrade waiting for debounce",
|
||||
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
)
|
||||
}
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// clear debounce on send
|
||||
if d.maxSubscribedQualityDebounce != nil {
|
||||
d.maxSubscribedQualityDebounce(func() {})
|
||||
d.maxSubscribedQualityDebouncePending = false
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"committing quality change",
|
||||
"force", force,
|
||||
"committedMaxSubscribedQuality", d.committedMaxSubscribedQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
)
|
||||
|
||||
// commit change
|
||||
d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality, len(d.maxSubscribedQuality))
|
||||
maps.Copy(d.committedMaxSubscribedQuality, d.maxSubscribedQuality)
|
||||
|
||||
d.enqueueSubscribedQualityChange()
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *dynacastManagerVideo) enqueueSubscribedQualityChange() {
|
||||
if d.isClosed || d.params.Listener == nil {
|
||||
return
|
||||
}
|
||||
|
||||
subscribedCodecs := make([]*livekit.SubscribedCodec, 0, len(d.committedMaxSubscribedQuality))
|
||||
maxSubscribedQualities := make([]types.SubscribedCodecQuality, 0, len(d.committedMaxSubscribedQuality))
|
||||
for mime, quality := range d.committedMaxSubscribedQuality {
|
||||
maxSubscribedQualities = append(maxSubscribedQualities, types.SubscribedCodecQuality{
|
||||
CodecMime: mime,
|
||||
Quality: quality,
|
||||
})
|
||||
|
||||
if quality == livekit.VideoQuality_OFF {
|
||||
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedCodec{
|
||||
Codec: mime.String(),
|
||||
Qualities: []*livekit.SubscribedQuality{
|
||||
{Quality: livekit.VideoQuality_LOW, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_MEDIUM, Enabled: false},
|
||||
{Quality: livekit.VideoQuality_HIGH, Enabled: false},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
var subscribedQualities []*livekit.SubscribedQuality
|
||||
for q := livekit.VideoQuality_LOW; q <= livekit.VideoQuality_HIGH; q++ {
|
||||
subscribedQualities = append(subscribedQualities, &livekit.SubscribedQuality{
|
||||
Quality: q,
|
||||
Enabled: q <= quality,
|
||||
})
|
||||
}
|
||||
subscribedCodecs = append(subscribedCodecs, &livekit.SubscribedCodec{
|
||||
Codec: mime.String(),
|
||||
Qualities: subscribedQualities,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
d.params.Logger.Debugw(
|
||||
"subscribedMaxQualityChange",
|
||||
"subscribedCodecs", subscribedCodecs,
|
||||
"maxSubscribedQualities", maxSubscribedQualities,
|
||||
)
|
||||
d.notifyOpsQueue.Enqueue(func() {
|
||||
d.params.Listener.OnDynacastSubscribedMaxQualityChange(subscribedCodecs, maxSubscribedQualities)
|
||||
})
|
||||
}
|
||||
168
pkg/rtc/dynacast/dynacastqualityaudio.go
Normal file
168
pkg/rtc/dynacast/dynacastqualityaudio.go
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityAudio)(nil)
|
||||
|
||||
type dynacastQualityAudioParams struct {
|
||||
MimeType mime.MimeType
|
||||
Listener dynacastQualityListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
// dynacastQualityAudio manages enable a single receiver of a media track
|
||||
type dynacastQualityAudio struct {
|
||||
params dynacastQualityAudioParams
|
||||
|
||||
// quality level enable/disable
|
||||
lock sync.RWMutex
|
||||
initialized bool
|
||||
subscriberEnables map[livekit.ParticipantID]bool
|
||||
subscriberNodeEnables map[livekit.NodeID]bool
|
||||
enabled bool
|
||||
regressTo dynacastQuality
|
||||
|
||||
dynacastQualityNull
|
||||
}
|
||||
|
||||
func newDynacastQualityAudio(params dynacastQualityAudioParams) dynacastQuality {
|
||||
return &dynacastQualityAudio{
|
||||
params: params,
|
||||
subscriberEnables: make(map[livekit.ParticipantID]bool),
|
||||
subscriberNodeEnables: make(map[livekit.NodeID]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Start() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Restart() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Stop() {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) NotifySubscription(subscriberID livekit.ParticipantID, enabled bool) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber codec enable",
|
||||
"mime", d.params.MimeType,
|
||||
"subscriberID", subscriberID,
|
||||
"enabled", enabled,
|
||||
)
|
||||
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
delete(d.subscriberEnables, subscriberID)
|
||||
} else {
|
||||
d.subscriberEnables[subscriberID] = true
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber node codec enabled",
|
||||
"mime", d.params.MimeType,
|
||||
"subscriberNodeID", nodeID,
|
||||
"enabled", enabled,
|
||||
)
|
||||
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
// the downstream node will synthesize correct enable (its dynacast manager has codec regression), just ignore it
|
||||
d.params.Logger.Debugw(
|
||||
"ignoring node codec change, regressed to another dynacast quality",
|
||||
"mime", d.params.MimeType,
|
||||
"regressedMime", d.regressTo.Mime(),
|
||||
)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
delete(d.subscriberNodeEnables, nodeID)
|
||||
} else {
|
||||
d.subscriberNodeEnables[nodeID] = true
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
d.subscriberNodeEnables = make(map[livekit.NodeID]bool)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) Mime() mime.MimeType {
|
||||
return d.params.MimeType
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) RegressTo(other dynacastQuality) {
|
||||
d.lock.Lock()
|
||||
d.regressTo = other
|
||||
d.lock.Unlock()
|
||||
|
||||
other.Restart()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) reset() {
|
||||
d.lock.Lock()
|
||||
d.initialized = false
|
||||
d.lock.Unlock()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityAudio) updateQualityChange(force bool) {
|
||||
d.lock.Lock()
|
||||
enabled := len(d.subscriberEnables) != 0 || len(d.subscriberNodeEnables) != 0
|
||||
if enabled == d.enabled && d.initialized && !force {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
d.initialized = true
|
||||
d.enabled = enabled
|
||||
d.params.Logger.Debugw(
|
||||
"notifying enabled change",
|
||||
"mime", d.params.MimeType,
|
||||
"enabled", d.enabled,
|
||||
"subscriberNodeEnables", d.subscriberNodeEnables,
|
||||
"subscribedEnables", d.subscriberEnables,
|
||||
"force", force,
|
||||
)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.params.Listener.OnUpdateAudioCodecForMime(d.params.MimeType, enabled)
|
||||
}
|
||||
249
pkg/rtc/dynacast/dynacastqualityvideo.go
Normal file
249
pkg/rtc/dynacast/dynacastqualityvideo.go
Normal file
@@ -0,0 +1,249 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/livekit/protocol/logger"
|
||||
)
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityVideo)(nil)
|
||||
|
||||
const (
|
||||
initialQualityUpdateWait = 10 * time.Second
|
||||
)
|
||||
|
||||
type dynacastQualityVideoParams struct {
|
||||
MimeType mime.MimeType
|
||||
Listener dynacastQualityListener
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
// dynacastQualityVideo manages max subscribed quality of a single receiver of a media track
|
||||
type dynacastQualityVideo struct {
|
||||
params dynacastQualityVideoParams
|
||||
|
||||
// quality level enable/disable
|
||||
lock sync.RWMutex
|
||||
initialized bool
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality
|
||||
maxSubscribedQuality livekit.VideoQuality
|
||||
maxQualityTimer *time.Timer
|
||||
regressTo dynacastQuality
|
||||
|
||||
dynacastQualityNull
|
||||
}
|
||||
|
||||
func newDynacastQualityVideo(params dynacastQualityVideoParams) dynacastQuality {
|
||||
return &dynacastQualityVideo{
|
||||
params: params,
|
||||
maxSubscriberQuality: make(map[livekit.ParticipantID]livekit.VideoQuality),
|
||||
maxSubscriberNodeQuality: make(map[livekit.NodeID]livekit.VideoQuality),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Start() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Restart() {
|
||||
d.reset()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Stop() {
|
||||
d.stopMaxQualityTimer()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber max quality",
|
||||
"mime", d.params.MimeType,
|
||||
"subscriberID", subscriberID,
|
||||
"quality", quality.String(),
|
||||
)
|
||||
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
d.lock.Unlock()
|
||||
r.NotifySubscriberMaxQuality(subscriberID, quality)
|
||||
return
|
||||
}
|
||||
|
||||
if quality == livekit.VideoQuality_OFF {
|
||||
delete(d.maxSubscriberQuality, subscriberID)
|
||||
} else {
|
||||
d.maxSubscriberQuality[subscriberID] = quality
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
|
||||
d.params.Logger.Debugw(
|
||||
"setting subscriber node max quality",
|
||||
"mime", d.params.MimeType,
|
||||
"subscriberNodeID", nodeID,
|
||||
"quality", quality.String(),
|
||||
)
|
||||
|
||||
d.lock.Lock()
|
||||
if r := d.regressTo; r != nil {
|
||||
// the downstream node will synthesize correct quality notify (its dynacast manager has codec regression), just ignore it
|
||||
d.params.Logger.Debugw(
|
||||
"ignoring node quality change, regressed to another dynacast quality",
|
||||
"mime", d.params.MimeType,
|
||||
"regressedMime", d.regressTo.Mime(),
|
||||
)
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if quality == livekit.VideoQuality_OFF {
|
||||
delete(d.maxSubscriberNodeQuality, nodeID)
|
||||
} else {
|
||||
d.maxSubscriberNodeQuality[nodeID] = quality
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) ClearSubscriberNodes() {
|
||||
d.lock.Lock()
|
||||
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.updateQualityChange(false)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Mime() mime.MimeType {
|
||||
return d.params.MimeType
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) RegressTo(other dynacastQuality) {
|
||||
d.lock.Lock()
|
||||
d.regressTo = other
|
||||
maxSubscriberQuality := d.maxSubscriberQuality
|
||||
maxSubscriberNodeQuality := d.maxSubscriberNodeQuality
|
||||
d.maxSubscriberQuality = make(map[livekit.ParticipantID]livekit.VideoQuality)
|
||||
d.maxSubscriberNodeQuality = make(map[livekit.NodeID]livekit.VideoQuality)
|
||||
d.lock.Unlock()
|
||||
|
||||
other.Replace(maxSubscriberQuality, maxSubscriberNodeQuality)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
) {
|
||||
d.lock.Lock()
|
||||
for subID, quality := range maxSubscriberQuality {
|
||||
if oldQuality, ok := d.maxSubscriberQuality[subID]; ok {
|
||||
// no QUALITY_OFF in the map
|
||||
if quality > oldQuality {
|
||||
d.maxSubscriberQuality[subID] = quality
|
||||
}
|
||||
} else {
|
||||
d.maxSubscriberQuality[subID] = quality
|
||||
}
|
||||
}
|
||||
|
||||
for nodeID, quality := range maxSubscriberNodeQuality {
|
||||
if oldQuality, ok := d.maxSubscriberNodeQuality[nodeID]; ok {
|
||||
// no QUALITY_OFF in the map
|
||||
if quality > oldQuality {
|
||||
d.maxSubscriberNodeQuality[nodeID] = quality
|
||||
}
|
||||
} else {
|
||||
d.maxSubscriberNodeQuality[nodeID] = quality
|
||||
}
|
||||
}
|
||||
d.lock.Unlock()
|
||||
|
||||
d.Restart()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) reset() {
|
||||
d.lock.Lock()
|
||||
d.initialized = false
|
||||
d.lock.Unlock()
|
||||
|
||||
d.startMaxQualityTimer()
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) updateQualityChange(force bool) {
|
||||
d.lock.Lock()
|
||||
maxSubscribedQuality := livekit.VideoQuality_OFF
|
||||
for _, subQuality := range d.maxSubscriberQuality {
|
||||
if maxSubscribedQuality == livekit.VideoQuality_OFF || (subQuality != livekit.VideoQuality_OFF && subQuality > maxSubscribedQuality) {
|
||||
maxSubscribedQuality = subQuality
|
||||
}
|
||||
}
|
||||
for _, nodeQuality := range d.maxSubscriberNodeQuality {
|
||||
if maxSubscribedQuality == livekit.VideoQuality_OFF || (nodeQuality != livekit.VideoQuality_OFF && nodeQuality > maxSubscribedQuality) {
|
||||
maxSubscribedQuality = nodeQuality
|
||||
}
|
||||
}
|
||||
|
||||
if maxSubscribedQuality == d.maxSubscribedQuality && d.initialized && !force {
|
||||
d.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
d.initialized = true
|
||||
d.maxSubscribedQuality = maxSubscribedQuality
|
||||
d.params.Logger.Debugw(
|
||||
"notifying quality change",
|
||||
"mime", d.params.MimeType,
|
||||
"maxSubscriberQuality", d.maxSubscriberQuality,
|
||||
"maxSubscriberNodeQuality", d.maxSubscriberNodeQuality,
|
||||
"maxSubscribedQuality", d.maxSubscribedQuality,
|
||||
"force", force,
|
||||
)
|
||||
d.lock.Unlock()
|
||||
|
||||
d.params.Listener.OnUpdateMaxQualityForMime(d.params.MimeType, maxSubscribedQuality)
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) startMaxQualityTimer() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if d.maxQualityTimer != nil {
|
||||
d.maxQualityTimer.Stop()
|
||||
d.maxQualityTimer = nil
|
||||
}
|
||||
|
||||
d.maxQualityTimer = time.AfterFunc(initialQualityUpdateWait, func() {
|
||||
d.stopMaxQualityTimer()
|
||||
d.updateQualityChange(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (d *dynacastQualityVideo) stopMaxQualityTimer() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
if d.maxQualityTimer != nil {
|
||||
d.maxQualityTimer.Stop()
|
||||
d.maxQualityTimer = nil
|
||||
}
|
||||
}
|
||||
185
pkg/rtc/dynacast/interfaces.go
Normal file
185
pkg/rtc/dynacast/interfaces.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright 2023 LiveKit, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dynacast
|
||||
|
||||
import (
|
||||
"github.com/livekit/protocol/codecs/mime"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
|
||||
"github.com/livekit/livekit-server/pkg/rtc/types"
|
||||
)
|
||||
|
||||
type DynacastManagerListener interface {
|
||||
OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
)
|
||||
|
||||
OnDynacastSubscribedAudioCodecChange(codecs []*livekit.SubscribedAudioCodec)
|
||||
}
|
||||
|
||||
var _ DynacastManagerListener = (*DynacastManagerListenerNull)(nil)
|
||||
|
||||
type DynacastManagerListenerNull struct {
|
||||
}
|
||||
|
||||
func (d *DynacastManagerListenerNull) OnDynacastSubscribedMaxQualityChange(
|
||||
subscribedQualities []*livekit.SubscribedCodec,
|
||||
maxSubscribedQualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
}
|
||||
func (d *DynacastManagerListenerNull) OnDynacastSubscribedAudioCodecChange(
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
type DynacastManager interface {
|
||||
AddCodec(mime mime.MimeType)
|
||||
HandleCodecRegression(fromMime, toMime mime.MimeType)
|
||||
Restart()
|
||||
Close()
|
||||
ForceUpdate()
|
||||
ForceQuality(quality livekit.VideoQuality)
|
||||
ForceEnable(enabled bool)
|
||||
|
||||
NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
)
|
||||
NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
)
|
||||
|
||||
NotifySubscriberNodeMaxQuality(
|
||||
nodeID livekit.NodeID,
|
||||
qualities []types.SubscribedCodecQuality,
|
||||
)
|
||||
NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
)
|
||||
ClearSubscriberNodes()
|
||||
}
|
||||
|
||||
var _ DynacastManager = (*dynacastManagerNull)(nil)
|
||||
|
||||
type dynacastManagerNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastManagerNull) AddCodec(mime mime.MimeType) {}
|
||||
func (d *dynacastManagerNull) HandleCodecRegression(fromMime, toMime mime.MimeType) {}
|
||||
func (d *dynacastManagerNull) Restart() {}
|
||||
func (d *dynacastManagerNull) Close() {}
|
||||
func (d *dynacastManagerNull) ForceUpdate() {}
|
||||
func (d *dynacastManagerNull) ForceQuality(quality livekit.VideoQuality) {}
|
||||
func (d *dynacastManagerNull) ForceEnable(enabled bool) {}
|
||||
func (d *dynacastManagerNull) NotifySubscriberMaxQuality(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
quality livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscription(
|
||||
subscriberID livekit.ParticipantID,
|
||||
mime mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscriberNodeMaxQuality(
|
||||
nodeID livekit.NodeID,
|
||||
qualities []types.SubscribedCodecQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) NotifySubscriptionNode(
|
||||
nodeID livekit.NodeID,
|
||||
codecs []*livekit.SubscribedAudioCodec,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastManagerNull) ClearSubscriberNodes() {}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type dynacastQualityListener interface {
|
||||
OnUpdateMaxQualityForMime(mimeType mime.MimeType, maxQuality livekit.VideoQuality)
|
||||
OnUpdateAudioCodecForMime(mimeType mime.MimeType, enabled bool)
|
||||
}
|
||||
|
||||
var _ dynacastQualityListener = (*dynacastQualityListenerNull)(nil)
|
||||
|
||||
type dynacastQualityListenerNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityListenerNull) OnUpdateMaxQualityForMime(
|
||||
mimeType mime.MimeType,
|
||||
maxQuality livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityListenerNull) OnUpdateAudioCodecForMime(
|
||||
mimeType mime.MimeType,
|
||||
enabled bool,
|
||||
) {
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
|
||||
type dynacastQuality interface {
|
||||
Start()
|
||||
Restart()
|
||||
Stop()
|
||||
|
||||
NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality)
|
||||
NotifySubscription(subscriberID livekit.ParticipantID, enabled bool)
|
||||
|
||||
NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality)
|
||||
NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool)
|
||||
ClearSubscriberNodes()
|
||||
|
||||
Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
)
|
||||
|
||||
Mime() mime.MimeType
|
||||
RegressTo(other dynacastQuality)
|
||||
}
|
||||
|
||||
var _ dynacastQuality = (*dynacastQualityNull)(nil)
|
||||
|
||||
type dynacastQualityNull struct {
|
||||
}
|
||||
|
||||
func (d *dynacastQualityNull) Start() {}
|
||||
func (d *dynacastQualityNull) Restart() {}
|
||||
func (d *dynacastQualityNull) Stop() {}
|
||||
func (d *dynacastQualityNull) NotifySubscriberMaxQuality(subscriberID livekit.ParticipantID, quality livekit.VideoQuality) {
|
||||
}
|
||||
func (d *dynacastQualityNull) NotifySubscription(subscriberID livekit.ParticipantID, enabled bool) {}
|
||||
func (d *dynacastQualityNull) NotifySubscriberNodeMaxQuality(nodeID livekit.NodeID, quality livekit.VideoQuality) {
|
||||
}
|
||||
func (d *dynacastQualityNull) NotifySubscriptionNode(nodeID livekit.NodeID, enabled bool) {}
|
||||
func (d *dynacastQualityNull) ClearSubscriberNodes() {}
|
||||
func (d *dynacastQualityNull) Replace(
|
||||
maxSubscriberQuality map[livekit.ParticipantID]livekit.VideoQuality,
|
||||
maxSubscriberNodeQuality map[livekit.NodeID]livekit.VideoQuality,
|
||||
) {
|
||||
}
|
||||
func (d *dynacastQualityNull) Mime() mime.MimeType { return mime.MimeTypeUnknown }
|
||||
func (d *dynacastQualityNull) RegressTo(other dynacastQuality) {}
|
||||
Reference in New Issue
Block a user