ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
sdp.c
Go to the documentation of this file.
1
11#include "sdp.h"
12#include "log/logging.h"
13#include <string.h>
14#include <stdio.h>
15#include <time.h>
16
17/* ============================================================================
18 * Utility Functions
19 * ============================================================================ */
20
21const char *sdp_codec_name(acip_codec_t codec) {
22 switch (codec) {
24 return "ACIP-TC";
26 return "ACIP-256";
28 return "ACIP-16";
29 case ACIP_CODEC_MONO:
30 return "ACIP-MONO";
31 default:
32 return "UNKNOWN";
33 }
34}
35
36const char *sdp_renderer_name(int renderer_type) {
37 switch (renderer_type) {
38 case RENDERER_BLOCK:
39 return "block";
40 case RENDERER_HALFBLOCK:
41 return "halfblock";
42 case RENDERER_BRAILLE:
43 return "braille";
44 default:
45 return "unknown";
46 }
47}
48
49/* ============================================================================
50 * SDP Offer Generation
51 * ============================================================================ */
52
53asciichat_error_t sdp_generate_offer(const terminal_capability_t *capabilities, size_t capability_count,
54 const opus_config_t *audio_config, const terminal_format_params_t *format,
55 sdp_session_t *offer_out) {
56 if (!capabilities || capability_count == 0 || !audio_config || !offer_out) {
57 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP offer parameters");
58 }
59
60 memset(offer_out, 0, sizeof(sdp_session_t));
61
62 // Generate session ID and version from current time
63 time_t now = time(NULL);
64 snprintf(offer_out->session_id, sizeof(offer_out->session_id), "%ld", now);
65 snprintf(offer_out->session_version, sizeof(offer_out->session_version), "%ld", now);
66
67 // Allocate and copy video codecs
68 offer_out->video_codecs = SAFE_MALLOC(capability_count * sizeof(terminal_capability_t), terminal_capability_t *);
69 memcpy(offer_out->video_codecs, capabilities, capability_count * sizeof(terminal_capability_t));
70 offer_out->video_codec_count = capability_count;
71
72 offer_out->has_audio = true;
73 memcpy(&offer_out->audio_config, audio_config, sizeof(opus_config_t));
74
75 offer_out->has_video = true;
76 if (format) {
77 memcpy(&offer_out->video_format, format, sizeof(terminal_format_params_t));
78 }
79
80 // Build complete SDP offer
81 char *sdp = offer_out->sdp_string;
82 size_t remaining = sizeof(offer_out->sdp_string);
83 int written = 0;
84
85 // Session-level attributes
86 written = snprintf(sdp, remaining, "v=0\r\n");
87 sdp += written;
88 remaining -= written;
89
90 written = snprintf(sdp, remaining, "o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", offer_out->session_id,
91 offer_out->session_version);
92 sdp += written;
93 remaining -= written;
94
95 written = snprintf(sdp, remaining, "s=-\r\n");
96 sdp += written;
97 remaining -= written;
98
99 written = snprintf(sdp, remaining, "t=0 0\r\n");
100 sdp += written;
101 remaining -= written;
102
103 // Audio media section
104 written = snprintf(sdp, remaining, "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
105 sdp += written;
106 remaining -= written;
107
108 written = snprintf(sdp, remaining, "a=rtpmap:111 opus/48000/2\r\n");
109 sdp += written;
110 remaining -= written;
111
112 written = snprintf(sdp, remaining, "a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
113 sdp += written;
114 remaining -= written;
115
116 // Video media section with terminal capabilities
117 char codec_list[128] = "96";
118 for (size_t i = 1; i < capability_count; i++) {
119 char codec_num[4];
120 snprintf(codec_num, sizeof(codec_num), " %d", 96 + (int)i);
121 size_t len = strlen(codec_list);
122 strncat(codec_list, codec_num, sizeof(codec_list) - len - 1);
123 }
124
125 written = snprintf(sdp, remaining, "m=video 9 UDP/TLS/RTP/SAVPF %s\r\n", codec_list);
126 sdp += written;
127 remaining -= written;
128
129 // Add rtpmap and fmtp for each capability
130 for (size_t i = 0; i < capability_count; i++) {
131 int pt = 96 + (int)i;
132 const char *codec_name = sdp_codec_name(capabilities[i].codec);
133
134 written = snprintf(sdp, remaining, "a=rtpmap:%d %s/90000\r\n", pt, codec_name);
135 sdp += written;
136 remaining -= written;
137
138 // Format parameters
139 const terminal_format_params_t *cap_format = &capabilities[i].format;
140 const char *renderer_name = sdp_renderer_name(cap_format->renderer);
141 const char *charset_name = (cap_format->charset == CHARSET_UTF8) ? "utf8" : "ascii";
142 const char *compression_name = "none";
143 if (cap_format->compression == COMPRESSION_RLE) {
144 compression_name = "rle";
145 } else if (cap_format->compression == COMPRESSION_ZSTD) {
146 compression_name = "zstd";
147 }
148
149 written =
150 snprintf(sdp, remaining, "a=fmtp:%d width=%u;height=%u;renderer=%s;charset=%s;compression=%s;csi_rep=%d\r\n",
151 pt, cap_format->width, cap_format->height, renderer_name, charset_name, compression_name,
152 cap_format->csi_rep_support ? 1 : 0);
153 sdp += written;
154 remaining -= written;
155 }
156
157 offer_out->sdp_length = strlen(offer_out->sdp_string);
158
159 log_debug("SDP: Generated offer with %zu video codecs and Opus audio", capability_count);
160
161 return ASCIICHAT_OK;
162}
163
164/* ============================================================================
165 * SDP Answer Generation
166 * ============================================================================ */
167
169 size_t server_capability_count, const opus_config_t *audio_config,
170 const terminal_format_params_t *server_format, sdp_session_t *answer_out) {
171 if (!offer || !server_capabilities || server_capability_count == 0 || !audio_config || !answer_out) {
172 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP answer parameters");
173 }
174
175 memset(answer_out, 0, sizeof(sdp_session_t));
176
177 // Copy session ID from offer, increment version
178 SAFE_STRNCPY(answer_out->session_id, offer->session_id, sizeof(answer_out->session_id));
179
180 time_t now = time(NULL);
181 snprintf(answer_out->session_version, sizeof(answer_out->session_version), "%ld", now);
182
183 // Answer audio section: accept Opus
184 answer_out->has_audio = offer->has_audio;
185 if (answer_out->has_audio) {
186 memcpy(&answer_out->audio_config, audio_config, sizeof(opus_config_t));
187 }
188
189 // Answer video section: find best mutually-supported codec
190 answer_out->has_video = offer->has_video;
191
192 if (answer_out->has_video && offer->video_codecs && offer->video_codec_count > 0) {
193 // Find best codec: iterate through server preferences and find first match in offer
194 int selected_index = -1;
195
196 for (size_t s = 0; s < server_capability_count; s++) {
197 for (size_t o = 0; o < offer->video_codec_count; o++) {
198 if (server_capabilities[s].codec == offer->video_codecs[o].codec) {
199 selected_index = (int)s;
200 break; // Found match, use server's preference order
201 }
202 }
203 if (selected_index >= 0) {
204 break;
205 }
206 }
207
208 // Allocate and fill selected codec
210 answer_out->video_codec_count = 1;
211
212 // Use server's capability with any server_format overrides
213 if (selected_index >= 0) {
214 memcpy(answer_out->video_codecs, &server_capabilities[selected_index], sizeof(terminal_capability_t));
215 } else {
216 // Fallback to monochrome
217 memcpy(answer_out->video_codecs, &server_capabilities[0], sizeof(terminal_capability_t));
218 }
219
220 // Apply server format constraints if provided
221 if (server_format) {
222 if (server_format->width > 0) {
223 answer_out->video_codecs[0].format.width = server_format->width;
224 }
225 if (server_format->height > 0) {
226 answer_out->video_codecs[0].format.height = server_format->height;
227 }
228 if (server_format->renderer != RENDERER_BLOCK) {
229 answer_out->video_codecs[0].format.renderer = server_format->renderer;
230 }
231 if (server_format->compression != COMPRESSION_NONE) {
232 answer_out->video_codecs[0].format.compression = server_format->compression;
233 }
234 }
235
236 memcpy(&answer_out->video_format, &answer_out->video_codecs[0].format, sizeof(terminal_format_params_t));
237 }
238
239 // Build complete SDP answer
240 char *sdp = answer_out->sdp_string;
241 size_t remaining = sizeof(answer_out->sdp_string);
242 int written = 0;
243
244 // Session-level attributes
245 written = snprintf(sdp, remaining, "v=0\r\n");
246 sdp += written;
247 remaining -= written;
248
249 written = snprintf(sdp, remaining, "o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", answer_out->session_id,
250 answer_out->session_version);
251 sdp += written;
252 remaining -= written;
253
254 written = snprintf(sdp, remaining, "s=-\r\n");
255 sdp += written;
256 remaining -= written;
257
258 written = snprintf(sdp, remaining, "t=0 0\r\n");
259 sdp += written;
260 remaining -= written;
261
262 // Audio media section (if present in offer)
263 if (answer_out->has_audio) {
264 written = snprintf(sdp, remaining, "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
265 sdp += written;
266 remaining -= written;
267
268 written = snprintf(sdp, remaining, "a=rtpmap:111 opus/48000/2\r\n");
269 sdp += written;
270 remaining -= written;
271
272 written = snprintf(sdp, remaining, "a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
273 sdp += written;
274 remaining -= written;
275 }
276
277 // Video media section (if present in offer)
278 if (answer_out->has_video && answer_out->video_codecs && answer_out->video_codec_count > 0) {
279 written = snprintf(sdp, remaining, "m=video 9 UDP/TLS/RTP/SAVPF 96\r\n");
280 sdp += written;
281 remaining -= written;
282
283 const char *codec_name = sdp_codec_name(answer_out->video_codecs[0].codec);
284 written = snprintf(sdp, remaining, "a=rtpmap:96 %s/90000\r\n", codec_name);
285 sdp += written;
286 remaining -= written;
287
288 // Format parameters
289 const terminal_format_params_t *cap_format = &answer_out->video_codecs[0].format;
290 const char *renderer_name = sdp_renderer_name(cap_format->renderer);
291 const char *charset_name = (cap_format->charset == CHARSET_UTF8) ? "utf8" : "ascii";
292 const char *compression_name = "none";
293 if (cap_format->compression == COMPRESSION_RLE) {
294 compression_name = "rle";
295 } else if (cap_format->compression == COMPRESSION_ZSTD) {
296 compression_name = "zstd";
297 }
298
299 written =
300 snprintf(sdp, remaining, "a=fmtp:96 width=%u;height=%u;renderer=%s;charset=%s;compression=%s;csi_rep=%d\r\n",
301 cap_format->width, cap_format->height, renderer_name, charset_name, compression_name,
302 cap_format->csi_rep_support ? 1 : 0);
303 sdp += written;
304 remaining -= written;
305 }
306
307 answer_out->sdp_length = strlen(answer_out->sdp_string);
308
309 log_debug("SDP: Generated answer with codec %s", sdp_codec_name(answer_out->video_codecs[0].codec));
310
311 return ASCIICHAT_OK;
312}
313
314/* ============================================================================
315 * SDP Parsing
316 * ============================================================================ */
317
318asciichat_error_t sdp_parse(const char *sdp_string, sdp_session_t *session) {
319 if (!sdp_string || !session) {
320 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP parse parameters");
321 }
322
323 memset(session, 0, sizeof(sdp_session_t));
324
325 // Copy original SDP string for reference
326 SAFE_STRNCPY(session->sdp_string, sdp_string, sizeof(session->sdp_string));
327 session->sdp_length = strlen(sdp_string);
328
329 // Parse line by line
330 char line_buffer[512];
331 SAFE_STRNCPY(line_buffer, sdp_string, sizeof(line_buffer));
332
333 char *saveptr = NULL;
334 char *line = strtok_r(line_buffer, "\r\n", &saveptr);
335
336 int in_audio_section = 0;
337 int in_video_section = 0;
338 size_t video_codec_index = 0;
339
340 while (line) {
341 // Parse line format: "key=value"
342 char *equals = strchr(line, '=');
343 if (!equals) {
344 line = strtok_r(NULL, "\r\n", &saveptr);
345 continue;
346 }
347
348 char key = line[0];
349 const char *value = equals + 1;
350
351 switch (key) {
352 case 'v':
353 // v=0 (version)
354 break;
355
356 case 'o':
357 // o=username session_id session_version ... session_id ...
358 // Parse session ID (second field)
359 {
360 char o_copy[256];
361 SAFE_STRNCPY(o_copy, value, sizeof(o_copy));
362 char *o_saveptr = NULL;
363 strtok_r(o_copy, " ", &o_saveptr); // username
364 char *session_id_str = strtok_r(NULL, " ", &o_saveptr);
365 if (session_id_str) {
366 SAFE_STRNCPY(session->session_id, session_id_str, sizeof(session->session_id));
367 }
368 }
369 break;
370
371 case 's':
372 // s=session_name (typically "-" or empty)
373 break;
374
375 case 'm':
376 // m=audio 9 UDP/TLS/RTP/SAVPF ...
377 // m=video 9 UDP/TLS/RTP/SAVPF ...
378 in_audio_section = 0;
379 in_video_section = 0;
380
381 if (strstr(value, "audio")) {
382 in_audio_section = 1;
383 session->has_audio = true;
384 } else if (strstr(value, "video")) {
385 in_video_section = 1;
386 session->has_video = true;
387 video_codec_index = 0;
388 }
389 break;
390
391 case 'a':
392 // a=rtpmap:111 opus/48000/2
393 // a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
394 // a=rtpmap:96 ACIP-TC/90000
395 // a=fmtp:96 ...
396
397 if (strstr(value, "rtpmap:")) {
398 const char *rtpmap = value + 7; // Skip "rtpmap:"
399
400 // Parse: PT codec/rate[/channels]
401 int pt = 0;
402 char codec_name[32] = {0};
403 sscanf(rtpmap, "%d %s", &pt, codec_name);
404
405 if (in_audio_section && pt == 111 && strstr(codec_name, "opus")) {
406 // Opus audio
407 session->audio_config.sample_rate = 48000;
408 session->audio_config.channels = 2;
409 } else if (in_video_section && video_codec_index < 4) {
410 // Terminal capability codec
411 if (!session->video_codecs) {
413 }
414
415 terminal_capability_t *cap = &session->video_codecs[video_codec_index];
416 memset(cap, 0, sizeof(terminal_capability_t));
417
418 if (pt == 96 && strstr(codec_name, "ACIP-TC")) {
420 video_codec_index++;
421 session->video_codec_count++;
422 } else if (pt == 97 && strstr(codec_name, "ACIP-256")) {
424 video_codec_index++;
425 session->video_codec_count++;
426 } else if (pt == 98 && strstr(codec_name, "ACIP-16")) {
428 video_codec_index++;
429 session->video_codec_count++;
430 } else if (pt == 99 && strstr(codec_name, "ACIP-MONO")) {
431 cap->codec = ACIP_CODEC_MONO;
432 video_codec_index++;
433 session->video_codec_count++;
434 }
435 }
436 } else if (strstr(value, "fmtp:")) {
437 // Parse format parameters
438 // a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
439 // a=fmtp:96 width=80;height=24;renderer=block;charset=utf8;compression=rle;csi_rep=1
440
441 const char *fmtp = value + 5; // Skip "fmtp:"
442
443 if (in_audio_section) {
444 // Parse Opus parameters
445 if (strstr(fmtp, "useinbandfec=1")) {
446 session->audio_config.fec_enabled = true;
447 }
448 if (strstr(fmtp, "usedtx=1")) {
449 session->audio_config.dtx_enabled = true;
450 }
451 // Default values for Opus
452 session->audio_config.bitrate = 24000;
453 session->audio_config.frame_duration = 20;
454 } else if (in_video_section && video_codec_index > 0) {
455 // Parse terminal format parameters
456 terminal_capability_t *cap = &session->video_codecs[video_codec_index - 1];
457
458 char fmtp_copy[256];
459 SAFE_STRNCPY(fmtp_copy, fmtp, sizeof(fmtp_copy));
460
461 // Skip PT number
462 char *saveptr = NULL;
463 strtok_r(fmtp_copy, " ", &saveptr);
464 const char *params = fmtp_copy + strcspn(fmtp_copy, " ") + 1;
465
466 // Parse width=N
467 const char *width_str = strstr(params, "width=");
468 if (width_str) {
469 sscanf(width_str + 6, "%hu", &cap->format.width);
470 }
471
472 // Parse height=N
473 const char *height_str = strstr(params, "height=");
474 if (height_str) {
475 sscanf(height_str + 7, "%hu", &cap->format.height);
476 }
477
478 // Parse renderer type
479 const char *renderer_str = strstr(params, "renderer=");
480 if (renderer_str) {
481 if (strstr(renderer_str, "block")) {
482 cap->format.renderer = RENDERER_BLOCK;
483 } else if (strstr(renderer_str, "halfblock")) {
484 cap->format.renderer = RENDERER_HALFBLOCK;
485 } else if (strstr(renderer_str, "braille")) {
486 cap->format.renderer = RENDERER_BRAILLE;
487 }
488 }
489
490 // Parse charset
491 const char *charset_str = strstr(params, "charset=");
492 if (charset_str) {
493 if (strstr(charset_str, "utf8")) {
494 cap->format.charset = CHARSET_UTF8;
495 } else if (strstr(charset_str, "utf8_wide")) {
496 cap->format.charset = CHARSET_UTF8_WIDE;
497 } else {
498 cap->format.charset = CHARSET_ASCII;
499 }
500 }
501
502 // Parse compression
503 const char *compression_str = strstr(params, "compression=");
504 if (compression_str) {
505 if (strstr(compression_str, "rle")) {
506 cap->format.compression = COMPRESSION_RLE;
507 } else if (strstr(compression_str, "zstd")) {
508 cap->format.compression = COMPRESSION_ZSTD;
509 } else {
510 cap->format.compression = COMPRESSION_NONE;
511 }
512 }
513
514 // Parse CSI REP support
515 const char *csi_rep_str = strstr(params, "csi_rep=");
516 if (csi_rep_str) {
517 cap->format.csi_rep_support = (csi_rep_str[8] == '1');
518 }
519 }
520 }
521 break;
522 }
523
524 line = strtok_r(NULL, "\r\n", &saveptr);
525 }
526
527 return ASCIICHAT_OK;
528}
529
530/* ============================================================================
531 * Video Codec Selection
532 * ============================================================================ */
533
535 terminal_format_params_t *selected_format) {
536 if (!answer || !selected_codec || !selected_format) {
537 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for codec selection");
538 }
539
540 if (!answer->has_video || !answer->video_codecs || answer->video_codec_count == 0) {
541 return SET_ERRNO(ERROR_NOT_FOUND, "No video codec in answer");
542 }
543
544 // In SDP answer, server selects only ONE codec (server's preference)
545 // The first codec in the answer is the selected one
546 terminal_capability_t *selected_cap = &answer->video_codecs[0];
547
548 *selected_codec = selected_cap->codec;
549 memcpy(selected_format, &selected_cap->format, sizeof(terminal_format_params_t));
550
551 log_debug("SDP: Selected video codec %s with resolution %ux%u", sdp_codec_name(selected_cap->codec),
552 selected_cap->format.width, selected_cap->format.height);
553
554 return ASCIICHAT_OK;
555}
556
557/* ============================================================================
558 * Terminal Capability Detection
559 * ============================================================================ */
560
562 size_t *detected_count) {
563 if (!capabilities || capability_count == 0 || !detected_count) {
564 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid capability detection parameters");
565 }
566
567 *detected_count = 0;
568 memset(capabilities, 0, capability_count * sizeof(terminal_capability_t));
569
570 // Detect terminal color level using platform terminal module
571 const char *colorterm = SAFE_GETENV("COLORTERM");
572 const char *term = SAFE_GETENV("TERM");
574
575 if (colorterm && (strcmp(colorterm, "truecolor") == 0 || strcmp(colorterm, "24bit") == 0)) {
576 color_level = TERM_COLOR_TRUECOLOR;
577 } else if (term) {
578 if (strstr(term, "256color") || strstr(term, "256")) {
579 color_level = TERM_COLOR_256;
580 } else if (strstr(term, "color") || strcmp(term, "xterm") == 0) {
581 color_level = TERM_COLOR_16;
582 }
583 }
584
585 // Detect UTF-8 support from LANG environment variable
586 const char *lang = SAFE_GETENV("LANG");
587 bool has_utf8 = (lang && (strstr(lang, "UTF-8") || strstr(lang, "utf8")));
588
589 // CSI REP support correlates with UTF-8
590 bool has_csi_rep = has_utf8;
591
592 // Get terminal size from platform module
593 uint16_t term_width = 80; // default
594 uint16_t term_height = 24; // default
595
596 terminal_size_t term_size;
597 asciichat_error_t term_err = terminal_get_size(&term_size);
598 if (term_err == ASCIICHAT_OK) {
599 term_width = (uint16_t)term_size.cols;
600 term_height = (uint16_t)term_size.rows;
601 } else {
602 log_debug("SDP: Using default terminal size %ux%u", term_width, term_height);
603 }
604
605 // Fill capabilities array based on detected color level (preference order)
606 size_t idx = 0;
607
608 if (color_level >= TERM_COLOR_TRUECOLOR && idx < capability_count) {
609 capabilities[idx].codec = ACIP_CODEC_TRUECOLOR;
610 capabilities[idx].format.width = term_width;
611 capabilities[idx].format.height = term_height;
612 capabilities[idx].format.renderer = RENDERER_BLOCK;
613 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
614 capabilities[idx].format.compression = COMPRESSION_RLE;
615 capabilities[idx].format.csi_rep_support = has_csi_rep;
616 idx++;
617 }
618
619 if (color_level >= TERM_COLOR_256 && idx < capability_count) {
620 capabilities[idx].codec = ACIP_CODEC_256COLOR;
621 capabilities[idx].format.width = term_width;
622 capabilities[idx].format.height = term_height;
623 capabilities[idx].format.renderer = RENDERER_BLOCK;
624 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
625 capabilities[idx].format.compression = COMPRESSION_RLE;
626 capabilities[idx].format.csi_rep_support = has_csi_rep;
627 idx++;
628 }
629
630 if (color_level >= TERM_COLOR_16 && idx < capability_count) {
631 capabilities[idx].codec = ACIP_CODEC_16COLOR;
632 capabilities[idx].format.width = term_width;
633 capabilities[idx].format.height = term_height;
634 capabilities[idx].format.renderer = RENDERER_BLOCK;
635 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
636 capabilities[idx].format.compression = COMPRESSION_NONE;
637 capabilities[idx].format.csi_rep_support = has_csi_rep;
638 idx++;
639 }
640
641 // Monochrome always supported
642 if (idx < capability_count) {
643 capabilities[idx].codec = ACIP_CODEC_MONO;
644 capabilities[idx].format.width = term_width;
645 capabilities[idx].format.height = term_height;
646 capabilities[idx].format.renderer = RENDERER_BLOCK;
647 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
648 capabilities[idx].format.compression = COMPRESSION_NONE;
649 capabilities[idx].format.csi_rep_support = has_csi_rep;
650 idx++;
651 }
652
653 *detected_count = idx;
654
655 log_debug("SDP: Detected %zu terminal capabilities (colors=%d, utf8=%s, csi_rep=%s, size=%ux%u)", *detected_count,
656 color_level, has_utf8 ? "yes" : "no", has_csi_rep ? "yes" : "no", term_width, term_height);
657
658 return ASCIICHAT_OK;
659}
660
661/* ============================================================================
662 * Resource Cleanup
663 * ============================================================================ */
664
666 if (!session) {
667 return;
668 }
669
670 // Free allocated video codec array
671 if (session->video_codecs) {
672 SAFE_FREE(session->video_codecs);
673 session->video_codec_count = 0;
674 }
675
676 // Zero out the session structure
677 memset(session, 0, sizeof(sdp_session_t));
678}
unsigned short uint16_t
Definition common.h:57
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_FREE(ptr)
Definition common.h:320
#define SAFE_MALLOC(size, cast)
Definition common.h:208
#define SAFE_GETENV(name)
Definition common.h:378
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ERROR_NOT_FOUND
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_INVALID_PARAM
#define log_debug(...)
Log a DEBUG message.
asciichat_error_t terminal_get_size(terminal_size_t *size)
Get terminal size.
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:424
@ TERM_COLOR_NONE
No color support (monochrome terminal)
Definition terminal.h:428
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:430
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:432
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:434
📝 Logging API with multiple log levels and terminal output control
asciichat_error_t sdp_detect_terminal_capabilities(terminal_capability_t *capabilities, size_t capability_count, size_t *detected_count)
Detect client terminal capabilities at startup.
Definition sdp.c:561
asciichat_error_t sdp_parse(const char *sdp_string, sdp_session_t *session)
Parse SDP offer or answer.
Definition sdp.c:318
const char * sdp_codec_name(acip_codec_t codec)
Get human-readable codec name.
Definition sdp.c:21
void sdp_session_free(sdp_session_t *session)
Free SDP session resources.
Definition sdp.c:665
asciichat_error_t sdp_get_selected_video_codec(const sdp_session_t *answer, acip_codec_t *selected_codec, terminal_format_params_t *selected_format)
Extract selected video codec from SDP answer.
Definition sdp.c:534
asciichat_error_t sdp_generate_offer(const terminal_capability_t *capabilities, size_t capability_count, const opus_config_t *audio_config, const terminal_format_params_t *format, sdp_session_t *offer_out)
Generate SDP offer (client side)
Definition sdp.c:53
const char * sdp_renderer_name(int renderer_type)
Get human-readable renderer name.
Definition sdp.c:36
asciichat_error_t sdp_generate_answer(const sdp_session_t *offer, const terminal_capability_t *server_capabilities, size_t server_capability_count, const opus_config_t *audio_config, const terminal_format_params_t *server_format, sdp_session_t *answer_out)
Generate SDP answer (server side)
Definition sdp.c:168
SDP (Session Description Protocol) for WebRTC audio/video negotiation.
acip_codec_t
Terminal rendering capability payload types.
Definition sdp.h:76
@ ACIP_CODEC_16COLOR
16-color (ANSI standard)
Definition sdp.h:79
@ ACIP_CODEC_MONO
Monochrome (ASCII only)
Definition sdp.h:80
@ ACIP_CODEC_256COLOR
256-color (xterm palette)
Definition sdp.h:78
@ ACIP_CODEC_TRUECOLOR
24-bit RGB (truecolor)
Definition sdp.h:77
Opus codec parameters for ascii-chat.
Definition sdp.h:57
bool dtx_enabled
Discontinuous Transmission (silence suppression)
Definition sdp.h:62
bool fec_enabled
Forward Error Correction for lossy networks.
Definition sdp.h:63
uint32_t sample_rate
48000 Hz (Opus native rate)
Definition sdp.h:58
uint32_t bitrate
24000 bps (good quality for speech)
Definition sdp.h:60
uint16_t frame_duration
20 ms (balance latency/efficiency)
Definition sdp.h:61
uint8_t channels
1 (mono for voice chat)
Definition sdp.h:59
SDP media session (simplified for WebRTC)
Definition sdp.h:114
terminal_format_params_t video_format
Default format parameters.
Definition sdp.h:127
size_t video_codec_count
Number of supported capabilities.
Definition sdp.h:126
char session_version[16]
Session version (timestamp)
Definition sdp.h:117
size_t sdp_length
Length of SDP string (excluding null)
Definition sdp.h:131
bool has_audio
Audio media included.
Definition sdp.h:120
char session_id[32]
Session identifier.
Definition sdp.h:116
terminal_capability_t * video_codecs
Array of supported capabilities.
Definition sdp.h:125
opus_config_t audio_config
Opus codec configuration.
Definition sdp.h:121
bool has_video
Video media included.
Definition sdp.h:124
char sdp_string[4096]
Complete SDP as null-terminated string.
Definition sdp.h:130
Supported terminal capability (for offer/answer)
Definition sdp.h:99
terminal_format_params_t format
Format parameters.
Definition sdp.h:101
acip_codec_t codec
Rendering capability type.
Definition sdp.h:100
Terminal rendering format parameters.
Definition sdp.h:86
enum terminal_format_params_t::@5 charset
bool csi_rep_support
CSI REP (repeat) support.
Definition sdp.h:92
enum terminal_format_params_t::@4 renderer
uint16_t height
Terminal height in characters.
Definition sdp.h:88
uint16_t width
Terminal width in characters.
Definition sdp.h:87
enum terminal_format_params_t::@6 compression
Terminal size structure.
Definition terminal.h:55
int cols
Number of columns (width) in terminal.
Definition terminal.h:57
int rows
Number of rows (height) in terminal.
Definition terminal.h:56
⏱️ High-precision timing utilities using sokol_time.h and uthash