正在显示
7 个修改的文件
包含
195 行增加
和
42 行删除
| @@ -23,7 +23,7 @@ kotlin.code.style=official | @@ -23,7 +23,7 @@ kotlin.code.style=official | ||
| 23 | ############################################################### | 23 | ############################################################### |
| 24 | 24 | ||
| 25 | GROUP=io.livekit | 25 | GROUP=io.livekit |
| 26 | -VERSION_NAME=0.5.1 | 26 | +VERSION_NAME=0.6.0 |
| 27 | 27 | ||
| 28 | POM_DESCRIPTION=Android SDK for WebRTC communication | 28 | POM_DESCRIPTION=Android SDK for WebRTC communication |
| 29 | 29 |
| 1 | -<manifest package="io.livekit.android" /> | 1 | +<manifest package="io.livekit.android" |
| 2 | + xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| 3 | + | ||
| 4 | + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||
| 5 | + <uses-permission android:name="android.permission.INTERNET" /> | ||
| 6 | +</manifest> |
| @@ -19,13 +19,13 @@ class LiveKit { | @@ -19,13 +19,13 @@ class LiveKit { | ||
| 19 | options: ConnectOptions, | 19 | options: ConnectOptions, |
| 20 | listener: RoomListener? | 20 | listener: RoomListener? |
| 21 | ): Room { | 21 | ): Room { |
| 22 | - | 22 | + val ctx = appContext.applicationContext |
| 23 | val component = DaggerLiveKitComponent | 23 | val component = DaggerLiveKitComponent |
| 24 | .factory() | 24 | .factory() |
| 25 | - .create(appContext.applicationContext) | 25 | + .create(ctx) |
| 26 | 26 | ||
| 27 | val room = component.roomFactory() | 27 | val room = component.roomFactory() |
| 28 | - .create(options) | 28 | + .create(options, ctx) |
| 29 | room.listener = listener | 29 | room.listener = listener |
| 30 | room.connect(url, token) | 30 | room.connect(url, token) |
| 31 | 31 |
| @@ -23,12 +23,11 @@ class PublisherTransportObserver( | @@ -23,12 +23,11 @@ class PublisherTransportObserver( | ||
| 23 | override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { | 23 | override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { |
| 24 | val state = newState ?: throw NullPointerException("unexpected null new state, what do?") | 24 | val state = newState ?: throw NullPointerException("unexpected null new state, what do?") |
| 25 | Timber.v { "onIceConnection new state: $newState" } | 25 | Timber.v { "onIceConnection new state: $newState" } |
| 26 | - if (state == PeerConnection.IceConnectionState.CONNECTED && !engine.iceConnected) { | ||
| 27 | - engine.iceConnected = true | ||
| 28 | - engine.listener?.onICEConnected() | 26 | + if (state == PeerConnection.IceConnectionState.CONNECTED) { |
| 27 | + engine.iceState = IceState.CONNECTED | ||
| 29 | } else if (state == PeerConnection.IceConnectionState.FAILED) { | 28 | } else if (state == PeerConnection.IceConnectionState.FAILED) { |
| 30 | // when we publish tracks, some WebRTC versions will send out disconnected events periodically | 29 | // when we publish tracks, some WebRTC versions will send out disconnected events periodically |
| 31 | - engine.iceConnected = false | 30 | + engine.iceState = IceState.DISCONNECTED |
| 32 | engine.listener?.onDisconnect("Peer connection disconnected") | 31 | engine.listener?.onDisconnect("Peer connection disconnected") |
| 33 | } | 32 | } |
| 34 | } | 33 | } |
| @@ -22,6 +22,7 @@ import javax.inject.Inject | @@ -22,6 +22,7 @@ import javax.inject.Inject | ||
| 22 | import javax.inject.Named | 22 | import javax.inject.Named |
| 23 | 23 | ||
| 24 | /** | 24 | /** |
| 25 | + * SignalClient to LiveKit WS servers | ||
| 25 | * @suppress | 26 | * @suppress |
| 26 | */ | 27 | */ |
| 27 | class RTCClient | 28 | class RTCClient |
| @@ -37,24 +38,39 @@ constructor( | @@ -37,24 +38,39 @@ constructor( | ||
| 37 | var isConnected = false | 38 | var isConnected = false |
| 38 | private set | 39 | private set |
| 39 | private var currentWs: WebSocket? = null | 40 | private var currentWs: WebSocket? = null |
| 41 | + private var isReconnecting: Boolean = false | ||
| 40 | var listener: Listener? = null | 42 | var listener: Listener? = null |
| 41 | 43 | ||
| 42 | fun join( | 44 | fun join( |
| 43 | url: String, | 45 | url: String, |
| 44 | token: String, | 46 | token: String, |
| 47 | + reconnect: Boolean = false | ||
| 45 | ) { | 48 | ) { |
| 46 | - val wsUrlString = "$url/rtc?protocol=$PROTOCOL_VERSION&access_token=$token" | 49 | + var wsUrlString = "$url/rtc?protocol=$PROTOCOL_VERSION&access_token=$token" |
| 50 | + if (reconnect) { | ||
| 51 | + wsUrlString += "&reconnect=1" | ||
| 52 | + } | ||
| 47 | Timber.i { "connecting to $wsUrlString" } | 53 | Timber.i { "connecting to $wsUrlString" } |
| 48 | 54 | ||
| 55 | + isReconnecting = reconnect | ||
| 56 | + isConnected = false | ||
| 57 | + currentWs?.cancel() | ||
| 58 | + | ||
| 49 | val request = Request.Builder() | 59 | val request = Request.Builder() |
| 50 | .url(wsUrlString) | 60 | .url(wsUrlString) |
| 51 | .build() | 61 | .build() |
| 52 | currentWs = websocketFactory.newWebSocket(request, this) | 62 | currentWs = websocketFactory.newWebSocket(request, this) |
| 53 | } | 63 | } |
| 54 | 64 | ||
| 65 | + //--------------------------------- WebSocket Listener --------------------------------------// | ||
| 55 | override fun onOpen(webSocket: WebSocket, response: Response) { | 66 | override fun onOpen(webSocket: WebSocket, response: Response) { |
| 56 | - Timber.v { response.message } | ||
| 57 | super.onOpen(webSocket, response) | 67 | super.onOpen(webSocket, response) |
| 68 | + | ||
| 69 | + if (isReconnecting) { | ||
| 70 | + isReconnecting = false | ||
| 71 | + isConnected = true | ||
| 72 | + listener?.onReconnected() | ||
| 73 | + } | ||
| 58 | } | 74 | } |
| 59 | 75 | ||
| 60 | override fun onMessage(webSocket: WebSocket, text: String) { | 76 | override fun onMessage(webSocket: WebSocket, text: String) { |
| @@ -91,6 +107,7 @@ constructor( | @@ -91,6 +107,7 @@ constructor( | ||
| 91 | super.onFailure(webSocket, t, response) | 107 | super.onFailure(webSocket, t, response) |
| 92 | } | 108 | } |
| 93 | 109 | ||
| 110 | + //------------------------------- End WebSocket Listener ------------------------------------// | ||
| 94 | 111 | ||
| 95 | fun fromProtoSessionDescription(sd: LivekitRtc.SessionDescription): SessionDescription { | 112 | fun fromProtoSessionDescription(sd: LivekitRtc.SessionDescription): SessionDescription { |
| 96 | val rtcSdpType = when (sd.type) { | 113 | val rtcSdpType = when (sd.type) { |
| @@ -292,6 +309,7 @@ constructor( | @@ -292,6 +309,7 @@ constructor( | ||
| 292 | 309 | ||
| 293 | interface Listener { | 310 | interface Listener { |
| 294 | fun onJoin(info: LivekitRtc.JoinResponse) | 311 | fun onJoin(info: LivekitRtc.JoinResponse) |
| 312 | + fun onReconnected() | ||
| 295 | fun onAnswer(sessionDescription: SessionDescription) | 313 | fun onAnswer(sessionDescription: SessionDescription) |
| 296 | fun onOffer(sessionDescription: SessionDescription) | 314 | fun onOffer(sessionDescription: SessionDescription) |
| 297 | fun onTrickle(candidate: IceCandidate, target: LivekitRtc.SignalTarget) | 315 | fun onTrickle(candidate: IceCandidate, target: LivekitRtc.SignalTarget) |
| @@ -8,6 +8,7 @@ import io.livekit.android.util.CloseableCoroutineScope | @@ -8,6 +8,7 @@ import io.livekit.android.util.CloseableCoroutineScope | ||
| 8 | import io.livekit.android.util.Either | 8 | import io.livekit.android.util.Either |
| 9 | import kotlinx.coroutines.CoroutineDispatcher | 9 | import kotlinx.coroutines.CoroutineDispatcher |
| 10 | import kotlinx.coroutines.SupervisorJob | 10 | import kotlinx.coroutines.SupervisorJob |
| 11 | +import kotlinx.coroutines.delay | ||
| 11 | import kotlinx.coroutines.launch | 12 | import kotlinx.coroutines.launch |
| 12 | import livekit.LivekitModels | 13 | import livekit.LivekitModels |
| 13 | import livekit.LivekitRtc | 14 | import livekit.LivekitRtc |
| @@ -30,12 +31,36 @@ constructor( | @@ -30,12 +31,36 @@ constructor( | ||
| 30 | private val pctFactory: PeerConnectionTransport.Factory, | 31 | private val pctFactory: PeerConnectionTransport.Factory, |
| 31 | @Named(InjectionNames.DISPATCHER_IO) ioDispatcher: CoroutineDispatcher, | 32 | @Named(InjectionNames.DISPATCHER_IO) ioDispatcher: CoroutineDispatcher, |
| 32 | ) : RTCClient.Listener, DataChannel.Observer { | 33 | ) : RTCClient.Listener, DataChannel.Observer { |
| 33 | - | ||
| 34 | var listener: Listener? = null | 34 | var listener: Listener? = null |
| 35 | - var rtcConnected: Boolean = false | ||
| 36 | - var iceConnected: Boolean = false | 35 | + internal var iceState: IceState = IceState.DISCONNECTED |
| 36 | + set(value) { | ||
| 37 | + val oldVal = field | ||
| 38 | + field = value | ||
| 39 | + if (value == oldVal) { | ||
| 40 | + return | ||
| 41 | + } | ||
| 42 | + when (value) { | ||
| 43 | + IceState.CONNECTED -> { | ||
| 44 | + if (oldVal == IceState.DISCONNECTED) { | ||
| 45 | + Timber.d { "publisher ICE connected" } | ||
| 46 | + listener?.onIceConnected() | ||
| 47 | + } else if (oldVal == IceState.RECONNECTING) { | ||
| 48 | + Timber.d { "publisher ICE reconnected" } | ||
| 49 | + listener?.onIceReconnected() | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | + IceState.DISCONNECTED -> { | ||
| 53 | + Timber.d { "publisher ICE disconnected" } | ||
| 54 | + listener?.onDisconnect("Peer connection disconnected") | ||
| 55 | + } | ||
| 56 | + else -> {} | ||
| 57 | + } | ||
| 58 | + } | ||
| 59 | + private var wsRetries: Int = 0 | ||
| 37 | private val pendingTrackResolvers: MutableMap<String, Continuation<LivekitModels.TrackInfo>> = | 60 | private val pendingTrackResolvers: MutableMap<String, Continuation<LivekitModels.TrackInfo>> = |
| 38 | mutableMapOf() | 61 | mutableMapOf() |
| 62 | + private var sessionUrl: String? = null | ||
| 63 | + private var sessionToken: String? = null | ||
| 39 | 64 | ||
| 40 | private val publisherObserver = PublisherTransportObserver(this) | 65 | private val publisherObserver = PublisherTransportObserver(this) |
| 41 | private val subscriberObserver = SubscriberTransportObserver(this) | 66 | private val subscriberObserver = SubscriberTransportObserver(this) |
| @@ -50,6 +75,8 @@ constructor( | @@ -50,6 +75,8 @@ constructor( | ||
| 50 | } | 75 | } |
| 51 | 76 | ||
| 52 | fun join(url: String, token: String) { | 77 | fun join(url: String, token: String) { |
| 78 | + sessionUrl = url | ||
| 79 | + sessionToken = token | ||
| 53 | client.join(url, token) | 80 | client.join(url, token) |
| 54 | } | 81 | } |
| 55 | 82 | ||
| @@ -75,13 +102,39 @@ constructor( | @@ -75,13 +102,39 @@ constructor( | ||
| 75 | client.close() | 102 | client.close() |
| 76 | } | 103 | } |
| 77 | 104 | ||
| 78 | - fun negotiate() { | 105 | + /** |
| 106 | + * reconnect Signal and PeerConnections | ||
| 107 | + */ | ||
| 108 | + internal fun reconnect() { | ||
| 109 | + if (sessionUrl == null || sessionToken == null) { | ||
| 110 | + return | ||
| 111 | + } | ||
| 112 | + if (iceState == IceState.DISCONNECTED || wsRetries >= MAX_SIGNAL_RETRIES) { | ||
| 113 | + Timber.w { "could not connect to signal after max attempts, giving up" } | ||
| 114 | + close() | ||
| 115 | + listener?.onDisconnect("could not reconnect after limit") | ||
| 116 | + return | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + var startDelay = wsRetries.toLong() * wsRetries * 500 | ||
| 120 | + if (startDelay > 5000) { | ||
| 121 | + startDelay = 5000 | ||
| 122 | + } | ||
| 123 | + coroutineScope.launch { | ||
| 124 | + delay(startDelay) | ||
| 125 | + if (iceState != IceState.DISCONNECTED && sessionUrl != null && sessionToken != null) { | ||
| 126 | + client.join(sessionUrl!!, sessionToken!!, true) | ||
| 127 | + } | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + internal fun negotiate() { | ||
| 79 | if (!client.isConnected) { | 132 | if (!client.isConnected) { |
| 80 | return | 133 | return |
| 81 | } | 134 | } |
| 82 | coroutineScope.launch { | 135 | coroutineScope.launch { |
| 83 | val sdpOffer = | 136 | val sdpOffer = |
| 84 | - when (val outcome = publisher.peerConnection.createOffer(OFFER_CONSTRAINTS)) { | 137 | + when (val outcome = publisher.peerConnection.createOffer(getOfferConstraints())) { |
| 85 | is Either.Left -> outcome.value | 138 | is Either.Left -> outcome.value |
| 86 | is Either.Right -> { | 139 | is Either.Right -> { |
| 87 | Timber.d { "error creating offer: ${outcome.value}" } | 140 | Timber.d { "error creating offer: ${outcome.value}" } |
| @@ -101,16 +154,23 @@ constructor( | @@ -101,16 +154,23 @@ constructor( | ||
| 101 | } | 154 | } |
| 102 | } | 155 | } |
| 103 | 156 | ||
| 104 | - private fun onRTCConnected() { | ||
| 105 | - Timber.v { "RTC Connected" } | ||
| 106 | - rtcConnected = true | 157 | + private fun getOfferConstraints(): MediaConstraints { |
| 158 | + return MediaConstraints().apply { | ||
| 159 | + with(mandatory) { | ||
| 160 | + add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")) | ||
| 161 | + add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")) | ||
| 162 | + if (iceState == IceState.RECONNECTING) { | ||
| 163 | + add(MediaConstraints.KeyValuePair("IceRestart", "true")) | ||
| 164 | + } | ||
| 165 | + } | ||
| 166 | + } | ||
| 107 | } | 167 | } |
| 108 | 168 | ||
| 109 | interface Listener { | 169 | interface Listener { |
| 110 | fun onJoin(response: LivekitRtc.JoinResponse) | 170 | fun onJoin(response: LivekitRtc.JoinResponse) |
| 111 | - fun onICEConnected() | 171 | + fun onIceConnected() |
| 172 | + fun onIceReconnected() | ||
| 112 | fun onAddTrack(track: MediaStreamTrack, streams: Array<out MediaStream>) | 173 | fun onAddTrack(track: MediaStreamTrack, streams: Array<out MediaStream>) |
| 113 | -// fun onPublishLocalTrack(cid: String, track: LivekitModels.TrackInfo) | ||
| 114 | fun onUpdateParticipants(updates: List<LivekitModels.ParticipantInfo>) | 174 | fun onUpdateParticipants(updates: List<LivekitModels.ParticipantInfo>) |
| 115 | fun onUpdateSpeakers(speakers: List<LivekitRtc.SpeakerInfo>) | 175 | fun onUpdateSpeakers(speakers: List<LivekitRtc.SpeakerInfo>) |
| 116 | fun onDisconnect(reason: String) | 176 | fun onDisconnect(reason: String) |
| @@ -122,15 +182,7 @@ constructor( | @@ -122,15 +182,7 @@ constructor( | ||
| 122 | private const val RELIABLE_DATA_CHANNEL_LABEL = "_reliable" | 182 | private const val RELIABLE_DATA_CHANNEL_LABEL = "_reliable" |
| 123 | private const val LOSSY_DATA_CHANNEL_LABEL = "_lossy" | 183 | private const val LOSSY_DATA_CHANNEL_LABEL = "_lossy" |
| 124 | internal const val MAX_DATA_PACKET_SIZE = 15000 | 184 | internal const val MAX_DATA_PACKET_SIZE = 15000 |
| 125 | - | ||
| 126 | - private val OFFER_CONSTRAINTS = MediaConstraints().apply { | ||
| 127 | - with(mandatory) { | ||
| 128 | - add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "false")) | ||
| 129 | - add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "false")) | ||
| 130 | - } | ||
| 131 | - } | ||
| 132 | - | ||
| 133 | - private val MEDIA_CONSTRAINTS = MediaConstraints() | 185 | + private const val MAX_SIGNAL_RETRIES = 5 |
| 134 | 186 | ||
| 135 | internal val CONN_CONSTRAINTS = MediaConstraints().apply { | 187 | internal val CONN_CONSTRAINTS = MediaConstraints().apply { |
| 136 | with(optional) { | 188 | with(optional) { |
| @@ -155,7 +207,7 @@ constructor( | @@ -155,7 +207,7 @@ constructor( | ||
| 155 | ) | 207 | ) |
| 156 | } | 208 | } |
| 157 | 209 | ||
| 158 | - if(iceServers.isEmpty()){ | 210 | + if (iceServers.isEmpty()) { |
| 159 | iceServers.addAll(RTCClient.DEFAULT_ICE_SERVERS) | 211 | iceServers.addAll(RTCClient.DEFAULT_ICE_SERVERS) |
| 160 | } | 212 | } |
| 161 | info.iceServersList.forEach { | 213 | info.iceServersList.forEach { |
| @@ -192,7 +244,7 @@ constructor( | @@ -192,7 +244,7 @@ constructor( | ||
| 192 | 244 | ||
| 193 | coroutineScope.launch { | 245 | coroutineScope.launch { |
| 194 | val sdpOffer = | 246 | val sdpOffer = |
| 195 | - when (val outcome = publisher.peerConnection.createOffer(OFFER_CONSTRAINTS)) { | 247 | + when (val outcome = publisher.peerConnection.createOffer(getOfferConstraints())) { |
| 196 | is Either.Left -> outcome.value | 248 | is Either.Left -> outcome.value |
| 197 | is Either.Right -> { | 249 | is Either.Right -> { |
| 198 | Timber.d { "error creating offer: ${outcome.value}" } | 250 | Timber.d { "error creating offer: ${outcome.value}" } |
| @@ -212,14 +264,25 @@ constructor( | @@ -212,14 +264,25 @@ constructor( | ||
| 212 | listener?.onJoin(info) | 264 | listener?.onJoin(info) |
| 213 | } | 265 | } |
| 214 | 266 | ||
| 267 | + override fun onReconnected() { | ||
| 268 | + Timber.v { "reconnected, restarting ICE" } | ||
| 269 | + wsRetries = 0 | ||
| 270 | + | ||
| 271 | + // trigger ICE restart | ||
| 272 | + iceState = IceState.RECONNECTING | ||
| 273 | + negotiate() | ||
| 274 | + } | ||
| 275 | + | ||
| 215 | override fun onAnswer(sessionDescription: SessionDescription) { | 276 | override fun onAnswer(sessionDescription: SessionDescription) { |
| 216 | Timber.v { "received server answer: ${sessionDescription.type}, ${publisher.peerConnection.signalingState()}" } | 277 | Timber.v { "received server answer: ${sessionDescription.type}, ${publisher.peerConnection.signalingState()}" } |
| 217 | coroutineScope.launch { | 278 | coroutineScope.launch { |
| 218 | Timber.i { sessionDescription.toString() } | 279 | Timber.i { sessionDescription.toString() } |
| 219 | when (val outcome = publisher.setRemoteDescription(sessionDescription)) { | 280 | when (val outcome = publisher.setRemoteDescription(sessionDescription)) { |
| 220 | is Either.Left -> { | 281 | is Either.Left -> { |
| 221 | - if (!rtcConnected) { | ||
| 222 | - onRTCConnected() | 282 | + // when reconnecting, ICE might not have disconnected and won't trigger |
| 283 | + // our connected callback, so we'll take a shortcut and set it to active | ||
| 284 | + if (iceState == IceState.RECONNECTING) { | ||
| 285 | + iceState = IceState.CONNECTED | ||
| 223 | } | 286 | } |
| 224 | } | 287 | } |
| 225 | is Either.Right -> { | 288 | is Either.Right -> { |
| @@ -243,7 +306,7 @@ constructor( | @@ -243,7 +306,7 @@ constructor( | ||
| 243 | } | 306 | } |
| 244 | 307 | ||
| 245 | val answer = run { | 308 | val answer = run { |
| 246 | - when (val outcome = subscriber.peerConnection.createAnswer(OFFER_CONSTRAINTS)) { | 309 | + when (val outcome = subscriber.peerConnection.createAnswer(MediaConstraints())) { |
| 247 | is Either.Left -> outcome.value | 310 | is Either.Left -> outcome.value |
| 248 | is Either.Right -> { | 311 | is Either.Right -> { |
| 249 | Timber.e { "error creating answer: ${outcome.value}" } | 312 | Timber.e { "error creating answer: ${outcome.value}" } |
| @@ -275,11 +338,10 @@ constructor( | @@ -275,11 +338,10 @@ constructor( | ||
| 275 | } | 338 | } |
| 276 | 339 | ||
| 277 | override fun onLocalTrackPublished(response: LivekitRtc.TrackPublishedResponse) { | 340 | override fun onLocalTrackPublished(response: LivekitRtc.TrackPublishedResponse) { |
| 278 | - val signalCid = response.cid ?: run { | 341 | + val cid = response.cid ?: run { |
| 279 | Timber.e { "local track published with null cid?" } | 342 | Timber.e { "local track published with null cid?" } |
| 280 | return | 343 | return |
| 281 | } | 344 | } |
| 282 | - val cid = signalCid | ||
| 283 | 345 | ||
| 284 | val track = response.track | 346 | val track = response.track |
| 285 | if (track == null) { | 347 | if (track == null) { |
| @@ -293,7 +355,6 @@ constructor( | @@ -293,7 +355,6 @@ constructor( | ||
| 293 | return | 355 | return |
| 294 | } | 356 | } |
| 295 | cont.resume(response.track) | 357 | cont.resume(response.track) |
| 296 | -// listener?.onPublishLocalTrack(cid, track) | ||
| 297 | } | 358 | } |
| 298 | 359 | ||
| 299 | override fun onParticipantUpdate(updates: List<LivekitModels.ParticipantInfo>) { | 360 | override fun onParticipantUpdate(updates: List<LivekitModels.ParticipantInfo>) { |
| @@ -345,4 +406,10 @@ constructor( | @@ -345,4 +406,10 @@ constructor( | ||
| 345 | } | 406 | } |
| 346 | } | 407 | } |
| 347 | } | 408 | } |
| 409 | +} | ||
| 410 | + | ||
| 411 | + internal enum class IceState { | ||
| 412 | + DISCONNECTED, | ||
| 413 | + RECONNECTING, | ||
| 414 | + CONNECTED, | ||
| 348 | } | 415 | } |
| 1 | package io.livekit.android.room | 1 | package io.livekit.android.room |
| 2 | 2 | ||
| 3 | +import android.content.Context | ||
| 4 | +import android.net.ConnectivityManager | ||
| 5 | +import android.net.Network | ||
| 6 | +import android.net.NetworkCapabilities | ||
| 7 | +import android.net.NetworkRequest | ||
| 3 | import com.github.ajalt.timberkt.Timber | 8 | import com.github.ajalt.timberkt.Timber |
| 4 | import dagger.assisted.Assisted | 9 | import dagger.assisted.Assisted |
| 5 | import dagger.assisted.AssistedFactory | 10 | import dagger.assisted.AssistedFactory |
| @@ -22,10 +27,11 @@ class Room | @@ -22,10 +27,11 @@ class Room | ||
| 22 | @AssistedInject | 27 | @AssistedInject |
| 23 | constructor( | 28 | constructor( |
| 24 | @Assisted private val connectOptions: ConnectOptions, | 29 | @Assisted private val connectOptions: ConnectOptions, |
| 30 | + @Assisted private val context: Context, | ||
| 25 | private val engine: RTCEngine, | 31 | private val engine: RTCEngine, |
| 26 | private val eglBase: EglBase, | 32 | private val eglBase: EglBase, |
| 27 | private val localParticipantFactory: LocalParticipant.Factory | 33 | private val localParticipantFactory: LocalParticipant.Factory |
| 28 | -) : RTCEngine.Listener, ParticipantListener { | 34 | +) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() { |
| 29 | init { | 35 | init { |
| 30 | engine.listener = this | 36 | engine.listener = this |
| 31 | } | 37 | } |
| @@ -57,10 +63,16 @@ constructor( | @@ -57,10 +63,16 @@ constructor( | ||
| 57 | val activeSpeakers: List<Participant> | 63 | val activeSpeakers: List<Participant> |
| 58 | get() = mutableActiveSpeakers | 64 | get() = mutableActiveSpeakers |
| 59 | 65 | ||
| 66 | + private var hasLostConnectivity: Boolean = false | ||
| 60 | private var connectContinuation: Continuation<Unit>? = null | 67 | private var connectContinuation: Continuation<Unit>? = null |
| 61 | suspend fun connect(url: String, token: String) { | 68 | suspend fun connect(url: String, token: String) { |
| 69 | + state = State.CONNECTING | ||
| 62 | engine.join(url, token) | 70 | engine.join(url, token) |
| 63 | - | 71 | + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager |
| 72 | + val networkRequest = NetworkRequest.Builder() | ||
| 73 | + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) | ||
| 74 | + .build() | ||
| 75 | + cm.registerNetworkCallback(networkRequest, this) | ||
| 64 | return suspendCoroutine { connectContinuation = it } | 76 | return suspendCoroutine { connectContinuation = it } |
| 65 | } | 77 | } |
| 66 | 78 | ||
| @@ -136,7 +148,18 @@ constructor( | @@ -136,7 +148,18 @@ constructor( | ||
| 136 | listener?.onActiveSpeakersChanged(speakers, this) | 148 | listener?.onActiveSpeakersChanged(speakers, this) |
| 137 | } | 149 | } |
| 138 | 150 | ||
| 151 | + private fun reconnect() { | ||
| 152 | + if (state == State.RECONNECTING) { | ||
| 153 | + return | ||
| 154 | + } | ||
| 155 | + state = State.RECONNECTING | ||
| 156 | + engine.reconnect() | ||
| 157 | + listener?.onReconnecting(this) | ||
| 158 | + } | ||
| 159 | + | ||
| 139 | private fun handleDisconnect() { | 160 | private fun handleDisconnect() { |
| 161 | + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager | ||
| 162 | + cm.unregisterNetworkCallback(this) | ||
| 140 | for (pub in localParticipant.tracks.values) { | 163 | for (pub in localParticipant.tracks.values) { |
| 141 | pub.track?.stop() | 164 | pub.track?.stop() |
| 142 | } | 165 | } |
| @@ -149,6 +172,7 @@ constructor( | @@ -149,6 +172,7 @@ constructor( | ||
| 149 | engine.close() | 172 | engine.close() |
| 150 | state = State.DISCONNECTED | 173 | state = State.DISCONNECTED |
| 151 | listener?.onDisconnect(this, null) | 174 | listener?.onDisconnect(this, null) |
| 175 | + listener = null | ||
| 152 | } | 176 | } |
| 153 | 177 | ||
| 154 | /** | 178 | /** |
| @@ -156,9 +180,33 @@ constructor( | @@ -156,9 +180,33 @@ constructor( | ||
| 156 | */ | 180 | */ |
| 157 | @AssistedFactory | 181 | @AssistedFactory |
| 158 | interface Factory { | 182 | interface Factory { |
| 159 | - fun create(connectOptions: ConnectOptions): Room | 183 | + fun create(connectOptions: ConnectOptions, context: Context): Room |
| 184 | + } | ||
| 185 | + | ||
| 186 | + //------------------------------------- NetworkCallback -------------------------------------// | ||
| 187 | + | ||
| 188 | + /** | ||
| 189 | + * @suppress | ||
| 190 | + */ | ||
| 191 | + override fun onLost(network: Network) { | ||
| 192 | + // lost connection, flip to reconnecting | ||
| 193 | + hasLostConnectivity = true | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + /** | ||
| 197 | + * @suppress | ||
| 198 | + */ | ||
| 199 | + override fun onAvailable(network: Network) { | ||
| 200 | + // only actually reconnect after connection is re-established | ||
| 201 | + if (!hasLostConnectivity) { | ||
| 202 | + return | ||
| 203 | + } | ||
| 204 | + Timber.i { "network connection available, reconnecting" } | ||
| 205 | + reconnect() | ||
| 206 | + hasLostConnectivity = false | ||
| 160 | } | 207 | } |
| 161 | 208 | ||
| 209 | + | ||
| 162 | //----------------------------------- RTCEngine.Listener ------------------------------------// | 210 | //----------------------------------- RTCEngine.Listener ------------------------------------// |
| 163 | /** | 211 | /** |
| 164 | * @suppress | 212 | * @suppress |
| @@ -186,12 +234,17 @@ constructor( | @@ -186,12 +234,17 @@ constructor( | ||
| 186 | } | 234 | } |
| 187 | } | 235 | } |
| 188 | 236 | ||
| 189 | - override fun onICEConnected() { | 237 | + override fun onIceConnected() { |
| 190 | state = State.CONNECTED | 238 | state = State.CONNECTED |
| 191 | connectContinuation?.resume(Unit) | 239 | connectContinuation?.resume(Unit) |
| 192 | connectContinuation = null | 240 | connectContinuation = null |
| 193 | } | 241 | } |
| 194 | 242 | ||
| 243 | + override fun onIceReconnected() { | ||
| 244 | + state = State.CONNECTED | ||
| 245 | + listener?.onReconnected(this) | ||
| 246 | + } | ||
| 247 | + | ||
| 195 | /** | 248 | /** |
| 196 | * @suppress | 249 | * @suppress |
| 197 | */ | 250 | */ |
| @@ -346,6 +399,17 @@ constructor( | @@ -346,6 +399,17 @@ constructor( | ||
| 346 | */ | 399 | */ |
| 347 | interface RoomListener { | 400 | interface RoomListener { |
| 348 | /** | 401 | /** |
| 402 | + * A network change has been detected and LiveKit attempts to reconnect to the room | ||
| 403 | + * When reconnect attempts succeed, the room state will be kept, including tracks that are subscribed/published | ||
| 404 | + */ | ||
| 405 | + fun onReconnecting(room: Room) {} | ||
| 406 | + | ||
| 407 | + /** | ||
| 408 | + * The reconnect attempt had been successful | ||
| 409 | + */ | ||
| 410 | + fun onReconnected(room: Room) {} | ||
| 411 | + | ||
| 412 | + /** | ||
| 349 | * Disconnected from room | 413 | * Disconnected from room |
| 350 | */ | 414 | */ |
| 351 | fun onDisconnect(room: Room, error: Exception?) {} | 415 | fun onDisconnect(room: Room, error: Exception?) {} |
-
请 注册 或 登录 后发表评论