@@ -174,7 +174,6 @@ import stream.video.sfu.signal.UpdateSubscriptionsResponse
174174import java.io.InterruptedIOException
175175import java.net.SocketTimeoutException
176176import java.util.Collections
177- import java.util.concurrent.ConcurrentHashMap
178177import java.util.concurrent.atomic.AtomicBoolean
179178import 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
0 commit comments