ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
lib/network/webrtc/webrtc.c
Go to the documentation of this file.
1
16#include <ascii-chat/common.h>
17#include <ascii-chat/log/log.h>
22#include <ascii-chat/atomic.h>
23
24#include <string.h>
25#include <rtc/rtc.h>
26
27// ============================================================================
28// Internal Structures
29// ============================================================================
30
45
53 int rtc_id;
56
57 // Per-channel callbacks (set via webrtc_datachannel_set_callbacks)
60 void (*user_on_error)(webrtc_data_channel_t *dc, const char *error, void *user_data);
61 void (*user_on_message)(webrtc_data_channel_t *dc, const uint8_t *data, size_t len,
62 void *user_data);
63 void *user_data;
64};
65
66// ============================================================================
67// Global State
68// ============================================================================
69
70// WebRTC library lifecycle (one-time init/cleanup, no refcounting needed)
71static lifecycle_t g_webrtc_lc = LIFECYCLE_INIT;
72
73// ============================================================================
74// libdatachannel Callback Adapters
75// ============================================================================
76
77static void rtc_log_callback(rtcLogLevel level, const char *message) {
78 switch (level) {
79 case RTC_LOG_FATAL:
80 case RTC_LOG_ERROR:
81 log_error("[libdatachannel] %s", message);
82 break;
83 case RTC_LOG_WARNING:
84 log_warn("[libdatachannel] %s", message);
85 break;
86 case RTC_LOG_INFO:
87 log_info("[libdatachannel] %s", message);
88 break;
89 case RTC_LOG_DEBUG:
90 case RTC_LOG_VERBOSE:
91 log_debug("[libdatachannel] %s", message);
92 break;
93 default:
94 break;
95 }
96}
97
98// Forward declarations for callback functions
99static void on_datachannel_open_adapter(int dc_id, void *user_data);
100static void on_datachannel_closed_adapter(int dc_id, void *user_data);
101static void on_datachannel_message_adapter(int dc_id, const char *data, int size, void *user_data);
102static void on_datachannel_error_adapter(int dc_id, const char *error, void *user_data);
103
104static void on_state_change_adapter(int pc_id, rtcState state, void *user_data) {
106 if (!pc)
107 return;
108
109 // Map libdatachannel state to our enum
110 webrtc_state_t new_state;
111 switch (state) {
112 case RTC_NEW:
113 new_state = WEBRTC_STATE_NEW;
114 break;
115 case RTC_CONNECTING:
116 new_state = WEBRTC_STATE_CONNECTING;
117 break;
118 case RTC_CONNECTED:
119 new_state = WEBRTC_STATE_CONNECTED;
120 char local_candidate[512], remote_candidate[512];
121 if (rtcGetSelectedCandidatePair(pc_id, local_candidate, sizeof(local_candidate), remote_candidate,
122 sizeof(remote_candidate)) >= 0) {
123 log_info("WebRTC selected local candidate: %s", local_candidate);
124 log_info("WebRTC selected remote candidate: %s", remote_candidate);
125 }
126 break;
127 case RTC_DISCONNECTED:
128 new_state = WEBRTC_STATE_DISCONNECTED;
129 break;
130 case RTC_FAILED:
131 new_state = WEBRTC_STATE_FAILED;
132 break;
133 case RTC_CLOSED:
134 new_state = WEBRTC_STATE_CLOSED;
135 break;
136 default:
137 new_state = WEBRTC_STATE_NEW;
138 break;
139 }
140
141 atomic_store_u64(&pc->state, (uint64_t)new_state);
142
143 if (pc->config.on_state_change) {
144 pc->config.on_state_change(pc, new_state, pc->config.user_data);
145 }
146}
147
148static void on_gathering_state_change_adapter(int pc_id, rtcGatheringState state, void *user_data) {
149 (void)pc_id; // Unused - we get peer connection from user_data
151 if (!pc)
152 return;
153
154 // Map libdatachannel gathering state to our enum
155 webrtc_gathering_state_t new_state;
156 switch (state) {
157 case RTC_GATHERING_NEW:
158 new_state = WEBRTC_GATHERING_NEW;
159 break;
160 case RTC_GATHERING_INPROGRESS:
161 new_state = WEBRTC_GATHERING_GATHERING;
162 // Record start time when gathering begins
164 uint64_t start_time_ms = platform_get_monotonic_time_us() / 1000;
165 atomic_store_u64(&pc->gathering_start_time_ms, start_time_ms);
166 log_debug("ICE gathering started at %llu ms", (unsigned long long)start_time_ms);
167 }
168 break;
169 case RTC_GATHERING_COMPLETE:
170 new_state = WEBRTC_GATHERING_COMPLETE;
171 uint64_t gathering_start_time_ms = atomic_load_u64(&pc->gathering_start_time_ms);
172 if (gathering_start_time_ms > 0) {
173 uint64_t duration = (platform_get_monotonic_time_us() / 1000) - gathering_start_time_ms;
174 log_info("ICE gathering completed in %llu ms", (unsigned long long)duration);
175 }
176 break;
177 default:
178 new_state = WEBRTC_GATHERING_NEW;
179 break;
180 }
181
182 atomic_store_u64(&pc->gathering_state, (uint64_t)new_state);
183
185 pc->config.on_gathering_state_change(pc, new_state, pc->config.user_data);
186 }
187}
188
189static void on_local_description_adapter(int pc_id, const char *sdp, const char *type, void *user_data) {
190 (void)pc_id; // Unused - we get peer connection from user_data
192 if (!pc)
193 return;
194
196 pc->config.on_local_description(pc, sdp, type, pc->config.user_data);
197 }
198}
199
200static void on_local_candidate_adapter(int pc_id, const char *candidate, const char *mid, void *user_data) {
201 (void)pc_id; // Unused - we get peer connection from user_data
203 if (!pc)
204 return;
205
206 if (pc->config.on_local_candidate) {
207 pc->config.on_local_candidate(pc, candidate, mid, pc->config.user_data);
208 }
209}
210
211static void on_datachannel_adapter(int pc_id, int dc_id, void *user_data) {
212 (void)pc_id; // Unused - we get peer connection from user_data
213 log_info("on_datachannel_adapter: pc_id=%d, dc_id=%d, user_data=%p", pc_id, dc_id, user_data);
214
216 if (!pc) {
217 log_error("on_datachannel_adapter: peer connection user_data is NULL!");
218 return;
219 }
220
221 log_info("on_datachannel_adapter: received DataChannel (dc_id=%d) from remote peer", dc_id);
222
223 // Allocate data channel wrapper
225 if (!dc) {
226 log_error("Failed to allocate data channel wrapper");
227 rtcDeleteDataChannel(dc_id);
228 return;
229 }
230
231 dc->rtc_id = dc_id;
232 dc->pc = pc;
233 atomic_store_bool(&dc->is_open, false);
234 log_debug("Initialized DataChannel wrapper: dc=%p, is_open=false", (void *)dc);
235
236 // Store in peer connection
237 pc->dc = dc;
238
239 // Set up data channel callbacks for incoming channel
240 rtcSetUserPointer(dc_id, dc);
241 rtcSetOpenCallback(dc_id, on_datachannel_open_adapter);
242 rtcSetMessageCallback(dc_id, on_datachannel_message_adapter);
243 rtcSetErrorCallback(dc_id, on_datachannel_error_adapter);
244
245 log_info("Set up callbacks for incoming DataChannel (dc_id=%d, dc=%p)", dc_id, (void *)dc);
246
247 // Check if DataChannel is already open (can happen if it opened before callbacks were set)
248 // In libdatachannel, negotiated DataChannels may be open immediately when received
249 // We manually trigger the open callback if that's the case
250 if (rtcIsOpen(dc_id)) {
251 log_info("DataChannel was already open when received, manually triggering open callback");
252 on_datachannel_open_adapter(dc_id, dc);
253 }
254}
255
256static void on_datachannel_open_adapter(int dc_id, void *user_data) {
258 log_info("on_datachannel_open_adapter called: dc_id=%d, dc=%p", dc_id, (void *)dc);
259 if (!dc) {
260 log_error("on_datachannel_open_adapter: dc is NULL!");
261 return;
262 }
263
264 atomic_store_bool(&dc->is_open, true);
265 log_info("DataChannel opened (id=%d, dc=%p), set is_open=true", dc_id, (void *)dc);
266
267 // Check per-channel callback first, then fall back to peer connection callback
268 if (dc->user_on_open) {
269 log_debug("Calling per-channel on_open callback");
270 dc->user_on_open(dc, dc->user_data);
271 } else if (dc->pc && dc->pc->config.on_datachannel_open) {
272 log_debug("Calling user on_datachannel_open callback");
274 } else {
275 log_warn("No user on_datachannel_open callback registered");
276 }
277}
278
279static void on_datachannel_closed_adapter(int dc_id, void *user_data) {
281 log_info("on_datachannel_closed_adapter called: dc_id=%d, dc=%p", dc_id, (void *)dc);
282 if (!dc) {
283 log_error("on_datachannel_closed_adapter: dc is NULL!");
284 return;
285 }
286
287 atomic_store_bool(&dc->is_open, false);
288 log_info("DataChannel closed (id=%d, dc=%p), set is_open=false", dc_id, (void *)dc);
289
290 // Check per-channel callback first
291 if (dc->user_on_close) {
292 log_debug("Calling per-channel on_close callback");
293 dc->user_on_close(dc, dc->user_data);
294 }
295}
296
297static void on_datachannel_message_adapter(int dc_id, const char *data, int size, void *user_data) {
298 (void)dc_id; // Unused - we get data channel from user_data
299
300 // Log at libdatachannel callback level (debug level for normal operation)
301 if (size >= 20 && data) {
302 const uint8_t *pkt = (const uint8_t *)data;
303 log_dev("★ LIBDATACHANNEL_RX: dc_id=%d, size=%d, first_20_bytes: %02x%02x%02x%02x %02x%02x%02x%02x "
304 "%02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x",
305 dc_id, size, pkt[0], pkt[1], pkt[2], pkt[3], pkt[4], pkt[5], pkt[6], pkt[7], pkt[8], pkt[9], pkt[10],
306 pkt[11], pkt[12], pkt[13], pkt[14], pkt[15], pkt[16], pkt[17], pkt[18], pkt[19]);
307 } else {
308 log_dev("★ LIBDATACHANNEL_RX: dc_id=%d, size=%d (no data or too small)", dc_id, size);
309 }
310
312 if (!dc) {
313 log_debug("★ LIBDATACHANNEL_RX: dc=NULL, dropping message");
314 return;
315 }
316
317 // Check per-channel callback first, then fall back to peer connection callback
318 if (dc->user_on_message) {
319 dc->user_on_message(dc, (const uint8_t *)data, (size_t)size, dc->user_data);
320 } else if (dc->pc && dc->pc->config.on_datachannel_message) {
321 dc->pc->config.on_datachannel_message(dc, (const uint8_t *)data, (size_t)size, dc->pc->config.user_data);
322 }
323}
324
325static void on_datachannel_error_adapter(int dc_id, const char *error, void *user_data) {
327 if (!dc)
328 return;
329
330 log_error("DataChannel error (id=%d): %s", dc_id, error);
331
332 // Check per-channel callback first, then fall back to peer connection callback
333 if (dc->user_on_error) {
334 dc->user_on_error(dc, error, dc->user_data);
335 } else if (dc->pc && dc->pc->config.on_datachannel_error) {
336 dc->pc->config.on_datachannel_error(dc, error, dc->pc->config.user_data);
337 }
338}
339
340// ============================================================================
341// Initialization and Cleanup
342// ============================================================================
343
351static asciichat_error_t webrtc_ensure_initialized(void) {
352 if (!lifecycle_init(&g_webrtc_lc, "webrtc")) {
353 return ASCIICHAT_OK; // Already initialized
354 }
355
356 // First initialization - call library initialization exactly once
357 rtcInitLogger(log_get_level() <= LOG_DEV ? RTC_LOG_VERBOSE : RTC_LOG_INFO, rtc_log_callback);
358 rtcPreload();
359
360 log_debug("WebRTC library initialized (libdatachannel)");
361 return ASCIICHAT_OK;
362}
363
370static void webrtc_release(void) {
371 if (lifecycle_shutdown(&g_webrtc_lc)) {
372 rtcCleanup();
373 log_info("WebRTC library cleaned up");
374 }
375}
376
378 return webrtc_ensure_initialized();
379}
380
381void webrtc_destroy(void) {
382 webrtc_release();
383}
384
385// ============================================================================
386// Peer Connection Management
387// ============================================================================
388
389// libdatachannel's C API accepts TURN credentials as URL userinfo.
390static void encode_turn_userinfo(const char *value, char *encoded, size_t size) {
391 static const char hex[] = "0123456789ABCDEF";
392 size_t out = 0;
393 for (size_t i = 0; value[i] && out + 3 < size; i++) {
394 unsigned char c = (unsigned char)value[i];
395 encoded[out++] = '%';
396 encoded[out++] = hex[c >> 4];
397 encoded[out++] = hex[c & 15];
398 }
399 encoded[out] = '\0';
400}
401
403 if (!config || !pc_out) {
404 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid config or output parameter");
405 }
406 *pc_out = NULL;
407 if ((config->stun_count && !config->stun_servers) || (config->turn_count && !config->turn_servers)) {
408 return SET_ERRNO(ERROR_INVALID_PARAM, "ICE server counts require server arrays");
409 }
410 if ((config->turn_username != NULL) != (config->turn_credential != NULL)) {
411 return SET_ERRNO(ERROR_INVALID_PARAM, "TURN overrides require both username and credential");
412 }
413
414 if (!lifecycle_is_initialized(&g_webrtc_lc)) {
415 return SET_ERRNO(ERROR_INIT, "WebRTC library not initialized");
416 }
417 if (config->relay_only && config->turn_count == 0) {
418 return SET_ERRNO(ERROR_INVALID_PARAM, "Relay only requires a TURN server with credentials");
419 }
420 for (size_t i = 0; i < config->turn_count; i++) {
421 const turn_server_t *server = &config->turn_servers[i];
422 const char *username = config->turn_username ? config->turn_username : server->username;
423 const char *credential = config->turn_credential ? config->turn_credential : server->credential;
424 if (!username[0] || !credential[0] || strlen(username) > 127 || strlen(credential) > 127) {
425 return SET_ERRNO(ERROR_INVALID_PARAM, "TURN server requires both username and credential");
426 }
427 if (strncmp(server->url, "turn:", 5) != 0 && strncmp(server->url, "turns:", 6) != 0) {
428 return SET_ERRNO(ERROR_INVALID_PARAM, "TURN server URL must start with turn: or turns:");
429 }
430 }
431
432 // Allocate peer connection wrapper
434 if (!pc) {
435 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate peer connection");
436 }
437
438 pc->config = *config; // Copy config
440 pc->dc = NULL;
441
442 // Build ICE server list for libdatachannel
443 const char **ice_servers = NULL;
444 size_t ice_count = 0;
445 char *turn_urls = NULL;
446
447 if (config->stun_count > 0 || config->turn_count > 0) {
448 ice_count = config->stun_count + config->turn_count;
449 ice_servers = SAFE_MALLOC(ice_count * sizeof(char *), const char **);
450 if (!ice_servers) {
451 SAFE_FREE(pc);
452 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ICE server list");
453 }
454
455 // Add STUN servers
456 for (size_t i = 0; i < config->stun_count; i++) {
457 ice_servers[i] = config->stun_servers[i].host;
458 }
459
460 // Add TURN servers
461 if (config->turn_count > 0) {
462 turn_urls = SAFE_CALLOC(config->turn_count, 1024, char *);
463 if (!turn_urls) {
464 SAFE_FREE(ice_servers);
465 SAFE_FREE(pc);
466 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate authenticated TURN URLs");
467 }
468 }
469 for (size_t i = 0; i < config->turn_count; i++) {
470 const turn_server_t *server = &config->turn_servers[i];
471 char username[128 * 3];
472 char credential[128 * 3];
473 encode_turn_userinfo(config->turn_username ? config->turn_username : server->username, username,
474 sizeof(username));
475 encode_turn_userinfo(config->turn_credential ? config->turn_credential : server->credential, credential,
476 sizeof(credential));
477 const char *address = strchr(server->url, ':') + 1;
478 while (*address == '/')
479 address++;
480 SAFE_SNPRINTF(turn_urls + i * 1024, 1024, "%.*s%s:%s@%s", (int)(strchr(server->url, ':') - server->url + 1),
481 server->url, username, credential, address);
482 ice_servers[config->stun_count + i] = turn_urls + i * 1024;
483 }
484 }
485
486 // Create libdatachannel configuration
487 rtcConfiguration rtc_config;
488 memset(&rtc_config, 0, sizeof(rtc_config));
489 rtc_config.bindAddress = config->bind_address;
490 rtc_config.iceServers = ice_servers;
491 rtc_config.iceServersCount = (int)ice_count;
492 rtc_config.iceTransportPolicy = config->relay_only ? RTC_TRANSPORT_POLICY_RELAY : RTC_TRANSPORT_POLICY_ALL;
493 if (rtc_config.bindAddress && rtc_config.bindAddress[0]) {
494 log_info("Binding WebRTC ICE sockets to %s", rtc_config.bindAddress);
495 }
496
497 // Create peer connection
498 int pc_id = rtcCreatePeerConnection(&rtc_config);
499 SAFE_FREE(turn_urls);
500
501 // Free ICE server list (libdatachannel makes a copy)
502 if (ice_servers) {
503 SAFE_FREE(ice_servers);
504 }
505
506 if (pc_id < 0) {
507 SAFE_FREE(pc);
508 return SET_ERRNO(ERROR_NETWORK, "Failed to create peer connection (rtc error %d)", pc_id);
509 }
510
511 pc->rtc_id = pc_id;
512
513 // Initialize gathering state
516
517 // Set up callbacks
518 rtcSetUserPointer(pc_id, pc);
519 rtcSetStateChangeCallback(pc_id, on_state_change_adapter);
520 rtcSetGatheringStateChangeCallback(pc_id, on_gathering_state_change_adapter);
521 rtcSetLocalDescriptionCallback(pc_id, on_local_description_adapter);
522 rtcSetLocalCandidateCallback(pc_id, on_local_candidate_adapter);
523 rtcSetDataChannelCallback(pc_id, on_datachannel_adapter);
524
525 /* Register peer connection with named registry */
526 char pc_name[64];
527 snprintf(pc_name, sizeof(pc_name), "webrtc_peer_connection:%d", pc_id);
528 NAMED_REGISTER(pc, pc_name, "webrtc_peer_connection_t", "0x%tx", NULL);
529
530 *pc_out = pc;
531 log_debug("Created WebRTC peer connection (id=%d)", pc_id);
532 return ASCIICHAT_OK;
533}
534
536 if (!pc) {
537 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid peer connection");
538 return;
539 }
540
541 // Close data channel if exists
542 if (pc->dc) {
544 pc->dc = NULL;
545 }
546
547 // Close peer connection
548 rtcDeletePeerConnection(pc->rtc_id);
549 log_debug("Closed WebRTC peer connection (id=%d)", pc->rtc_id);
550
551 SAFE_FREE(pc);
552}
553
555 if (!pc) {
556 log_warn("Peer connection is NULL");
557 return WEBRTC_STATE_CLOSED;
558 }
560}
561
569
571 if (!pc) {
572 return false;
573 }
574
575 // Only check timeout if we're actively gathering
577 return false;
578 }
579
580 // If gathering hasn't started yet (start_time == 0), no timeout
581 uint64_t gathering_start_time_ms = atomic_load_u64(&pc->gathering_start_time_ms);
582 if (gathering_start_time_ms == 0) {
583 return false;
584 }
585
586 // Check if elapsed time exceeds timeout
587 uint64_t current_time_ms = platform_get_monotonic_time_us() / 1000;
588 uint64_t elapsed_ms = current_time_ms - gathering_start_time_ms;
589
590 if (elapsed_ms > timeout_ms) {
591 return true;
592 }
593
594 return false;
595}
596
598 if (!pc) {
599 log_warn("Peer connection is NULL");
600 return NULL;
601 }
602 return pc->config.user_data;
603}
604
605// ============================================================================
606// SDP Offer/Answer Exchange
607// ============================================================================
608
610 if (!pc) {
611 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid peer connection");
612 }
613
614 // Set local description with NULL type to trigger offer generation
615 int result = rtcSetLocalDescription(pc->rtc_id, NULL);
616 if (result != RTC_ERR_SUCCESS) {
617 return SET_ERRNO(ERROR_NETWORK, "Failed to create SDP offer (rtc error %d)", result);
618 }
619
620 log_debug("Creating SDP offer (pc_id=%d)", pc->rtc_id);
621 return ASCIICHAT_OK;
622}
623
625 if (!pc || !sdp || !type) {
626 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
627 }
628
629 int result = rtcSetRemoteDescription(pc->rtc_id, sdp, type);
630 if (result != RTC_ERR_SUCCESS) {
631 return SET_ERRNO(ERROR_NETWORK, "Failed to set remote SDP (rtc error %d)", result);
632 }
633
634 const char *dcmap = strstr(sdp, "a=dcmap:");
635 if (strcmp(type, "offer") == 0) {
636 log_debug("Remote WebRTC offer DataChannel map: %s", dcmap ? dcmap : "(none)");
637 }
638 if (strcmp(type, "offer") == 0 && strstr(sdp, "\na=dcmap:0 label=\"acip\"") && !pc->dc) {
639 rtcDataChannelInit init = {0};
640 init.negotiated = true;
641 init.manualStream = true;
642 init.stream = 0;
643 int dc_id = rtcCreateDataChannelEx(pc->rtc_id, "acip", &init);
644 if (dc_id < 0)
645 return SET_ERRNO(ERROR_NETWORK, "Failed to create negotiated ACIP channel");
646 on_datachannel_adapter(pc->rtc_id, dc_id, pc);
647 }
648
649 log_debug("Set remote SDP description (pc_id=%d, type=%s)", pc->rtc_id, type);
650 return ASCIICHAT_OK;
651}
652
653// ============================================================================
654// ICE Candidate Exchange
655// ============================================================================
656
657asciichat_error_t webrtc_add_remote_candidate(webrtc_peer_connection_t *pc, const char *candidate, const char *mid) {
658 if (!pc || !candidate) {
659 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
660 }
661
662 log_debug(" [4] Before libdatachannel - candidate: '%s' (len=%zu)", candidate, strlen(candidate));
663 log_debug(" [4] Before libdatachannel - mid: '%s' (len=%zu)", mid ? mid : "(null)", mid ? strlen(mid) : 0);
664
665 int result = rtcAddRemoteCandidate(pc->rtc_id, candidate, mid);
666 if (result != RTC_ERR_SUCCESS) {
667 return SET_ERRNO(ERROR_NETWORK, "Failed to add remote ICE candidate (rtc error %d)", result);
668 }
669
670 log_debug("Added remote ICE candidate (pc_id=%d)", pc->rtc_id);
671 return ASCIICHAT_OK;
672}
673
674// ============================================================================
675// DataChannel Management
676// ============================================================================
677
679 webrtc_data_channel_t **dc_out) {
680 if (!pc || !label || !dc_out) {
681 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
682 }
683
684 // Native peers use DCEP. Browser offers advertise the separately negotiated
685 // ACIP channel and are handled in webrtc_set_remote_description().
686 int dc_id = rtcCreateDataChannel(pc->rtc_id, label);
687 if (dc_id < 0) {
688 return SET_ERRNO(ERROR_NETWORK, "Failed to create data channel (rtc error %d)", dc_id);
689 }
690
691 // Allocate wrapper
693 if (!dc) {
694 rtcDeleteDataChannel(dc_id);
695 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate data channel wrapper");
696 }
697
698 dc->rtc_id = dc_id;
699 dc->pc = pc;
700 atomic_store_bool(&dc->is_open, false);
701
702 // Set up callbacks
703 rtcSetUserPointer(dc_id, dc);
704 rtcSetOpenCallback(dc_id, on_datachannel_open_adapter);
705 rtcSetMessageCallback(dc_id, on_datachannel_message_adapter);
706 rtcSetErrorCallback(dc_id, on_datachannel_error_adapter);
707
708 pc->dc = dc;
709 *dc_out = dc;
710
711 // Register data channel with debug naming system
712 NAMED_REGISTER_DATACHANNEL(dc, label, NULL);
713
714 log_debug("Created DataChannel '%s' (dc_id=%d, pc_id=%d)", label, dc_id, pc->rtc_id);
715 return ASCIICHAT_OK;
716}
717
719 if (!dc || dc->rtc_id < 0 || !amount)
720 return SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel and amount are required");
721 int result = rtcGetBufferedAmount(dc->rtc_id);
722 if (result < 0)
723 return SET_ERRNO(ERROR_NETWORK, "Failed to query DataChannel buffered amount");
724 *amount = (size_t)result;
725 return ASCIICHAT_OK;
726}
727
729 if (!dc || dc->rtc_id < 0 || !size)
730 return SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel and max message size are required");
731 int result = rtcMaxMessageSize(dc->rtc_id);
732 if (result <= 0)
733 return SET_ERRNO(ERROR_NETWORK, "Failed to query DataChannel max message size");
734 *size = (size_t)result;
735 return ASCIICHAT_OK;
736}
737
739 if (!dc || dc->rtc_id < 0 || !data) {
740 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
741 }
742
743 if (!atomic_load_bool(&dc->is_open)) {
744 log_error("★ WEBRTC_DATACHANNEL_SEND: Channel not open! size=%zu, dc->rtc_id=%d", size, dc ? dc->rtc_id : -1);
745 return SET_ERRNO(ERROR_NETWORK, "DataChannel not open");
746 }
747
748 // Log packet details at network layer (debug level for normal operation)
749 if (size >= 20) {
750 const uint8_t *pkt = (const uint8_t *)data;
751 log_dev("★ RTCSENDMESSAGE_BEFORE: dc_id=%d, size=%zu, first_20_bytes: %02x%02x%02x%02x %02x%02x%02x%02x "
752 "%02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x",
753 dc->rtc_id, size, pkt[0], pkt[1], pkt[2], pkt[3], pkt[4], pkt[5], pkt[6], pkt[7], pkt[8], pkt[9], pkt[10],
754 pkt[11], pkt[12], pkt[13], pkt[14], pkt[15], pkt[16], pkt[17], pkt[18], pkt[19]);
755 } else {
756 log_dev("★ RTCSENDMESSAGE_BEFORE: dc_id=%d, size=%zu (too small to log content)", dc->rtc_id, size);
757 }
758
759 int result = rtcSendMessage(dc->rtc_id, (const char *)data, (int)size);
760
761 log_dev("★ RTCSENDMESSAGE_AFTER: dc_id=%d, rtcSendMessage returned %d for size=%zu", dc->rtc_id, result, size);
762
763 if (result < 0) {
764 log_error("★ WEBRTC_DATACHANNEL_SEND: FAILED with error code %d", result);
765 return SET_ERRNO(ERROR_NETWORK, "Failed to send data (rtc error %d)", result);
766 }
767
768 log_dev("★ WEBRTC_DATACHANNEL_SEND: SUCCESS - sent %zu bytes", size);
769 return ASCIICHAT_OK;
770}
771
773 if (!dc) {
774 SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
775 return false;
776 }
777 return dc->rtc_id >= 0 && atomic_load_bool(&dc->is_open);
778}
779
781 if (!dc) {
782 SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
783 return;
784 }
785 atomic_store_bool(&dc->is_open, is_open);
786}
787
789 if (!dc || dc->rtc_id < 0) {
790 SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
791 return NULL;
792 }
793
794 static char label[256];
795 int result = rtcGetDataChannelLabel(dc->rtc_id, label, sizeof(label));
796 if (result < 0) {
797 return NULL;
798 }
799
800 return label;
801}
802
804 if (!dc) {
805 SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
806 return;
807 }
808 if (dc->rtc_id < 0) {
809 atomic_store_bool(&dc->is_open, false);
810 return;
811 }
812
813 // Save the ID before deleting the native channel.
814 int dc_id = dc->rtc_id;
815
816 rtcDeleteDataChannel(dc_id);
817 log_debug("Closed DataChannel (dc_id=%d)", dc_id);
818 dc->rtc_id = -1;
819 atomic_store_bool(&dc->is_open, false);
820}
821
822// =============================================================================
823// DataChannel Callbacks
824// =============================================================================
825
827 const webrtc_datachannel_callbacks_t *callbacks) {
828 if (!dc) {
829 return SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
830 }
831
832 if (!callbacks) {
833 return SET_ERRNO(ERROR_INVALID_PARAM, "Callbacks struct is NULL");
834 }
835
836 // Store user callbacks in the data channel struct
837 dc->user_on_open = callbacks->on_open;
838 dc->user_on_close = callbacks->on_close;
839 dc->user_on_error = callbacks->on_error;
840 dc->user_on_message = callbacks->on_message;
841 dc->user_data = callbacks->user_data;
842
843 // Register adapter functions with libdatachannel (adapters use correct signature)
844 // The adapters will look up our stored callbacks and invoke them with proper types
845 if (callbacks->on_open) {
846 rtcSetOpenCallback(dc->rtc_id, on_datachannel_open_adapter);
847 }
848
849 if (callbacks->on_close) {
850 rtcSetClosedCallback(dc->rtc_id, on_datachannel_closed_adapter);
851 }
852
853 if (callbacks->on_error) {
854 rtcSetErrorCallback(dc->rtc_id, on_datachannel_error_adapter);
855 }
856
857 if (callbacks->on_message) {
858 rtcSetMessageCallback(dc->rtc_id, on_datachannel_message_adapter);
859 }
860
861 // Set user pointer to the data channel so adapters can retrieve it
862 rtcSetUserPointer(dc->rtc_id, dc);
863
864 log_debug("Set DataChannel callbacks (dc_id=%d)", dc->rtc_id);
865 return ASCIICHAT_OK;
866}
867
869 if (!dc) {
870 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid null data channel");
871 return;
872 }
873
874 // Close if still open, then free the wrapper.
876 SAFE_FREE(dc);
877}
878
879// =============================================================================
880// Peer Connection Lifecycle
881// =============================================================================
882
884 if (!pc) {
885 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid null peer connectiont");
886 return;
887 }
888
889 rtcClose(pc->rtc_id);
890 log_debug("Closed peer connection (pc_id=%d)", pc->rtc_id);
891}
892
894 if (!pc) {
895 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid null peer connectiont");
896 return;
897 }
898
899 // Close and delete peer connection
900 rtcDeletePeerConnection(pc->rtc_id);
901 log_debug("Destroyed peer connection (pc_id=%d)", pc->rtc_id);
902
903 SAFE_FREE(pc);
904}
905
916 if (!pc) {
917 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid null peer connectiont");
918 return -1;
919 }
920 return pc->rtc_id;
921}
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
⚛️ Atomic operations abstraction layer with debug tracking
⚙️ Common definitions, error codes, macros, and types shared throughout the application
Named object registry for debugging — log identifiable resource names.
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_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
unsigned char uint8_t
Definition common.h:56
#define NAMED_REGISTER(ptr, name, type, fmt, parent_ptr)
Register any pointer with base name, type, format spec, and location (auto-suffix)
#define NAMED_REGISTER_DATACHANNEL(datachannel, name, parent_ptr)
Register a data channel with automatic format specifier.
#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_INIT
Definition error_codes.h:61
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
log_level_t log_get_level(void)
Get the current minimum log level.
Definition log/log.c:696
#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
uint64_t platform_get_monotonic_time_us(void)
Get monotonic time in microseconds.
turn_server_t
Definition turn.h:101
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)
void webrtc_close_peer_connection(webrtc_peer_connection_t *pc)
Close and destroy a peer connection.
asciichat_error_t webrtc_create_offer(webrtc_peer_connection_t *pc)
Create and set local SDP offer (for connection initiator)
asciichat_error_t webrtc_datachannel_set_callbacks(webrtc_data_channel_t *dc, const webrtc_datachannel_callbacks_t *callbacks)
Set DataChannel callbacks.
int webrtc_get_rtc_id(webrtc_peer_connection_t *pc)
Get the internal libdatachannel peer connection ID.
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.
asciichat_error_t webrtc_init(void)
Initialize WebRTC library (libdatachannel)
asciichat_error_t webrtc_datachannel_get_max_message_size(webrtc_data_channel_t *dc, size_t *size)
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.
const char * webrtc_datachannel_get_label(webrtc_data_channel_t *dc)
Get DataChannel label.
webrtc_gathering_state_t
ICE gathering state.
void * webrtc_get_user_data(webrtc_peer_connection_t *pc)
Get user data pointer from connection.
asciichat_error_t webrtc_datachannel_get_buffered_amount(webrtc_data_channel_t *dc, size_t *amount)
void webrtc_peer_connection_close(webrtc_peer_connection_t *pc)
Close a peer connection.
void webrtc_destroy(void)
Cleanup WebRTC library resources.
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.
void webrtc_close_datachannel(webrtc_data_channel_t *dc)
Close a DataChannel.
bool webrtc_datachannel_is_open(webrtc_data_channel_t *dc)
Check if DataChannel is open and ready.
asciichat_error_t webrtc_datachannel_send(webrtc_data_channel_t *dc, const uint8_t *data, size_t size)
Send data over DataChannel.
bool webrtc_is_gathering_timed_out(webrtc_peer_connection_t *pc, uint32_t timeout_ms)
Check if ICE gathering has timed out.
void webrtc_datachannel_set_open_state(webrtc_data_channel_t *dc, bool is_open)
Set DataChannel open state (internal use)
@ WEBRTC_GATHERING_COMPLETE
All candidates gathered.
@ WEBRTC_GATHERING_GATHERING
Gathering candidates.
@ WEBRTC_GATHERING_NEW
Not started.
@ 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.
Platform initialization and static synchronization helpers.
bool lifecycle_is_initialized(const lifecycle_t *lc)
Definition lifecycle.c:155
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
📝 Logging API with multiple log levels and terminal output control
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
webrtc_datachannel_message_callback_t on_datachannel_message
webrtc_local_description_callback_t on_local_description
const char * turn_username
Optional credential override, including full ACDS usernames.
webrtc_local_candidate_callback_t on_local_candidate
size_t stun_count
Number of STUN servers.
webrtc_gathering_state_callback_t on_gathering_state_change
bool relay_only
Restrict ICE to TURN relay candidates.
webrtc_datachannel_error_callback_t on_datachannel_error
turn_server_t * turn_servers
Array of TURN servers.
stun_server_t * stun_servers
Array of STUN servers.
void * user_data
Passed to all callbacks.
size_t turn_count
Number of TURN servers.
const char * bind_address
Optional local interface address for ICE sockets.
const char * turn_credential
Optional credential override.
webrtc_datachannel_open_callback_t on_datachannel_open
WebRTC data channel for sending/receiving messages.
void(* user_on_message)(webrtc_data_channel_t *dc, const uint8_t *data, size_t len, void *user_data)
Message callback.
void(* user_on_close)(webrtc_data_channel_t *dc, void *user_data)
Close callback.
void(* user_on_open)(webrtc_data_channel_t *dc, void *user_data)
Open callback.
atomic_t is_open
Channel open state.
webrtc_peer_connection_t * pc
Parent peer connection.
int rtc_id
libdatachannel data channel ID
void * user_data
User data for per-channel callbacks.
void(* user_on_error)(webrtc_data_channel_t *dc, const char *error, void *user_data)
Error callback.
void(* on_close)(webrtc_data_channel_t *dc, void *user_data)
Channel closed.
void(* on_error)(webrtc_data_channel_t *dc, const char *error, void *user_data)
Error occurred.
void(* on_open)(webrtc_data_channel_t *dc, void *user_data)
Channel opened.
void(* on_message)(webrtc_data_channel_t *dc, const uint8_t *data, size_t len, void *user_data)
Message received.
WebRTC peer connection state.
webrtc_data_channel_t * dc
Primary data channel (if created/received)
atomic_t gathering_state
Current ICE gathering state.
int rtc_id
libdatachannel peer connection ID
atomic_t gathering_start_time_ms
When gathering started (platform_get_time_ms)
atomic_t state
Current connection state.
webrtc_config_t config
Configuration with callbacks.
Cross-platform system functions interface for ascii-chat.
#define LOG_DEV
Definition types.h:38