Skip to content

Commit c7ef8e6

Browse files
authored
Add support for the VLA RTP header extension (#2263)
* feat: Add BitReader utilities and tests. * feat: Add a parser for the VLA RTP header extension. * ref: Remove unused function. * feat: Update layers with info found in VLA. * feat: Add an option to use targetBitrate instead of measured bitrate for allocation * feat: Warn if replacing width/frameRate. * test: Add tests for invalid VLAs. * ref: Simplify code, add a comment. * feat: Retain the VLA extension between relays.
1 parent f18bf2e commit c7ef8e6

File tree

17 files changed

+858
-26
lines changed

17 files changed

+858
-26
lines changed

jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpLayerDesc.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ import org.jitsi.utils.OrderedJsonObject
2828
*
2929
* @author George Politis
3030
*/
31-
abstract class RtpLayerDesc
32-
constructor(
31+
abstract class RtpLayerDesc(
3332
/**
3433
* The index of this instance's encoding in the source encoding array.
3534
*/
@@ -54,7 +53,7 @@ constructor(
5453
* represents. The actual frame rate may be less due to bad network or
5554
* system load. [NO_FRAME_RATE] for unknown.
5655
*/
57-
val frameRate: Double,
56+
var frameRate: Double,
5857
) {
5958
abstract fun copy(height: Int = this.height, tid: Int = this.tid, inherit: Boolean = true): RtpLayerDesc
6059

@@ -63,6 +62,8 @@ constructor(
6362
*/
6463
protected var bitrateTracker = BitrateCalculator.createBitrateTracker()
6564

65+
var targetBitrate: Bandwidth? = null
66+
6667
/**
6768
* @return the "id" of this layer within this encoding. This is a server-side id and should
6869
* not be confused with any encoding id defined in the client (such as the
@@ -87,6 +88,7 @@ constructor(
8788
*/
8889
internal open fun inheritFrom(other: RtpLayerDesc) {
8990
inheritStatistics(other.bitrateTracker)
91+
targetBitrate = other.targetBitrate
9092
}
9193

9294
/**
@@ -110,12 +112,6 @@ constructor(
110112
*/
111113
abstract fun getBitrate(nowMs: Long): Bandwidth
112114

113-
/**
114-
* Expose [getBitrate] as a [Double] in order to make it accessible from java (since [Bandwidth] is an inline
115-
* class).
116-
*/
117-
fun getBitrateBps(nowMs: Long): Double = getBitrate(nowMs).bps
118-
119115
/**
120116
* Recursively checks this layer and its dependencies to see if the bitrate is zero.
121117
* Note that unlike [calcBitrate] this does not avoid double-visiting layers; the overhead
@@ -131,6 +127,7 @@ constructor(
131127
addNumber("height", height)
132128
addNumber("index", index)
133129
addNumber("bitrate_bps", getBitrate(System.currentTimeMillis()).bps)
130+
addNumber("target_bitrate", targetBitrate?.bps ?: 0)
134131
}
135132

136133
fun debugState(): OrderedJsonObject = getNodeStats().toJson().apply { put("indexString", indexString()) }

jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpReceiverImpl.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import org.jitsi.nlj.transform.node.incoming.VideoBitrateCalculator
5656
import org.jitsi.nlj.transform.node.incoming.VideoMuteNode
5757
import org.jitsi.nlj.transform.node.incoming.VideoParser
5858
import org.jitsi.nlj.transform.node.incoming.VideoQualityLayerLookup
59+
import org.jitsi.nlj.transform.node.incoming.VlaReaderNode
5960
import org.jitsi.nlj.transform.packetPath
6061
import org.jitsi.nlj.transform.pipeline
6162
import org.jitsi.nlj.util.Bandwidth
@@ -248,6 +249,7 @@ class RtpReceiverImpl @JvmOverloads constructor(
248249
node(videoParser)
249250
node(VideoQualityLayerLookup(logger))
250251
node(videoBitrateCalculator)
252+
node(VlaReaderNode(streamInformationStore, logger))
251253
node(packetHandlerWrapper)
252254
}
253255
}

jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSender.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.jitsi.nlj
1717

1818
import org.jitsi.nlj.rtp.LossListener
19+
import org.jitsi.nlj.rtp.RtpExtensionType
1920
import org.jitsi.nlj.rtp.TransportCcEngine
2021
import org.jitsi.nlj.rtp.bandwidthestimation.BandwidthEstimator
2122
import org.jitsi.nlj.srtp.SrtpTransformers
@@ -47,6 +48,7 @@ abstract class RtpSender :
4748
abstract fun setFeature(feature: Features, enabled: Boolean)
4849
abstract fun isFeatureEnabled(feature: Features): Boolean
4950
abstract fun tearDown()
51+
abstract fun addRtpExtensionToRetain(extensionType: RtpExtensionType)
5052

5153
/**
5254
* An optional function to be executed for each RTP packet, as the first step of the send pipeline.

jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/RtpSenderImpl.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import org.jitsi.nlj.rtcp.NackHandler
2323
import org.jitsi.nlj.rtcp.RtcpEventNotifier
2424
import org.jitsi.nlj.rtcp.RtcpSrUpdater
2525
import org.jitsi.nlj.rtp.LossListener
26+
import org.jitsi.nlj.rtp.RtpExtensionType
2627
import org.jitsi.nlj.rtp.TransportCcEngine
2728
import org.jitsi.nlj.rtp.bandwidthestimation.BandwidthEstimator
2829
import org.jitsi.nlj.rtp.bandwidthestimation.GoogleCcEstimator
@@ -111,6 +112,7 @@ class RtpSenderImpl(
111112
private val srtcpEncryptWrapper = SrtcpEncryptNode()
112113
private val toggleablePcapWriter = ToggleablePcapWriter(logger, "$id-tx")
113114
private val outgoingPacketCache = PacketCacher()
115+
private val headerExtensionStripper = HeaderExtStripper(streamInformationStore)
114116
private val absSendTime = AbsSendTime(streamInformationStore)
115117
private val statsTracker = OutgoingStatisticsTracker()
116118
private val packetStreamStats = PacketStreamStatsNode()
@@ -144,7 +146,7 @@ class RtpSenderImpl(
144146
outgoingRtpRoot = pipeline {
145147
node(PluggableTransformerNode("RTP pre-processor") { preProcesor })
146148
node(AudioRedHandler(streamInformationStore, logger))
147-
node(HeaderExtStripper(streamInformationStore))
149+
node(headerExtensionStripper)
148150
node(outgoingPacketCache)
149151
node(absSendTime)
150152
node(statsTracker)
@@ -333,6 +335,10 @@ class RtpSenderImpl(
333335
toggleablePcapWriter.disable()
334336
}
335337

338+
override fun addRtpExtensionToRetain(extensionType: RtpExtensionType) {
339+
headerExtensionStripper.addRtpExtensionToRetain(extensionType)
340+
}
341+
336342
companion object {
337343
var queueErrorCounter = CountingErrorHandler()
338344

jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/Transceiver.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package org.jitsi.nlj
1818
import org.jitsi.nlj.format.PayloadType
1919
import org.jitsi.nlj.rtcp.RtcpEventNotifier
2020
import org.jitsi.nlj.rtp.RtpExtension
21+
import org.jitsi.nlj.rtp.RtpExtensionType
2122
import org.jitsi.nlj.rtp.bandwidthestimation.BandwidthEstimator
2223
import org.jitsi.nlj.srtp.SrtpTransformers
2324
import org.jitsi.nlj.srtp.SrtpUtil
@@ -211,6 +212,10 @@ class Transceiver(
211212
rtpReceiver.handleEvent(localSsrcSetEvent)
212213
}
213214

215+
fun addRtpExtensionToRetain(extensionType: RtpExtensionType) {
216+
rtpSender.addRtpExtensionToRetain(extensionType)
217+
}
218+
214219
fun receivesSsrc(ssrc: Long): Boolean = streamInformationStore.receiveSsrcs.contains(ssrc)
215220

216221
val receiveSsrcs: Set<Long>

jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/rtp/RtpExtensions.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,13 @@ enum class RtpExtensionType(val uri: String) {
103103
*/
104104
AV1_DEPENDENCY_DESCRIPTOR(
105105
"https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension"
106-
);
106+
),
107+
108+
/**
109+
* Video Layers Allocation
110+
* https://webrtc.googlesource.com/src/+/refs/heads/main/docs/native-code/rtp-hdrext/video-layers-allocation00
111+
*/
112+
VLA("http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00");
107113

108114
companion object {
109115
private val uriMap = RtpExtensionType.values().associateBy(RtpExtensionType::uri)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright @ 2024-Present 8x8, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.jitsi.nlj.transform.node.incoming
17+
18+
import org.jitsi.nlj.Event
19+
import org.jitsi.nlj.MediaSourceDesc
20+
import org.jitsi.nlj.PacketInfo
21+
import org.jitsi.nlj.SetMediaSourcesEvent
22+
import org.jitsi.nlj.findRtpSource
23+
import org.jitsi.nlj.rtp.RtpExtensionType.VLA
24+
import org.jitsi.nlj.transform.node.ObserverNode
25+
import org.jitsi.nlj.util.ReadOnlyStreamInformationStore
26+
import org.jitsi.nlj.util.kbps
27+
import org.jitsi.rtp.rtp.RtpPacket
28+
import org.jitsi.rtp.rtp.header_extensions.VlaExtension
29+
import org.jitsi.utils.logging2.Logger
30+
import org.jitsi.utils.logging2.LoggerImpl
31+
import org.jitsi.utils.logging2.cdebug
32+
import org.jitsi.utils.logging2.createChildLogger
33+
34+
/**
35+
* A node which reads the Video Layers Allocation (VLA) RTP header extension and updates the media sources.
36+
*/
37+
class VlaReaderNode(
38+
streamInformationStore: ReadOnlyStreamInformationStore,
39+
parentLogger: Logger = LoggerImpl(VlaReaderNode::class.simpleName)
40+
) : ObserverNode("Video Layers Allocation reader") {
41+
private val logger = createChildLogger(parentLogger)
42+
private var vlaExtId: Int? = null
43+
private var mediaSourceDescs: Array<MediaSourceDesc> = arrayOf()
44+
45+
init {
46+
streamInformationStore.onRtpExtensionMapping(VLA) {
47+
vlaExtId = it
48+
logger.debug("VLA extension ID set to $it")
49+
}
50+
}
51+
52+
override fun handleEvent(event: Event) {
53+
when (event) {
54+
is SetMediaSourcesEvent -> {
55+
mediaSourceDescs = event.mediaSourceDescs.copyOf()
56+
logger.cdebug { "Media sources changed:\n${mediaSourceDescs.joinToString()}" }
57+
}
58+
}
59+
}
60+
61+
override fun observe(packetInfo: PacketInfo) {
62+
val rtpPacket = packetInfo.packetAs<RtpPacket>()
63+
vlaExtId?.let {
64+
rtpPacket.getHeaderExtension(it)?.let { ext ->
65+
val vla = try {
66+
VlaExtension.parse(ext)
67+
} catch (e: Exception) {
68+
logger.warn("Failed to parse VLA extension", e)
69+
return
70+
}
71+
72+
val sourceDesc = mediaSourceDescs.findRtpSource(rtpPacket)
73+
74+
logger.debug("Found VLA=$vla for sourceDesc=$sourceDesc")
75+
76+
vla.forEachIndexed { streamIdx, stream ->
77+
val rtpEncoding = sourceDesc?.rtpEncodings?.get(streamIdx)
78+
stream.spatialLayers.forEach { spatialLayer ->
79+
spatialLayer.targetBitratesKbps.forEachIndexed { tlIdx, targetBitrateKbps ->
80+
rtpEncoding?.layers?.find {
81+
// With VP8 simulcast all layers have sid -1
82+
(it.sid == spatialLayer.id || it.sid == -1) && it.tid == tlIdx
83+
}?.let { layer ->
84+
logger.debug(
85+
"Setting target bitrate for rtpEncoding=$rtpEncoding layer=$layer to " +
86+
"${targetBitrateKbps.kbps} (res=${spatialLayer.res})"
87+
)
88+
layer.targetBitrate = targetBitrateKbps.kbps
89+
spatialLayer.res?.let { res ->
90+
if (layer.height > 0 && layer.height != res.height) {
91+
logger.warn("Updating layer height from ${layer.height} to ${res.height}")
92+
}
93+
layer.height = res.height
94+
layer.frameRate = res.maxFramerate.toDouble()
95+
}
96+
}
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}
103+
104+
override fun trace(f: () -> Unit) {}
105+
}

jitsi-media-transform/src/main/kotlin/org/jitsi/nlj/transform/node/outgoing/HeaderExtStripper.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,40 @@ import org.jitsi.nlj.util.ReadOnlyStreamInformationStore
2323
import org.jitsi.rtp.rtp.RtpPacket
2424

2525
/**
26-
* Strip all hop-by-hop header extensions. Currently this leaves ssrc-audio-level and video-orientation,
26+
* Strip all hop-by-hop header extensions. By default, this leaves ssrc-audio-level and video-orientation,
2727
* plus the AV1 dependency descriptor if the packet is an Av1DDPacket.
2828
*/
2929
class HeaderExtStripper(
30-
streamInformationStore: ReadOnlyStreamInformationStore
30+
streamInformationStore: ReadOnlyStreamInformationStore,
3131
) : ModifierNode("Strip header extensions") {
3232
private var retainedExts: Set<Int> = emptySet()
3333
private var retainedExtsWithAv1DD: Set<Int> = emptySet()
34+
private var retainedExtTypes = defaultRetainedExtTypes
3435

3536
init {
3637
retainedExtTypes.forEach { rtpExtensionType ->
3738
streamInformationStore.onRtpExtensionMapping(rtpExtensionType) {
3839
it?.let {
39-
retainedExts = retainedExts.plus(it)
40-
retainedExtsWithAv1DD = retainedExtsWithAv1DD.plus(it)
40+
retainedExts += it
41+
retainedExtsWithAv1DD += it
4142
}
4243
}
4344
}
4445
streamInformationStore.onRtpExtensionMapping(RtpExtensionType.AV1_DEPENDENCY_DESCRIPTOR) {
45-
it?.let { retainedExtsWithAv1DD = retainedExtsWithAv1DD.plus(it) }
46+
it?.let { retainedExtsWithAv1DD += it }
4647
}
4748
}
4849

50+
fun addRtpExtensionToRetain(extensionType: RtpExtensionType) {
51+
retainedExtTypes += extensionType
52+
}
53+
4954
override fun modify(packetInfo: PacketInfo): PacketInfo {
5055
val rtpPacket = packetInfo.packetAs<RtpPacket>()
5156

5257
val retained = if (rtpPacket is Av1DDPacket) retainedExtsWithAv1DD else retainedExts
5358

59+
// TODO: we should also retain any extensions that were not signaled.
5460
rtpPacket.removeHeaderExtensionsExcept(retained)
5561

5662
return packetInfo
@@ -59,7 +65,7 @@ class HeaderExtStripper(
5965
override fun trace(f: () -> Unit) = f.invoke()
6066

6167
companion object {
62-
private val retainedExtTypes: Set<RtpExtensionType> = setOf(
68+
val defaultRetainedExtTypes: Set<RtpExtensionType> = setOf(
6369
RtpExtensionType.SSRC_AUDIO_LEVEL,
6470
RtpExtensionType.VIDEO_ORIENTATION
6571
)

jvb/src/main/kotlin/org/jitsi/videobridge/cc/allocation/BitrateController.kt

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.jitsi.nlj.MediaSourceDesc
1919
import org.jitsi.nlj.PacketInfo
2020
import org.jitsi.nlj.format.PayloadType
2121
import org.jitsi.nlj.format.PayloadTypeEncoding
22+
import org.jitsi.nlj.util.Bandwidth
2223
import org.jitsi.nlj.util.bps
2324
import org.jitsi.rtp.rtcp.RtcpSrPacket
2425
import org.jitsi.utils.event.SyncEventEmitter
@@ -192,13 +193,26 @@ class BitrateController<T : MediaSourceContainer> @JvmOverloads constructor(
192193

193194
val nowMs = clock.instant().toEpochMilli()
194195
val allocation = bandwidthAllocator.allocation
195-
allocation.allocations.forEach {
196-
it.targetLayer?.getBitrate(nowMs)?.let { targetBitrate ->
197-
totalTargetBitrate += targetBitrate
198-
it.mediaSource?.primarySSRC?.let { primarySsrc -> activeSsrcs.add(primarySsrc) }
196+
allocation.allocations.forEach { singleAllocation ->
197+
val allocationTargetBitrate: Bandwidth? = if (config.useVlaTargetBitrate) {
198+
singleAllocation.targetLayer?.targetBitrate ?: singleAllocation.targetLayer?.getBitrate(nowMs)
199+
} else {
200+
singleAllocation.targetLayer?.getBitrate(nowMs)
201+
}
202+
203+
allocationTargetBitrate?.let {
204+
totalTargetBitrate += it
205+
singleAllocation.mediaSource?.primarySSRC?.let { primarySsrc -> activeSsrcs.add(primarySsrc) }
199206
}
200-
it.idealLayer?.getBitrate(nowMs)?.let { idealBitrate ->
201-
totalIdealBitrate += idealBitrate
207+
208+
val allocationIdealBitrate: Bandwidth? = if (config.useVlaTargetBitrate) {
209+
singleAllocation.idealLayer?.targetBitrate ?: singleAllocation.idealLayer?.getBitrate(nowMs)
210+
} else {
211+
singleAllocation.idealLayer?.getBitrate(nowMs)
212+
}
213+
214+
allocationIdealBitrate?.let {
215+
totalIdealBitrate += it
202216
}
203217
}
204218

@@ -220,18 +234,24 @@ class BitrateController<T : MediaSourceContainer> @JvmOverloads constructor(
220234

221235
var totalTargetBps = 0.0
222236
var totalIdealBps = 0.0
237+
var totalTargetMeasuredBps = 0.0
238+
var totalIdealMeasuredBps = 0.0
223239

224240
allocation.allocations.forEach {
225241
it.targetLayer?.getBitrate(nowMs)?.let { bitrate -> totalTargetBps += bitrate.bps }
226242
it.idealLayer?.getBitrate(nowMs)?.let { bitrate -> totalIdealBps += bitrate.bps }
243+
it.targetLayer?.targetBitrate?.let { bitrate -> totalTargetMeasuredBps += bitrate.bps }
244+
it.idealLayer?.targetBitrate?.let { bitrate -> totalIdealMeasuredBps += bitrate.bps }
227245
trace(
228246
diagnosticContext
229247
.makeTimeSeriesPoint("allocation_for_source", nowMs)
230248
.addField("remote_endpoint_id", it.endpointId)
231249
.addField("target_idx", it.targetLayer?.index ?: -1)
232250
.addField("ideal_idx", it.idealLayer?.index ?: -1)
233-
.addField("target_bps", it.targetLayer?.getBitrate(nowMs)?.bps ?: -1)
234-
.addField("ideal_bps", it.idealLayer?.getBitrate(nowMs)?.bps ?: -1)
251+
.addField("target_bps_measured", it.targetLayer?.getBitrate(nowMs)?.bps ?: -1)
252+
.addField("target_bps", it.targetLayer?.targetBitrate?.bps ?: -1)
253+
.addField("ideal_bps_measured", it.idealLayer?.getBitrate(nowMs)?.bps ?: -1)
254+
.addField("ideal_bps", it.idealLayer?.targetBitrate?.bps ?: -1)
235255
)
236256
}
237257

@@ -240,6 +260,8 @@ class BitrateController<T : MediaSourceContainer> @JvmOverloads constructor(
240260
.makeTimeSeriesPoint("allocation", nowMs)
241261
.addField("total_target_bps", totalTargetBps)
242262
.addField("total_ideal_bps", totalIdealBps)
263+
.addField("total_target_measured_bps", totalTargetMeasuredBps)
264+
.addField("total_ideal_measured_bps", totalIdealMeasuredBps)
243265
)
244266
}
245267

0 commit comments

Comments
 (0)