From 3dbe09b91c3bb574c24ff24243b49400d6b42b4e Mon Sep 17 00:00:00 2001 From: shijing xian Date: Mon, 29 Jun 2026 18:05:19 +0800 Subject: [PATCH] fix: improve initial video quality by setting x-google-start-bitrate for all video codecs - Apply x-google-start-bitrate SDP hint to all video codecs (VP8, VP9, AV1, H264, H265), not just SVC codecs - Use 90% of target bitrate as start bitrate to prevent initial blurriness - Default degradationPreference to MAINTAIN_RESOLUTION for video tracks to prefer frame drops over resolution reduction when bandwidth is constrained This addresses the issue where video starts blurry for several seconds before improving, by telling WebRTC's bandwidth estimator to start at a higher bitrate instead of ramping up from ~300kbps. Co-Authored-By: Claude Opus 4.5 --- .../android/room/PeerConnectionTransport.kt | 31 +- .../room/participant/LocalParticipant.kt | 11 +- munging.patch | 591 ++++++++++++++++++ protocol | 2 +- 4 files changed, 621 insertions(+), 14 deletions(-) create mode 100644 munging.patch diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/PeerConnectionTransport.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/PeerConnectionTransport.kt index efae279a4..04f405bd3 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/room/PeerConnectionTransport.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/PeerConnectionTransport.kt @@ -437,13 +437,17 @@ fun ensureVideoDDExtensionForSVC(mediaDesc: MediaDescription) { } } -/* The svc codec (av1/vp9) would use a very low bitrate at the beginning and -increase slowly by the bandwidth estimator until it reach the target bitrate. The -process commonly cost more than 10 seconds cause subscriber will get blur video at -the first few seconds. So we use a 70% of target bitrate here as the start bitrate to -eliminate this issue. -*/ -private const val startBitrateForSVC = 0.7 +/* + * Video codecs use a very low bitrate at the beginning and increase slowly by + * the bandwidth estimator until they reach the target bitrate. The process commonly + * costs more than 10 seconds causing subscribers to get blurry video at the first + * few seconds. We use x-google-start-bitrate to hint the BWE to start higher. + * + * Why 90%: Gives ~10% headroom for bandwidth estimation while starting close to target. + * Why same for all codecs: Target bitrate already accounts for codec efficiency + * (e.g., users set lower targets for VP9/AV1 knowing they're more efficient). + */ +private const val startBitrateMultiplier = 0.9 /** * @suppress @@ -476,7 +480,7 @@ fun ensureCodecBitrates( fmtpFound = true var newFmtpConfig = fmtp.config if (!fmtp.config.contains("x-google-start-bitrate")) { - newFmtpConfig = "$newFmtpConfig;x-google-start-bitrate=${(trackBr.maxBitrate * startBitrateForSVC).roundToLong()}" + newFmtpConfig = "$newFmtpConfig;x-google-start-bitrate=${(trackBr.maxBitrate * startBitrateMultiplier).roundToLong()}" } if (!fmtp.config.contains("x-google-max-bitrate")) { newFmtpConfig = "$newFmtpConfig;x-google-max-bitrate=${trackBr.maxBitrate}" @@ -492,7 +496,7 @@ fun ensureCodecBitrates( media.addAttribute( SdpFmtp( payload = codecPayload, - config = "x-google-start-bitrate=${trackBr.maxBitrate * startBitrateForSVC};" + + config = "x-google-start-bitrate=${trackBr.maxBitrate * startBitrateMultiplier};" + "x-google-max-bitrate=${trackBr.maxBitrate}", ).toAttributeField(), ) @@ -506,6 +510,15 @@ internal fun isSVCCodec(codec: String?): Boolean { "vp9".equals(codec, ignoreCase = true)) } +internal fun isVideoCodec(codec: String?): Boolean { + return codec != null && + ("vp8".equals(codec, ignoreCase = true) || + "vp9".equals(codec, ignoreCase = true) || + "av1".equals(codec, ignoreCase = true) || + "h264".equals(codec, ignoreCase = true) || + "h265".equals(codec, ignoreCase = true)) +} + /** * @suppress */ diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt index 5daa840db..ee70db5e8 100644 --- a/livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt +++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt @@ -38,6 +38,7 @@ import io.livekit.android.room.Room import io.livekit.android.room.TrackBitrateInfo import io.livekit.android.room.datastream.outgoing.OutgoingDataStreamManager import io.livekit.android.room.isSVCCodec +import io.livekit.android.room.isVideoCodec import io.livekit.android.room.rpc.RpcClientManager import io.livekit.android.room.rpc.RpcManager import io.livekit.android.room.rpc.RpcServerManager @@ -697,9 +698,9 @@ internal constructor( track.statsGetter = engine.createStatsGetter(transceiver.sender) val finalOptions = options - // Handle trackBitrates + // Handle trackBitrates - apply start bitrate for all video codecs to prevent initial blurriness if (encodings.isNotEmpty()) { - if (finalOptions is VideoTrackPublishOptions && isSVCCodec(finalOptions.videoCodec) && encodings.firstOrNull()?.maxBitrateBps != null) { + if (finalOptions is VideoTrackPublishOptions && isVideoCodec(finalOptions.videoCodec) && encodings.firstOrNull()?.maxBitrateBps != null) { engine.registerTrackBitrateInfo( cid = cid, TrackBitrateInfo( @@ -1481,7 +1482,8 @@ data class VideoTrackPublishDefaults( override val videoCodec: String = VideoCodec.VP8.codecName, override val scalabilityMode: String? = null, override val backupCodec: BackupVideoCodec? = null, - override val degradationPreference: RtpParameters.DegradationPreference? = null, + // Default to MAINTAIN_RESOLUTION to prevent initial video blurriness + override val degradationPreference: RtpParameters.DegradationPreference? = RtpParameters.DegradationPreference.MAINTAIN_RESOLUTION, override val simulcastLayers: List? = null, ) : BaseVideoTrackPublishOptions() @@ -1494,7 +1496,8 @@ data class VideoTrackPublishOptions( override val backupCodec: BackupVideoCodec? = null, override val source: Track.Source? = null, override val stream: String? = null, - override val degradationPreference: RtpParameters.DegradationPreference? = null, + // Default to MAINTAIN_RESOLUTION to prevent initial video blurriness + override val degradationPreference: RtpParameters.DegradationPreference? = RtpParameters.DegradationPreference.MAINTAIN_RESOLUTION, override val simulcastLayers: List? = null, ) : BaseVideoTrackPublishOptions(), TrackPublishOptions { constructor( diff --git a/munging.patch b/munging.patch new file mode 100644 index 000000000..1fb38448a --- /dev/null +++ b/munging.patch @@ -0,0 +1,591 @@ +diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml +index 3501a8d..df624b7 100644 +--- a/gradle/libs.versions.toml ++++ b/gradle/libs.versions.toml +@@ -1,7 +1,6 @@ + [versions] + webrtc = "137.7151.05" + +-androidJainSipRi = "1.3.0-91" + androidx-activity = "1.9.0" + androidx-camera = "1.4.2" + androidx-core = "1.13.1" +@@ -31,7 +30,6 @@ noise = "2.0.0" + lifecycleProcess = "2.8.7" + + [libraries] +-android-jain-sip-ri = { module = "javax.sip:android-jain-sip-ri", version.ref = "androidJainSipRi" } + androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } + androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "androidx-camera" } + androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "androidx-camera" } +@@ -111,4 +109,3 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3" + lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycleProcess" } + + [plugins] +- +diff --git a/livekit-android-sdk/build.gradle b/livekit-android-sdk/build.gradle +index 45a3f69..2c019e1 100644 +--- a/livekit-android-sdk/build.gradle ++++ b/livekit-android-sdk/build.gradle +@@ -154,8 +154,6 @@ dependencies { + implementation libs.androidx.core + implementation libs.protobuf.javalite + +- implementation libs.android.jain.sip.ri +- + implementation libs.dagger.lib + kapt libs.dagger.compiler + +diff --git a/livekit-android-sdk/consumer-rules.pro b/livekit-android-sdk/consumer-rules.pro +index a3d6542..6ae7318 100644 +--- a/livekit-android-sdk/consumer-rules.pro ++++ b/livekit-android-sdk/consumer-rules.pro +@@ -28,13 +28,6 @@ + @livekit.**.CalledByNativeUnchecked ; + } + +-# NIST sdp parser +-######################################### +-# Preserve reflection used for Parser registrations +--keep class android.gov.nist.javax.sdp.parser.*Parser { *; } +--keep class android.gov.nist.javax.sdp.parser.ParserFactory { *; } +--keep class android.gov.nist.javax.sdp.parser.SDPParser { *; } +- + # Protobuf + ######################################### + -keep class * extends com.google.protobuf.GeneratedMessageLite { *; } +diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/dagger/RTCModule.kt b/livekit-android-sdk/src/main/java/io/livekit/android/dagger/RTCModule.kt +index 9f6da92..075f139 100644 +--- a/livekit-android-sdk/src/main/java/io/livekit/android/dagger/RTCModule.kt ++++ b/livekit-android-sdk/src/main/java/io/livekit/android/dagger/RTCModule.kt +@@ -17,7 +17,6 @@ + package io.livekit.android.dagger + + import android.content.Context +-import android.javax.sdp.SdpFactory + import android.media.AudioAttributes + import android.media.MediaRecorder + import android.os.Build +@@ -408,8 +407,6 @@ internal object RTCModule { + @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL) + fun videoHwAccel() = true + +- @Provides +- fun sdpFactory() = SdpFactory.getInstance() + } + + /** +diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/room/PeerConnectionTransport.kt b/livekit-android-sdk/src/main/java/io/livekit/android/room/PeerConnectionTransport.kt +index fb6f5a0..f1e533b 100644 +--- a/livekit-android-sdk/src/main/java/io/livekit/android/room/PeerConnectionTransport.kt ++++ b/livekit-android-sdk/src/main/java/io/livekit/android/room/PeerConnectionTransport.kt +@@ -16,8 +16,6 @@ + + package io.livekit.android.room + +-import android.javax.sdp.MediaDescription +-import android.javax.sdp.SdpFactory + import androidx.annotation.VisibleForTesting + import dagger.assisted.Assisted + import dagger.assisted.AssistedFactory +@@ -38,6 +36,8 @@ import io.livekit.android.webrtc.getFmtps + import io.livekit.android.webrtc.getMsid + import io.livekit.android.webrtc.getRtps + import io.livekit.android.webrtc.isConnected ++import io.livekit.android.webrtc.parseSdpSections ++import io.livekit.android.webrtc.SdpMediaSection + import io.livekit.android.webrtc.peerconnection.RTCThreadToken + import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread + import io.livekit.android.webrtc.peerconnection.launchBlockingOnRTCThread +@@ -76,7 +76,6 @@ constructor( + @Named(InjectionNames.DISPATCHER_IO) + private val ioDispatcher: CoroutineDispatcher, + connectionFactory: PeerConnectionFactory, +- private val sdpFactory: SdpFactory, + private val rtcThreadToken: RTCThreadToken, + ) { + private val coroutineScope = CoroutineScope(ioDispatcher + SupervisorJob()) +@@ -203,21 +202,16 @@ constructor( + return@launchRTCIfNotClosed + } + // munge sdp +- val sdpDescription = sdpFactory.createSessionDescription(sdpOffer.description) +- +- val mediaDescs = sdpDescription.getMediaDescriptions(true) +- for (mediaDesc in mediaDescs) { +- if (mediaDesc !is MediaDescription) { +- continue +- } +- if (mediaDesc.media.mediaType == "audio") { ++ val sdpSections = parseSdpSections(sdpOffer.description) ++ for (mediaDesc in sdpSections.mediaSections) { ++ if (mediaDesc.mediaType == "audio") { + // TODO +- } else if (mediaDesc.media.mediaType == "video") { ++ } else if (mediaDesc.mediaType == "video") { + ensureVideoDDExtensionForSVC(mediaDesc) + ensureCodecBitrates(mediaDesc, trackBitrates = trackBitrates) + } + } +- finalSdp = setMungedSdp(sdpOffer, sdpDescription.toString()) ++ finalSdp = setMungedSdp(sdpOffer, sdpSections.toSdpString()) + } + + finalSdp?.let { sdp -> +@@ -402,7 +396,7 @@ private const val DD_EXTENSION_URI = "https://aomediacodec.github.io/av1-rtp-spe + * @suppress + */ + @VisibleForTesting +-fun ensureVideoDDExtensionForSVC(mediaDesc: MediaDescription) { ++fun ensureVideoDDExtensionForSVC(mediaDesc: SdpMediaSection) { + val codec = mediaDesc.getRtps() + .firstOrNull() + ?.second +@@ -426,13 +420,14 @@ fun ensureVideoDDExtensionForSVC(mediaDesc: MediaDescription) { + // Not found, add manually + if (!ddFound) { + mediaDesc.addAttribute( ++ "extmap", + SdpExt( + value = maxId + 1, + uri = DD_EXTENSION_URI, + config = null, + direction = null, + encryptUri = null, +- ).toAttributeField(), ++ ).toAttributeValue(), + ) + } + } +@@ -450,7 +445,7 @@ private const val startBitrateForSVC = 0.7 + */ + @VisibleForTesting + fun ensureCodecBitrates( +- media: MediaDescription, ++ media: SdpMediaSection, + trackBitrates: Map, + ) { + val msid = media.getMsid()?.value ?: return +@@ -482,7 +477,7 @@ fun ensureCodecBitrates( + newFmtpConfig = "$newFmtpConfig;x-google-max-bitrate=${trackBr.maxBitrate}" + } + if (fmtp.config != newFmtpConfig) { +- attribute.value = "${fmtp.payload} $newFmtpConfig" ++ attribute.mutableValue = "${fmtp.payload} $newFmtpConfig" + break + } + } +@@ -490,11 +485,12 @@ fun ensureCodecBitrates( + + if (!fmtpFound) { + media.addAttribute( ++ "fmtp", + SdpFmtp( + payload = codecPayload, + config = "x-google-start-bitrate=${trackBr.maxBitrate * startBitrateForSVC};" + + "x-google-max-bitrate=${trackBr.maxBitrate}", +- ).toAttributeField(), ++ ).toAttributeValue(), + ) + } + } +diff --git a/livekit-android-sdk/src/main/java/io/livekit/android/webrtc/JainSdpUtils.kt b/livekit-android-sdk/src/main/java/io/livekit/android/webrtc/JainSdpUtils.kt +index c6e91c1..371669e 100644 +--- a/livekit-android-sdk/src/main/java/io/livekit/android/webrtc/JainSdpUtils.kt ++++ b/livekit-android-sdk/src/main/java/io/livekit/android/webrtc/JainSdpUtils.kt +@@ -16,8 +16,6 @@ + + package io.livekit.android.webrtc + +-import android.gov.nist.javax.sdp.fields.AttributeField +-import android.javax.sdp.MediaDescription + import io.livekit.android.util.LKLog + + /** +@@ -25,17 +23,142 @@ import io.livekit.android.util.LKLog + */ + data class SdpRtp(val payload: Long, val codec: String, val rate: Long?, val encoding: String?) + ++data class SdpAttributeRef( ++ val name: String, ++ val value: String, ++ private val onSetValue: (String) -> Unit, ++) { ++ var mutableValue: String = value ++ set(newValue) { ++ field = newValue ++ onSetValue(newValue) ++ } ++} ++ ++/** ++ * Represents a single `m=` section in SDP. ++ * ++ * @suppress ++ */ ++class SdpMediaSection internal constructor( ++ private val lines: MutableList, ++) { ++ internal fun toLines(): List = lines.toList() ++ ++ val mediaType: String? ++ get() { ++ val mediaLine = lines.firstOrNull()?.trim() ?: return null ++ if (!mediaLine.startsWith("m=")) return null ++ return mediaLine.removePrefix("m=") ++ .substringBefore(' ') ++ .ifEmpty { null } ++ } ++ ++ fun addAttribute(name: String, value: String? = null) { ++ if (value == null) { ++ lines.add("a=$name") ++ } else { ++ lines.add("a=$name:$value") ++ } ++ } ++ ++ fun getAttribute(name: String): String? { ++ return getAttributes() ++ .firstOrNull { it.name == name } ++ ?.value ++ } ++ ++ fun getAttributes(): List { ++ val output = mutableListOf() ++ lines.forEachIndexed { index, rawLine -> ++ if (!rawLine.startsWith("a=")) { ++ return@forEachIndexed ++ } ++ ++ val content = rawLine.removePrefix("a=") ++ val separator = content.indexOf(':') ++ val (attributeName, attributeValue) = if (separator < 0) { ++ content to "" ++ } else { ++ content.substring(0, separator) to content.substring(separator + 1) ++ } ++ ++ output.add( ++ SdpAttributeRef( ++ name = attributeName, ++ value = attributeValue, ++ ) { newValue -> ++ lines[index] = "a=$attributeName:$newValue" ++ }, ++ ) ++ } ++ return output ++ } ++} ++ + /** + * @suppress + */ +-fun MediaDescription.getRtps(): List> { +- return getAttributes(true) +- .filterIsInstance() +- .filter { it.attribute.name == "rtpmap" } ++data class SdpSections( ++ val sessionLines: List, ++ val mediaSections: List, ++) { ++ fun toSdpString(): String { ++ return buildString { ++ (sessionLines + mediaSections.flatMap { it.toLines() }).forEachIndexed { index, line -> ++ if (index > 0) append("\r\n") ++ append(line) ++ } ++ append("\r\n") ++ } ++ } ++} ++ ++/** ++ * @suppress ++ */ ++fun parseSdpSections(description: String): SdpSections { ++ val normalizedLines = description ++ .replace("\r\n", "\n") ++ .replace('\r', '\n') ++ .split('\n') ++ .filter { it.isNotEmpty() } ++ ++ val sessionLines = mutableListOf() ++ val mediaSections = mutableListOf() ++ ++ var currentMediaSection: MutableList? = null ++ for (line in normalizedLines) { ++ if (line.startsWith("m=")) { ++ currentMediaSection?.let { mediaSections.add(SdpMediaSection(it)) } ++ currentMediaSection = mutableListOf(line) ++ continue ++ } ++ ++ if (currentMediaSection == null) { ++ sessionLines.add(line) ++ } else { ++ currentMediaSection.add(line) ++ } ++ } ++ currentMediaSection?.let { mediaSections.add(SdpMediaSection(it)) } ++ ++ return SdpSections( ++ sessionLines = sessionLines, ++ mediaSections = mediaSections, ++ ) ++} ++ ++/** ++ * @suppress ++ */ ++fun SdpMediaSection.getRtps(): List> { ++ return getAttributes() ++ .filter { it.name == "rtpmap" } + .mapNotNull { +- val rtp = tryParseRtp(it.value) ++ val rtp = tryParseRtp(it.mutableValue) + if (rtp == null) { +- LKLog.w { "could not parse rtpmap: ${it.encode()}" } ++ LKLog.w { "could not parse rtpmap: a=${it.name}:${it.mutableValue}" } + return@mapNotNull null + } + it to rtp +@@ -60,7 +183,7 @@ data class SdpMsid( + /** + * @suppress + */ +-fun MediaDescription.getMsid(): SdpMsid? { ++fun SdpMediaSection.getMsid(): SdpMsid? { + val attribute = getAttribute("msid") ?: return null + return SdpMsid(attribute) + } +@@ -69,25 +192,21 @@ fun MediaDescription.getMsid(): SdpMsid? { + * @suppress + */ + data class SdpFmtp(val payload: Long, val config: String) { +- fun toAttributeField(): AttributeField { +- return AttributeField().apply { +- name = "fmtp" +- value = "$payload $config" +- } ++ fun toAttributeValue(): String { ++ return "$payload $config" + } + } + + /** + * @suppress + */ +-fun MediaDescription.getFmtps(): List> { +- return getAttributes(true) +- .filterIsInstance() +- .filter { it.attribute.name == "fmtp" } ++fun SdpMediaSection.getFmtps(): List> { ++ return getAttributes() ++ .filter { it.name == "fmtp" } + .mapNotNull { +- val fmtp = tryParseFmtp(it.value) ++ val fmtp = tryParseFmtp(it.mutableValue) + if (fmtp == null) { +- LKLog.w { "could not parse fmtp: ${it.encode()}" } ++ LKLog.w { "could not parse fmtp: a=${it.name}:${it.mutableValue}" } + return@mapNotNull null + } + it to fmtp +@@ -105,21 +224,18 @@ internal fun tryParseFmtp(string: String): SdpFmtp? { + * @suppress + */ + data class SdpExt(val value: Long, val direction: String?, val encryptUri: String?, val uri: String, val config: String?) { +- fun toAttributeField(): AttributeField { +- return AttributeField().apply { +- name = "extmap" +- value = buildString { +- append(this@SdpExt.value) +- if (direction != null) { +- append(" $direction") +- } +- if (encryptUri != null) { +- append(" $encryptUri") +- } +- append(" $uri") +- if (config != null) { +- append(" $config") +- } ++ fun toAttributeValue(): String { ++ return buildString { ++ append(this@SdpExt.value) ++ if (direction != null) { ++ append(" $direction") ++ } ++ if (encryptUri != null) { ++ append(" $encryptUri") ++ } ++ append(" $uri") ++ if (config != null) { ++ append(" $config") + } + } + } +@@ -128,14 +244,13 @@ data class SdpExt(val value: Long, val direction: String?, val encryptUri: Strin + /** + * @suppress + */ +-fun MediaDescription.getExts(): List> { +- return getAttributes(true) +- .filterIsInstance() +- .filter { it.attribute.name == "extmap" } ++fun SdpMediaSection.getExts(): List> { ++ return getAttributes() ++ .filter { it.name == "extmap" } + .mapNotNull { +- val ext = tryParseExt(it.value) ++ val ext = tryParseExt(it.mutableValue) + if (ext == null) { +- LKLog.w { "could not parse extmap: ${it.encode()}" } ++ LKLog.w { "could not parse extmap: a=${it.name}:${it.mutableValue}" } + return@mapNotNull null + } + it to ext +diff --git a/livekit-android-test/build.gradle b/livekit-android-test/build.gradle +index 30d84c2..a117d63 100644 +--- a/livekit-android-test/build.gradle ++++ b/livekit-android-test/build.gradle +@@ -82,7 +82,6 @@ dependencies { + api libs.audioswitch + implementation libs.androidx.annotation + api libs.protobuf.javalite +- implementation libs.android.jain.sip.ri + implementation libs.junit + implementation libs.robolectric + implementation libs.mockito.core +diff --git a/livekit-android-test/src/main/java/io/livekit/android/test/mock/dagger/TestRTCModule.kt b/livekit-android-test/src/main/java/io/livekit/android/test/mock/dagger/TestRTCModule.kt +index 0ed771c..5eb2761 100644 +--- a/livekit-android-test/src/main/java/io/livekit/android/test/mock/dagger/TestRTCModule.kt ++++ b/livekit-android-test/src/main/java/io/livekit/android/test/mock/dagger/TestRTCModule.kt +@@ -17,7 +17,6 @@ + package io.livekit.android.test.mock.dagger + + import android.content.Context +-import android.javax.sdp.SdpFactory + import dagger.Module + import dagger.Provides + import dagger.Reusable +@@ -129,9 +128,6 @@ object TestRTCModule { + @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL) + fun videoHwAccel() = true + +- @Provides +- fun sdpFactory() = SdpFactory.getInstance() +- + @Provides + fun dataPacketCryptorManagerFactory(): DataPacketCryptorManager.Factory = object : DataPacketCryptorManager.Factory { + override fun create(keyProvider: KeyProvider): DataPacketCryptorManager { +diff --git a/livekit-android-test/src/test/java/io/livekit/android/room/SdpMungingTest.kt b/livekit-android-test/src/test/java/io/livekit/android/room/SdpMungingTest.kt +index bfecd60..bd9db73 100644 +--- a/livekit-android-test/src/test/java/io/livekit/android/room/SdpMungingTest.kt ++++ b/livekit-android-test/src/test/java/io/livekit/android/room/SdpMungingTest.kt +@@ -16,11 +16,10 @@ + + package io.livekit.android.room + +-import android.javax.sdp.MediaDescription +-import android.javax.sdp.SdpFactory + import io.livekit.android.webrtc.JainSdpUtilsTest + import io.livekit.android.webrtc.getExts + import io.livekit.android.webrtc.getFmtps ++import io.livekit.android.webrtc.parseSdpSections + import org.junit.Assert.assertEquals + import org.junit.Assert.assertNotNull + import org.junit.Assert.assertNull +@@ -30,8 +29,7 @@ class SdpMungingTest { + + @Test + fun ensureVideoDDExtensionForSVCTest() { +- val sdp = SdpFactory.getInstance().createSessionDescription(NO_DD_DESCRIPTION) +- val mediaDescription = sdp.getMediaDescriptions(true).filterIsInstance()[1] ++ val mediaDescription = parseSdpSections(NO_DD_DESCRIPTION).mediaSections[1] + + ensureVideoDDExtensionForSVC(mediaDescription) + +@@ -53,8 +51,7 @@ class SdpMungingTest { + + @Test + fun ensureCodecBitratesTest() { +- val sdp = SdpFactory.getInstance().createSessionDescription(JainSdpUtilsTest.DESCRIPTION) +- val mediaDescription = sdp.getMediaDescriptions(true).filterIsInstance()[1] ++ val mediaDescription = parseSdpSections(JainSdpUtilsTest.DESCRIPTION).mediaSections[1] + + ensureCodecBitrates( + mediaDescription, +diff --git a/livekit-android-test/src/test/java/io/livekit/android/webrtc/JainSdpUtilsTest.kt b/livekit-android-test/src/test/java/io/livekit/android/webrtc/JainSdpUtilsTest.kt +index 18870af..f9f9a77 100644 +--- a/livekit-android-test/src/test/java/io/livekit/android/webrtc/JainSdpUtilsTest.kt ++++ b/livekit-android-test/src/test/java/io/livekit/android/webrtc/JainSdpUtilsTest.kt +@@ -16,9 +16,6 @@ + + package io.livekit.android.webrtc + +-import android.javax.sdp.MediaDescription +-import android.javax.sdp.SdpFactory +-import android.javax.sdp.SessionDescription + import org.junit.Assert.assertEquals + import org.junit.Assert.assertNotNull + import org.junit.Assert.assertNull +@@ -26,17 +23,13 @@ import org.junit.Test + + class JainSdpUtilsTest { + +- private val sdpFactory = SdpFactory.getInstance() +- private fun createSessionDescription(): SessionDescription { +- return sdpFactory.createSessionDescription(DESCRIPTION) ++ private fun createMediaDescription(): SdpMediaSection { ++ return parseSdpSections(DESCRIPTION).mediaSections[1] + } + + @Test + fun getRtpAttributes() { +- val sdp = createSessionDescription() +- val mediaDescriptions = sdp.getMediaDescriptions(true) +- .filterIsInstance() +- val mediaDesc = mediaDescriptions[1] ++ val mediaDesc = createMediaDescription() + val rtps = mediaDesc.getRtps() + assertEquals(13, rtps.size) + +@@ -50,10 +43,7 @@ class JainSdpUtilsTest { + + @Test + fun getExtmapAttributes() { +- val sdp = createSessionDescription() +- val mediaDescriptions = sdp.getMediaDescriptions(true) +- .filterIsInstance() +- val mediaDesc = mediaDescriptions[1] ++ val mediaDesc = createMediaDescription() + val exts = mediaDesc.getExts() + + assertEquals(12, exts.size) +@@ -68,10 +58,7 @@ class JainSdpUtilsTest { + + @Test + fun getMsid() { +- val sdp = createSessionDescription() +- val mediaDescriptions = sdp.getMediaDescriptions(true) +- .filterIsInstance() +- val mediaDesc = mediaDescriptions[1] ++ val mediaDesc = createMediaDescription() + + val msid = mediaDesc.getMsid() + assertNotNull(msid) +@@ -80,10 +67,7 @@ class JainSdpUtilsTest { + + @Test + fun getFmtps() { +- val sdp = createSessionDescription() +- val mediaDescriptions = sdp.getMediaDescriptions(true) +- .filterIsInstance() +- val mediaDesc = mediaDescriptions[1] ++ val mediaDesc = createMediaDescription() + + val fmtps = mediaDesc.getFmtps() + .filter { (_, fmtp) -> fmtp.payload == 97L } diff --git a/protocol b/protocol index 8381f2180..4c05a3325 160000 --- a/protocol +++ b/protocol @@ -1 +1 @@ -Subproject commit 8381f2180c45ab926b3ebf19df0608f1dadcac1e +Subproject commit 4c05a3325ec35760bee1c0bfe57b7011604a124f