-
Notifications
You must be signed in to change notification settings - Fork 7
RU-T50 Voice fix for ios #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,6 +118,7 @@ jest.mock('expo-audio', () => ({ | |
| jest.mock('../../../lib/logging', () => ({ | ||
| logger: { | ||
| info: jest.fn(), | ||
| warn: jest.fn(), | ||
| error: jest.fn(), | ||
| debug: jest.fn(), | ||
| }, | ||
|
|
@@ -259,58 +260,41 @@ describe('LiveKit Store - Permission Management', () => { | |
| (Platform as any).OS = 'ios'; | ||
| }); | ||
|
|
||
| it('should successfully request permissions on iOS', async () => { | ||
| // Mock initial permission check - not granted | ||
| it('should return true on iOS without requesting when permission already granted', async () => { | ||
| mockGetRecordingPermissionsAsync.mockResolvedValueOnce({ | ||
| granted: false, | ||
| canAskAgain: true, | ||
| expires: 'never', | ||
| status: 'undetermined', | ||
| } as any); | ||
|
|
||
| // Mock permission request - granted | ||
| mockRequestRecordingPermissionsAsync.mockResolvedValueOnce({ | ||
| granted: true, | ||
| canAskAgain: true, | ||
| expires: 'never', | ||
| status: 'granted', | ||
| } as any); | ||
|
Comment on lines
264
to
269
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
rg -n -g '!node_modules/**' '"typescript"|typescript' package.json tsconfig*.json 2>/dev/null || trueRepository: Resgrid/Unit Length of output: 388 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'livekit-store.test.ts' . || true
echo "== relevant lines =="
if [ -f src/stores/app/__tests__/livekit-store.test.ts ]; then
nl -ba src/stores/app/__tests__/livekit-store.test.ts | sed -n '240,300p'
fi
echo "== imports around file =="
if [ -f src/stores/app/__tests__/livekit-store.test.ts ]; then
nl -ba src/stores/app/__tests__/livekit-store.test.ts | sed -n '1,80p'
fi
echo "== usages of getRecordingPermissionsAsync in tests/source =="
rg -n "getRecordingPermissionsAsync|mockGetRecordingPermissionsAsync|MockedFn|As any|as any" src -g '!node_modules/**' || trueRepository: Resgrid/Unit Length of output: 295 🏁 Script executed: #!/bin/bash
set -euo pipefail
file="src/stores/app/__tests__/livekit-store.test.ts"
echo "== imports header =="
awk 'NR>=1 && NR<=90 {printf "%6d %s\n", NR, $0}' "$file"
echo "== relevant lines =="
awk 'NR>=240 && NR<=300 {printf "%6d %s\n", NR, $0}' "$file"
echo "== imports/permissions usages =="
rg -n "getRecordingPermissionsAsync|mockGetRecordingPermissionsAsync|mockGetCameraPermissionsAsync|as any" "$file" || trueRepository: Resgrid/Unit Length of output: 7024 Replace the unsafe permission-response casts.
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| const { requestPermissions } = useLiveKitStore.getState(); | ||
| await requestPermissions(); | ||
| const result = await requestPermissions(); | ||
|
|
||
| expect(result).toBe(true); | ||
| expect(mockGetRecordingPermissionsAsync).toHaveBeenCalledTimes(1); | ||
| expect(mockRequestRecordingPermissionsAsync).toHaveBeenCalledTimes(1); | ||
| expect(mockLogger.info).toHaveBeenCalledWith({ | ||
| message: 'Microphone permission granted successfully', | ||
| context: { platform: 'ios' }, | ||
| }); | ||
| expect(mockRequestRecordingPermissionsAsync).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should handle iOS permission denial', async () => { | ||
| // Mock initial permission check - not granted | ||
| it('should never call session-activating request on iOS, even when not granted', async () => { | ||
| // expo-audio's requestRecordingPermissionsAsync activates AVAudioSession | ||
| // and deadlocks against expo-av — the store must only perform the | ||
| // non-activating check and let WebRTC prompt natively on publish. | ||
| mockGetRecordingPermissionsAsync.mockResolvedValueOnce({ | ||
| granted: false, | ||
| canAskAgain: false, | ||
| expires: 'never', | ||
| status: 'denied', | ||
| } as any); | ||
|
|
||
| // Mock permission request - still denied | ||
| mockRequestRecordingPermissionsAsync.mockResolvedValueOnce({ | ||
| granted: false, | ||
| canAskAgain: false, | ||
| expires: 'never', | ||
| status: 'denied', | ||
| } as any); | ||
|
|
||
| const { requestPermissions } = useLiveKitStore.getState(); | ||
| await requestPermissions(); | ||
| const result = await requestPermissions(); | ||
|
|
||
| expect(result).toBe(false); | ||
| expect(mockGetRecordingPermissionsAsync).toHaveBeenCalledTimes(1); | ||
| expect(mockRequestRecordingPermissionsAsync).toHaveBeenCalledTimes(1); | ||
| expect(mockLogger.error).toHaveBeenCalledWith({ | ||
| message: 'Microphone permission not granted', | ||
| expect(mockRequestRecordingPermissionsAsync).not.toHaveBeenCalled(); | ||
| expect(mockLogger.warn).toHaveBeenCalledWith({ | ||
| message: 'Microphone permission permanently denied', | ||
| context: { platform: 'ios' }, | ||
| }); | ||
| }); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,7 @@ | ||
| import { AudioSession } from '@livekit/react-native'; | ||
| import { RTCAudioSession } from '@livekit/react-native-webrtc'; | ||
| import notifee, { AndroidForegroundServiceType, AndroidImportance } from '@notifee/react-native'; | ||
| import { getRecordingPermissionsAsync, requestRecordingPermissionsAsync } from 'expo-audio'; | ||
| import { getRecordingPermissionsAsync } from 'expo-audio'; | ||
| import { Audio, InterruptionModeIOS } from 'expo-av'; | ||
| import * as Device from 'expo-device'; | ||
| import { Room, RoomEvent } from 'livekit-client'; | ||
|
|
@@ -397,23 +397,27 @@ export const useLiveKitStore = create<LiveKitState>((set, get) => ({ | |
| }); | ||
| return true; | ||
| } else if (Platform.OS === 'ios') { | ||
| // NOTE: expo-audio's requestRecordingPermissionsAsync activates | ||
| // AVAudioSession in record mode, which deadlocks against expo-av's | ||
| // active session and freezes the app. Only perform the | ||
| // non-activating status check here; WebRTC triggers the native mic | ||
| // prompt itself when a track is published. | ||
| const micPermission = await getRecordingPermissionsAsync(); | ||
|
|
||
| if (!micPermission.granted) { | ||
| const result = await requestRecordingPermissionsAsync(); | ||
| if (!result.granted) { | ||
| logger.error({ | ||
| message: 'Microphone permission not granted', | ||
| if (!micPermission.canAskAgain) { | ||
| logger.warn({ | ||
| message: 'Microphone permission permanently denied', | ||
| context: { platform: Platform.OS }, | ||
| }); | ||
| return false; | ||
| } | ||
|
Comment on lines
+408
to
414
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicated permission-permanently-denied check-and-log blocks at lines 408–414 and 974–980 in Kody rule violation: Extract duplicated logic into functions Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| logger.info({ | ||
| message: 'Microphone permission not yet granted - WebRTC will prompt on publish', | ||
| context: { platform: Platform.OS }, | ||
| }); | ||
| } | ||
|
|
||
| logger.info({ | ||
| message: 'Microphone permission granted successfully', | ||
| context: { platform: Platform.OS }, | ||
| }); | ||
| return true; | ||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
407
to
421
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Silent WebRTC publish failure occurs because const micPermission = await getRecordingPermissionsAsync();
if (!micPermission.granted) {
if (micPermission.canAskAgain) {
// Undetermined — WebRTC will trigger the native prompt on publish
logger.info({
message: 'Microphone permission not yet granted - WebRTC will prompt on publish',
context: { platform: Platform.OS },
});
return true;
}
// Permanently denied — iOS will not re-prompt; direct user to Settings
logger.error({
message: 'Microphone permission denied',
context: { platform: Platform.OS },
});
return false;
}
return true;Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| } | ||
| return true; // Web/other platforms don't need runtime permissions | ||
|
|
@@ -963,20 +967,21 @@ export const useLiveKitStore = create<LiveKitState>((set, get) => ({ | |
| } | ||
| return true; | ||
| } else if (Platform.OS === 'ios') { | ||
| // Non-activating check only — see requestPermissions for why | ||
| // requestRecordingPermissionsAsync must not be called on iOS. | ||
| const mic = await getRecordingPermissionsAsync(); | ||
| if (mic.granted) return true; | ||
|
|
||
| logger.info({ | ||
| message: 'Requesting microphone permission before opening voice UI', | ||
| context: { platform: Platform.OS }, | ||
| }); | ||
| const result = await requestRecordingPermissionsAsync(); | ||
| if (!result.granted) { | ||
| logger.warn({ | ||
| message: 'Microphone permission denied - voice UI will still open but joining will fail', | ||
| if (!mic.granted) { | ||
| if (!mic.canAskAgain) { | ||
| logger.warn({ | ||
| message: 'Microphone permission permanently denied', | ||
| context: { platform: Platform.OS }, | ||
| }); | ||
| return false; | ||
| } | ||
| logger.info({ | ||
| message: 'Microphone permission not yet granted - WebRTC will prompt on publish', | ||
| context: { platform: Platform.OS }, | ||
| }); | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 187
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 903
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 8225
🏁 Script executed:
Repository: Resgrid/Unit
Length of output: 7251
Use a portable word-boundary replacement.
This script runs on both Ubuntu and macOS runners, and the macOS runner uses BSD
sed, where\bis not guaranteed to match “PR” at string boundaries. Replace the GNU-specificsed -E 's/\bPR\b/Release/g'with a portable boundary expression before publishing release notes.Proposed portable replacement
📝 Committable suggestion
🤖 Prompt for AI Agents