ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
sdp.c File Reference

SDP offer/answer generation and parsing. More...

Go to the source code of this file.

Functions

const char * sdp_codec_name (acip_codec_t codec)
 Get human-readable codec name.
 
const char * sdp_renderer_name (int renderer_type)
 Get human-readable renderer name.
 
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)
 
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)
 
asciichat_error_t sdp_parse (const char *sdp_string, sdp_session_t *session)
 Parse SDP offer or answer.
 
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_detect_terminal_capabilities (terminal_capability_t *capabilities, size_t capability_count, size_t *detected_count)
 Detect client terminal capabilities at startup.
 
void sdp_session_free (sdp_session_t *session)
 Free SDP session resources.
 

Detailed Description

SDP offer/answer generation and parsing.

Implements SDP generation for audio (Opus) and video (terminal capabilities). Terminal capabilities are negotiated as custom "codecs" in the video media section.

Date
January 2026

Definition in file sdp.c.

Function Documentation

◆ sdp_codec_name()

const char * sdp_codec_name ( acip_codec_t  codec)

Get human-readable codec name.

Parameters
codecCodec type
Returns
Static string (e.g., "ACIP-TC", "ACIP-256")

Definition at line 21 of file sdp.c.

21 {
22 switch (codec) {
24 return "ACIP-TC";
26 return "ACIP-256";
28 return "ACIP-16";
29 case ACIP_CODEC_MONO:
30 return "ACIP-MONO";
31 default:
32 return "UNKNOWN";
33 }
34}
@ ACIP_CODEC_16COLOR
16-color (ANSI standard)
Definition sdp.h:79
@ ACIP_CODEC_MONO
Monochrome (ASCII only)
Definition sdp.h:80
@ ACIP_CODEC_256COLOR
256-color (xterm palette)
Definition sdp.h:78
@ ACIP_CODEC_TRUECOLOR
24-bit RGB (truecolor)
Definition sdp.h:77

References ACIP_CODEC_16COLOR, ACIP_CODEC_256COLOR, ACIP_CODEC_MONO, and ACIP_CODEC_TRUECOLOR.

Referenced by sdp_generate_answer(), sdp_generate_offer(), and sdp_get_selected_video_codec().

◆ sdp_detect_terminal_capabilities()

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.

Auto-detects from environment and terminal:

  1. COLORTERM env var (truecolor/24bit)
  2. Terminal color query (terminfo/XTGETTCAP)
  3. UTF-8 support (LANG, test character)
  4. CSI REP support (escape sequence test)
  5. Terminal size (TIOCGWINSZ)
Parameters
capabilitiesDetected capabilities (output)
capability_countMaximum capabilities to detect
detected_countActual number detected (output)
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 561 of file sdp.c.

