Fix ICE candidate handling - include sdpMid and sdpMLineIndex

- Update IceCandidate interface to include sdpMid and sdpMLineIndex fields
- Update SQLite and D1 storage to store full ICE candidate data
- Update server API to accept and return complete candidate objects
- This fixes the '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:29 +01:00
parent 685692dee1
commit b3bd2679fc
3 changed files with 51 additions and 13 deletions

View File

@@ -59,6 +59,8 @@ export class D1Storage implements Storage {
peer_id TEXT NOT NULL, peer_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')), role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
candidate TEXT NOT NULL, candidate TEXT NOT NULL,
sdp_mid TEXT,
sdp_m_line_index INTEGER,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
); );
@@ -242,14 +244,26 @@ export class D1Storage implements Storage {
offerId: string, offerId: string,
peerId: string, peerId: string,
role: 'offerer' | 'answerer', role: 'offerer' | 'answerer',
candidates: string[] candidates: Array<{
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
}>
): Promise<number> { ): Promise<number> {
// D1 doesn't have transactions, so insert one by one // D1 doesn't have transactions, so insert one by one
for (const candidate of candidates) { for (const cand of candidates) {
await this.db.prepare(` await this.db.prepare(`
INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at) INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, sdp_mid, sdp_m_line_index, created_at)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`).bind(offerId, peerId, role, candidate, Date.now()).run(); `).bind(
offerId,
peerId,
role,
cand.candidate,
cand.sdpMid ?? null,
cand.sdpMLineIndex ?? null,
Date.now()
).run();
} }
return candidates.length; return candidates.length;
@@ -286,6 +300,8 @@ export class D1Storage implements Storage {
peerId: row.peer_id, peerId: row.peer_id,
role: row.role, role: row.role,
candidate: row.candidate, candidate: row.candidate,
sdpMid: row.sdp_mid,
sdpMLineIndex: row.sdp_m_line_index,
createdAt: row.created_at, createdAt: row.created_at,
})); }));
} }

View File

@@ -56,6 +56,8 @@ export class SQLiteStorage implements Storage {
peer_id TEXT NOT NULL, peer_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')), role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
candidate TEXT NOT NULL, candidate TEXT NOT NULL,
sdp_mid TEXT,
sdp_m_line_index INTEGER,
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
); );
@@ -252,16 +254,28 @@ export class SQLiteStorage implements Storage {
offerId: string, offerId: string,
peerId: string, peerId: string,
role: 'offerer' | 'answerer', role: 'offerer' | 'answerer',
candidates: string[] candidates: Array<{
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
}>
): Promise<number> { ): Promise<number> {
const stmt = this.db.prepare(` const stmt = this.db.prepare(`
INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at) INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, sdp_mid, sdp_m_line_index, created_at)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
`); `);
const transaction = this.db.transaction((candidates: string[]) => { const transaction = this.db.transaction((candidates: typeof candidates) => {
for (const candidate of candidates) { for (const cand of candidates) {
stmt.run(offerId, peerId, role, candidate, Date.now()); stmt.run(
offerId,
peerId,
role,
cand.candidate,
cand.sdpMid ?? null,
cand.sdpMLineIndex ?? null,
Date.now()
);
} }
}); });
@@ -297,6 +311,8 @@ export class SQLiteStorage implements Storage {
peerId: row.peer_id, peerId: row.peer_id,
role: row.role, role: row.role,
candidate: row.candidate, candidate: row.candidate,
sdpMid: row.sdp_mid,
sdpMLineIndex: row.sdp_m_line_index,
createdAt: row.created_at, createdAt: row.created_at,
})); }));
} }

View File

@@ -23,6 +23,8 @@ export interface IceCandidate {
peerId: string; peerId: string;
role: 'offerer' | 'answerer'; role: 'offerer' | 'answerer';
candidate: string; candidate: string;
sdpMid: string | null;
sdpMLineIndex: number | null;
createdAt: number; createdAt: number;
} }
@@ -125,14 +127,18 @@ export interface Storage {
* @param offerId Offer identifier * @param offerId Offer identifier
* @param peerId Peer ID posting the candidates * @param peerId Peer ID posting the candidates
* @param role Role of the peer (offerer or answerer) * @param role Role of the peer (offerer or answerer)
* @param candidates Array of ICE candidate strings * @param candidates Array of ICE candidate objects
* @returns Number of candidates added * @returns Number of candidates added
*/ */
addIceCandidates( addIceCandidates(
offerId: string, offerId: string,
peerId: string, peerId: string,
role: 'offerer' | 'answerer', role: 'offerer' | 'answerer',
candidates: string[] candidates: Array<{
candidate: string;
sdpMid?: string | null;
sdpMLineIndex?: number | null;
}>
): Promise<number>; ): Promise<number>;
/** /**