Skip to content

Commit 1189b56

Browse files
committed
Fix publish deadlock when no response from server
1 parent db1607e commit 1189b56

File tree

4 files changed

+77
-19
lines changed

4 files changed

+77
-19
lines changed

.changeset/chilly-kiwis-travel.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 publish deadlock when no response from server

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

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ import kotlinx.coroutines.ensureActive
5454
import kotlinx.coroutines.joinAll
5555
import kotlinx.coroutines.launch
5656
import kotlinx.coroutines.runBlocking
57+
import kotlinx.coroutines.suspendCancellableCoroutine
5758
import kotlinx.coroutines.sync.Mutex
5859
import kotlinx.coroutines.sync.withLock
60+
import kotlinx.coroutines.withTimeout
5961
import kotlinx.coroutines.withTimeoutOrNull
6062
import kotlinx.coroutines.yield
6163
import livekit.LivekitModels
@@ -87,7 +89,7 @@ import javax.inject.Singleton
8789
import kotlin.coroutines.Continuation
8890
import kotlin.coroutines.resume
8991
import kotlin.coroutines.resumeWithException
90-
import kotlin.coroutines.suspendCoroutine
92+
import kotlin.time.Duration.Companion.seconds
9193

9294
/**
9395
* @suppress
@@ -332,17 +334,19 @@ internal constructor(
332334
}
333335

334336
// Suspend until signal client receives message confirming track publication.
335-
return suspendCoroutine { cont ->
336-
synchronized(pendingTrackResolvers) {
337-
pendingTrackResolvers[cid] = cont
337+
return withTimeout(20.seconds) {
338+
suspendCancellableCoroutine { cont ->
339+
synchronized(pendingTrackResolvers) {
340+
pendingTrackResolvers[cid] = cont
341+
}
342+
client.sendAddTrack(
343+
cid = cid,
344+
name = name,
345+
type = kind,
346+
stream = stream,
347+
builder = builder,
348+
)
338349
}
339-
client.sendAddTrack(
340-
cid = cid,
341-
name = name,
342-
type = kind,
343-
stream = stream,
344-
builder = builder,
345-
)
346350
}
347351
}
348352

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,8 +1390,12 @@ internal constructor(
13901390
)
13911391
}
13921392
negotiateJob.join()
1393-
val trackInfo = publishJob.await()
1394-
LKLog.d { "published $codec for track ${track.sid}, $trackInfo" }
1393+
try {
1394+
val trackInfo = publishJob.await()
1395+
LKLog.d { "published $codec for track ${track.sid}, $trackInfo" }
1396+
} catch (e: Exception) {
1397+
LKLog.w(e) { "exception when publishing $codec for track ${track.sid}" }
1398+
}
13951399
}
13961400
}
13971401

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

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
4747
import kotlinx.coroutines.Job
4848
import kotlinx.coroutines.cancel
4949
import kotlinx.coroutines.launch
50+
import kotlinx.coroutines.test.StandardTestDispatcher
5051
import kotlinx.coroutines.test.advanceUntilIdle
5152
import livekit.LivekitModels
5253
import livekit.LivekitModels.AudioTrackFeature
@@ -67,6 +68,7 @@ import org.mockito.kotlin.argThat
6768
import org.robolectric.RobolectricTestRunner
6869
import org.robolectric.Shadows
6970
import java.nio.ByteBuffer
71+
import kotlin.time.Duration.Companion.seconds
7072

7173
@ExperimentalCoroutinesApi
7274
@RunWith(RobolectricTestRunner::class)
@@ -123,11 +125,23 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
123125
wsFactory.unregisterSignalRequestHandler(wsFactory.defaultSignalRequestHandler)
124126
wsFactory.ws.clearRequests()
125127

126-
val backgroundScope = CoroutineScope(coroutineContext + Job())
128+
val standardTestDispatcher = StandardTestDispatcher()
129+
val backgroundScope = CoroutineScope(coroutineContext + Job() + standardTestDispatcher)
127130
try {
128-
backgroundScope.launch { room.localParticipant.setMicrophoneEnabled(true) }
129-
backgroundScope.launch { room.localParticipant.setMicrophoneEnabled(true) }
131+
backgroundScope.launch {
132+
try {
133+
room.localParticipant.setMicrophoneEnabled(true)
134+
} catch (_: Exception) {
135+
}
136+
}
137+
backgroundScope.launch {
138+
try {
139+
room.localParticipant.setMicrophoneEnabled(true)
140+
} catch (_: Exception) {
141+
}
142+
}
130143

144+
standardTestDispatcher.scheduler.advanceTimeBy(1.seconds.inWholeMilliseconds)
131145
assertEquals(1, wsFactory.ws.sentRequests.size)
132146
} finally {
133147
backgroundScope.cancel()
@@ -145,10 +159,23 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
145159
wsFactory.unregisterSignalRequestHandler(wsFactory.defaultSignalRequestHandler)
146160
wsFactory.ws.clearRequests()
147161

148-
val backgroundScope = CoroutineScope(coroutineContext + Job())
162+
val standardTestDispatcher = StandardTestDispatcher()
163+
val backgroundScope = CoroutineScope(coroutineContext + Job() + standardTestDispatcher)
149164
try {
150-
backgroundScope.launch { room.localParticipant.setMicrophoneEnabled(true) }
151-
backgroundScope.launch { room.localParticipant.setCameraEnabled(true) }
165+
backgroundScope.launch {
166+
try {
167+
room.localParticipant.setMicrophoneEnabled(true)
168+
} catch (_: Exception) {
169+
}
170+
}
171+
backgroundScope.launch {
172+
try {
173+
room.localParticipant.setCameraEnabled(true)
174+
} catch (_: Exception) {
175+
}
176+
}
177+
178+
standardTestDispatcher.scheduler.advanceTimeBy(1.seconds.inWholeMilliseconds)
152179

153180
assertEquals(2, wsFactory.ws.sentRequests.size)
154181
} finally {
@@ -562,4 +589,22 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
562589

563590
assertTrue(didThrow)
564591
}
592+
593+
@Test
594+
fun publishWithNoResponseCausesException() = runTest {
595+
connect()
596+
597+
wsFactory.unregisterSignalRequestHandler(wsFactory.defaultSignalRequestHandler)
598+
var didThrow = false
599+
launch {
600+
try {
601+
room.localParticipant.publishVideoTrack(createLocalTrack())
602+
} catch (e: TrackException.PublishException) {
603+
didThrow = true
604+
}
605+
}
606+
607+
coroutineRule.dispatcher.scheduler.advanceUntilIdle()
608+
assertTrue(didThrow)
609+
}
565610
}

0 commit comments

Comments
 (0)