562 {
563 if (!capabilities || capability_count == 0 || !detected_count) {
564 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid capability detection parameters");
565 }
566
567 *detected_count = 0;
568 memset(capabilities, 0, capability_count * sizeof(terminal_capability_t));
569
570 // Detect terminal color level using platform terminal module
571 const char *colorterm = SAFE_GETENV("COLORTERM");
572 const char *term = SAFE_GETENV("TERM");
574
575 if (colorterm && (strcmp(colorterm, "truecolor") == 0 || strcmp(colorterm, "24bit") == 0)) {
576 color_level = TERM_COLOR_TRUECOLOR;
577 } else if (term) {
578 if (strstr(term, "256color") || strstr(term, "256")) {
579 color_level = TERM_COLOR_256;
580 } else if (strstr(term, "color") || strcmp(term, "xterm") == 0) {
581 color_level = TERM_COLOR_16;
582 }
583 }
584
585 // Detect UTF-8 support from LANG environment variable
586 const char *lang = SAFE_GETENV("LANG");
587 bool has_utf8 = (lang && (strstr(lang, "UTF-8") || strstr(lang, "utf8")));
588
589 // CSI REP support correlates with UTF-8
590 bool has_csi_rep = has_utf8;
591
592 // Get terminal size from platform module
593 uint16_t term_width = 80; // default
594 uint16_t term_height = 24; // default
595
596 terminal_size_t term_size;
597 asciichat_error_t term_err = terminal_get_size(&term_size);
598 if (term_err == ASCIICHAT_OK) {
599 term_width = (uint16_t)term_size.cols;
600 term_height = (uint16_t)term_size.rows;
601 } else {
602 log_debug("SDP: Using default terminal size %ux%u", term_width, term_height);
603 }
604
605 // Fill capabilities array based on detected color level (preference order)
606 size_t idx = 0;
607
608 if (color_level >= TERM_COLOR_TRUECOLOR && idx < capability_count) {
609 capabilities[idx].codec = ACIP_CODEC_TRUECOLOR;
610 capabilities[idx].format.width = term_width;
611 capabilities[idx].format.height = term_height;
612 capabilities[idx].format.renderer = RENDERER_BLOCK;
613 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
614 capabilities[idx].format.compression = COMPRESSION_RLE;
615 capabilities[idx].format.csi_rep_support = has_csi_rep;
616 idx++;
617 }
618
619 if (color_level >= TERM_COLOR_256 && idx < capability_count) {
620 capabilities[idx].codec = ACIP_CODEC_256COLOR;
621 capabilities[idx].format.width = term_width;
622 capabilities[idx].format.height = term_height;
623 capabilities[idx].format.renderer = RENDERER_BLOCK;
624 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
625 capabilities[idx].format.compression = COMPRESSION_RLE;
626 capabilities[idx].format.csi_rep_support = has_csi_rep;
627 idx++;
628 }
629
630 if (color_level >= TERM_COLOR_16 && idx < capability_count) {
631 capabilities[idx].codec = ACIP_CODEC_16COLOR;
632 capabilities[idx].format.width = term_width;
633 capabilities[idx].format.height = term_height;
634 capabilities[idx].format.renderer = RENDERER_BLOCK;
635 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
636 capabilities[idx].format.compression = COMPRESSION_NONE;
637 capabilities[idx].format.csi_rep_support = has_csi_rep;
638 idx++;
639 }
640
641 // Monochrome always supported
642 if (idx < capability_count) {
643 capabilities[idx].codec = ACIP_CODEC_MONO;
644 capabilities[idx].format.width = term_width;
645 capabilities[idx].format.height = term_height;
646 capabilities[idx].format.renderer = RENDERER_BLOCK;
647 capabilities[idx].format.charset = has_utf8 ? CHARSET_UTF8 : CHARSET_ASCII;
648 capabilities[idx].format.compression = COMPRESSION_NONE;
649 capabilities[idx].format.csi_rep_support = has_csi_rep;
650 idx++;
651 }
652
653 *detected_count = idx;
654
655 log_debug("SDP: Detected %zu terminal capabilities (colors=%d, utf8=%s, csi_rep=%s, size=%ux%u)", *detected_count,
656 color_level, has_utf8 ? "yes" : "no", has_csi_rep ? "yes" : "no", term_width, term_height);
657
658 return ASCIICHAT_OK;
659}
unsigned short uint16_t
Definition common.h:57
#define SAFE_GETENV(name)
Definition common.h:378
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:46
@ ASCIICHAT_OK
Definition error_codes.h:48
@ ERROR_INVALID_PARAM
#define log_debug(...)
Log a DEBUG message.
asciichat_error_t terminal_get_size(terminal_size_t *size)
Get terminal size.
terminal_color_mode_t
Terminal color support levels.
Definition terminal.h:424
@ TERM_COLOR_NONE
No color support (monochrome terminal)
Definition terminal.h:428
@ TERM_COLOR_16
16-color support (standard ANSI colors)
Definition terminal.h:430
@ TERM_COLOR_256
256-color support (extended ANSI palette)
Definition terminal.h:432
@ TERM_COLOR_TRUECOLOR
24-bit truecolor support (RGB colors)
Definition terminal.h:434
Supported terminal capability (for offer/answer)
Definition sdp.h:99
terminal_format_params_t format
Format parameters.
Definition sdp.h:101
acip_codec_t codec
Rendering capability type.
Definition sdp.h:100
enum terminal_format_params_t::@5 charset
bool csi_rep_support
CSI REP (repeat) support.
Definition sdp.h:92
enum terminal_format_params_t::@4 renderer
uint16_t height
Terminal height in characters.
Definition sdp.h:88
uint16_t width
Terminal width in characters.
Definition sdp.h:87
enum terminal_format_params_t::@6 compression
Terminal size structure.
Definition terminal.h:55
int cols
Number of columns (width) in terminal.
Definition terminal.h:57
int rows
Number of rows (height) in terminal.
Definition terminal.h:56

