Skip to content

feat: Refactor media session handling and improve disconnect logic#7065

Merged
diegolmello merged 1 commit into
feat.voip-lib-newfrom
feat.voip-change-ws
Mar 24, 2026
Merged

feat: Refactor media session handling and improve disconnect logic#7065
diegolmello merged 1 commit into
feat.voip-lib-newfrom
feat.voip-change-ws

Conversation

@diegolmello

@diegolmello diegolmello commented Mar 24, 2026

Copy link
Copy Markdown
Member

Proposed changes

This PR tightens VoIP / media-signaling lifecycle so teardown matches SDK disconnect and server or permission changes.

  • connect.disconnect() now calls mediaSessionInstance.reset() after sdk.disconnect(), so VoIP state and listeners are cleared whenever the app disconnects through the connect service.
  • Call sites that previously used sdk.disconnect() directly (logout, delete-account flow in login saga, useConnectServer) now use disconnect() from connect so VoIP reset always runs.
  • MediaSessionInstance: unsubscribe from mediaSessionStore.onChange on teardown (fixes stacked handlers on re-init), null out listener references after stop, and reset() / stop() call mediaSessionStore.dispose() and reset the call store.
  • MediaSessionStore.dispose(): end the signaling session, clear send-signal and WebRTC factory, and notify subscribers.
  • selectServer: reset the media session when switching workspace before reconnecting.
  • startVoipFork: mediaSessionInstance.reset() when the user does not have VoIP permissions, so prior init is not left active after permission loss.
  • Tests: mock MediaSessionInstance in connect / connect.ios tests; add MediaSessionInstance.test.ts for init, signal routing, user switch, reset, and idempotent teardown.

Issue(s)

https://rocketchat.atlassian.net/browse/VMUX-60

How to test or reproduce

  1. Log in on a server with VoIP enabled, then log out — confirm no duplicate or stale stream handlers after logging in again (e.g. repeated / unexpected signaling behavior).
  2. Switch servers (multi-server) with VoIP — confirm VoIP re-initializes for the new workspace and does not keep state from the previous server.
  3. Connect to a new workspace from New Server View (path that disconnects the previous session) — confirm a clean disconnect + reconnect.
  4. If applicable: user loses VoIP permissions (or logs into a context without VoIP) after having had VoIP — confirm media session is torn down.
  5. Run yarn test (including MediaSessionInstance.test.ts and connect tests).

Screenshots

N/A — behavioral / lifecycle changes, no UI changes.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

selectServer calls mediaSessionInstance.reset() before connect(), and connect() still begins with disconnect(), which resets VoIP again — redundant but safe and idempotent. This can be simplified later to a single reset path if desired.

Summary by CodeRabbit

  • Bug Fixes

    • Improved disconnect and logout flow with proper media session cleanup
    • Enhanced VoIP session lifecycle management when switching servers or logging out
    • Better state handling during account deletion and connection teardown
  • Tests

    • Added comprehensive test coverage for media session lifecycle behavior

@diegolmello diegolmello had a problem deploying to experimental_android_build March 24, 2026 15:11 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to official_android_build March 24, 2026 15:11 — with GitHub Actions Failure
@diegolmello diegolmello had a problem deploying to experimental_ios_build March 24, 2026 15:11 — with GitHub Actions Failure
@diegolmello

Copy link
Copy Markdown
Member Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 26504cc4-85c7-4e40-a733-f362777f5798

📥 Commits

Reviewing files that changed from the base of the PR and between 8274083 and 40a4b0d.

📒 Files selected for processing (10)
  • app/lib/methods/logout.ts
  • app/lib/services/connect.ios.test.ts
  • app/lib/services/connect.test.ts
  • app/lib/services/connect.ts
  • app/lib/services/voip/MediaSessionInstance.test.ts
  • app/lib/services/voip/MediaSessionInstance.ts
  • app/lib/services/voip/MediaSessionStore.ts
  • app/sagas/login.js
  • app/sagas/selectServer.ts
  • app/views/NewServerView/hooks/useConnectServer.tsx
