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 个修改的文件 包含 405 行增加164 行删除
@@ -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,25 +84,23 @@ constructor( @@ -83,25 +84,23 @@ constructor(
83 } 84 }
84 85
85 fun addIceCandidate(candidate: IceCandidate) { 86 fun addIceCandidate(candidate: IceCandidate) {
86 - runBlocking {  
87 - withNotClosedLock {  
88 - if (peerConnection.remoteDescription != null && !restartingIce) {  
89 - peerConnection.addIceCandidate(candidate)  
90 - } else {  
91 - pendingCandidates.add(candidate)  
92 - } 87 + executeRTCIfNotClosed {
  88 + if (peerConnection.remoteDescription != null && !restartingIce) {
  89 + peerConnection.addIceCandidate(candidate)
  90 + } else {
  91 + pendingCandidates.add(candidate)
93 } 92 }
94 } 93 }
95 } 94 }
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) {  
199 - peerConnection.setRemoteDescription(mungedSdp)  
200 - } else {  
201 - peerConnection.setLocalDescription(mungedSdp)  
202 - } 200 +
  201 + val mungedResult = launchRTCIfNotClosed {
  202 + if (remote) {
  203 + peerConnection.setRemoteDescription(mungedSdp)
  204 + } else {
  205 + peerConnection.setLocalDescription(mungedSdp)
  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) {  
228 - peerConnection.setRemoteDescription(sdp)  
229 - } else {  
230 - peerConnection.setLocalDescription(sdp)  
231 - } 232 + val result = launchRTCIfNotClosed {
  233 + if (remote) {
  234 + peerConnection.setRemoteDescription(sdp)
  235 + } else {
  236 + peerConnection.setLocalDescription(sdp)
  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,19 +268,15 @@ constructor( @@ -261,19 +268,15 @@ 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 {  
275 - peerConnection.setConfiguration(config)  
276 - } 278 + executeRTCIfNotClosed {
  279 + peerConnection.setConfiguration(config)
277 } 280 }
278 } 281 }
279 282
@@ -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 {  
319 - if (isClosed()) {  
320 - return@withReentrantLock null 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) }
  333 + if (isClosed()) {
  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,13 +39,17 @@ class PublisherTransportObserver( @@ -31,13 +39,17 @@ 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  
35 - LKLog.v { "onIceCandidate: $candidate" }  
36 - client.sendCandidate(candidate, target = LivekitRtc.SignalTarget.PUBLISHER) 42 + executeOnRTCThread {
  43 + val candidate = iceCandidate ?: return@executeOnRTCThread
  44 + LKLog.v { "onIceCandidate: $candidate" }
  45 + client.sendCandidate(candidate, target = LivekitRtc.SignalTarget.PUBLISHER)
  46 + }
37 } 47 }
38 48
39 override fun onRenegotiationNeeded() { 49 override fun onRenegotiationNeeded() {
40 - engine.negotiatePublisher() 50 + executeOnRTCThread {
  51 + engine.negotiatePublisher()
  52 + }
41 } 53 }
42 54
43 override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { 55 override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
@@ -45,15 +57,19 @@ class PublisherTransportObserver( @@ -45,15 +57,19 @@ class PublisherTransportObserver(
45 } 57 }
46 58
47 override fun onOffer(sd: SessionDescription) { 59 override fun onOffer(sd: SessionDescription) {
48 - client.sendOffer(sd) 60 + executeOnRTCThread {
  61 + client.sendOffer(sd)
  62 + }
49 } 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) {
55 - LKLog.v { "onConnection new state: $newState" }  
56 - connectionChangeListener?.invoke(newState) 69 + executeOnRTCThread {
  70 + LKLog.v { "onConnection new state: $newState" }
  71 + connectionChangeListener?.invoke(newState)
  72 + }
57 } 73 }
58 74
59 override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent?) { 75 override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent?) {
@@ -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,27 +318,29 @@ internal constructor( @@ -316,27 +318,29 @@ internal constructor(
316 } 318 }
317 319
318 private fun closeResources(reason: String) { 320 private fun closeResources(reason: String) {
319 - publisherObserver.connectionChangeListener = null  
320 - subscriberObserver.connectionChangeListener = null  
321 - publisher?.closeBlocking()  
322 - publisher = null  
323 - subscriber?.closeBlocking()  
324 - subscriber = null  
325 -  
326 - fun DataChannel?.completeDispose() {  
327 - this?.unregisterObserver()  
328 - this?.close()  
329 - this?.dispose() 321 + executeBlockingOnRTCThread {
  322 + publisherObserver.connectionChangeListener = null
  323 + subscriberObserver.connectionChangeListener = null
  324 + publisher?.closeBlocking()
  325 + publisher = null
  326 + subscriber?.closeBlocking()
  327 + subscriber = null
  328 +
  329 + fun DataChannel?.completeDispose() {
  330 + this?.unregisterObserver()
  331 + this?.close()
  332 + this?.dispose()
  333 + }
  334 + reliableDataChannel?.completeDispose()
  335 + reliableDataChannel = null
  336 + reliableDataChannelSub?.completeDispose()
  337 + reliableDataChannelSub = null
  338 + lossyDataChannel?.completeDispose()
  339 + lossyDataChannel = null
  340 + lossyDataChannelSub?.completeDispose()
  341 + lossyDataChannelSub = null
  342 + isSubscriberPrimary = false
330 } 343 }
331 - reliableDataChannel?.completeDispose()  
332 - reliableDataChannel = null  
333 - reliableDataChannelSub?.completeDispose()  
334 - reliableDataChannelSub = null  
335 - lossyDataChannel?.completeDispose()  
336 - lossyDataChannel = null  
337 - lossyDataChannelSub?.completeDispose()  
338 - lossyDataChannelSub = null  
339 - isSubscriberPrimary = false  
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,59 +732,49 @@ internal constructor( @@ -732,59 +732,49 @@ 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 {  
738 - when (  
739 - val outcome =  
740 - subscriber?.setRemoteDescription(sessionDescription)  
741 - ) {  
742 - is Either.Right -> {  
743 - LKLog.e { "error setting remote description for answer: ${outcome.value} " }  
744 - return@withPeerConnection null  
745 - }  
746 -  
747 - else -> {} 735 + run {
  736 + when (val outcome = subscriber?.setRemoteDescription(sessionDescription).nullSafe()) {
  737 + is Either.Right -> {
  738 + LKLog.e { "error setting remote description for answer: ${outcome.value} " }
  739 + return@launch
748 } 740 }
749 - }  
750 741
751 - if (isClosed) {  
752 - return@withPeerConnection null 742 + else -> {}
753 } 743 }
  744 + }
754 745
755 - val answer = run {  
756 - when (val outcome = createAnswer(MediaConstraints())) {  
757 - is Either.Left -> outcome.value  
758 - is Either.Right -> {  
759 - LKLog.e { "error creating answer: ${outcome.value}" }  
760 - return@withPeerConnection null  
761 - }  
762 - }  
763 - } 746 + if (isClosed) {
  747 + return@launch
  748 + }
764 749
765 - if (isClosed) {  
766 - return@withPeerConnection null 750 + val answer = run {
  751 + when (val outcome = subscriber?.withPeerConnection { createAnswer(MediaConstraints()) }.nullSafe()) {
  752 + is Either.Left -> outcome.value
  753 + is Either.Right -> {
  754 + LKLog.e { "error creating answer: ${outcome.value}" }
  755 + return@launch
  756 + }
767 } 757 }
  758 + }
768 759
769 - run<Unit> {  
770 - when (val outcome = setLocalDescription(answer)) {  
771 - is Either.Right -> {  
772 - LKLog.e { "error setting local description for answer: ${outcome.value}" }  
773 - return@withPeerConnection null  
774 - } 760 + if (isClosed) {
  761 + return@launch
  762 + }
775 763
776 - else -> {} 764 + run<Unit> {
  765 + when (val outcome = subscriber?.withPeerConnection { setLocalDescription(answer) }.nullSafe()) {
  766 + is Either.Left -> Unit
  767 + is Either.Right -> {
  768 + LKLog.e { "error setting local description for answer: ${outcome.value}" }
  769 + return@launch
777 } 770 }
778 } 771 }
779 -  
780 - if (isClosed) {  
781 - return@withPeerConnection null  
782 - }  
783 - return@withPeerConnection answer  
784 } 772 }
785 - answer?.let {  
786 - client.sendAnswer(it) 773 +
  774 + if (isClosed) {
  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,14 +40,18 @@ class SubscriberTransportObserver( @@ -39,14 +40,18 @@ 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) {
42 - LKLog.v { "onIceCandidate: $candidate" }  
43 - client.sendCandidate(candidate, LivekitRtc.SignalTarget.SUBSCRIBER) 43 + executeOnRTCThread {
  44 + LKLog.v { "onIceCandidate: $candidate" }
  45 + client.sendCandidate(candidate, LivekitRtc.SignalTarget.SUBSCRIBER)
  46 + }
44 } 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  
48 - LKLog.v { "onAddTrack: ${track.kind()}, ${track.id()}, ${streams.fold("") { sum, it -> "$sum, $it" }}" }  
49 - engine.listener?.onAddTrack(receiver, track, streams) 50 + executeOnRTCThread {
  51 + val track = receiver.track() ?: return@executeOnRTCThread
  52 + LKLog.v { "onAddTrack: ${track.kind()}, ${track.id()}, ${streams.fold("") { sum, it -> "$sum, $it" }}" }
  53 + engine.listener?.onAddTrack(receiver, track, streams)
  54 + }
50 } 55 }
51 56
52 override fun onTrack(transceiver: RtpTransceiver) { 57 override fun onTrack(transceiver: RtpTransceiver) {
@@ -58,15 +63,19 @@ class SubscriberTransportObserver( @@ -58,15 +63,19 @@ class SubscriberTransportObserver(
58 } 63 }
59 64
60 override fun onDataChannel(channel: DataChannel) { 65 override fun onDataChannel(channel: DataChannel) {
61 - dataChannelListener?.invoke(channel) 66 + executeOnRTCThread {
  67 + dataChannelListener?.invoke(channel)
  68 + }
62 } 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) {
68 - LKLog.v { "onConnectionChange new state: $newState" }  
69 - connectionChangeListener?.invoke(newState) 75 + executeOnRTCThread {
  76 + LKLog.v { "onConnectionChange new state: $newState" }
  77 + connectionChangeListener?.invoke(newState)
  78 + }
70 } 79 }
71 80
72 override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent?) { 81 override fun onSelectedCandidatePairChanged(event: CandidatePairChangeEvent?) {
@@ -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) {
38 - rtcTrack.addSink(sink) 39 + executeBlockingOnRTCThread {
  40 + rtcTrack.addSink(sink)
  41 + }
39 } 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) {
45 - rtcTrack.removeSink(sink) 48 + executeBlockingOnRTCThread {
  49 + rtcTrack.removeSink(sink)
  50 + }
46 } 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,15 +150,21 @@ abstract class Track( @@ -149,15 +150,21 @@ 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() {
152 - rtcTrack.setEnabled(true) 153 + executeBlockingOnRTCThread {
  154 + rtcTrack.setEnabled(true)
  155 + }
153 } 156 }
154 157
155 open fun stop() { 158 open fun stop() {
156 - rtcTrack.setEnabled(false) 159 + executeBlockingOnRTCThread {
  160 + rtcTrack.setEnabled(false)
  161 + }
157 } 162 }
158 163
159 open fun dispose() { 164 open fun dispose() {
160 - rtcTrack.dispose() 165 + executeBlockingOnRTCThread {
  166 + rtcTrack.dispose()
  167 + }
161 } 168 }
162 } 169 }
163 170
@@ -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) {
33 - sinks.add(renderer)  
34 - rtcTrack.addSink(renderer) 34 + executeBlockingOnRTCThread {
  35 + sinks.add(renderer)
  36 + rtcTrack.addSink(renderer)
  37 + }
35 } 38 }
36 39
37 open fun removeRenderer(renderer: VideoSink) { 40 open fun removeRenderer(renderer: VideoSink) {
38 - rtcTrack.removeSink(renderer)  
39 - sinks.remove(renderer) 41 + executeBlockingOnRTCThread {
  42 + rtcTrack.removeSink(renderer)
  43 + sinks.remove(renderer)
  44 + }
40 } 45 }
41 46
42 override fun stop() { 47 override fun stop() {
43 - for (sink in sinks) {  
44 - rtcTrack.removeSink(sink) 48 + executeBlockingOnRTCThread {
  49 + for (sink in sinks) {
  50 + rtcTrack.removeSink(sink)
  51 + sinks.clear()
  52 + }
45 } 53 }
46 - sinks.clear()  
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,21 +28,24 @@ import timber.log.Timber @@ -28,21 +28,24 @@ import timber.log.Timber
28 */ 28 */
29 class LoggingRule : TestRule { 29 class LoggingRule : TestRule {
30 30
31 - val logTree = object : Timber.DebugTree() {  
32 - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {  
33 - val priorityChar = when (priority) {  
34 - Log.VERBOSE -> "v"  
35 - Log.DEBUG -> "d"  
36 - Log.INFO -> "i"  
37 - Log.WARN -> "w"  
38 - Log.ERROR -> "e"  
39 - Log.ASSERT -> "a"  
40 - else -> "?"  
41 - } 31 + companion object {
  32 +
  33 + val logTree = object : Timber.DebugTree() {
  34 + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
  35 + val priorityChar = when (priority) {
  36 + Log.VERBOSE -> "v"
  37 + Log.DEBUG -> "d"
  38 + Log.INFO -> "i"
  39 + Log.WARN -> "w"
  40 + Log.ERROR -> "e"
  41 + Log.ASSERT -> "a"
  42 + else -> "?"
  43 + }
42 44
43 - println("$priorityChar: $tag: $message")  
44 - if (t != null) {  
45 - println(t.toString()) 45 + println("$priorityChar: $tag: $message")
  46 + if (t != null) {
  47 + println(t.toString())
  48 + }
46 } 49 }
47 } 50 }
48 } 51 }