References ACIP_CODEC_16COLOR, ACIP_CODEC_256COLOR, ACIP_CODEC_MONO, ACIP_CODEC_TRUECOLOR, ASCIICHAT_OK, terminal_format_params_t::charset, terminal_capability_t::codec, terminal_size_t::cols, terminal_format_params_t::compression, terminal_format_params_t::csi_rep_support, ERROR_INVALID_PARAM, terminal_capability_t::format, terminal_format_params_t::height, log_debug, terminal_format_params_t::renderer, terminal_size_t::rows, SAFE_GETENV, SET_ERRNO, TERM_COLOR_16, TERM_COLOR_256, TERM_COLOR_NONE, TERM_COLOR_TRUECOLOR, terminal_get_size(), and terminal_format_params_t::width.

◆ sdp_generate_answer()

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)

Creates an SDP answer by selecting best mutually-supported mode from offer. Server enforces its rendering constraints.

Parameters
offerReceived SDP offer from client
server_capabilitiesArray of server-supported capabilities
server_capability_countNumber of server capabilities
audio_configOpus codec configuration
server_formatServer's rendering constraints
answer_outGenerated SDP answer (output)
Returns
ASCIICHAT_OK on success, error code on failure
Note
Caller must free answer_out using sdp_session_free()

Definition at line 168 of file sdp.c.

