Fix ICE candidate handling - send full candidate objects

- Update IceCandidate interface to include sdpMid and sdpMLineIndex
- Update addIceCandidates to accept full candidate objects
- Update connection manager to send and receive complete ICE data
- Fixes 'Either sdpMid or sdpMLineIndex must be specified' error

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-14 19:31:30 +01:00
parent cd78a16c66
commit dd64a565aa
2 changed files with 26 additions and 8 deletions

View File

@@ -60,7 +60,11 @@ export class RondevuConnection {
private lastIceTimestamp: number = Date.now();
private eventListeners: Map<keyof RondevuConnectionEvents, Set<Function>> = new Map();
private dataChannel?: RTCDataChannel;
private pendingIceCandidates: string[] = [];
private pendingIceCandidates: Array<{
candidate: string;
sdpMid: string | null;
sdpMLineIndex: number | null;
}> = [];
/**
* Current connection state
@@ -103,18 +107,22 @@ export class RondevuConnection {
private setupPeerConnection(): void {
this.pc.onicecandidate = async (event) => {
if (event.candidate) {
const candidateString = event.candidate.candidate;
const candidateData = {
candidate: event.candidate.candidate,
sdpMid: event.candidate.sdpMid,
sdpMLineIndex: event.candidate.sdpMLineIndex,
};
if (this.offerId) {
// offerId is set, send immediately (trickle ICE)
try {
await this.offersApi.addIceCandidates(this.offerId, [candidateString]);
await this.offersApi.addIceCandidates(this.offerId, [candidateData]);
} catch (err) {
console.error('Error sending ICE candidate:', err);
}
} else {
// offerId not set yet, buffer the candidate
this.pendingIceCandidates.push(candidateString);
this.pendingIceCandidates.push(candidateData);
}
}
};
@@ -275,10 +283,11 @@ export class RondevuConnection {
);
for (const candidate of candidates) {
// Create ICE candidate from candidate string only
// Don't hardcode sdpMLineIndex/sdpMid - let WebRTC parse from candidate string
// Create ICE candidate with all fields
await this.pc.addIceCandidate(new RTCIceCandidate({
candidate: candidate.candidate
candidate: candidate.candidate,
sdpMid: candidate.sdpMid ?? undefined,
sdpMLineIndex: candidate.sdpMLineIndex ?? undefined,
}));
this.lastIceTimestamp = candidate.createdAt;
}

View File

@@ -26,6 +26,8 @@ export interface Offer {
export interface IceCandidate {
candidate: string;
sdpMid: string | null;
sdpMLineIndex: number | null;
peerId: string;
role: 'offerer' | 'answerer';
createdAt: number;
@@ -278,7 +280,14 @@ export class RondevuOffers {
/**
* Post ICE candidates for an offer
*/
async addIceCandidates(offerId: string, candidates: string[]): Promise<void> {
async addIceCandidates(
offerId: string,
candidates: Array<{
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
}>
): Promise<void> {
const response = await this.fetchFn(`${this.baseUrl}/offers/${encodeURIComponent(offerId)}/ice-candidates`, {
method: 'POST',
headers: {