ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
sdp.c
Go to the documentation of this file.
1
13#include <ascii-chat/log/log.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) {
27 return "ACIP-TC";
29 return "ACIP-256";
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 uint32_t video_codec_caps, uint32_t audio_codec_caps, 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 (terminal capabilities)
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 // Store codec capabilities (for SDP negotiation)
76 offer_out->codec_capabilities_video = video_codec_caps;
77 offer_out->codec_capabilities_audio = audio_codec_caps;
78
79 offer_out->has_audio = true;
80 memcpy(&offer_out->audio_config, audio_config, sizeof(opus_config_t));
81
82 offer_out->has_video = true;
83 if (format) {
84 memcpy(&offer_out->video_format, format, sizeof(terminal_format_params_t));
85 }
86
87 // Build complete SDP offer
88 char *sdp = offer_out->sdp_string;
89 size_t remaining = sizeof(offer_out->sdp_string);
90 int written = 0;
91
92 // Session-level attributes
93 written = safe_snprintf(sdp, remaining, "v=0\r\n");
94 sdp += written;
95 remaining -= written;
96
97 written = safe_snprintf(sdp, remaining, "o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", offer_out->session_id,
98 offer_out->session_version);
99 sdp += written;
100 remaining -= written;
101
102 written = safe_snprintf(sdp, remaining, "s=-\r\n");
103 sdp += written;
104 remaining -= written;
105
106 written = safe_snprintf(sdp, remaining, "t=0 0\r\n");
107 sdp += written;
108 remaining -= written;
109
110 // Audio media section
111 written = safe_snprintf(sdp, remaining, "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
112 sdp += written;
113 remaining -= written;
114
115 written = safe_snprintf(sdp, remaining, "a=rtpmap:111 opus/48000/2\r\n");
116 sdp += written;
117 remaining -= written;
118
119 written = safe_snprintf(sdp, remaining, "a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
120 sdp += written;
121 remaining -= written;
122
123 // Audio codec capabilities (custom attribute)
124 written = safe_snprintf(sdp, remaining, "a=codec-capabilities-audio:0x%x\r\n", audio_codec_caps);
125 sdp += written;
126 remaining -= written;
127
128 // Video media section with terminal capabilities
129 char codec_list[128] = "96";
130 size_t codec_pos = strlen(codec_list);
131 for (size_t i = 1; i < capability_count && codec_pos < sizeof(codec_list) - 1; i++) {
132 codec_pos += safe_snprintf(codec_list + codec_pos, sizeof(codec_list) - codec_pos, " %d", 96 + (int)i);
133 }
134
135 written = safe_snprintf(sdp, remaining, "m=video 9 UDP/TLS/RTP/SAVPF %s\r\n", codec_list);
136 sdp += written;
137 remaining -= written;
138
139 // Add rtpmap and fmtp for each capability
140 for (size_t i = 0; i < capability_count; i++) {
141 int pt = 96 + (int)i;
142 const char *codec_name = sdp_codec_name(capabilities[i].codec);
143
144 written = safe_snprintf(sdp, remaining, "a=rtpmap:%d %s/90000\r\n", pt, codec_name);
145 sdp += written;
146 remaining -= written;
147
148 // Format parameters
149 const terminal_format_params_t *cap_format = &capabilities[i].format;
150 const char *renderer_name = sdp_renderer_name(cap_format->renderer);
151 const char *charset_name = (cap_format->charset == CHARSET_UTF8) ? "utf8" : "ascii";
152 const char *compression_name = "none";
153 if (cap_format->compression == COMPRESSION_RLE) {
154 compression_name = "rle";
155 } else if (cap_format->compression == COMPRESSION_ZSTD) {
156 compression_name = "zstd";
157 }
158
159 written = safe_snprintf(sdp, remaining,
160 "a=fmtp:%d width=%u;height=%u;renderer=%s;charset=%s;compression=%s;csi_rep=%d\r\n", pt,
161 cap_format->width, cap_format->height, renderer_name, charset_name, compression_name,
162 cap_format->csi_rep_support ? 1 : 0);
163 sdp += written;
164 remaining -= written;
165 }
166
167 // Video codec capabilities (custom attribute)
168 written = safe_snprintf(sdp, remaining, "a=codec-capabilities-video:0x%x\r\n", video_codec_caps);
169 sdp += written;
170 remaining -= written;
171
172 offer_out->sdp_length = strlen(offer_out->sdp_string);
173
174 log_debug("SDP: Generated offer with %zu video codecs and Opus audio", capability_count);
175
176 return ASCIICHAT_OK;
177}
178
179/* ============================================================================
180 * SDP Answer Generation
181 * ============================================================================ */
182
184 size_t server_capability_count, const opus_config_t *audio_config,
185 const terminal_format_params_t *server_format, uint32_t server_video_codec_caps,
186 uint32_t server_audio_codec_caps, sdp_session_t *answer_out) {
187 if (!offer || !server_capabilities || server_capability_count == 0 || !audio_config || !answer_out) {
188 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP answer parameters");
189 }
190
191 memset(answer_out, 0, sizeof(sdp_session_t));
192
193 // Copy session ID from offer, increment version
194 SAFE_STRNCPY(answer_out->session_id, offer->session_id, sizeof(answer_out->session_id));
195
196 time_t now = time(NULL);
197 safe_snprintf(answer_out->session_version, sizeof(answer_out->session_version), "%ld", now);
198
199 // Answer audio section: accept Opus with codec capability negotiation
200 answer_out->has_audio = offer->has_audio;
201 if (answer_out->has_audio) {
202 memcpy(&answer_out->audio_config, audio_config, sizeof(opus_config_t));
203 // Negotiate audio codec capabilities: intersection of client and server
204 answer_out->codec_capabilities_audio = offer->codec_capabilities_audio & server_audio_codec_caps;
205 log_debug("SDP: Audio codec negotiation - client=0x%x server=0x%x negotiated=0x%x", offer->codec_capabilities_audio,
206 server_audio_codec_caps, answer_out->codec_capabilities_audio);
207 }
208
209 // Answer video section: find best mutually-supported codec with capability negotiation
210 answer_out->has_video = offer->has_video;
211
212 if (answer_out->has_video && offer->video_codecs && offer->video_codec_count > 0) {
213 // Negotiate video codec capabilities: intersection of client and server
214 answer_out->codec_capabilities_video = offer->codec_capabilities_video & server_video_codec_caps;
215 log_debug("SDP: Video codec negotiation - client=0x%x server=0x%x negotiated=0x%x", offer->codec_capabilities_video,
216 server_video_codec_caps, answer_out->codec_capabilities_video);
217
218 // Find best terminal capability codec: iterate through server preferences and find first match in offer
219 int selected_index = -1;
220
221 for (size_t s = 0; s < server_capability_count; s++) {
222 for (size_t o = 0; o < offer->video_codec_count; o++) {
223 if (server_capabilities[s].codec == offer->video_codecs[o].codec) {
224 selected_index = (int)s;
225 break; // Found match, use server's preference order
226 }
227 }
228 if (selected_index >= 0) {
229 break;
230 }
231 }
232
233 // Allocate and fill selected codec
235 answer_out->video_codec_count = 1;
236
237 // Use server's capability with any server_format overrides
238 if (selected_index >= 0) {
239 memcpy(answer_out->video_codecs, &server_capabilities[selected_index], sizeof(terminal_capability_t));
240 } else {
241 // Fallback to monochrome
242 memcpy(answer_out->video_codecs, &server_capabilities[0], sizeof(terminal_capability_t));
243 }
244
245 // Apply server format constraints if provided
246 if (server_format) {
247 if (server_format->width > 0) {
248 answer_out->video_codecs[0].format.width = server_format->width;
249 }
250 if (server_format->height > 0) {
251 answer_out->video_codecs[0].format.height = server_format->height;
252 }
253 if (server_format->renderer != RENDERER_BLOCK) {
254 answer_out->video_codecs[0].format.renderer = server_format->renderer;
255 }
256 if (server_format->compression != COMPRESSION_NONE) {
257 answer_out->video_codecs[0].format.compression = server_format->compression;
258 }
259 }
260
261 memcpy(&answer_out->video_format, &answer_out->video_codecs[0].format, sizeof(terminal_format_params_t));
262 }
263
264 // Build complete SDP answer
265 char *sdp = answer_out->sdp_string;
266 size_t remaining = sizeof(answer_out->sdp_string);
267 int written = 0;
268
269 // Session-level attributes
270 written = safe_snprintf(sdp, remaining, "v=0\r\n");
271 sdp += written;
272 remaining -= written;
273
274 written = safe_snprintf(sdp, remaining, "o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", answer_out->session_id,
275 answer_out->session_version);
276 sdp += written;
277 remaining -= written;
278
279 written = safe_snprintf(sdp, remaining, "s=-\r\n");
280 sdp += written;
281 remaining -= written;
282
283 written = safe_snprintf(sdp, remaining, "t=0 0\r\n");
284 sdp += written;
285 remaining -= written;
286
287 // Audio media section (if present in offer)
288 if (answer_out->has_audio) {
289 written = safe_snprintf(sdp, remaining, "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
290 sdp += written;
291 remaining -= written;
292
293 written = safe_snprintf(sdp, remaining, "a=rtpmap:111 opus/48000/2\r\n");
294 sdp += written;
295 remaining -= written;
296
297 written = safe_snprintf(sdp, remaining, "a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
298 sdp += written;
299 remaining -= written;
300
301 // Negotiated audio codec capabilities (custom attribute)
302 written =
303 safe_snprintf(sdp, remaining, "a=codec-capabilities-audio:0x%x\r\n", answer_out->codec_capabilities_audio);
304 sdp += written;
305 remaining -= written;
306 }
307
308 // Video media section (if present in offer)
309 if (answer_out->has_video && answer_out->video_codecs && answer_out->video_codec_count > 0) {
310 written = safe_snprintf(sdp, remaining, "m=video 9 UDP/TLS/RTP/SAVPF 96\r\n");
311 sdp += written;
312 remaining -= written;
313
314 const char *codec_name = sdp_codec_name(answer_out->video_codecs[0].codec);
315 written = safe_snprintf(sdp, remaining, "a=rtpmap:96 %s/90000\r\n", codec_name);
316 sdp += written;
317 remaining -= written;
318
319 // Format parameters
320 const terminal_format_params_t *cap_format = &answer_out->video_codecs[0].format;
321 const char *renderer_name = sdp_renderer_name(cap_format->renderer);
322 const char *charset_name = (cap_format->charset == CHARSET_UTF8) ? "utf8" : "ascii";
323 const char *compression_name = "none";
324 if (cap_format->compression == COMPRESSION_RLE) {
325 compression_name = "rle";
326 } else if (cap_format->compression == COMPRESSION_ZSTD) {
327 compression_name = "zstd";
328 }
329
330 written = safe_snprintf(sdp, remaining,
331 "a=fmtp:96 width=%u;height=%u;renderer=%s;charset=%s;compression=%s;csi_rep=%d\r\n",
332 cap_format->width, cap_format->height, renderer_name, charset_name, compression_name,
333 cap_format->csi_rep_support ? 1 : 0);
334 sdp += written;
335 remaining -= written;
336
337 // Negotiated video codec capabilities (custom attribute)
338 written =
339 safe_snprintf(sdp, remaining, "a=codec-capabilities-video:0x%x\r\n", answer_out->codec_capabilities_video);
340 sdp += written;
341 remaining -= written;
342 }
343
344 answer_out->sdp_length = strlen(answer_out->sdp_string);
345
346 log_debug("SDP: Generated answer with codec %s", sdp_codec_name(answer_out->video_codecs[0].codec));
347
348 return ASCIICHAT_OK;
349}
350
351/* ============================================================================
352 * SDP Parsing with PCRE2
353 * ============================================================================ */
354
362static const char *SDP_FMTP_VIDEO_PATTERN = "width=([0-9]+)" // 1: width
363 ".*?height=([0-9]+)" // 2: height
364 ".*?renderer=([a-z]+)" // 3: renderer (block/halfblock/braille)
365 "(?:.*?charset=([a-z0-9_]+))?" // 4: charset (optional)
366 "(?:.*?compression=([a-z0-9]+))?" // 5: compression (optional)
367 "(?:.*?csi_rep=([01]))?"; // 6: csi_rep (optional)
368
369static pcre2_singleton_t *g_sdp_fmtp_video_regex = NULL;
370
375static pcre2_code *sdp_fmtp_video_regex_get(void) {
376 if (g_sdp_fmtp_video_regex == NULL) {
377 g_sdp_fmtp_video_regex = asciichat_pcre2_singleton_compile(SDP_FMTP_VIDEO_PATTERN, PCRE2_DOTALL);
378 }
379 return asciichat_pcre2_singleton_get_code(g_sdp_fmtp_video_regex);
380}
381
392static bool sdp_parse_fmtp_video_pcre2(const char *fmtp_params, terminal_capability_t *cap) {
393 if (!fmtp_params || !cap) {
394 return false;
395 }
396
397 // Get compiled regex (lazy initialization)
398 pcre2_code *regex = sdp_fmtp_video_regex_get();
399
400 // If PCRE2 not available, fall through to manual parsing
401 if (!regex) {
402 return false; // Fall back to manual parser in caller
403 }
404
405 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
406 if (!match_data) {
407 return false;
408 }
409
410 int rc = pcre2_jit_match(regex, (PCRE2_SPTR8)fmtp_params, strlen(fmtp_params), 0, 0, match_data, NULL);
411
412 bool success = false;
413 if (rc >= 4) { // At least 4 groups (width, height, renderer must be present)
414 // Extract width (group 1)
415 unsigned long width;
416 if (asciichat_pcre2_extract_group_ulong(match_data, 1, fmtp_params, &width)) {
417 cap->format.width = (uint16_t)width;
418 }
419
420 // Extract height (group 2)
421 unsigned long height;
422 if (asciichat_pcre2_extract_group_ulong(match_data, 2, fmtp_params, &height)) {
423 cap->format.height = (uint16_t)height;
424 }
425
426 // Extract renderer (group 3)
427 char *renderer_str = asciichat_pcre2_extract_group(match_data, 3, fmtp_params);
428 if (renderer_str) {
429 if (strcmp(renderer_str, "block") == 0) {
430 cap->format.renderer = RENDERER_BLOCK;
431 } else if (strcmp(renderer_str, "halfblock") == 0) {
432 cap->format.renderer = RENDERER_HALFBLOCK;
433 } else if (strcmp(renderer_str, "braille") == 0) {
434 cap->format.renderer = RENDERER_BRAILLE;
435 }
436 SAFE_FREE(renderer_str);
437 }
438
439 // Extract charset (group 4, optional)
440 char *charset_str = asciichat_pcre2_extract_group(match_data, 4, fmtp_params);
441 if (charset_str) {
442 if (strcmp(charset_str, "utf8_wide") == 0) {
443 cap->format.charset = CHARSET_UTF8_WIDE;
444 } else if (strcmp(charset_str, "utf8") == 0) {
445 cap->format.charset = CHARSET_UTF8;
446 } else {
447 cap->format.charset = CHARSET_ASCII;
448 }
449 SAFE_FREE(charset_str);
450 }
451
452 // Extract compression (group 5, optional)
453 char *compression_str = asciichat_pcre2_extract_group(match_data, 5, fmtp_params);
454 if (compression_str) {
455 if (strcmp(compression_str, "zstd") == 0) {
456 cap->format.compression = COMPRESSION_ZSTD;
457 } else if (strcmp(compression_str, "rle") == 0) {
458 cap->format.compression = COMPRESSION_RLE;
459 } else {
460 cap->format.compression = COMPRESSION_NONE;
461 }
462 SAFE_FREE(compression_str);
463 }
464
465 // Extract csi_rep (group 6, optional)
466 char *csi_rep_str = asciichat_pcre2_extract_group(match_data, 6, fmtp_params);
467 if (csi_rep_str) {
468 cap->format.csi_rep_support = (csi_rep_str[0] == '1');
469 SAFE_FREE(csi_rep_str);
470 }
471
472 success = true;
473 }
474
475 pcre2_match_data_free(match_data);
476 return success;
477}
478
479asciichat_error_t sdp_parse(const char *sdp_string, sdp_session_t *session) {
480 if (!sdp_string || !session) {
481 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP parse parameters");
482 }
483
484 memset(session, 0, sizeof(sdp_session_t));
485
486 // Copy original SDP string for reference
487 SAFE_STRNCPY(session->sdp_string, sdp_string, sizeof(session->sdp_string));
488 session->sdp_length = strlen(sdp_string);
489
490 // Parse line by line
491 char line_buffer[512];
492 SAFE_STRNCPY(line_buffer, sdp_string, sizeof(line_buffer));
493
494 char *saveptr = NULL;
495 char *line = platform_strtok_r(line_buffer, "\r\n", &saveptr);
496
497 int in_audio_section = 0;
498 int in_video_section = 0;
499 size_t video_codec_index = 0;
500
501 while (line) {
502 // Parse line format: "key=value"
503 char *equals = strchr(line, '=');
504 if (!equals) {
505 line = platform_strtok_r(NULL, "\r\n", &saveptr);
506 continue;
507 }
508
509 char key = line[0];
510 const char *value = equals + 1;
511
512 switch (key) {
513 case 'v':
514 // v=0 (version)
515 break;
516
517 case 'o':
518 // o=username session_id session_version ... session_id ...
519 // Parse session ID (second field)
520 {
521 char o_copy[256];
522 SAFE_STRNCPY(o_copy, value, sizeof(o_copy));
523 char *o_saveptr = NULL;
524 platform_strtok_r(o_copy, " ", &o_saveptr); // username
525 char *session_id_str = platform_strtok_r(NULL, " ", &o_saveptr);
526 if (session_id_str) {
527 SAFE_STRNCPY(session->session_id, session_id_str, sizeof(session->session_id));
528 }
529 }
530 break;
531
532 case 's':
533 // s=session_name (typically "-" or empty)
534 break;
535
536 case 'm':
537 // m=audio 9 UDP/TLS/RTP/SAVPF ...
538 // m=video 9 UDP/TLS/RTP/SAVPF ...
539 in_audio_section = 0;
540 in_video_section = 0;
541
542 if (strstr(value, "audio")) {
543 in_audio_section = 1;
544 session->has_audio = true;
545 } else if (strstr(value, "video")) {
546 in_video_section = 1;
547 session->has_video = true;
548 video_codec_index = 0;
549 }
550 break;
551
552 case 'a':
553 // a=rtpmap:111 opus/48000/2
554 // a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
555 // a=rtpmap:96 ACIP-TC/90000
556 // a=fmtp:96 ...
557 // a=codec-capabilities-video:0x...
558 // a=codec-capabilities-audio:0x...
559
560 if (strstr(value, "codec-capabilities-video:")) {
561 // Parse video codec capabilities: "codec-capabilities-video:0xHH"
562 const char *cap_str = value + strlen("codec-capabilities-video:");
563 session->codec_capabilities_video = (uint32_t)strtol(cap_str, NULL, 16);
564 log_debug("SDP: Parsed video codec capabilities: 0x%x", session->codec_capabilities_video);
565 } else if (strstr(value, "codec-capabilities-audio:")) {
566 // Parse audio codec capabilities: "codec-capabilities-audio:0xHH"
567 const char *cap_str = value + strlen("codec-capabilities-audio:");
568 session->codec_capabilities_audio = (uint32_t)strtol(cap_str, NULL, 16);
569 log_debug("SDP: Parsed audio codec capabilities: 0x%x", session->codec_capabilities_audio);
570 } else if (strstr(value, "rtpmap:")) {
571 const char *rtpmap = value + 7; // Skip "rtpmap:"
572
573 // Parse: PT codec/rate[/channels]
574 char *endptr = NULL;
575 int pt = (int)strtol(rtpmap, &endptr, 10);
576 char codec_name[32] = {0};
577 if (endptr && *endptr == ' ') {
578 SAFE_STRNCPY(codec_name, endptr + 1, sizeof(codec_name));
579 }
580
581 if (in_audio_section && pt == 111 && strstr(codec_name, "opus")) {
582 // Opus audio
583 session->audio_config.sample_rate = 48000;
584 session->audio_config.channels = 2;
585 } else if (in_video_section && video_codec_index < 4) {
586 // Terminal capability codec
587 if (!session->video_codecs) {
589 }
590
591 terminal_capability_t *cap = &session->video_codecs[video_codec_index];
592 memset(cap, 0, sizeof(terminal_capability_t));
593
594 if (pt == 96 && strstr(codec_name, "ACIP-TC")) {
596 video_codec_index++;
597 session->video_codec_count++;
598 } else if (pt == 97 && strstr(codec_name, "ACIP-256")) {
600 video_codec_index++;
601 session->video_codec_count++;
602 } else if (pt == 98 && strstr(codec_name, "ACIP-16")) {
604 video_codec_index++;
605 session->video_codec_count++;
606 } else if (pt == 99 && strstr(codec_name, "ACIP-MONO")) {
607 cap->codec = ACIP_CODEC_MONO;
608 video_codec_index++;
609 session->video_codec_count++;
610 }
611 }
612 } else if (strstr(value, "fmtp:")) {
613 // Parse format parameters
614 // a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
615 // a=fmtp:96 width=80;height=24;renderer=block;charset=utf8;compression=rle;csi_rep=1
616
617 const char *fmtp = value + 5; // Skip "fmtp:"
618
619 if (in_audio_section) {
620 // Parse Opus parameters
621 if (strstr(fmtp, "useinbandfec=1")) {
622 session->audio_config.fec_enabled = true;
623 }
624 if (strstr(fmtp, "usedtx=1")) {
625 session->audio_config.dtx_enabled = true;
626 }
627 // Default values for Opus
628 session->audio_config.bitrate = 24000;
629 session->audio_config.frame_duration = 20;
630 } else if (in_video_section && video_codec_index > 0) {
631 // Parse terminal format parameters
632 terminal_capability_t *cap = &session->video_codecs[video_codec_index - 1];
633
634 // Skip PT number in fmtp string (format: "96 width=...")
635 const char *params = fmtp;
636 while (*params && *params != ' ') {
637 params++;
638 }
639 if (*params == ' ') {
640 params++; // Skip space after PT
641 }
642
643 // Parse with PCRE2 regex (atomic extraction)
644 sdp_parse_fmtp_video_pcre2(params, cap);
645 }
646 }
647 break;
648
649 default:
650 // Ignore unknown SDP attributes
651 break;
652 }
653
654 line = platform_strtok_r(NULL, "\r\n", &saveptr);
655 }
656
657 return ASCIICHAT_OK;
658}
659
660/* ============================================================================
661 * Video Codec Selection
662 * ============================================================================ */
663
665 terminal_format_params_t *selected_format) {
666 if (!answer || !selected_codec || !selected_format) {
667 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for codec selection");
668 }
669
670 if (!answer->has_video || !answer->video_codecs || answer->video_codec_count == 0) {
671 return SET_ERRNO(ERROR_NOT_FOUND, "No video codec in answer");
672 }
673
674 // In SDP answer, server selects only ONE codec (server's preference)
675 // The first codec in the answer is the selected one
676 terminal_capability_t *selected_cap = &answer->video_codecs[0];
677
678 *selected_codec = selected_cap->codec;
679 memcpy(selected_format, &selected_cap->format, sizeof(terminal_format_params_t));
680
681 log_debug("SDP: Selected video codec %s with resolution %ux%u", sdp_codec_name(selected_cap->codec),
682 selected_cap->format.width, selected_cap->format.height);
683
684 return ASCIICHAT_OK;
685}
686
687/* ============================================================================
688 * Terminal Capability Detection
689 * ============================================================================ */
690
692 size_t *detected_count) {
693 if (!capabilities || capability_count == 0 || !detected_count) {
694 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid capability detection parameters");
695 }
696
697 *detected_count = 0;
698 memset(capabilities, 0, capability_count * sizeof(terminal_capability_t));
699
700 // Detect terminal color level using platform terminal module
701 const char *colorterm = SAFE_GETENV("COLORTERM");
702 const char *term = SAFE_GETENV("TERM");
704
705 if (colorterm && (strcmp(colorterm, "truecolor") == 0 || strcmp(colorterm, "24bit") == 0)) {
706 color_level = TERM_COLOR_TRUECOLOR;
707 } else if (term) {
708 if (strstr(term, "256color") || strstr(term, "256")) {
709 color_level = TERM_COLOR_256;
710 } else if (strstr(term, "color") || strcmp(term, "xterm") == 0) {
711 color_level = TERM_COLOR_16;
712 }
713 }
714
715 // Detect UTF-8 support from LANG environment variable
716 const char *lang = SAFE_GETENV("LANG");
717 bool has_utf8 = (lang && (strstr(lang, "UTF-8") || strstr(lang, "utf8")));
718
719 // CSI REP support correlates with UTF-8
720 bool has_csi_rep = has_utf8;
721
722 // Get terminal size from platform module
723 uint16_t term_width = 80; // default
724 uint16_t term_height = 24; // default
725
726 terminal_size_t term_size;
727 asciichat_error_t term_err = terminal_get_size(&term_size);
728 if (term_err == ASCIICHAT_OK) {
729 term_width = (uint16_t)term_size.cols;
730 term_height = (uint16_t)term_size.rows;
731 } else {
732 log_debug("SDP: Using default terminal size %ux%u", term_width, term_height);
733 }
734
735 // Fill capabilities array based on detected color level (preference order)
736 size_t idx = 0;
737
738 if (color_level >= TERM_COLOR_TRUECOLOR && idx < capability_count) {
739 capabilities[idx].codec = ACIP_CODEC_TRUECOLOR;
740 capabilities[idx].format.width = term_width;
741 capabilities[idx].format.height = term_height;
742 capabilities[idx].format.renderer = RENDERER_BLOCK;
743 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
744 capabilities[idx].format.compression = COMPRESSION_RLE;
745 capabilities[idx].format.csi_rep_support = has_csi_rep;
746 idx++;
747 }
748
749 if (color_level >= TERM_COLOR_256 && idx < capability_count) {
750 capabilities[idx].codec = ACIP_CODEC_256COLOR;
751 capabilities[idx].format.width = term_width;
752 capabilities[idx].format.height = term_height;
753 capabilities[idx].format.renderer = RENDERER_BLOCK;
754 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
755 capabilities[idx].format.compression = COMPRESSION_RLE;
756 capabilities[idx].format.csi_rep_support = has_csi_rep;
757 idx++;
758 }
759
760 if (color_level >= TERM_COLOR_16 && idx < capability_count) {
761 capabilities[idx].codec = ACIP_CODEC_16COLOR;
762 capabilities[idx].format.width = term_width;
763 capabilities[idx].format.height = term_height;
764 capabilities[idx].format.renderer = RENDERER_BLOCK;
765 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
766 capabilities[idx].format.compression = COMPRESSION_NONE;
767 capabilities[idx].format.csi_rep_support = has_csi_rep;
768 idx++;
769 }
770
771 // Monochrome always supported
772 if (idx < capability_count) {
773 capabilities[idx].codec = ACIP_CODEC_MONO;
774 capabilities[idx].format.width = term_width;
775 capabilities[idx].format.height = term_height;
776 capabilities[idx].format.renderer = RENDERER_BLOCK;
777 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
778 capabilities[idx].format.compression = COMPRESSION_NONE;
779 capabilities[idx].format.csi_rep_support = has_csi_rep;
780 idx++;
781 }
782
783 *detected_count = idx;
784
785 log_debug("SDP: Detected %zu terminal capabilities (colors=%d, utf8=%s, csi_rep=%s, size=%ux%u)", *detected_count,
786 color_level, has_utf8 ? "yes" : "no", has_csi_rep ? "yes" : "no", term_width, term_height);
787
788 return ASCIICHAT_OK;
789}
790
791/* ============================================================================
792 * Resource Cleanup
793 * ============================================================================ */
794
796 if (!session) {
797 return;
798 }
799
800 // Free allocated video codec array
801 if (session->video_codecs) {
802 SAFE_FREE(session->video_codecs);
803 session->video_codec_count = 0;
804 }
805
806 // Zero out the session structure
807 memset(session, 0, sizeof(sdp_session_t));
808}
Video and audio codec definitions and capability bitmasks.
char ** line_buffer
Circular buffer for context_before.
Definition grep.c:89
unsigned short uint16_t
Definition common.h:57
unsigned int uint32_t
Definition common.h:58
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_GETENV(name)
Definition common.h:434
#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_NOT_FOUND
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
asciichat_error_t terminal_get_size(terminal_size_t *size)
Get terminal size.
char * platform_strtok_r(char *str, const char *delim, char **saveptr)
Thread-safe string tokenization (strtok_r replacement)
📝 Logging API with multiple log levels and terminal output control
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
Definition pcre2.c:98
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:308
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:361
pcre2_singleton_t * asciichat_pcre2_singleton_compile(const char *pattern, uint32_t flags)
Compile and cache a PCRE2 regex pattern with thread-safe singleton semantics.
Definition pcre2.c:47
🔍 Centralized PCRE2 singleton pattern for efficient regex compilation
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:691
asciichat_error_t sdp_parse(const char *sdp_string, sdp_session_t *session)
Parse SDP offer or answer.
Definition sdp.c:479
const char * sdp_codec_name(acip_codec_t codec)
Get human-readable codec name.
Definition sdp.c:24
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, uint32_t server_video_codec_caps, uint32_t server_audio_codec_caps, sdp_session_t *answer_out)
Generate SDP answer (server side)
Definition sdp.c:183
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:664
const char * sdp_renderer_name(int renderer_type)
Get human-readable renderer name.
Definition sdp.c:39
void sdp_session_destroy(sdp_session_t *session)
Free SDP session resources.
Definition sdp.c:795
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, uint32_t video_codec_caps, uint32_t audio_codec_caps, sdp_session_t *offer_out)
Generate SDP offer (client side)
Definition sdp.c:56
SDP (Session Description Protocol) for WebRTC audio/video negotiation.
acip_codec_t
Terminal rendering capability payload types.
Definition sdp.h:77
@ ACIP_CODEC_16COLOR
16-color (ANSI standard)
Definition sdp.h:80
@ ACIP_CODEC_MONO
Monochrome (ASCII only)
Definition sdp.h:81
@ ACIP_CODEC_256COLOR
256-color (xterm palette)
Definition sdp.h:79
@ ACIP_CODEC_TRUECOLOR
24-bit RGB (truecolor)
Definition sdp.h:78
Opus codec parameters for ascii-chat.
Definition sdp.h:58
bool dtx_enabled
Discontinuous Transmission (silence suppression)
Definition sdp.h:63
bool fec_enabled
Forward Error Correction for lossy networks.
Definition sdp.h:64
uint32_t sample_rate
48000 Hz (Opus native rate)
Definition sdp.h:59
uint32_t bitrate
24000 bps (good quality for speech)
Definition sdp.h:61
uint16_t frame_duration
20 ms (balance latency/efficiency)
Definition sdp.h:62
uint8_t channels
1 (mono for voice chat)
Definition sdp.h:60
Represents a thread-safe compiled PCRE2 regex singleton.
Definition pcre2.c:22
SDP media session (simplified for WebRTC)
Definition sdp.h:115
terminal_format_params_t video_format
Default format parameters.
Definition sdp.h:129
uint32_t codec_capabilities_audio
AUDIO_CODEC_CAP_* bitmask (which audio codecs peer supports)
Definition sdp.h:123
size_t video_codec_count
Number of supported capabilities.
Definition sdp.h:128
char session_version[16]
Session version (timestamp)
Definition sdp.h:118
size_t sdp_length
Length of SDP string (excluding null)
Definition sdp.h:134
bool has_audio
Audio media included.
Definition sdp.h:121
char session_id[32]
Session identifier.
Definition sdp.h:117
terminal_capability_t * video_codecs
Array of supported capabilities.
Definition sdp.h:127
uint32_t codec_capabilities_video
VIDEO_CODEC_CAP_* bitmask (which video codecs peer supports)
Definition sdp.h:130
opus_config_t audio_config
Opus codec configuration.
Definition sdp.h:122
bool has_video
Video media included.
Definition sdp.h:126
char sdp_string[4096]
Complete SDP as null-terminated string.
Definition sdp.h:133
Supported terminal capability (for offer/answer)
Definition sdp.h:100
terminal_format_params_t format
Format parameters.
Definition sdp.h:102
acip_codec_t codec
Rendering capability type.
Definition sdp.h:101
Terminal rendering format parameters.
Definition sdp.h:87
enum terminal_format_params_t::@17 charset
bool csi_rep_support
CSI REP (repeat) support.
Definition sdp.h:93
enum terminal_format_params_t::@18 compression
enum terminal_format_params_t::@16 renderer
uint16_t height
Terminal height in characters.
Definition sdp.h:89
uint16_t width
Terminal width in characters.
Definition sdp.h:88
Terminal size structure.
Definition terminal.h:163
int cols
Number of columns (width) in terminal.
Definition terminal.h:165
int rows
Number of rows (height) in terminal.
Definition terminal.h:164
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:578
@ TERM_COLOR_NONE
No color support (monochrome terminal)
Definition terminal.h:582
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:584
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:586
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:588
⏱️ High-precision timing utilities using sokol_time.h and uthash