170 {
171 if (!offer || !server_capabilities || server_capability_count == 0 || !audio_config || !answer_out) {
172 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP answer parameters");
173 }
174
175 memset(answer_out, 0, sizeof(sdp_session_t));
176
177 // Copy session ID from offer, increment version
178 SAFE_STRNCPY(answer_out->session_id, offer->session_id, sizeof(answer_out->session_id));
179
180 time_t now = time(NULL);
181 snprintf(answer_out->session_version, sizeof(answer_out->session_version), "%ld", now);
182
183 // Answer audio section: accept Opus
184 answer_out->has_audio = offer->has_audio;
185 if (answer_out->has_audio) {
186 memcpy(&answer_out->audio_config, audio_config, sizeof(opus_config_t));
187 }
188
189 // Answer video section: find best mutually-supported codec
190 answer_out->has_video = offer->has_video;
191
192 if (answer_out->has_video && offer->video_codecs && offer->video_codec_count > 0) {
193 // Find best codec: iterate through server preferences and find first match in offer
194 int selected_index = -1;
195
196 for (size_t s = 0; s < server_capability_count; s++) {
197 for (size_t o = 0; o < offer->video_codec_count; o++) {
198 if (server_capabilities[s].codec == offer->video_codecs[o].codec) {
199 selected_index = (int)s;
200 break; // Found match, use server's preference order
201 }
202 }
203 if (selected_index >= 0) {
204 break;
205 }
206 }
207
208 // Allocate and fill selected codec
210 answer_out->video_codec_count = 1;
211
212 // Use server's capability with any server_format overrides
213 if (selected_index >= 0) {
214 memcpy(answer_out->video_codecs, &server_capabilities[selected_index], sizeof(terminal_capability_t));
215 } else {
216 // Fallback to monochrome
217 memcpy(answer_out->video_codecs, &server_capabilities[0], sizeof(terminal_capability_t));
218 }
219
220 // Apply server format constraints if provided
221 if (server_format) {
222 if (server_format->width > 0) {
223 answer_out->video_codecs[0].format.width = server_format->width;
224 }
225 if (server_format->height > 0) {
226 answer_out->video_codecs[0].format.height = server_format->height;
227 }
228 if (server_format->renderer != RENDERER_BLOCK) {
229 answer_out->video_codecs[0].format.renderer = server_format->renderer;
230 }
231 if (server_format->compression != COMPRESSION_NONE) {
232 answer_out->video_codecs[0].format.compression = server_format->compression;
233 }
234 }
235
236 memcpy(&answer_out->video_format, &answer_out->video_codecs[0].format, sizeof(terminal_format_params_t));
237 }
238
239 // Build complete SDP answer
240 char *sdp = answer_out->sdp_string;
241 size_t remaining = sizeof(answer_out->sdp_string);
242 int written = 0;
243
244 // Session-level attributes
245 written = snprintf(sdp, remaining, "v=0\r\n");
246 sdp += written;
247 remaining -= written;
248
249 written = snprintf(sdp, remaining, "o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", answer_out->session_id,
250 answer_out->session_version);
251 sdp += written;
252 remaining -= written;
253
254 written = snprintf(sdp, remaining, "s=-\r\n");
255 sdp += written;
256 remaining -= written;
257
258 written = snprintf(sdp, remaining, "t=0 0\r\n");
259 sdp += written;
260 remaining -= written;
261
262 // Audio media section (if present in offer)
263 if (answer_out->has_audio) {
264 written = snprintf(sdp, remaining, "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
265 sdp += written;
266 remaining -= written;
267
268 written = snprintf(sdp, remaining, "a=rtpmap:111 opus/48000/2\r\n");
269 sdp += written;
270 remaining -= written;
271
272 written = snprintf(sdp, remaining, "a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
273 sdp += written;
274 remaining -= written;
275 }
276
277 // Video media section (if present in offer)
278 if (answer_out->has_video && answer_out->video_codecs && answer_out->video_codec_count > 0) {
279 written = snprintf(sdp, remaining, "m=video 9 UDP/TLS/RTP/SAVPF 96\r\n");
280 sdp += written;
281 remaining -= written;
282
283 const char *codec_name = sdp_codec_name(answer_out->video_codecs[0].codec);
284 written = snprintf(sdp, remaining, "a=rtpmap:96 %s/90000\r\n", codec_name);
285 sdp += written;
286 remaining -= written;
287
288 // Format parameters
289 const terminal_format_params_t *cap_format = &answer_out->video_codecs[0].format;
290 const char *renderer_name = sdp_renderer_name(cap_format->renderer);
291 const char *charset_name = (cap_format->charset == CHARSET_UTF8) ? "utf8" : "ascii";
292 const char *compression_name = "none";
293 if (cap_format->compression == COMPRESSION_RLE) {
294 compression_name = "rle";
295 } else if (cap_format->compression == COMPRESSION_ZSTD) {
296 compression_name = "zstd";
297 }
298
299 written =
300 snprintf(sdp, remaining, "a=fmtp:96 width=%u;height=%u;renderer=%s;charset=%s;compression=%s;csi_rep=%d\r\n",
301 cap_format->width, cap_format->height, renderer_name, charset_name, compression_name,
302 cap_format->csi_rep_support ? 1 : 0);
303 sdp += written;
304 remaining -= written;
305 }
306
307 answer_out->sdp_length = strlen(answer_out->sdp_string);
308
309 log_debug("SDP: Generated answer with codec %s", sdp_codec_name(answer_out->video_codecs[0].codec));
310
311 return ASCIICHAT_OK;
312}
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:358
#define SAFE_MALLOC(size, cast)
Definition common.h:208
const char * sdp_codec_name(acip_codec_t codec)
Get human-readable codec name.
Definition sdp.c:21
const char * sdp_renderer_name(int renderer_type)
Get human-readable renderer name.
Definition sdp.c:36
Opus codec parameters for ascii-chat.
Definition sdp.h:57
SDP media session (simplified for WebRTC)
Definition sdp.h:114
terminal_format_params_t video_format
Default format parameters.
Definition sdp.h:127
size_t video_codec_count
Number of supported capabilities.
Definition sdp.h:126
char session_version[16]
Session version (timestamp)
Definition sdp.h:117
size_t sdp_length
Length of SDP string (excluding null)
Definition sdp.h:131
bool has_audio
Audio media included.
Definition sdp.h:120
char session_id[32]
Session identifier.
Definition sdp.h:116
terminal_capability_t * video_codecs
Array of supported capabilities.
Definition sdp.h:125
opus_config_t audio_config
Opus codec configuration.
Definition sdp.h:121
bool has_video
Video media included.
Definition sdp.h:124
char sdp_string[4096]
Complete SDP as null-terminated string.
Definition sdp.h:130
Terminal rendering format parameters.
Definition sdp.h:86

References ASCIICHAT_OK, sdp_session_t::audio_config, terminal_format_params_t::charset, terminal_capability_t::codec, terminal_format_params_t::compression, terminal_format_params_t::csi_rep_support, ERROR_INVALID_PARAM, terminal_capability_t::format, sdp_session_t::has_audio, sdp_session_t::has_video, terminal_format_params_t::height, log_debug, terminal_format_params_t::renderer, SAFE_MALLOC, SAFE_STRNCPY, sdp_codec_name(), sdp_session_t::sdp_length, sdp_renderer_name(), sdp_session_t::sdp_string, sdp_session_t::session_id, sdp_session_t::session_version, SET_ERRNO, sdp_session_t::video_codec_count, sdp_session_t::video_codecs, sdp_session_t::video_format, and terminal_format_params_t::width.

◆ sdp_generate_offer()

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)

Creates an SDP offer with:

  • Opus audio codec
  • Terminal capabilities in client preference order
Parameters
capabilitiesArray of supported terminal capabilities
capability_countNumber of capabilities
audio_configOpus codec configuration
formatFormat parameters for video section
offer_outGenerated SDP offer (output)
Returns
ASCIICHAT_OK on success, error code on failure
Note
Caller must free offer_out using sdp_session_free()

Definition at line 53 of file sdp.c.

55 {
56 if (!capabilities || capability_count == 0 || !audio_config || !offer_out) {
57 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP offer parameters");
58 }
59
60 memset(offer_out, 0, sizeof(sdp_session_t));
61
62 // Generate session ID and version from current time
63 time_t now = time(NULL);
64 snprintf(offer_out->session_id, sizeof(offer_out->session_id), "%ld", now);
65 snprintf(offer_out->session_version, sizeof(offer_out->session_version), "%ld", now);
66
67 // Allocate and copy video codecs
68 offer_out->video_codecs = SAFE_MALLOC(capability_count * sizeof(terminal_capability_t), terminal_capability_t *);
69 memcpy(offer_out->video_codecs, capabilities, capability_count * sizeof(terminal_capability_t));
70 offer_out->video_codec_count = capability_count;
71
72 offer_out->has_audio = true;
73 memcpy(&offer_out->audio_config, audio_config, sizeof(opus_config_t));
74
75 offer_out->has_video = true;
76 if (format) {
77 memcpy(&offer_out->video_format, format, sizeof(terminal_format_params_t));
78 }
79
80 // Build complete SDP offer
81 char *sdp = offer_out->sdp_string;
82 size_t remaining = sizeof(offer_out->sdp_string);
83 int written = 0;
84
85 // Session-level attributes
86 written = snprintf(sdp, remaining, "v=0\r\n");
87 sdp += written;
88 remaining -= written;
89
90 written = snprintf(sdp, remaining, "o=ascii-chat %s %s IN IP4 0.0.0.0\r\n", offer_out->session_id,
91 offer_out->session_version);
92 sdp += written;
93 remaining -= written;
94
95 written = snprintf(sdp, remaining, "s=-\r\n");
96 sdp += written;
97 remaining -= written;
98
99 written = snprintf(sdp, remaining, "t=0 0\r\n");
100 sdp += written;
101 remaining -= written;
102
103 // Audio media section
104 written = snprintf(sdp, remaining, "m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n");
105 sdp += written;
106 remaining -= written;
107
108 written = snprintf(sdp, remaining, "a=rtpmap:111 opus/48000/2\r\n");
109 sdp += written;
110 remaining -= written;
111
112 written = snprintf(sdp, remaining, "a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1\r\n");
113 sdp += written;
114 remaining -= written;
115
116 // Video media section with terminal capabilities
117 char codec_list[128] = "96";
118 for (size_t i = 1; i < capability_count; i++) {
119 char codec_num[4];
120 snprintf(codec_num, sizeof(codec_num), " %d", 96 + (int)i);
121 size_t len = strlen(codec_list);
122 strncat(codec_list, codec_num, sizeof(codec_list) - len - 1);
123 }
124
125 written = snprintf(sdp, remaining, "m=video 9 UDP/TLS/RTP/SAVPF %s\r\n", codec_list);
126 sdp += written;
127 remaining -= written;
128
129 // Add rtpmap and fmtp for each capability
130 for (size_t i = 0; i < capability_count; i++) {
131 int pt = 96 + (int)i;
132 const char *codec_name = sdp_codec_name(capabilities[i].codec);
133
134 written = snprintf(sdp, remaining, "a=rtpmap:%d %s/90000\r\n", pt, codec_name);
135 sdp += written;
136 remaining -= written;
137
138 // Format parameters
139 const terminal_format_params_t *cap_format = &capabilities[i].format;
140 const char *renderer_name = sdp_renderer_name(cap_format->renderer);
141 const char *charset_name = (cap_format->charset == CHARSET_UTF8) ? "utf8" : "ascii";
142 const char *compression_name = "none";
143 if (cap_format->compression == COMPRESSION_RLE) {
144 compression_name = "rle";
145 } else if (cap_format->compression == COMPRESSION_ZSTD) {
146 compression_name = "zstd";
147 }
148
149 written =
150 snprintf(sdp, remaining, "a=fmtp:%d width=%u;height=%u;renderer=%s;charset=%s;compression=%s;csi_rep=%d\r\n",
151 pt, cap_format->width, cap_format->height, renderer_name, charset_name, compression_name,
152 cap_format->csi_rep_support ? 1 : 0);
153 sdp += written;
154 remaining -= written;
155 }
156
157 offer_out->sdp_length = strlen(offer_out->sdp_string);
158
159 log_debug("SDP: Generated offer with %zu video codecs and Opus audio", capability_count);
160
161 return ASCIICHAT_OK;
162}

References ASCIICHAT_OK, sdp_session_t::audio_config, terminal_format_params_t::charset, terminal_format_params_t::compression, terminal_format_params_t::csi_rep_support, ERROR_INVALID_PARAM, terminal_capability_t::format, sdp_session_t::has_audio, sdp_session_t::has_video, terminal_format_params_t::height, log_debug, terminal_format_params_t::renderer, SAFE_MALLOC, sdp_codec_name(), sdp_session_t::sdp_length, sdp_renderer_name(), sdp_session_t::sdp_string, sdp_session_t::session_id, sdp_session_t::session_version, SET_ERRNO, sdp_session_t::video_codec_count, sdp_session_t::video_codecs, sdp_session_t::video_format, and terminal_format_params_t::width.

◆ sdp_get_selected_video_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 
)

