37 switch (renderer_type) {
40 case RENDERER_HALFBLOCK:
42 case RENDERER_BRAILLE:
56 if (!capabilities || capability_count == 0 || !audio_config || !offer_out) {
63 time_t now = time(NULL);
82 size_t remaining =
sizeof(offer_out->
sdp_string);
86 written = snprintf(sdp, remaining,
"v=0\r\n");
90 written = snprintf(sdp, remaining,
"o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", offer_out->
session_id,
95 written = snprintf(sdp, remaining,
"s=-\r\n");
99 written = snprintf(sdp, remaining,
"t=0 0\r\n");
101 remaining -= written;
104 written = snprintf(sdp, remaining,
"m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
106 remaining -= written;
108 written = snprintf(sdp, remaining,
"a=rtpmap:111 opus/48000/2\r\n");
110 remaining -= written;
112 written = snprintf(sdp, remaining,
"a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
114 remaining -= written;
117 char codec_list[128] =
"96";
118 for (
size_t i = 1; i < capability_count; i++) {
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);
125 written = snprintf(sdp, remaining,
"m=video 9 UDP/TLS/RTP/SAVPF %s\r\n", codec_list);
127 remaining -= written;
130 for (
size_t i = 0; i < capability_count; i++) {
131 int pt = 96 + (int)i;
134 written = snprintf(sdp, remaining,
"a=rtpmap:%d %s/90000\r\n", pt, codec_name);
136 remaining -= written;
141 const char *charset_name = (cap_format->
charset == CHARSET_UTF8) ?
"utf8" :
"ascii";
142 const char *compression_name =
"none";
144 compression_name =
"rle";
145 }
else if (cap_format->
compression == COMPRESSION_ZSTD) {
146 compression_name =
"zstd";
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,
154 remaining -= written;
159 log_debug(
"SDP: Generated offer with %zu video codecs and Opus audio", capability_count);
169 size_t server_capability_count,
const opus_config_t *audio_config,
171 if (!offer || !server_capabilities || server_capability_count == 0 || !audio_config || !answer_out) {
180 time_t now = time(NULL);
194 int selected_index = -1;
196 for (
size_t s = 0; s < server_capability_count; s++) {
199 selected_index = (int)s;
203 if (selected_index >= 0) {
213 if (selected_index >= 0) {
222 if (server_format->
width > 0) {
225 if (server_format->
height > 0) {
228 if (server_format->
renderer != RENDERER_BLOCK) {
231 if (server_format->
compression != COMPRESSION_NONE) {
241 size_t remaining =
sizeof(answer_out->
sdp_string);
245 written = snprintf(sdp, remaining,
"v=0\r\n");
247 remaining -= written;
249 written = snprintf(sdp, remaining,
"o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", answer_out->
session_id,
252 remaining -= written;
254 written = snprintf(sdp, remaining,
"s=-\r\n");
256 remaining -= written;
258 written = snprintf(sdp, remaining,
"t=0 0\r\n");
260 remaining -= written;
264 written = snprintf(sdp, remaining,
"m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
266 remaining -= written;
268 written = snprintf(sdp, remaining,
"a=rtpmap:111 opus/48000/2\r\n");
270 remaining -= written;
272 written = snprintf(sdp, remaining,
"a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
274 remaining -= written;
279 written = snprintf(sdp, remaining,
"m=video 9 UDP/TLS/RTP/SAVPF 96\r\n");
281 remaining -= written;
284 written = snprintf(sdp, remaining,
"a=rtpmap:96 %s/90000\r\n", codec_name);
286 remaining -= written;
291 const char *charset_name = (cap_format->
charset == CHARSET_UTF8) ?
"utf8" :
"ascii";
292 const char *compression_name =
"none";
294 compression_name =
"rle";
295 }
else if (cap_format->
compression == COMPRESSION_ZSTD) {
296 compression_name =
"zstd";
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,
304 remaining -= written;
319 if (!sdp_string || !session) {
330 char line_buffer[512];
331 SAFE_STRNCPY(line_buffer, sdp_string,
sizeof(line_buffer));
333 char *saveptr = NULL;
334 char *line = strtok_r(line_buffer,
"\r\n", &saveptr);
336 int in_audio_section = 0;
337 int in_video_section = 0;
338 size_t video_codec_index = 0;
342 char *equals = strchr(line,
'=');
344 line = strtok_r(NULL,
"\r\n", &saveptr);
349 const char *value = equals + 1;
362 char *o_saveptr = NULL;
363 strtok_r(o_copy,
" ", &o_saveptr);
364 char *session_id_str = strtok_r(NULL,
" ", &o_saveptr);
365 if (session_id_str) {
378 in_audio_section = 0;
379 in_video_section = 0;
381 if (strstr(value,
"audio")) {
382 in_audio_section = 1;
384 }
else if (strstr(value,
"video")) {
385 in_video_section = 1;
387 video_codec_index = 0;
397 if (strstr(value,
"rtpmap:")) {
398 const char *rtpmap = value + 7;
402 char codec_name[32] = {0};
403 sscanf(rtpmap,
"%d %s", &pt, codec_name);
405 if (in_audio_section && pt == 111 && strstr(codec_name,
"opus")) {
409 }
else if (in_video_section && video_codec_index < 4) {
418 if (pt == 96 && strstr(codec_name,
"ACIP-TC")) {
422 }
else if (pt == 97 && strstr(codec_name,
"ACIP-256")) {
426 }
else if (pt == 98 && strstr(codec_name,
"ACIP-16")) {
430 }
else if (pt == 99 && strstr(codec_name,
"ACIP-MONO")) {
436 }
else if (strstr(value,
"fmtp:")) {
441 const char *fmtp = value + 5;
443 if (in_audio_section) {
445 if (strstr(fmtp,
"useinbandfec=1")) {
448 if (strstr(fmtp,
"usedtx=1")) {
454 }
else if (in_video_section && video_codec_index > 0) {
462 char *saveptr = NULL;
463 strtok_r(fmtp_copy,
" ", &saveptr);
464 const char *params = fmtp_copy + strcspn(fmtp_copy,
" ") + 1;
467 const char *width_str = strstr(params,
"width=");
473 const char *height_str = strstr(params,
"height=");
479 const char *renderer_str = strstr(params,
"renderer=");
481 if (strstr(renderer_str,
"block")) {
483 }
else if (strstr(renderer_str,
"halfblock")) {
485 }
else if (strstr(renderer_str,
"braille")) {
491 const char *charset_str = strstr(params,
"charset=");
493 if (strstr(charset_str,
"utf8")) {
495 }
else if (strstr(charset_str,
"utf8_wide")) {
503 const char *compression_str = strstr(params,
"compression=");
504 if (compression_str) {
505 if (strstr(compression_str,
"rle")) {
507 }
else if (strstr(compression_str,
"zstd")) {
515 const char *csi_rep_str = strstr(params,
"csi_rep=");
524 line = strtok_r(NULL,
"\r\n", &saveptr);
536 if (!answer || !selected_codec || !selected_format) {
548 *selected_codec = selected_cap->
codec;
562 size_t *detected_count) {
563 if (!capabilities || capability_count == 0 || !detected_count) {
575 if (colorterm && (strcmp(colorterm,
"truecolor") == 0 || strcmp(colorterm,
"24bit") == 0)) {
578 if (strstr(term,
"256color") || strstr(term,
"256")) {
580 }
else if (strstr(term,
"color") || strcmp(term,
"xterm") == 0) {
587 bool has_utf8 = (lang && (strstr(lang,
"UTF-8") || strstr(lang,
"utf8")));
590 bool has_csi_rep = has_utf8;
602 log_debug(
"SDP: Using default terminal size %ux%u", term_width, term_height);
613 capabilities[idx].
format.
charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
624 capabilities[idx].
format.
charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
630 if (color_level >=
TERM_COLOR_16 && idx < capability_count) {
635 capabilities[idx].
format.
charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
642 if (idx < capability_count) {
647 capabilities[idx].
format.
charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
653 *detected_count = idx;
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);
#define SAFE_STRNCPY(dst, src, size)
#define SAFE_MALLOC(size, cast)
#define SAFE_GETENV(name)
#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)
#define log_debug(...)
Log a DEBUG message.
📝 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.
asciichat_error_t sdp_parse(const char *sdp_string, sdp_session_t *session)
Parse SDP offer or answer.
const char * sdp_codec_name(acip_codec_t codec)
Get human-readable codec name.
void sdp_session_free(sdp_session_t *session)
Free SDP session resources.
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.
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)
const char * sdp_renderer_name(int renderer_type)
Get human-readable renderer name.
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)
SDP (Session Description Protocol) for WebRTC audio/video negotiation.
acip_codec_t
Terminal rendering capability payload types.
@ ACIP_CODEC_16COLOR
16-color (ANSI standard)
@ ACIP_CODEC_MONO
Monochrome (ASCII only)
@ ACIP_CODEC_256COLOR
256-color (xterm palette)
@ ACIP_CODEC_TRUECOLOR
24-bit RGB (truecolor)
Opus codec parameters for ascii-chat.
bool dtx_enabled
Discontinuous Transmission (silence suppression)
bool fec_enabled
Forward Error Correction for lossy networks.
uint32_t sample_rate
48000 Hz (Opus native rate)
uint32_t bitrate
24000 bps (good quality for speech)
uint16_t frame_duration
20 ms (balance latency/efficiency)
uint8_t channels
1 (mono for voice chat)
SDP media session (simplified for WebRTC)
terminal_format_params_t video_format
Default format parameters.
size_t video_codec_count
Number of supported capabilities.
char session_version[16]
Session version (timestamp)
size_t sdp_length
Length of SDP string (excluding null)
bool has_audio
Audio media included.
char session_id[32]
Session identifier.
terminal_capability_t * video_codecs
Array of supported capabilities.
opus_config_t audio_config
Opus codec configuration.
bool has_video
Video media included.
char sdp_string[4096]
Complete SDP as null-terminated string.
Supported terminal capability (for offer/answer)
terminal_format_params_t format
Format parameters.
acip_codec_t codec
Rendering capability type.
int cols
Number of columns (width) in terminal.
int rows
Number of rows (height) in terminal.
⏱️ High-precision timing utilities using sokol_time.h and uthash