ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
lib/network/webrtc/webrtc.c
Go to the documentation of this file.
1
14#include <ascii-chat/network/webrtc/webrtc.h>
15#include <ascii-chat/asciichat_errno.h>
16#include <ascii-chat/common.h>
17#include <ascii-chat/log/logging.h>
18#include <ascii-chat/platform/init.h>
19#include <ascii-chat/platform/system.h>
20
21#include <string.h>
22#include <rtc/rtc.h>
23
24// ============================================================================
25// Internal Structures
26// ============================================================================
27
35 int rtc_id;
36 webrtc_config_t config;
37 webrtc_state_t state;
38 webrtc_gathering_state_t gathering_state;
40 webrtc_data_channel_t *dc;
41};
42
50 int rtc_id;
51 webrtc_peer_connection_t *pc;
52 bool is_open;
53
54 // Per-channel callbacks (set via webrtc_datachannel_set_callbacks)
55 void (*user_on_open)(webrtc_data_channel_t *dc, void *user_data);
56 void (*user_on_close)(webrtc_data_channel_t *dc, void *user_data);
57 void (*user_on_error)(webrtc_data_channel_t *dc, const char *error, void *user_data);
58 void (*user_on_message)(webrtc_data_channel_t *dc, const uint8_t *data, size_t len,
59 void *user_data);
60 void *user_data;
61};
62
63// ============================================================================
64// Global State
65// ============================================================================
66
67// WebRTC library reference counting (same pattern as PortAudio)
68// Supports multiple init/cleanup pairs from different code paths
69static unsigned int g_webrtc_init_refcount = 0;
70static static_mutex_t g_webrtc_refcount_mutex = STATIC_MUTEX_INIT;
71
72// ============================================================================
73// libdatachannel Callback Adapters
74// ============================================================================
75
76static void rtc_log_callback(rtcLogLevel level, const char *message) {
77 switch (level) {
78 case RTC_LOG_FATAL:
79 case RTC_LOG_ERROR:
80 log_error("[libdatachannel] %s", message);
81 break;
82 case RTC_LOG_WARNING:
83 log_warn("[libdatachannel] %s", message);
84 break;
85 case RTC_LOG_INFO:
86 log_info("[libdatachannel] %s", message);
87 break;
88 case RTC_LOG_DEBUG:
89 case RTC_LOG_VERBOSE:
90 log_debug("[libdatachannel] %s", message);
91 break;
92 default:
93 break;
94 }
95}
96
97// Forward declarations for callback functions
98static void on_datachannel_open_adapter(int dc_id, void *user_data);
99static void on_datachannel_closed_adapter(int dc_id, void *user_data);
100static void on_datachannel_message_adapter(int dc_id, const char *data, int size, void *user_data);
101static void on_datachannel_error_adapter(int dc_id, const char *error, void *user_data);
102
103static void on_state_change_adapter(int pc_id, rtcState state, void *user_data) {
104 (void)pc_id; // Unused - we get peer connection from user_data
105 webrtc_peer_connection_t *pc = (webrtc_peer_connection_t *)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 break;
121 case RTC_DISCONNECTED:
122 new_state = WEBRTC_STATE_DISCONNECTED;
123 break;
124 case RTC_FAILED:
125 new_state = WEBRTC_STATE_FAILED;
126 break;
127 case RTC_CLOSED:
128 new_state = WEBRTC_STATE_CLOSED;
129 break;
130 default:
131 new_state = WEBRTC_STATE_NEW;
132 break;
133 }
134
135 pc->state = new_state;
136
137 if (pc->config.on_state_change) {
138 pc->config.on_state_change(pc, new_state, pc->config.user_data);
139 }
140}
141
142static void on_gathering_state_change_adapter(int pc_id, rtcGatheringState state, void *user_data) {
143 (void)pc_id; // Unused - we get peer connection from user_data
144 webrtc_peer_connection_t *pc = (webrtc_peer_connection_t *)user_data;
145 if (!pc)
146 return;
147
148 // Map libdatachannel gathering state to our enum
149 webrtc_gathering_state_t new_state;
150 switch (state) {
151 case RTC_GATHERING_NEW:
152 new_state = WEBRTC_GATHERING_NEW;
153 break;
154 case RTC_GATHERING_INPROGRESS:
155 new_state = WEBRTC_GATHERING_GATHERING;
156 // Record start time when gathering begins
157 if (pc->gathering_state != WEBRTC_GATHERING_GATHERING) {
158 pc->gathering_start_time_ms = platform_get_monotonic_time_us() / 1000;
159 log_debug("ICE gathering started at %llu ms", (unsigned long long)pc->gathering_start_time_ms);
160 }
161 break;
162 case RTC_GATHERING_COMPLETE:
163 new_state = WEBRTC_GATHERING_COMPLETE;
164 if (pc->gathering_start_time_ms > 0) {
165 uint64_t duration = (platform_get_monotonic_time_us() / 1000) - pc->gathering_start_time_ms;
166 log_info("ICE gathering completed in %llu ms", (unsigned long long)duration);
167 }
168 break;
169 default:
170 new_state = WEBRTC_GATHERING_NEW;
171 break;
172 }
173
174 pc->gathering_state = new_state;
175
176 if (pc->config.on_gathering_state_change) {
177 pc->config.on_gathering_state_change(pc, new_state, pc->config.user_data);
178 }
179}
180
181static void on_local_description_adapter(int pc_id, const char *sdp, const char *type, void *user_data) {
182 (void)pc_id; // Unused - we get peer connection from user_data
183 webrtc_peer_connection_t *pc = (webrtc_peer_connection_t *)user_data;
184 if (!pc)
185 return;
186
187 if (pc->config.on_local_description) {
188 pc->config.on_local_description(pc, sdp, type, pc->config.user_data);
189 }
190}
191
192static void on_local_candidate_adapter(int pc_id, const char *candidate, const char *mid, void *user_data) {
193 (void)pc_id; // Unused - we get peer connection from user_data
194 webrtc_peer_connection_t *pc = (webrtc_peer_connection_t *)user_data;
195 if (!pc)
196 return;
197
198 if (pc->config.on_local_candidate) {
199 pc->config.on_local_candidate(pc, candidate, mid, pc->config.user_data);
200 }
201}
202
203static void on_datachannel_adapter(int pc_id, int dc_id, void *user_data) {
204 (void)pc_id; // Unused - we get peer connection from user_data
205 log_info("on_datachannel_adapter: pc_id=%d, dc_id=%d, user_data=%p", pc_id, dc_id, user_data);
206
207 webrtc_peer_connection_t *pc = (webrtc_peer_connection_t *)user_data;
208 if (!pc) {
209 log_error("on_datachannel_adapter: peer connection user_data is NULL!");
210 return;
211 }
212
213 log_info("on_datachannel_adapter: received DataChannel (dc_id=%d) from remote peer", dc_id);
214
215 // Allocate data channel wrapper
216 webrtc_data_channel_t *dc = SAFE_MALLOC(sizeof(webrtc_data_channel_t), webrtc_data_channel_t *);
217 if (!dc) {
218 log_error("Failed to allocate data channel wrapper");
219 rtcDeleteDataChannel(dc_id);
220 return;
221 }
222
223 dc->rtc_id = dc_id;
224 dc->pc = pc;
225 dc->is_open = false;
226 log_debug("Initialized DataChannel wrapper: dc=%p, is_open=false", (void *)dc);
227
228 // Store in peer connection
229 pc->dc = dc;
230
231 // Set up data channel callbacks for incoming channel
232 rtcSetUserPointer(dc_id, dc);
233 rtcSetOpenCallback(dc_id, on_datachannel_open_adapter);
234 rtcSetMessageCallback(dc_id, on_datachannel_message_adapter);
235 rtcSetErrorCallback(dc_id, on_datachannel_error_adapter);
236
237 log_info("Set up callbacks for incoming DataChannel (dc_id=%d, dc=%p)", dc_id, (void *)dc);
238
239 // Check if DataChannel is already open (can happen if it opened before callbacks were set)
240 // In libdatachannel, negotiated DataChannels may be open immediately when received
241 // We manually trigger the open callback if that's the case
242 if (rtcIsOpen(dc_id)) {
243 log_info("DataChannel was already open when received, manually triggering open callback");
244 on_datachannel_open_adapter(dc_id, dc);
245 }
246}
247
248static void on_datachannel_open_adapter(int dc_id, void *user_data) {
249 webrtc_data_channel_t *dc = (webrtc_data_channel_t *)user_data;
250 log_info("on_datachannel_open_adapter called: dc_id=%d, dc=%p", dc_id, (void *)dc);
251 if (!dc) {
252 log_error("on_datachannel_open_adapter: dc is NULL!");
253 return;
254 }
255
256 dc->is_open = true;
257 log_info("DataChannel opened (id=%d, dc=%p), set is_open=true", dc_id, (void *)dc);
258
259 // Check per-channel callback first, then fall back to peer connection callback
260 if (dc->user_on_open) {
261 log_debug("Calling per-channel on_open callback");
262 dc->user_on_open(dc, dc->user_data);
263 } else if (dc->pc && dc->pc->config.on_datachannel_open) {
264 log_debug("Calling user on_datachannel_open callback");
265 dc->pc->config.on_datachannel_open(dc, dc->pc->config.user_data);
266 } else {
267 log_warn("No user on_datachannel_open callback registered");
268 }
269}
270
271static void on_datachannel_closed_adapter(int dc_id, void *user_data) {
272 webrtc_data_channel_t *dc = (webrtc_data_channel_t *)user_data;
273 log_info("on_datachannel_closed_adapter called: dc_id=%d, dc=%p", dc_id, (void *)dc);
274 if (!dc) {
275 log_error("on_datachannel_closed_adapter: dc is NULL!");
276 return;
277 }
278
279 dc->is_open = false;
280 log_info("DataChannel closed (id=%d, dc=%p), set is_open=false", dc_id, (void *)dc);
281
282 // Check per-channel callback first
283 if (dc->user_on_close) {
284 log_debug("Calling per-channel on_close callback");
285 dc->user_on_close(dc, dc->user_data);
286 }
287}
288
289static void on_datachannel_message_adapter(int dc_id, const char *data, int size, void *user_data) {
290 (void)dc_id; // Unused - we get data channel from user_data
291
292 // Log at libdatachannel callback level (debug level for normal operation)
293 if (size >= 20 && data) {
294 const uint8_t *pkt = (const uint8_t *)data;
295 log_debug("★ LIBDATACHANNEL_RX: dc_id=%d, size=%d, first_20_bytes: %02x%02x%02x%02x %02x%02x%02x%02x "
296 "%02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x",
297 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],
298 pkt[11], pkt[12], pkt[13], pkt[14], pkt[15], pkt[16], pkt[17], pkt[18], pkt[19]);
299 } else {
300 log_debug("★ LIBDATACHANNEL_RX: dc_id=%d, size=%d (no data or too small)", dc_id, size);
301 }
302
303 webrtc_data_channel_t *dc = (webrtc_data_channel_t *)user_data;
304 if (!dc) {
305 log_debug("★ LIBDATACHANNEL_RX: dc=NULL, dropping message");
306 return;
307 }
308
309 // Check per-channel callback first, then fall back to peer connection callback
310 if (dc->user_on_message) {
311 dc->user_on_message(dc, (const uint8_t *)data, (size_t)size, dc->user_data);
312 } else if (dc->pc && dc->pc->config.on_datachannel_message) {
313 dc->pc->config.on_datachannel_message(dc, (const uint8_t *)data, (size_t)size, dc->pc->config.user_data);
314 }
315}
316
317static void on_datachannel_error_adapter(int dc_id, const char *error, void *user_data) {
318 webrtc_data_channel_t *dc = (webrtc_data_channel_t *)user_data;
319 if (!dc)
320 return;
321
322 log_error("DataChannel error (id=%d): %s", dc_id, error);
323
324 // Check per-channel callback first, then fall back to peer connection callback
325 if (dc->user_on_error) {
326 dc->user_on_error(dc, error, dc->user_data);
327 } else if (dc->pc && dc->pc->config.on_datachannel_error) {
328 dc->pc->config.on_datachannel_error(dc, error, dc->pc->config.user_data);
329 }
330}
331
332// ============================================================================
333// Initialization and Cleanup
334// ============================================================================
335
344static asciichat_error_t webrtc_ensure_initialized(void) {
345 static_mutex_lock(&g_webrtc_refcount_mutex);
346
347 // If already initialized, just increment refcount
348 if (g_webrtc_init_refcount > 0) {
349 g_webrtc_init_refcount++;
350 static_mutex_unlock(&g_webrtc_refcount_mutex);
351 return ASCIICHAT_OK;
352 }
353
354 // First initialization - call library initialization exactly once
355 rtcInitLogger(RTC_LOG_VERBOSE, rtc_log_callback);
356 rtcPreload();
357
358 g_webrtc_init_refcount = 1;
359 static_mutex_unlock(&g_webrtc_refcount_mutex);
360
361 log_debug("WebRTC library initialized (libdatachannel)");
362 return ASCIICHAT_OK;
363}
364
371static void webrtc_release(void) {
372 static_mutex_lock(&g_webrtc_refcount_mutex);
373 if (g_webrtc_init_refcount > 0) {
374 g_webrtc_init_refcount--;
375 if (g_webrtc_init_refcount == 0) {
376 rtcCleanup();
377 log_info("WebRTC library cleaned up");
378 }
379 }
380 static_mutex_unlock(&g_webrtc_refcount_mutex);
381}
382
383asciichat_error_t webrtc_init(void) {
384 return webrtc_ensure_initialized();
385}
386
387void webrtc_destroy(void) {
388 webrtc_release();
389}
390
391// ============================================================================
392// Peer Connection Management
393// ============================================================================
394
395asciichat_error_t webrtc_create_peer_connection(const webrtc_config_t *config, webrtc_peer_connection_t **pc_out) {
396 if (!config || !pc_out) {
397 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid config or output parameter");
398 }
399
400 static_mutex_lock(&g_webrtc_refcount_mutex);
401 bool is_initialized = (g_webrtc_init_refcount > 0);
402 static_mutex_unlock(&g_webrtc_refcount_mutex);
403
404 if (!is_initialized) {
405 return SET_ERRNO(ERROR_INIT, "WebRTC library not initialized");
406 }
407
408 // Allocate peer connection wrapper
409 webrtc_peer_connection_t *pc = SAFE_MALLOC(sizeof(webrtc_peer_connection_t), webrtc_peer_connection_t *);
410 if (!pc) {
411 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate peer connection");
412 }
413
414 pc->config = *config; // Copy config
415 pc->state = WEBRTC_STATE_NEW;
416 pc->dc = NULL;
417
418 // Build ICE server list for libdatachannel
419 const char **ice_servers = NULL;
420 size_t ice_count = 0;
421
422 if (config->stun_count > 0 || config->turn_count > 0) {
423 ice_count = config->stun_count + config->turn_count;
424 ice_servers = SAFE_MALLOC(ice_count * sizeof(char *), const char **);
425 if (!ice_servers) {
426 SAFE_FREE(pc);
427 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate ICE server list");
428 }
429
430 // Add STUN servers
431 for (size_t i = 0; i < config->stun_count; i++) {
432 ice_servers[i] = config->stun_servers[i].host;
433 }
434
435 // Add TURN servers
436 for (size_t i = 0; i < config->turn_count; i++) {
437 ice_servers[config->stun_count + i] = config->turn_servers[i].url;
438 }
439 }
440
441 // Create libdatachannel configuration
442 rtcConfiguration rtc_config;
443 memset(&rtc_config, 0, sizeof(rtc_config));
444 rtc_config.iceServers = ice_servers;
445 rtc_config.iceServersCount = (int)ice_count;
446 rtc_config.iceTransportPolicy = RTC_TRANSPORT_POLICY_ALL;
447
448 // Create peer connection
449 int pc_id = rtcCreatePeerConnection(&rtc_config);
450
451 // Free ICE server list (libdatachannel makes a copy)
452 if (ice_servers) {
453 SAFE_FREE(ice_servers);
454 }
455
456 if (pc_id < 0) {
457 SAFE_FREE(pc);
458 return SET_ERRNO(ERROR_NETWORK, "Failed to create peer connection (rtc error %d)", pc_id);
459 }
460
461 pc->rtc_id = pc_id;
462
463 // Initialize gathering state
464 pc->gathering_state = WEBRTC_GATHERING_NEW;
465 pc->gathering_start_time_ms = 0;
466
467 // Set up callbacks
468 rtcSetUserPointer(pc_id, pc);
469 rtcSetStateChangeCallback(pc_id, on_state_change_adapter);
470 rtcSetGatheringStateChangeCallback(pc_id, on_gathering_state_change_adapter);
471 rtcSetLocalDescriptionCallback(pc_id, on_local_description_adapter);
472 rtcSetLocalCandidateCallback(pc_id, on_local_candidate_adapter);
473 rtcSetDataChannelCallback(pc_id, on_datachannel_adapter);
474
475 *pc_out = pc;
476 log_debug("Created WebRTC peer connection (id=%d)", pc_id);
477 return ASCIICHAT_OK;
478}
479
480void webrtc_close_peer_connection(webrtc_peer_connection_t *pc) {
481 if (!pc) {
482 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid peer connection");
483 return;
484 }
485
486 // Close data channel if exists
487 if (pc->dc) {
489 pc->dc = NULL;
490 }
491
492 // Close peer connection
493 rtcDeletePeerConnection(pc->rtc_id);
494 log_debug("Closed WebRTC peer connection (id=%d)", pc->rtc_id);
495
496 SAFE_FREE(pc);
497}
498
499webrtc_state_t webrtc_get_state(webrtc_peer_connection_t *pc) {
500 if (!pc) {
501 log_warn("Peer connection is NULL");
502 return WEBRTC_STATE_CLOSED;
503 }
504 return pc->state;
505}
506
507webrtc_gathering_state_t webrtc_get_gathering_state(webrtc_peer_connection_t *pc) {
508 if (!pc) {
509 log_warn("Peer connection is NULL");
510 return WEBRTC_GATHERING_NEW;
511 }
512 return pc->gathering_state;
513}
514
515bool webrtc_is_gathering_timed_out(webrtc_peer_connection_t *pc, uint32_t timeout_ms) {
516 if (!pc) {
517 return false;
518 }
519
520 // Only check timeout if we're actively gathering
521 if (pc->gathering_state != WEBRTC_GATHERING_GATHERING) {
522 return false;
523 }
524
525 // If gathering hasn't started yet (start_time == 0), no timeout
526 if (pc->gathering_start_time_ms == 0) {
527 return false;
528 }
529
530 // Check if elapsed time exceeds timeout
531 uint64_t current_time_ms = platform_get_monotonic_time_us() / 1000;
532 uint64_t elapsed_ms = current_time_ms - pc->gathering_start_time_ms;
533
534 if (elapsed_ms > timeout_ms) {
535 log_warn("ICE gathering timeout: %llu ms elapsed (timeout: %u ms)", (unsigned long long)elapsed_ms, timeout_ms);
536 return true;
537 }
538
539 return false;
540}
541
542void *webrtc_get_user_data(webrtc_peer_connection_t *pc) {
543 if (!pc) {
544 log_warn("Peer connection is NULL");
545 return NULL;
546 }
547 return pc->config.user_data;
548}
549
550// ============================================================================
551// SDP Offer/Answer Exchange
552// ============================================================================
553
554asciichat_error_t webrtc_create_offer(webrtc_peer_connection_t *pc) {
555 if (!pc) {
556 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid peer connection");
557 }
558
559 // Set local description with NULL type to trigger offer generation
560 int result = rtcSetLocalDescription(pc->rtc_id, NULL);
561 if (result != RTC_ERR_SUCCESS) {
562 return SET_ERRNO(ERROR_NETWORK, "Failed to create SDP offer (rtc error %d)", result);
563 }
564
565 log_debug("Creating SDP offer (pc_id=%d)", pc->rtc_id);
566 return ASCIICHAT_OK;
567}
568
569asciichat_error_t webrtc_set_remote_description(webrtc_peer_connection_t *pc, const char *sdp, const char *type) {
570 if (!pc || !sdp || !type) {
571 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
572 }
573
574 int result = rtcSetRemoteDescription(pc->rtc_id, sdp, type);
575 if (result != RTC_ERR_SUCCESS) {
576 return SET_ERRNO(ERROR_NETWORK, "Failed to set remote SDP (rtc error %d)", result);
577 }
578
579 log_debug("Set remote SDP description (pc_id=%d, type=%s)", pc->rtc_id, type);
580 return ASCIICHAT_OK;
581}
582
583// ============================================================================
584// ICE Candidate Exchange
585// ============================================================================
586
587asciichat_error_t webrtc_add_remote_candidate(webrtc_peer_connection_t *pc, const char *candidate, const char *mid) {
588 if (!pc || !candidate) {
589 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
590 }
591
592 log_debug(" [4] Before libdatachannel - candidate: '%s' (len=%zu)", candidate, strlen(candidate));
593 log_debug(" [4] Before libdatachannel - mid: '%s' (len=%zu)", mid ? mid : "(null)", mid ? strlen(mid) : 0);
594
595 int result = rtcAddRemoteCandidate(pc->rtc_id, candidate, mid);
596 if (result != RTC_ERR_SUCCESS) {
597 return SET_ERRNO(ERROR_NETWORK, "Failed to add remote ICE candidate (rtc error %d)", result);
598 }
599
600 log_debug("Added remote ICE candidate (pc_id=%d)", pc->rtc_id);
601 return ASCIICHAT_OK;
602}
603
604// ============================================================================
605// DataChannel Management
606// ============================================================================
607
608asciichat_error_t webrtc_create_datachannel(webrtc_peer_connection_t *pc, const char *label,
609 webrtc_data_channel_t **dc_out) {
610 if (!pc || !label || !dc_out) {
611 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
612 }
613
614 // Create data channel
615 int dc_id = rtcCreateDataChannel(pc->rtc_id, label);
616 if (dc_id < 0) {
617 return SET_ERRNO(ERROR_NETWORK, "Failed to create data channel (rtc error %d)", dc_id);
618 }
619
620 // Allocate wrapper
621 webrtc_data_channel_t *dc = SAFE_MALLOC(sizeof(webrtc_data_channel_t), webrtc_data_channel_t *);
622 if (!dc) {
623 rtcDeleteDataChannel(dc_id);
624 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate data channel wrapper");
625 }
626
627 dc->rtc_id = dc_id;
628 dc->pc = pc;
629 dc->is_open = false;
630
631 // Set up callbacks
632 rtcSetUserPointer(dc_id, dc);
633 rtcSetOpenCallback(dc_id, on_datachannel_open_adapter);
634 rtcSetMessageCallback(dc_id, on_datachannel_message_adapter);
635 rtcSetErrorCallback(dc_id, on_datachannel_error_adapter);
636
637 pc->dc = dc;
638 *dc_out = dc;
639
640 log_debug("Created DataChannel '%s' (dc_id=%d, pc_id=%d)", label, dc_id, pc->rtc_id);
641 return ASCIICHAT_OK;
642}
643
644asciichat_error_t webrtc_datachannel_send(webrtc_data_channel_t *dc, const uint8_t *data, size_t size) {
645 if (!dc || !data) {
646 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters");
647 }
648
649 if (!dc->is_open) {
650 log_error("★ WEBRTC_DATACHANNEL_SEND: Channel not open! size=%zu, dc->rtc_id=%d", size, dc ? dc->rtc_id : -1);
651 return SET_ERRNO(ERROR_NETWORK, "DataChannel not open");
652 }
653
654 // Log packet details at network layer (debug level for normal operation)
655 if (size >= 20) {
656 const uint8_t *pkt = (const uint8_t *)data;
657 log_debug("★ RTCSENDMESSAGE_BEFORE: dc_id=%d, size=%zu, first_20_bytes: %02x%02x%02x%02x %02x%02x%02x%02x "
658 "%02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x",
659 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],
660 pkt[11], pkt[12], pkt[13], pkt[14], pkt[15], pkt[16], pkt[17], pkt[18], pkt[19]);
661 } else {
662 log_debug("★ RTCSENDMESSAGE_BEFORE: dc_id=%d, size=%zu (too small to log content)", dc->rtc_id, size);
663 }
664
665 int result = rtcSendMessage(dc->rtc_id, (const char *)data, (int)size);
666
667 log_debug("★ RTCSENDMESSAGE_AFTER: dc_id=%d, rtcSendMessage returned %d for size=%zu", dc->rtc_id, result, size);
668
669 if (result < 0) {
670 log_error("★ WEBRTC_DATACHANNEL_SEND: FAILED with error code %d", result);
671 return SET_ERRNO(ERROR_NETWORK, "Failed to send data (rtc error %d)", result);
672 }
673
674 log_debug("★ WEBRTC_DATACHANNEL_SEND: SUCCESS - sent %zu bytes", size);
675 return ASCIICHAT_OK;
676}
677
678bool webrtc_datachannel_is_open(webrtc_data_channel_t *dc) {
679 if (!dc) {
680 SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
681 return false;
682 }
683 return dc->is_open;
684}
685
686void webrtc_datachannel_set_open_state(webrtc_data_channel_t *dc, bool is_open) {
687 if (!dc) {
688 SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
689 return;
690 }
691 dc->is_open = is_open;
692}
693
694const char *webrtc_datachannel_get_label(webrtc_data_channel_t *dc) {
695 if (!dc) {
696 SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
697 return NULL;
698 }
699
700 static char label[256];
701 int result = rtcGetDataChannelLabel(dc->rtc_id, label, sizeof(label));
702 if (result < 0) {
703 return NULL;
704 }
705
706 return label;
707}
708
709void webrtc_close_datachannel(webrtc_data_channel_t *dc) {
710 if (!dc) {
711 SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
712 return;
713 }
714
715 // Save the ID before freeing (dc might be partially freed already)
716 int dc_id = dc->rtc_id;
717
718 rtcDeleteDataChannel(dc_id);
719 log_debug("Closed DataChannel (dc_id=%d)", dc_id);
720
721 SAFE_FREE(dc);
722}
723
724// =============================================================================
725// DataChannel Callbacks
726// =============================================================================
727
728asciichat_error_t webrtc_datachannel_set_callbacks(webrtc_data_channel_t *dc,
729 const webrtc_datachannel_callbacks_t *callbacks) {
730 if (!dc) {
731 return SET_ERRNO(ERROR_INVALID_PARAM, "DataChannel is NULL");
732 }
733
734 if (!callbacks) {
735 return SET_ERRNO(ERROR_INVALID_PARAM, "Callbacks struct is NULL");
736 }
737
738 // Store user callbacks in the data channel struct
739 dc->user_on_open = callbacks->on_open;
740 dc->user_on_close = callbacks->on_close;
741 dc->user_on_error = callbacks->on_error;
742 dc->user_on_message = callbacks->on_message;
743 dc->user_data = callbacks->user_data;
744
745 // Register adapter functions with libdatachannel (adapters use correct signature)
746 // The adapters will look up our stored callbacks and invoke them with proper types
747 if (callbacks->on_open) {
748 rtcSetOpenCallback(dc->rtc_id, on_datachannel_open_adapter);
749 }
750
751 if (callbacks->on_close) {
752 rtcSetClosedCallback(dc->rtc_id, on_datachannel_closed_adapter);
753 }
754
755 if (callbacks->on_error) {
756 rtcSetErrorCallback(dc->rtc_id, on_datachannel_error_adapter);
757 }
758
759 if (callbacks->on_message) {
760 rtcSetMessageCallback(dc->rtc_id, on_datachannel_message_adapter);
761 }
762
763 // Set user pointer to the data channel so adapters can retrieve it
764 rtcSetUserPointer(dc->rtc_id, dc);
765
766 log_debug("Set DataChannel callbacks (dc_id=%d)", dc->rtc_id);
767 return ASCIICHAT_OK;
768}
769
770void webrtc_datachannel_destroy(webrtc_data_channel_t *dc) {
771 if (!dc) {
772 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid null data channel");
773 return;
774 }
775
776 // Close if still open, then free
778 // Note: webrtc_close_datachannel already calls SAFE_FREE(dc), so we're done
779}
780
781// =============================================================================
782// Peer Connection Lifecycle
783// =============================================================================
784
785void webrtc_peer_connection_close(webrtc_peer_connection_t *pc) {
786 if (!pc) {
787 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid null peer connectiont");
788 return;
789 }
790
791 rtcClose(pc->rtc_id);
792 log_debug("Closed peer connection (pc_id=%d)", pc->rtc_id);
793}
794
795void webrtc_peer_connection_destroy(webrtc_peer_connection_t *pc) {
796 if (!pc) {
797 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid null peer connectiont");
798 return;
799 }
800
801 // Close and delete peer connection
802 rtcDeletePeerConnection(pc->rtc_id);
803 log_debug("Destroyed peer connection (pc_id=%d)", pc->rtc_id);
804
805 SAFE_FREE(pc);
806}
807
817int webrtc_get_rtc_id(webrtc_peer_connection_t *pc) {
818 if (!pc) {
819 SET_ERRNO(ERROR_INVALID_PARAM, "Invalid null peer connectiont");
820 return -1;
821 }
822 return pc->rtc_id;
823}
asciichat_error_t webrtc_create_datachannel(webrtc_peer_connection_t *pc, const char *label, webrtc_data_channel_t **dc_out)
void webrtc_close_peer_connection(webrtc_peer_connection_t *pc)
asciichat_error_t webrtc_create_offer(webrtc_peer_connection_t *pc)
asciichat_error_t webrtc_datachannel_set_callbacks(webrtc_data_channel_t *dc, const webrtc_datachannel_callbacks_t *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)
webrtc_state_t webrtc_get_state(webrtc_peer_connection_t *pc)
webrtc_gathering_state_t webrtc_get_gathering_state(webrtc_peer_connection_t *pc)
asciichat_error_t webrtc_init(void)
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)
const char * webrtc_datachannel_get_label(webrtc_data_channel_t *dc)
void * webrtc_get_user_data(webrtc_peer_connection_t *pc)
void webrtc_peer_connection_close(webrtc_peer_connection_t *pc)
void webrtc_destroy(void)
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)
void webrtc_close_datachannel(webrtc_data_channel_t *dc)
bool webrtc_datachannel_is_open(webrtc_data_channel_t *dc)
asciichat_error_t webrtc_datachannel_send(webrtc_data_channel_t *dc, const uint8_t *data, size_t size)
bool webrtc_is_gathering_timed_out(webrtc_peer_connection_t *pc, uint32_t timeout_ms)
void webrtc_datachannel_set_open_state(webrtc_data_channel_t *dc, bool is_open)
uint64_t platform_get_monotonic_time_us(void)
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.
bool is_open
Channel open state.
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.
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.
WebRTC peer connection state.
webrtc_data_channel_t * dc
Primary data channel (if created/received)
uint64_t gathering_start_time_ms
When gathering started (platform_get_time_ms)
int rtc_id
libdatachannel peer connection ID
webrtc_state_t state
Current connection state.
webrtc_gathering_state_t gathering_state
Current ICE gathering state.
webrtc_config_t config
Configuration with callbacks.