📜 Recent review details
🔇 Additional comments (14)
app/lib/services/voip/MediaSessionStore.ts (1)

96-105: LGTM!

The dispose() method correctly tears down all state: ends the session if present, nulls out references (sessionInstance, sendSignalFn, _webrtcProcessorFactory), and emits a change event to notify subscribers. The implementation is idempotent and follows the same pattern as makeInstance().

app/lib/services/voip/MediaSessionInstance.ts (2)

34-55: LGTM!

Calling reset() at the start of init() ensures idempotent reinitialization. The onChange subscription is properly tracked via mediaSessionStoreChangeUnsubscribe and will be cleaned up when reset() is called again or on teardown.


173-193: LGTM!

The reset() method comprehensively cleans up all resources:

  • Unsubscribes from mediaSessionStore.onChange
  • Stops the media signal listener
  • Unsubscribes from Redux store subscriptions
  • Calls mediaSessionStore.dispose() to end the signaling session
  • Nulls out the instance reference
  • Resets the Zustand call store

The method is idempotent due to null checks and can be safely called multiple times.

app/lib/services/voip/MediaSessionInstance.test.ts (1)

81-159: LGTM!

Good test coverage for MediaSessionInstance lifecycle:

  • Verifies stream listener registration and session creation on init()
  • Validates signal routing through sdk.methodCall
  • Tests user switch properly calls endSession() on the previous session
  • Confirms only one onChange handler remains active after re-init
  • Verifies getInstance throws after reset() until init() is called again
  • Tests idempotent reset() (no throw on double-call)
app/lib/services/connect.ts (1)

410-414: LGTM!

Centralizing mediaSessionInstance.reset() in disconnect() ensures all callers (logout, account deletion, server switching) consistently clean up VoIP state. The order is correct: disconnect SDK first, then reset media session.

app/views/NewServerView/hooks/useConnectServer.tsx (1)

4-4: LGTM!

Switching from sdk.disconnect() to the shared disconnect() import ensures VoIP media session cleanup happens when clearing the previous workspace before connecting to a new server.

Also applies to: 26-28

app/sagas/selectServer.ts (1)

154-154: LGTM!

Calling mediaSessionInstance.reset() directly here is appropriate. The saga manages its own connection flow and will call connect() later (which internally calls disconnect()). This ensures VoIP state from the previous workspace is cleared early in the server selection flow, before loading user credentials for the new workspace.

app/lib/services/connect.test.ts (1)

3-6: LGTM!

The mock prevents real VoIP initialization during determineAuthType tests. Mocking only reset and init is sufficient since those are the only methods potentially invoked through the module's imports.

app/lib/services/connect.ios.test.ts (1)

3-6: LGTM!

Consistent mock setup with connect.test.ts, ensuring iOS-specific determineAuthType tests don't trigger real VoIP initialization.

app/lib/methods/logout.ts (2)

9-9: LGTM!

The import aligns with the PR objective to centralize disconnect logic through connect.disconnect(), which now handles VoIP teardown via mediaSessionInstance.reset().


114-116: No changes needed.

The disconnect() function is not asynchronous—it returns an Action type and does not return a Promise. Therefore, await is not appropriate and the current code is correct.

			> Likely an incorrect or invalid review comment.
app/sagas/login.js (3)

35-35: LGTM!

The import aligns with the PR objective to use the centralized disconnect() function that handles VoIP teardown.


228-243: LGTM! Good defensive cleanup for permission loss scenarios.

The new else branch ensures that when VoIP is unavailable or the user lacks required permissions, any previously initialized media session is properly reset. This prevents stale VoIP state from persisting across permission changes or module availability changes.


416-421: No changes needed — disconnect() is synchronous.

