davidliu
Committed by GitHub

Force webrtc method calls onto a single thread (#342)

* Force all rtc related calls onto a single dedicated thread

* Fix test issues

* clean up long lock

* Move any callbacks from peerconnection api into local RTC thread

* Spotless
正在显示 18 个修改的文件 包含 313 行增加72 行删除
@@ -18,10 +18,10 @@ package io.livekit.android.room @@ -18,10 +18,10 @@ package io.livekit.android.room
18 18
19 import android.javax.sdp.MediaDescription 19 import android.javax.sdp.MediaDescription
20 import android.javax.sdp.SdpFactory 20 import android.javax.sdp.SdpFactory
  21 +import androidx.annotation.VisibleForTesting
21 import dagger.assisted.Assisted 22 import dagger.assisted.Assisted
22 import dagger.assisted.AssistedFactory 23 import dagger.assisted.AssistedFactory
23 import dagger.assisted.AssistedInject 24 import dagger.assisted.AssistedInject
24 -import io.livekit.android.coroutines.withReentrantLock  
25 import io.livekit.android.dagger.InjectionNames 25 import io.livekit.android.dagger.InjectionNames
26 import io.livekit.android.room.util.* 26 import io.livekit.android.room.util.*
27 import io.livekit.android.util.Either 27 import io.livekit.android.util.Either
@@ -34,11 +34,12 @@ import io.livekit.android.webrtc.getFmtps @@ -34,11 +34,12 @@ import io.livekit.android.webrtc.getFmtps
34 import io.livekit.android.webrtc.getMsid 34 import io.livekit.android.webrtc.getMsid
35 import io.livekit.android.webrtc.getRtps 35 import io.livekit.android.webrtc.getRtps
36 import io.livekit.android.webrtc.isConnected 36 import io.livekit.android.webrtc.isConnected
  37 +import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread
  38 +import io.livekit.android.webrtc.peerconnection.launchBlockingOnRTCThread
37 import kotlinx.coroutines.CoroutineDispatcher 39 import kotlinx.coroutines.CoroutineDispatcher
38 import kotlinx.coroutines.CoroutineScope 40 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.SupervisorJob 41 import kotlinx.coroutines.SupervisorJob
40 import kotlinx.coroutines.runBlocking 42 import kotlinx.coroutines.runBlocking
41 -import kotlinx.coroutines.sync.Mutex  
42 import org.webrtc.* 43 import org.webrtc.*
43 import org.webrtc.PeerConnection.RTCConfiguration 44 import org.webrtc.PeerConnection.RTCConfiguration
44 import org.webrtc.PeerConnection.SignalingState 45 import org.webrtc.PeerConnection.SignalingState
@@ -64,7 +65,9 @@ constructor( @@ -64,7 +65,9 @@ constructor(
64 private val sdpFactory: SdpFactory, 65 private val sdpFactory: SdpFactory,
65 ) { 66 ) {
66 private val coroutineScope = CoroutineScope(ioDispatcher + SupervisorJob()) 67 private val coroutineScope = CoroutineScope(ioDispatcher + SupervisorJob())
67 - private val peerConnection: PeerConnection = connectionFactory.createPeerConnection( 68 +
  69 + @VisibleForTesting
  70 + internal val peerConnection: PeerConnection = connectionFactory.createPeerConnection(
68 config, 71 config,
69 pcObserver, 72 pcObserver,
70 ) ?: throw IllegalStateException("peer connection creation failed?") 73 ) ?: throw IllegalStateException("peer connection creation failed?")
@@ -73,8 +76,6 @@ constructor( @@ -73,8 +76,6 @@ constructor(
73 76
74 private var renegotiate = false 77 private var renegotiate = false
75 78
76 - private val mutex = Mutex()  
77 -  
78 private var trackBitrates = mutableMapOf<TrackBitrateInfoKey, TrackBitrateInfo>() 79 private var trackBitrates = mutableMapOf<TrackBitrateInfoKey, TrackBitrateInfo>()
79 private var isClosed = AtomicBoolean(false) 80 private var isClosed = AtomicBoolean(false)
80 81
@@ -83,8 +84,7 @@ constructor( @@ -83,8 +84,7 @@ constructor(
83 } 84 }
84 85
85 fun addIceCandidate(candidate: IceCandidate) { 86 fun addIceCandidate(candidate: IceCandidate) {
86 - runBlocking {  
87 - withNotClosedLock { 87 + executeRTCIfNotClosed {
88 if (peerConnection.remoteDescription != null && !restartingIce) { 88 if (peerConnection.remoteDescription != null && !restartingIce) {
89 peerConnection.addIceCandidate(candidate) 89 peerConnection.addIceCandidate(candidate)
90 } else { 90 } else {
@@ -92,16 +92,15 @@ constructor( @@ -92,16 +92,15 @@ constructor(
92 } 92 }
93 } 93 }
94 } 94 }
95 - }  
96 95
97 suspend fun <T> withPeerConnection(action: suspend PeerConnection.() -> T): T? { 96 suspend fun <T> withPeerConnection(action: suspend PeerConnection.() -> T): T? {
98 - return withNotClosedLock { 97 + return launchRTCIfNotClosed {
99 action(peerConnection) 98 action(peerConnection)
100 } 99 }
101 } 100 }
102 101
103 suspend fun setRemoteDescription(sd: SessionDescription): Either<Unit, String?> { 102 suspend fun setRemoteDescription(sd: SessionDescription): Either<Unit, String?> {
104 - val result = withNotClosedLock { 103 + val result = launchRTCIfNotClosed {
105 val result = peerConnection.setRemoteDescription(sd) 104 val result = peerConnection.setRemoteDescription(sd)
106 if (result is Either.Left) { 105 if (result is Either.Left) {
107 pendingCandidates.forEach { pending -> 106 pendingCandidates.forEach { pending ->
@@ -137,7 +136,7 @@ constructor( @@ -137,7 +136,7 @@ constructor(
137 var finalSdp: SessionDescription? = null 136 var finalSdp: SessionDescription? = null
138 137
139 // TODO: This is a potentially long lock hold. May need to break up. 138 // TODO: This is a potentially long lock hold. May need to break up.
140 - withNotClosedLock { 139 + launchRTCIfNotClosed {
141 val iceRestart = 140 val iceRestart =
142 constraints.findConstraint(MediaConstraintKeys.ICE_RESTART) == MediaConstraintKeys.TRUE 141 constraints.findConstraint(MediaConstraintKeys.ICE_RESTART) == MediaConstraintKeys.TRUE
143 if (iceRestart) { 142 if (iceRestart) {
@@ -155,7 +154,7 @@ constructor( @@ -155,7 +154,7 @@ constructor(
155 peerConnection.setRemoteDescription(curSd) 154 peerConnection.setRemoteDescription(curSd)
156 } else { 155 } else {
157 renegotiate = true 156 renegotiate = true
158 - return@withNotClosedLock 157 + return@launchRTCIfNotClosed
159 } 158 }
160 } 159 }
161 160
@@ -164,10 +163,13 @@ constructor( @@ -164,10 +163,13 @@ constructor(
164 is Either.Left -> outcome.value 163 is Either.Left -> outcome.value
165 is Either.Right -> { 164 is Either.Right -> {
166 LKLog.d { "error creating offer: ${outcome.value}" } 165 LKLog.d { "error creating offer: ${outcome.value}" }
167 - return@withNotClosedLock 166 + return@launchRTCIfNotClosed
168 } 167 }
169 } 168 }
170 169
  170 + if (isClosed()) {
  171 + return@launchRTCIfNotClosed
  172 + }
171 // munge sdp 173 // munge sdp
172 val sdpDescription = sdpFactory.createSessionDescription(sdpOffer.description) 174 val sdpDescription = sdpFactory.createSessionDescription(sdpOffer.description)
173 175
@@ -195,11 +197,14 @@ constructor( @@ -195,11 +197,14 @@ constructor(
195 197
196 LKLog.v { "sdp type: ${sdp.type}\ndescription:\n${sdp.description}" } 198 LKLog.v { "sdp type: ${sdp.type}\ndescription:\n${sdp.description}" }
197 LKLog.v { "munged sdp type: ${mungedSdp.type}\ndescription:\n${mungedSdp.description}" } 199 LKLog.v { "munged sdp type: ${mungedSdp.type}\ndescription:\n${mungedSdp.description}" }
198 - val mungedResult = if (remote) { 200 +
  201 + val mungedResult = launchRTCIfNotClosed {
  202 + if (remote) {
199 peerConnection.setRemoteDescription(mungedSdp) 203 peerConnection.setRemoteDescription(mungedSdp)
200 } else { 204 } else {
201 peerConnection.setLocalDescription(mungedSdp) 205 peerConnection.setLocalDescription(mungedSdp)
202 } 206 }
  207 + } ?: Either.Right("PCT closed")
203 208
204 val mungedErrorMessage = when (mungedResult) { 209 val mungedErrorMessage = when (mungedResult) {
205 is Either.Left -> { 210 is Either.Left -> {
@@ -224,11 +229,13 @@ constructor( @@ -224,11 +229,13 @@ constructor(
224 } 229 }
225 LKLog.w { "error: $mungedErrorMessage" } 230 LKLog.w { "error: $mungedErrorMessage" }
226 231
227 - val result = if (remote) { 232 + val result = launchRTCIfNotClosed {
  233 + if (remote) {
228 peerConnection.setRemoteDescription(sdp) 234 peerConnection.setRemoteDescription(sdp)
229 } else { 235 } else {
230 peerConnection.setLocalDescription(sdp) 236 peerConnection.setLocalDescription(sdp)
231 } 237 }
  238 + } ?: Either.Right("PCT closed")
232 239
233 if (result is Either.Right) { 240 if (result is Either.Right) {
234 val errorMessage = if (result.value.isNullOrBlank()) { 241 val errorMessage = if (result.value.isNullOrBlank()) {
@@ -261,21 +268,17 @@ constructor( @@ -261,21 +268,17 @@ constructor(
261 } 268 }
262 269
263 suspend fun close() { 270 suspend fun close() {
264 - withNotClosedLock { 271 + launchRTCIfNotClosed {
265 isClosed.set(true) 272 isClosed.set(true)
266 - peerConnection.close()  
267 -  
268 - // TODO: properly dispose of peer connection 273 + peerConnection.dispose()
269 } 274 }
270 } 275 }
271 276
272 fun updateRTCConfig(config: RTCConfiguration) { 277 fun updateRTCConfig(config: RTCConfiguration) {
273 - runBlocking {  
274 - withNotClosedLock { 278 + executeRTCIfNotClosed {
275 peerConnection.setConfiguration(config) 279 peerConnection.setConfiguration(config)
276 } 280 }
277 } 281 }
278 - }  
279 282
280 fun registerTrackBitrateInfo(cid: String, trackBitrateInfo: TrackBitrateInfo) { 283 fun registerTrackBitrateInfo(cid: String, trackBitrateInfo: TrackBitrateInfo) {
281 trackBitrates[TrackBitrateInfoKey.Cid(cid)] = trackBitrateInfo 284 trackBitrates[TrackBitrateInfoKey.Cid(cid)] = trackBitrateInfo
@@ -286,40 +289,56 @@ constructor( @@ -286,40 +289,56 @@ constructor(
286 } 289 }
287 290
288 suspend fun isConnected(): Boolean { 291 suspend fun isConnected(): Boolean {
289 - return withNotClosedLock { 292 + return launchRTCIfNotClosed {
290 peerConnection.isConnected() 293 peerConnection.isConnected()
291 } ?: false 294 } ?: false
292 } 295 }
293 296
294 suspend fun iceConnectionState(): PeerConnection.IceConnectionState { 297 suspend fun iceConnectionState(): PeerConnection.IceConnectionState {
295 - return withNotClosedLock { 298 + return launchRTCIfNotClosed {
296 peerConnection.iceConnectionState() 299 peerConnection.iceConnectionState()
297 } ?: PeerConnection.IceConnectionState.CLOSED 300 } ?: PeerConnection.IceConnectionState.CLOSED
298 } 301 }
299 302
300 suspend fun connectionState(): PeerConnection.PeerConnectionState { 303 suspend fun connectionState(): PeerConnection.PeerConnectionState {
301 - return withNotClosedLock { 304 + return launchRTCIfNotClosed {
302 peerConnection.connectionState() 305 peerConnection.connectionState()
303 } ?: PeerConnection.PeerConnectionState.CLOSED 306 } ?: PeerConnection.PeerConnectionState.CLOSED
304 } 307 }
305 308
306 suspend fun signalingState(): SignalingState { 309 suspend fun signalingState(): SignalingState {
307 - return withNotClosedLock { 310 + return launchRTCIfNotClosed {
308 peerConnection.signalingState() 311 peerConnection.signalingState()
309 } ?: SignalingState.CLOSED 312 } ?: SignalingState.CLOSED
310 } 313 }
311 314
312 @OptIn(ExperimentalContracts::class) 315 @OptIn(ExperimentalContracts::class)
313 - private suspend inline fun <T> withNotClosedLock(crossinline action: suspend () -> T): T? { 316 + private suspend inline fun <T> launchRTCIfNotClosed(noinline action: suspend () -> T): T? {
314 contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) } 317 contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) }
315 if (isClosed()) { 318 if (isClosed()) {
316 return null 319 return null
317 } 320 }
318 - return mutex.withReentrantLock { 321 + return launchBlockingOnRTCThread {
  322 + return@launchBlockingOnRTCThread if (isClosed()) {
  323 + null
  324 + } else {
  325 + action()
  326 + }
  327 + }
  328 + }
  329 +
  330 + @OptIn(ExperimentalContracts::class)
  331 + private fun <T> executeRTCIfNotClosed(action: () -> T): T? {
  332 + contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) }
319 if (isClosed()) { 333 if (isClosed()) {
320 - return@withReentrantLock null 334 + return null
  335 + }
  336 + return executeBlockingOnRTCThread {
  337 + return@executeBlockingOnRTCThread if (isClosed()) {
  338 + null
  339 + } else {
  340 + action()
321 } 341 }
322 - return@withReentrantLock action()  
323 } 342 }
324 } 343 }
325 344
@@ -17,8 +17,16 @@ @@ -17,8 +17,16 @@
17 package io.livekit.android.room 17 package io.livekit.android.room
18 18
19 import io.livekit.android.util.LKLog 19 import io.livekit.android.util.LKLog
  20 +import io.livekit.android.webrtc.peerconnection.executeOnRTCThread
20 import livekit.LivekitRtc 21 import livekit.LivekitRtc
21 -import org.webrtc.* 22 +import org.webrtc.CandidatePairChangeEvent
  23 +import org.webrtc.DataChannel
  24 +import org.webrtc.IceCandidate
  25 +import org.webrtc.MediaStream
  26 +import org.webrtc.PeerConnection
  27 +import org.webrtc.RtpReceiver
  28 +import org.webrtc.RtpTransceiver
  29 +import org.webrtc.SessionDescription
22 30
23 /** 31 /**
24 * @suppress 32 * @suppress
@@ -31,30 +39,38 @@ class PublisherTransportObserver( @@ -31,30 +39,38 @@ class PublisherTransportObserver(
31 var connectionChangeListener: ((newState: PeerConnection.PeerConnectionState) -> Unit)? = null 39 var connectionChangeListener: ((newState: PeerConnection.PeerConnectionState) -> Unit)? = null
32 40
33 override fun onIceCandidate(iceCandidate: IceCandidate?) { 41 override fun onIceCandidate(iceCandidate: IceCandidate?) {
34 - val candidate = iceCandidate ?: return 42 + executeOnRTCThread {
  43 + val candidate = iceCandidate ?: return@executeOnRTCThread
35 LKLog.v { "onIceCandidate: $candidate" } 44 LKLog.v { "onIceCandidate: $candidate" }
36 client.sendCandidate(candidate, target = LivekitRtc.SignalTarget.PUBLISHER) 45 client.sendCandidate(candidate, target = LivekitRtc.SignalTarget.PUBLISHER)
37 } 46 }
  47 + }
38 48
39 override fun onRenegotiationNeeded() { 49 override fun onRenegotiationNeeded() {
  50 + executeOnRTCThread {
40 engine.negotiatePublisher() 51 engine.negotiatePublisher()
41 } 52 }
  53 + }
42 54
43 override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { 55 override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
44 LKLog.v { "onIceConnection new state: $newState" } 56 LKLog.v { "onIceConnection new state: $newState" }
45 } 57 }
46 58
47 override fun onOffer(sd: SessionDescription) { 59 override fun onOffer(sd: SessionDescription) {
  60 + executeOnRTCThread {
48 client.sendOffer(sd) 61 client.sendOffer(sd)
49 } 62 }
  63 + }
50 64
51 override fun onStandardizedIceConnectionChange(newState: PeerConnection.IceConnectionState?) { 65 override fun onStandardizedIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
52 } 66 }
53 67
54 override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) { 68 override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) {
  69 + executeOnRTCThread {
55 LKLog.v { "onConnection new state: $newState" } 70 LKLog.v { "onConnection new state: $newState" }
56 connectionChangeListener?.invoke(newState) 71 connectionChangeListener?.invoke(newState)
57 } 72 }
  73 + }
58 74
59 override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent?) { 75 override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent?) {
60 } 76 }
@@ -32,10 +32,12 @@ import io.livekit.android.room.util.setLocalDescription @@ -32,10 +32,12 @@ import io.livekit.android.room.util.setLocalDescription
32 import io.livekit.android.util.CloseableCoroutineScope 32 import io.livekit.android.util.CloseableCoroutineScope
33 import io.livekit.android.util.Either 33 import io.livekit.android.util.Either
34 import io.livekit.android.util.LKLog 34 import io.livekit.android.util.LKLog
  35 +import io.livekit.android.util.nullSafe
35 import io.livekit.android.webrtc.RTCStatsGetter 36 import io.livekit.android.webrtc.RTCStatsGetter
36 import io.livekit.android.webrtc.copy 37 import io.livekit.android.webrtc.copy
37 import io.livekit.android.webrtc.isConnected 38 import io.livekit.android.webrtc.isConnected
38 import io.livekit.android.webrtc.isDisconnected 39 import io.livekit.android.webrtc.isDisconnected
  40 +import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread
39 import io.livekit.android.webrtc.toProtoSessionDescription 41 import io.livekit.android.webrtc.toProtoSessionDescription
40 import kotlinx.coroutines.* 42 import kotlinx.coroutines.*
41 import livekit.LivekitModels 43 import livekit.LivekitModels
@@ -316,6 +318,7 @@ internal constructor( @@ -316,6 +318,7 @@ internal constructor(
316 } 318 }
317 319
318 private fun closeResources(reason: String) { 320 private fun closeResources(reason: String) {
  321 + executeBlockingOnRTCThread {
319 publisherObserver.connectionChangeListener = null 322 publisherObserver.connectionChangeListener = null
320 subscriberObserver.connectionChangeListener = null 323 subscriberObserver.connectionChangeListener = null
321 publisher?.closeBlocking() 324 publisher?.closeBlocking()
@@ -337,6 +340,7 @@ internal constructor( @@ -337,6 +340,7 @@ internal constructor(
337 lossyDataChannelSub?.completeDispose() 340 lossyDataChannelSub?.completeDispose()
338 lossyDataChannelSub = null 341 lossyDataChannelSub = null
339 isSubscriberPrimary = false 342 isSubscriberPrimary = false
  343 + }
340 client.close(reason = reason) 344 client.close(reason = reason)
341 } 345 }
342 346
@@ -712,7 +716,7 @@ internal constructor( @@ -712,7 +716,7 @@ internal constructor(
712 LKLog.v { "received server answer: ${sessionDescription.type}, $signalingState" } 716 LKLog.v { "received server answer: ${sessionDescription.type}, $signalingState" }
713 coroutineScope.launch { 717 coroutineScope.launch {
714 LKLog.i { sessionDescription.toString() } 718 LKLog.i { sessionDescription.toString() }
715 - when (val outcome = publisher?.setRemoteDescription(sessionDescription)) { 719 + when (val outcome = publisher?.setRemoteDescription(sessionDescription).nullSafe()) {
716 is Either.Left -> { 720 is Either.Left -> {
717 // do nothing. 721 // do nothing.
718 } 722 }
@@ -720,10 +724,6 @@ internal constructor( @@ -720,10 +724,6 @@ internal constructor(
720 is Either.Right -> { 724 is Either.Right -> {
721 LKLog.e { "error setting remote description for answer: ${outcome.value} " } 725 LKLog.e { "error setting remote description for answer: ${outcome.value} " }
722 } 726 }
723 -  
724 - else -> {  
725 - LKLog.w { "publisher is null, can't set remote description." }  
726 - }  
727 } 727 }
728 } 728 }
729 } 729 }
@@ -732,16 +732,11 @@ internal constructor( @@ -732,16 +732,11 @@ internal constructor(
732 val signalingState = runBlocking { publisher?.signalingState() } 732 val signalingState = runBlocking { publisher?.signalingState() }
733 LKLog.v { "received server offer: ${sessionDescription.type}, $signalingState" } 733 LKLog.v { "received server offer: ${sessionDescription.type}, $signalingState" }
734 coroutineScope.launch { 734 coroutineScope.launch {
735 - // TODO: This is a potentially very long lock hold. May need to break up.  
736 - val answer = subscriber?.withPeerConnection {  
737 run { 735 run {
738 - when (  
739 - val outcome =  
740 - subscriber?.setRemoteDescription(sessionDescription)  
741 - ) { 736 + when (val outcome = subscriber?.setRemoteDescription(sessionDescription).nullSafe()) {
742 is Either.Right -> { 737 is Either.Right -> {
743 LKLog.e { "error setting remote description for answer: ${outcome.value} " } 738 LKLog.e { "error setting remote description for answer: ${outcome.value} " }
744 - return@withPeerConnection null 739 + return@launch
745 } 740 }
746 741
747 else -> {} 742 else -> {}
@@ -749,42 +744,37 @@ internal constructor( @@ -749,42 +744,37 @@ internal constructor(
749 } 744 }
750 745
751 if (isClosed) { 746 if (isClosed) {
752 - return@withPeerConnection null 747 + return@launch
753 } 748 }
754 749
755 val answer = run { 750 val answer = run {
756 - when (val outcome = createAnswer(MediaConstraints())) { 751 + when (val outcome = subscriber?.withPeerConnection { createAnswer(MediaConstraints()) }.nullSafe()) {
757 is Either.Left -> outcome.value 752 is Either.Left -> outcome.value
758 is Either.Right -> { 753 is Either.Right -> {
759 LKLog.e { "error creating answer: ${outcome.value}" } 754 LKLog.e { "error creating answer: ${outcome.value}" }
760 - return@withPeerConnection null 755 + return@launch
761 } 756 }
762 } 757 }
763 } 758 }
764 759
765 if (isClosed) { 760 if (isClosed) {
766 - return@withPeerConnection null 761 + return@launch
767 } 762 }
768 763
769 run<Unit> { 764 run<Unit> {
770 - when (val outcome = setLocalDescription(answer)) { 765 + when (val outcome = subscriber?.withPeerConnection { setLocalDescription(answer) }.nullSafe()) {
  766 + is Either.Left -> Unit
771 is Either.Right -> { 767 is Either.Right -> {
772 LKLog.e { "error setting local description for answer: ${outcome.value}" } 768 LKLog.e { "error setting local description for answer: ${outcome.value}" }
773 - return@withPeerConnection null 769 + return@launch
774 } 770 }
775 -  
776 - else -> {}  
777 } 771 }
778 } 772 }
779 773
780 if (isClosed) { 774 if (isClosed) {
781 - return@withPeerConnection null  
782 - }  
783 - return@withPeerConnection answer  
784 - }  
785 - answer?.let {  
786 - client.sendAnswer(it) 775 + return@launch
787 } 776 }
  777 + client.sendAnswer(answer)
788 } 778 }
789 } 779 }
790 780
@@ -1018,12 +1008,12 @@ internal constructor( @@ -1018,12 +1008,12 @@ internal constructor(
1018 } 1008 }
1019 1009
1020 @VisibleForTesting 1010 @VisibleForTesting
1021 - internal suspend fun getPublisherPeerConnection() =  
1022 - publisher?.withPeerConnection { this }!! 1011 + internal fun getPublisherPeerConnection() =
  1012 + publisher!!.peerConnection
1023 1013
1024 @VisibleForTesting 1014 @VisibleForTesting
1025 - internal suspend fun getSubscriberPeerConnection() =  
1026 - subscriber?.withPeerConnection { this }!! 1015 + internal fun getSubscriberPeerConnection() =
  1016 + subscriber!!.peerConnection
1027 } 1017 }
1028 1018
1029 /** 1019 /**
@@ -560,8 +560,8 @@ constructor( @@ -560,8 +560,8 @@ constructor(
560 } 560 }
561 561
562 state = State.DISCONNECTED 562 state = State.DISCONNECTED
563 - engine.close()  
564 cleanupRoom() 563 cleanupRoom()
  564 + engine.close()
565 565
566 listener?.onDisconnect(this, null) 566 listener?.onDisconnect(this, null)
567 listener = null 567 listener = null
@@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
17 package io.livekit.android.room 17 package io.livekit.android.room
18 18
19 import io.livekit.android.util.LKLog 19 import io.livekit.android.util.LKLog
  20 +import io.livekit.android.webrtc.peerconnection.executeOnRTCThread
20 import livekit.LivekitRtc 21 import livekit.LivekitRtc
21 import org.webrtc.CandidatePairChangeEvent 22 import org.webrtc.CandidatePairChangeEvent
22 import org.webrtc.DataChannel 23 import org.webrtc.DataChannel
@@ -39,15 +40,19 @@ class SubscriberTransportObserver( @@ -39,15 +40,19 @@ class SubscriberTransportObserver(
39 var connectionChangeListener: ((PeerConnection.PeerConnectionState) -> Unit)? = null 40 var connectionChangeListener: ((PeerConnection.PeerConnectionState) -> Unit)? = null
40 41
41 override fun onIceCandidate(candidate: IceCandidate) { 42 override fun onIceCandidate(candidate: IceCandidate) {
  43 + executeOnRTCThread {
42 LKLog.v { "onIceCandidate: $candidate" } 44 LKLog.v { "onIceCandidate: $candidate" }
43 client.sendCandidate(candidate, LivekitRtc.SignalTarget.SUBSCRIBER) 45 client.sendCandidate(candidate, LivekitRtc.SignalTarget.SUBSCRIBER)
44 } 46 }
  47 + }
45 48
46 override fun onAddTrack(receiver: RtpReceiver, streams: Array<out MediaStream>) { 49 override fun onAddTrack(receiver: RtpReceiver, streams: Array<out MediaStream>) {
47 - val track = receiver.track() ?: return 50 + executeOnRTCThread {
  51 + val track = receiver.track() ?: return@executeOnRTCThread
48 LKLog.v { "onAddTrack: ${track.kind()}, ${track.id()}, ${streams.fold("") { sum, it -> "$sum, $it" }}" } 52 LKLog.v { "onAddTrack: ${track.kind()}, ${track.id()}, ${streams.fold("") { sum, it -> "$sum, $it" }}" }
49 engine.listener?.onAddTrack(receiver, track, streams) 53 engine.listener?.onAddTrack(receiver, track, streams)
50 } 54 }
  55 + }
51 56
52 override fun onTrack(transceiver: RtpTransceiver) { 57 override fun onTrack(transceiver: RtpTransceiver) {
53 when (transceiver.mediaType) { 58 when (transceiver.mediaType) {
@@ -58,16 +63,20 @@ class SubscriberTransportObserver( @@ -58,16 +63,20 @@ class SubscriberTransportObserver(
58 } 63 }
59 64
60 override fun onDataChannel(channel: DataChannel) { 65 override fun onDataChannel(channel: DataChannel) {
  66 + executeOnRTCThread {
61 dataChannelListener?.invoke(channel) 67 dataChannelListener?.invoke(channel)
62 } 68 }
  69 + }
63 70
64 override fun onStandardizedIceConnectionChange(newState: PeerConnection.IceConnectionState?) { 71 override fun onStandardizedIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
65 } 72 }
66 73
67 override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) { 74 override fun onConnectionChange(newState: PeerConnection.PeerConnectionState) {
  75 + executeOnRTCThread {
68 LKLog.v { "onConnectionChange new state: $newState" } 76 LKLog.v { "onConnectionChange new state: $newState" }
69 connectionChangeListener?.invoke(newState) 77 connectionChangeListener?.invoke(newState)
70 } 78 }
  79 + }
71 80
72 override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent?) { 81 override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent?) {
73 } 82 }
@@ -182,7 +182,7 @@ class RemoteParticipant( @@ -182,7 +182,7 @@ class RemoteParticipant(
182 if (track != null) { 182 if (track != null) {
183 try { 183 try {
184 track.stop() 184 track.stop()
185 - } catch (e: IllegalStateException) { 185 + } catch (e: Exception) {
186 // track may already be disposed, ignore. 186 // track may already be disposed, ignore.
187 } 187 }
188 internalListener?.onTrackUnsubscribed(track, publication, this) 188 internalListener?.onTrackUnsubscribed(track, publication, this)
@@ -20,6 +20,7 @@ import android.Manifest @@ -20,6 +20,7 @@ import android.Manifest
20 import android.content.Context 20 import android.content.Context
21 import android.content.pm.PackageManager 21 import android.content.pm.PackageManager
22 import androidx.core.content.ContextCompat 22 import androidx.core.content.ContextCompat
  23 +import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread
23 import org.webrtc.MediaConstraints 24 import org.webrtc.MediaConstraints
24 import org.webrtc.PeerConnectionFactory 25 import org.webrtc.PeerConnectionFactory
25 import org.webrtc.RtpSender 26 import org.webrtc.RtpSender
@@ -36,9 +37,9 @@ class LocalAudioTrack( @@ -36,9 +37,9 @@ class LocalAudioTrack(
36 mediaTrack: org.webrtc.AudioTrack 37 mediaTrack: org.webrtc.AudioTrack
37 ) : AudioTrack(name, mediaTrack) { 38 ) : AudioTrack(name, mediaTrack) {
38 var enabled: Boolean 39 var enabled: Boolean
39 - get() = rtcTrack.enabled() 40 + get() = executeBlockingOnRTCThread { rtcTrack.enabled() }
40 set(value) { 41 set(value) {
41 - rtcTrack.setEnabled(value) 42 + executeBlockingOnRTCThread { rtcTrack.setEnabled(value) }
42 } 43 }
43 44
44 internal var transceiver: RtpTransceiver? = null 45 internal var transceiver: RtpTransceiver? = null
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 package io.livekit.android.room.track 17 package io.livekit.android.room.track
18 18
  19 +import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread
19 import org.webrtc.AudioTrack 20 import org.webrtc.AudioTrack
20 import org.webrtc.AudioTrackSink 21 import org.webrtc.AudioTrackSink
21 import org.webrtc.RtpReceiver 22 import org.webrtc.RtpReceiver
@@ -23,7 +24,7 @@ import org.webrtc.RtpReceiver @@ -23,7 +24,7 @@ import org.webrtc.RtpReceiver
23 class RemoteAudioTrack( 24 class RemoteAudioTrack(
24 name: String, 25 name: String,
25 rtcTrack: AudioTrack, 26 rtcTrack: AudioTrack,
26 - internal val receiver: RtpReceiver 27 + internal val receiver: RtpReceiver,
27 ) : io.livekit.android.room.track.AudioTrack(name, rtcTrack) { 28 ) : io.livekit.android.room.track.AudioTrack(name, rtcTrack) {
28 29
29 /** 30 /**
@@ -35,13 +36,17 @@ class RemoteAudioTrack( @@ -35,13 +36,17 @@ class RemoteAudioTrack(
35 * to use the data after this function returns. 36 * to use the data after this function returns.
36 */ 37 */
37 fun addSink(sink: AudioTrackSink) { 38 fun addSink(sink: AudioTrackSink) {
  39 + executeBlockingOnRTCThread {
38 rtcTrack.addSink(sink) 40 rtcTrack.addSink(sink)
39 } 41 }
  42 + }
40 43
41 /** 44 /**
42 * Removes a previously added sink. 45 * Removes a previously added sink.
43 */ 46 */
44 fun removeSink(sink: AudioTrackSink) { 47 fun removeSink(sink: AudioTrackSink) {
  48 + executeBlockingOnRTCThread {
45 rtcTrack.removeSink(sink) 49 rtcTrack.removeSink(sink)
46 } 50 }
  51 + }
47 } 52 }
@@ -21,6 +21,7 @@ import io.livekit.android.events.TrackEvent @@ -21,6 +21,7 @@ import io.livekit.android.events.TrackEvent
21 import io.livekit.android.util.flowDelegate 21 import io.livekit.android.util.flowDelegate
22 import io.livekit.android.webrtc.RTCStatsGetter 22 import io.livekit.android.webrtc.RTCStatsGetter
23 import io.livekit.android.webrtc.getStats 23 import io.livekit.android.webrtc.getStats
  24 +import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread
24 import livekit.LivekitModels 25 import livekit.LivekitModels
25 import livekit.LivekitRtc 26 import livekit.LivekitRtc
26 import org.webrtc.MediaStreamTrack 27 import org.webrtc.MediaStreamTrack
@@ -149,16 +150,22 @@ abstract class Track( @@ -149,16 +150,22 @@ abstract class Track(
149 data class Dimensions(val width: Int, val height: Int) 150 data class Dimensions(val width: Int, val height: Int)
150 151
151 open fun start() { 152 open fun start() {
  153 + executeBlockingOnRTCThread {
152 rtcTrack.setEnabled(true) 154 rtcTrack.setEnabled(true)
153 } 155 }
  156 + }
154 157
155 open fun stop() { 158 open fun stop() {
  159 + executeBlockingOnRTCThread {
156 rtcTrack.setEnabled(false) 160 rtcTrack.setEnabled(false)
157 } 161 }
  162 + }
158 163
159 open fun dispose() { 164 open fun dispose() {
  165 + executeBlockingOnRTCThread {
160 rtcTrack.dispose() 166 rtcTrack.dispose()
161 } 167 }
  168 + }
162 } 169 }
163 170
164 sealed class TrackException(message: String? = null, cause: Throwable? = null) : 171 sealed class TrackException(message: String? = null, cause: Throwable? = null) :
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 package io.livekit.android.room.track 17 package io.livekit.android.room.track
18 18
  19 +import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread
19 import org.webrtc.VideoSink 20 import org.webrtc.VideoSink
20 import org.webrtc.VideoTrack 21 import org.webrtc.VideoTrack
21 22
@@ -30,20 +31,26 @@ abstract class VideoTrack(name: String, override val rtcTrack: VideoTrack) : @@ -30,20 +31,26 @@ abstract class VideoTrack(name: String, override val rtcTrack: VideoTrack) :
30 } 31 }
31 32
32 open fun addRenderer(renderer: VideoSink) { 33 open fun addRenderer(renderer: VideoSink) {
  34 + executeBlockingOnRTCThread {
33 sinks.add(renderer) 35 sinks.add(renderer)
34 rtcTrack.addSink(renderer) 36 rtcTrack.addSink(renderer)
35 } 37 }
  38 + }
36 39
37 open fun removeRenderer(renderer: VideoSink) { 40 open fun removeRenderer(renderer: VideoSink) {
  41 + executeBlockingOnRTCThread {
38 rtcTrack.removeSink(renderer) 42 rtcTrack.removeSink(renderer)
39 sinks.remove(renderer) 43 sinks.remove(renderer)
40 } 44 }
  45 + }
41 46
42 override fun stop() { 47 override fun stop() {
  48 + executeBlockingOnRTCThread {
43 for (sink in sinks) { 49 for (sink in sinks) {
44 rtcTrack.removeSink(sink) 50 rtcTrack.removeSink(sink)
45 - }  
46 sinks.clear() 51 sinks.clear()
  52 + }
  53 + }
47 super.stop() 54 super.stop()
48 } 55 }
49 } 56 }
@@ -20,3 +20,7 @@ sealed class Either<out A, out B> { @@ -20,3 +20,7 @@ sealed class Either<out A, out B> {
20 class Left<out A>(val value: A) : Either<A, Nothing>() 20 class Left<out A>(val value: A) : Either<A, Nothing>()
21 class Right<out B>(val value: B) : Either<Nothing, B>() 21 class Right<out B>(val value: B) : Either<Nothing, B>()
22 } 22 }
  23 +
  24 +fun <A> Either<A, String?>?.nullSafe(): Either<A, String?> {
  25 + return this ?: Either.Right("null")
  26 +}
  1 +/*
  2 + * Copyright 2023 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.webrtc.peerconnection
  18 +
  19 +import org.webrtc.PeerConnection
  20 +import org.webrtc.RtpReceiver
  21 +import org.webrtc.RtpSender
  22 +import org.webrtc.RtpTransceiver
  23 +
  24 +/**
  25 + * Objects obtained through [PeerConnection] are transient,
  26 + * and should not be kept in memory. Calls to these methods
  27 + * dispose all existing objects in the tree and refresh with
  28 + * new updated objects:
  29 + *
  30 + * * [PeerConnection.getTransceivers]
  31 + * * [PeerConnection.getReceivers]
  32 + * * [PeerConnection.getSenders]
  33 + *
  34 + * For this reason, any object gotten through the PeerConnection
  35 + * should instead be looked up through the PeerConnection as needed.
  36 + */
  37 +internal abstract class PeerConnectionResource<T>(val parentPeerConnection: PeerConnection) {
  38 + abstract fun get(): T?
  39 +}
  40 +
  41 +internal class RtpTransceiverResource(parentPeerConnection: PeerConnection, private val senderId: String) : PeerConnectionResource<RtpTransceiver>(parentPeerConnection) {
  42 + override fun get() = executeBlockingOnRTCThread {
  43 + parentPeerConnection.transceivers.firstOrNull { t -> t.sender.id() == senderId }
  44 + }
  45 +}
  46 +
  47 +internal class RtpReceiverResource(parentPeerConnection: PeerConnection, private val receiverId: String) : PeerConnectionResource<RtpReceiver>(parentPeerConnection) {
  48 + override fun get() = executeBlockingOnRTCThread {
  49 + parentPeerConnection.receivers.firstOrNull { r -> r.id() == receiverId }
  50 + }
  51 +}
  52 +
  53 +internal class RtpSenderResource(parentPeerConnection: PeerConnection, private val senderId: String) : PeerConnectionResource<RtpSender>(parentPeerConnection) {
  54 + override fun get() = executeBlockingOnRTCThread {
  55 + parentPeerConnection.senders.firstOrNull { s -> s.id() == senderId }
  56 + }
  57 +}
  1 +/*
  2 + * Copyright 2023 LiveKit, Inc.
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +package io.livekit.android.webrtc.peerconnection
  18 +
  19 +import androidx.annotation.VisibleForTesting
  20 +import kotlinx.coroutines.CoroutineDispatcher
  21 +import kotlinx.coroutines.asCoroutineDispatcher
  22 +import kotlinx.coroutines.async
  23 +import kotlinx.coroutines.coroutineScope
  24 +import java.util.concurrent.ExecutorService
  25 +import java.util.concurrent.Executors
  26 +import java.util.concurrent.ThreadFactory
  27 +import java.util.concurrent.atomic.AtomicInteger
  28 +
  29 +// Executor thread is started once and is used for all
  30 +// peer connection API calls to ensure new peer connection factory is
  31 +// created on the same thread as previously destroyed factory.
  32 +
  33 +private const val EXECUTOR_THREADNAME_PREFIX = "LK_RTC_THREAD"
  34 +private val threadFactory = object : ThreadFactory {
  35 + private val idGenerator = AtomicInteger(0)
  36 +
  37 + override fun newThread(r: Runnable): Thread {
  38 + val thread = Thread(r)
  39 + thread.name = EXECUTOR_THREADNAME_PREFIX + "_" + idGenerator.incrementAndGet()
  40 + return thread
  41 + }
  42 +}
  43 +
  44 +// var only for testing purposes, do not alter!
  45 +private var executor = Executors.newSingleThreadExecutor(threadFactory)
  46 +private var rtcDispatcher: CoroutineDispatcher = executor.asCoroutineDispatcher()
  47 +
  48 +@VisibleForTesting
  49 +internal fun overrideExecutorAndDispatcher(executorService: ExecutorService, dispatcher: CoroutineDispatcher) {
  50 + executor = executorService
  51 + rtcDispatcher = dispatcher
  52 +}
  53 +
  54 +/**
  55 + * Execute [action] on the RTC thread. The PeerConnection API
  56 + * is generally not thread safe, so all actions relating to
  57 + * peer connection objects should go through the RTC thread.
  58 + */
  59 +fun <T> executeOnRTCThread(action: () -> T) {
  60 + if (Thread.currentThread().name.startsWith(EXECUTOR_THREADNAME_PREFIX)) {
  61 + action()
  62 + } else {
  63 + executor.submit(action)
  64 + }
  65 +}
  66 +
  67 +/**
  68 + * Execute [action] synchronously on the RTC thread. The PeerConnection API
  69 + * is generally not thread safe, so all actions relating to
  70 + * peer connection objects should go through the RTC thread.
  71 + */
  72 +fun <T> executeBlockingOnRTCThread(action: () -> T): T {
  73 + return if (Thread.currentThread().name.startsWith(EXECUTOR_THREADNAME_PREFIX)) {
  74 + action()
  75 + } else {
  76 + executor.submit(action).get()
  77 + }
  78 +}
  79 +
  80 +/**
  81 + * Launch [action] synchronously on the RTC thread. The PeerConnection API
  82 + * is generally not thread safe, so all actions relating to
  83 + * peer connection objects should go through the RTC thread.
  84 + */
  85 +suspend fun <T> launchBlockingOnRTCThread(action: suspend () -> T): T = coroutineScope {
  86 + return@coroutineScope if (Thread.currentThread().name.startsWith(EXECUTOR_THREADNAME_PREFIX)) {
  87 + action()
  88 + } else {
  89 + async(rtcDispatcher) {
  90 + action()
  91 + }.await()
  92 + }
  93 +}
@@ -16,11 +16,14 @@ @@ -16,11 +16,14 @@
16 16
17 package io.livekit.android 17 package io.livekit.android
18 18
  19 +import com.google.common.util.concurrent.MoreExecutors
19 import io.livekit.android.coroutines.TestCoroutineRule 20 import io.livekit.android.coroutines.TestCoroutineRule
20 import io.livekit.android.util.LoggingRule 21 import io.livekit.android.util.LoggingRule
  22 +import io.livekit.android.webrtc.peerconnection.overrideExecutorAndDispatcher
21 import kotlinx.coroutines.ExperimentalCoroutinesApi 23 import kotlinx.coroutines.ExperimentalCoroutinesApi
22 import kotlinx.coroutines.test.TestScope 24 import kotlinx.coroutines.test.TestScope
23 import kotlinx.coroutines.test.runTest 25 import kotlinx.coroutines.test.runTest
  26 +import org.junit.Before
24 import org.junit.Rule 27 import org.junit.Rule
25 import org.mockito.junit.MockitoJUnit 28 import org.mockito.junit.MockitoJUnit
26 29
@@ -36,6 +39,14 @@ abstract class BaseTest { @@ -36,6 +39,14 @@ abstract class BaseTest {
36 @get:Rule 39 @get:Rule
37 var coroutineRule = TestCoroutineRule() 40 var coroutineRule = TestCoroutineRule()
38 41
  42 + @Before
  43 + fun setupRTCThread() {
  44 + overrideExecutorAndDispatcher(
  45 + executorService = MoreExecutors.newDirectExecutorService(),
  46 + dispatcher = coroutineRule.dispatcher,
  47 + )
  48 + }
  49 +
39 @OptIn(ExperimentalCoroutinesApi::class) 50 @OptIn(ExperimentalCoroutinesApi::class)
40 fun runTest(testBody: suspend TestScope.() -> Unit) = coroutineRule.scope.runTest(testBody = testBody) 51 fun runTest(testBody: suspend TestScope.() -> Unit) = coroutineRule.scope.runTest(testBody = testBody)
41 } 52 }
@@ -24,6 +24,9 @@ class MockAudioStreamTrack( @@ -24,6 +24,9 @@ class MockAudioStreamTrack(
24 var enabled: Boolean = true, 24 var enabled: Boolean = true,
25 var state: State = State.LIVE, 25 var state: State = State.LIVE,
26 ) : AudioTrack(1L) { 26 ) : AudioTrack(1L) {
  27 +
  28 + var disposed = false
  29 +
27 override fun id(): String = id 30 override fun id(): String = id
28 31
29 override fun kind(): String = kind 32 override fun kind(): String = kind
@@ -40,6 +43,10 @@ class MockAudioStreamTrack( @@ -40,6 +43,10 @@ class MockAudioStreamTrack(
40 } 43 }
41 44
42 override fun dispose() { 45 override fun dispose() {
  46 + if (disposed) {
  47 + throw IllegalStateException("already disposed")
  48 + }
  49 + disposed = true
43 } 50 }
44 51
45 override fun setVolume(volume: Double) { 52 override fun setVolume(volume: Double) {
@@ -24,6 +24,8 @@ class MockMediaStreamTrack( @@ -24,6 +24,8 @@ class MockMediaStreamTrack(
24 var enabled: Boolean = true, 24 var enabled: Boolean = true,
25 var state: State = State.LIVE, 25 var state: State = State.LIVE,
26 ) : MediaStreamTrack(1L) { 26 ) : MediaStreamTrack(1L) {
  27 +
  28 + var disposed = false
27 override fun id(): String = id 29 override fun id(): String = id
28 30
29 override fun kind(): String = kind 31 override fun kind(): String = kind
@@ -40,5 +42,9 @@ class MockMediaStreamTrack( @@ -40,5 +42,9 @@ class MockMediaStreamTrack(
40 } 42 }
41 43
42 override fun dispose() { 44 override fun dispose() {
  45 + if (disposed) {
  46 + throw IllegalStateException("already disposed")
  47 + }
  48 + disposed = true
43 } 49 }
44 } 50 }
@@ -214,7 +214,8 @@ class MockPeerConnection( @@ -214,7 +214,8 @@ class MockPeerConnection(
214 IceConnectionState.NEW -> PeerConnectionState.NEW 214 IceConnectionState.NEW -> PeerConnectionState.NEW
215 IceConnectionState.CHECKING -> PeerConnectionState.CONNECTING 215 IceConnectionState.CHECKING -> PeerConnectionState.CONNECTING
216 IceConnectionState.CONNECTED, 216 IceConnectionState.CONNECTED,
217 - IceConnectionState.COMPLETED -> PeerConnectionState.CONNECTED 217 + IceConnectionState.COMPLETED,
  218 + -> PeerConnectionState.CONNECTED
218 219
219 IceConnectionState.DISCONNECTED -> PeerConnectionState.DISCONNECTED 220 IceConnectionState.DISCONNECTED -> PeerConnectionState.DISCONNECTED
220 IceConnectionState.FAILED -> PeerConnectionState.FAILED 221 IceConnectionState.FAILED -> PeerConnectionState.FAILED
@@ -242,7 +243,8 @@ class MockPeerConnection( @@ -242,7 +243,8 @@ class MockPeerConnection(
242 IceConnectionState.NEW, 243 IceConnectionState.NEW,
243 IceConnectionState.CHECKING, 244 IceConnectionState.CHECKING,
244 IceConnectionState.CONNECTED, 245 IceConnectionState.CONNECTED,
245 - IceConnectionState.COMPLETED -> { 246 + IceConnectionState.COMPLETED,
  247 + -> {
246 val currentOrdinal = iceConnectionState.ordinal 248 val currentOrdinal = iceConnectionState.ordinal
247 val newOrdinal = newState.ordinal 249 val newOrdinal = newState.ordinal
248 250
@@ -258,7 +260,8 @@ class MockPeerConnection( @@ -258,7 +260,8 @@ class MockPeerConnection(
258 260
259 IceConnectionState.FAILED, 261 IceConnectionState.FAILED,
260 IceConnectionState.DISCONNECTED, 262 IceConnectionState.DISCONNECTED,
261 - IceConnectionState.CLOSED -> { 263 + IceConnectionState.CLOSED,
  264 + -> {
262 // jump to state directly. 265 // jump to state directly.
263 iceConnectionState = newState 266 iceConnectionState = newState
264 } 267 }
@@ -278,6 +281,9 @@ class MockPeerConnection( @@ -278,6 +281,9 @@ class MockPeerConnection(
278 override fun dispose() { 281 override fun dispose() {
279 iceConnectionState = IceConnectionState.CLOSED 282 iceConnectionState = IceConnectionState.CLOSED
280 closed = true 283 closed = true
  284 +
  285 + transceivers.forEach { t -> t.dispose() }
  286 + transceivers.clear()
281 } 287 }
282 288
283 override fun getNativePeerConnection(): Long = 0L 289 override fun getNativePeerConnection(): Long = 0L
@@ -28,6 +28,8 @@ import timber.log.Timber @@ -28,6 +28,8 @@ import timber.log.Timber
28 */ 28 */
29 class LoggingRule : TestRule { 29 class LoggingRule : TestRule {
30 30
  31 + companion object {
  32 +
31 val logTree = object : Timber.DebugTree() { 33 val logTree = object : Timber.DebugTree() {
32 override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 34 override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
33 val priorityChar = when (priority) { 35 val priorityChar = when (priority) {
@@ -46,6 +48,7 @@ class LoggingRule : TestRule { @@ -46,6 +48,7 @@ class LoggingRule : TestRule {
46 } 48 }
47 } 49 }
48 } 50 }
  51 + }
49 52
50 override fun apply(base: Statement, description: Description?) = object : Statement() { 53 override fun apply(base: Statement, description: Description?) = object : Statement() {
51 override fun evaluate() { 54 override fun evaluate() {