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 efae279a..04f405bd 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 5daa840d..ee70db5e 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 00000000..1fb38448 --- /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 8381f218..4c05a332 160000 --- a/protocol +++ b/protocol @@ -1 +1 @@ -Subproject commit 8381f2180c45ab926b3ebf19df0608f1dadcac1e +Subproject commit 4c05a3325ec35760bee1c0bfe57b7011604a124f