feat(livekit): improve native audio/video handling and stability
Implement robust WebRTC polyfills and native audio session management for LiveKit on mobile platforms. This includes configuring iOS audio sessions to prevent issues during calls and implementing a delayed video activation strategy to avoid main thread deadlocks on iOS. - Add WebRTC and environment polyfills in `src/polyfills.ts` - Implement iOS-specific `AudioSession` configuration in `LiveKitService` - Add support for toggling speaker output on iOS - Update `callStore` to handle video enablement after connection settles - Fix `useUnreadCountQuery` to correctly handle async fetching
This commit is contained in:
@@ -81,6 +81,7 @@ let callTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let initCallUnsub: (() => void) | null = null;
|
||||
let unsubInvited: (() => void) | null = null;
|
||||
let liveKitUnsubs: (() => void)[] = [];
|
||||
let pendingVideoEnabled: boolean | null = null;
|
||||
|
||||
// Constants
|
||||
const CALL_LIFETIME_MS = 55000;
|
||||
@@ -100,6 +101,7 @@ function cleanupProcessedCallIds() {
|
||||
|
||||
/**
|
||||
* Fetch LiveKit token from the backend and connect to the room.
|
||||
* Connects audio-only first, then enables video separately.
|
||||
*/
|
||||
async function joinLiveKitRoom(callId: string, videoEnabled: boolean): Promise<void> {
|
||||
const res = await api.get<{ token: string; url: string }>('/calls/token', {
|
||||
@@ -111,12 +113,35 @@ async function joinLiveKitRoom(callId: string, videoEnabled: boolean): Promise<v
|
||||
throw new Error('Invalid LiveKit token response');
|
||||
}
|
||||
|
||||
// Store video preference for enabling after connection
|
||||
pendingVideoEnabled = videoEnabled;
|
||||
|
||||
setupLiveKitEvents(callId);
|
||||
|
||||
await liveKitService.connect(url, token);
|
||||
// Always connect as audio-only first
|
||||
await liveKitService.connect(url, token, videoEnabled);
|
||||
|
||||
// Enable/disable video based on call type
|
||||
await liveKitService.setVideoEnabled(videoEnabled);
|
||||
// Enable microphone
|
||||
await liveKitService.setMuted(false);
|
||||
|
||||
// Video will be enabled by enablePendingVideo() called from connected event
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable video after a delay to avoid iOS main thread deadlock.
|
||||
* Called from the connected event handler.
|
||||
*/
|
||||
function enablePendingVideo(): void {
|
||||
if (!pendingVideoEnabled) return;
|
||||
const enable = pendingVideoEnabled;
|
||||
pendingVideoEnabled = null;
|
||||
|
||||
// Use a longer timeout to let the JS thread fully settle
|
||||
setTimeout(() => {
|
||||
liveKitService.setVideoEnabled(enable).catch((err) => {
|
||||
console.warn('[CallStore] Failed to enable video:', err);
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,10 +164,14 @@ function setupLiveKitEvents(callId: string): void {
|
||||
|
||||
wsService.sendCallReady(callId);
|
||||
|
||||
// Start duration timer immediately
|
||||
if (durationTimer) clearInterval(durationTimer);
|
||||
durationTimer = setInterval(() => {
|
||||
callStore.setState((s) => ({ callDuration: s.callDuration + 1 }));
|
||||
}, 1000);
|
||||
|
||||
// Schedule video enable after settling (avoids iOS deadlock)
|
||||
enablePendingVideo();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -414,7 +443,7 @@ export const callStore = create<CallState>((set, get) => ({
|
||||
processedCallIds.set(msg.call_id, Date.now());
|
||||
|
||||
if (currentCall?.id === msg.call_id) {
|
||||
console.log('[CallStore] Call ended, duration:', msg.duration);
|
||||
console.log('[CallStore] Call ended, reason:', msg.reason, 'ended_by:', msg.ended_by, 'duration:', msg.duration);
|
||||
get().endCall('ended');
|
||||
return;
|
||||
}
|
||||
@@ -523,7 +552,7 @@ export const callStore = create<CallState>((set, get) => ({
|
||||
status: 'calling',
|
||||
duration: 0,
|
||||
isMuted: false,
|
||||
isSpeakerOn: false,
|
||||
isSpeakerOn: callType === 'video',
|
||||
isVideoEnabled: callType === 'video',
|
||||
isPeerVideoEnabled: false,
|
||||
isInitiator: true,
|
||||
@@ -579,7 +608,7 @@ export const callStore = create<CallState>((set, get) => ({
|
||||
status: 'connecting',
|
||||
duration: 0,
|
||||
isMuted: false,
|
||||
isSpeakerOn: false,
|
||||
isSpeakerOn: isVideoCall,
|
||||
isVideoEnabled: isVideoCall,
|
||||
isPeerVideoEnabled: false,
|
||||
isInitiator: false,
|
||||
@@ -654,14 +683,16 @@ export const callStore = create<CallState>((set, get) => ({
|
||||
}));
|
||||
},
|
||||
|
||||
toggleSpeaker: () => {
|
||||
toggleSpeaker: async () => {
|
||||
const { currentCall } = get();
|
||||
if (!currentCall) return;
|
||||
const newSpeakerOn = !currentCall.isSpeakerOn;
|
||||
set((s) => ({
|
||||
currentCall: s.currentCall
|
||||
? { ...s.currentCall, isSpeakerOn: !s.currentCall.isSpeakerOn }
|
||||
? { ...s.currentCall, isSpeakerOn: newSpeakerOn }
|
||||
: null,
|
||||
}));
|
||||
await liveKitService.setSpeakerOn(newSpeakerOn);
|
||||
},
|
||||
|
||||
toggleMinimize: () => {
|
||||
|
||||
Reference in New Issue
Block a user