Skip to content

Commit 902c4d8

Browse files
authored
Fix crash when publishing disposed tracks (#758)
* Fix crash when publishing disposed tracks * Fix tests
1 parent 67be216 commit 902c4d8

File tree

6 files changed

+88
-51
lines changed

6 files changed

+88
-51
lines changed

.changeset/gold-cats-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"client-sdk-android": patch
3+
---
4+
5+
Fix crash when publishing disposed tracks

livekit-android-sdk/src/main/java/io/livekit/android/room/participant/LocalParticipant.kt

Lines changed: 77 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ internal constructor(
432432
*
433433
* @param track The track to publish.
434434
* @param options The publish options to use, or [Room.audioTrackPublishDefaults] if none is passed.
435+
* @return true if the track published successfully
435436
*/
436437
suspend fun publishAudioTrack(
437438
track: LocalAudioTrack,
@@ -441,25 +442,35 @@ internal constructor(
441442
).copy(preconnect = defaultsManager.isPrerecording),
442443
publishListener: PublishListener? = null,
443444
): Boolean {
445+
if (track.isDisposed) {
446+
LKLog.w { "Attempting to publish a disposed track, ignoring." }
447+
return false
448+
}
449+
444450
val encodings = listOf(
445451
RtpParameters.Encoding(null, true, null).apply {
446452
if (options.audioBitrate != null && options.audioBitrate > 0) {
447453
maxBitrateBps = options.audioBitrate
448454
}
449455
},
450456
)
451-
val publication = publishTrackImpl(
452-
track = track,
453-
options = options,
454-
requestConfig = {
455-
disableDtx = !options.dtx
456-
disableRed = !options.red
457-
addAllAudioFeatures(options.getFeaturesList())
458-
source = options.source?.toProto() ?: LivekitModels.TrackSource.MICROPHONE
459-
},
460-
encodings = encodings,
461-
publishListener = publishListener,
462-
)
457+
var publication: LocalTrackPublication? = null
458+
try {
459+
publication = publishTrackImpl(
460+
track = track,
461+
options = options,
462+
requestConfig = {
463+
disableDtx = !options.dtx
464+
disableRed = !options.red
465+
addAllAudioFeatures(options.getFeaturesList())
466+
source = options.source?.toProto() ?: LivekitModels.TrackSource.MICROPHONE
467+
},
468+
encodings = encodings,
469+
publishListener = publishListener,
470+
)
471+
} catch (e: TrackException.PublishException) {
472+
LKLog.e(e) { "Error thrown when publishing track:" }
473+
}
463474

464475
if (publication != null) {
465476
val job = scope.launch {
@@ -478,6 +489,7 @@ internal constructor(
478489
*
479490
* @param track The track to publish.
480491
* @param options The publish options to use, or [Room.videoTrackPublishDefaults] if none is passed.
492+
* @return true if the track published successfully
481493
*/
482494
suspend fun publishVideoTrack(
483495
track: LocalVideoTrack,
@@ -489,6 +501,17 @@ internal constructor(
489501
): Boolean {
490502
@Suppress("NAME_SHADOWING") var options = options
491503

504+
if (track.isDisposed) {
505+
LKLog.w { "Attempting to publish a disposed track, ignoring." }
506+
return false
507+
}
508+
509+
val rtcTrackId = track.withRTCTrack(null) { id() }
510+
if (rtcTrackId == null) {
511+
LKLog.w { "Attempting to publish a disposed track, ignoring." }
512+
return false
513+
}
514+
492515
synchronized(enabledPublishVideoCodecs) {
493516
if (enabledPublishVideoCodecs.isNotEmpty()) {
494517
if (enabledPublishVideoCodecs.none { allowedCodec -> allowedCodec.mime.mimeTypeToVideoCodec() == options.videoCodec }) {
@@ -522,40 +545,47 @@ internal constructor(
522545
val videoLayers =
523546
EncodingUtils.videoLayersFromEncodings(track.dimensions.width, track.dimensions.height, encodings, isSVC)
524547

525-
return publishTrackImpl(
526-
track = track,
527-
options = options,
528-
requestConfig = {
529-
width = track.dimensions.width
530-
height = track.dimensions.height
531-
source = options.source?.toProto() ?: if (track.options.isScreencast) {
532-
LivekitModels.TrackSource.SCREEN_SHARE
533-
} else {
534-
LivekitModels.TrackSource.CAMERA
535-
}
536-
addAllLayers(videoLayers)
548+
var publication: LocalTrackPublication? = null
549+
try {
550+
publication = publishTrackImpl(
551+
track = track,
552+
options = options,
553+
requestConfig = {
554+
width = track.dimensions.width
555+
height = track.dimensions.height
556+
source = options.source?.toProto() ?: if (track.options.isScreencast) {
557+
LivekitModels.TrackSource.SCREEN_SHARE
558+
} else {
559+
LivekitModels.TrackSource.CAMERA
560+
}
561+
addAllLayers(videoLayers)
537562

538-
addSimulcastCodecs(
539-
with(SimulcastCodec.newBuilder()) {
540-
codec = options.videoCodec
541-
cid = track.rtcTrack.id()
542-
build()
543-
},
544-
)
545-
// set up backup codec
546-
if (options.backupCodec?.codec != null && options.videoCodec != options.backupCodec?.codec) {
547563
addSimulcastCodecs(
548564
with(SimulcastCodec.newBuilder()) {
549-
codec = options.backupCodec!!.codec
550-
cid = ""
565+
codec = options.videoCodec
566+
cid = rtcTrackId
551567
build()
552568
},
553569
)
554-
}
555-
},
556-
encodings = encodings,
557-
publishListener = publishListener,
558-
) != null
570+
// set up backup codec
571+
if (options.backupCodec?.codec != null && options.videoCodec != options.backupCodec?.codec) {
572+
addSimulcastCodecs(
573+
with(SimulcastCodec.newBuilder()) {
574+
codec = options.backupCodec!!.codec
575+
cid = ""
576+
build()
577+
},
578+
)
579+
}
580+
},
581+
encodings = encodings,
582+
publishListener = publishListener,
583+
)
584+
} catch (e: TrackException.PublishException) {
585+
LKLog.e(e) { "Error thrown when publishing track:" }
586+
}
587+
588+
return publication != null
559589
}
560590

561591
private fun hasPermissionsToPublish(source: Track.Source): Boolean {
@@ -581,13 +611,19 @@ internal constructor(
581611
* @throws TrackException.PublishException thrown when the publish fails. see [TrackException.PublishException.message] for details.
582612
* @return true if the track publish was successful.
583613
*/
614+
@Throws(TrackException.PublishException::class)
584615
private suspend fun publishTrackImpl(
585616
track: Track,
586617
options: TrackPublishOptions,
587618
requestConfig: AddTrackRequest.Builder.() -> Unit,
588619
encodings: List<RtpParameters.Encoding> = emptyList(),
589620
publishListener: PublishListener? = null,
590621
): LocalTrackPublication? {
622+
if (track.isDisposed) {
623+
LKLog.w { "Attempting to publish a disposed track, ignoring." }
624+
return null
625+
}
626+
591627
fun onPublishFailure(e: TrackException.PublishException, triggerEvent: Boolean = true) {
592628
publishListener?.onPublishFailure(e)
593629
if (triggerEvent) {
@@ -1164,6 +1200,7 @@ internal constructor(
11641200
continuation.cancel(RpcError.BuiltinRpcError.RESPONSE_TIMEOUT.create())
11651201
}
11661202
}
1203+
responseTimeoutJob // workaround for lint marking this unused. used in cleanup()
11671204

11681205
pendingResponses[requestId] = PendingRpcResponse(
11691206
participantIdentity = destinationIdentity,

livekit-android-sdk/src/main/java/io/livekit/android/room/track/Track.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ abstract class Track(
191191
}
192192
}
193193

194+
/**
195+
* Ensures the track is valid before attempting to run [action].
196+
*/
194197
@OptIn(ExperimentalContracts::class)
195198
internal inline fun <T> withRTCTrack(crossinline action: MediaStreamTrack.() -> T) {
196199
contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) }

livekit-android-test/src/main/java/io/livekit/android/test/mock/MockRTCThreadToken.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ import io.livekit.android.webrtc.peerconnection.RTCThreadToken
2020

2121
class MockRTCThreadToken : RTCThreadToken {
2222
override val isDisposed: Boolean
23-
get() = true
23+
get() = false
2424
}

livekit-android-test/src/main/java/io/livekit/android/test/mock/dagger/TestRTCModule.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ object TestRTCModule {
9090
appContext: Context,
9191
): PeerConnectionFactory {
9292
WebRTCInitializer.initialize(appContext)
93-
9493
return MockPeerConnectionFactory()
9594
}
9695

livekit-android-test/src/test/java/io/livekit/android/room/participant/LocalParticipantMockE2ETest.kt

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,7 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
789789
}
790790

791791
@Test
792-
fun lackOfPublishPermissionCausesException() = runTest {
792+
fun lackOfPublishPermissionReturnsFalse() = runTest {
793793
val noCanPublishJoin = with(TestData.JOIN.toBuilder()) {
794794
join = with(join.toBuilder()) {
795795
participant = with(participant.toBuilder()) {
@@ -805,14 +805,7 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
805805
}
806806
connect(noCanPublishJoin)
807807

808-
var didThrow = false
809-
try {
810-
room.localParticipant.publishVideoTrack(createLocalTrack())
811-
} catch (e: TrackException.PublishException) {
812-
didThrow = true
813-
}
814-
815-
assertTrue(didThrow)
808+
assertFalse(room.localParticipant.publishVideoTrack(createLocalTrack()))
816809
}
817810

818811
@Test

0 commit comments

Comments
 (0)