Control Ringing→Active transition using ActiveStateGate and PeerConnection strategies#1633
Control Ringing→Active transition using ActiveStateGate and PeerConnection strategies#1633rahul-lohra wants to merge 13 commits intodevelopfrom
Conversation
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
WalkthroughThis pull request refactors session and ringing state management by converting Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant CallState
participant ActiveStateTransition
participant RtcSession
participant PeerConnection
Client->>CallState: Update ringing state to Active
CallState->>ActiveStateTransition: transitionToActiveState(currentRingingState, call)
activate ActiveStateTransition
alt Is incoming/outgoing call?
ActiveStateTransition->>RtcSession: Observe subscriber & publisher<br/>state flows
activate RtcSession
RtcSession->>PeerConnection: Monitor connection states
alt Strategy: ANY_PEER_CONNECTED
PeerConnection-->>RtcSession: Any peer reaches CONNECTED
else Strategy: BOTH_PEER_CONNECTED
PeerConnection-->>RtcSession: Both reach CONNECTED
end
alt Within 5s timeout
RtcSession-->>ActiveStateTransition: State reached
else Timeout
RtcSession-->>ActiveStateTransition: Timeout (log & proceed)
end
deactivate RtcSession
end
ActiveStateTransition->>CallState: Invoke transitionToActiveState()
CallState->>CallState: Set _ringingState to Active
deactivate ActiveStateTransition
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt (2)
1482-1496:⚠️ Potential issue | 🔴 CriticalBug: Comparing StateFlow objects to null instead of their values.
Lines 1482 and 1490 check
publisher == nullandsubscriber == null, but these are nowMutableStateFlowobjects that are never null. The check should be against.value.🐛 Proposed fix
- if (event.peerType == PeerType.PEER_TYPE_PUBLISHER_UNSPECIFIED && publisher == null && sfuConnectionModule.socketConnection.state().value is SfuSocketState.Connected) { + if (event.peerType == PeerType.PEER_TYPE_PUBLISHER_UNSPECIFIED && publisher.value == null && sfuConnectionModule.socketConnection.state().value is SfuSocketState.Connected) { logger.v { "[handleIceTrickle] `#sfu`; #${event.peerType.stringify()}; publisher is null, adding to pending" } publisherPendingEvents.add(event) return } - if (event.peerType == PeerType.PEER_TYPE_SUBSCRIBER && subscriber == null && sfuConnectionModule.socketConnection.state().value is SfuSocketState.Connected) { + if (event.peerType == PeerType.PEER_TYPE_SUBSCRIBER && subscriber.value == null && sfuConnectionModule.socketConnection.state().value is SfuSocketState.Connected) { logger.v { "[handleIceTrickle] `#sfu`; #${event.peerType.stringify()}; subscriber is null, adding to pending" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt` around lines 1482 - 1496, The null checks are currently comparing the StateFlow objects themselves (publisher and subscriber) instead of their contained values; update the conditions in handleIceTrickle to test publisher.value == null and subscriber.value == null respectively (while keeping the existing sfuConnectionModule.socketConnection.state().value is SfuSocketState.Connected checks) and continue to add the event to publisherPendingEvents or subscriberPendingEvents when the .value is null; ensure you reference the same symbols (publisher, subscriber, publisherPendingEvents, subscriberPendingEvents, sfuConnectionModule.socketConnection.state().value, SfuSocketState.Connected) so the logic behaves correctly with MutableStateFlow.
1521-1526:⚠️ Potential issue | 🔴 CriticalSimilar issue: Line 1521 checks
subscriber == nullinstead ofsubscriber.value == null.🐛 Proposed fix
suspend fun handleSubscriberOffer(offerEvent: SubscriberOfferEvent) { logger.d { "[handleSubscriberOffer] `#sfu`; `#subscriber`; event: $offerEvent" } - if (subscriber == null) { + if (subscriber.value == null) { subscriberPendingEvents.add(offerEvent) return }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt` around lines 1521 - 1526, The null check is using the wrapper reference instead of the held value; change the condition in RtcSession where it currently does if (subscriber == null) to if (subscriber.value == null) so that you add offerEvent to subscriberPendingEvents when there is no actual subscriber, then keep calling subscriber.value?.negotiate(offerEvent.sdp) afterwards; target the block that references subscriber, subscriberPendingEvents, offerEvent, and negotiate to make this replacement.
🧹 Nitpick comments (3)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt (1)
490-491: Consider using a simpler Set implementation.
ConcurrentHashMap.newKeySet()is thread-safe but the set is only written inupdateRingingState()and read inActiveStateTransition. Since both operations appear to occur on the same scope, a simplemutableSetOfguarded by the existing serialization might suffice. However, if thread safety is intentional for future use, this is acceptable.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt` around lines 490 - 491, Replace the thread-safe ConcurrentHashMap.newKeySet() used for previousRingingStates with a simple mutableSetOf<RingingState>() and rely on the existing serialization that already guards writes in updateRingingState() and reads in ActiveStateTransition; update any nullability or type expectations accordingly and add a brief comment on the assumed synchronization (or keep the ConcurrentHashMap.newKeySet and add a comment if you want to intentionally preserve thread-safety for future use).stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateTransition.kt (2)
33-33: Consider making the timeout configurable.The 5-second timeout is hardcoded. For varying network conditions or testing scenarios, consider accepting this as a constructor parameter with a default value.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateTransition.kt` at line 33, The hardcoded PEER_CONNECTION_OBSERVER_TIMEOUT should be made configurable: replace the top-level constant with a configurable parameter (e.g., timeoutMs) on the ActiveStateTransition class constructor (or factory) with a default of 5_000L, update any references that used PEER_CONNECTION_OBSERVER_TIMEOUT to use the instance property (timeoutMs), and keep the original default so behavior is unchanged unless overridden for testing or different network conditions.
60-63: Unused enum valueANY_PEER_CONNECTED.The
ANY_PEER_CONNECTEDstrategy is defined but never used—onlyBOTH_PEER_CONNECTEDis passed on line 52. Consider removing it to avoid dead code, or document if it's intended for future use.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateTransition.kt` around lines 60 - 63, The enum TransitionToRingingStateStrategy declares an unused member ANY_PEER_CONNECTED (only BOTH_PEER_CONNECTED is actually used); either remove ANY_PEER_CONNECTED to eliminate dead code or explicitly document its intended future use (or implement its logic where transition strategy is chosen); update the enum declaration accordingly and adjust any references to TransitionToRingingStateStrategy to reflect the reduced set or add a TODO/KDoc on ANY_PEER_CONNECTED explaining why it remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateTransition.kt`:
- Around line 111-124: The callback can run after cleanup() due to a race
between withTimeoutOrNull returning and calling transitionToActiveState(); guard
against this by checking coroutine cancellation before proceeding: after
withTimeoutOrNull (where result is read) and before calling
transitionToActiveState(), verify the coroutine is still active (e.g., check
coroutineContext.isActive or call ensureActive()) and bail out if cancelled;
update the block handling result/null in ActiveStateTransition (around
withTimeoutOrNull and transitionToActiveState) to perform this cancellation
check so transitionToActiveState() never runs post-cleanup().
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt`:
- Around line 235-238: Tests fail because callers try to assign the session flow
directly but _session is private and session is read-only; add a test-only
accessor or setter so tests can update the flow. For example, in the Call class
expose a `@VisibleForTesting` fun setSession(value: RtcSession?) (or mark _session
as internal with `@VisibleForTesting`) that updates the MutableStateFlow
_session.value, so tests that reference session assignments
(ReconnectSessionIdTest and ReconnectAttemptsCountTest) can set the session
without changing the public read-only session: StateFlow<RtcSession?>.
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt`:
- Line 682: The property peerConnectionObserverJob in CallState is unused dead
code; remove its declaration and any cancel call (currently at line where
cancelled) to avoid confusion and duplicate state since ActiveStateTransition
already manages its own peerConnectionObserverJob; search for
"peerConnectionObserverJob" and delete the private var in CallState plus the
cancel/cleanup call that references it, or alternatively if intended to be
shared, initialize and use it consistently across CallState methods and
ActiveStateTransition (prefer removal unless you have a real shared
responsibility).
---
Outside diff comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.kt`:
- Around line 1482-1496: The null checks are currently comparing the StateFlow
objects themselves (publisher and subscriber) instead of their contained values;
update the conditions in handleIceTrickle to test publisher.value == null and
subscriber.value == null respectively (while keeping the existing
sfuConnectionModule.socketConnection.state().value is SfuSocketState.Connected
checks) and continue to add the event to publisherPendingEvents or
subscriberPendingEvents when the .value is null; ensure you reference the same
symbols (publisher, subscriber, publisherPendingEvents, subscriberPendingEvents,
sfuConnectionModule.socketConnection.state().value, SfuSocketState.Connected) so
the logic behaves correctly with MutableStateFlow.
- Around line 1521-1526: The null check is using the wrapper reference instead
of the held value; change the condition in RtcSession where it currently does if
(subscriber == null) to if (subscriber.value == null) so that you add offerEvent
to subscriberPendingEvents when there is no actual subscriber, then keep calling
subscriber.value?.negotiate(offerEvent.sdp) afterwards; target the block that
references subscriber, subscriberPendingEvents, offerEvent, and negotiate to
make this replacement.
---
Nitpick comments:
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateTransition.kt`:
- Line 33: The hardcoded PEER_CONNECTION_OBSERVER_TIMEOUT should be made
configurable: replace the top-level constant with a configurable parameter
(e.g., timeoutMs) on the ActiveStateTransition class constructor (or factory)
with a default of 5_000L, update any references that used
PEER_CONNECTION_OBSERVER_TIMEOUT to use the instance property (timeoutMs), and
keep the original default so behavior is unchanged unless overridden for testing
or different network conditions.
- Around line 60-63: The enum TransitionToRingingStateStrategy declares an
unused member ANY_PEER_CONNECTED (only BOTH_PEER_CONNECTED is actually used);
either remove ANY_PEER_CONNECTED to eliminate dead code or explicitly document
its intended future use (or implement its logic where transition strategy is
chosen); update the enum declaration accordingly and adjust any references to
TransitionToRingingStateStrategy to reflect the reduced set or add a TODO/KDoc
on ANY_PEER_CONNECTED explaining why it remains.
In
`@stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt`:
- Around line 490-491: Replace the thread-safe ConcurrentHashMap.newKeySet()
used for previousRingingStates with a simple mutableSetOf<RingingState>() and
rely on the existing serialization that already guards writes in
updateRingingState() and reads in ActiveStateTransition; update any nullability
or type expectations accordingly and add a brief comment on the assumed
synchronization (or keep the ConcurrentHashMap.newKeySet and add a comment if
you want to intentionally preserve thread-safety for future use).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: aabc82c3-ef94-452b-8aad-f185e736a6c2
📒 Files selected for processing (7)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateTransition.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallStats.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoClient.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/call/RtcSession.ktstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/handlers/StreamMediaSessionController.kt
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateGate.kt
Show resolved
Hide resolved
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
Outdated
Show resolved
Hide resolved
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
Outdated
Show resolved
Hide resolved
104a380 to
2d6d91d
Compare
2d6d91d to
85975bf
Compare
|


Goal
Improve the logic to render
RingingState.Activewhen both of thePeerConnectionsPublisher's & Subscriber's are connectedPreviously
In ringing calls, we transitioned to
RingingState.Activebased on signaling events—either when the call was accepted by the other participant or when call.join() completed.CallAcceptedEventjoin()CallAcceptedEventjoin()Problem
With the current approach, we transition to
RingingState.Activebefore the publisher and subscriberRTCPeerConnectionsare fully connected to the SFU.Since these connections are established asynchronously, this results in a gap where the UI is active but no media packets are flowing yet. Internally, the publisher and subscriber are still in the process of establishing their transport (ICE + DTLS), leading to a few seconds of silence.
Solution: Transition to
RingingState.Activewill include one more logic that either one of thePublisherorSubscribershould get connected to SFU first with a custimizable internal timeoutProposed Solution
Update the transition logic for
RingingState.Activeto include an additional condition:RingingState.Activewhen both the publisher or subscriber reachesPeerConnectionState.CONNECTED, instead of relying solely on signaling events.internal timeoutfor 5 seconds to ensure we don’t block indefinitely if neither connection reachesCONNECTED.CallAcceptedEvent& both one publisher & subscriber is connected with SFUjoin()& both publisher & subscriber is connected with SFUCallAcceptedEvent& both one publisher and subscriber is connected with SFUjoin()& both publisher and subscriber is connected with SFUWe can use any strategy to experiment, by default we will select
BOTH_PEER_CONNECTEDImplementation
Call.sessionobservablePublisher&Subscriberboth observabletransitionToActiveStatelogicPeerConnectionstate withtimeoutbefore transition to RingingState.Active🎨 UI Changes
None, just the time to render
RingState.Activehas increasedTesting
Summary by CodeRabbit
Release Notes
New Features
Improvements