Ensure smoother Ringing to Active transition#1633
Ensure smoother Ringing to Active transition#1633aleksandar-apostolov merged 20 commits intodevelopfrom
Ringing to Active transition#1633Conversation
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 | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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
After considering the non-negotiable privacy concern, where either user must not be rendered on the other side unless they are able to see themselves on their device. We evaluated a few approaches — using a unified strategy across platforms vs. platform-specific handling — and also reviewed how WhatsApp and Telegram handle similar UX. One key difference is that WhatsApp and Telegram render the local camera feed during the outgoing ringing state. On the callee side, as soon as the call is accepted, they immediately render the local camera feed and then transition to the remote stream. This avoids the privacy gap we’re currently facing in our flow (and in Blink’s case). Based on this, we agree to use Publisher Connected which feels like the right balance. Even if we standardize on Both Peers Connected across platforms, it still introduces potential privacy risks.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/CallState.kt
Show resolved
Hide resolved
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateGate.kt
Show resolved
Hide resolved
...o-android-core/src/main/kotlin/io/getstream/video/android/core/call/connection/Subscriber.kt
Outdated
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/Call.kt
Show resolved
Hide resolved
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateGate.kt
Show resolved
Hide resolved
...o-android-core/src/main/kotlin/io/getstream/video/android/core/call/connection/Subscriber.kt
Outdated
Show resolved
Hide resolved
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ActiveStateGate.kt
Outdated
Show resolved
Hide resolved
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt
Outdated
Show resolved
Hide resolved
|
aleksandar-apostolov
left a comment
There was a problem hiding this comment.
All feedback addressed. LGTM.
Ringing to Active transition
|
🚀 Available in v1.21.0 |


Goal
Ensure a responsive transition to
RingingState.Active, where a user is not rendered to others unless their own local media is available.Previously
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
The Both Peers Connected strategy introduces one key issue:
• Privacy Risk: There can still be edge cases where a user is rendered remotely before they have a confirmed local preview, violating a strict privacy expectation.
Additionally, this approach does not align well with established real-world patterns.
Apps like WhatsApp and Telegram:
• Render the local camera preview immediately during the outgoing ringing state
• Show local preview on the callee side right after call acceptance
• Transition to remote media once available
This avoids the “active UI but no media” gap while maintaining privacy.
Solution
Adopt the Publisher Connected strategy:
• Transition to RingingState.Active when the Publisher reaches PeerConnectionState.CONNECTED
• Ensure the user’s local media is available before exposing them to others
• Avoid waiting for Subscriber connection to reduce latency
This provides a better balance between:
• Privacy guarantees
• Faster perceived call setup
• Alignment with industry-standard UX patterns
CallAcceptedEvent& both one publisher & subscriber is connected with SFUjoin()& publisher is connected with SFUCallAcceptedEvent& when publisher is connected with SFUjoin()& both publisher and subscriber is connected with SFUWe can use any strategy to experiment, by default we will select
PUBLISHER_CONNECTED⸻
Implementation
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