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>
26 case ACIP_CODEC_TRUECOLOR:
28 case ACIP_CODEC_256COLOR:
30 case ACIP_CODEC_16COLOR:
40 switch (renderer_type) {
43 case RENDERER_HALFBLOCK:
45 case RENDERER_BRAILLE:
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");
63 memset(offer_out, 0,
sizeof(sdp_session_t));
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);
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;
75 offer_out->has_audio =
true;
76 memcpy(&offer_out->audio_config, audio_config,
sizeof(opus_config_t));
78 offer_out->has_video =
true;
80 memcpy(&offer_out->video_format, format,
sizeof(terminal_format_params_t));
84 char *sdp = offer_out->sdp_string;
85 size_t remaining =
sizeof(offer_out->sdp_string);
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);
100 remaining -= written;
104 remaining -= written;
107 written =
safe_snprintf(sdp, remaining,
"m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
109 remaining -= written;
111 written =
safe_snprintf(sdp, remaining,
"a=rtpmap:111 opus/48000/2\r\n");
113 remaining -= written;
115 written =
safe_snprintf(sdp, remaining,
"a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
117 remaining -= written;
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);
126 written =
safe_snprintf(sdp, remaining,
"m=video 9 UDP/TLS/RTP/SAVPF %s\r\n", codec_list);
128 remaining -= written;
131 for (
size_t i = 0; i < capability_count; i++) {
132 int pt = 96 + (int)i;
135 written =
safe_snprintf(sdp, remaining,
"a=rtpmap:%d %s/90000\r\n", pt, codec_name);
137 remaining -= written;
140 const terminal_format_params_t *cap_format = &capabilities[i].format;
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";
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);
155 remaining -= written;
158 offer_out->sdp_length = strlen(offer_out->sdp_string);
160 log_debug(
"SDP: Generated offer with %zu video codecs and Opus audio", capability_count);
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");
176 memset(answer_out, 0,
sizeof(sdp_session_t));
179 SAFE_STRNCPY(answer_out->session_id, offer->session_id,
sizeof(answer_out->session_id));
181 time_t now = time(NULL);
182 safe_snprintf(answer_out->session_version,
sizeof(answer_out->session_version),
"%ld", now);
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));
191 answer_out->has_video = offer->has_video;
193 if (answer_out->has_video && offer->video_codecs && offer->video_codec_count > 0) {
195 int selected_index = -1;
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;
204 if (selected_index >= 0) {
210 answer_out->video_codecs = SAFE_MALLOC(
sizeof(terminal_capability_t), terminal_capability_t *);
211 answer_out->video_codec_count = 1;
214 if (selected_index >= 0) {
215 memcpy(answer_out->video_codecs, &server_capabilities[selected_index],
sizeof(terminal_capability_t));
218 memcpy(answer_out->video_codecs, &server_capabilities[0],
sizeof(terminal_capability_t));
223 if (server_format->width > 0) {
224 answer_out->video_codecs[0].format.width = server_format->width;
226 if (server_format->height > 0) {
227 answer_out->video_codecs[0].format.height = server_format->height;
229 if (server_format->renderer != RENDERER_BLOCK) {
230 answer_out->video_codecs[0].format.renderer = server_format->renderer;
232 if (server_format->compression != COMPRESSION_NONE) {
233 answer_out->video_codecs[0].format.compression = server_format->compression;
237 memcpy(&answer_out->video_format, &answer_out->video_codecs[0].format,
sizeof(terminal_format_params_t));
241 char *sdp = answer_out->sdp_string;
242 size_t remaining =
sizeof(answer_out->sdp_string);
248 remaining -= written;
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);
253 remaining -= written;
257 remaining -= written;
261 remaining -= written;
264 if (answer_out->has_audio) {
265 written =
safe_snprintf(sdp, remaining,
"m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
267 remaining -= written;
269 written =
safe_snprintf(sdp, remaining,
"a=rtpmap:111 opus/48000/2\r\n");
271 remaining -= written;
273 written =
safe_snprintf(sdp, remaining,
"a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
275 remaining -= written;
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");
282 remaining -= written;
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);
287 remaining -= written;
290 const terminal_format_params_t *cap_format = &answer_out->video_codecs[0].format;
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";
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);
305 remaining -= written;
308 answer_out->sdp_length = strlen(answer_out->sdp_string);
310 log_debug(
"SDP: Generated answer with codec %s",
sdp_codec_name(answer_out->video_codecs[0].codec));
326static const char *SDP_FMTP_VIDEO_PATTERN =
"width=([0-9]+)"
328 ".*?renderer=([a-z]+)"
329 "(?:.*?charset=([a-z0-9_]+))?"
330 "(?:.*?compression=([a-z0-9]+))?"
331 "(?:.*?csi_rep=([01]))?";
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);
356static bool sdp_parse_fmtp_video_pcre2(
const char *fmtp_params, terminal_capability_t *cap) {
357 if (!fmtp_params || !cap) {
362 pcre2_code *regex = sdp_fmtp_video_regex_get();
369 pcre2_match_data *match_data = pcre2_match_data_create_from_pattern(regex, NULL);
374 int rc = pcre2_jit_match(regex, (PCRE2_SPTR8)fmtp_params, strlen(fmtp_params), 0, 0, match_data, NULL);
376 bool success =
false;
381 cap->format.width = (uint16_t)width;
385 unsigned long height;
387 cap->format.height = (uint16_t)height;
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;
400 SAFE_FREE(renderer_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;
411 cap->format.charset = CHARSET_ASCII;
413 SAFE_FREE(charset_str);
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;
424 cap->format.compression = COMPRESSION_NONE;
426 SAFE_FREE(compression_str);
432 cap->format.csi_rep_support = (csi_rep_str[0] ==
'1');
433 SAFE_FREE(csi_rep_str);
439 pcre2_match_data_free(match_data);
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");
448 memset(session, 0,
sizeof(sdp_session_t));
451 SAFE_STRNCPY(session->sdp_string, sdp_string,
sizeof(session->sdp_string));
452 session->sdp_length = strlen(sdp_string);
458 char *saveptr = NULL;
459 char *line = platform_strtok_r(
line_buffer,
"\r\n", &saveptr);
461 int in_audio_section = 0;
462 int in_video_section = 0;
463 size_t video_codec_index = 0;
467 char *equals = strchr(line,
'=');
469 line = platform_strtok_r(NULL,
"\r\n", &saveptr);
474 const char *value = equals + 1;
486 SAFE_STRNCPY(o_copy, value,
sizeof(o_copy));
487 char *o_saveptr = NULL;
488 platform_strtok_r(o_copy,
" ", &o_saveptr);
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));
503 in_audio_section = 0;
504 in_video_section = 0;
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;
522 if (strstr(value,
"rtpmap:")) {
523 const char *rtpmap = value + 7;
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));
533 if (in_audio_section && pt == 111 && strstr(codec_name,
"opus")) {
535 session->audio_config.sample_rate = 48000;
536 session->audio_config.channels = 2;
537 }
else if (in_video_section && video_codec_index < 4) {
539 if (!session->video_codecs) {
540 session->video_codecs = SAFE_MALLOC(4 *
sizeof(terminal_capability_t), terminal_capability_t *);
543 terminal_capability_t *cap = &session->video_codecs[video_codec_index];
544 memset(cap, 0,
sizeof(terminal_capability_t));
546 if (pt == 96 && strstr(codec_name,
"ACIP-TC")) {
547 cap->codec = ACIP_CODEC_TRUECOLOR;
549 session->video_codec_count++;
550 }
else if (pt == 97 && strstr(codec_name,
"ACIP-256")) {
551 cap->codec = ACIP_CODEC_256COLOR;
553 session->video_codec_count++;
554 }
else if (pt == 98 && strstr(codec_name,
"ACIP-16")) {
555 cap->codec = ACIP_CODEC_16COLOR;
557 session->video_codec_count++;
558 }
else if (pt == 99 && strstr(codec_name,
"ACIP-MONO")) {
559 cap->codec = ACIP_CODEC_MONO;
561 session->video_codec_count++;
564 }
else if (strstr(value,
"fmtp:")) {
569 const char *fmtp = value + 5;
571 if (in_audio_section) {
573 if (strstr(fmtp,
"useinbandfec=1")) {
574 session->audio_config.fec_enabled =
true;
576 if (strstr(fmtp,
"usedtx=1")) {
577 session->audio_config.dtx_enabled =
true;
580 session->audio_config.bitrate = 24000;
581 session->audio_config.frame_duration = 20;
582 }
else if (in_video_section && video_codec_index > 0) {
584 terminal_capability_t *cap = &session->video_codecs[video_codec_index - 1];
587 const char *params = fmtp;
588 while (*params && *params !=
' ') {
591 if (*params ==
' ') {
596 sdp_parse_fmtp_video_pcre2(params, cap);
602 line = platform_strtok_r(NULL,
"\r\n", &saveptr);
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");
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");
624 terminal_capability_t *selected_cap = &answer->video_codecs[0];
626 *selected_codec = selected_cap->codec;
627 memcpy(selected_format, &selected_cap->format,
sizeof(terminal_format_params_t));
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);
640 size_t *detected_count) {
641 if (!capabilities || capability_count == 0 || !detected_count) {
642 return SET_ERRNO(ERROR_INVALID_PARAM,
"Invalid capability detection parameters");
646 memset(capabilities, 0, capability_count *
sizeof(terminal_capability_t));
649 const char *colorterm = SAFE_GETENV(
"COLORTERM");
650 const char *term = SAFE_GETENV(
"TERM");
651 terminal_color_mode_t color_level = TERM_COLOR_NONE;
653 if (colorterm && (strcmp(colorterm,
"truecolor") == 0 || strcmp(colorterm,
"24bit") == 0)) {
654 color_level = TERM_COLOR_TRUECOLOR;
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;
664 const char *lang = SAFE_GETENV(
"LANG");
665 bool has_utf8 = (lang && (strstr(lang,
"UTF-8") || strstr(lang,
"utf8")));
668 bool has_csi_rep = has_utf8;
671 uint16_t term_width = 80;
672 uint16_t term_height = 24;
674 terminal_size_t term_size;
676 if (term_err == ASCIICHAT_OK) {
677 term_width = (uint16_t)term_size.cols;
678 term_height = (uint16_t)term_size.rows;
680 log_debug(
"SDP: Using default terminal size %ux%u", term_width, term_height);
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;
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;
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;
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;
731 *detected_count = idx;
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);
749 if (session->video_codecs) {
750 SAFE_FREE(session->video_codecs);
751 session->video_codec_count = 0;
755 memset(session, 0,
sizeof(sdp_session_t));
char ** line_buffer
Circular buffer for context_before.
pcre2_code * asciichat_pcre2_singleton_get_code(pcre2_singleton_t *singleton)
Get the compiled pcre2_code from a singleton handle.
char * asciichat_pcre2_extract_group(pcre2_match_data *match_data, int group_num, const char *subject)
Extract numbered capture group as allocated string.
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.
asciichat_error_t sdp_detect_terminal_capabilities(terminal_capability_t *capabilities, size_t capability_count, size_t *detected_count)
asciichat_error_t sdp_parse(const char *sdp_string, sdp_session_t *session)
const char * sdp_codec_name(acip_codec_t codec)
asciichat_error_t sdp_get_selected_video_codec(const sdp_session_t *answer, acip_codec_t *selected_codec, terminal_format_params_t *selected_format)
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)
const char * sdp_renderer_name(int renderer_type)
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)
void sdp_session_destroy(sdp_session_t *session)
Represents a thread-safe compiled PCRE2 regex singleton.
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.