Skip to content

Commit cbad05c

Browse files
committed
Fix hydration errors and WebRTC connection issues
- Fix React hydration error in debug logs page with ClientOnly wrapper - Add robust WebRTC connection handling with retry logic and timeouts - Enhanced error recovery for both multi-room and single-room connections - Added comprehensive audio debugging logs for troubleshooting - Improved connection state monitoring and error handling This should resolve the React error livekit-examples#418 and WebRTC peer connection issues preventing proper talkgroup functionality.
1 parent 1ed7f16 commit cbad05c

File tree

4 files changed

+393
-30
lines changed

4 files changed

+393
-30
lines changed

app/custom/VideoConferenceClientImpl.tsx

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,75 @@ export function VideoConferenceClientImpl(props: {
6868

6969
useEffect(() => {
7070
if (e2eeSetupComplete) {
71-
room.connect(props.liveKitUrl, props.token, connectOptions).catch((error) => {
72-
console.error(error);
73-
});
74-
room.localParticipant.enableCameraAndMicrophone().catch((error) => {
75-
console.error(error);
76-
});
71+
console.log('🔗 Attempting to connect to LiveKit room...');
72+
73+
// Add connection timeout
74+
const connectWithTimeout = Promise.race([
75+
room.connect(props.liveKitUrl, props.token, connectOptions),
76+
new Promise((_, reject) => {
77+
setTimeout(() => reject(new Error('Connection timeout after 15 seconds')), 15000);
78+
})
79+
]);
80+
81+
connectWithTimeout
82+
.then(() => {
83+
console.log('✅ Successfully connected to LiveKit room');
84+
85+
// Try to enable camera and microphone, but don't fail if it doesn't work
86+
room.localParticipant.enableCameraAndMicrophone().catch((error) => {
87+
console.warn('⚠️ Could not enable camera/microphone:', error);
88+
// Try just microphone
89+
return room.localParticipant.setMicrophoneEnabled(true).catch((micError) => {
90+
console.warn('⚠️ Could not enable microphone only:', micError);
91+
});
92+
});
93+
})
94+
.catch((error) => {
95+
console.error('❌ Failed to connect to LiveKit room:', error);
96+
97+
// Try reconnecting after a delay
98+
setTimeout(() => {
99+
console.log('🔄 Retrying connection...');
100+
room.connect(props.liveKitUrl, props.token, connectOptions).catch((retryError) => {
101+
console.error('❌ Retry connection failed:', retryError);
102+
});
103+
}, 3000);
104+
});
77105
}
78106
}, [room, props.liveKitUrl, props.token, connectOptions, e2eeSetupComplete]);
79107

80108
useLowCPUOptimizer(room);
81109

110+
useEffect(() => {
111+
if (!room) return;
112+
113+
console.log('🔊 Listening to audio track state changes');
114+
115+
const handleTrackSubscribed = (track: any) => {
116+
if (track.kind === 'audio') {
117+
console.log(`🎵 Audio track subscribed: id=${track.sid}, enabled=${track.isEnabled}`);
118+
track.on('enabled', () => console.log(`🔊 Audio track enabled: id=${track.sid}`));
119+
track.on('disabled', () => console.log(`🔇 Audio track disabled: id=${track.sid}`));
120+
track.on('started', () => console.log(`▶️ Audio track started: id=${track.sid}`));
121+
track.on('stopped', () => console.log(`⏹️ Audio track stopped: id=${track.sid}`));
122+
}
123+
};
124+
125+
const handleTrackUnsubscribed = (track: any) => {
126+
if (track.kind === 'audio') {
127+
console.log(`🔇 Audio track unsubscribed: id=${track.sid}`);
128+
}
129+
};
130+
131+
room.on('trackSubscribed', handleTrackSubscribed);
132+
room.on('trackUnsubscribed', handleTrackUnsubscribed);
133+
134+
return () => {
135+
room.off('trackSubscribed', handleTrackSubscribed);
136+
room.off('trackUnsubscribed', handleTrackUnsubscribed);
137+
};
138+
}, [room]);
139+
82140
return (
83141
<div className="lk-room-container" style={{ position: 'relative' }}>
84142
<RoomContext.Provider value={room}>

app/debug/logs/page.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
'use client';
22

33
import React, { useState, useEffect, useRef } from 'react';
4+
import dynamic from 'next/dynamic';
5+
6+
// Client-only component wrapper to prevent hydration issues
7+
const ClientOnly = ({ children }: { children: React.ReactNode }) => {
8+
const [isMounted, setIsMounted] = useState(false);
9+
10+
useEffect(() => {
11+
setIsMounted(true);
12+
}, []);
13+
14+
if (!isMounted) {
15+
return null;
16+
}
17+
18+
return <>{children}</>;
19+
};
420

521
interface LogEntry {
622
timestamp: string;
@@ -144,8 +160,9 @@ export default function LogsPage() {
144160
const levels = Array.from(new Set(logs.map(log => log.level)));
145161

146162
return (
147-
<div className="min-h-screen bg-gray-900 text-white p-6">
148-
<div className="max-w-7xl mx-auto">
163+
<ClientOnly>
164+
<div className="min-h-screen bg-gray-900 text-white p-6">
165+
<div className="max-w-7xl mx-auto">
149166
<div className="mb-6">
150167
<div className="flex items-center justify-between mb-4">
151168
<div>
@@ -290,7 +307,8 @@ export default function LogsPage() {
290307
</div>
291308
)}
292309
</div>
310+
</div>
293311
</div>
294-
</div>
312+
</ClientOnly>
295313
);
296314
}

lib/MultiRoomLiveKitClient.ts

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -60,29 +60,75 @@ export class MultiRoomLiveKitClient {
6060
}
6161

6262
/**
63-
* Connect to individual talkgroup room
63+
* Connect to individual talkgroup room with retry logic
6464
*/
6565
private async connectToRoom(roomInfo: TalkgroupRoom, token: string, serverUrl: string): Promise<void> {
66-
const room = new Room({
67-
adaptiveStream: true,
68-
dynacast: true,
69-
audioCaptureDefaults: {
70-
echoCancellation: true,
71-
noiseSuppression: true,
72-
autoGainControl: true,
73-
},
74-
});
75-
76-
// Set up room event handlers
77-
this.setupRoomEventHandlers(room, roomInfo);
78-
79-
// Connect to room
80-
await room.connect(serverUrl, token);
81-
82-
// Store room reference
83-
this.rooms.set(roomInfo.roomName, room);
66+
const maxRetries = 3;
67+
let retryCount = 0;
8468

85-
console.log(`📞 Connected to room: ${roomInfo.talkgroupName} (${roomInfo.type})`);
69+
while (retryCount < maxRetries) {
70+
try {
71+
console.log(`🔗 Attempting to connect to ${roomInfo.talkgroupName} (attempt ${retryCount + 1}/${maxRetries})`);
72+
73+
const room = new Room({
74+
adaptiveStream: true,
75+
dynacast: true,
76+
audioCaptureDefaults: {
77+
echoCancellation: true,
78+
noiseSuppression: true,
79+
autoGainControl: true,
80+
},
81+
// Add connection timeout and retry options
82+
reconnectPolicy: {
83+
nextRetryDelayInMs: (context) => {
84+
const delay = Math.min(1000 * Math.pow(2, context.retryCount), 10000);
85+
console.log(`⏱️ Next retry for ${roomInfo.talkgroupName} in ${delay}ms`);
86+
return delay;
87+
},
88+
maxReconnectAttempts: 5,
89+
},
90+
});
91+
92+
// Set up room event handlers first
93+
this.setupRoomEventHandlers(room, roomInfo);
94+
95+
// Add pre-connection error handling
96+
const connectPromise = room.connect(serverUrl, token);
97+
98+
// Set a reasonable timeout for connection
99+
const timeoutPromise = new Promise<void>((_, reject) => {
100+
setTimeout(() => {
101+
reject(new Error(`Connection timeout after 15 seconds for ${roomInfo.talkgroupName}`));
102+
}, 15000);
103+
});
104+
105+
await Promise.race([connectPromise, timeoutPromise]);
106+
107+
// Verify connection state
108+
if (room.state !== 'connected') {
109+
throw new Error(`Room connection failed - state: ${room.state}`);
110+
}
111+
112+
// Store room reference
113+
this.rooms.set(roomInfo.roomName, room);
114+
115+
console.log(`✅ Successfully connected to room: ${roomInfo.talkgroupName} (${roomInfo.type})`);
116+
return; // Success, exit retry loop
117+
118+
} catch (error) {
119+
console.error(`❌ Connection attempt ${retryCount + 1} failed for ${roomInfo.talkgroupName}:`, error);
120+
retryCount++;
121+
122+
if (retryCount < maxRetries) {
123+
const delay = Math.min(1000 * Math.pow(2, retryCount), 5000);
124+
console.log(`⏳ Retrying connection to ${roomInfo.talkgroupName} in ${delay}ms...`);
125+
await new Promise(resolve => setTimeout(resolve, delay));
126+
} else {
127+
console.error(`💥 Failed to connect to ${roomInfo.talkgroupName} after ${maxRetries} attempts`);
128+
throw error;
129+
}
130+
}
131+
}
86132
}
87133

88134
/**
@@ -183,25 +229,57 @@ export class MultiRoomLiveKitClient {
183229
*/
184230
private handleIncomingAudioTrack(track: AudioTrack, roomInfo: TalkgroupRoom, participant: RemoteParticipant): void {
185231
console.log(`🔊 Audio track subscribed from ${roomInfo.talkgroupName}:`, participant.identity);
232+
console.log('🎵 Track details:', {
233+
kind: track.kind,
234+
enabled: track.enabled,
235+
muted: track.muted,
236+
mediaStreamTrack: track.mediaStreamTrack?.id,
237+
readyState: track.mediaStreamTrack?.readyState
238+
});
186239

187240
// Get the audio element
188241
const audioElement = track.attach() as HTMLAudioElement;
189242
audioElement.autoplay = true;
190-
// playsInline is for video elements, not needed for audio
243+
audioElement.volume = 1.0; // Ensure full volume
244+
audioElement.muted = false; // Ensure not muted
245+
246+
console.log('🎧 Audio element created:', {
247+
volume: audioElement.volume,
248+
muted: audioElement.muted,
249+
autoplay: audioElement.autoplay,
250+
srcObject: !!audioElement.srcObject
251+
});
252+
253+
// Add event listeners for debugging
254+
audioElement.addEventListener('loadstart', () => console.log(`🎵 Audio loading started for ${participant.identity}`));
255+
audioElement.addEventListener('canplay', () => console.log(`🎵 Audio can play for ${participant.identity}`));
256+
audioElement.addEventListener('playing', () => console.log(`🎵 Audio playing for ${participant.identity}`));
257+
audioElement.addEventListener('error', (e) => console.error(`❌ Audio error for ${participant.identity}:`, e));
258+
audioElement.addEventListener('stalled', () => console.warn(`⚠️ Audio stalled for ${participant.identity}`));
191259

192260
// Connect to ducking engine
193261
if (this.duckingEngine) {
262+
console.log(`🔀 Connecting audio to ducking engine for ${roomInfo.talkgroupName}`);
194263
this.duckingEngine.connectRoomAudio(roomInfo.roomName, audioElement);
195264
}
196265

197266
// Add to DOM (hidden)
198267
audioElement.style.display = 'none';
268+
audioElement.setAttribute('data-room', roomInfo.roomName);
269+
audioElement.setAttribute('data-participant', participant.identity);
199270
document.body.appendChild(audioElement);
200271

272+
console.log(`🎯 Audio element added to DOM for ${roomInfo.talkgroupName}/${participant.identity}`);
273+
201274
// Clean up when track ends
202275
track.on('ended', () => {
276+
console.log(`🔚 Audio track ended for ${participant.identity}`);
203277
audioElement.remove();
204278
});
279+
280+
// Log audio elements count
281+
const totalAudioElements = document.querySelectorAll('audio').length;
282+
console.log(`📊 Total audio elements in DOM: ${totalAudioElements}`);
205283
}
206284

207285
/**

0 commit comments

Comments
 (0)