stream-video-android icon indicating copy to clipboard operation
stream-video-android copied to clipboard

Querying calls triggers unintended side effects: `call.leave()` invoked on detached instances

Open amaslanka opened this issue 8 months ago β€’ 2 comments

Describe the bug Querying a call using queryCalls() (or get() method in the Call objects) results in a side effect where the SDK automatically invokes internal call state update logic. Specifically, this leads to invoking updateFromResponse() β†’ updateRingingState(), which checks if the call has been rejected (rejectedBy.size == 1) and no active outgoing members (outgoingMemberCount == 0). If these conditions are met, the SDK incorrectly calls call.leave(), which cleans up the call state and terminates the call screen, even if the call was simply queried for other purposes.

This is especially problematic because:

  • The queried Call instance is expected to be detached and passive.
  • However, invoking queryCalls() creates a full Call object that mutates the shared singleton StreamVideoClient state.
  • As a result, calling leave() on this newly queried object affects global state: it disables the microphone and camera, stops CallService, and removes the ringing call from memory.

Due to this, it becomes impossible to re-join a previously rejected call, as fetching it forcibly triggers termination logic.

Additional context

  • Our use case requires querying calls in order to manage group calling behavior within chat rooms. Specifically:
    • We want to detect if there's already an active call in a group chat before starting a new one.
    • If another user taps the Start Call button while a call is ongoing, they should join the existing session instead of creating a parallel one.
    • This logic is necessary to avoid race conditions and ensure a single source of truth for the ongoing call.
    • However, due to the current SDK behavior, querying a call causes SDK-internal state changes, which undermines this logic and breaks active sessions.

SDK version

  • 1.4.4

To Reproduce Steps to reproduce the behavior:

  1. Start a call between three users (with unique id and ring = true)
  2. User A starts the call, user B accepts the call, user C rejects the call.
  3. User C tries to query the same call using queryCalls().

Image 4. If an ongoing call is found, its id is passed to the StreamVideoActivity. 5. This causes a race condition, because while we invoke the StreamVideoActivity, the previous Call object fetched using queryCall also updates the internal state of the StreamVideoClient, which is a singleton. 6. The StreamVideoClient enters unexpected and undesired state -> when user C tries to rej-oin the call, the UI closes unexpectedly, because call.leave() is called on the Call object fetched via queryCalls. 7. The above bug causes that the call connection leaks and even if the StreamVideoActivity is closed, User C is still visible in the call for other users. Each attempt of joining the same call results in a new "instance" of User C being present in the call.

Expected behavior Querying a call using queryCalls() or getCall() should not have side effects. It should return a detached Call object that does not modify the global StreamVideoClient state unless explicitly joined or modified.

Device:

  • Vendor and model: Google Pixel 6
  • Android version: 15

Screenshots

  • Internal logic tracing to updateRingingState() and call.leave() Image

  • Call instance destructively mutating StreamVideoClient

Image

Additional context

  • The internal getCall() method in StreamVideoClient (which is safe and side-effect free) is not publicly accessible.
  • We suggest exposing a safe, detached version of getCall() for UI use cases that don’t require active state mutation.

amaslanka avatar Apr 24 '25 13:04 amaslanka

It looks like this issue is related to the following PR:

  • https://github.com/GetStream/stream-video-android/pull/1363

Fingers crossed that the PR gets merged soon! 🀞

amaslanka avatar Apr 25 '25 07:04 amaslanka

Hey @aleksandar-apostolov!

We would like to report another related issue we encountered while fetching calls.

It seems that queryCalls() does not return full API response, even though the complete data is present there (we verified this using Proxyman). In particular, we need access to certain properties of a call's session, specifically call.session.participants, to determine who is currently in a given call β€” and we need to fetch this data without introducing any side effects on the client.

From what I can see in the linked PR, it looks like the QueriedCalls object will include a watchedCalls: List<Call>, but only for calls queried with the watch = true flag β€” is that correct? https://github.com/GetStream/stream-video-android/pull/1363/files#diff-f47619517acae686cd6d550853ce96a69579f373495e22e422a58ce9d89bdaf2R956 If so, unfortunately, that would not solve our use case.

Would it be possible to expose the entire QueryCallsResponse object in the response? We have a use case in our implementation where we need to access more information than just the very basic ones included in the CallData object.

Additionally, it would be great if you could make the suspend fun getCall(type: String, id: String): Result<...> method from StreamVideoClient implementation public and expose it through the StreamVideo interface. This would allow us to retrieve call data by ID without unwanted side effects, effectively mitigating the issue.

Please let me know if one of these changes could be included in the current PR β€” it would really help us.

Thanks a lot for your support!

amaslanka avatar Apr 28 '25 09:04 amaslanka