继续操作前请注册或者登录。
davidliu
Committed by GitHub

Fast track publication support (#612)

* protocol update

* Fast track publication support
  1 +---
  2 +"client-sdk-android": minor
  3 +---
  4 +
  5 +Fast track publication support
1 <component name="InspectionProjectProfileManager"> 1 <component name="InspectionProjectProfileManager">
2 <profile version="1.0"> 2 <profile version="1.0">
3 <option name="myName" value="Project Default" /> 3 <option name="myName" value="Project Default" />
  4 + <inspection_tool class="AndroidLintVisibleForTests" enabled="true" level="WARNING" enabled_by_default="true">
  5 + <scope name="Library Projects" level="WARNING" enabled="false" />
  6 + </inspection_tool>
  7 + <inspection_tool class="MemberVisibilityCanBePrivate" enabled="true" level="WEAK WARNING" enabled_by_default="true">
  8 + <scope name="Library Projects" level="WEAK WARNING" enabled="false" />
  9 + </inspection_tool>
4 <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true"> 10 <inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
5 <option name="composableFile" value="true" /> 11 <option name="composableFile" value="true" />
6 <option name="previewFile" value="true" /> 12 <option name="previewFile" value="true" />
@@ -37,5 +43,8 @@ @@ -37,5 +43,8 @@
37 <option name="composableFile" value="true" /> 43 <option name="composableFile" value="true" />
38 <option name="previewFile" value="true" /> 44 <option name="previewFile" value="true" />
39 </inspection_tool> 45 </inspection_tool>
  46 + <inspection_tool class="UnusedSymbol" enabled="true" level="WARNING" enabled_by_default="true">
  47 + <scope name="Library Projects" level="WARNING" enabled="false" />
  48 + </inspection_tool>
40 </profile> 49 </profile>
41 </component> 50 </component>
  1 +<component name="DependencyValidationManager">
  2 + <scope name="Library Projects" pattern="file[livekit-android.livekit-android-sdk*]:*//*||file[livekit-android.livekit-android-camerax*]:*//*||file[livekit-android.livekit-android-test*]:*//*||file[livekit-android.livekit-lint*]:*//*" />
  3 +</component>
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -131,7 +131,7 @@ constructor( @@ -131,7 +131,7 @@ constructor(
131 return result 131 return result
132 } 132 }
133 133
134 - val negotiate = debounce<MediaConstraints?, Unit>(100, coroutineScope) { 134 + val negotiate = debounce<MediaConstraints?, Unit>(20, coroutineScope) {
135 if (it != null) { 135 if (it != null) {
136 createAndSendOffer(it) 136 createAndSendOffer(it)
137 } else { 137 } else {
@@ -156,7 +156,7 @@ internal constructor( @@ -156,7 +156,7 @@ internal constructor(
156 private val publisherObserver = PublisherTransportObserver(this, client) 156 private val publisherObserver = PublisherTransportObserver(this, client)
157 private val subscriberObserver = SubscriberTransportObserver(this, client) 157 private val subscriberObserver = SubscriberTransportObserver(this, client)
158 158
159 - private var publisher: PeerConnectionTransport? = null 159 + internal var publisher: PeerConnectionTransport? = null
160 private var subscriber: PeerConnectionTransport? = null 160 private var subscriber: PeerConnectionTransport? = null
161 161
162 private var reliableDataChannel: DataChannel? = null 162 private var reliableDataChannel: DataChannel? = null
@@ -214,7 +214,7 @@ internal constructor( @@ -214,7 +214,7 @@ internal constructor(
214 configure(joinResponse, options) 214 configure(joinResponse, options)
215 215
216 // create offer 216 // create offer
217 - if (!isSubscriberPrimary) { 217 + if (!isSubscriberPrimary || joinResponse.fastPublish) {
218 negotiatePublisher() 218 negotiatePublisher()
219 } 219 }
220 client.onReadyForResponses() 220 client.onReadyForResponses()
@@ -1082,6 +1082,10 @@ internal constructor( @@ -1082,6 +1082,10 @@ internal constructor(
1082 LivekitModels.DataPacket.ValueCase.STREAM_CHUNK -> { 1082 LivekitModels.DataPacket.ValueCase.STREAM_CHUNK -> {
1083 // TODO 1083 // TODO
1084 } 1084 }
  1085 +
  1086 + LivekitModels.DataPacket.ValueCase.STREAM_TRAILER -> {
  1087 + // TODO
  1088 + }
1085 } 1089 }
1086 } 1090 }
1087 1091
@@ -585,6 +585,7 @@ constructor( @@ -585,6 +585,7 @@ constructor(
585 } 585 }
586 586
587 localParticipant.updateFromInfo(response.participant) 587 localParticipant.updateFromInfo(response.participant)
  588 + localParticipant.setEnabledPublishCodecs(response.enabledPublishCodecsList)
588 589
589 if (response.otherParticipantsList.isNotEmpty()) { 590 if (response.otherParticipantsList.isNotEmpty()) {
590 response.otherParticipantsList.forEach { info -> 591 response.otherParticipantsList.forEach { info ->
@@ -57,6 +57,7 @@ import io.livekit.android.util.flow @@ -57,6 +57,7 @@ import io.livekit.android.util.flow
57 import io.livekit.android.webrtc.sortVideoCodecPreferences 57 import io.livekit.android.webrtc.sortVideoCodecPreferences
58 import kotlinx.coroutines.CoroutineDispatcher 58 import kotlinx.coroutines.CoroutineDispatcher
59 import kotlinx.coroutines.Job 59 import kotlinx.coroutines.Job
  60 +import kotlinx.coroutines.async
60 import kotlinx.coroutines.coroutineScope 61 import kotlinx.coroutines.coroutineScope
61 import kotlinx.coroutines.delay 62 import kotlinx.coroutines.delay
62 import kotlinx.coroutines.launch 63 import kotlinx.coroutines.launch
@@ -64,7 +65,9 @@ import kotlinx.coroutines.suspendCancellableCoroutine @@ -64,7 +65,9 @@ import kotlinx.coroutines.suspendCancellableCoroutine
64 import kotlinx.coroutines.sync.Mutex 65 import kotlinx.coroutines.sync.Mutex
65 import kotlinx.coroutines.sync.withLock 66 import kotlinx.coroutines.sync.withLock
66 import livekit.LivekitModels 67 import livekit.LivekitModels
  68 +import livekit.LivekitModels.Codec
67 import livekit.LivekitModels.DataPacket 69 import livekit.LivekitModels.DataPacket
  70 +import livekit.LivekitModels.TrackInfo
68 import livekit.LivekitRtc 71 import livekit.LivekitRtc
69 import livekit.LivekitRtc.AddTrackRequest 72 import livekit.LivekitRtc.AddTrackRequest
70 import livekit.LivekitRtc.SimulcastCodec 73 import livekit.LivekitRtc.SimulcastCodec
@@ -127,8 +130,9 @@ internal constructor( @@ -127,8 +130,9 @@ internal constructor(
127 // For ensuring that only one caller can execute setTrackEnabled at a time. 130 // For ensuring that only one caller can execute setTrackEnabled at a time.
128 // Without it, there's a potential to create multiple of the same source, 131 // Without it, there's a potential to create multiple of the same source,
129 // Camera has deadlock issues with multiple CameraCapturers trying to activate/stop. 132 // Camera has deadlock issues with multiple CameraCapturers trying to activate/stop.
130 - private val sourcePubLocks = Track.Source.values()  
131 - .associate { source -> source to Mutex() } 133 + private val sourcePubLocks = Track.Source.entries.associateWith { Mutex() }
  134 +
  135 + internal val enabledPublishVideoCodecs = Collections.synchronizedList(mutableListOf<Codec>())
132 136
133 /** 137 /**
134 * Creates an audio track, recording audio through the microphone with the given [options]. 138 * Creates an audio track, recording audio through the microphone with the given [options].
@@ -405,9 +409,26 @@ internal constructor( @@ -405,9 +409,26 @@ internal constructor(
405 options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults), 409 options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults),
406 publishListener: PublishListener? = null, 410 publishListener: PublishListener? = null,
407 ) { 411 ) {
  412 + @Suppress("NAME_SHADOWING") var options = options
  413 +
  414 + synchronized(enabledPublishVideoCodecs) {
  415 + if (enabledPublishVideoCodecs.isNotEmpty()) {
  416 + if (enabledPublishVideoCodecs.none { allowedCodec -> allowedCodec.mime.mimeTypeToVideoCodec() == options.videoCodec }) {
  417 + val oldCodec = options.videoCodec
  418 + val newCodec = enabledPublishVideoCodecs
  419 + .firstOrNull { it.mime.mimeTypeToVideoCodec() != null }
  420 + ?.mime?.mimeTypeToVideoCodec()
  421 +
  422 + if (newCodec != null) {
  423 + LKLog.w { "$oldCodec not enabled on server, falling back to supported codec $newCodec" }
  424 + options = options.copy(videoCodec = newCodec)
  425 + }
  426 + }
  427 + }
  428 + }
  429 +
408 val isSVC = isSVCCodec(options.videoCodec) 430 val isSVC = isSVCCodec(options.videoCodec)
409 431
410 - @Suppress("NAME_SHADOWING") var options = options  
411 if (isSVC) { 432 if (isSVC) {
412 dynacast = true 433 dynacast = true
413 434
@@ -478,84 +499,122 @@ internal constructor( @@ -478,84 +499,122 @@ internal constructor(
478 return null 499 return null
479 } 500 }
480 501
481 - val cid = track.rtcTrack.id()  
482 - val builder = AddTrackRequest.newBuilder().apply {  
483 - this.requestConfig() 502 + if (engine.connectionState == ConnectionState.DISCONNECTED) {
  503 + publishListener?.onPublishFailure(TrackException.PublishException("Not connected!"))
484 } 504 }
485 505
486 - val trackInfo = try {  
487 - engine.addTrack(  
488 - cid = cid,  
489 - name = options.name ?: track.name,  
490 - kind = track.kind.toProto(),  
491 - stream = options.stream,  
492 - builder = builder, 506 + val cid = track.rtcTrack.id()
  507 +
  508 + // For fast publish, we can negotiate PC and request add track at the same time
  509 + suspend fun negotiate() {
  510 + if (this.engine.publisher == null) {
  511 + throw IllegalStateException("publisher is not configured yet!")
  512 + }
  513 +
  514 + val transInit = RtpTransceiverInit(
  515 + RtpTransceiver.RtpTransceiverDirection.SEND_ONLY,
  516 + listOf(this.sid.value),
  517 + encodings,
493 ) 518 )
494 - } catch (e: Exception) {  
495 - publishListener?.onPublishFailure(TrackException.PublishException("Failed to publish track", e))  
496 - return null  
497 - } 519 + val transceiver = engine.createSenderTransceiver(track.rtcTrack, transInit)
498 520
499 - if (options is VideoTrackPublishOptions) {  
500 - // server might not support the codec the client has requested, in that case, fallback  
501 - // to a supported codec  
502 - val primaryCodecMime = trackInfo.codecsList.firstOrNull()?.mimeType 521 + when (track) {
  522 + is LocalVideoTrack -> track.transceiver = transceiver
  523 + is LocalAudioTrack -> track.transceiver = transceiver
  524 + else -> {
  525 + throw IllegalArgumentException("Trying to publish a non local track of type ${track.javaClass}")
  526 + }
  527 + }
503 528
504 - if (primaryCodecMime != null) {  
505 - val updatedCodec = primaryCodecMime.mimeTypeToVideoCodec()  
506 - if (updatedCodec != null && updatedCodec != options.videoCodec) {  
507 - LKLog.d { "falling back to server selected codec: $updatedCodec" }  
508 - options = options.copy(videoCodec = updatedCodec) 529 + if (transceiver == null) {
  530 + val exception = TrackException.PublishException("null sender returned from peer connection")
  531 + publishListener?.onPublishFailure(exception)
  532 + throw exception
  533 + }
509 534
510 - // recompute encodings since bitrates/etc could have changed  
511 - encodings = computeVideoEncodings((track as LocalVideoTrack).dimensions, options) 535 + track.statsGetter = engine.createStatsGetter(transceiver.sender)
  536 +
  537 + val finalOptions = options
  538 + // Handle trackBitrates
  539 + if (encodings.isNotEmpty()) {
  540 + if (finalOptions is VideoTrackPublishOptions && isSVCCodec(finalOptions.videoCodec) && encodings.firstOrNull()?.maxBitrateBps != null) {
  541 + engine.registerTrackBitrateInfo(
  542 + cid = cid,
  543 + TrackBitrateInfo(
  544 + codec = finalOptions.videoCodec,
  545 + maxBitrate = (encodings.first().maxBitrateBps?.div(1000) ?: 0).toLong(),
  546 + ),
  547 + )
512 } 548 }
513 } 549 }
514 - }  
515 550
516 - val transInit = RtpTransceiverInit(  
517 - RtpTransceiver.RtpTransceiverDirection.SEND_ONLY,  
518 - listOf(this.sid.value),  
519 - encodings,  
520 - )  
521 - val transceiver = engine.createSenderTransceiver(track.rtcTrack, transInit) 551 + if (finalOptions is VideoTrackPublishOptions) {
  552 + // Set preferred video codec order
  553 + transceiver.sortVideoCodecPreferences(finalOptions.videoCodec, capabilitiesGetter)
  554 + (track as LocalVideoTrack).codec = finalOptions.videoCodec
522 555
523 - when (track) {  
524 - is LocalVideoTrack -> track.transceiver = transceiver  
525 - is LocalAudioTrack -> track.transceiver = transceiver  
526 - else -> {  
527 - throw IllegalArgumentException("Trying to publish a non local track of type ${track.javaClass}") 556 + val rtpParameters = transceiver.sender.parameters
  557 + rtpParameters.degradationPreference = finalOptions.degradationPreference
  558 + transceiver.sender.parameters = rtpParameters
528 } 559 }
529 - }  
530 560
531 - if (transceiver == null) {  
532 - publishListener?.onPublishFailure(TrackException.PublishException("null sender returned from peer connection"))  
533 - return null 561 + // PublisherTransportObserver.onRenegotiationNeeded() gets triggered automatically
  562 + // so no need to call negotiate manually.
534 } 563 }
535 564
536 - track.statsGetter = engine.createStatsGetter(transceiver.sender) 565 + suspend fun requestAddTrack(): TrackInfo {
  566 + val builder = AddTrackRequest.newBuilder().apply {
  567 + this.requestConfig()
  568 + }
537 569
538 - // Handle trackBitrates  
539 - if (encodings.isNotEmpty()) {  
540 - if (options is VideoTrackPublishOptions && isSVCCodec(options.videoCodec) && encodings.firstOrNull()?.maxBitrateBps != null) {  
541 - engine.registerTrackBitrateInfo( 570 + return try {
  571 + engine.addTrack(
542 cid = cid, 572 cid = cid,
543 - TrackBitrateInfo(  
544 - codec = options.videoCodec,  
545 - maxBitrate = (encodings.first().maxBitrateBps?.div(1000) ?: 0).toLong(),  
546 - ), 573 + name = options.name ?: track.name,
  574 + kind = track.kind.toProto(),
  575 + stream = options.stream,
  576 + builder = builder,
547 ) 577 )
  578 + } catch (e: Exception) {
  579 + val exception = TrackException.PublishException("Failed to publish track", e)
  580 + publishListener?.onPublishFailure(exception)
  581 + throw exception
548 } 582 }
549 } 583 }
550 584
551 - if (options is VideoTrackPublishOptions) {  
552 - // Set preferred video codec order  
553 - transceiver.sortVideoCodecPreferences(options.videoCodec, capabilitiesGetter)  
554 - (track as LocalVideoTrack).codec = options.videoCodec 585 + val trackInfo: TrackInfo
  586 + if (enabledPublishVideoCodecs.isNotEmpty()) {
  587 + // Can simultaneous publish and negotiate.
  588 + // codec is pre-verified in publishVideoTrack
  589 + trackInfo = coroutineScope {
  590 + val negotiateJob = launch { negotiate() }
  591 + val publishJob = async { requestAddTrack() }
  592 +
  593 + negotiateJob.join()
  594 + return@coroutineScope publishJob.await()
  595 + }
  596 + } else {
  597 + // legacy path.
  598 + trackInfo = requestAddTrack()
  599 +
  600 + if (options is VideoTrackPublishOptions) {
  601 + // server might not support the codec the client has requested, in that case, fallback
  602 + // to a supported codec
  603 + val primaryCodecMime = trackInfo.codecsList.firstOrNull()?.mimeType
  604 +
  605 + if (primaryCodecMime != null) {
  606 + val updatedCodec = primaryCodecMime.mimeTypeToVideoCodec()
  607 + if (updatedCodec != null && updatedCodec != options.videoCodec) {
  608 + LKLog.d { "falling back to server selected codec: $updatedCodec" }
  609 + options = options.copy(videoCodec = updatedCodec)
  610 +
  611 + // recompute encodings since bitrates/etc could have changed
  612 + encodings = computeVideoEncodings((track as LocalVideoTrack).dimensions, options)
  613 + }
  614 + }
  615 + }
555 616
556 - val rtpParameters = transceiver.sender.parameters  
557 - rtpParameters.degradationPreference = options.degradationPreference  
558 - transceiver.sender.parameters = rtpParameters 617 + negotiate()
559 } 618 }
560 619
561 val publication = LocalTrackPublication( 620 val publication = LocalTrackPublication(
@@ -1266,13 +1325,8 @@ internal constructor( @@ -1266,13 +1325,8 @@ internal constructor(
1266 LKLog.w { "couldn't create new transceiver! $codec" } 1325 LKLog.w { "couldn't create new transceiver! $codec" }
1267 return@launch 1326 return@launch
1268 } 1327 }
1269 - transceiver.sortVideoCodecPreferences(newOptions.videoCodec, capabilitiesGetter)  
1270 - simulcastTrack.sender = transceiver.sender  
1271 -  
1272 val trackRequest = AddTrackRequest.newBuilder().apply { 1328 val trackRequest = AddTrackRequest.newBuilder().apply {
1273 - cid = transceiver.sender.id()  
1274 sid = existingPublication.sid 1329 sid = existingPublication.sid
1275 - type = track.kind.toProto()  
1276 muted = !track.enabled 1330 muted = !track.enabled
1277 source = existingPublication.source.toProto() 1331 source = existingPublication.source.toProto()
1278 addSimulcastCodecs( 1332 addSimulcastCodecs(
@@ -1291,17 +1345,23 @@ internal constructor( @@ -1291,17 +1345,23 @@ internal constructor(
1291 ), 1345 ),
1292 ) 1346 )
1293 } 1347 }
  1348 + val negotiateJob = launch {
  1349 + transceiver.sortVideoCodecPreferences(newOptions.videoCodec, capabilitiesGetter)
  1350 + simulcastTrack.sender = transceiver.sender
1294 1351
1295 - val trackInfo = engine.addTrack(  
1296 - cid = simulcastTrack.rtcTrack.id(),  
1297 - name = existingPublication.name,  
1298 - kind = existingPublication.kind.toProto(),  
1299 - stream = options.stream,  
1300 - builder = trackRequest,  
1301 - )  
1302 -  
1303 - engine.negotiatePublisher()  
1304 - 1352 + engine.negotiatePublisher()
  1353 + }
  1354 + val publishJob = async {
  1355 + engine.addTrack(
  1356 + cid = simulcastTrack.rtcTrack.id(),
  1357 + name = existingPublication.name,
  1358 + kind = existingPublication.kind.toProto(),
  1359 + stream = options.stream,
  1360 + builder = trackRequest,
  1361 + )
  1362 + }
  1363 + negotiateJob.join()
  1364 + val trackInfo = publishJob.await()
1305 LKLog.d { "published $codec for track ${track.sid}, $trackInfo" } 1365 LKLog.d { "published $codec for track ${track.sid}, $trackInfo" }
1306 } 1366 }
1307 } 1367 }
@@ -1360,6 +1420,20 @@ internal constructor( @@ -1360,6 +1420,20 @@ internal constructor(
1360 eventBus.postEvent(ParticipantEvent.LocalTrackSubscribed(this, publication), scope) 1420 eventBus.postEvent(ParticipantEvent.LocalTrackSubscribed(this, publication), scope)
1361 } 1421 }
1362 1422
  1423 + internal fun setEnabledPublishCodecs(codecs: List<Codec>) {
  1424 + synchronized(enabledPublishVideoCodecs) {
  1425 + enabledPublishVideoCodecs.clear()
  1426 + enabledPublishVideoCodecs.addAll(
  1427 + codecs.filter { codec ->
  1428 + codec.mime.split('/')
  1429 + .takeIf { it.isNotEmpty() }
  1430 + ?.get(0)
  1431 + ?.lowercase() == "video"
  1432 + },
  1433 + )
  1434 + }
  1435 + }
  1436 +
1363 /** 1437 /**
1364 * @suppress 1438 * @suppress
1365 */ 1439 */
@@ -1386,6 +1460,7 @@ internal constructor( @@ -1386,6 +1460,7 @@ internal constructor(
1386 */ 1460 */
1387 override fun dispose() { 1461 override fun dispose() {
1388 cleanup() 1462 cleanup()
  1463 + enabledPublishVideoCodecs.clear()
1389 super.dispose() 1464 super.dispose()
1390 } 1465 }
1391 1466
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -85,7 +85,7 @@ enum class VideoCodec(val codecName: String) { @@ -85,7 +85,7 @@ enum class VideoCodec(val codecName: String) {
85 85
86 companion object { 86 companion object {
87 fun fromCodecName(codecName: String): VideoCodec { 87 fun fromCodecName(codecName: String): VideoCodec {
88 - return VideoCodec.values().first { it.codecName.equals(codecName, ignoreCase = true) } 88 + return entries.first { it.codecName.equals(codecName, ignoreCase = true) }
89 } 89 }
90 } 90 }
91 } 91 }
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -18,9 +18,12 @@ package io.livekit.android.test.mock @@ -18,9 +18,12 @@ package io.livekit.android.test.mock
18 18
19 import livekit.org.webrtc.RtpReceiver 19 import livekit.org.webrtc.RtpReceiver
20 import org.mockito.Mockito 20 import org.mockito.Mockito
  21 +import org.mockito.kotlin.whenever
21 22
22 object MockRtpReceiver { 23 object MockRtpReceiver {
23 - fun create(): RtpReceiver {  
24 - return Mockito.mock(RtpReceiver::class.java) 24 + fun create(id: String = "receiver_id"): RtpReceiver {
  25 + return Mockito.mock(RtpReceiver::class.java).apply {
  26 + whenever(this.id()).thenReturn(id)
  27 + }
25 } 28 }
26 } 29 }
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@ import org.mockito.kotlin.whenever @@ -25,7 +25,7 @@ import org.mockito.kotlin.whenever
25 import java.util.UUID 25 import java.util.UUID
26 26
27 object MockRtpSender { 27 object MockRtpSender {
28 - fun create(): RtpSender { 28 + fun create(id: String = "sender_id"): RtpSender {
29 var rtpParameters: RtpParameters = MockRtpParameters( 29 var rtpParameters: RtpParameters = MockRtpParameters(
30 transactionId = UUID.randomUUID().toString(), 30 transactionId = UUID.randomUUID().toString(),
31 degradationPreference = null, 31 degradationPreference = null,
@@ -36,6 +36,7 @@ object MockRtpSender { @@ -36,6 +36,7 @@ object MockRtpSender {
36 ) 36 )
37 return Mockito.mock(RtpSender::class.java).apply { 37 return Mockito.mock(RtpSender::class.java).apply {
38 whenever(this.parameters).thenAnswer { rtpParameters } 38 whenever(this.parameters).thenAnswer { rtpParameters }
  39 + whenever(this.id()).thenReturn(id)
39 whenever(this.setParameters(any())).thenAnswer { 40 whenever(this.setParameters(any())).thenAnswer {
40 rtpParameters = it.getArgument(0) 41 rtpParameters = it.getArgument(0)
41 true 42 true
1 /* 1 /*
2 - * Copyright 2023-2024 LiveKit, Inc. 2 + * Copyright 2023-2025 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import io.livekit.android.test.mock.MockRtpReceiver @@ -20,6 +20,7 @@ import io.livekit.android.test.mock.MockRtpReceiver
20 import io.livekit.android.test.mock.MockRtpSender 20 import io.livekit.android.test.mock.MockRtpSender
21 import livekit.org.webrtc.RtpTransceiver.RtpTransceiverDirection 21 import livekit.org.webrtc.RtpTransceiver.RtpTransceiverDirection
22 import org.mockito.Mockito 22 import org.mockito.Mockito
  23 +import java.util.UUID
23 24
24 object MockRtpTransceiver { 25 object MockRtpTransceiver {
25 fun create( 26 fun create(
@@ -27,7 +28,7 @@ object MockRtpTransceiver { @@ -27,7 +28,7 @@ object MockRtpTransceiver {
27 init: RtpTransceiver.RtpTransceiverInit = RtpTransceiver.RtpTransceiverInit(), 28 init: RtpTransceiver.RtpTransceiverInit = RtpTransceiver.RtpTransceiverInit(),
28 ): RtpTransceiver { 29 ): RtpTransceiver {
29 val mock = Mockito.mock(RtpTransceiver::class.java) 30 val mock = Mockito.mock(RtpTransceiver::class.java)
30 - 31 + val id = UUID.randomUUID().toString()
31 Mockito.`when`(mock.mediaType).then { 32 Mockito.`when`(mock.mediaType).then {
32 return@then when (track.kind()) { 33 return@then when (track.kind()) {
33 MediaStreamTrack.AUDIO_TRACK_KIND -> MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO 34 MediaStreamTrack.AUDIO_TRACK_KIND -> MediaStreamTrack.MediaType.MEDIA_TYPE_AUDIO
@@ -40,7 +41,7 @@ object MockRtpTransceiver { @@ -40,7 +41,7 @@ object MockRtpTransceiver {
40 41
41 when (direction) { 42 when (direction) {
42 RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.SEND_ONLY -> { 43 RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.SEND_ONLY -> {
43 - val sender = MockRtpSender.create() 44 + val sender = MockRtpSender.create(id = id)
44 Mockito.`when`(mock.sender) 45 Mockito.`when`(mock.sender)
45 .then { sender } 46 .then { sender }
46 } 47 }
@@ -50,7 +51,7 @@ object MockRtpTransceiver { @@ -50,7 +51,7 @@ object MockRtpTransceiver {
50 51
51 when (direction) { 52 when (direction) {
52 RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.RECV_ONLY -> { 53 RtpTransceiverDirection.SEND_RECV, RtpTransceiverDirection.RECV_ONLY -> {
53 - val receiver = MockRtpReceiver.create() 54 + val receiver = MockRtpReceiver.create(id = id)
54 Mockito.`when`(mock.receiver) 55 Mockito.`when`(mock.receiver)
55 .then { receiver } 56 .then { receiver }
56 } 57 }
1 -Subproject commit 9e8d1e37c5eb4434424bc16c657c83e7dc63bc2a 1 +Subproject commit 02ee5e6947593443d0dfc90cae0b27ce03b6c1fe