ascii-chat 0.8.38
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 <ascii-chat/network/webrtc/sdp.h>
12#include <ascii-chat/log/logging.h>
13#include <ascii-chat/platform/util.h>
14#include <ascii-chat/util/pcre2.h>
15#include <string.h>
16#include <stdio.h>
17#include <time.h>
18#include <pcre2.h>
19
20/* ============================================================================
21 * Utility Functions
22 * ============================================================================ */
23
24const char *sdp_codec_name(acip_codec_t codec) {
25 switch (codec) {
26 case ACIP_CODEC_TRUECOLOR:
27 return "ACIP-TC";
28 case ACIP_CODEC_256COLOR:
29 return "ACIP-256";
30 case ACIP_CODEC_16COLOR:
31 return "ACIP-16";
32 case ACIP_CODEC_MONO:
33 return "ACIP-MONO";
34 default:
35 return "UNKNOWN";
36 }
37}
38
39const char *sdp_renderer_name(int renderer_type) {
40 switch (renderer_type) {
41 case RENDERER_BLOCK:
42 return "block";
43 case RENDERER_HALFBLOCK:
44 return "halfblock";
45 case RENDERER_BRAILLE:
46 return "braille";
47 default:
48 return "unknown";
49 }
50}
51
52/* ============================================================================
53 * SDP Offer Generation
54 * ============================================================================ */
55
56asciichat_error_t sdp_generate_offer(const terminal_capability_t *capabilities, size_t capability_count,
57 const opus_config_t *audio_config, const terminal_format_params_t *format,
58 sdp_session_t *offer_out) {
59 if (!capabilities || capability_count == 0 || !audio_config || !offer_out) {
60 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP offer parameters");
61 }
62
63 memset(offer_out, 0, sizeof(sdp_session_t));
64
65 // Generate session ID and version from current time
66 time_t now = time(NULL);
67 safe_snprintf(offer_out->session_id, sizeof(offer_out->session_id), "%ld", now);
68 safe_snprintf(offer_out->session_version, sizeof(offer_out->session_version), "%ld", now);
69
70 // Allocate and copy video codecs
71 offer_out->video_codecs = SAFE_MALLOC(capability_count * sizeof(terminal_capability_t), terminal_capability_t *);
72 memcpy(offer_out->video_codecs, capabilities, capability_count * sizeof(terminal_capability_t));
73 offer_out->video_codec_count = capability_count;
74
75 offer_out->has_audio = true;
76 memcpy(&offer_out->audio_config, audio_config, sizeof(opus_config_t));
77
78 offer_out->has_video = true;
79 if (format) {
80 memcpy(&offer_out->video_format, format, sizeof(terminal_format_params_t));
81 }
82
83 // Build complete SDP offer
84 char *sdp = offer_out->sdp_string;
85 size_t remaining = sizeof(offer_out->sdp_string);
86 int written = 0;
87
88 // Session-level attributes
89 written = safe_snprintf(sdp, remaining, "v=0\r\n");
90 sdp += written;
91 remaining -= written;
92
93 written = safe_snprintf(sdp, remaining, "o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", offer_out->session_id,
94 offer_out->session_version);
95 sdp += written;
96 remaining -= written;
97
98 written = safe_snprintf(sdp, remaining, "s=-\r\n");
99 sdp += written;
100 remaining -= written;
101
102 written = safe_snprintf(sdp, remaining, "t=0 0\r\n");
103 sdp += written;
104 remaining -= written;
105
106 // Audio media section
107 written = safe_snprintf(sdp, remaining, "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
108 sdp += written;
109 remaining -= written;
110
111 written = safe_snprintf(sdp, remaining, "a=rtpmap:111 opus/48000/2\r\n");
112 sdp += written;
113 remaining -= written;
114
115 written = safe_snprintf(sdp, remaining, "a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
116 sdp += written;
117 remaining -= written;
118
119 // Video media section with terminal capabilities
120 char codec_list[128] = "96";
121 size_t codec_pos = strlen(codec_list);
122 for (size_t i = 1; i < capability_count && codec_pos < sizeof(codec_list) - 1; i++) {
123 codec_pos += safe_snprintf(codec_list + codec_pos, sizeof(codec_list) - codec_pos, " %d", 96 + (int)i);
124 }
125
126 written = safe_snprintf(sdp, remaining, "m=video 9 UDP/TLS/RTP/SAVPF %s\r\n", codec_list);
127 sdp += written;
128 remaining -= written;
129
130 // Add rtpmap and fmtp for each capability
131 for (size_t i = 0; i < capability_count; i++) {
132 int pt = 96 + (int)i;
133 const char *codec_name = sdp_codec_name(capabilities[i].codec);
134
135 written = safe_snprintf(sdp, remaining, "a=rtpmap:%d %s/90000\r\n", pt, codec_name);
136 sdp += written;
137 remaining -= written;
138
139 // Format parameters
140 const terminal_format_params_t *cap_format = &capabilities[i].format;
141 const char *renderer_name = sdp_renderer_name(cap_format->renderer);
142 const char *charset_name = (cap_format->charset == CHARSET_UTF8) ? "utf8" : "ascii";
143 const char *compression_name = "none";
144 if (cap_format->compression == COMPRESSION_RLE) {
145 compression_name = "rle";
146 } else if (cap_format->compression == COMPRESSION_ZSTD) {
147 compression_name = "zstd";
148 }
149
150 written = safe_snprintf(sdp, remaining,
151 "a=fmtp:%d width=%u;height=%u;renderer=%s;charset=%s;compression=%s;csi_rep=%d\r\n", pt,
152 cap_format->width, cap_format->height, renderer_name, charset_name, compression_name,
153 cap_format->csi_rep_support ? 1 : 0);
154 sdp += written;
155 remaining -= written;
156 }
157
158 offer_out->sdp_length = strlen(offer_out->sdp_string);
159
160 log_debug("SDP: Generated offer with %zu video codecs and Opus audio", capability_count);
161
162 return ASCIICHAT_OK;
163}
164
165/* ============================================================================
166 * SDP Answer Generation
167 * ============================================================================ */
168
169asciichat_error_t sdp_generate_answer(const sdp_session_t *offer, const terminal_capability_t *server_capabilities,
170 size_t server_capability_count, const opus_config_t *audio_config,
171 const terminal_format_params_t *server_format, sdp_session_t *answer_out) {
172 if (!offer || !server_capabilities || server_capability_count == 0 || !audio_config || !answer_out) {
173 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP answer parameters");
174 }
175
176 memset(answer_out, 0, sizeof(sdp_session_t));
177
178 // Copy session ID from offer, increment version
179 SAFE_STRNCPY(answer_out->session_id, offer->session_id, sizeof(answer_out->session_id));
180
181 time_t now = time(NULL);
182 safe_snprintf(answer_out->session_version, sizeof(answer_out->session_version), "%ld", now);
183
184 // Answer audio section: accept Opus
185 answer_out->has_audio = offer->has_audio;
186 if (answer_out->has_audio) {
187 memcpy(&answer_out->audio_config, audio_config, sizeof(opus_config_t));
188 }
189
190 // Answer video section: find best mutually-supported codec
191 answer_out->has_video = offer->has_video;
192
193 if (answer_out->has_video && offer->video_codecs && offer->video_codec_count > 0) {
194 // Find best codec: iterate through server preferences and find first match in offer
195 int selected_index = -1;
196
197 for (size_t s = 0; s < server_capability_count; s++) {
198 for (size_t o = 0; o < offer->video_codec_count; o++) {
199 if (server_capabilities[s].codec == offer->video_codecs[o].codec) {
200 selected_index = (int)s;
201 break; // Found match, use server's preference order
202 }
203 }
204 if (selected_index >= 0) {
205 break;
206 }
207 }
208
209 // Allocate and fill selected codec
210 answer_out->video_codecs = SAFE_MALLOC(sizeof(terminal_capability_t), terminal_capability_t *);
211 answer_out->video_codec_count = 1;
212
213 // Use server's capability with any server_format overrides
214 if (selected_index >= 0) {
215 memcpy(answer_out->video_codecs, &server_capabilities[selected_index], sizeof(terminal_capability_t));
216 } else {
217 // Fallback to monochrome
218 memcpy(answer_out->video_codecs, &server_capabilities[0], sizeof(terminal_capability_t));
219 }
220
221 // Apply server format constraints if provided
222 if (server_format) {
223 if (server_format->width > 0) {
224 answer_out->video_codecs[0].format.width = server_format->width;
225 }
226 if (server_format->height > 0) {
227 answer_out->video_codecs[0].format.height = server_format->height;
228 }
229 if (server_format->renderer != RENDERER_BLOCK) {
230 answer_out->video_codecs[0].format.renderer = server_format->renderer;
231 }
232 if (server_format->compression != COMPRESSION_NONE) {
233 answer_out->video_codecs[0].format.compression = server_format->compression;
234 }
235 }
236
237 memcpy(&answer_out->video_format, &answer_out->video_codecs[0].format, sizeof(terminal_format_params_t));
238 }
239
240 // Build complete SDP answer
241 char *sdp = answer_out->sdp_string;
242 size_t remaining = sizeof(answer_out->sdp_string);
243 int written = 0;
244
245 // Session-level attributes
246 written = safe_snprintf(sdp, remaining, "v=0\r\n");
247 sdp += written;
248 remaining -= written;
249
250 written = safe_snprintf(sdp, remaining, "o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", answer_out->session_id,
251 answer_out->session_version);
252 sdp += written;
253 remaining -= written;
254
255 written = safe_snprintf(sdp, remaining, "s=-\r\n");
256 sdp += written;
257 remaining -= written;
258
259 written = safe_snprintf(sdp, remaining, "t=0 0\r\n");
260 sdp += written;
261 remaining -= written;
262
263 // Audio media section (if present in offer)
264 if (answer_out->has_audio) {
265 written = safe_snprintf(sdp, remaining, "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
266 sdp += written;
267 remaining -= written;
268
269 written = safe_snprintf(sdp, remaining, "a=rtpmap:111 opus/48000/2\r\n");
270 sdp += written;
271 remaining -= written;
272
273 written = safe_snprintf(sdp, remaining, "a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
274 sdp += written;
275 remaining -= written;
276 }
277
278 // Video media section (if present in offer)
279 if (answer_out->has_video && answer_out->video_codecs && answer_out->video_codec_count > 0) {
280 written = safe_snprintf(sdp, remaining, "m=video 9 UDP/TLS/RTP/SAVPF 96\r\n");
281 sdp += written;
282 remaining -= written;
283
284 const char *codec_name = sdp_codec_name(answer_out->video_codecs[0].codec);
285 written = safe_snprintf(sdp, remaining, "a=rtpmap:96 %s/90000\r\n", codec_name);
286 sdp += written;
287 remaining -= written;
288
289 // Format parameters
290 const terminal_format_params_t *cap_format = &answer_out->video_codecs[0].format;
291 const char *renderer_name = sdp_renderer_name(cap_format->renderer);
292 const char *charset_name = (cap_format->charset == CHARSET_UTF8) ? "utf8" : "ascii";
293 const char *compression_name = "none";
294 if (cap_format->compression == COMPRESSION_RLE) {
295 compression_name = "rle";
296 } else if (cap_format->compression == COMPRESSION_ZSTD) {
297 compression_name = "zstd";
298 }
299
300 written = safe_snprintf(sdp, remaining,
301 "a=fmtp:96 width=%u;height=%u;renderer=%s;charset=%s;compression=%s;csi_rep=%d\r\n",
302 cap_format->width, cap_format->height, renderer_name, charset_name, compression_name,
303 cap_format->csi_rep_support ? 1 : 0);
304 sdp += written;
305 remaining -= written;
306 }
307
308 answer_out->sdp_length = strlen(answer_out->sdp_string);
309
310 log_debug("SDP: Generated answer with codec %s", sdp_codec_name(answer_out->video_codecs[0].codec));
311
312 return ASCIICHAT_OK;
313}
314
315/* ============================================================================
316 * SDP Parsing with PCRE2
317 * ============================================================================ */
318
326static const char *SDP_FMTP_VIDEO_PATTERN = "width=([0-9]+)" // 1: width
327 ".*?height=([0-9]+)" // 2: height
328 ".*?renderer=([a-z]+)" // 3: renderer (block/halfblock/braille)
329 "(?:.*?charset=([a-z0-9_]+))?" // 4: charset (optional)
330 "(?:.*?compression=([a-z0-9]+))?" // 5: compression (optional)
331 "(?:.*?csi_rep=([01]))?"; // 6: csi_rep (optional)
332
333static pcre2_singleton_t *g_sdp_fmtp_video_regex = NULL;
334
339static pcre2_code *sdp_fmtp_video_regex_get(void) {
340 if (g_sdp_fmtp_video_regex == NULL) {
341 g_sdp_fmtp_video_regex = asciichat_pcre2_singleton_compile(SDP_FMTP_VIDEO_PATTERN, PCRE2_DOTALL);
342 }
343 return asciichat_pcre2_singleton_get_code(g_sdp_fmtp_video_regex);
344}
345
356static bool sdp_parse_fmtp_video_pcre2(const char *fmtp_params, terminal_capability_t *cap) {
357 if (!fmtp_params || !cap) {
358 return false;
359 }
360
361 // Get compiled regex (lazy initialization)
362 pcre2_code *regex = sdp_fmtp_video_regex_get();
363
364 // If PCRE2 not available, fall through to manual parsing
365 if (!regex) {
366 return false; // Fall back to manual parser in caller
367 }
368
369 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
370 if (!match_data) {
371 return false;
372 }
373
374 int rc = pcre2_jit_match(regex, (PCRE2_SPTR8)fmtp_params, strlen(fmtp_params), 0, 0, match_data, NULL);
375
376 bool success = false;
377 if (rc >= 4) { // At least 4 groups (width, height, renderer must be present)
378 // Extract width (group 1)
379 unsigned long width;
380 if (asciichat_pcre2_extract_group_ulong(match_data, 1, fmtp_params, &width)) {
381 cap->format.width = (uint16_t)width;
382 }
383
384 // Extract height (group 2)
385 unsigned long height;
386 if (asciichat_pcre2_extract_group_ulong(match_data, 2, fmtp_params, &height)) {
387 cap->format.height = (uint16_t)height;
388 }
389
390 // Extract renderer (group 3)
391 char *renderer_str = asciichat_pcre2_extract_group(match_data, 3, fmtp_params);
392 if (renderer_str) {
393 if (strcmp(renderer_str, "block") == 0) {
394 cap->format.renderer = RENDERER_BLOCK;
395 } else if (strcmp(renderer_str, "halfblock") == 0) {
396 cap->format.renderer = RENDERER_HALFBLOCK;
397 } else if (strcmp(renderer_str, "braille") == 0) {
398 cap->format.renderer = RENDERER_BRAILLE;
399 }
400 SAFE_FREE(renderer_str);
401 }
402
403 // Extract charset (group 4, optional)
404 char *charset_str = asciichat_pcre2_extract_group(match_data, 4, fmtp_params);
405 if (charset_str) {
406 if (strcmp(charset_str, "utf8_wide") == 0) {
407 cap->format.charset = CHARSET_UTF8_WIDE;
408 } else if (strcmp(charset_str, "utf8") == 0) {
409 cap->format.charset = CHARSET_UTF8;
410 } else {
411 cap->format.charset = CHARSET_ASCII;
412 }
413 SAFE_FREE(charset_str);
414 }
415
416 // Extract compression (group 5, optional)
417 char *compression_str = asciichat_pcre2_extract_group(match_data, 5, fmtp_params);
418 if (compression_str) {
419 if (strcmp(compression_str, "zstd") == 0) {
420 cap->format.compression = COMPRESSION_ZSTD;
421 } else if (strcmp(compression_str, "rle") == 0) {
422 cap->format.compression = COMPRESSION_RLE;
423 } else {
424 cap->format.compression = COMPRESSION_NONE;
425 }
426 SAFE_FREE(compression_str);
427 }
428
429 // Extract csi_rep (group 6, optional)
430 char *csi_rep_str = asciichat_pcre2_extract_group(match_data, 6, fmtp_params);
431 if (csi_rep_str) {
432 cap->format.csi_rep_support = (csi_rep_str[0] == '1');
433 SAFE_FREE(csi_rep_str);
434 }
435
436 success = true;
437 }
438
439 pcre2_match_data_free(match_data);
440 return success;
441}
442
443asciichat_error_t sdp_parse(const char *sdp_string, sdp_session_t *session) {
444 if (!sdp_string || !session) {
445 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP parse parameters");
446 }
447
448 memset(session, 0, sizeof(sdp_session_t));
449
450 // Copy original SDP string for reference
451 SAFE_STRNCPY(session->sdp_string, sdp_string, sizeof(session->sdp_string));
452 session->sdp_length = strlen(sdp_string);
453
454 // Parse line by line
455 char line_buffer[512];
456 SAFE_STRNCPY(line_buffer, sdp_string, sizeof(line_buffer));
457
458 char *saveptr = NULL;
459 char *line = platform_strtok_r(line_buffer, "\r\n", &saveptr);
460
461 int in_audio_section = 0;
462 int in_video_section = 0;
463 size_t video_codec_index = 0;
464
465 while (line) {
466 // Parse line format: "key=value"
467 char *equals = strchr(line, '=');
468 if (!equals) {
469 line = platform_strtok_r(NULL, "\r\n", &saveptr);
470 continue;
471 }
472
473 char key = line[0];
474 const char *value = equals + 1;
475
476 switch (key) {
477 case 'v':
478 // v=0 (version)
479 break;
480
481 case 'o':
482 // o=username session_id session_version ... session_id ...
483 // Parse session ID (second field)
484 {
485 char o_copy[256];
486 SAFE_STRNCPY(o_copy, value, sizeof(o_copy));
487 char *o_saveptr = NULL;
488 platform_strtok_r(o_copy, " ", &o_saveptr); // username
489 char *session_id_str = platform_strtok_r(NULL, " ", &o_saveptr);
490 if (session_id_str) {
491 SAFE_STRNCPY(session->session_id, session_id_str, sizeof(session->session_id));
492 }
493 }
494 break;
495
496 case 's':
497 // s=session_name (typically "-" or empty)
498 break;
499
500 case 'm':
501 // m=audio 9 UDP/TLS/RTP/SAVPF ...
502 // m=video 9 UDP/TLS/RTP/SAVPF ...
503 in_audio_section = 0;
504 in_video_section = 0;
505
506 if (strstr(value, "audio")) {
507 in_audio_section = 1;
508 session->has_audio = true;
509 } else if (strstr(value, "video")) {
510 in_video_section = 1;
511 session->has_video = true;
512 video_codec_index = 0;
513 }
514 break;
515
516 case 'a':
517 // a=rtpmap:111 opus/48000/2
518 // a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
519 // a=rtpmap:96 ACIP-TC/90000
520 // a=fmtp:96 ...
521
522 if (strstr(value, "rtpmap:")) {
523 const char *rtpmap = value + 7; // Skip "rtpmap:"
524
525 // Parse: PT codec/rate[/channels]
526 char *endptr = NULL;
527 int pt = (int)strtol(rtpmap, &endptr, 10);
528 char codec_name[32] = {0};
529 if (endptr && *endptr == ' ') {
530 SAFE_STRNCPY(codec_name, endptr + 1, sizeof(codec_name));
531 }
532
533 if (in_audio_section && pt == 111 && strstr(codec_name, "opus")) {
534 // Opus audio
535 session->audio_config.sample_rate = 48000;
536 session->audio_config.channels = 2;
537 } else if (in_video_section && video_codec_index < 4) {
538 // Terminal capability codec
539 if (!session->video_codecs) {
540 session->video_codecs = SAFE_MALLOC(4 * sizeof(terminal_capability_t), terminal_capability_t *);
541 }
542
543 terminal_capability_t *cap = &session->video_codecs[video_codec_index];
544 memset(cap, 0, sizeof(terminal_capability_t));
545
546 if (pt == 96 && strstr(codec_name, "ACIP-TC")) {
547 cap->codec = ACIP_CODEC_TRUECOLOR;
548 video_codec_index++;
549 session->video_codec_count++;
550 } else if (pt == 97 && strstr(codec_name, "ACIP-256")) {
551 cap->codec = ACIP_CODEC_256COLOR;
552 video_codec_index++;
553 session->video_codec_count++;
554 } else if (pt == 98 && strstr(codec_name, "ACIP-16")) {
555 cap->codec = ACIP_CODEC_16COLOR;
556 video_codec_index++;
557 session->video_codec_count++;
558 } else if (pt == 99 && strstr(codec_name, "ACIP-MONO")) {
559 cap->codec = ACIP_CODEC_MONO;
560 video_codec_index++;
561 session->video_codec_count++;
562 }
563 }
564 } else if (strstr(value, "fmtp:")) {
565 // Parse format parameters
566 // a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
567 // a=fmtp:96 width=80;height=24;renderer=block;charset=utf8;compression=rle;csi_rep=1
568
569 const char *fmtp = value + 5; // Skip "fmtp:"
570
571 if (in_audio_section) {
572 // Parse Opus parameters
573 if (strstr(fmtp, "useinbandfec=1")) {
574 session->audio_config.fec_enabled = true;
575 }
576 if (strstr(fmtp, "usedtx=1")) {
577 session->audio_config.dtx_enabled = true;
578 }
579 // Default values for Opus
580 session->audio_config.bitrate = 24000;
581 session->audio_config.frame_duration = 20;
582 } else if (in_video_section && video_codec_index > 0) {
583 // Parse terminal format parameters
584 terminal_capability_t *cap = &session->video_codecs[video_codec_index - 1];
585
586 // Skip PT number in fmtp string (format: "96 width=...")
587 const char *params = fmtp;
588 while (*params && *params != ' ') {
589 params++;
590 }
591 if (*params == ' ') {
592 params++; // Skip space after PT
593 }
594
595 // Parse with PCRE2 regex (atomic extraction)
596 sdp_parse_fmtp_video_pcre2(params, cap);
597 }
598 }
599 break;
600 }
601
602 line = platform_strtok_r(NULL, "\r\n", &saveptr);
603 }
604
605 return ASCIICHAT_OK;
606}
607
608/* ============================================================================
609 * Video Codec Selection
610 * ============================================================================ */
611
612asciichat_error_t sdp_get_selected_video_codec(const sdp_session_t *answer, acip_codec_t *selected_codec,
613 terminal_format_params_t *selected_format) {
614 if (!answer || !selected_codec || !selected_format) {
615 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for codec selection");
616 }
617
618 if (!answer->has_video || !answer->video_codecs || answer->video_codec_count == 0) {
619 return SET_ERRNO(ERROR_NOT_FOUND, "No video codec in answer");
620 }
621
622 // In SDP answer, server selects only ONE codec (server's preference)
623 // The first codec in the answer is the selected one
624 terminal_capability_t *selected_cap = &answer->video_codecs[0];
625
626 *selected_codec = selected_cap->codec;
627 memcpy(selected_format, &selected_cap->format, sizeof(terminal_format_params_t));
628
629 log_debug("SDP: Selected video codec %s with resolution %ux%u", sdp_codec_name(selected_cap->codec),
630 selected_cap->format.width, selected_cap->format.height);
631
632 return ASCIICHAT_OK;
633}
634
635/* ============================================================================
636 * Terminal Capability Detection
637 * ============================================================================ */
638
639asciichat_error_t sdp_detect_terminal_capabilities(terminal_capability_t *capabilities, size_t capability_count,
640 size_t *detected_count) {
641 if (!capabilities || capability_count == 0 || !detected_count) {
642 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid capability detection parameters");
643 }
644
645 *detected_count = 0;
646 memset(capabilities, 0, capability_count * sizeof(terminal_capability_t));
647
648 // Detect terminal color level using platform terminal module
649 const char *colorterm = SAFE_GETENV("COLORTERM");
650 const char *term = SAFE_GETENV("TERM");
651 terminal_color_mode_t color_level = TERM_COLOR_NONE;
652
653 if (colorterm && (strcmp(colorterm, "truecolor") == 0 || strcmp(colorterm, "24bit") == 0)) {
654 color_level = TERM_COLOR_TRUECOLOR;
655 } else if (term) {
656 if (strstr(term, "256color") || strstr(term, "256")) {
657 color_level = TERM_COLOR_256;
658 } else if (strstr(term, "color") || strcmp(term, "xterm") == 0) {
659 color_level = TERM_COLOR_16;
660 }
661 }
662
663 // Detect UTF-8 support from LANG environment variable
664 const char *lang = SAFE_GETENV("LANG");
665 bool has_utf8 = (lang && (strstr(lang, "UTF-8") || strstr(lang, "utf8")));
666
667 // CSI REP support correlates with UTF-8
668 bool has_csi_rep = has_utf8;
669
670 // Get terminal size from platform module
671 uint16_t term_width = 80; // default
672 uint16_t term_height = 24; // default
673
674 terminal_size_t term_size;
675 asciichat_error_t term_err = terminal_get_size(&term_size);
676 if (term_err == ASCIICHAT_OK) {
677 term_width = (uint16_t)term_size.cols;
678 term_height = (uint16_t)term_size.rows;
679 } else {
680 log_debug("SDP: Using default terminal size %ux%u", term_width, term_height);
681 }
682
683 // Fill capabilities array based on detected color level (preference order)
684 size_t idx = 0;
685
686 if (color_level >= TERM_COLOR_TRUECOLOR && idx < capability_count) {
687 capabilities[idx].codec = ACIP_CODEC_TRUECOLOR;
688 capabilities[idx].format.width = term_width;
689 capabilities[idx].format.height = term_height;
690 capabilities[idx].format.renderer = RENDERER_BLOCK;
691 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
692 capabilities[idx].format.compression = COMPRESSION_RLE;
693 capabilities[idx].format.csi_rep_support = has_csi_rep;
694 idx++;
695 }
696
697 if (color_level >= TERM_COLOR_256 && idx < capability_count) {
698 capabilities[idx].codec = ACIP_CODEC_256COLOR;
699 capabilities[idx].format.width = term_width;
700 capabilities[idx].format.height = term_height;
701 capabilities[idx].format.renderer = RENDERER_BLOCK;
702 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
703 capabilities[idx].format.compression = COMPRESSION_RLE;
704 capabilities[idx].format.csi_rep_support = has_csi_rep;
705 idx++;
706 }
707
708 if (color_level >= TERM_COLOR_16 && idx < capability_count) {
709 capabilities[idx].codec = ACIP_CODEC_16COLOR;
710 capabilities[idx].format.width = term_width;
711 capabilities[idx].format.height = term_height;
712 capabilities[idx].format.renderer = RENDERER_BLOCK;
713 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
714 capabilities[idx].format.compression = COMPRESSION_NONE;
715 capabilities[idx].format.csi_rep_support = has_csi_rep;
716 idx++;
717 }
718
719 // Monochrome always supported
720 if (idx < capability_count) {
721 capabilities[idx].codec = ACIP_CODEC_MONO;
722 capabilities[idx].format.width = term_width;
723 capabilities[idx].format.height = term_height;
724 capabilities[idx].format.renderer = RENDERER_BLOCK;
725 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
726 capabilities[idx].format.compression = COMPRESSION_NONE;
727 capabilities[idx].format.csi_rep_support = has_csi_rep;
728 idx++;
729 }
730
731 *detected_count = idx;
732
733 log_debug("SDP: Detected %zu terminal capabilities (colors=%d, utf8=%s, csi_rep=%s, size=%ux%u)", *detected_count,
734 color_level, has_utf8 ? "yes" : "no", has_csi_rep ? "yes" : "no", term_width, term_height);
735
736 return ASCIICHAT_OK;
737}
738
739/* ============================================================================
740 * Resource Cleanup
741 * ============================================================================ */
742
743void sdp_session_destroy(sdp_session_t *session) {
744 if (!session) {
745 return;
746 }
747
748 // Free allocated video codec array
749 if (session->video_codecs) {
750 SAFE_FREE(session->video_codecs);
751 session->video_codec_count = 0;
752 }
753
754 // Zero out the session structure
755 memset(session, 0, sizeof(sdp_session_t));
756}
char ** line_buffer
Circular buffer for context_before.
Definition grep.c:83
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
Definition pcre2.c:95
char * asciichat_pcre2_extract_group(pcre2_match_data *match_data, int group_num, const char *subject)
Extract numbered capture group as allocated string.
Definition pcre2.c:291
bool asciichat_pcre2_extract_group_ulong(pcre2_match_data *match_data, int group_num, const char *subject, unsigned long *out_value)
Extract numbered capture group and convert to unsigned long.
Definition pcre2.c:344
asciichat_error_t terminal_get_size(terminal_size_t *size)
asciichat_error_t sdp_detect_terminal_capabilities(terminal_capability_t *capabilities, size_t capability_count, size_t *detected_count)
Definition sdp.c:639
asciichat_error_t sdp_parse(const char *sdp_string, sdp_session_t *session)
Definition sdp.c:443
const char * sdp_codec_name(acip_codec_t codec)
Definition sdp.c:24
asciichat_error_t sdp_get_selected_video_codec(const sdp_session_t *answer, acip_codec_t *selected_codec, terminal_format_params_t *selected_format)
Definition sdp.c:612
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)
Definition sdp.c:56
const char * sdp_renderer_name(int renderer_type)
Definition sdp.c:39
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)
Definition sdp.c:169
void sdp_session_destroy(sdp_session_t *session)
Definition sdp.c:743
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:21
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456