Extract selected video codec from SDP answer.

Determines which terminal rendering capability the peer selected.

Parameters
answerSDP answer from peer
selected_codecSelected codec (output)
selected_formatFormat parameters (output)
Returns
ASCIICHAT_OK if valid selection found, error otherwise

Definition at line 534 of file sdp.c.

535 {
536 if (!answer || !selected_codec || !selected_format) {
537 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for codec selection");
538 }
539
540 if (!answer->has_video || !answer->video_codecs || answer->video_codec_count == 0) {
541 return SET_ERRNO(ERROR_NOT_FOUND, "No video codec in answer");
542 }
543
544 // In SDP answer, server selects only ONE codec (server's preference)
545 // The first codec in the answer is the selected one
546 terminal_capability_t *selected_cap = &answer->video_codecs[0];
547
548 *selected_codec = selected_cap->codec;
549 memcpy(selected_format, &selected_cap->format, sizeof(terminal_format_params_t));
550
551 log_debug("SDP: Selected video codec %s with resolution %ux%u", sdp_codec_name(selected_cap->codec),
552 selected_cap->format.width, selected_cap->format.height);
553
554 return ASCIICHAT_OK;
555}
@ ERROR_NOT_FOUND

References ASCIICHAT_OK, terminal_capability_t::codec, ERROR_INVALID_PARAM, ERROR_NOT_FOUND, terminal_capability_t::format, sdp_session_t::has_video, terminal_format_params_t::height, log_debug, sdp_codec_name(), SET_ERRNO, sdp_session_t::video_codec_count, sdp_session_t::video_codecs, and terminal_format_params_t::width.

