From febe3b727086a3ecaa9bc980fd656c81c66950ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Dec 2025 09:56:27 +0000 Subject: [PATCH 1/2] Fix early ICE candidates lost due to late handler setup in createOffer() Queue ICE candidates that are generated before we have the offerId from the server. When the factory calls setLocalDescription(), ICE gathering starts immediately, but we couldn't send candidates until we had the offerId from publishService(). Now we: 1. Set up a queuing handler immediately after getting the pc from factory 2. Buffer any early candidates while publishing to get the offerId 3. Flush all queued candidates once we have the offerId 4. Continue handling future candidates normally Fixes #2 --- src/rondevu.ts | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/src/rondevu.ts b/src/rondevu.ts index 9c0da05..fecd208 100644 --- a/src/rondevu.ts +++ b/src/rondevu.ts @@ -418,11 +418,42 @@ export class Rondevu { this.debug('Creating new offer...') // Create the offer using the factory + // Note: The factory may call setLocalDescription() which triggers ICE gathering const { pc, dc, offer } = await this.offerFactory(rtcConfig) // Auto-append username to service const serviceFqn = `${this.currentService}@${this.username}` + // Queue to buffer ICE candidates generated before we have the offerId + // This fixes the race condition where ICE candidates are lost because + // they're generated before we can set up the handler with the offerId + const earlyIceCandidates: RTCIceCandidateInit[] = [] + let offerId: string | null = null + + // Set up a queuing ICE candidate handler immediately after getting the pc + // This captures any candidates that fire before we have the offerId + pc.onicecandidate = async (event) => { + if (event.candidate) { + // Handle both browser and Node.js (wrtc) environments + const candidateData = typeof event.candidate.toJSON === 'function' + ? event.candidate.toJSON() + : event.candidate + + if (offerId) { + // We have the offerId, send directly + try { + await this.api.addOfferIceCandidates(serviceFqn, offerId, [candidateData]) + } catch (err) { + console.error('[Rondevu] Failed to send ICE candidate:', err) + } + } else { + // Queue for later - we don't have the offerId yet + this.debug('Queuing early ICE candidate') + earlyIceCandidates.push(candidateData) + } + } + } + // Publish to server const result = await this.api.publishService({ serviceFqn, @@ -432,7 +463,7 @@ export class Rondevu { message: '', }) - const offerId = result.offers[0].offerId + offerId = result.offers[0].offerId // Store active offer this.activeOffers.set(offerId, { @@ -446,15 +477,22 @@ export class Rondevu { this.debug(`Offer created: ${offerId}`) - // Set up ICE candidate handler - this.setupIceCandidateHandler(pc, serviceFqn, offerId) + // Send any queued early ICE candidates + if (earlyIceCandidates.length > 0) { + this.debug(`Sending ${earlyIceCandidates.length} early ICE candidates`) + try { + await this.api.addOfferIceCandidates(serviceFqn, offerId, earlyIceCandidates) + } catch (err) { + console.error('[Rondevu] Failed to send early ICE candidates:', err) + } + } // Monitor connection state pc.onconnectionstatechange = () => { this.debug(`Offer ${offerId} connection state: ${pc.connectionState}`) if (pc.connectionState === 'failed' || pc.connectionState === 'closed') { - this.activeOffers.delete(offerId) + this.activeOffers.delete(offerId!) this.fillOffers() // Try to replace failed offer } } From 62a6cdcb996e6c1eb90b5b2ad36f8d619714053c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Dec 2025 09:57:07 +0000 Subject: [PATCH 2/2] Update package-lock.json --- package-lock.json | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a325f9..a27a661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "0.18.0", "license": "MIT", "dependencies": { - "@noble/ed25519": "^3.0.0", - "@xtr-dev/rondevu-client": "^0.9.2" + "@noble/ed25519": "^3.0.0" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -1310,15 +1309,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@xtr-dev/rondevu-client": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.9.2.tgz", - "integrity": "sha512-DVow5AOPU40dqQtlfQK7J2GNX8dz2/4UzltMqublaPZubbkRYgocvp0b76NQu5F6v150IstMV2N49uxAYqogVw==", - "license": "MIT", - "dependencies": { - "@noble/ed25519": "^3.0.0" - } - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",