The disconnect() function at app/lib/services/connect.ts:410 is synchronous and does not return a Promise. It immediately calls sdk.disconnect(), resets mediaSessionInstance, and returns. Since it completes synchronously, calling it directly without yield is correct and matches the behavior intended for synchronous operations.

			> Likely an incorrect or invalid review comment.

Walkthrough

The changes introduce proper cleanup for VoIP media sessions during workspace switching. A new reset() method is added to MediaSessionInstance that tears down VoIP resources. The disconnect() function now invokes this reset. Multiple integration points are updated to ensure media session cleanup happens at logout, server selection, and account deletion.

Changes

Cohort / File(s) Summary
VoIP Media Session Lifecycle
app/lib/services/voip/MediaSessionInstance.ts, app/lib/services/voip/MediaSessionInstance.test.ts, app/lib/services/voip/MediaSessionStore.ts
Refactored media session cleanup: stop() is replaced with a public reset() method that properly disposes stores and clears references. MediaSessionStore.dispose() added to terminate sessions and nullify resources. Comprehensive test suite added to verify initialization, re-initialization with different users, and reset semantics.
Disconnect Service & Tests
app/lib/services/connect.ts, app/lib/services/connect.test.ts, app/lib/services/connect.ios.test.ts
Enhanced disconnect() to call mediaSessionInstance.reset() after SDK disconnect before returning result. Jest mocks added for MediaSessionInstance in iOS and general test files to isolate unit tests from real media session behavior.
Workspace Transition Integration
app/lib/methods/logout.ts, app/sagas/login.js, app/sagas/selectServer.ts, app/views/NewServerView/hooks/useConnectServer.tsx
Updated all call sites to use extracted disconnect() function instead of sdk.disconnect() method. Added explicit mediaSessionInstance.reset() calls in login saga's VoIP fork and server selection flow to ensure cleanup during workspace transitions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

