Skip to content

Commit 9a2f685

Browse files
Merge branch 'develop' into aleksandarapostolov/and-1617-move-dokka-api-docs-publishing-out-of-the-release-workflow
2 parents ec44086 + f7bc965 commit 9a2f685

4 files changed

Lines changed: 27 additions & 68 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<div align="center">
1616

1717
![stream-video-android-core](https://img.shields.io/badge/stream--video--android--core-12.37%20MB-lightgreen)
18-
![stream-video-android-ui-xml](https://img.shields.io/badge/stream--video--android--ui--xml-5.67%20MB-lightgreen)
18+
![stream-video-android-ui-xml](https://img.shields.io/badge/stream--video--android--ui--xml-5.66%20MB-lightgreen)
1919
![stream-video-android-ui-compose](https://img.shields.io/badge/stream--video--android--ui--compose-6.18%20MB-lightgreen)
2020

2121
</div>

‎metrics/size.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
},
77
"release": {
88
"stream-video-android-core": 12676,
9-
"stream-video-android-ui-xml": 5812,
9+
"stream-video-android-ui-xml": 5796,
1010
"stream-video-android-ui-compose": 6336
1111
}
1212
}

‎stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt‎

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,6 @@ import stream.video.sfu.signal.UpdateSubscriptionsResponse
174174
import java.io.InterruptedIOException
175175
import java.net.SocketTimeoutException
176176
import java.util.Collections
177-
import java.util.concurrent.ConcurrentHashMap
178177
import java.util.concurrent.atomic.AtomicBoolean
179178
import java.util.concurrent.atomic.AtomicInteger
180179
/**
@@ -339,13 +338,6 @@ public class RtcSession internal constructor(
339338
*/
340339
private val muteSyncEnabled = AtomicBoolean(true)
341340

342-
/**
343-
* Tracks whose SFU sync was skipped while [muteSyncEnabled] was false. [resumeMuteSync]
344-
* flushes only these so we do not POST for unpublished tracks (the map is pre-seeded
345-
* with audio/video/screen-share) or race [listenToMediaChanges] collectLatest replay.
346-
*/
347-
private val pendingMuteSyncTracks: MutableSet<TrackType> =
348-
ConcurrentHashMap.newKeySet()
349341
private val oneBasedSessionCounter = sessionCounter + 1
350342

351343
/**
@@ -1402,7 +1394,6 @@ public class RtcSession internal constructor(
14021394

14031395
mediaScope.cancel()
14041396
muteSyncEnabled.set(false)
1405-
pendingMuteSyncTracks.clear()
14061397
muteStateSyncJobs.cancelAll()
14071398

14081399
// cleanup all non-local tracks
@@ -1451,11 +1442,10 @@ public class RtcSession internal constructor(
14511442
* cannot lose another track's bit via a stale read–copy–write.
14521443
*
14531444
* During reconnect/migration, [cancelActiveWork] pauses SFU sync so collectors can keep
1454-
* recording the latest desired bits without posting to a stale connection. Tracks that
1455-
* would have posted are recorded in [pendingMuteSyncTracks]. Sync resumes from
1456-
* [connectRtc] once the active SFU is ready and flushes only those pending tracks —
1457-
* not the whole pre-seeded map — so unpublished tracks stay silent and
1458-
* [listenToMediaChanges] collectLatest replay does not cancel a redundant flush.
1445+
* recording the latest desired bits without posting to a stale connection. [resumeMuteSync]
1446+
* re-opens the gate from [connectRtc] once the active SFU is ready; it does not replay the
1447+
* skipped tracks itself, because [listenToMediaChanges] restarts in the same call and its
1448+
* collectLatest replay re-signals every track anyway.
14591449
*/
14601450
private fun setMuteState(isEnabled: Boolean, trackType: TrackType) {
14611451
logger.d { "[setPublishState] #sfu; $trackType isEnabled: $isEnabled" }
@@ -1466,15 +1456,10 @@ public class RtcSession internal constructor(
14661456

14671457
private fun syncMuteStateToSfu(trackType: TrackType, isEnabled: Boolean) {
14681458
if (!muteSyncEnabled.get()) {
1469-
pendingMuteSyncTracks.add(trackType)
1470-
// Resume may have flipped the flag between the check and the add. Fall through
1471-
// and post; same-track launch coalesces with any flush already in flight.
1472-
if (!muteSyncEnabled.get()) {
1473-
logger.d {
1474-
"[syncMuteStateToSfu] pending until SFU is ready; $trackType isEnabled: $isEnabled"
1475-
}
1476-
return
1459+
logger.d {
1460+
"[syncMuteStateToSfu] skipped, SFU not ready; $trackType isEnabled: $isEnabled"
14771461
}
1462+
return
14781463
}
14791464

14801465
val currentSfu = sfuUrl
@@ -1510,15 +1495,18 @@ public class RtcSession internal constructor(
15101495
}
15111496
}
15121497

1498+
/**
1499+
* Re-opens the SFU mute sync gate closed by [cancelActiveWork].
1500+
*
1501+
* Deliberately does not replay the tracks that were skipped while paused. [connectRtc]
1502+
* restarts [listenToMediaChanges] immediately after this, and its collectLatest replay
1503+
* re-signals camera, microphone and screen-share from their current state. Flushing here
1504+
* as well produced a second RPC for the same track that the replay then cancelled
1505+
* (`IOException: Canceled`), which is what this gate exists to avoid.
1506+
*/
15131507
private fun resumeMuteSync() {
15141508
if (!muteSyncEnabled.compareAndSet(false, true)) return
1515-
val tracksToFlush = pendingMuteSyncTracks.toSet()
1516-
pendingMuteSyncTracks.clear()
1517-
logger.d { "[resumeMuteSync] flushing pending mute state for $tracksToFlush" }
1518-
tracksToFlush.forEach { trackType ->
1519-
val isEnabled = muteState.value[trackType] ?: return@forEach
1520-
syncMuteStateToSfu(trackType, isEnabled)
1521-
}
1509+
logger.d { "[resumeMuteSync] SFU ready, mute sync re-enabled" }
15221510
}
15231511

15241512
private fun isPermanentError(cause: Throwable): Boolean {
@@ -2260,11 +2248,6 @@ public class RtcSession internal constructor(
22602248
iceMonitoringJob?.cancel()
22612249
iceMonitoringJob = null
22622250
muteSyncEnabled.set(false)
2263-
// Nothing is deferred at the moment we pause, so anything still in the set is
2264-
// a leftover from a collector that raced the previous [resumeMuteSync] and
2265-
// posted anyway. Dropping it here keeps the next resume from flushing a track
2266-
// this pause never deferred.
2267-
pendingMuteSyncTracks.clear()
22682251
muteStateSyncJobs.cancelAll()
22692252
participantsMonitoringJob?.cancel()
22702253
participantsMonitoringJob = null

‎stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/rtc/RtcSessionTest2.kt‎

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,7 @@ class RtcSessionTest2 {
11111111
}
11121112

11131113
@Test
1114-
fun `mute sync resumes and flushes local state once the SFU is ready`() = runTest(
1114+
fun `mute sync resumes without replaying the tracks it skipped`() = runTest(
11151115
testDispatcher,
11161116
) {
11171117
val signalService = mockk<SignalServerService>(relaxed = true)
@@ -1129,42 +1129,25 @@ class RtcSessionTest2 {
11291129
testScheduler.advanceUntilIdle()
11301130

11311131
assertEquals(true, rtcSession.muteState.value[TrackType.TRACK_TYPE_AUDIO])
1132-
assertTrue(pendingMuteSyncTracks(rtcSession).contains(TrackType.TRACK_TYPE_AUDIO))
1133-
coVerify(exactly = 0) { signalService.updateMuteStates(any()) }
11341132
assertEquals(false, muteSyncEnabled(rtcSession).get())
1133+
coVerify(exactly = 0) { signalService.updateMuteStates(any()) }
11351134

11361135
RtcSession::class.java.getDeclaredMethod("resumeMuteSync").apply {
11371136
isAccessible = true
11381137
invoke(rtcSession)
11391138
}
11401139
testScheduler.advanceUntilIdle()
11411140

1142-
coVerify(exactly = 1) { signalService.updateMuteStates(any()) }
1143-
assertTrue(pendingMuteSyncTracks(rtcSession).isEmpty())
1141+
// Resume only re-opens the gate. listenToMediaChanges' collectLatest replay is what
1142+
// re-signals each track, so resuming must not POST on its own — a flush here races
1143+
// that replay and gets cancelled mid-request.
11441144
assertEquals(true, muteSyncEnabled(rtcSession).get())
1145-
}
1146-
1147-
@Test
1148-
fun `resumeMuteSync does not flush tracks that were never pending`() = runTest(
1149-
testDispatcher,
1150-
) {
1151-
val signalService = mockk<SignalServerService>(relaxed = true)
1152-
val (rtcSession, _) = muteSyncSession(signalService)
1153-
1154-
rtcSession.enterMigration()
1155-
testScheduler.advanceUntilIdle()
11561145
coVerify(exactly = 0) { signalService.updateMuteStates(any()) }
11571146

1158-
RtcSession::class.java.getDeclaredMethod("resumeMuteSync").apply {
1159-
isAccessible = true
1160-
invoke(rtcSession)
1161-
}
1147+
// The gate really is open: the next change reaches the SFU.
1148+
rtcSession.createAndPublishAudioTrack()
11621149
testScheduler.advanceUntilIdle()
1163-
1164-
// muteState is pre-seeded with audio/video/screen-share; none of those were
1165-
// recorded while paused, so resume must not POST UpdateMuteStates.
1166-
coVerify(exactly = 0) { signalService.updateMuteStates(any()) }
1167-
assertEquals(true, muteSyncEnabled(rtcSession).get())
1150+
coVerify(exactly = 1) { signalService.updateMuteStates(any()) }
11681151
}
11691152

11701153
@Test
@@ -1190,13 +1173,6 @@ class RtcSessionTest2 {
11901173
return field.get(rtcSession) as AtomicBoolean
11911174
}
11921175

1193-
@Suppress("UNCHECKED_CAST")
1194-
private fun pendingMuteSyncTracks(rtcSession: RtcSession): MutableSet<TrackType> {
1195-
val field = RtcSession::class.java.getDeclaredField("pendingMuteSyncTracks")
1196-
field.isAccessible = true
1197-
return field.get(rtcSession) as MutableSet<TrackType>
1198-
}
1199-
12001176
private fun muteSyncSession(
12011177
signalService: SignalServerService,
12021178
): Pair<RtcSession, Publisher> {

0 commit comments

Comments
 (0)