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