◆ sdp_parse()

asciichat_error_t sdp_parse ( const char *  sdp_string,
sdp_session_t session 
)

Parse SDP offer or answer.

Parses an SDP string and extracts audio/video configurations.

Parameters
sdp_stringSDP string (null-terminated)
sessionParsed session (output)
Returns
ASCIICHAT_OK on success, error code on failure

Definition at line 318 of file sdp.c.

318 {
319 if (!sdp_string || !session) {
320 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid SDP parse parameters");
321 }
322
323 memset(session, 0, sizeof(sdp_session_t));
324
325 // Copy original SDP string for reference
326 SAFE_STRNCPY(session->sdp_string, sdp_string, sizeof(session->sdp_string));
327 session->sdp_length = strlen(sdp_string);
328
329 // Parse line by line
330 char line_buffer[512];
331 SAFE_STRNCPY(line_buffer, sdp_string, sizeof(line_buffer));
332
333 char *saveptr = NULL;
334 char *line = strtok_r(line_buffer, "\r\n", &saveptr);
335
336 int in_audio_section = 0;
337 int in_video_section = 0;
338 size_t video_codec_index = 0;
339
340 while (line) {
341 // Parse line format: "key=value"
342 char *equals = strchr(line, '=');
343 if (!equals) {
344 line = strtok_r(NULL, "\r\n", &saveptr);
345 continue;
346 }
347
348 char key = line[0];
349 const char *value = equals + 1;
350
351 switch (key) {
352 case 'v':
353 // v=0 (version)
354 break;
355
356 case 'o':
357 // o=username session_id session_version ... session_id ...
358 // Parse session ID (second field)
359 {
360 char o_copy[256];
361 SAFE_STRNCPY(o_copy, value, sizeof(o_copy));
362 char *o_saveptr = NULL;
363 strtok_r(o_copy, " ", &o_saveptr); // username
364 char *session_id_str = strtok_r(NULL, " ", &o_saveptr);
365 if (session_id_str) {
366 SAFE_STRNCPY(session->session_id, session_id_str, sizeof(session->session_id));
367 }
368 }
369 break;
370
371 case 's':
372 // s=session_name (typically "-" or empty)
373 break;
374
375 case 'm':
376 // m=audio 9 UDP/TLS/RTP/SAVPF ...
377 // m=video 9 UDP/TLS/RTP/SAVPF ...
378 in_audio_section = 0;
379 in_video_section = 0;
380
381 if (strstr(value, "audio")) {
382 in_audio_section = 1;
383 session->has_audio = true;
384 } else if (strstr(value, "video")) {
385 in_video_section = 1;
386 session->has_video = true;
387 video_codec_index = 0;
388 }
389 break;
390
391 case 'a':
392 // a=rtpmap:111 opus/48000/2
393 // a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
394 // a=rtpmap:96 ACIP-TC/90000
395 // a=fmtp:96 ...
396
397 if (strstr(value, "rtpmap:")) {
398 const char *rtpmap = value + 7; // Skip "rtpmap:"
399
400 // Parse: PT codec/rate[/channels]
401 int pt = 0;
402 char codec_name[32] = {0};
403 sscanf(rtpmap, "%d %s", &pt, codec_name);
404
405 if (in_audio_section && pt == 111 && strstr(codec_name, "opus")) {
406 // Opus audio
407 session->audio_config.sample_rate = 48000;
408 session->audio_config.channels = 2;
409 } else if (in_video_section && video_codec_index < 4) {
410 // Terminal capability codec
411 if (!session->video_codecs) {
413 }
414
415 terminal_capability_t *cap = &session->video_codecs[video_codec_index];
416 memset(cap, 0, sizeof(terminal_capability_t));
417
418 if (pt == 96 && strstr(codec_name, "ACIP-TC")) {
420 video_codec_index++;
421 session->video_codec_count++;
422 } else if (pt == 97 && strstr(codec_name, "ACIP-256")) {
424 video_codec_index++;
425 session->video_codec_count++;
426 } else if (pt == 98 && strstr(codec_name, "ACIP-16")) {
428 video_codec_index++;
429 session->video_codec_count++;
430 } else if (pt == 99 && strstr(codec_name, "ACIP-MONO")) {
431 cap->codec = ACIP_CODEC_MONO;
432 video_codec_index++;
433 session->video_codec_count++;
434 }
435 }
436 } else if (strstr(value, "fmtp:")) {
437 // Parse format parameters
438 // a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
439 // a=fmtp:96 width=80;height=24;renderer=block;charset=utf8;compression=rle;csi_rep=1
440
441 const char *fmtp = value + 5; // Skip "fmtp:"
442
443 if (in_audio_section) {
444 // Parse Opus parameters
445 if (strstr(fmtp, "useinbandfec=1")) {
446 session->audio_config.fec_enabled = true;
447 }
448 if (strstr(fmtp, "usedtx=1")) {
449 session->audio_config.dtx_enabled = true;
450 }
451 // Default values for Opus
452 session->audio_config.bitrate = 24000;
453 session->audio_config.frame_duration = 20;
454 } else if (in_video_section && video_codec_index > 0) {
455 // Parse terminal format parameters
456 terminal_capability_t *cap = &session->video_codecs[video_codec_index - 1];
457
458 char fmtp_copy[256];
459 SAFE_STRNCPY(fmtp_copy, fmtp, sizeof(fmtp_copy));
460
461 // Skip PT number
462 char *saveptr = NULL;
463 strtok_r(fmtp_copy, " ", &saveptr);
464 const char *params = fmtp_copy + strcspn(fmtp_copy, " ") + 1;
465
466 // Parse width=N
467 const char *width_str = strstr(params, "width=");
468 if (width_str) {
469 sscanf(width_str + 6, "%hu", &cap->format.width);
470 }
471
472 // Parse height=N
473 const char *height_str = strstr(params, "height=");
474 if (height_str) {
475 sscanf(height_str + 7, "%hu", &cap->format.height);
476 }
477
478 // Parse renderer type
479 const char *renderer_str = strstr(params, "renderer=");
480 if (renderer_str) {
481 if (strstr(renderer_str, "block")) {
482 cap->format.renderer = RENDERER_BLOCK;
483 } else if (strstr(renderer_str, "halfblock")) {
484 cap->format.renderer = RENDERER_HALFBLOCK;
485 } else if (strstr(renderer_str, "braille")) {
486 cap->format.renderer = RENDERER_BRAILLE;
487 }
488 }
489
490 // Parse charset
491 const char *charset_str = strstr(params, "charset=");
492 if (charset_str) {
493 if (strstr(charset_str, "utf8")) {
494 cap->format.charset = CHARSET_UTF8;
495 } else if (strstr(charset_str, "utf8_wide")) {
496 cap->format.charset = CHARSET_UTF8_WIDE;
497 } else {
498 cap->format.charset = CHARSET_ASCII;
499 }
500 }
501
502 // Parse compression
503 const char *compression_str = strstr(params, "compression=");
504 if (compression_str) {
505 if (strstr(compression_str, "rle")) {
506 cap->format.compression = COMPRESSION_RLE;
507 } else if (strstr(compression_str, "zstd")) {
508 cap->format.compression = COMPRESSION_ZSTD;
509 } else {
510 cap->format.compression = COMPRESSION_NONE;
511 }
512 }
513
514 // Parse CSI REP support
515 const char *csi_rep_str = strstr(params, "csi_rep=");
516 if (csi_rep_str) {
517 cap->format.csi_rep_support = (csi_rep_str[8] == '1');
518 }
519 }
520 }
521 break;
522 }
523
524 line = strtok_r(NULL, "\r\n", &saveptr);
525 }
526
527 return ASCIICHAT_OK;
528}
bool dtx_enabled
Discontinuous Transmission (silence suppression)
Definition sdp.h:62
bool fec_enabled
Forward Error Correction for lossy networks.
Definition sdp.h:63
uint32_t sample_rate
48000 Hz (Opus native rate)
Definition sdp.h:58
uint32_t bitrate
24000 bps (good quality for speech)
Definition sdp.h:60
uint16_t frame_duration
20 ms (balance latency/efficiency)
Definition sdp.h:61
uint8_t channels
1 (mono for voice chat)
Definition sdp.h:59

