ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
peer_manager.c
Go to the documentation of this file.
1
16#include <ascii-chat/log/log.h>
20#include <ascii-chat/uthash.h>
21#include <string.h>
22
39
46
60
61#define MAX_PENDING_ICE_CANDIDATES 64
62
63static void free_pending_ice_candidate(pending_ice_candidate_t *candidate) {
64 if (!candidate) {
65 return;
66 }
67 SAFE_FREE(candidate->candidate);
68 SAFE_FREE(candidate->mid);
69 SAFE_FREE(candidate);
70}
71
72static asciichat_error_t queue_pending_ice_candidate(webrtc_peer_manager_t *manager, const uint8_t participant_id[16],
73 const char *candidate, const char *mid) {
75 return SET_ERRNO(ERROR_NETWORK, "Too many ICE candidates arrived before remote SDP");
76 }
77
78 pending_ice_candidate_t *pending = SAFE_CALLOC(1, sizeof(*pending), pending_ice_candidate_t *);
79 if (!pending) {
80 return SET_ERRNO(ERROR_MEMORY, "Failed to queue early ICE candidate");
81 }
82 memcpy(pending->participant_id, participant_id, sizeof(pending->participant_id));
83 SAFE_STRDUP(pending->candidate, candidate);
84 SAFE_STRDUP(pending->mid, mid);
85 if (!pending->candidate || !pending->mid) {
86 free_pending_ice_candidate(pending);
87 return SET_ERRNO(ERROR_MEMORY, "Failed to copy early ICE candidate");
88 }
89
90 pending_ice_candidate_t **tail = &manager->pending_ice;
91 while (*tail) {
92 tail = &(*tail)->next;
93 }
94 *tail = pending;
95 manager->pending_ice_count++;
96 return ASCIICHAT_OK;
97}
98
99static asciichat_error_t apply_pending_ice_candidates(webrtc_peer_manager_t *manager, peer_entry_t *peer) {
100 pending_ice_candidate_t **link = &manager->pending_ice;
101 while (*link) {
102 pending_ice_candidate_t *pending = *link;
103 if (memcmp(pending->participant_id, peer->participant_id, sizeof(peer->participant_id)) != 0) {
104 link = &pending->next;
105 continue;
106 }
107
108 asciichat_error_t result = webrtc_add_remote_candidate(peer->pc, pending->candidate, pending->mid);
109 if (result != ASCIICHAT_OK) {
110 // A candidate queued before a retry offer can belong to the previous ICE
111 // generation. Discard that candidate, but keep processing the new SDP and
112 // the remaining trickled candidates.
113 log_warn("Discarding queued ICE candidate rejected by the new SDP: %s", asciichat_error_string(result));
114 }
115
116 *link = pending->next;
117 manager->pending_ice_count--;
118 free_pending_ice_candidate(pending);
119 }
120 return ASCIICHAT_OK;
121}
122
123static bool extract_ice_ufrag(const char *sdp, char *ufrag, size_t capacity) {
124 if (!sdp || !ufrag || capacity == 0) {
125 return false;
126 }
127
128 static const char prefix[] = "a=ice-ufrag:";
129 const char *value = strstr(sdp, prefix);
130 if (!value) {
131 ufrag[0] = '\0';
132 return false;
133 }
134
135 value += sizeof(prefix) - 1;
136 size_t length = strcspn(value, "\r\n");
137 if (length == 0 || length >= capacity) {
138 ufrag[0] = '\0';
139 return false;
140 }
141
142 memcpy(ufrag, value, length);
143 ufrag[length] = '\0';
144 return true;
145}
146
147// =============================================================================
148// Helper Functions
149// =============================================================================
150
155static peer_entry_t *find_peer_locked(webrtc_peer_manager_t *manager, const uint8_t participant_id[16]) {
156 peer_entry_t *peer = NULL;
157 HASH_FIND(hh, manager->peers, participant_id, 16, peer);
158 return peer;
159}
160
165static void add_peer_locked(webrtc_peer_manager_t *manager, peer_entry_t *peer) {
166 HASH_ADD(hh, manager->peers, participant_id, 16, peer);
167}
168
173static void remove_peer_locked(webrtc_peer_manager_t *manager, peer_entry_t *peer) {
174 if (!peer) {
175 return;
176 }
177
178 HASH_DEL(manager->peers, peer);
179
180 if (peer->dc) {
182 }
183 if (peer->pc) {
185 }
186
187 SAFE_FREE(peer);
188}
189
190// =============================================================================
191// WebRTC Callbacks
192// =============================================================================
193
197static void on_datachannel_open(webrtc_data_channel_t *dc, void *user_data) {
198 peer_entry_t *peer = (peer_entry_t *)user_data;
199
200 log_info("WebRTC DataChannel opened for participant");
201
202 // Get manager to access crypto context
203 webrtc_peer_manager_t *manager = peer->manager;
204 if (!manager) {
205 log_error("No manager found for peer");
206 return;
207 }
208
209 // For CREATOR role (server), peer->dc is NULL because the DataChannel is received, not created
210 // Update peer->dc from the dc parameter passed to this callback
211 if (!peer->dc) {
212 peer->dc = dc;
213 log_debug("Updated peer->dc from DataChannel callback (dc=%p)", (void *)peer->dc);
214 }
215
216 // Create ACIP transport wrapper
217 acip_transport_t *transport = acip_webrtc_transport_create(peer->pc, peer->dc, manager->config.crypto_ctx);
218 if (!transport) {
219 log_error("Failed to create ACIP transport for WebRTC DataChannel");
220 return;
221 }
222
223 log_debug("Transport created, checking callback: on_transport_ready=%p, user_data=%p",
224 (void *)manager->config.on_transport_ready, manager->config.user_data);
225
226 // Notify application
227 if (manager->config.on_transport_ready) {
228 log_debug("Calling on_transport_ready callback");
229 manager->config.on_transport_ready(transport, peer->participant_id, manager->config.user_data);
230 log_debug("Callback completed");
231 } else {
232 // No callback - clean up transport
233 log_warn("No on_transport_ready callback registered, cleaning up transport");
234 acip_transport_destroy(transport);
235 }
236
237 peer->is_connected = true;
238}
239
243static void on_local_description(webrtc_peer_connection_t *pc, const char *sdp, const char *type, void *user_data) {
244 (void)pc; // Unused
245 peer_entry_t *peer = (peer_entry_t *)user_data;
246 webrtc_peer_manager_t *manager = peer->manager;
247
248 if (!manager || !manager->signaling.send_sdp) {
249 log_error("No signaling callback registered for SDP");
250 return;
251 }
252
253 log_debug("on_local_description: peer->session_id=%02x%02x%02x%02x..., peer->participant_id=%02x%02x%02x%02x...",
254 peer->session_id[0], peer->session_id[1], peer->session_id[2], peer->session_id[3], peer->participant_id[0],
255 peer->participant_id[1], peer->participant_id[2], peer->participant_id[3]);
256
257 log_debug("Sending SDP %s to remote peer via ACDS", type);
258
259 asciichat_error_t result =
260 manager->signaling.send_sdp(peer->session_id, peer->participant_id, type, sdp, manager->signaling.user_data);
261
262 if (result != ASCIICHAT_OK) {
263 log_error("Failed to send SDP via signaling: %s", asciichat_error_string(result));
264 }
265}
266
270static void on_local_candidate(webrtc_peer_connection_t *pc, const char *candidate, const char *mid, void *user_data) {
271 (void)pc; // Unused
272 peer_entry_t *peer = (peer_entry_t *)user_data;
273 webrtc_peer_manager_t *manager = peer->manager;
274
275 if (!manager || !manager->signaling.send_ice) {
276 log_error("No signaling callback registered for ICE");
277 return;
278 }
279
280 // Filter out host candidates if --webrtc-skip-host is enabled (for testing STUN/TURN)
281 if (GET_OPTION(webrtc_skip_host) && strstr(candidate, "typ host") != NULL) {
282 log_debug("Skipping host candidate (--webrtc-skip-host enabled): '%s'", candidate);
283 return;
284 }
285
286 log_debug("Sending ICE candidate to remote peer via ACDS");
287 log_debug(" [1] libdatachannel gave us candidate: '%s' (len=%zu)", candidate, strlen(candidate));
288 log_debug(" [1] libdatachannel gave us mid: '%s' (len=%zu)", mid, strlen(mid));
289
290 asciichat_error_t result =
291 manager->signaling.send_ice(peer->session_id, peer->participant_id, candidate, mid, manager->signaling.user_data);
292
293 if (result != ASCIICHAT_OK) {
294 log_error("Failed to send ICE candidate via signaling: %s", asciichat_error_string(result));
295 }
296}
297
301static void on_state_change(webrtc_peer_connection_t *pc, webrtc_state_t state, void *user_data) {
302 (void)user_data;
303 (void)pc;
304
305 const char *state_str = "UNKNOWN";
306 switch (state) {
307 case WEBRTC_STATE_NEW:
308 state_str = "NEW";
309 break;
311 state_str = "CONNECTING";
312 break;
314 state_str = "CONNECTED";
315 break;
317 state_str = "DISCONNECTED";
318 break;
320 state_str = "FAILED";
321 break;
323 state_str = "CLOSED";
324 break;
325 }
326
327 log_debug("WebRTC peer connection state: %s", state_str);
328}
329
330// =============================================================================
331// Peer Connection Creation
332// =============================================================================
333
338static asciichat_error_t create_peer_connection_locked(webrtc_peer_manager_t *manager, const uint8_t session_id[16],
339 const uint8_t participant_id[16], peer_entry_t **peer_out) {
340 // Check if peer already exists
341 peer_entry_t *existing = find_peer_locked(manager, participant_id);
342 if (existing) {
343 *peer_out = existing;
344 return ASCIICHAT_OK;
345 }
346
347 // Allocate peer entry
349 if (!peer) {
350 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate peer entry");
351 }
352
353 memcpy(peer->participant_id, participant_id, 16);
354 memcpy(peer->session_id, session_id, 16);
355 peer->pc = NULL;
356 peer->dc = NULL;
357 peer->is_connected = false;
358 peer->remote_sdp_attempted = false;
359 peer->remote_description_set = false;
360 peer->gathering_timeout_reported = false;
361 peer->manager = manager;
362
363 // Create WebRTC configuration
364 webrtc_config_t webrtc_config = {
365 .bind_address = manager->config.bind_address,
366 .stun_servers = manager->config.stun_servers,
367 .stun_count = manager->config.stun_count,
368 .turn_servers = manager->config.turn_servers,
369 .turn_count = manager->config.turn_count,
370 .relay_only = manager->config.relay_only,
371 .turn_username = manager->config.turn_username,
372 .turn_credential = manager->config.turn_credential,
373 .on_state_change = on_state_change,
374 .on_local_description = on_local_description,
375 .on_local_candidate = on_local_candidate,
376 .on_datachannel_open = on_datachannel_open,
377 .on_datachannel_message = NULL, // Handled by transport layer
378 .on_datachannel_error = NULL, // Handled by transport layer
379 .user_data = peer, // Pass peer for callbacks
380 };
381
382 // Create peer connection
383 asciichat_error_t result = webrtc_create_peer_connection(&webrtc_config, &peer->pc);
384 if (result != ASCIICHAT_OK) {
385 SAFE_FREE(peer);
386 return SET_ERRNO(result, "Failed to create WebRTC peer connection");
387 }
388
389 // The joiner creates the negotiated channel before making its offer. The
390 // creator waits for the remote offer; webrtc_set_remote_description creates
391 // its matching channel after applying that offer to avoid offer glare.
392 if (manager->role == WEBRTC_ROLE_JOINER) {
393 result = webrtc_create_datachannel(peer->pc, "acip", &peer->dc);
394 if (result != ASCIICHAT_OK) {
396 SAFE_FREE(peer);
397 return SET_ERRNO(result, "Failed to create WebRTC data channel");
398 }
399
400 webrtc_datachannel_callbacks_t dc_callbacks = {
401 .on_open = on_datachannel_open,
402 .on_close = NULL,
403 .on_error = NULL,
404 .on_message = NULL, // Handled by transport
405 .user_data = peer,
406 };
407
408 result = webrtc_datachannel_set_callbacks(peer->dc, &dc_callbacks);
409 if (result != ASCIICHAT_OK) {
412 SAFE_FREE(peer);
413 return SET_ERRNO(result, "Failed to set datachannel callbacks");
414 }
415 }
416
417 // Add to hash table
418 add_peer_locked(manager, peer);
419
420 log_debug("Created WebRTC peer connection for participant (role: %s)",
421 manager->role == WEBRTC_ROLE_CREATOR ? "creator" : "joiner");
422
423 *peer_out = peer;
424 return ASCIICHAT_OK;
425}
426
427// =============================================================================
428// Public API
429// =============================================================================
430
432 const webrtc_signaling_callbacks_t *signaling_callbacks,
433 webrtc_peer_manager_t **manager_out) {
434 if (!config || !signaling_callbacks || !manager_out) {
435 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
436 }
437 *manager_out = NULL;
438
439 if (!signaling_callbacks->send_sdp || !signaling_callbacks->send_ice) {
440 return SET_ERRNO(ERROR_INVALID_PARAM, "Signaling callbacks required");
441 }
442 if (config->relay_only && config->turn_count == 0) {
443 return SET_ERRNO(ERROR_INVALID_PARAM, "Relay only requires a TURN server with credentials");
444 }
445
446 // Allocate manager
448 if (!manager) {
449 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate peer manager");
450 }
451
452 memcpy(&manager->config, config, sizeof(*config));
453 memcpy(&manager->signaling, signaling_callbacks, sizeof(*signaling_callbacks));
454 manager->role = config->role;
455 manager->peers = NULL;
456 manager->pending_ice = NULL;
457 manager->pending_ice_count = 0;
458
459 if (mutex_init(&manager->peers_mutex, "peers") != 0) {
460 SAFE_FREE(manager);
461 return SET_ERRNO(ERROR_INTERNAL, "Failed to initialize peers mutex");
462 }
463 if (mutex_init(&manager->signaling_mutex, "webrtc_signaling") != 0) {
464 mutex_destroy(&manager->peers_mutex);
465 SAFE_FREE(manager);
466 return SET_ERRNO(ERROR_INTERNAL, "Failed to initialize WebRTC signaling mutex");
467 }
468
469 log_info("Created WebRTC peer manager (role: %s)", manager->role == WEBRTC_ROLE_CREATOR ? "creator" : "joiner");
470
471 *manager_out = manager;
472 return ASCIICHAT_OK;
473}
474
476 if (!manager) {
477 return;
478 }
479
480 mutex_lock(&manager->signaling_mutex);
481 mutex_lock(&manager->peers_mutex);
482
483 // Close all peer connections
484 peer_entry_t *peer = NULL, *tmp = NULL;
485 HASH_ITER(hh, manager->peers, peer, tmp) {
486 remove_peer_locked(manager, peer);
487 }
488
489 mutex_unlock(&manager->peers_mutex);
490 pending_ice_candidate_t *pending = manager->pending_ice;
491 while (pending) {
492 pending_ice_candidate_t *next = pending->next;
493 free_pending_ice_candidate(pending);
494 pending = next;
495 }
496 mutex_unlock(&manager->signaling_mutex);
498 mutex_destroy(&manager->peers_mutex);
499
500 SAFE_FREE(manager);
501
502 log_debug("Destroyed WebRTC peer manager");
503}
504
506 if (!manager || !sdp) {
507 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
508 }
509
510 // Extract SDP string and type
511 const uint8_t *sdp_data = (const uint8_t *)(sdp + 1); // After header
512 const char *sdp_type = (sdp->sdp_type == 0) ? "offer" : "answer";
513 uint16_t sdp_len = NET_TO_HOST_U16(sdp->sdp_len);
514
515 // ACDS can deliver ICE immediately after SDP. Keep both operations ordered
516 // until libdatachannel has installed the remote description and ICE transport.
517 mutex_lock(&manager->signaling_mutex);
518
519 // Allocate null-terminated buffer for SDP string (libdatachannel requires C string)
520 char *sdp_str = SAFE_MALLOC(sdp_len + 1, char *);
521 if (!sdp_str) {
522 mutex_unlock(&manager->signaling_mutex);
523 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate remote SDP buffer");
524 }
525 memcpy(sdp_str, sdp_data, sdp_len);
526 sdp_str[sdp_len] = '\0'; // Null-terminate
527
528 char incoming_ice_ufrag[257] = {0};
529 bool has_incoming_ice_ufrag = extract_ice_ufrag(sdp_str, incoming_ice_ufrag, sizeof(incoming_ice_ufrag));
530
531 log_debug("Handling incoming SDP %s from remote peer (len=%u)", sdp_type, sdp_len);
532
533 mutex_lock(&manager->peers_mutex);
534
535 // Find or create peer connection
536 peer_entry_t *peer;
537
538 // Special case: If receiving an answer and we're a joiner, we may have created
539 // a peer with broadcast ID (00000000...) and need to update it to the real sender_id
540 if (sdp->sdp_type == 1 && manager->role == WEBRTC_ROLE_JOINER) {
541 static const uint8_t broadcast_id[16] = {0};
542 peer = find_peer_locked(manager, broadcast_id);
543 if (peer) {
544 log_debug("Updating broadcast peer with real participant_id from answer");
545 // Remove from hash with old ID
546 HASH_DEL(manager->peers, peer);
547 // Update to real participant_id
548 memcpy(peer->participant_id, sdp->sender_id, 16);
549 // Re-add with new ID
550 HASH_ADD(hh, manager->peers, participant_id, 16, peer);
551 }
552 }
553
554 // libdatachannel cannot apply an ICE restart to an existing peer connection.
555 // Repeated signaling can also redeliver the same offer, which must not be
556 // mistaken for a new ICE generation or applied twice.
557 if (sdp->sdp_type == 0 && manager->role == WEBRTC_ROLE_CREATOR) {
558 peer_entry_t *existing = find_peer_locked(manager, sdp->sender_id);
559 if (existing && existing->remote_sdp_attempted && existing->pc) {
560 webrtc_state_t state = webrtc_get_state(existing->pc);
561 bool comparable_ice_generation = has_incoming_ice_ufrag && existing->remote_ice_ufrag[0] != '\0';
562 bool new_ice_generation = comparable_ice_generation &&
563 strcmp(incoming_ice_ufrag, existing->remote_ice_ufrag) != 0;
564 bool terminal_state = state == WEBRTC_STATE_FAILED || state == WEBRTC_STATE_CLOSED;
565 if (!terminal_state && comparable_ice_generation && !new_ice_generation && existing->remote_description_set) {
566 log_debug("Ignoring duplicate WebRTC offer for existing ICE generation");
567 mutex_unlock(&manager->peers_mutex);
568 SAFE_FREE(sdp_str);
569 mutex_unlock(&manager->signaling_mutex);
570 return ASCIICHAT_OK;
571 }
572 if (terminal_state || new_ice_generation) {
573 log_info("Replacing WebRTC peer for %s", new_ice_generation ? "new ICE generation" : "terminal connection state");
574 remove_peer_locked(manager, existing);
575 }
576 }
577 }
578
579 asciichat_error_t result = create_peer_connection_locked(manager, sdp->session_id, sdp->sender_id, &peer);
580 if (result != ASCIICHAT_OK) {
581 mutex_unlock(&manager->peers_mutex);
582 SAFE_FREE(sdp_str);
583 mutex_unlock(&manager->signaling_mutex);
584 return SET_ERRNO(result, "Failed to create peer connection for SDP");
585 }
586
587 if (sdp->sdp_type == 0 && manager->role == WEBRTC_ROLE_CREATOR) {
588 peer->remote_sdp_attempted = true;
589 }
590
591 mutex_unlock(&manager->peers_mutex);
592
593 // Set remote SDP
594 result = webrtc_set_remote_description(peer->pc, sdp_str, sdp_type);
595 SAFE_FREE(sdp_str); // Free after use
596
597 if (result != ASCIICHAT_OK) {
598 mutex_unlock(&manager->signaling_mutex);
599 return SET_ERRNO(result, "Failed to set remote SDP");
600 }
601
602 // A peer entry can be created before its offer arrives (for example when
603 // ACDS reports a participant join). Do not treat that entry as ready for
604 // trickle ICE until libdatachannel has accepted its remote description.
605 peer->remote_description_set = true;
606 if (sdp->sdp_type == 0 && has_incoming_ice_ufrag) {
607 memcpy(peer->remote_ice_ufrag, incoming_ice_ufrag, strlen(incoming_ice_ufrag) + 1);
608 }
609
610 result = apply_pending_ice_candidates(manager, peer);
611 if (result != ASCIICHAT_OK) {
612 mutex_unlock(&manager->signaling_mutex);
613 return SET_ERRNO(result, "Failed to apply ICE candidates queued before remote SDP");
614 }
615
616 // If this is an offer and we're the creator, generate answer automatically
617 // (libdatachannel triggers on_local_description callback with answer)
618 if (sdp->sdp_type == 0 && manager->role == WEBRTC_ROLE_CREATOR) {
619 log_debug("Offer received, answer will be generated automatically");
620 }
621
622 mutex_unlock(&manager->signaling_mutex);
623 return ASCIICHAT_OK;
624}
625
627 if (!manager || !ice) {
628 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
629 }
630
631 // Extract ICE candidate and mid (both are null-terminated in the packet)
632 const char *candidate = (const char *)(ice + 1); // After header
633 size_t candidate_str_len = strlen(candidate);
634 const char *mid = candidate + candidate_str_len + 1; // After candidate + null terminator
635
636 log_debug("Handling incoming ICE candidate from remote peer (mid=%s)", mid);
637 log_debug(" [3] After ACDS recv - candidate: '%s' (len=%zu)", candidate, strlen(candidate));
638 log_debug(" [3] After ACDS recv - mid: '%s' (len=%zu)", mid, strlen(mid));
639 log_debug(" [3] After ACDS recv - header.candidate_len=%u", NET_TO_HOST_U16(ice->candidate_len));
640
641 // Hex dump first 100 bytes of payload for debugging
642 const uint8_t *payload = (const uint8_t *)(ice + 1);
643 log_debug(" [3] Hex dump of payload (first 100 bytes):");
644 for (int i = 0; i < 100 && i < (int)candidate_str_len + 20; i += 16) {
645 char hex[64] = {0};
646 char ascii[20] = {0};
647 for (int j = 0; j < 16 && (i + j) < 100; j++) {
648 snprintf(hex + j * 3, sizeof(hex) - j * 3, "%02x ", payload[i + j]);
649 ascii[j] = (payload[i + j] >= 32 && payload[i + j] < 127) ? payload[i + j] : '.';
650 }
651 log_debug(" [%04x] %-48s %s", i, hex, ascii);
652 }
653
654 // Do not pass a candidate to libdatachannel until a preceding SDP packet has
655 // finished installing the remote ICE transport.
656 mutex_lock(&manager->signaling_mutex);
657 mutex_lock(&manager->peers_mutex);
658
659 // Find peer connection
660 peer_entry_t *peer = find_peer_locked(manager, ice->sender_id);
661 if (!peer || !peer->remote_description_set) {
662 mutex_unlock(&manager->peers_mutex);
663 asciichat_error_t result = queue_pending_ice_candidate(manager, ice->sender_id, candidate, mid);
664 mutex_unlock(&manager->signaling_mutex);
665 if (result == ASCIICHAT_OK) {
666 log_debug("Queued ICE candidate received before remote SDP was installed");
667 }
668 return result;
669 }
670
671 mutex_unlock(&manager->peers_mutex);
672
673 // Add remote ICE candidate
674 asciichat_error_t result = webrtc_add_remote_candidate(peer->pc, candidate, mid);
675 if (result != ASCIICHAT_OK) {
676 mutex_unlock(&manager->signaling_mutex);
677 return SET_ERRNO(result, "Failed to add remote ICE candidate");
678 }
679
680 mutex_unlock(&manager->signaling_mutex);
681 return ASCIICHAT_OK;
682}
683
685 const uint8_t participant_id[16]) {
686 if (!manager || !session_id || !participant_id) {
687 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
688 }
689
690 if (manager->role != WEBRTC_ROLE_JOINER) {
691 return SET_ERRNO(ERROR_INVALID_PARAM, "Only joiners can initiate connections");
692 }
693
694 log_debug("webrtc_peer_manager_connect: session_id=%02x%02x%02x%02x..., participant_id=%02x%02x%02x%02x...",
697
698 mutex_lock(&manager->peers_mutex);
699
700 // Create peer connection
701 peer_entry_t *peer;
702 asciichat_error_t result = create_peer_connection_locked(manager, session_id, participant_id, &peer);
703 if (result != ASCIICHAT_OK) {
704 mutex_unlock(&manager->peers_mutex);
705 return SET_ERRNO(result, "Failed to create peer connection");
706 }
707
708 mutex_unlock(&manager->peers_mutex);
709
710 // Note: SDP offer is automatically created by libdatachannel when rtcCreateDataChannel() is called
711 // The on_local_description callback will be triggered automatically with the offer
712 // No need to manually call webrtc_create_offer() - doing so causes "Unexpected local description" error
713
714 log_info("Initiated WebRTC connection to participant (offer auto-created by DataChannel)");
715
716 return ASCIICHAT_OK;
717}
718
720 webrtc_state_t *state_out) {
721 if (!manager || !participant_id || !state_out) {
722 return false;
723 }
724
725 mutex_lock(&manager->peers_mutex);
726 peer_entry_t *peer = find_peer_locked(manager, participant_id);
727 bool found = peer && peer->pc;
728 if (found) {
729 *state_out = webrtc_get_state(peer->pc);
730 }
731 mutex_unlock(&manager->peers_mutex);
732 return found;
733}
734
736 const uint8_t participant_id[16]) {
737 if (!manager || !participant_id) {
738 return SET_ERRNO(ERROR_INVALID_PARAM, "Peer manager and participant ID are required");
739 }
740
741 // SDP and ICE handlers retain peer pointers after releasing peers_mutex.
742 // Serialize removal with those handlers before freeing the peer wrappers.
743 mutex_lock(&manager->signaling_mutex);
744 mutex_lock(&manager->peers_mutex);
745 peer_entry_t *peer = find_peer_locked(manager, participant_id);
746 if (peer) {
747 remove_peer_locked(manager, peer);
748 }
749 mutex_unlock(&manager->peers_mutex);
750 mutex_unlock(&manager->signaling_mutex);
751 return ASCIICHAT_OK;
752}
753
755 if (!manager) {
756 return 0;
757 }
758
759 int timeout_count = 0;
760 peer_entry_t *peer = NULL, *tmp = NULL;
761
762 mutex_lock(&manager->signaling_mutex);
763 mutex_lock(&manager->peers_mutex);
764
765 // Iterate through all peers and check for gathering timeout
766 HASH_ITER(hh, manager->peers, peer, tmp) {
767 if (!peer || !peer->pc) {
768 continue;
769 }
770
771 // Trickle ICE can connect before gathering completes. Report a slow gather,
772 // but keep the peer alive until libdatachannel reports a failed connection.
773 webrtc_state_t connection_state = webrtc_get_state(peer->pc);
774 bool gathering_timed_out = webrtc_is_gathering_timed_out(peer->pc, timeout_ms);
775 if (gathering_timed_out && !peer->gathering_timeout_reported) {
776 peer->gathering_timeout_reported = true;
777 if (manager->config.on_gathering_timeout) {
778 manager->config.on_gathering_timeout(peer->participant_id, timeout_ms, timeout_ms, manager->config.user_data);
779 }
780 log_warn("ICE gathering exceeded %ums; keeping peer alive while trickle ICE continues", timeout_ms);
781 }
782
783 if (connection_state == WEBRTC_STATE_FAILED || connection_state == WEBRTC_STATE_CLOSED) {
785
786 log_error("WebRTC negotiation failed for peer (participant_id=%02x%02x%02x%02x..., timeout=%ums, "
787 "gathering=%d, connection=%d)",
788 peer->participant_id[0], peer->participant_id[1], peer->participant_id[2], peer->participant_id[3],
789 timeout_ms, state, connection_state);
790
791 // Remove and close peers whose ICE connection has actually failed.
792 remove_peer_locked(manager, peer);
793 timeout_count++;
794
795 log_info("Closed and removed timed-out peer connection (count: %d)", timeout_count);
796 }
797 }
798
799 mutex_unlock(&manager->peers_mutex);
800 mutex_unlock(&manager->signaling_mutex);
801
802 return timeout_count;
803}
🔄 Network byte order conversion helpers
#define NET_TO_HOST_U16(val)
Definition endian.h:111
acip_webrtc_ice_t
Definition acds.h:650
acip_webrtc_sdp_t
Definition acds.h:604
unsigned short uint16_t
Definition common.h:57
unsigned int uint32_t
Definition common.h:58
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned char uint8_t
Definition common.h:56
#define SAFE_STRDUP(dst, src)
Definition common.h:418
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_NETWORK
Definition error_codes.h:77
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_INTERNAL
Definition error_codes.h:92
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22
bool webrtc_peer_manager_get_state(webrtc_peer_manager_t *manager, const uint8_t participant_id[16], webrtc_state_t *state_out)
asciichat_error_t webrtc_peer_manager_remove_peer(webrtc_peer_manager_t *manager, const uint8_t participant_id[16])
asciichat_error_t webrtc_peer_manager_create(const webrtc_peer_manager_config_t *config, const webrtc_signaling_callbacks_t *signaling_callbacks, webrtc_peer_manager_t **manager_out)
Create a WebRTC peer manager.
asciichat_error_t webrtc_peer_manager_handle_ice(webrtc_peer_manager_t *manager, const acip_webrtc_ice_t *ice)
Handle incoming ICE candidate from ACDS.
webrtc_peer_role_t
Peer connection role (server or client)
asciichat_error_t webrtc_peer_manager_handle_sdp(webrtc_peer_manager_t *manager, const acip_webrtc_sdp_t *sdp)
Handle incoming SDP message from ACDS.
void webrtc_peer_manager_destroy(webrtc_peer_manager_t *manager)
Destroy peer manager and close all connections.
asciichat_error_t webrtc_peer_manager_connect(webrtc_peer_manager_t *manager, const uint8_t session_id[16], const uint8_t participant_id[16])
Initiate connection to remote peer (joiner role only)
int webrtc_peer_manager_check_gathering_timeouts(webrtc_peer_manager_t *manager, uint32_t timeout_ms)
Report slow ICE gathering and remove failed connections.
@ WEBRTC_ROLE_JOINER
Session joiner - generates offers, receives answers.
@ WEBRTC_ROLE_CREATOR
Session creator - accepts offers, generates answers.
asciichat_error_t webrtc_create_datachannel(webrtc_peer_connection_t *pc, const char *label, webrtc_data_channel_t **dc_out)
Create a DataChannel (for connection initiator)
asciichat_error_t webrtc_datachannel_set_callbacks(webrtc_data_channel_t *dc, const webrtc_datachannel_callbacks_t *callbacks)
Set DataChannel callbacks.
asciichat_error_t webrtc_add_remote_candidate(webrtc_peer_connection_t *pc, const char *candidate, const char *mid)
Add remote ICE candidate.
webrtc_state_t webrtc_get_state(webrtc_peer_connection_t *pc)
Get current connection state.
webrtc_gathering_state_t webrtc_get_gathering_state(webrtc_peer_connection_t *pc)
Get current ICE gathering state.
void webrtc_datachannel_destroy(webrtc_data_channel_t *dc)
Destroy a DataChannel and free resources.
asciichat_error_t webrtc_create_peer_connection(const webrtc_config_t *config, webrtc_peer_connection_t **pc_out)
Create a new WebRTC peer connection.
webrtc_gathering_state_t
ICE gathering state.
webrtc_state_t
WebRTC connection state.
void webrtc_peer_connection_destroy(webrtc_peer_connection_t *pc)
Destroy a peer connection and free resources.
asciichat_error_t webrtc_set_remote_description(webrtc_peer_connection_t *pc, const char *sdp, const char *type)
Set remote SDP offer/answer.
bool webrtc_is_gathering_timed_out(webrtc_peer_connection_t *pc, uint32_t timeout_ms)
Check if ICE gathering has timed out.
@ WEBRTC_STATE_NEW
Connection created but not started.
@ WEBRTC_STATE_DISCONNECTED
Connection lost.
@ WEBRTC_STATE_FAILED
Connection failed (fatal)
@ WEBRTC_STATE_CLOSED
Connection closed cleanly.
@ WEBRTC_STATE_CONNECTING
ICE gathering/connection in progress.
@ WEBRTC_STATE_CONNECTED
DataChannel established and ready.
📝 Logging API with multiple log levels and terminal output control
struct pending_ice_candidate pending_ice_candidate_t
struct webrtc_peer_manager webrtc_peer_manager_t
WebRTC peer manager structure.
#define MAX_PENDING_ICE_CANDIDATES
Cross-platform mutex interface for ascii-chat.
uint8_t session_id[16]
uint8_t participant_id[16]
Transport instance structure.
Definition transport.h:214
Mutex type (POSIX: pthread_mutex_t with debug tracking)
Per-peer connection state.
char remote_ice_ufrag[257]
ICE generation currently installed on this peer.
webrtc_peer_connection_t * pc
WebRTC peer connection.
uint8_t participant_id[16]
Remote participant UUID (hash key)
bool is_connected
DataChannel opened.
bool remote_sdp_attempted
A remote description was applied or attempted.
struct webrtc_peer_manager * manager
Back-reference to manager.
bool gathering_timeout_reported
ICE gathering timed out while trickle candidates may still connect.
UT_hash_handle hh
uthash handle
bool remote_description_set
libdatachannel has installed the remote SDP and ICE transport
uint8_t session_id[16]
Session UUID.
webrtc_data_channel_t * dc
WebRTC data channel.
struct pending_ice_candidate * next
uint8_t participant_id[16]
const char * bind_address
Optional local interface address for ICE sockets.
WebRTC data channel for sending/receiving messages.
void(* on_open)(webrtc_data_channel_t *dc, void *user_data)
Channel opened.
WebRTC peer connection state.
Peer manager configuration.
webrtc_peer_role_t role
Session role (creator or joiner)
size_t turn_count
Number of TURN servers.
const char * turn_credential
Optional TURN credential override.
turn_server_t * turn_servers
TURN servers for relay.
const char * turn_username
Optional TURN credential override.
const char * bind_address
Optional local interface address for ICE sockets.
size_t stun_count
Number of STUN servers.
bool relay_only
Restrict ICE to TURN relay candidates.
void * user_data
Passed to callbacks.
webrtc_transport_ready_callback_t on_transport_ready
Called when DataChannel ready.
crypto_context_t * crypto_ctx
Crypto context for transports.
stun_server_t * stun_servers
STUN servers for ICE.
webrtc_gathering_timeout_callback_t on_gathering_timeout
Called when ICE gathering times out.
WebRTC peer manager structure.
webrtc_peer_role_t role
Session role.
mutex_t peers_mutex
Protect peers hash table.
pending_ice_candidate_t * pending_ice
ICE delivered before the peer's remote SDP.
peer_entry_t * peers
Hash table of peer connections.
webrtc_peer_manager_config_t config
Manager configuration.
webrtc_signaling_callbacks_t signaling
Signaling callbacks.
mutex_t signaling_mutex
Serialize SDP and ICE updates for each peer.
Signaling callbacks for sending SDP/ICE.
void * user_data
Passed to callbacks.
webrtc_send_sdp_callback_t send_sdp
Send SDP via ACDS.
webrtc_send_ice_callback_t send_ice
Send ICE via ACDS.
Cross-platform system functions interface for ascii-chat.
void acip_transport_destroy(acip_transport_t *transport)
Destroy transport and free all resources.
Transport abstraction layer for ACIP protocol.
acip_transport_t * acip_webrtc_transport_create(webrtc_peer_connection_t *peer_conn, webrtc_data_channel_t *data_channel, crypto_context_t *crypto_ctx)