davidliu
Committed by GitHub

Fix memory leak caused by disconnecting before connect finished (#386)

* State locking for Room and RTC engine around critical spots

* Cancel connect job if invoking coroutine is cancelled

* cleanup

* Clean up test logs

* revert stress test changes to sample apps
@@ -144,7 +144,7 @@ constructor( @@ -144,7 +144,7 @@ constructor(
144 restartingIce = true 144 restartingIce = true
145 } 145 }
146 146
147 - if (this.peerConnection.signalingState() == SignalingState.HAVE_LOCAL_OFFER) { 147 + if (peerConnection.signalingState() == SignalingState.HAVE_LOCAL_OFFER) {
148 // we're waiting for the peer to accept our offer, so we'll just wait 148 // we're waiting for the peer to accept our offer, so we'll just wait
149 // the only exception to this is when ICE restart is needed 149 // the only exception to this is when ICE restart is needed
150 val curSd = peerConnection.remoteDescription 150 val curSd = peerConnection.remoteDescription
@@ -313,7 +313,7 @@ constructor( @@ -313,7 +313,7 @@ constructor(
313 } 313 }
314 314
315 @OptIn(ExperimentalContracts::class) 315 @OptIn(ExperimentalContracts::class)
316 - private suspend inline fun <T> launchRTCIfNotClosed(noinline action: suspend () -> T): T? { 316 + private suspend inline fun <T> launchRTCIfNotClosed(noinline action: suspend CoroutineScope.() -> T): T? {
317 contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) } 317 contract { callsInPlace(action, InvocationKind.AT_MOST_ONCE) }
318 if (isClosed()) { 318 if (isClosed()) {
319 return null 319 return null
@@ -35,13 +35,17 @@ import io.livekit.android.util.FlowObservable @@ -35,13 +35,17 @@ import io.livekit.android.util.FlowObservable
35 import io.livekit.android.util.LKLog 35 import io.livekit.android.util.LKLog
36 import io.livekit.android.util.flowDelegate 36 import io.livekit.android.util.flowDelegate
37 import io.livekit.android.util.nullSafe 37 import io.livekit.android.util.nullSafe
  38 +import io.livekit.android.util.withCheckLock
38 import io.livekit.android.webrtc.RTCStatsGetter 39 import io.livekit.android.webrtc.RTCStatsGetter
39 import io.livekit.android.webrtc.copy 40 import io.livekit.android.webrtc.copy
40 import io.livekit.android.webrtc.isConnected 41 import io.livekit.android.webrtc.isConnected
41 import io.livekit.android.webrtc.isDisconnected 42 import io.livekit.android.webrtc.isDisconnected
42 import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread 43 import io.livekit.android.webrtc.peerconnection.executeBlockingOnRTCThread
  44 +import io.livekit.android.webrtc.peerconnection.launchBlockingOnRTCThread
43 import io.livekit.android.webrtc.toProtoSessionDescription 45 import io.livekit.android.webrtc.toProtoSessionDescription
44 import kotlinx.coroutines.* 46 import kotlinx.coroutines.*
  47 +import kotlinx.coroutines.sync.Mutex
  48 +import kotlinx.coroutines.sync.withLock
45 import livekit.LivekitModels 49 import livekit.LivekitModels
46 import livekit.LivekitRtc 50 import livekit.LivekitRtc
47 import livekit.LivekitRtc.JoinResponse 51 import livekit.LivekitRtc.JoinResponse
@@ -134,6 +138,12 @@ internal constructor( @@ -134,6 +138,12 @@ internal constructor(
134 138
135 private var coroutineScope = CloseableCoroutineScope(SupervisorJob() + ioDispatcher) 139 private var coroutineScope = CloseableCoroutineScope(SupervisorJob() + ioDispatcher)
136 140
  141 + /**
  142 + * Note: If this lock is ever used in conjunction with the RTC thread,
  143 + * this must be grabbed on the RTC thread to prevent deadlocks.
  144 + */
  145 + private var configurationLock = Mutex()
  146 +
137 init { 147 init {
138 client.listener = this 148 client.listener = this
139 } 149 }
@@ -158,8 +168,10 @@ internal constructor( @@ -158,8 +168,10 @@ internal constructor(
158 token: String, 168 token: String,
159 options: ConnectOptions, 169 options: ConnectOptions,
160 roomOptions: RoomOptions, 170 roomOptions: RoomOptions,
161 - ): JoinResponse { 171 + ): JoinResponse = coroutineScope {
162 val joinResponse = client.join(url, token, options, roomOptions) 172 val joinResponse = client.join(url, token, options, roomOptions)
  173 + ensureActive()
  174 +
163 listener?.onJoinResponse(joinResponse) 175 listener?.onJoinResponse(joinResponse)
164 isClosed = false 176 isClosed = false
165 listener?.onSignalConnected(false) 177 listener?.onSignalConnected(false)
@@ -169,93 +181,103 @@ internal constructor( @@ -169,93 +181,103 @@ internal constructor(
169 configure(joinResponse, options) 181 configure(joinResponse, options)
170 182
171 // create offer 183 // create offer
172 - if (!this.isSubscriberPrimary) { 184 + if (!isSubscriberPrimary) {
173 negotiatePublisher() 185 negotiatePublisher()
174 } 186 }
175 client.onReadyForResponses() 187 client.onReadyForResponses()
176 - return joinResponse 188 +
  189 + return@coroutineScope joinResponse
177 } 190 }
178 191
179 private suspend fun configure(joinResponse: JoinResponse, connectOptions: ConnectOptions) { 192 private suspend fun configure(joinResponse: JoinResponse, connectOptions: ConnectOptions) {
180 - if (publisher != null && subscriber != null) {  
181 - // already configured  
182 - return  
183 - } 193 + launchBlockingOnRTCThread {
  194 + configurationLock.withCheckLock(
  195 + {
  196 + ensureActive()
  197 + if (publisher != null && subscriber != null) {
  198 + // already configured
  199 + return@launchBlockingOnRTCThread
  200 + }
  201 + },
  202 + ) {
  203 + participantSid = if (joinResponse.hasParticipant()) {
  204 + joinResponse.participant.sid
  205 + } else {
  206 + null
  207 + }
184 208
185 - participantSid = if (joinResponse.hasParticipant()) {  
186 - joinResponse.participant.sid  
187 - } else {  
188 - null  
189 - } 209 + // Setup peer connections
  210 + val rtcConfig = makeRTCConfig(Either.Left(joinResponse), connectOptions)
190 211
191 - // Setup peer connections  
192 - val rtcConfig = makeRTCConfig(Either.Left(joinResponse), connectOptions) 212 + publisher?.close()
  213 + publisher = pctFactory.create(
  214 + rtcConfig,
  215 + publisherObserver,
  216 + publisherObserver,
  217 + )
  218 + subscriber?.close()
  219 + subscriber = pctFactory.create(
  220 + rtcConfig,
  221 + subscriberObserver,
  222 + null,
  223 + )
193 224
194 - publisher?.close()  
195 - publisher = pctFactory.create(  
196 - rtcConfig,  
197 - publisherObserver,  
198 - publisherObserver,  
199 - )  
200 - subscriber?.close()  
201 - subscriber = pctFactory.create(  
202 - rtcConfig,  
203 - subscriberObserver,  
204 - null,  
205 - ) 225 + val connectionStateListener: (PeerConnection.PeerConnectionState) -> Unit = { newState ->
  226 + LKLog.v { "onIceConnection new state: $newState" }
  227 + if (newState.isConnected()) {
  228 + connectionState = ConnectionState.CONNECTED
  229 + } else if (newState.isDisconnected()) {
  230 + connectionState = ConnectionState.DISCONNECTED
  231 + }
  232 + }
206 233
207 - val connectionStateListener: (PeerConnection.PeerConnectionState) -> Unit = { newState ->  
208 - LKLog.v { "onIceConnection new state: $newState" }  
209 - if (newState.isConnected()) {  
210 - connectionState = ConnectionState.CONNECTED  
211 - } else if (newState.isDisconnected()) {  
212 - connectionState = ConnectionState.DISCONNECTED  
213 - }  
214 - } 234 + if (joinResponse.subscriberPrimary) {
  235 + // in subscriber primary mode, server side opens sub data channels.
  236 + subscriberObserver.dataChannelListener = onDataChannel@{ dataChannel: DataChannel ->
  237 + when (dataChannel.label()) {
  238 + RELIABLE_DATA_CHANNEL_LABEL -> reliableDataChannelSub = dataChannel
  239 + LOSSY_DATA_CHANNEL_LABEL -> lossyDataChannelSub = dataChannel
  240 + else -> return@onDataChannel
  241 + }
  242 + dataChannel.registerObserver(DataChannelObserver(dataChannel))
  243 + }
215 244
216 - if (joinResponse.subscriberPrimary) {  
217 - // in subscriber primary mode, server side opens sub data channels.  
218 - subscriberObserver.dataChannelListener = onDataChannel@{ dataChannel: DataChannel ->  
219 - when (dataChannel.label()) {  
220 - RELIABLE_DATA_CHANNEL_LABEL -> reliableDataChannelSub = dataChannel  
221 - LOSSY_DATA_CHANNEL_LABEL -> lossyDataChannelSub = dataChannel  
222 - else -> return@onDataChannel 245 + subscriberObserver.connectionChangeListener = connectionStateListener
  246 + // Also reconnect on publisher disconnect
  247 + publisherObserver.connectionChangeListener = { newState ->
  248 + if (newState.isDisconnected()) {
  249 + reconnect()
  250 + }
  251 + }
  252 + } else {
  253 + publisherObserver.connectionChangeListener = connectionStateListener
223 } 254 }
224 - dataChannel.registerObserver(DataChannelObserver(dataChannel))  
225 - }  
226 255
227 - subscriberObserver.connectionChangeListener = connectionStateListener  
228 - // Also reconnect on publisher disconnect  
229 - publisherObserver.connectionChangeListener = { newState ->  
230 - if (newState.isDisconnected()) {  
231 - reconnect() 256 + ensureActive()
  257 + // data channels
  258 + val reliableInit = DataChannel.Init()
  259 + reliableInit.ordered = true
  260 + reliableDataChannel = publisher?.withPeerConnection {
  261 + createDataChannel(
  262 + RELIABLE_DATA_CHANNEL_LABEL,
  263 + reliableInit,
  264 + ).also { dataChannel ->
  265 + dataChannel.registerObserver(DataChannelObserver(dataChannel))
  266 + }
232 } 267 }
233 - }  
234 - } else {  
235 - publisherObserver.connectionChangeListener = connectionStateListener  
236 - }  
237 -  
238 - // data channels  
239 - val reliableInit = DataChannel.Init()  
240 - reliableInit.ordered = true  
241 - reliableDataChannel = publisher?.withPeerConnection {  
242 - createDataChannel(  
243 - RELIABLE_DATA_CHANNEL_LABEL,  
244 - reliableInit,  
245 - ).also { dataChannel ->  
246 - dataChannel.registerObserver(DataChannelObserver(dataChannel))  
247 - }  
248 - }  
249 268
250 - val lossyInit = DataChannel.Init()  
251 - lossyInit.ordered = true  
252 - lossyInit.maxRetransmits = 0  
253 - lossyDataChannel = publisher?.withPeerConnection {  
254 - createDataChannel(  
255 - LOSSY_DATA_CHANNEL_LABEL,  
256 - lossyInit,  
257 - ).also { dataChannel ->  
258 - dataChannel.registerObserver(DataChannelObserver(dataChannel)) 269 + ensureActive()
  270 + val lossyInit = DataChannel.Init()
  271 + lossyInit.ordered = true
  272 + lossyInit.maxRetransmits = 0
  273 + lossyDataChannel = publisher?.withPeerConnection {
  274 + createDataChannel(
  275 + LOSSY_DATA_CHANNEL_LABEL,
  276 + lossyInit,
  277 + ).also { dataChannel ->
  278 + dataChannel.registerObserver(DataChannelObserver(dataChannel))
  279 + }
  280 + }
259 } 281 }
260 } 282 }
261 } 283 }
@@ -327,27 +349,32 @@ internal constructor( @@ -327,27 +349,32 @@ internal constructor(
327 349
328 private fun closeResources(reason: String) { 350 private fun closeResources(reason: String) {
329 executeBlockingOnRTCThread { 351 executeBlockingOnRTCThread {
330 - publisherObserver.connectionChangeListener = null  
331 - subscriberObserver.connectionChangeListener = null  
332 - publisher?.closeBlocking()  
333 - publisher = null  
334 - subscriber?.closeBlocking()  
335 - subscriber = null  
336 -  
337 - fun DataChannel?.completeDispose() {  
338 - this?.unregisterObserver()  
339 - this?.close()  
340 - this?.dispose() 352 + runBlocking {
  353 + configurationLock.withLock {
  354 + publisherObserver.connectionChangeListener = null
  355 + subscriberObserver.connectionChangeListener = null
  356 + publisher?.closeBlocking()
  357 + publisher = null
  358 + subscriber?.closeBlocking()
  359 + subscriber = null
  360 +
  361 + fun DataChannel?.completeDispose() {
  362 + this?.unregisterObserver()
  363 + this?.close()
  364 + this?.dispose()
  365 + }
  366 +
  367 + reliableDataChannel?.completeDispose()
  368 + reliableDataChannel = null
  369 + reliableDataChannelSub?.completeDispose()
  370 + reliableDataChannelSub = null
  371 + lossyDataChannel?.completeDispose()
  372 + lossyDataChannel = null
  373 + lossyDataChannelSub?.completeDispose()
  374 + lossyDataChannelSub = null
  375 + isSubscriberPrimary = false
  376 + }
341 } 377 }
342 - reliableDataChannel?.completeDispose()  
343 - reliableDataChannel = null  
344 - reliableDataChannelSub?.completeDispose()  
345 - reliableDataChannelSub = null  
346 - lossyDataChannel?.completeDispose()  
347 - lossyDataChannel = null  
348 - lossyDataChannelSub?.completeDispose()  
349 - lossyDataChannelSub = null  
350 - isSubscriberPrimary = false  
351 } 378 }
352 client.close(reason = reason) 379 client.close(reason = reason)
353 } 380 }
@@ -49,6 +49,8 @@ import io.livekit.android.webrtc.getFilteredStats @@ -49,6 +49,8 @@ import io.livekit.android.webrtc.getFilteredStats
49 import kotlinx.coroutines.* 49 import kotlinx.coroutines.*
50 import kotlinx.coroutines.flow.filterNotNull 50 import kotlinx.coroutines.flow.filterNotNull
51 import kotlinx.coroutines.flow.first 51 import kotlinx.coroutines.flow.first
  52 +import kotlinx.coroutines.sync.Mutex
  53 +import kotlinx.coroutines.sync.withLock
52 import kotlinx.serialization.Serializable 54 import kotlinx.serialization.Serializable
53 import livekit.LivekitModels 55 import livekit.LivekitModels
54 import livekit.LivekitRtc 56 import livekit.LivekitRtc
@@ -243,6 +245,8 @@ constructor( @@ -243,6 +245,8 @@ constructor(
243 private var hasLostConnectivity: Boolean = false 245 private var hasLostConnectivity: Boolean = false
244 private var connectOptions: ConnectOptions = ConnectOptions() 246 private var connectOptions: ConnectOptions = ConnectOptions()
245 247
  248 + private var stateLock = Mutex()
  249 +
246 private fun getCurrentRoomOptions(): RoomOptions = 250 private fun getCurrentRoomOptions(): RoomOptions =
247 RoomOptions( 251 RoomOptions(
248 adaptiveStream = adaptiveStream, 252 adaptiveStream = adaptiveStream,
@@ -260,93 +264,133 @@ constructor( @@ -260,93 +264,133 @@ constructor(
260 * @param url 264 * @param url
261 * @param token 265 * @param token
262 * @param options 266 * @param options
  267 + *
  268 + * @throws IllegalStateException when connect is attempted while the room is not disconnected.
  269 + * @throws Exception when connection fails
263 */ 270 */
264 @Throws(Exception::class) 271 @Throws(Exception::class)
265 - suspend fun connect(url: String, token: String, options: ConnectOptions = ConnectOptions()) {  
266 - if (this::coroutineScope.isInitialized) {  
267 - coroutineScope.cancel() 272 + suspend fun connect(url: String, token: String, options: ConnectOptions = ConnectOptions()) = coroutineScope {
  273 + if (state != State.DISCONNECTED) {
  274 + throw IllegalStateException("Room.connect attempted while room is not disconnected!")
268 } 275 }
269 - coroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob()) 276 + val roomOptions: RoomOptions
  277 + stateLock.withLock {
  278 + if (state != State.DISCONNECTED) {
  279 + throw IllegalStateException("Room.connect attempted while room is not disconnected!")
  280 + }
  281 + if (::coroutineScope.isInitialized) {
  282 + val job = coroutineScope.coroutineContext.job
  283 + coroutineScope.cancel()
  284 + job.join()
  285 + }
270 286
271 - val roomOptions = getCurrentRoomOptions() 287 + state = State.CONNECTING
  288 + connectOptions = options
272 289
273 - // Setup local participant.  
274 - localParticipant.reinitialize()  
275 - coroutineScope.launch {  
276 - localParticipant.events.collect {  
277 - when (it) {  
278 - is ParticipantEvent.TrackPublished -> emitWhenConnected(  
279 - RoomEvent.TrackPublished(  
280 - room = this@Room,  
281 - publication = it.publication,  
282 - participant = it.participant,  
283 - ),  
284 - ) 290 + coroutineScope = CoroutineScope(defaultDispatcher + SupervisorJob())
285 291
286 - is ParticipantEvent.ParticipantPermissionsChanged -> emitWhenConnected(  
287 - RoomEvent.ParticipantPermissionsChanged(  
288 - room = this@Room,  
289 - participant = it.participant,  
290 - newPermissions = it.newPermissions,  
291 - oldPermissions = it.oldPermissions,  
292 - ),  
293 - ) 292 + roomOptions = getCurrentRoomOptions()
294 293
295 - is ParticipantEvent.MetadataChanged -> {  
296 - emitWhenConnected(  
297 - RoomEvent.ParticipantMetadataChanged(  
298 - this@Room,  
299 - it.participant,  
300 - it.prevMetadata, 294 + // Setup local participant.
  295 + localParticipant.reinitialize()
  296 + coroutineScope.launch {
  297 + localParticipant.events.collect {
  298 + when (it) {
  299 + is ParticipantEvent.TrackPublished -> emitWhenConnected(
  300 + RoomEvent.TrackPublished(
  301 + room = this@Room,
  302 + publication = it.publication,
  303 + participant = it.participant,
301 ), 304 ),
302 ) 305 )
303 - }  
304 306
305 - is ParticipantEvent.NameChanged -> {  
306 - emitWhenConnected(  
307 - RoomEvent.ParticipantNameChanged(  
308 - this@Room,  
309 - it.participant,  
310 - it.name, 307 + is ParticipantEvent.ParticipantPermissionsChanged -> emitWhenConnected(
  308 + RoomEvent.ParticipantPermissionsChanged(
  309 + room = this@Room,
  310 + participant = it.participant,
  311 + newPermissions = it.newPermissions,
  312 + oldPermissions = it.oldPermissions,
311 ), 313 ),
312 ) 314 )
313 - }  
314 315
315 - else -> {  
316 - // do nothing 316 + is ParticipantEvent.MetadataChanged -> {
  317 + emitWhenConnected(
  318 + RoomEvent.ParticipantMetadataChanged(
  319 + this@Room,
  320 + it.participant,
  321 + it.prevMetadata,
  322 + ),
  323 + )
  324 + }
  325 +
  326 + is ParticipantEvent.NameChanged -> {
  327 + emitWhenConnected(
  328 + RoomEvent.ParticipantNameChanged(
  329 + this@Room,
  330 + it.participant,
  331 + it.name,
  332 + ),
  333 + )
  334 + }
  335 +
  336 + else -> {
  337 + // do nothing
  338 + }
317 } 339 }
318 } 340 }
319 } 341 }
320 - }  
321 -  
322 - state = State.CONNECTING  
323 - connectOptions = options  
324 342
325 - if (roomOptions.e2eeOptions != null) {  
326 - e2eeManager = e2EEManagerFactory.create(roomOptions.e2eeOptions.keyProvider).apply {  
327 - setup(this@Room) { event ->  
328 - coroutineScope.launch {  
329 - emitWhenConnected(event) 343 + if (roomOptions.e2eeOptions != null) {
  344 + e2eeManager = e2EEManagerFactory.create(roomOptions.e2eeOptions.keyProvider).apply {
  345 + setup(this@Room) { event ->
  346 + coroutineScope.launch {
  347 + emitWhenConnected(event)
  348 + }
330 } 349 }
331 } 350 }
332 } 351 }
333 } 352 }
334 353
335 - engine.join(url, token, options, roomOptions)  
336 - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager  
337 - val networkRequest = NetworkRequest.Builder()  
338 - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)  
339 - .build()  
340 - cm.registerNetworkCallback(networkRequest, networkCallback) 354 + // Use an empty coroutineExceptionHandler since we want to
  355 + // rethrow all throwables from the connect job.
  356 + val emptyCoroutineExceptionHandler = CoroutineExceptionHandler { _, _ -> }
  357 + val connectJob = coroutineScope.launch(
  358 + ioDispatcher + emptyCoroutineExceptionHandler,
  359 + ) {
  360 + engine.join(url, token, options, roomOptions)
  361 + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
  362 + val networkRequest = NetworkRequest.Builder()
  363 + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
  364 + .build()
  365 + cm.registerNetworkCallback(networkRequest, networkCallback)
  366 +
  367 + ensureActive()
  368 + if (options.audio) {
  369 + val audioTrack = localParticipant.createAudioTrack()
  370 + localParticipant.publishAudioTrack(audioTrack)
  371 + }
  372 + ensureActive()
  373 + if (options.video) {
  374 + val videoTrack = localParticipant.createVideoTrack()
  375 + localParticipant.publishVideoTrack(videoTrack)
  376 + }
  377 + }
341 378
342 - if (options.audio) {  
343 - val audioTrack = localParticipant.createAudioTrack()  
344 - localParticipant.publishAudioTrack(audioTrack) 379 + val outerHandler = coroutineContext.job.invokeOnCompletion { cause ->
  380 + // Cancel connect job if invoking coroutine is cancelled.
  381 + if (cause is CancellationException) {
  382 + connectJob.cancel(cause)
  383 + }
345 } 384 }
346 - if (options.video) {  
347 - val videoTrack = localParticipant.createVideoTrack()  
348 - localParticipant.publishVideoTrack(videoTrack) 385 +
  386 + var error: Throwable? = null
  387 + connectJob.invokeOnCompletion { cause ->
  388 + outerHandler.dispose()
  389 + error = cause
349 } 390 }
  391 + connectJob.join()
  392 +
  393 + error?.let { throw it }
350 } 394 }
351 395
352 /** 396 /**
@@ -592,6 +636,35 @@ constructor( @@ -592,6 +636,35 @@ constructor(
592 engine.reconnect() 636 engine.reconnect()
593 } 637 }
594 638
  639 + private fun handleDisconnect(reason: DisconnectReason) {
  640 + if (state == State.DISCONNECTED) {
  641 + return
  642 + }
  643 + runBlocking {
  644 + stateLock.withLock {
  645 + if (state == State.DISCONNECTED) {
  646 + return@runBlocking
  647 + }
  648 + try {
  649 + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
  650 + cm.unregisterNetworkCallback(networkCallback)
  651 + } catch (e: IllegalArgumentException) {
  652 + // do nothing, may happen on older versions if attempting to unregister twice.
  653 + }
  654 +
  655 + state = State.DISCONNECTED
  656 + cleanupRoom()
  657 + engine.close()
  658 +
  659 + localParticipant.dispose()
  660 +
  661 + // Ensure all observers see the disconnected before closing scope.
  662 + eventBus.postEvent(RoomEvent.Disconnected(this@Room, null, reason), coroutineScope).join()
  663 + coroutineScope.cancel()
  664 + }
  665 + }
  666 + }
  667 +
595 /** 668 /**
596 * Removes all participants and tracks from the room. 669 * Removes all participants and tracks from the room.
597 */ 670 */
@@ -609,31 +682,6 @@ constructor( @@ -609,31 +682,6 @@ constructor(
609 sidToIdentity.clear() 682 sidToIdentity.clear()
610 } 683 }
611 684
612 - private fun handleDisconnect(reason: DisconnectReason) {  
613 - if (state == State.DISCONNECTED) {  
614 - return  
615 - }  
616 -  
617 - try {  
618 - val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager  
619 - cm.unregisterNetworkCallback(networkCallback)  
620 - } catch (e: IllegalArgumentException) {  
621 - // do nothing, may happen on older versions if attempting to unregister twice.  
622 - }  
623 -  
624 - state = State.DISCONNECTED  
625 - cleanupRoom()  
626 - engine.close()  
627 -  
628 - localParticipant.dispose()  
629 -  
630 - // Ensure all observers see the disconnected before closing scope.  
631 - runBlocking {  
632 - eventBus.postEvent(RoomEvent.Disconnected(this@Room, null, reason), coroutineScope).join()  
633 - }  
634 - coroutineScope.cancel()  
635 - }  
636 -  
637 private fun sendSyncState() { 685 private fun sendSyncState() {
638 // Whether we're sending subscribed tracks or tracks to unsubscribe. 686 // Whether we're sending subscribed tracks or tracks to unsubscribe.
639 val sendUnsub = connectOptions.autoSubscribe 687 val sendUnsub = connectOptions.autoSubscribe
  1 +/*
  2 + * Copyright 2024 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.util
  18 +
  19 +import kotlinx.coroutines.sync.Mutex
  20 +import kotlinx.coroutines.sync.withLock
  21 +
  22 +/**
  23 + * Applies a double-checked lock before running [action].
  24 + */
  25 +suspend inline fun <T> Mutex.withCheckLock(check: () -> Unit, action: () -> T): T {
  26 + check()
  27 + return withLock {
  28 + check()
  29 + action()
  30 + }
  31 +}
1 /* 1 /*
2 - * Copyright 2023 LiveKit, Inc. 2 + * Copyright 2023-2024 LiveKit, Inc.
3 * 3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License. 5 * you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package io.livekit.android.webrtc.peerconnection @@ -18,6 +18,7 @@ package io.livekit.android.webrtc.peerconnection
18 18
19 import androidx.annotation.VisibleForTesting 19 import androidx.annotation.VisibleForTesting
20 import kotlinx.coroutines.CoroutineDispatcher 20 import kotlinx.coroutines.CoroutineDispatcher
  21 +import kotlinx.coroutines.CoroutineScope
21 import kotlinx.coroutines.asCoroutineDispatcher 22 import kotlinx.coroutines.asCoroutineDispatcher
22 import kotlinx.coroutines.async 23 import kotlinx.coroutines.async
23 import kotlinx.coroutines.coroutineScope 24 import kotlinx.coroutines.coroutineScope
@@ -41,7 +42,7 @@ private val threadFactory = object : ThreadFactory { @@ -41,7 +42,7 @@ private val threadFactory = object : ThreadFactory {
41 } 42 }
42 } 43 }
43 44
44 -// var only for testing purposes, do not alter! 45 +// var only for testing purposes, do not alter in production!
45 private var executor = Executors.newSingleThreadExecutor(threadFactory) 46 private var executor = Executors.newSingleThreadExecutor(threadFactory)
46 private var rtcDispatcher: CoroutineDispatcher = executor.asCoroutineDispatcher() 47 private var rtcDispatcher: CoroutineDispatcher = executor.asCoroutineDispatcher()
47 48
@@ -82,12 +83,12 @@ fun <T> executeBlockingOnRTCThread(action: () -> T): T { @@ -82,12 +83,12 @@ fun <T> executeBlockingOnRTCThread(action: () -> T): T {
82 * is generally not thread safe, so all actions relating to 83 * is generally not thread safe, so all actions relating to
83 * peer connection objects should go through the RTC thread. 84 * peer connection objects should go through the RTC thread.
84 */ 85 */
85 -suspend fun <T> launchBlockingOnRTCThread(action: suspend () -> T): T = coroutineScope { 86 +suspend fun <T> launchBlockingOnRTCThread(action: suspend CoroutineScope.() -> T): T = coroutineScope {
86 return@coroutineScope if (Thread.currentThread().name.startsWith(EXECUTOR_THREADNAME_PREFIX)) { 87 return@coroutineScope if (Thread.currentThread().name.startsWith(EXECUTOR_THREADNAME_PREFIX)) {
87 - action() 88 + this.action()
88 } else { 89 } else {
89 async(rtcDispatcher) { 90 async(rtcDispatcher) {
90 - action() 91 + this.action()
91 }.await() 92 }.await()
92 } 93 }
93 } 94 }