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
正在显示
5 个修改的文件
包含
294 行增加
和
187 行删除
| @@ -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 | } |
-
请 注册 或 登录 后发表评论