Skip to content

Comments

RU-T47 Fixing issue with voice channel#224

Merged
ucswift merged 2 commits intomasterfrom
develop
Feb 20, 2026
Merged

RU-T47 Fixing issue with voice channel#224
ucswift merged 2 commits intomasterfrom
develop

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Feb 20, 2026

Summary by CodeRabbit

  • New Features

    • Added microphone permission checks with prompts and quick link to device settings when needed
    • Managed audio session lifecycle for voice calls, including background audio support on Android
    • Improved connection validation with user-facing alerts for missing configuration
  • Bug Fixes

    • Better error handling and cleanup of audio resources on connection failures
    • Clearer user-facing error messages including room context
  • Tests

    • Added an audio mock to support predictable testing of voice flows

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

Adds a Jest mock module for LiveKit React Native and enhances the LiveKit store connection flow with VoIP config checks, microphone permission prompts, native AudioSession lifecycle management (start/stop), foreground service handling, and user-facing error alerts.

Changes

Cohort / File(s) Summary
Mock Infrastructure
__mocks__/@livekit/react-native.ts
New Jest mock exporting AudioSession (async methods: startAudioSession, stopAudioSession, configureAudio, getAudioOutputs, selectAudioOutput, showAudioRoutePicker, setAppleAudioConfiguration), registerGlobals(), and a default export aggregating them. Methods return resolved promises for predictable tests.
LiveKit Store Updates
src/stores/app/livekit-store.ts
Added pre-connection VoIP address/token validation, microphone permission checks with user alerts and settings prompt, start of native AudioSession before connecting (and stop on failure/disconnect), foreground Android service handling, extended logging with roomName, CallKeep/foreground init on success, and user-facing connection failure alerts.

Sequence Diagram

sequenceDiagram
    actor User
    participant Store as LiveKit Store
    participant Permissions as Permission Handler
    participant AudioSession as AudioSession
    participant LiveKit as LiveKit Connection
    participant FGService as Foreground Service

    User->>Store: Request join voice channel
    Store->>Permissions: Check microphone permission
    Permissions-->>Store: Granted / Denied

    alt Permission denied
        Store->>User: Show permission alert (offer settings)
    else Permission granted
        Store->>Store: Validate VoIP server & token
        alt Config invalid
            Store->>User: Show config error alert
        else Config valid
            Store->>AudioSession: startAudioSession()
            AudioSession-->>Store: started / error

            alt AudioSession started / ok
                Store->>FGService: start foreground service (Android)
                FGService-->>Store: service started / error

                Store->>LiveKit: connectToRoom()
                LiveKit-->>Store: connected / error

                alt Connected
                    Store->>Store: init CallKeep, set audio routing
                else Connection failed
                    Store->>AudioSession: stopAudioSession()
                    Store->>User: Show connection error alert
                end
            else Start failed
                Store->>User: Show audio session start error
            end
        end
    end

    alt On disconnect
        Store->>AudioSession: stopAudioSession()
        Store->>FGService: stop foreground service
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰
In pockets of code I quietly hop,
Mock sessions spring so tests won't stop,
I check the mic, I start and cease,
Foreground hum, then graceful peace,
Hops of audio — ready, connect, stop.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title mentions fixing a voice channel issue but lacks specificity about what aspect of voice channel functionality is being addressed. The changeset involves substantial additions including mock setup for LiveKit audio session, VoIP validation, microphone permissions, audio session management, and foreground service handling—details the generic title does not convey. Consider a more specific title that highlights the main contribution, such as 'Add LiveKit audio session management and VoIP connection validation' or 'Implement microphone permission checks and audio session lifecycle management for voice calls'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/stores/app/livekit-store.ts (1)

647-666: ⚠️ Potential issue | 🟠 Major

Fix type mismatch: disconnectFromRoom is async but typed as () => void, causing un-awaited cleanup with audio session lifecycle.

