diff --git a/jvb/src/main/java/org/jitsi/videobridge/cc/allocation/BandwidthAllocator.java b/jvb/src/main/java/org/jitsi/videobridge/cc/allocation/BandwidthAllocator.java index ecab83bd49..f5ae205c69 100644 --- a/jvb/src/main/java/org/jitsi/videobridge/cc/allocation/BandwidthAllocator.java +++ b/jvb/src/main/java/org/jitsi/videobridge/cc/allocation/BandwidthAllocator.java @@ -43,14 +43,14 @@ public class BandwidthAllocator { /** - * Returns a boolean that indicates whether or not the current bandwidth estimation (in bps) has changed above the + * Returns a boolean that indicates whether the current bandwidth estimation (in bps) has changed above the * configured threshold with respect to the previous bandwidth estimation. * * @param previousBwe the previous bandwidth estimation (in bps). * @param currentBwe the current bandwidth estimation (in bps). * @return true if the bandwidth has changed above the configured threshold, * false otherwise. */ - private static boolean bweChangeIsLargerThanThreshold(long previousBwe, long currentBwe) + private boolean bweChangeIsLargerThanThreshold(long previousBwe, long currentBwe) { if (previousBwe == -1 || currentBwe == -1) { @@ -65,7 +65,7 @@ private static boolean bweChangeIsLargerThanThreshold(long previousBwe, long cur // In any case, there are other triggers for re-allocation, so any suppression we do here will only last up to // a few seconds. long deltaBwe = Math.abs(currentBwe - previousBwe); - return deltaBwe > previousBwe * BitrateControllerConfig.bweChangeThreshold(); + return deltaBwe > previousBwe * config.bweChangeThreshold(); // If, on the other hand, the bwe has decreased, we require at least a 15% drop in order to update the bitrate // allocation. This is an ugly hack to prevent too many resolution/UI changes in case the bridge produces too @@ -107,10 +107,13 @@ private static boolean bweChangeIsLargerThanThreshold(long previousBwe, long cur */ private final Supplier trustBwe; + private final BitrateControllerConfig config = new BitrateControllerConfig(); + /** * The allocations settings signalled by the receiver. */ - private AllocationSettings allocationSettings = new AllocationSettings(); + private AllocationSettings allocationSettings + = new AllocationSettings(new VideoConstraints(config.thumbnailMaxHeightPx())); /** * The last time {@link BandwidthAllocator#update()} was called. @@ -384,7 +387,8 @@ public boolean hasNonZeroEffectiveConstraints(String endpointId) effectiveConstraints.get(endpoint.getId()), allocationSettings.getOnStageEndpoints().contains(endpoint.getId()), diagnosticContext, - clock)); + clock, + config)); } } @@ -396,8 +400,7 @@ public boolean hasNonZeroEffectiveConstraints(String endpointId) */ void maybeUpdate() { - if (Duration.between(lastUpdateTime, clock.instant()) - .compareTo(BitrateControllerConfig.maxTimeBetweenCalculations()) > 0) + if (Duration.between(lastUpdateTime, clock.instant()).compareTo(config.maxTimeBetweenCalculations()) > 0) { logger.debug("Forcing an update"); TaskPools.CPU_POOL.execute(this::update); diff --git a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/AllocationSettings.kt b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/AllocationSettings.kt index 67aa7902dc..9b36cc2b07 100644 --- a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/AllocationSettings.kt +++ b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/AllocationSettings.kt @@ -16,20 +16,20 @@ package org.jitsi.videobridge.cc.allocation import org.jitsi.utils.OrderedJsonObject +import org.jitsi.videobridge.cc.config.BitrateControllerConfig import org.jitsi.videobridge.message.ReceiverVideoConstraintsMessage import java.util.stream.Collectors import kotlin.math.min -import org.jitsi.videobridge.cc.config.BitrateControllerConfig as config /** * This class encapsulates all of the client-controlled settings for bandwidth allocation. */ -data class AllocationSettings( +data class AllocationSettings @JvmOverloads constructor( val onStageEndpoints: List = emptyList(), val selectedEndpoints: List = emptyList(), val videoConstraints: Map = emptyMap(), val lastN: Int = -1, - val defaultConstraints: VideoConstraints = VideoConstraints(config.thumbnailMaxHeightPx()) + val defaultConstraints: VideoConstraints ) { fun toJson() = OrderedJsonObject().apply { @@ -64,6 +64,8 @@ internal class AllocationSettingsWrapper { private var videoConstraints: Map = emptyMap() + private val config = BitrateControllerConfig() + private var defaultConstraints: VideoConstraints = VideoConstraints(config.thumbnailMaxHeightPx()) private var onStageEndpoints: List = emptyList() diff --git a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/BitrateController.kt b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/BitrateController.kt index 026fd7554f..3931a2e546 100644 --- a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/BitrateController.kt +++ b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/BitrateController.kt @@ -59,6 +59,8 @@ class BitrateController @JvmOverloads constructor( */ private var forwardedEndpoints: Set = emptySet() + private val config = BitrateControllerConfig() + /** * Keep track of how much time we spend knowingly oversending (due to enableOnstageVideoSuspend being false) */ @@ -104,7 +106,7 @@ class BitrateController @JvmOverloads constructor( * TODO: Is this comment still accurate? */ private val trustBwe: Boolean - get() = BitrateControllerConfig.trustBwe() && supportsRtx && packetHandler.timeSinceFirstMedia() >= 10000 + get() = config.trustBwe() && supportsRtx && packetHandler.timeSinceFirstMedia() >= 10000 // Proxy to the allocator fun endpointOrderingChanged() = bandwidthAllocator.update() diff --git a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/SingleSourceAllocation.kt b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/SingleSourceAllocation.kt index f83d43981c..a85efb9da4 100644 --- a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/SingleSourceAllocation.kt +++ b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/SingleSourceAllocation.kt @@ -21,8 +21,6 @@ import org.jitsi.nlj.VideoType import org.jitsi.utils.logging.DiagnosticContext import org.jitsi.utils.logging.TimeSeriesLogger import org.jitsi.videobridge.cc.config.BitrateControllerConfig -import org.jitsi.videobridge.cc.config.BitrateControllerConfig.Companion.onstagePreferredFramerate -import org.jitsi.videobridge.cc.config.BitrateControllerConfig.Companion.onstagePreferredHeightPx import java.lang.Integer.max import java.time.Clock @@ -39,7 +37,8 @@ internal class SingleSourceAllocation( /** Whether the endpoint is on stage. */ private val onStage: Boolean, diagnosticContext: DiagnosticContext, - clock: Clock + clock: Clock, + val config: BitrateControllerConfig = BitrateControllerConfig() ) { /** * The immutable list of layers to be considered when allocating bandwidth. @@ -119,7 +118,7 @@ internal class SingleSourceAllocation( // `maxOversendBitrate`. if (allowOversending && layers.oversendIndex >= 0 && targetIdx < layers.oversendIndex) { for (i in layers.oversendIndex downTo targetIdx + 1) { - if (layers[i].bitrate <= maxBps + BitrateControllerConfig.maxOversendBitrateBps()) { + if (layers[i].bitrate <= maxBps + config.maxOversendBitrateBps()) { targetIdx = i } } @@ -188,6 +187,138 @@ internal class SingleSourceAllocation( ) } + /** + * Gets the "preferred" height and frame rate based on the constraints signaled from the receiver. + * + * For participants with sufficient maxHeight we favor frame rate over resolution. We consider all + * temporal layers for resolutions lower than the preferred, but for resolutions >= preferred, we only + * consider frame rates at least as high as the preferred. In practice this means we consider + * 180p/7.5fps, 180p/15fps, 180p/30fps, 360p/30fps and 720p/30fps. + */ + private fun getPreferred(constraints: VideoConstraints): Pair { + return if (constraints.maxHeight > 180) { + Pair(config.onstagePreferredHeightPx(), config.onstagePreferredFramerate()) + } else { + noPreferredHeightAndFrameRate + } + } + + /** + * Selects from a list of layers the ones which should be considered when allocating bandwidth, as well as the + * "preferred" and "oversend" layers. Logic specific to screensharing: we prioritize resolution over framerate, + * prioritize the highest layer over other endpoints (by setting the highest layer as "preferred"), and allow + * oversending up to the highest resolution (with low frame rate). + */ + private fun selectLayersForScreensharing( + layers: List, + constraints: VideoConstraints, + onStage: Boolean + ): Layers { + + var activeLayers = layers.filter { it.bitrate > 0 } + // No active layers usually happens when the source has just been signaled and we haven't received + // any packets yet. Add the layers here, so one gets selected and we can start forwarding sooner. + if (activeLayers.isEmpty()) activeLayers = layers + + // We select all layers that satisfy the constraints. + var selectedLayers = + if (constraints.maxHeight < 0) { + activeLayers + } else { + activeLayers.filter { it.layer.height <= constraints.maxHeight } + } + // If no layers satisfy the constraints, we use the layers with the lowest resolution. + if (selectedLayers.isEmpty()) { + val minHeight = activeLayers.map { it.layer.height }.minOrNull() ?: return Layers.noLayers + selectedLayers = activeLayers.filter { it.layer.height == minHeight } + + // This recognizes the structure used with VP9 (multiple encodings with the same resolution and unknown frame + // rate). In this case, we only want the low quality layer. + if (selectedLayers.isNotEmpty() && selectedLayers[0].layer.frameRate < 0) { + selectedLayers = listOf(selectedLayers[0]) + } + } + + val oversendIdx = if (onStage && config.allowOversendOnStage()) { + val maxHeight = selectedLayers.map { it.layer.height }.maxOrNull() ?: return Layers.noLayers + selectedLayers.firstIndexWhich { it.layer.height == maxHeight } + } else { + -1 + } + return Layers(selectedLayers, selectedLayers.size - 1, oversendIdx) + } + + /** + * Selects from the layers of a [MediaSourceContainer] the ones which should be considered when allocating bandwidth for + * an endpoint. Also selects the indices of the "preferred" and "oversend" layers. + * + * @param endpoint the [MediaSourceContainer] that describes the available layers. + * @param constraints the constraints signaled for the endpoint. + * @return the ordered list of [endpoint]'s layers which should be considered when allocating bandwidth, as well as the + * indices of the "preferred" and "oversend" layers. + */ + private fun selectLayers( + /** The endpoint which is the source of the stream(s). */ + endpoint: MediaSourceContainer, + onStage: Boolean, + /** The constraints that the receiver specified for [endpoint]. */ + constraints: VideoConstraints, + nowMs: Long + ): Layers { + val source = endpoint.mediaSource + if (constraints.maxHeight <= 0 || source == null || !source.hasRtpLayers()) { + return Layers.noLayers + } + val layers = source.rtpLayers.map { LayerSnapshot(it, it.getBitrateBps(nowMs)) } + + return when (endpoint.videoType) { + VideoType.CAMERA -> selectLayersForCamera(layers, constraints) + VideoType.DESKTOP, VideoType.DESKTOP_HIGH_FPS -> selectLayersForScreensharing(layers, constraints, onStage) + else -> Layers.noLayers + } + } + + /** + * Selects from a list of layers the ones which should be considered when allocating bandwidth, as well as the + * "preferred" and "oversend" layers. Logic specific to a camera stream: once the "preferred" height is reached we + * require a high frame rate, with preconfigured values for the "preferred" height and frame rate, and we do not allow + * oversending. + */ + private fun selectLayersForCamera( + layers: List, + constraints: VideoConstraints, + ): Layers { + + val minHeight = layers.map { it.layer.height }.minOrNull() ?: return Layers.noLayers + val noActiveLayers = layers.none { (_, bitrate) -> bitrate > 0 } + val (preferredHeight, preferredFps) = getPreferred(constraints) + + val ratesList: MutableList = ArrayList() + // Initialize the list of layers to be considered. These are the layers that satisfy the constraints, with + // a couple of exceptions (see comments below). + for (layerSnapshot in layers) { + val layer = layerSnapshot.layer + val lessThanPreferredHeight = layer.height < preferredHeight + val lessThanOrEqualMaxHeight = layer.height <= constraints.maxHeight + // If frame rate is unknown, consider it to be sufficient. + val atLeastPreferredFps = layer.frameRate < 0 || layer.frameRate >= preferredFps + if (lessThanPreferredHeight || + (lessThanOrEqualMaxHeight && atLeastPreferredFps) || + layer.height == minHeight + ) { + // No active layers usually happens when the source has just been signaled and we haven't received + // any packets yet. Add the layers here, so one gets selected and we can start forwarding sooner. + if (noActiveLayers || layerSnapshot.bitrate > 0) { + ratesList.add(layerSnapshot) + } + } + } + + val effectivePreferredHeight = max(preferredHeight, minHeight) + val preferredIndex = ratesList.lastIndexWhich { it.layer.height <= effectivePreferredHeight } + return Layers(ratesList, preferredIndex, -1) + } + companion object { private val timeSeriesLogger = TimeSeriesLogger.getTimeSeriesLogger(BandwidthAllocator::class.java) } @@ -220,99 +351,8 @@ data class Layers( } } -/** - * Gets the "preferred" height and frame rate based on the constraints signaled from the receiver. - * - * For participants with sufficient maxHeight we favor frame rate over resolution. We consider all - * temporal layers for resolutions lower than the preferred, but for resolutions >= preferred, we only - * consider frame rates at least as high as the preferred. In practice this means we consider - * 180p/7.5fps, 180p/15fps, 180p/30fps, 360p/30fps and 720p/30fps. - */ -private fun getPreferred(constraints: VideoConstraints): Pair { - return if (constraints.maxHeight > 180) { - Pair(onstagePreferredHeightPx(), onstagePreferredFramerate()) - } else { - noPreferredHeightAndFrameRate - } -} - private val noPreferredHeightAndFrameRate = Pair(-1, -1.0) -/** - * Selects from the layers of a [MediaSourceContainer] the ones which should be considered when allocating bandwidth for - * an endpoint. Also selects the indices of the "preferred" and "oversend" layers. - * - * @param endpoint the [MediaSourceContainer] that describes the available layers. - * @param constraints the constraints signaled for the endpoint. - * @return the ordered list of [endpoint]'s layers which should be considered when allocating bandwidth, as well as the - * indices of the "preferred" and "oversend" layers. - */ -private fun selectLayers( - /** The endpoint which is the source of the stream(s). */ - endpoint: MediaSourceContainer, - onStage: Boolean, - /** The constraints that the receiver specified for [endpoint]. */ - constraints: VideoConstraints, - nowMs: Long -): Layers { - val source = endpoint.mediaSource - if (constraints.maxHeight <= 0 || source == null || !source.hasRtpLayers()) { - return Layers.noLayers - } - val layers = source.rtpLayers.map { LayerSnapshot(it, it.getBitrateBps(nowMs)) } - - return when (endpoint.videoType) { - VideoType.CAMERA -> selectLayersForCamera(layers, constraints) - VideoType.DESKTOP, VideoType.DESKTOP_HIGH_FPS -> selectLayersForScreensharing(layers, constraints, onStage) - else -> Layers.noLayers - } -} - -/** - * Selects from a list of layers the ones which should be considered when allocating bandwidth, as well as the - * "preferred" and "oversend" layers. Logic specific to screensharing: we prioritize resolution over framerate, - * prioritize the highest layer over other endpoints (by setting the highest layer as "preferred"), and allow - * oversending up to the highest resolution (with low frame rate). - */ -private fun selectLayersForScreensharing( - layers: List, - constraints: VideoConstraints, - onStage: Boolean -): Layers { - - var activeLayers = layers.filter { it.bitrate > 0 } - // No active layers usually happens when the source has just been signaled and we haven't received - // any packets yet. Add the layers here, so one gets selected and we can start forwarding sooner. - if (activeLayers.isEmpty()) activeLayers = layers - - // We select all layers that satisfy the constraints. - var selectedLayers = - if (constraints.maxHeight < 0) { - activeLayers - } else { - activeLayers.filter { it.layer.height <= constraints.maxHeight } - } - // If no layers satisfy the constraints, we use the layers with the lowest resolution. - if (selectedLayers.isEmpty()) { - val minHeight = activeLayers.map { it.layer.height }.minOrNull() ?: return Layers.noLayers - selectedLayers = activeLayers.filter { it.layer.height == minHeight } - - // This recognizes the structure used with VP9 (multiple encodings with the same resolution and unknown frame - // rate). In this case, we only want the low quality layer. - if (selectedLayers.isNotEmpty() && selectedLayers[0].layer.frameRate < 0) { - selectedLayers = listOf(selectedLayers[0]) - } - } - - val oversendIdx = if (onStage && BitrateControllerConfig.allowOversendOnStage()) { - val maxHeight = selectedLayers.map { it.layer.height }.maxOrNull() ?: return Layers.noLayers - selectedLayers.firstIndexWhich { it.layer.height == maxHeight } - } else { - -1 - } - return Layers(selectedLayers, selectedLayers.size - 1, oversendIdx) -} - /** Return the index of the first item in the list which satisfies a predicate, or -1 if none do. */ private fun List.firstIndexWhich(predicate: (T) -> Boolean): Int { forEachIndexed { index, item -> @@ -321,47 +361,6 @@ private fun List.firstIndexWhich(predicate: (T) -> Boolean): Int { return -1 } -/** - * Selects from a list of layers the ones which should be considered when allocating bandwidth, as well as the - * "preferred" and "oversend" layers. Logic specific to a camera stream: once the "preferred" height is reached we - * require a high frame rate, with preconfigured values for the "preferred" height and frame rate, and we do not allow - * oversending. - */ -private fun selectLayersForCamera( - layers: List, - constraints: VideoConstraints, -): Layers { - - val minHeight = layers.map { it.layer.height }.minOrNull() ?: return Layers.noLayers - val noActiveLayers = layers.none { (_, bitrate) -> bitrate > 0 } - val (preferredHeight, preferredFps) = getPreferred(constraints) - - val ratesList: MutableList = ArrayList() - // Initialize the list of layers to be considered. These are the layers that satisfy the constraints, with - // a couple of exceptions (see comments below). - for (layerSnapshot in layers) { - val layer = layerSnapshot.layer - val lessThanPreferredHeight = layer.height < preferredHeight - val lessThanOrEqualMaxHeight = layer.height <= constraints.maxHeight - // If frame rate is unknown, consider it to be sufficient. - val atLeastPreferredFps = layer.frameRate < 0 || layer.frameRate >= preferredFps - if (lessThanPreferredHeight || - (lessThanOrEqualMaxHeight && atLeastPreferredFps) || - layer.height == minHeight - ) { - // No active layers usually happens when the source has just been signaled and we haven't received - // any packets yet. Add the layers here, so one gets selected and we can start forwarding sooner. - if (noActiveLayers || layerSnapshot.bitrate > 0) { - ratesList.add(layerSnapshot) - } - } - } - - val effectivePreferredHeight = max(preferredHeight, minHeight) - val preferredIndex = ratesList.lastIndexWhich { it.layer.height <= effectivePreferredHeight } - return Layers(ratesList, preferredIndex, -1) -} - /** * Returns the index of the last element of this list which satisfies the given predicate, or -1 if no elements do. */ diff --git a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/VideoConstraints.kt b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/VideoConstraints.kt index 2a3296962e..7b6edb8637 100644 --- a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/VideoConstraints.kt +++ b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/VideoConstraints.kt @@ -19,7 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties import org.json.simple.JSONObject @JsonIgnoreProperties(ignoreUnknown = true) -data class VideoConstraints( +data class VideoConstraints @JvmOverloads constructor( val maxHeight: Int, val maxFrameRate: Double = -1.0 ) { diff --git a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/config/BitrateControllerConfig.kt b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/config/BitrateControllerConfig.kt index 10696c06ca..f1d5e7b8de 100644 --- a/jvb/src/main/kotlin/org/jitsi/videobridge/cc/config/BitrateControllerConfig.kt +++ b/jvb/src/main/kotlin/org/jitsi/videobridge/cc/config/BitrateControllerConfig.kt @@ -23,116 +23,96 @@ import org.jitsi.nlj.util.Bandwidth import java.time.Duration class BitrateControllerConfig { - companion object { - /** - * The bandwidth estimation threshold. - * - * In order to limit the resolution changes due to bandwidth changes we only react to bandwidth changes greater - * than {@code bweChangeThreshold * last_bandwidth_estimation}. - */ - private val bweChangeThreshold: Double by config { - "org.jitsi.videobridge.BWE_CHANGE_THRESHOLD_PCT".from(JitsiConfig.legacyConfig) - .transformedBy { it / 100.0 } - // This is an old version, include for backward compat. - "videobridge.cc.bwe-change-threshold-pct".from(JitsiConfig.newConfig) - .transformedBy { it / 100.0 } - "videobridge.cc.bwe-change-threshold".from(JitsiConfig.newConfig) - } - - @JvmStatic - fun bweChangeThreshold() = bweChangeThreshold - - /** - * The max resolution to allocate for the thumbnails. - */ - private val thumbnailMaxHeightPx: Int by config { - "org.jitsi.videobridge.THUMBNAIL_MAX_HEIGHT".from(JitsiConfig.legacyConfig) - "videobridge.cc.thumbnail-max-height-px".from(JitsiConfig.newConfig) - } - - @JvmStatic - fun thumbnailMaxHeightPx() = thumbnailMaxHeightPx - - /** - * The default preferred resolution to allocate for the onstage participant, - * before allocating bandwidth for the thumbnails. - */ - private val onstagePreferredHeightPx: Int by config { - "org.jitsi.videobridge.ONSTAGE_PREFERRED_HEIGHT".from(JitsiConfig.legacyConfig) - "videobridge.cc.onstage-preferred-height-px".from(JitsiConfig.newConfig) - } - - @JvmStatic - fun onstagePreferredHeightPx() = onstagePreferredHeightPx - - /** - * The preferred frame rate to allocate for the onstage participant. - */ - private val onstagePreferredFramerate: Double by config { - "org.jitsi.videobridge.ONSTAGE_PREFERRED_FRAME_RATE".from(JitsiConfig.legacyConfig) - "videobridge.cc.onstage-preferred-framerate".from(JitsiConfig.newConfig) - } - - @JvmStatic - fun onstagePreferredFramerate() = onstagePreferredFramerate - - /** - * Whether or not we are allowed to oversend (exceed available bandwidth) for the video of the on-stage - * participant. - */ - private val allowOversendOnStage: Boolean by config { - "org.jitsi.videobridge.ENABLE_ONSTAGE_VIDEO_SUSPEND".from(JitsiConfig.legacyConfig).transformedBy { !it } - "videobridge.cc.enable-onstage-video-suspend".from(JitsiConfig.newConfig).transformedBy { !it } - "videobridge.cc.allow-oversend-onstage".from(JitsiConfig.newConfig) - } - - @JvmStatic - fun allowOversendOnStage(): Boolean = allowOversendOnStage - - /** - * The maximum bitrate by which the bridge may exceed the estimated available bandwidth when oversending. - */ - private val maxOversendBitrate: Bandwidth by config { - "videobridge.cc.max-oversend-bitrate".from(JitsiConfig.newConfig) - .convertFrom { Bandwidth.fromString(it) } - } - - @JvmStatic - fun maxOversendBitrateBps(): Double = maxOversendBitrate.bps - - /** - * Whether or not we should trust the bandwidth - * estimations. If this is se to false, then we assume a bandwidth - * estimation of Long.MAX_VALUE. - */ - private val trustBwe: Boolean by config { - "org.jitsi.videobridge.TRUST_BWE".from(JitsiConfig.legacyConfig) - "videobridge.cc.trust-bwe".from(JitsiConfig.newConfig) - } - - @JvmStatic - fun trustBwe(): Boolean = trustBwe - - /** - * The property for the max resolution to allocate for the onstage - * participant. - */ - private val onstageIdealHeightPx: Int by config( - "videobridge.cc.onstage-ideal-height-px".from(JitsiConfig.newConfig) - ) - - @JvmStatic - fun onstageIdealHeightPx() = onstageIdealHeightPx - - /** - * The maximum amount of time we'll run before recalculating which streams we'll - * forward. - */ - private val maxTimeBetweenCalculations: Duration by config( - "videobridge.cc.max-time-between-calculations".from(JitsiConfig.newConfig) - ) - - @JvmStatic - fun maxTimeBetweenCalculations() = maxTimeBetweenCalculations + /** + * The bandwidth estimation threshold. + * + * In order to limit the resolution changes due to bandwidth changes we only react to bandwidth changes greater + * than {@code bweChangeThreshold * last_bandwidth_estimation}. + */ + private val bweChangeThreshold: Double by config { + "org.jitsi.videobridge.BWE_CHANGE_THRESHOLD_PCT".from(JitsiConfig.legacyConfig) + .transformedBy { it / 100.0 } + // This is an old version, include for backward compat. + "videobridge.cc.bwe-change-threshold-pct".from(JitsiConfig.newConfig) + .transformedBy { it / 100.0 } + "videobridge.cc.bwe-change-threshold".from(JitsiConfig.newConfig) + } + fun bweChangeThreshold() = bweChangeThreshold + + /** + * The max resolution to allocate for the thumbnails. + */ + private val thumbnailMaxHeightPx: Int by config { + "org.jitsi.videobridge.THUMBNAIL_MAX_HEIGHT".from(JitsiConfig.legacyConfig) + "videobridge.cc.thumbnail-max-height-px".from(JitsiConfig.newConfig) + } + fun thumbnailMaxHeightPx() = thumbnailMaxHeightPx + + /** + * The default preferred resolution to allocate for the onstage participant, + * before allocating bandwidth for the thumbnails. + */ + private val onstagePreferredHeightPx: Int by config { + "org.jitsi.videobridge.ONSTAGE_PREFERRED_HEIGHT".from(JitsiConfig.legacyConfig) + "videobridge.cc.onstage-preferred-height-px".from(JitsiConfig.newConfig) + } + fun onstagePreferredHeightPx() = onstagePreferredHeightPx + + /** + * The preferred frame rate to allocate for the onstage participant. + */ + private val onstagePreferredFramerate: Double by config { + "org.jitsi.videobridge.ONSTAGE_PREFERRED_FRAME_RATE".from(JitsiConfig.legacyConfig) + "videobridge.cc.onstage-preferred-framerate".from(JitsiConfig.newConfig) + } + fun onstagePreferredFramerate() = onstagePreferredFramerate + + /** + * Whether or not we are allowed to oversend (exceed available bandwidth) for the video of the on-stage + * participant. + */ + private val allowOversendOnStage: Boolean by config { + "org.jitsi.videobridge.ENABLE_ONSTAGE_VIDEO_SUSPEND".from(JitsiConfig.legacyConfig).transformedBy { !it } + "videobridge.cc.enable-onstage-video-suspend".from(JitsiConfig.newConfig).transformedBy { !it } + "videobridge.cc.allow-oversend-onstage".from(JitsiConfig.newConfig) + } + fun allowOversendOnStage(): Boolean = allowOversendOnStage + + /** + * The maximum bitrate by which the bridge may exceed the estimated available bandwidth when oversending. + */ + private val maxOversendBitrate: Bandwidth by config { + "videobridge.cc.max-oversend-bitrate".from(JitsiConfig.newConfig) + .convertFrom { Bandwidth.fromString(it) } + } + fun maxOversendBitrateBps(): Double = maxOversendBitrate.bps + + /** + * Whether or not we should trust the bandwidth + * estimations. If this is se to false, then we assume a bandwidth + * estimation of Long.MAX_VALUE. + */ + private val trustBwe: Boolean by config { + "org.jitsi.videobridge.TRUST_BWE".from(JitsiConfig.legacyConfig) + "videobridge.cc.trust-bwe".from(JitsiConfig.newConfig) } + fun trustBwe(): Boolean = trustBwe + + /** + * The property for the max resolution to allocate for the onstage + * participant. + */ + private val onstageIdealHeightPx: Int by config( + "videobridge.cc.onstage-ideal-height-px".from(JitsiConfig.newConfig) + ) + fun onstageIdealHeightPx() = onstageIdealHeightPx + + /** + * The maximum amount of time we'll run before recalculating which streams we'll + * forward. + */ + private val maxTimeBetweenCalculations: Duration by config( + "videobridge.cc.max-time-between-calculations".from(JitsiConfig.newConfig) + ) + fun maxTimeBetweenCalculations() = maxTimeBetweenCalculations } diff --git a/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTest.kt b/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTest.kt index 9c05145d9f..34c3c83218 100644 --- a/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTest.kt +++ b/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTest.kt @@ -57,7 +57,7 @@ class BitrateControllerTest : ShouldSpec() { /** * We disable the threshold, causing [BandwidthAllocator] to make a new decision every time BWE changes. This is - * because these tests are designed to test the decisions themselves and not necessariry when they are made. + * because these tests are designed to test the decisions themselves and not necessarily when they are made. */ override fun beforeSpec(spec: Spec) = super.beforeSpec(spec).also { setNewConfig("videobridge.cc.bwe-change-threshold=0", true) diff --git a/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTest2.kt b/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTraceTest.kt similarity index 93% rename from jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTest2.kt rename to jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTraceTest.kt index 391586616a..a07b394a08 100644 --- a/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTest2.kt +++ b/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/BitrateControllerTraceTest.kt @@ -15,6 +15,7 @@ */ package org.jitsi.videobridge.cc.allocation +import io.kotest.assertions.fail import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.ShouldSpec import io.kotest.matchers.ints.shouldBeLessThan @@ -23,7 +24,6 @@ import org.jitsi.nlj.RtpEncodingDesc import org.jitsi.nlj.VideoType import org.jitsi.nlj.util.bps import org.jitsi.test.time.FakeClock -import java.io.File import java.time.Instant /** @@ -36,7 +36,7 @@ import java.time.Instant * number and/or quality of the videos being shown. Specifically, we want to limit the "flickering" which happens as * a result of fluctuations in the bitrates of the layers and the changes in BWE. */ -class BitrateControllerTest2 : ShouldSpec() { +class BitrateControllerTraceTest : ShouldSpec() { override fun isolationMode() = IsolationMode.InstancePerLeaf private val clock = FakeClock() @@ -46,12 +46,13 @@ class BitrateControllerTest2 : ShouldSpec() { private val D = Endpoint("D") private val E = Endpoint("E") private val F = Endpoint("F") - private val bc = BitrateControllerWrapper(listOf(A, B, C, D, E, F), clock = clock) + private val bc = BitrateControllerWrapper(listOf(A, B, C, D, E, F), clock = clock).apply { + bc.endpointOrderingChanged() + } init { - bc.bc.endpointOrderingChanged() - val parsedLines = File("${System.getProperty("user.dir")}/src/test/resources/bwe-events.csv") - .readLines().drop(1).map { ParsedLine(it) }.toList() + val bweEvents = javaClass.getResource("/bwe-events.csv") ?: fail("Can not read bwe-events.csv") + val parsedLines = bweEvents.readText().split("\n").drop(1).dropLast(1).map { ParsedLine(it) }.toList() context("Number of allocation changes") { diff --git a/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/EffectiveConstraintsTest.kt b/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/EffectiveConstraintsTest.kt index 501f030b09..5b15562e41 100644 --- a/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/EffectiveConstraintsTest.kt +++ b/jvb/src/test/kotlin/org/jitsi/videobridge/cc/allocation/EffectiveConstraintsTest.kt @@ -19,6 +19,7 @@ import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.ShouldSpec import io.kotest.matchers.shouldBe import org.jitsi.nlj.VideoType +import org.jitsi.videobridge.cc.config.BitrateControllerConfig @Suppress("NAME_SHADOWING") class EffectiveConstraintsTest : ShouldSpec() { @@ -32,6 +33,8 @@ class EffectiveConstraintsTest : ShouldSpec() { val e5 = TestEndpoint("e5", videoType = VideoType.NONE) val e6 = TestEndpoint("e6", videoType = VideoType.NONE) + val defaultConstraints = VideoConstraints(BitrateControllerConfig().thumbnailMaxHeightPx()) + val endpoints = listOf(e1, e2, e3, e4, e5, e6) val zeroEffectiveConstraints = mutableMapOf( "e1" to VideoConstraints.NOTHING, @@ -43,12 +46,12 @@ class EffectiveConstraintsTest : ShouldSpec() { ) context("With lastN=0") { - val allocationSettings = AllocationSettings(lastN = 0) + val allocationSettings = AllocationSettings(lastN = 0, defaultConstraints = defaultConstraints) getEffectiveConstraints(endpoints, allocationSettings) shouldBe zeroEffectiveConstraints } context("With lastN=1") { context("And no other constraints") { - val allocationSettings = AllocationSettings(lastN = 1) + val allocationSettings = AllocationSettings(lastN = 1, defaultConstraints = defaultConstraints) getEffectiveConstraints(endpoints, allocationSettings) shouldBe zeroEffectiveConstraints.apply { // The default defaultConstraints are 180 put("e1", VideoConstraints(180)) @@ -93,7 +96,7 @@ class EffectiveConstraintsTest : ShouldSpec() { val endpoints = listOf(e4, e5, e6, e1, e2, e3) context("With default settings") { - val allocationSettings = AllocationSettings(lastN = 1) + val allocationSettings = AllocationSettings(lastN = 1, defaultConstraints = defaultConstraints) getEffectiveConstraints(endpoints, allocationSettings) shouldBe zeroEffectiveConstraints.apply { put("e4", VideoConstraints(180)) } @@ -132,7 +135,7 @@ class EffectiveConstraintsTest : ShouldSpec() { } context("With lastN=3") { context("And default settings") { - val allocationSettings = AllocationSettings(lastN = 3) + val allocationSettings = AllocationSettings(lastN = 3, defaultConstraints = defaultConstraints) getEffectiveConstraints(endpoints, allocationSettings) shouldBe zeroEffectiveConstraints.apply { put("e1", VideoConstraints(180)) put("e2", VideoConstraints(180)) @@ -144,7 +147,7 @@ class EffectiveConstraintsTest : ShouldSpec() { val endpoints = listOf(e4, e5, e6, e1, e2, e3) context("And default settings") { - val allocationSettings = AllocationSettings(lastN = 3) + val allocationSettings = AllocationSettings(lastN = 3, defaultConstraints = defaultConstraints) getEffectiveConstraints(endpoints, allocationSettings) shouldBe zeroEffectiveConstraints.apply { put("e4", VideoConstraints(180)) put("e5", VideoConstraints(180))