Implement combined polling and ICE candidate exchange for host

- Add offerId to RTCPeerConnection mapping for answer processing
- Setup ICE candidate handlers on offerer peer connections
- Replace answer-only polling with combined pollOffers() endpoint
- Process both answers and ICE candidates in single poll operation
- Track last poll timestamp to avoid reprocessing old data
- Send offerer ICE candidates to server via addOfferIceCandidates()
- Reduces HTTP requests and completes bidirectional ICE exchange
This commit is contained in:
2025-12-10 19:33:31 +01:00
parent 538315c51f
commit a6329c8708

View File

@@ -210,26 +210,27 @@ export default function App() {
} }
}, [setupStep, myUsername, rondevu, myServicePublished]); }, [setupStep, myUsername, rondevu, myServicePublished]);
// Poll for answered offers (host side) // Combined polling for answers and ICE candidates (host side)
useEffect(() => { useEffect(() => {
if (!myServicePublished || !rondevu || Object.keys(offerIdToPeerConnection).length === 0) { if (!myServicePublished || !rondevu || Object.keys(offerIdToPeerConnection).length === 0) {
return; return;
} }
console.log('[Answer Polling] Starting to poll for answers...'); console.log('[Host Polling] Starting combined polling for answers and ICE candidates...');
const pollForAnswers = async () => { const poll = async () => {
try { try {
const result = await rondevu.getAnsweredOffers(lastAnswerTimestamp); const result = await rondevu.pollOffers(lastAnswerTimestamp);
if (result.offers.length > 0) { // Process answers
console.log(`[Answer Polling] Found ${result.offers.length} new answered offer(s)`); if (result.answers.length > 0) {
console.log(`[Host Polling] Found ${result.answers.length} new answer(s)`);
for (const answer of result.offers) { for (const answer of result.answers) {
const pc = offerIdToPeerConnection[answer.offerId]; const pc = offerIdToPeerConnection[answer.offerId];
if (pc && pc.signalingState !== 'stable') { if (pc && pc.signalingState !== 'stable') {
console.log(`[Answer Polling] Setting remote answer for offer ${answer.offerId}`); console.log(`[Host Polling] Setting remote answer for offer ${answer.offerId}`);
await pc.setRemoteDescription({ await pc.setRemoteDescription({
type: 'answer', type: 'answer',
@@ -239,20 +240,47 @@ export default function App() {
// Update last answer timestamp // Update last answer timestamp
setLastAnswerTimestamp(prev => Math.max(prev, answer.answeredAt)); setLastAnswerTimestamp(prev => Math.max(prev, answer.answeredAt));
console.log(`✅ [Answer Polling] Remote answer set for offer ${answer.offerId}`); console.log(`✅ [Host Polling] Remote answer set for offer ${answer.offerId}`);
} else if (pc) { } else if (pc) {
console.log(`[Answer Polling] Skipping offer ${answer.offerId} - already in stable state`); console.log(`[Host Polling] Skipping offer ${answer.offerId} - already in stable state`);
} }
} }
} }
// Process ICE candidates
let totalIceCandidates = 0;
for (const [offerId, candidates] of Object.entries(result.iceCandidates)) {
const pc = offerIdToPeerConnection[offerId];
if (pc && candidates.length > 0) {
console.log(`[Host Polling] Processing ${candidates.length} ICE candidate(s) for offer ${offerId}`);
for (const item of candidates) {
if (item.candidate) {
try {
await pc.addIceCandidate(new RTCIceCandidate(item.candidate));
totalIceCandidates++;
// Update timestamp
setLastAnswerTimestamp(prev => Math.max(prev, item.createdAt));
} catch (err) {
console.warn(`[Host Polling] Failed to add ICE candidate for offer ${offerId}:`, err);
}
}
}
}
}
if (totalIceCandidates > 0) {
console.log(`✅ [Host Polling] Added ${totalIceCandidates} ICE candidate(s)`);
}
} catch (err) { } catch (err) {
console.error('[Answer Polling] Error polling for answers:', err); console.error('[Host Polling] Error polling:', err);
} }
}; };
// Poll every 2 seconds // Poll every 2 seconds
const interval = setInterval(pollForAnswers, 2000); const interval = setInterval(poll, 2000);
pollForAnswers(); // Initial poll poll(); // Initial poll
return () => clearInterval(interval); return () => clearInterval(interval);
}, [myServicePublished, rondevu, offerIdToPeerConnection, lastAnswerTimestamp]); }, [myServicePublished, rondevu, offerIdToPeerConnection, lastAnswerTimestamp]);
@@ -359,7 +387,7 @@ export default function App() {
const pc = new RTCPeerConnection(getCurrentRtcConfig()); const pc = new RTCPeerConnection(getCurrentRtcConfig());
const dc = pc.createDataChannel('chat'); const dc = pc.createDataChannel('chat');
// Setup handlers // Setup handlers (will be enhanced with offerId later)
setupHostConnection(pc, dc, myUsername); setupHostConnection(pc, dc, myUsername);
// Create offer // Create offer
@@ -381,13 +409,24 @@ export default function App() {
ttl: 300000, // 5 minutes ttl: 300000, // 5 minutes
}); });
// Map offerIds to peer connections // Map offerIds to peer connections and setup ICE handlers
const offerMapping = {}; const offerMapping = {};
const hostConnMap = {}; const hostConnMap = {};
publishResult.offers.forEach((offer, idx) => { publishResult.offers.forEach((offer, idx) => {
const conn = connections[idx]; const conn = connections[idx];
offerMapping[offer.offerId] = conn.pc; offerMapping[offer.offerId] = conn.pc;
hostConnMap[`host-${idx}`] = { pc: conn.pc, dc: conn.dc, offerId: offer.offerId, status: 'waiting' }; hostConnMap[`host-${idx}`] = { pc: conn.pc, dc: conn.dc, offerId: offer.offerId, status: 'waiting' };
// Setup ICE candidate handler for offerer
conn.pc.onicecandidate = (event) => {
if (event.candidate) {
rondevu.addOfferIceCandidates(
fqn,
offer.offerId,
[event.candidate.toJSON()]
).catch(err => console.error(`[Host] Failed to send ICE candidate for offer ${offer.offerId}:`, err));
}
};
}); });
setOfferIdToPeerConnection(offerMapping); setOfferIdToPeerConnection(offerMapping);