The interface at line 185 declares disconnectFromRoom: () => void, but the implementation is async. Three call sites do not await: the CallKeep callback (line 523), LiveKitCallModal.handleLeaveRoom (line 56), and livekit-bottom-sheet.handleDisconnect (line 144). With the new AudioSession.stopAudioSession() and callKeepService.endCall() cleanup, un-awaited disconnects create a race condition—if a user ends the call via system UI or modal and reconnects immediately, the old audio session may still be active.

  • Update the interface: disconnectFromRoom: () => Promise<void>;
  • Await the disconnect in LiveKitCallModal (line 56) and livekit-bottom-sheet (line 144)
  • For the CallKeep callback (line 523), ensure the callback can be async or refactor the cleanup logic
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/app/livekit-store.ts` around lines 647 - 666, The
disconnectFromRoom implementation is async but the declared type is () => void,
causing callers (CallKeep callback, LiveKitCallModal.handleLeaveRoom,
livekit-bottom-sheet.handleDisconnect) to not await cleanup and permit audio
session races; change the store interface declaration for disconnectFromRoom to
return Promise<void>, update all call sites to await get().disconnectFromRoom()
in LiveKitCallModal.handleLeaveRoom and livekit-bottom-sheet.handleDisconnect,
and modify the CallKeep callback (or refactor callKeepService.endCall flow) so
it can await disconnectFromRoom (or invokes an async wrapper) ensuring
AudioSession.stopAudioSession() and callKeepService.endCall() complete before
proceeding.
🧹 Nitpick comments (1)
src/stores/app/livekit-store.ts (1)

385-388: Hardcoded user-facing strings should use t() for i18n.

All Alert.alert calls added in this PR (lines 385, 400, 409, 639–643) use hardcoded English strings. The same pattern exists in the older alerts above (e.g., lines 339–348), so this is a pre-existing gap, but new code shouldn't widen it. As per coding guidelines: "Ensure all text is wrapped in t() from react-i18next for translations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/app/livekit-store.ts` around lines 385 - 388, The Alert.alert call
is using hardcoded English strings; replace the title and message and button
labels with i18n keys using t() from react-i18next (e.g.,
t('voiceConnectionError.title'), t('voiceConnectionError.message'),
t('common.cancel'), t('common.openSettings')) and ensure t is available in this
module (import/use the i18n hook or exported t) and pass the translated strings
into Alert.alert and the onPress for Linking.openSettings remains unchanged;
update all similar Alert.alert occurrences (including those at lines referenced)
to use corresponding t() keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/stores/app/livekit-store.ts`:
- Around line 413-416: The code currently calls currentRoom.disconnect()
directly which bypasses the full cleanup flow; replace that direct call with the
store's full disconnect handler by calling get().disconnectFromRoom() (or
otherwise delegating to disconnectFromRoom) so AudioSession.stopAudioSession(),
CallKeep end, foreground service stop, callback teardown, and state reset run;
locate the currentRoom usage in this function and swap the direct disconnect to
invoke disconnectFromRoom() on the store to ensure complete cleanup.

---

Outside diff comments:
In `@src/stores/app/livekit-store.ts`:
- Around line 647-666: The disconnectFromRoom implementation is async but the
declared type is () => void, causing callers (CallKeep callback,
LiveKitCallModal.handleLeaveRoom, livekit-bottom-sheet.handleDisconnect) to not
await cleanup and permit audio session races; change the store interface
declaration for disconnectFromRoom to return Promise<void>, update all call
sites to await get().disconnectFromRoom() in LiveKitCallModal.handleLeaveRoom
and livekit-bottom-sheet.handleDisconnect, and modify the CallKeep callback (or
refactor callKeepService.endCall flow) so it can await disconnectFromRoom (or
invokes an async wrapper) ensuring AudioSession.stopAudioSession() and
callKeepService.endCall() complete before proceeding.

---

Nitpick comments:
In `@src/stores/app/livekit-store.ts`:
- Around line 385-388: The Alert.alert call is using hardcoded English strings;
replace the title and message and button labels with i18n keys using t() from
react-i18next (e.g., t('voiceConnectionError.title'),
t('voiceConnectionError.message'), t('common.cancel'), t('common.openSettings'))
and ensure t is available in this module (import/use the i18n hook or exported
t) and pass the translated strings into Alert.alert and the onPress for
Linking.openSettings remains unchanged; update all similar Alert.alert
occurrences (including those at lines referenced) to use corresponding t() keys.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/stores/app/livekit-store.ts (1)

647-666: ⚠️ Potential issue | 🟠 Major

Update disconnectFromRoom signature from () => void to () => Promise<void> and await all call sites.

The implementation performs multiple awaited async operations (currentRoom.disconnect(), AudioSession.stopAudioSession(), callKeepService.endCall(), notifee.stopForegroundService()), but the interface still declares () => void. This causes three call sites to skip awaiting, creating race conditions:

  • Line 523 (CallKeep callback): get().disconnectFromRoom() – cleanup may not complete before callback returns
  • LiveKitCallModal line 56 (handleLeaveRoom): actions.disconnectFromRoom() followed by onClose() – modal closes before cleanup finishes
  • livekit-bottom-sheet line 144 (handleDisconnect): disconnectFromRoom() followed by setCurrentView() – state changes before cleanup is done

Update the interface to match the implementation and await these three call sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/app/livekit-store.ts` around lines 647 - 666, The
disconnectFromRoom implementation is async but its type and callers still treat
it as synchronous; change the disconnectFromRoom signature from () => void to ()
=> Promise<void> (update the store/interface declaration where
disconnectFromRoom is typed) and ensure all awaited async operations inside
(currentRoom.disconnect(), AudioSession.stopAudioSession(),
callKeepService.endCall(), notifee.stopForegroundService()) remain awaited. Then
update all call sites to await it: the CallKeep callback that calls
get().disconnectFromRoom(), the LiveKitCallModal handleLeaveRoom that calls
actions.disconnectFromRoom() before onClose(), and the livekit-bottom-sheet
handleDisconnect that calls disconnectFromRoom() before setCurrentView(); make
these callers async and prepend await to the disconnectFromRoom calls so cleanup
completes before continuing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/stores/app/livekit-store.ts`:
- Around line 385-411: Replace hardcoded Alert.alert messages with localized
strings by calling t() from react-i18next: wrap each user-facing title and body
string in t() using descriptive translation keys (e.g., 'voice.connectionError',
'voice.serverAddressUnavailable', 'voice.tokenMissing',
'voice.microphonePermissionRequired') and ensure the same keys are added to the
src/translations dictionary files; update the Alert.alert calls in
livekit-store.ts (the microphone permission block, the
voipServerWebsocketSslAddress check, and the token check) to use t('...') for
both title and message instead of raw English text.
- Around line 618-633: The connection error handler is too broad and treats
post-connect failures (e.g., audio routing, sound playback) as a room connection
failure; fix by distinguishing true connect failures from post-connect
side-effect errors: after the LiveKit connect step completes successfully set an
explicit flag (e.g., connectionConfirmed or set isConnected = true) before
running post-connect operations and move the logger.error/connection-failure
alert to only trigger when that flag is not set, and/or wrap each post-connect
call (playConnectToAudioRoomSound -> playSound, AudioSession.stopAudioSession
calls, setupAudioRouting, foreground service, CallKeep) in their own try/catch
so they log warnings without re-throwing or triggering the "Failed to connect to
room" path.

---

Outside diff comments:
In `@src/stores/app/livekit-store.ts`:
- Around line 647-666: The disconnectFromRoom implementation is async but its
type and callers still treat it as synchronous; change the disconnectFromRoom
signature from () => void to () => Promise<void> (update the store/interface
declaration where disconnectFromRoom is typed) and ensure all awaited async
operations inside (currentRoom.disconnect(), AudioSession.stopAudioSession(),
callKeepService.endCall(), notifee.stopForegroundService()) remain awaited. Then
update all call sites to await it: the CallKeep callback that calls
get().disconnectFromRoom(), the LiveKitCallModal handleLeaveRoom that calls
actions.disconnectFromRoom() before onClose(), and the livekit-bottom-sheet
handleDisconnect that calls disconnectFromRoom() before setCurrentView(); make
these callers async and prepend await to the disconnectFromRoom calls so cleanup
completes before continuing.

Comment on lines +385 to +411
Alert.alert('Voice Connection Error', 'Microphone permission is required to join a voice channel. Please grant the permission in your device settings.', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
]);
return;
}

const { currentRoom, voipServerWebsocketSslAddress } = get();

// Disconnect from current room if connected
// Validate connection parameters before attempting to connect
if (!voipServerWebsocketSslAddress) {
logger.error({
message: 'Cannot connect to room - no VoIP server address available',
context: { roomName: roomInfo.Name },
});
Alert.alert('Voice Connection Error', 'Voice server address is not available. Please try again later.');
return;
}

if (!token) {
logger.error({
message: 'Cannot connect to room - no token provided',
context: { roomName: roomInfo.Name },
});
Alert.alert('Voice Connection Error', 'Voice channel token is missing. Please try refreshing the voice channels.');
return;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Localize new user-facing Alert strings.

These new alerts introduce hardcoded English strings; route them through t() with translation keys so they’re localized.

💡 Example adjustment
- Alert.alert('Voice Connection Error', 'Microphone permission is required to join a voice channel. Please grant the permission in your device settings.', [
+ Alert.alert(t('voice.connectionErrorTitle'), t('voice.micPermissionRequired'), [
    { text: t('common.cancel'), style: 'cancel' },
    { text: t('common.openSettings'), onPress: () => Linking.openSettings() },
  ]);

As per coding guidelines, "Ensure all text is wrapped in t() from react-i18next for translations with dictionary files stored in src/translations".

Also applies to: 638-642

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/app/livekit-store.ts` around lines 385 - 411, Replace hardcoded
Alert.alert messages with localized strings by calling t() from react-i18next:
wrap each user-facing title and body string in t() using descriptive translation
keys (e.g., 'voice.connectionError', 'voice.serverAddressUnavailable',
'voice.tokenMissing', 'voice.microphonePermissionRequired') and ensure the same
keys are added to the src/translations dictionary files; update the Alert.alert
calls in livekit-store.ts (the microphone permission block, the
voipServerWebsocketSslAddress check, and the token check) to use t('...') for
both title and message instead of raw English text.