type: feature, type: bug

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: refactoring media session handling and improving disconnect logic by adding mediaSessionInstance.reset() calls and replacing direct sdk.disconnect() calls with a dedicated disconnect function.
Linked Issues check ✅ Passed The PR comprehensively addresses VMUX-60's requirement to prevent cross-workspace state leakage by implementing deterministic teardown and reinitialization of media sessions across all disconnect paths (logout, delete-account, server switching, permission loss).
Out of Scope Changes check ✅ Passed All changes directly support the core objective of preventing VoIP/media-signaling state leakage across workspaces; no unrelated modifications detected in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@diegolmello diegolmello merged commit 306f8cc into feat.voip-lib-new Mar 24, 2026
6 of 11 checks passed
@diegolmello diegolmello deleted the feat.voip-change-ws branch March 24, 2026 16:29
diegolmello added a commit that referenced this pull request Apr 22, 2026
…/Decline (#7215)

* merge feat.voip-lib

* feat(voip): enhance call handling with UUID mapping and event listeners

* Base call UI

* feat(voip): integrate Zustand for call state management and enhance CallView UI

* feat(voip): add simulateCall function for mock call handling in UI development

* refactor(CallView): update button handlers and improve UI responsiveness

* Add pause-shape-unfilled icon

* Base CallHeader

* toggleFocus

* collapse buttons

* Header components

* Hide header when no call

* Timer

* Add use memo

* Add voice call item on sidebar

* cleanup

* Temp use @rocket.chat/media-signaling from .tgz

* cleanup

* Check module and permissions to enable voip

* Refactor stop method to use optional chaining for media signal listeners

* voip push first test

* Add VoIP call handling with pending call management

- Implemented VoIP push notification handling in index.js, including storing call info for later processing.
- Added CallKeep event handlers for answering and ending calls from a cold start.
- Introduced a new CallIdUUID module to convert call IDs to deterministic UUIDs for compatibility with CallKit.
- Created a pending call store to manage incoming calls when the app is not fully initialized.
- Updated deep linking actions to include VoIP call handling.
- Enhanced MediaSessionInstance to process pending calls and manage call states effectively.

* Remove pending store and create getInitialEvents on app/index

* Attempt to make iOS calls work from cold state

* lint and format

* Patch callkeep ios

* Temp send iOS voip push token on gcm

* Temp fix require cycle

* chore: format code and fix lint issues [skip ci]

* CallIDUUID module on android and voip push

* Add setCallUUID on useCallStore to persist calls accepted on native Android

* remove callkeep from notification

* Android Incoming Call UI POC

* Refactor VoIP handling: Migrate VoIP-related classes to a new package structure, removing deprecated modules and consolidating functionality. Update imports in MainApplication and NotificationIntentHandler to reflect changes. This cleanup enhances code organization and prepares for future VoIP feature enhancements.

* Remove VoipForegroundService

* cleanup and use caller instead of callerName

* Cleanup and make iOS build again

* Refactor VoIP handling: Remove unused event emissions for call answered and declined, switch from SharedPreferences to in-memory storage for pending VoIP call data, and update method signatures for better clarity. This cleanup enhances performance and prepares for future VoIP feature improvements.

* Refactor VoIP handling: Introduce a new VoipPayload class to encapsulate call data, streamline notification processing, and enhance method signatures across the VoIP module. This update improves code clarity and prepares for future feature enhancements.

* Migrate react-native-voip-push-notifications to VoipModule

* Refactor VoIP module: Update package structure by moving VoipTurboPackage to the main package and removing the obsolete NativeVoipSpec class. Adjust imports in MainApplication and VoipModule to reflect these changes, enhancing code organization and maintainability.

* Unify emitters

* Move CallKeep listeners from MediaSessionInstance to getInitialEvents

* Clear callkeep on endcall

* Unify getInitialEvents logic

* getInitialEvents -> MediaCallEvents

* chore: format code and fix lint issues [skip ci]

* feat(Android): Add full screen incoming call (#6977)

* feat: Update call UI (#6990)

* feat: Handle audio routing, e.g., Bluetooth headset vs. internal speaker switching (#6992)

* fix: empty space when not on call (#6993)

* feat: Dialpad (#7000)

* action: organized translations

* feat: start call (#7024)

* chore: format code and fix lint issues

* feat: Pre flight (#7038)

* action: organized translations

* feat: Receive voip push notifications from backend (#7045)

* feat: Refactor media session handling and improve disconnect logic (#7065)

* feat: Control incoming call from native (#7066)

* feat: Voice message blocks (#7057)

* feat: native accept success event (#7068)

* feat(voip): call waiting, busy detection, and videoconf blocking (#7077)

* action: organized translations

* feat(voip): tap-to-hide call controls with animations (#7078)

* feat(voip): navigate to call DM from message button and header (#7082)

* feat(voip): tablet and landscape layout (#7110)

* chore: develop into feat.voip-lib-new (RN 81 + Expo 54 + reanimated 4 + true-sheet + iOS 26) (#7114)

* chore: format code and fix lint issues

* feat(voip): android landscape layout for IncomingCallActivity (#7116)

* Update agents files

* feat(voip): Support a11y (#7106)

* Fix content cutting on iOS on some edge cases

* pods

* Ignore .worktrees on jest

* chore: Merge develop into feat.voip-lib-new (#7129)

* fix(voip): show CallKit UI when call is active in background (#7128)

* chore: Update media-signaling to 0.2.0 (#7153)

* feat(voip): migrate iOS accept/reject from DDP to REST (#7124)

* Fix icons

* feat(voip): migrate Android accept/reject from DDP to REST (#7127)

* test(voip): integration tests for CallView pipeline (#7161)

* feat(voip): display video conf provider as subtitle (#7160)

* fix(voip): CallView button grid and correct landscape/dialpad layouts (#7164)

* fix(voip): prevent stale MMKV cache on Android first-install accept

MMKVKeyManager.initialize ran in MainApplication.onCreate before the JS
engine started and opened the default MMKV file via the Tencent 1.2 JAR
when it was still empty. Tencent caches instances per-ID in a singleton
registry, so that empty-state view was held for the rest of the process.
JS later wrote credentials through react-native-mmkv (MMKV Core 2.0),
which has its own separate registry. When a VoIP push arrived,
Ejson.getMMKV() got the cached empty Tencent instance and reported
"No userId found in MMKV for server". Closing and reopening the app
cleared the cache, which is why only the very first call after install
failed.

Drop the open/verify block — the encryption key is already cached from
SecureKeystore, so no MMKV handle is needed here. The first Tencent
instance is now created inside Ejson.getMMKV() after JS has written,
so it scans the file fresh.

* fix(voip): prevent duplicate ringtone on Android incoming call (#7158)

* fix(voip): set explicit snaps for NewMediaCall bottom sheet (#7165)

* Update app/lib/services/voip/MediaSessionStore.ts

Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>

* fix: make startVoipFork reactive to permissions-changed (#7151)

* fix(android): remove MediaProjectionService from merged manifest (#7190)

* fix(voip): Phone account creation (#7170)

* feat: add Enable Mobile Ringing toggle in user preferences (#7155)

* fix(voip): ship blockers for PushKit, licensing, outbound calls, push tokens (#7167)

* fix(android): Play Store mic discoverability, safer FCM logs, avatar auth via headers (#7171)

* fix(ios): serialize VoipService bridge statics (#7169)

* fix(voip): Android DDP thread safety and VoipPayload bundle parity (#7168)

* chore(voip): dead-code and hygiene sweep (#7174)

* refactor(voip): decouple navigateToCallRoom from Redux and backfill REST/connect tests (#7176)

* test(voip): tighten ringing endCall assertion and add VideoConf VoIP-lock saga coverage (#7177)

* fix(ios): harden VoIP DDP WebSocket client on receive failures and TLS (#7173)

* refactor(voip): MediaCallEvents Redux adapters and resetVoipState (#7178)

* refactor(voip): decouple peer autocomplete from Redux; simplify NewMediaCall (#7175)

* fix(ios): add NS_SWIFT_NAME to Challenge.runChallenge for Swift 6.2 compatibility

Swift 6.2 (Xcode 26.x / macos-26 runner) auto-renames the Objective-C
method runChallenge:didReceiveChallenge:completionHandler: to
run(_:didReceive:completionHandler:) when imported into Swift.

Add NS_SWIFT_NAME to explicitly pin the Swift import name, preventing
the compiler from applying its heuristics. This keeps the existing
Swift call site in DDPClient.swift working without changes.

* fix(ios): cancel old URLSession/webSocketTask before reconnecting in DDPClient.connect (#7197)

* fix(ios): add NSLock to nativeAcceptHandledCallIds and 10s REST timeout to handleNativeAccept (#7198)

* feat(android): create VoipCallService with FOREGROUND_SERVICE_MICROPHONE (#7199)

* fix(android): start VoipCallService on accept, stop on hangup/timeout, install end-call listener (#7200)

* fix(voip): enable DM nav for users with SIP extension (#7203)

* fix(android): handle null VoiceConnection in answerIncomingCall, notify JS (#7201)

* fix(voip): resolve closure capture ordering in handleNativeAccept (#7209)

* fix(android): integrate VoIP modules with SSL-pinned OkHttpClient (#7208)

* fix(push): gate id and voipToken behind server version checks, fix VideoConf caller extra (#7210)

* fix(voip): remove sensitive data from production logs (#7207)

* fix(android): remove isRunning guard + add double-tap guard on Accept/Decline

- VoipCallService: remove if (!isRunning) guard, call startForeground unconditionally
  (idempotent on Android, fixes Android 14+ foreground service requirement)
- IncomingCallActivity: add AtomicBoolean guard on handleAccept/handleDecline
  to prevent double-tap from triggering multiple service starts

---------

Co-authored-by: diegolmello <diegolmello@users.noreply.github.com>
Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant