ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
peer_manager.c
Go to the documentation of this file.
1
14#include <ascii-chat/network/webrtc/peer_manager.h>
15#include <ascii-chat/network/acip/transport.h>
16#include <ascii-chat/log/logging.h>
17#include <ascii-chat/platform/mutex.h>
18#include <ascii-chat/platform/system.h>
19#include <ascii-chat/util/endian.h>
20#include <ascii-chat/uthash/uthash.h>
21#include <string.h>
22
26typedef struct {
27 uint8_t participant_id[16];
28 uint8_t session_id[16];
29 webrtc_peer_connection_t *pc;
30 webrtc_data_channel_t *dc;
33 UT_hash_handle hh;
35
39typedef struct webrtc_peer_manager {
40 webrtc_peer_role_t role;
41 webrtc_peer_manager_config_t config;
42 webrtc_signaling_callbacks_t signaling;
44 mutex_t peers_mutex;
46
47// =============================================================================
48// Helper Functions
49// =============================================================================
50
55static peer_entry_t *find_peer_locked(webrtc_peer_manager_t *manager, const uint8_t participant_id[16]) {
56 peer_entry_t *peer = NULL;
57 HASH_FIND(hh, manager->peers, participant_id, 16, peer);
58 return peer;
59}
60
65static void add_peer_locked(webrtc_peer_manager_t *manager, peer_entry_t *peer) {
66 HASH_ADD(hh, manager->peers, participant_id, 16, peer);
67}
68
73static void remove_peer_locked(webrtc_peer_manager_t *manager, peer_entry_t *peer) {
74 if (!peer) {
75 return;
76 }
77
78 HASH_DEL(manager->peers, peer);
79
80 if (peer->dc) {
82 }
83 if (peer->pc) {
85 }
86
87 SAFE_FREE(peer);
88}
89
90// =============================================================================
91// WebRTC Callbacks
92// =============================================================================
93
97static void on_datachannel_open(webrtc_data_channel_t *dc, void *user_data) {
98 peer_entry_t *peer = (peer_entry_t *)user_data;
99
100 log_info("WebRTC DataChannel opened for participant");
101
102 // Get manager to access crypto context
103 webrtc_peer_manager_t *manager = peer->manager;
104 if (!manager) {
105 log_error("No manager found for peer");
106 return;
107 }
108
109 // For CREATOR role (server), peer->dc is NULL because the DataChannel is received, not created
110 // Update peer->dc from the dc parameter passed to this callback
111 if (!peer->dc) {
112 peer->dc = dc;
113 log_debug("Updated peer->dc from DataChannel callback (dc=%p)", (void *)peer->dc);
114 }
115
116 // Create ACIP transport wrapper
117 acip_transport_t *transport = acip_webrtc_transport_create(peer->pc, peer->dc, manager->config.crypto_ctx);
118 if (!transport) {
119 log_error("Failed to create ACIP transport for WebRTC DataChannel");
120 return;
121 }
122
123 log_debug("Transport created, checking callback: on_transport_ready=%p, user_data=%p",
124 (void *)manager->config.on_transport_ready, manager->config.user_data);
125
126 // Notify application
127 if (manager->config.on_transport_ready) {
128 log_debug("Calling on_transport_ready callback");
129 manager->config.on_transport_ready(transport, peer->participant_id, manager->config.user_data);
130 log_debug("Callback completed");
131 } else {
132 // No callback - clean up transport
133 log_warn("No on_transport_ready callback registered, cleaning up transport");
134 acip_transport_destroy(transport);
135 }
136
137 peer->is_connected = true;
138}
139
143static void on_local_description(webrtc_peer_connection_t *pc, const char *sdp, const char *type, void *user_data) {
144 (void)pc; // Unused
145 peer_entry_t *peer = (peer_entry_t *)user_data;
146 webrtc_peer_manager_t *manager = peer->manager;
147
148 if (!manager || !manager->signaling.send_sdp) {
149 log_error("No signaling callback registered for SDP");
150 return;
151 }
152
153 log_debug("on_local_description: peer->session_id=%02x%02x%02x%02x..., peer->participant_id=%02x%02x%02x%02x...",
154 peer->session_id[0], peer->session_id[1], peer->session_id[2], peer->session_id[3], peer->participant_id[0],
155 peer->participant_id[1], peer->participant_id[2], peer->participant_id[3]);
156
157 log_debug("Sending SDP %s to remote peer via ACDS", type);
158
159 asciichat_error_t result =
160 manager->signaling.send_sdp(peer->session_id, peer->participant_id, type, sdp, manager->signaling.user_data);
161
162 if (result != ASCIICHAT_OK) {
163 log_error("Failed to send SDP via signaling: %s", asciichat_error_string(result));
164 }
165}
166
170static void on_local_candidate(webrtc_peer_connection_t *pc, const char *candidate, const char *mid, void *user_data) {
171 (void)pc; // Unused
172 peer_entry_t *peer = (peer_entry_t *)user_data;
173 webrtc_peer_manager_t *manager = peer->manager;
174
175 if (!manager || !manager->signaling.send_ice) {
176 log_error("No signaling callback registered for ICE");
177 return;
178 }
179
180 // Filter out host candidates if --webrtc-skip-host is enabled (for testing STUN/TURN)
181 if (GET_OPTION(webrtc_skip_host) && strstr(candidate, "typ host") != NULL) {
182 log_debug("Skipping host candidate (--webrtc-skip-host enabled): '%s'", candidate);
183 return;
184 }
185
186 log_debug("Sending ICE candidate to remote peer via ACDS");
187 log_debug(" [1] libdatachannel gave us candidate: '%s' (len=%zu)", candidate, strlen(candidate));
188 log_debug(" [1] libdatachannel gave us mid: '%s' (len=%zu)", mid, strlen(mid));
189
190 asciichat_error_t result =
191 manager->signaling.send_ice(peer->session_id, peer->participant_id, candidate, mid, manager->signaling.user_data);
192
193 if (result != ASCIICHAT_OK) {
194 log_error("Failed to send ICE candidate via signaling: %s", asciichat_error_string(result));
195 }
196}
197
201static void on_state_change(webrtc_peer_connection_t *pc, webrtc_state_t state, void *user_data) {
202 (void)user_data;
203 (void)pc;
204
205 const char *state_str = "UNKNOWN";
206 switch (state) {
207 case WEBRTC_STATE_NEW:
208 state_str = "NEW";
209 break;
210 case WEBRTC_STATE_CONNECTING:
211 state_str = "CONNECTING";
212 break;
213 case WEBRTC_STATE_CONNECTED:
214 state_str = "CONNECTED";
215 break;
216 case WEBRTC_STATE_DISCONNECTED:
217 state_str = "DISCONNECTED";
218 break;
219 case WEBRTC_STATE_FAILED:
220 state_str = "FAILED";
221 break;
222 case WEBRTC_STATE_CLOSED:
223 state_str = "CLOSED";
224 break;
225 }
226
227 log_debug("WebRTC peer connection state: %s", state_str);
228}
229
230// =============================================================================
231// Peer Connection Creation
232// =============================================================================
233
238static asciichat_error_t create_peer_connection_locked(webrtc_peer_manager_t *manager, const uint8_t session_id[16],
239 const uint8_t participant_id[16], peer_entry_t **peer_out) {
240 // Check if peer already exists
241 peer_entry_t *existing = find_peer_locked(manager, participant_id);
242 if (existing) {
243 *peer_out = existing;
244 return ASCIICHAT_OK;
245 }
246
247 // Allocate peer entry
248 peer_entry_t *peer = SAFE_MALLOC(sizeof(peer_entry_t), peer_entry_t *);
249 if (!peer) {
250 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate peer entry");
251 }
252
253 memcpy(peer->participant_id, participant_id, 16);
254 memcpy(peer->session_id, session_id, 16);
255 peer->pc = NULL;
256 peer->dc = NULL;
257 peer->is_connected = false;
258 peer->manager = manager;
259
260 // Create WebRTC configuration
261 webrtc_config_t webrtc_config = {
262 .stun_servers = manager->config.stun_servers,
263 .stun_count = manager->config.stun_count,
264 .turn_servers = manager->config.turn_servers,
265 .turn_count = manager->config.turn_count,
266 .on_state_change = on_state_change,
267 .on_local_description = on_local_description,
268 .on_local_candidate = on_local_candidate,
269 .on_datachannel_open = on_datachannel_open,
270 .on_datachannel_message = NULL, // Handled by transport layer
271 .on_datachannel_error = NULL, // Handled by transport layer
272 .user_data = peer, // Pass peer for callbacks
273 };
274
275 // Create peer connection
276 asciichat_error_t result = webrtc_create_peer_connection(&webrtc_config, &peer->pc);
277 if (result != ASCIICHAT_OK) {
278 SAFE_FREE(peer);
279 return SET_ERRNO(result, "Failed to create WebRTC peer connection");
280 }
281
282 // For joiner role, create data channel (creator receives it via callback)
283 if (manager->role == WEBRTC_ROLE_JOINER) {
284 result = webrtc_create_datachannel(peer->pc, "acip", &peer->dc);
285 if (result != ASCIICHAT_OK) {
287 SAFE_FREE(peer);
288 return SET_ERRNO(result, "Failed to create WebRTC data channel");
289 }
290
291 // Set datachannel callbacks
292 webrtc_datachannel_callbacks_t dc_callbacks = {
293 .on_open = on_datachannel_open,
294 .on_close = NULL,
295 .on_error = NULL,
296 .on_message = NULL, // Handled by transport
297 .user_data = peer,
298 };
299
300 result = webrtc_datachannel_set_callbacks(peer->dc, &dc_callbacks);
301 if (result != ASCIICHAT_OK) {
304 SAFE_FREE(peer);
305 return SET_ERRNO(result, "Failed to set datachannel callbacks");
306 }
307 }
308
309 // Add to hash table
310 add_peer_locked(manager, peer);
311
312 log_debug("Created WebRTC peer connection for participant (role: %s)",
313 manager->role == WEBRTC_ROLE_CREATOR ? "creator" : "joiner");
314
315 *peer_out = peer;
316 return ASCIICHAT_OK;
317}
318
319// =============================================================================
320// Public API
321// =============================================================================
322
323asciichat_error_t webrtc_peer_manager_create(const webrtc_peer_manager_config_t *config,
324 const webrtc_signaling_callbacks_t *signaling_callbacks,
325 webrtc_peer_manager_t **manager_out) {
326 if (!config || !signaling_callbacks || !manager_out) {
327 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
328 }
329
330 if (!signaling_callbacks->send_sdp || !signaling_callbacks->send_ice) {
331 return SET_ERRNO(ERROR_INVALID_PARAM, "Signaling callbacks required");
332 }
333
334 // Allocate manager
335 webrtc_peer_manager_t *manager = SAFE_MALLOC(sizeof(webrtc_peer_manager_t), webrtc_peer_manager_t *);
336 if (!manager) {
337 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate peer manager");
338 }
339
340 memcpy(&manager->config, config, sizeof(*config));
341 memcpy(&manager->signaling, signaling_callbacks, sizeof(*signaling_callbacks));
342 manager->role = config->role;
343 manager->peers = NULL;
344
345 if (mutex_init(&manager->peers_mutex) != 0) {
346 SAFE_FREE(manager);
347 return SET_ERRNO(ERROR_INTERNAL, "Failed to initialize peers mutex");
348 }
349
350 log_info("Created WebRTC peer manager (role: %s)", manager->role == WEBRTC_ROLE_CREATOR ? "creator" : "joiner");
351
352 *manager_out = manager;
353 return ASCIICHAT_OK;
354}
355
357 if (!manager) {
358 return;
359 }
360
361 mutex_lock(&manager->peers_mutex);
362
363 // Close all peer connections
364 peer_entry_t *peer = NULL, *tmp = NULL;
365 HASH_ITER(hh, manager->peers, peer, tmp) {
366 remove_peer_locked(manager, peer);
367 }
368
369 mutex_unlock(&manager->peers_mutex);
370 mutex_destroy(&manager->peers_mutex);
371
372 SAFE_FREE(manager);
373
374 log_debug("Destroyed WebRTC peer manager");
375}
376
377asciichat_error_t webrtc_peer_manager_handle_sdp(webrtc_peer_manager_t *manager, const acip_webrtc_sdp_t *sdp) {
378 if (!manager || !sdp) {
379 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
380 }
381
382 // Extract SDP string and type
383 const uint8_t *sdp_data = (const uint8_t *)(sdp + 1); // After header
384 const char *sdp_type = (sdp->sdp_type == 0) ? "offer" : "answer";
385 uint16_t sdp_len = NET_TO_HOST_U16(sdp->sdp_len);
386
387 // Allocate null-terminated buffer for SDP string (libdatachannel requires C string)
388 char *sdp_str = SAFE_MALLOC(sdp_len + 1, char *);
389 memcpy(sdp_str, sdp_data, sdp_len);
390 sdp_str[sdp_len] = '\0'; // Null-terminate
391
392 log_debug("Handling incoming SDP %s from remote peer (len=%u)", sdp_type, sdp_len);
393
394 mutex_lock(&manager->peers_mutex);
395
396 // Find or create peer connection
397 peer_entry_t *peer;
398
399 // Special case: If receiving an answer and we're a joiner, we may have created
400 // a peer with broadcast ID (00000000...) and need to update it to the real sender_id
401 if (sdp->sdp_type == 1 && manager->role == WEBRTC_ROLE_JOINER) {
402 static const uint8_t broadcast_id[16] = {0};
403 peer = find_peer_locked(manager, broadcast_id);
404 if (peer) {
405 log_debug("Updating broadcast peer with real participant_id from answer");
406 // Remove from hash with old ID
407 HASH_DEL(manager->peers, peer);
408 // Update to real participant_id
409 memcpy(peer->participant_id, sdp->sender_id, 16);
410 // Re-add with new ID
411 HASH_ADD(hh, manager->peers, participant_id, 16, peer);
412 }
413 }
414
415 asciichat_error_t result = create_peer_connection_locked(manager, sdp->session_id, sdp->sender_id, &peer);
416 if (result != ASCIICHAT_OK) {
417 mutex_unlock(&manager->peers_mutex);
418 SAFE_FREE(sdp_str);
419 return SET_ERRNO(result, "Failed to create peer connection for SDP");
420 }
421
422 mutex_unlock(&manager->peers_mutex);
423
424 // Set remote SDP
425 result = webrtc_set_remote_description(peer->pc, sdp_str, sdp_type);
426 SAFE_FREE(sdp_str); // Free after use
427
428 if (result != ASCIICHAT_OK) {
429 return SET_ERRNO(result, "Failed to set remote SDP");
430 }
431
432 // If this is an offer and we're the creator, generate answer automatically
433 // (libdatachannel triggers on_local_description callback with answer)
434 if (sdp->sdp_type == 0 && manager->role == WEBRTC_ROLE_CREATOR) {
435 log_debug("Offer received, answer will be generated automatically");
436 }
437
438 return ASCIICHAT_OK;
439}
440
441asciichat_error_t webrtc_peer_manager_handle_ice(webrtc_peer_manager_t *manager, const acip_webrtc_ice_t *ice) {
442 if (!manager || !ice) {
443 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
444 }
445
446 // Extract ICE candidate and mid (both are null-terminated in the packet)
447 const char *candidate = (const char *)(ice + 1); // After header
448 size_t candidate_str_len = strlen(candidate);
449 const char *mid = candidate + candidate_str_len + 1; // After candidate + null terminator
450
451 log_debug("Handling incoming ICE candidate from remote peer (mid=%s)", mid);
452 log_debug(" [3] After ACDS recv - candidate: '%s' (len=%zu)", candidate, strlen(candidate));
453 log_debug(" [3] After ACDS recv - mid: '%s' (len=%zu)", mid, strlen(mid));
454 log_debug(" [3] After ACDS recv - header.candidate_len=%u", NET_TO_HOST_U16(ice->candidate_len));
455
456 // Hex dump first 100 bytes of payload for debugging
457 const uint8_t *payload = (const uint8_t *)(ice + 1);
458 log_debug(" [3] Hex dump of payload (first 100 bytes):");
459 for (int i = 0; i < 100 && i < (int)candidate_str_len + 20; i += 16) {
460 char hex[64] = {0};
461 char ascii[20] = {0};
462 for (int j = 0; j < 16 && (i + j) < 100; j++) {
463 snprintf(hex + j * 3, sizeof(hex) - j * 3, "%02x ", payload[i + j]);
464 ascii[j] = (payload[i + j] >= 32 && payload[i + j] < 127) ? payload[i + j] : '.';
465 }
466 log_debug(" [%04x] %-48s %s", i, hex, ascii);
467 }
468
469 mutex_lock(&manager->peers_mutex);
470
471 // Find peer connection
472 peer_entry_t *peer = find_peer_locked(manager, ice->sender_id);
473 if (!peer) {
474 mutex_unlock(&manager->peers_mutex);
475 log_warn("ICE candidate for unknown peer, ignoring");
476 return ASCIICHAT_OK;
477 }
478
479 mutex_unlock(&manager->peers_mutex);
480
481 // Add remote ICE candidate
482 asciichat_error_t result = webrtc_add_remote_candidate(peer->pc, candidate, mid);
483 if (result != ASCIICHAT_OK) {
484 return SET_ERRNO(result, "Failed to add remote ICE candidate");
485 }
486
487 return ASCIICHAT_OK;
488}
489
490asciichat_error_t webrtc_peer_manager_connect(webrtc_peer_manager_t *manager, const uint8_t session_id[16],
491 const uint8_t participant_id[16]) {
492 if (!manager || !session_id || !participant_id) {
493 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
494 }
495
496 if (manager->role != WEBRTC_ROLE_JOINER) {
497 return SET_ERRNO(ERROR_INVALID_PARAM, "Only joiners can initiate connections");
498 }
499
500 log_debug("webrtc_peer_manager_connect: session_id=%02x%02x%02x%02x..., participant_id=%02x%02x%02x%02x...",
503
504 mutex_lock(&manager->peers_mutex);
505
506 // Create peer connection
507 peer_entry_t *peer;
508 asciichat_error_t result = create_peer_connection_locked(manager, session_id, participant_id, &peer);
509 if (result != ASCIICHAT_OK) {
510 mutex_unlock(&manager->peers_mutex);
511 return SET_ERRNO(result, "Failed to create peer connection");
512 }
513
514 mutex_unlock(&manager->peers_mutex);
515
516 // Note: SDP offer is automatically created by libdatachannel when rtcCreateDataChannel() is called
517 // The on_local_description callback will be triggered automatically with the offer
518 // No need to manually call webrtc_create_offer() - doing so causes "Unexpected local description" error
519
520 log_info("Initiated WebRTC connection to participant (offer auto-created by DataChannel)");
521
522 return ASCIICHAT_OK;
523}
524
526 if (!manager) {
527 return 0;
528 }
529
530 int timeout_count = 0;
531 peer_entry_t *peer = NULL, *tmp = NULL;
532
533 mutex_lock(&manager->peers_mutex);
534
535 // Iterate through all peers and check for gathering timeout
536 HASH_ITER(hh, manager->peers, peer, tmp) {
537 if (!peer || !peer->pc) {
538 continue;
539 }
540
541 // Check if this peer's ICE gathering has timed out
542 if (webrtc_is_gathering_timed_out(peer->pc, timeout_ms)) {
543 webrtc_gathering_state_t state = webrtc_get_gathering_state(peer->pc);
544
545 log_error("ICE gathering timeout for peer (participant_id=%02x%02x%02x%02x..., timeout=%ums, state=%d)",
546 peer->participant_id[0], peer->participant_id[1], peer->participant_id[2], peer->participant_id[3],
547 timeout_ms, state);
548
549 // Call timeout callback if configured
550 if (manager->config.on_gathering_timeout) {
551 manager->config.on_gathering_timeout(peer->participant_id, timeout_ms, timeout_ms, manager->config.user_data);
552 }
553
554 // Remove and close the timed-out peer connection
555 remove_peer_locked(manager, peer);
556 timeout_count++;
557
558 log_info("Closed and removed timed-out peer connection (count: %d)", timeout_count);
559 }
560 }
561
562 mutex_unlock(&manager->peers_mutex);
563
564 return timeout_count;
565}
asciichat_error_t webrtc_create_datachannel(webrtc_peer_connection_t *pc, const char *label, webrtc_data_channel_t **dc_out)
asciichat_error_t webrtc_datachannel_set_callbacks(webrtc_data_channel_t *dc, const webrtc_datachannel_callbacks_t *callbacks)
asciichat_error_t webrtc_add_remote_candidate(webrtc_peer_connection_t *pc, const char *candidate, const char *mid)
webrtc_gathering_state_t webrtc_get_gathering_state(webrtc_peer_connection_t *pc)
void webrtc_datachannel_destroy(webrtc_data_channel_t *dc)
asciichat_error_t webrtc_create_peer_connection(const webrtc_config_t *config, webrtc_peer_connection_t **pc_out)
void webrtc_peer_connection_destroy(webrtc_peer_connection_t *pc)
asciichat_error_t webrtc_set_remote_description(webrtc_peer_connection_t *pc, const char *sdp, const char *type)
bool webrtc_is_gathering_timed_out(webrtc_peer_connection_t *pc, uint32_t timeout_ms)
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)
asciichat_error_t webrtc_peer_manager_handle_ice(webrtc_peer_manager_t *manager, const acip_webrtc_ice_t *ice)
asciichat_error_t webrtc_peer_manager_handle_sdp(webrtc_peer_manager_t *manager, const acip_webrtc_sdp_t *sdp)
void webrtc_peer_manager_destroy(webrtc_peer_manager_t *manager)
struct webrtc_peer_manager webrtc_peer_manager_t
WebRTC peer manager structure.
asciichat_error_t webrtc_peer_manager_connect(webrtc_peer_manager_t *manager, const uint8_t session_id[16], const uint8_t participant_id[16])
int webrtc_peer_manager_check_gathering_timeouts(webrtc_peer_manager_t *manager, uint32_t timeout_ms)
uint8_t session_id[16]
uint8_t participant_id[16]
Per-peer connection state.
webrtc_peer_connection_t * pc
WebRTC peer connection.
uint8_t participant_id[16]
Remote participant UUID (hash key)
bool is_connected
DataChannel opened.
struct webrtc_peer_manager * manager
Back-reference to manager.
UT_hash_handle hh
uthash handle
uint8_t session_id[16]
Session UUID.
webrtc_data_channel_t * dc
WebRTC data channel.
WebRTC peer manager structure.
webrtc_peer_role_t role
Session role.
mutex_t peers_mutex
Protect peers hash table.
peer_entry_t * peers
Hash table of peer connections.
webrtc_peer_manager_config_t config
Manager configuration.
webrtc_signaling_callbacks_t signaling
Signaling callbacks.
void acip_transport_destroy(acip_transport_t *transport)
int mutex_init(mutex_t *mutex)
Definition threading.c:16
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21
acip_transport_t * acip_webrtc_transport_create(webrtc_peer_connection_t *peer_conn, webrtc_data_channel_t *data_channel, crypto_context_t *crypto_ctx)