Comment on lines 618 to +633
logger.error({
message: 'Failed to connect to room',
context: { error },
context: { error, roomName: roomInfo?.Name },
});

// Stop audio session on failure since we started it above
if (Platform.OS !== 'web') {
try {
await AudioSession.stopAudioSession();
} catch (stopError) {
logger.warn({
message: 'Failed to stop audio session after connection error',
context: { error: stopError },
});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's see the structure of livekit-store.ts and find the connectToRoom method
fd -a "livekit-store.ts" src/

Repository: Resgrid/Unit

Length of output: 106


🏁 Script executed:

# Get the full connectToRoom method to understand the complete flow
wc -l src/stores/app/livekit-store.ts

Repository: Resgrid/Unit

Length of output: 91


🏁 Script executed:

# Search for the connectToRoom method and its context
rg -n "connectToRoom|async.*connectToRoom" src/stores/app/livekit-store.ts -A 150 | head -200

Repository: Resgrid/Unit

Length of output: 8313


🏁 Script executed:

# Check for any connected flag or state tracking
rg -n "connected|isConnected|connectionStatus" src/stores/app/livekit-store.ts -B 2 -A 2

Repository: Resgrid/Unit

Length of output: 3428


🏁 Script executed:

# Look at the audio service for error handling as indicated in the original request
fd -a "audio.service" src/

Repository: Resgrid/Unit

Length of output: 380


🏁 Script executed:

# Get the complete connectToRoom method from line 372 onwards
sed -n '372,640p' src/stores/app/livekit-store.ts | cat -n

Repository: Resgrid/Unit

Length of output: 12776


🏁 Script executed:

# Check the audio.service.ts for error handling
cat -n src/services/audio.service.ts | head -100

Repository: Resgrid/Unit

Length of output: 4057


🏁 Script executed:

# Get the complete catch block to see all error handling
sed -n '617,680p' src/stores/app/livekit-store.ts | cat -n

Repository: Resgrid/Unit

Length of output: 2477


🏁 Script executed:

# Find and inspect setupAudioRouting function
rg -n "setupAudioRouting" src/stores/app/livekit-store.ts -B 2 -A 20

Repository: Resgrid/Unit

Length of output: 2265


🏁 Script executed:

# Check audio.service.ts for error handling in playConnectToAudioRoomSound
rg -n "playConnectToAudioRoomSound" src/services/audio.service.ts -B 2 -A 15

Repository: Resgrid/Unit

Length of output: 774


🏁 Script executed:

# Find playSound implementation in audio.service.ts
rg -n "playSound" src/services/audio.service.ts -B 2 -A 20 | head -100

Repository: Resgrid/Unit

Length of output: 2295


Consider guarding post-connect side effects from room connection status.

The outer try/catch will treat any later error (audio routing, sound playback) as a connection failure and show a misleading "Connection Failed" alert, even though the room is already connected and set to isConnected: true. While most operations are currently guarded (setupAudioRouting, foreground service, and CallKeep all have inner try/catch blocks), playConnectToAudioRoomSound() delegates to playSound() which silently logs failures without re-throwing. However, future post-connect operations without inner error handling would trigger the false-failure behavior. Consider either:

  • Wrapping each post-connect operation individually to prevent false-failure alerts, or
  • Using an explicit connection confirmation flag to distinguish actual connect failures from post-connect errors
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/app/livekit-store.ts` around lines 618 - 633, The connection error
handler is too broad and treats post-connect failures (e.g., audio routing,
sound playback) as a room connection failure; fix by distinguishing true connect
failures from post-connect side-effect errors: after the LiveKit connect step
completes successfully set an explicit flag (e.g., connectionConfirmed or set
isConnected = true) before running post-connect operations and move the
logger.error/connection-failure alert to only trigger when that flag is not set,
and/or wrap each post-connect call (playConnectToAudioRoomSound -> playSound,
AudioSession.stopAudioSession calls, setupAudioRouting, foreground service,
CallKeep) in their own try/catch so they log warnings without re-throwing or
triggering the "Failed to connect to room" path.

@ucswift
Copy link
Member Author

ucswift commented Feb 20, 2026

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is approved.

@ucswift ucswift merged commit 598bb30 into master Feb 20, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant