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

SDP (Session Description Protocol) for WebRTC audio/video negotiation. More...

Go to the source code of this file.

Data Structures

struct  opus_config_t
 Opus codec parameters for ascii-chat. More...
 
struct  terminal_format_params_t
 Terminal rendering format parameters. More...
 
struct  terminal_capability_t
 Supported terminal capability (for offer/answer) More...
 
struct  sdp_session_t
 SDP media session (simplified for WebRTC) More...
 

Enumerations

enum  acip_codec_t { ACIP_CODEC_TRUECOLOR = 96 , ACIP_CODEC_256COLOR = 97 , ACIP_CODEC_16COLOR = 98 , ACIP_CODEC_MONO = 99 }
 Terminal rendering capability payload types. More...
 

Functions

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.
 
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.
 

Detailed Description

SDP (Session Description Protocol) for WebRTC audio/video negotiation.

Handles SDP offer/answer generation and parsing for WebRTC connections. Includes:

  • Opus audio codec negotiation (48kHz, mono, 24kbps)
  • Terminal capability negotiation via custom ACIP video "codecs"
  • Format parameters for resolution, renderer, charset, compression

SDP Offer/Answer Flow

Client (Joiner) generates OFFER:

  • Lists supported audio codecs (Opus)
  • Lists supported terminal rendering modes in preference order
  • Server selects best mutually-supported mode

Server (Creator) generates ANSWER:

  • Selects single audio codec (Opus)
  • Selects best terminal rendering mode
  • Server enforces its rendering constraints

Terminal Capability "Codecs"

These are RTP payload types that represent terminal rendering modes:

  • PT 96: ACIP-TC (Truecolor, 24-bit RGB)
  • PT 97: ACIP-256 (256-color xterm)
  • PT 98: ACIP-16 (16-color ANSI)
  • PT 99: ACIP-MONO (Monochrome, ASCII only)
Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
January 2026

Definition in file sdp.h.

Enumeration Type Documentation

◆ acip_codec_t

Terminal rendering capability payload types.

These are custom "codecs" that represent terminal rendering modes. Used in SDP to negotiate which color mode both peers can support.

Enumerator
ACIP_CODEC_TRUECOLOR 

24-bit RGB (truecolor)

ACIP_CODEC_256COLOR 

256-color (xterm palette)

ACIP_CODEC_16COLOR 

16-color (ANSI standard)

ACIP_CODEC_MONO 

Monochrome (ASCII only)

Definition at line 76 of file sdp.h.

76 {
80 ACIP_CODEC_MONO = 99,
acip_codec_t
Terminal rendering capability payload types.
Definition sdp.h:76
@ 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

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}

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.