-
Notifications
You must be signed in to change notification settings - Fork 33
fix: livekit disconnect issue #240
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
Conversation
✅ Deploy Preview for hoppdocs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughThis PR introduces automatic reconnection functionality for LiveKit-based video calls. It adds connection state monitoring to detect disconnections and trigger reconnection attempts, introduces an Changes
Sequence Diagram(s)sequenceDiagram
participant App as App Component
participant Store as Store
participant MediaDevices as MediaDevicesSettings
participant Room as LiveKit Room
participant UI as Call Center UI
App->>Store: setLivekitUrl(url)
Store->>MediaDevices: provides livekitUrl & updateCallTokens
Room-->>MediaDevices: ConnectionStateChanged event (Disconnected)
alt Has callTokens
MediaDevices->>Room: room.connect(livekitUrl, audioToken)
MediaDevices->>Store: updateCallTokens({isReconnecting: true})
Store->>UI: re-render (display reconnecting banner)
alt Reconnection Succeeds
Room-->>MediaDevices: ConnectionStateChanged event (Connected)
MediaDevices->>Store: updateCallTokens({isReconnecting: false})
Store->>UI: re-render (hide reconnecting banner)
else Reconnection Fails
MediaDevices->>Store: updateCallTokens({isReconnecting: false})
Store->>UI: re-render
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
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.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@tauri/src/components/ui/call-center.tsx`:
- Around line 628-659: The reconnection handler (handleConnectionStateChange)
captures stale callTokens/livekitUrl and retries immediately; change it to read
the latest values at reconnection time (e.g., store callTokens and livekitUrl in
refs like callTokensRef/livekitUrlRef or call a getter to retrieve current
state) and guard before attempting reconnect by re-checking room.state and
callTokensRef.current; implement a retry loop with exponential backoff and a max
attempts counter when calling room.connect, and before each attempt verify token
validity (call an existing token refresh function or add refreshCallTokens() to
obtain a fresh callTokens.audioToken) so room.connect uses a non-expired token;
keep updateCallTokens({ isReconnecting }) usage to reflect state transitions.
🧹 Nitpick comments (1)
tauri/src/windows/main-window/app.tsx (1)
392-394: Consider guarding against missinglivekitUrlbefore renderingLiveKitRoom.If
callTokensis set beforelivekitUrlis populated (e.g., during initial load or after a store sync),LiveKitRoomwill receive an empty string forserverUrl, which may cause connection failures or errors.Consider either:
- Adding
livekitUrlto theconditioncheck inConditionalWrap- Showing a loading state while
livekitUrlis not yet availableProposed fix
<ConditionalWrap - condition={!!callTokens} + condition={!!callTokens && !!livekitUrl} wrap={(children) => ( <LiveKitRoom key={callTokens?.audioToken} token={callTokens?.audioToken} serverUrl={livekitUrl || ""}>
| // Listen to connection state changes and handle reconnection | ||
| useEffect(() => { | ||
| const handleConnectionStateChange = async (state: ConnectionState) => { | ||
| console.log("Connection state changed:", state); | ||
|
|
||
| if (state === ConnectionState.Disconnected && callTokens) { | ||
| // Room disconnected but we still have callTokens - try to reconnect | ||
| console.log("Room disconnected, attempting to reconnect..."); | ||
| updateCallTokens({ isReconnecting: true }); | ||
|
|
||
| try { | ||
| if (!livekitUrl) { | ||
| throw new Error("LiveKit URL not available"); | ||
| } | ||
| await room.connect(livekitUrl, callTokens.audioToken); | ||
| } catch (error) { | ||
| console.error("Reconnection failed:", error); | ||
| updateCallTokens({ isReconnecting: false }); | ||
| } | ||
| } else if (state === ConnectionState.Connected && callTokens?.isReconnecting) { | ||
| // Successfully reconnected | ||
| console.log("Successfully reconnected!"); | ||
| updateCallTokens({ isReconnecting: false }); | ||
| } | ||
| }; | ||
|
|
||
| room.on(RoomEvent.ConnectionStateChanged, handleConnectionStateChange); | ||
|
|
||
| return () => { | ||
| room.off(RoomEvent.ConnectionStateChanged, handleConnectionStateChange); | ||
| }; | ||
| }, [room, callTokens, updateCallTokens, livekitUrl]); |
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.
Reconnection logic has potential issues with stale closures and lacks retry backoff.
Several concerns with the current implementation:
-
Stale closure: The async handler captures
callTokensandlivekitUrlfrom the closure. If state changes during the asyncroom.connect()call, the values used may be stale. For example, if the user ends the call while reconnecting,callTokensin the closure would still be truthy. -
No retry delay/backoff: Immediate reconnection without delay could cause rapid retry loops if the server is temporarily unavailable.
-
Token expiration: After a network issue,
callTokens.audioTokenmight have expired, causing auth failures without any token refresh mechanism.
Proposed fix with stale closure guard and retry delay
useEffect(() => {
const handleConnectionStateChange = async (state: ConnectionState) => {
console.log("Connection state changed:", state);
- if (state === ConnectionState.Disconnected && callTokens) {
+ // Capture current state for the async operation
+ const currentCallTokens = useStore.getState().callTokens;
+ const currentLivekitUrl = useStore.getState().livekitUrl;
+
+ if (state === ConnectionState.Disconnected && currentCallTokens) {
// Room disconnected but we still have callTokens - try to reconnect
console.log("Room disconnected, attempting to reconnect...");
updateCallTokens({ isReconnecting: true });
try {
- if (!livekitUrl) {
+ if (!currentLivekitUrl) {
throw new Error("LiveKit URL not available");
}
- await room.connect(livekitUrl, callTokens.audioToken);
+ // Add a small delay before reconnecting
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ // Re-check that we still want to reconnect
+ if (!useStore.getState().callTokens) {
+ console.log("Call ended during reconnection delay, aborting");
+ return;
+ }
+ await room.connect(currentLivekitUrl, currentCallTokens.audioToken);
} catch (error) {
console.error("Reconnection failed:", error);
updateCallTokens({ isReconnecting: false });
}
- } else if (state === ConnectionState.Connected && callTokens?.isReconnecting) {
+ } else if (state === ConnectionState.Connected && useStore.getState().callTokens?.isReconnecting) {
// Successfully reconnected
console.log("Successfully reconnected!");
updateCallTokens({ isReconnecting: false });
}
};
room.on(RoomEvent.ConnectionStateChanged, handleConnectionStateChange);
return () => {
room.off(RoomEvent.ConnectionStateChanged, handleConnectionStateChange);
};
- }, [room, callTokens, updateCallTokens, livekitUrl]);
+ }, [room, updateCallTokens]);🤖 Prompt for AI Agents
In `@tauri/src/components/ui/call-center.tsx` around lines 628 - 659, The
reconnection handler (handleConnectionStateChange) captures stale
callTokens/livekitUrl and retries immediately; change it to read the latest
values at reconnection time (e.g., store callTokens and livekitUrl in refs like
callTokensRef/livekitUrlRef or call a getter to retrieve current state) and
guard before attempting reconnect by re-checking room.state and
callTokensRef.current; implement a retry loop with exponential backoff and a max
attempts counter when calling room.connect, and before each attempt verify token
validity (call an existing token refresh function or add refreshCallTokens() to
obtain a fresh callTokens.audioToken) so room.connect uses a non-expired token;
keep updateCallTokens({ isReconnecting }) usage to reflect state transitions.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.