References ACIP_CODEC_16COLOR, ACIP_CODEC_256COLOR, ACIP_CODEC_MONO, ACIP_CODEC_TRUECOLOR, ASCIICHAT_OK, sdp_session_t::audio_config, opus_config_t::bitrate, opus_config_t::channels, terminal_format_params_t::charset, terminal_capability_t::codec, terminal_format_params_t::compression, terminal_format_params_t::csi_rep_support, opus_config_t::dtx_enabled, ERROR_INVALID_PARAM, opus_config_t::fec_enabled, terminal_capability_t::format, opus_config_t::frame_duration, sdp_session_t::has_audio, sdp_session_t::has_video, terminal_format_params_t::height, terminal_format_params_t::renderer, SAFE_MALLOC, SAFE_STRNCPY, opus_config_t::sample_rate, sdp_session_t::sdp_length, sdp_session_t::sdp_string, sdp_session_t::session_id, SET_ERRNO, sdp_session_t::video_codec_count, sdp_session_t::video_codecs, and terminal_format_params_t::width.

◆ sdp_renderer_name()

const char * sdp_renderer_name ( int  renderer_type)

Get human-readable renderer name.

Returns
Static string (e.g., "block", "halfblock")

Definition at line 36 of file sdp.c.

36 {
37 switch (renderer_type) {
38 case RENDERER_BLOCK:
39 return "block";
40 case RENDERER_HALFBLOCK:
41 return "halfblock";
42 case RENDERER_BRAILLE:
43 return "braille";
44 default:
45 return "unknown";
46 }
47}

Referenced by sdp_generate_answer(), and sdp_generate_offer().

◆ sdp_session_free()

void sdp_session_free ( sdp_session_t session)

Free SDP session resources.

Parameters
sessionSession to free

Definition at line 665 of file sdp.c.

665 {
666 if (!session) {
667 return;
668 }
669
670 // Free allocated video codec array
671 if (session->video_codecs) {
672 SAFE_FREE(session->video_codecs);
673 session->video_codec_count = 0;
674 }
675
676 // Zero out the session structure
677 memset(session, 0, sizeof(sdp_session_t));
678}
#define SAFE_FREE(ptr)
Definition common.h:320

References SAFE_FREE, sdp_session_t::video_codec_count, and sdp_session_t::video_codecs.