aes67_sdp.c 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. /*
  2. * SPDX-FileCopyrightText: 2026 DatanoiseTV
  3. *
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. /*
  7. * SDP (Session Description Protocol) parser and generator for AES67.
  8. *
  9. * Generates and parses SDP documents conforming to AES67 / RAVENNA
  10. * requirements, including PTP reference clock and media clock attributes.
  11. */
  12. #include "aes67_sdp.h"
  13. #include "aes67_net.h"
  14. #include <string.h>
  15. #include <stdio.h>
  16. #include <stdlib.h>
  17. #include <ctype.h>
  18. #include <inttypes.h>
  19. #include "esp_log.h"
  20. static const char *TAG = "aes67_sdp";
  21. const char *aes67_codec_to_str(aes67_codec_t codec)
  22. {
  23. switch (codec) {
  24. case AES67_CODEC_L16: return "L16";
  25. case AES67_CODEC_L24: return "L24";
  26. case AES67_CODEC_L32: return "L32";
  27. case AES67_CODEC_AM824: return "AM824";
  28. default: return "L24";
  29. }
  30. }
  31. int aes67_codec_from_str(const char *name)
  32. {
  33. if (!name) return -1;
  34. if (strcmp(name, "L16") == 0) return AES67_CODEC_L16;
  35. if (strcmp(name, "L24") == 0) return AES67_CODEC_L24;
  36. if (strcmp(name, "L32") == 0) return AES67_CODEC_L32;
  37. if (strcmp(name, "AM824") == 0) return AES67_CODEC_AM824;
  38. return -1;
  39. }
  40. /* aes67_codec_word_length() is now inline in aes67_config.h */
  41. int aes67_sdp_generate(const aes67_sdp_t *sdp, char *buf, size_t buf_len)
  42. {
  43. if (!sdp || !buf || buf_len == 0) {
  44. return -1;
  45. }
  46. char origin_ip_str[16];
  47. char conn_ip_str[16];
  48. aes67_net_u32_to_ip(sdp->origin_ip, origin_ip_str, sizeof(origin_ip_str));
  49. aes67_net_u32_to_ip(sdp->connection_ip, conn_ip_str, sizeof(conn_ip_str));
  50. const char *codec_str = aes67_codec_to_str(sdp->codec);
  51. const char *username = sdp->origin_username[0] ? sdp->origin_username : "-";
  52. /* Build ptime string. AES67 uses fractional ms for sub-ms packet times. */
  53. char ptime_str[16];
  54. if (sdp->ptime_us >= 1000 && (sdp->ptime_us % 1000) == 0) {
  55. snprintf(ptime_str, sizeof(ptime_str), "%u", sdp->ptime_us / 1000);
  56. } else if (sdp->ptime_us >= 1000) {
  57. /* e.g. 1500us -> "1.5" */
  58. snprintf(ptime_str, sizeof(ptime_str), "%u.%03u",
  59. sdp->ptime_us / 1000, sdp->ptime_us % 1000);
  60. /* Strip trailing zeros after decimal point */
  61. size_t len = strlen(ptime_str);
  62. while (len > 0 && ptime_str[len - 1] == '0') {
  63. ptime_str[--len] = '\0';
  64. }
  65. } else {
  66. /* Sub-millisecond: e.g. 125us -> "0.125" */
  67. snprintf(ptime_str, sizeof(ptime_str), "0.%03u", sdp->ptime_us);
  68. size_t len = strlen(ptime_str);
  69. while (len > 0 && ptime_str[len - 1] == '0') {
  70. ptime_str[--len] = '\0';
  71. }
  72. }
  73. /* Build PTP grandmaster ID string: XX-XX-XX-FF-FE-XX-XX-XX */
  74. char ptp_gm_str[24] = {0};
  75. if (sdp->has_ptp_refclk) {
  76. snprintf(ptp_gm_str, sizeof(ptp_gm_str),
  77. "%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X",
  78. sdp->ptp_grandmaster_id[0], sdp->ptp_grandmaster_id[1],
  79. sdp->ptp_grandmaster_id[2], sdp->ptp_grandmaster_id[3],
  80. sdp->ptp_grandmaster_id[4], sdp->ptp_grandmaster_id[5],
  81. sdp->ptp_grandmaster_id[6], sdp->ptp_grandmaster_id[7]);
  82. }
  83. /* Compute samples per packet for the framecount attribute */
  84. uint32_t framecount = (sdp->sample_rate * sdp->ptime_us) / 1000000;
  85. if (framecount == 0) framecount = 48;
  86. int written = snprintf(buf, buf_len,
  87. "v=0\r\n"
  88. "o=%s %" PRIu32 " %" PRIu32 " IN IP4 %s\r\n"
  89. "s=%s\r\n"
  90. "c=IN IP4 %s/%u\r\n"
  91. "t=0 0\r\n"
  92. "m=audio %u RTP/AVP %u\r\n"
  93. "i=Channels 1-%u\r\n"
  94. "a=sync-time:0\r\n"
  95. "a=framecount:%" PRIu32 "\r\n"
  96. "a=rtpmap:%u %s/%" PRIu32 "/%u\r\n"
  97. "a=ptime:%s\r\n"
  98. "a=%s\r\n",
  99. username, sdp->session_id, sdp->session_version, origin_ip_str,
  100. sdp->session_name,
  101. conn_ip_str, sdp->ttl,
  102. sdp->port, sdp->payload_type,
  103. sdp->channels,
  104. framecount,
  105. sdp->payload_type, codec_str, sdp->sample_rate, sdp->channels,
  106. ptime_str,
  107. sdp->is_source ? "sendonly" : "recvonly");
  108. if (written < 0 || (size_t)written >= buf_len) {
  109. ESP_LOGE(TAG, "SDP buffer overflow during generate");
  110. return -1;
  111. }
  112. /* Append PTP reference clock attribute.
  113. * Use "traceable" when the GM is known and traceable,
  114. * otherwise include the specific GM identity. */
  115. if (sdp->has_ptp_refclk) {
  116. int n;
  117. if (sdp->ptp_traceable) {
  118. n = snprintf(buf + written, buf_len - written,
  119. "a=ts-refclk:ptp=IEEE1588-2008:traceable\r\n");
  120. } else {
  121. n = snprintf(buf + written, buf_len - written,
  122. "a=ts-refclk:ptp=IEEE1588-2008:%s:%u\r\n",
  123. ptp_gm_str, sdp->ptp_domain);
  124. }
  125. if (n < 0 || (size_t)(written + n) >= buf_len) {
  126. return -1;
  127. }
  128. written += n;
  129. }
  130. /* Append media clock attribute */
  131. if (sdp->has_mediaclk) {
  132. int n = snprintf(buf + written, buf_len - written,
  133. "a=mediaclk:direct=%" PRIu32 "\r\n",
  134. sdp->mediaclk_offset);
  135. if (n < 0 || (size_t)(written + n) >= buf_len) {
  136. return -1;
  137. }
  138. written += n;
  139. }
  140. /* Append clock domain */
  141. {
  142. int n = snprintf(buf + written, buf_len - written,
  143. "a=clock-domain:PTPv2 %u\r\n",
  144. sdp->ptp_domain);
  145. if (n < 0 || (size_t)(written + n) >= buf_len) {
  146. return -1;
  147. }
  148. written += n;
  149. }
  150. return written;
  151. }
  152. /* Parse a single PTP grandmaster ID from "XX-XX-XX-XX-XX-XX-XX-XX" format */
  153. static bool parse_ptp_gmid(const char *str, uint8_t gm[8])
  154. {
  155. if (strlen(str) < 23) return false;
  156. unsigned int vals[8];
  157. int matched = sscanf(str, "%02X-%02X-%02X-%02X-%02X-%02X-%02X-%02X",
  158. &vals[0], &vals[1], &vals[2], &vals[3],
  159. &vals[4], &vals[5], &vals[6], &vals[7]);
  160. if (matched != 8) return false;
  161. for (int i = 0; i < 8; i++) {
  162. gm[i] = (uint8_t)vals[i];
  163. }
  164. return true;
  165. }
  166. /* Parse ptime value which may be fractional (e.g. "1", "0.125", "1.5") */
  167. static uint16_t parse_ptime_us(const char *str)
  168. {
  169. /* Split on decimal point */
  170. const char *dot = strchr(str, '.');
  171. if (!dot) {
  172. /* Integer milliseconds */
  173. return (uint16_t)(atoi(str) * 1000);
  174. }
  175. /* Integer part in ms */
  176. int integer_ms = atoi(str);
  177. /* Fractional part - pad to 3 digits for microseconds */
  178. const char *frac = dot + 1;
  179. int frac_val = 0;
  180. int digits = 0;
  181. while (*frac && isdigit((unsigned char)*frac) && digits < 3) {
  182. frac_val = frac_val * 10 + (*frac - '0');
  183. frac++;
  184. digits++;
  185. }
  186. /* Pad remaining digits to get microseconds */
  187. while (digits < 3) {
  188. frac_val *= 10;
  189. digits++;
  190. }
  191. return (uint16_t)(integer_ms * 1000 + frac_val);
  192. }
  193. /* Parse a=rtpmap line, e.g. "98 L24/48000/2" */
  194. static void parse_rtpmap(const char *val, aes67_sdp_t *sdp)
  195. {
  196. unsigned int pt;
  197. char codec_name[16];
  198. unsigned int rate, ch;
  199. int matched = sscanf(val, "%u %15[^/]/%u/%u", &pt, codec_name, &rate, &ch);
  200. if (matched >= 3) {
  201. sdp->payload_type = (uint8_t)pt;
  202. int codec = aes67_codec_from_str(codec_name);
  203. if (codec >= 0) {
  204. sdp->codec = (aes67_codec_t)codec;
  205. sdp->word_length = aes67_codec_word_length(sdp->codec);
  206. }
  207. sdp->sample_rate = rate;
  208. sdp->channels = (matched >= 4) ? (uint8_t)ch : 1;
  209. }
  210. }
  211. /* Parse a=ts-refclk line, e.g. "ptp=IEEE1588-2008:XX-XX-...:0" */
  212. static void parse_ts_refclk(const char *val, aes67_sdp_t *sdp)
  213. {
  214. /* Check for PTP reference clock */
  215. if (strncmp(val, "ptp=IEEE1588-2008:", 18) != 0) {
  216. /* Could be "ptp=traceable" */
  217. if (strcmp(val, "ptp=traceable") == 0) {
  218. sdp->has_ptp_refclk = true;
  219. sdp->ptp_traceable = true;
  220. }
  221. return;
  222. }
  223. const char *gm_start = val + 18;
  224. if (parse_ptp_gmid(gm_start, sdp->ptp_grandmaster_id)) {
  225. sdp->has_ptp_refclk = true;
  226. }
  227. /* Parse domain after the GM ID: "XX-XX-XX-XX-XX-XX-XX-XX:0" */
  228. const char *colon = strchr(gm_start + 23, ':');
  229. if (colon) {
  230. sdp->ptp_domain = (uint8_t)atoi(colon + 1);
  231. }
  232. }
  233. /* Parse a=mediaclk line, e.g. "direct=0" */
  234. static void parse_mediaclk(const char *val, aes67_sdp_t *sdp)
  235. {
  236. if (strncmp(val, "direct=", 7) == 0) {
  237. sdp->has_mediaclk = true;
  238. sdp->mediaclk_offset = (uint32_t)strtoul(val + 7, NULL, 10);
  239. }
  240. }
  241. /* Parse origin line: "- 12345 1 IN IP4 192.168.1.10" */
  242. static void parse_origin(const char *val, aes67_sdp_t *sdp)
  243. {
  244. char username[32];
  245. unsigned int sid, sver;
  246. char net_type[4], addr_type[8], addr[16];
  247. int matched = sscanf(val, "%31s %u %u %3s %7s %15s",
  248. username, &sid, &sver, net_type, addr_type, addr);
  249. if (matched >= 6) {
  250. strncpy(sdp->origin_username, username, sizeof(sdp->origin_username) - 1);
  251. sdp->origin_username[sizeof(sdp->origin_username) - 1] = '\0';
  252. sdp->session_id = sid;
  253. sdp->session_version = sver;
  254. sdp->origin_ip = aes67_net_ip_to_u32(addr);
  255. }
  256. }
  257. /* Parse connection line: "IN IP4 239.69.0.1/32" */
  258. static void parse_connection(const char *val, aes67_sdp_t *sdp)
  259. {
  260. char net_type[4], addr_type[8], addr_with_ttl[32];
  261. int matched = sscanf(val, "%3s %7s %31s", net_type, addr_type, addr_with_ttl);
  262. if (matched < 3) return;
  263. /* Split address and TTL at '/' */
  264. char *slash = strchr(addr_with_ttl, '/');
  265. if (slash) {
  266. *slash = '\0';
  267. sdp->ttl = (uint8_t)atoi(slash + 1);
  268. }
  269. sdp->connection_ip = aes67_net_ip_to_u32(addr_with_ttl);
  270. }
  271. /* Parse media line: "audio 5004 RTP/AVP 98" */
  272. static void parse_media(const char *val, aes67_sdp_t *sdp)
  273. {
  274. char media_type[16], transport[16];
  275. unsigned int port, pt;
  276. int matched = sscanf(val, "%15s %u %15s %u", media_type, &port, transport, &pt);
  277. if (matched >= 4 && strcmp(media_type, "audio") == 0) {
  278. sdp->port = (uint16_t)port;
  279. sdp->payload_type = (uint8_t)pt;
  280. }
  281. }
  282. /* Process a single attribute (a=) line */
  283. static void parse_attribute(const char *val, aes67_sdp_t *sdp)
  284. {
  285. if (strncmp(val, "rtpmap:", 7) == 0) {
  286. parse_rtpmap(val + 7, sdp);
  287. } else if (strncmp(val, "ptime:", 6) == 0) {
  288. sdp->ptime_us = parse_ptime_us(val + 6);
  289. } else if (strncmp(val, "ts-refclk:", 10) == 0) {
  290. parse_ts_refclk(val + 10, sdp);
  291. } else if (strncmp(val, "mediaclk:", 9) == 0) {
  292. parse_mediaclk(val + 9, sdp);
  293. }
  294. }
  295. esp_err_t aes67_sdp_parse(const char *sdp_text, aes67_sdp_t *sdp)
  296. {
  297. if (!sdp_text || !sdp) {
  298. return ESP_ERR_INVALID_ARG;
  299. }
  300. memset(sdp, 0, sizeof(aes67_sdp_t));
  301. /* Work on a mutable copy for line tokenization */
  302. size_t text_len = strlen(sdp_text);
  303. if (text_len == 0 || text_len >= AES67_SDP_MAX_LEN) {
  304. ESP_LOGE(TAG, "SDP text empty or too long (%u bytes)", (unsigned)text_len);
  305. return ESP_ERR_INVALID_ARG;
  306. }
  307. char buf[AES67_SDP_MAX_LEN];
  308. memcpy(buf, sdp_text, text_len + 1);
  309. /* Parse line by line. Handle both \r\n and \n line endings. */
  310. char *saveptr = NULL;
  311. char *line = strtok_r(buf, "\n", &saveptr);
  312. while (line) {
  313. /* Strip trailing \r if present */
  314. size_t len = strlen(line);
  315. if (len > 0 && line[len - 1] == '\r') {
  316. line[len - 1] = '\0';
  317. len--;
  318. }
  319. /* Each SDP line must be at least "X=Y" (3 chars) */
  320. if (len >= 2 && line[1] == '=') {
  321. const char *val = line + 2;
  322. switch (line[0]) {
  323. case 'o':
  324. parse_origin(val, sdp);
  325. break;
  326. case 's':
  327. strncpy(sdp->session_name, val, sizeof(sdp->session_name) - 1);
  328. sdp->session_name[sizeof(sdp->session_name) - 1] = '\0';
  329. break;
  330. case 'c':
  331. parse_connection(val, sdp);
  332. break;
  333. case 'm':
  334. parse_media(val, sdp);
  335. break;
  336. case 'a':
  337. parse_attribute(val, sdp);
  338. break;
  339. default:
  340. /* v=, t= and others we silently skip */
  341. break;
  342. }
  343. }
  344. line = strtok_r(NULL, "\n", &saveptr);
  345. }
  346. /* Sanity check: at minimum we need a port and sample rate */
  347. if (sdp->port == 0 || sdp->sample_rate == 0) {
  348. ESP_LOGW(TAG, "SDP parse incomplete: port=%u rate=%u",
  349. sdp->port, sdp->sample_rate);
  350. return ESP_ERR_INVALID_ARG;
  351. }
  352. /* Derive word length from codec if not set */
  353. if (sdp->word_length == 0) {
  354. sdp->word_length = aes67_codec_word_length(sdp->codec);
  355. }
  356. ESP_LOGD(TAG, "Parsed SDP: %s, %s/%u/%u, port %u, ptime %u us",
  357. sdp->session_name, aes67_codec_to_str(sdp->codec),
  358. sdp->sample_rate, sdp->channels, sdp->port, sdp->ptime_us);
  359. return ESP_OK;
  360. }