/* Session Description Protocol parsing and generator * This shall be simple and is incomplete. * * (C) 2019 by Andreas Eversberg * All Rights Reserved * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include "../libdebug/debug.h" #include "../libtimer/timer.h" #include "endpoint.h" #include "sdp.h" #define strncat_printf(sdp, fmt, arg...) \ { \ snprintf(sdp + strlen(sdp), sizeof(sdp) - strlen(sdp), fmt, ## arg); \ sdp[sizeof(sdp) - 1] = '\0'; \ } /* generate SDP from session structure */ char *osmo_cc_session_gensdp(osmo_cc_session_t *session) { /* calc max size of SDP: quick an dirty (close to max UDP payload size) */ static char sdp[65000]; const char *username, *sess_id, *sess_version, *nettype, *addrtype, *unicast_address; const char *session_name; int individual_connection_data = 1; /* in case there is no media, there is no connection data */ int individual_send_receive = 1; /* in case there is no media, there is no send/receive attribute */ struct osmo_cc_session_media *media; struct osmo_cc_session_codec *codec; sdp[0] = 0; /* Version */ strncat_printf(sdp, "v=0\r\n"); /* Origin */ username = session->origin_local.username; sess_id = session->origin_local.sess_id; sess_version = session->origin_local.sess_version; nettype = session->origin_local.nettype; addrtype = session->origin_local.addrtype; unicast_address = session->origin_local.unicast_address; strncat_printf(sdp, "o=%s %s %s %s %s %s\r\n", username, sess_id, sess_version, nettype, addrtype, unicast_address); /* Session */ session_name = session->name; strncat_printf(sdp, "s=%s\r\n", session_name); /* Connection Data (if all media have the same data) */ if (session->media_list) { osmo_cc_session_for_each_media(session->media_list->next, media) { if (session->media_list->connection_data_local.nettype != media->connection_data_local.nettype) break; if (session->media_list->connection_data_local.addrtype != media->connection_data_local.addrtype) break; if (!!strcmp(session->media_list->connection_data_local.address, media->connection_data_local.address)) break; } if (!media) individual_connection_data = 0; } if (!individual_connection_data) strncat_printf(sdp, "c=%s %s %s\r\n", osmo_cc_session_nettype2string(session->media_list->connection_data_local.nettype), osmo_cc_session_addrtype2string(session->media_list->connection_data_local.addrtype), session->media_list->connection_data_local.address); /* timestamp */ strncat_printf(sdp, "t=0 0\r\n"); /* sendonly /recvonly (if all media have the same data) */ if (session->media_list) { osmo_cc_session_for_each_media(session->media_list->next, media) { if (session->media_list->send != media->send) break; if (session->media_list->receive != media->receive) break; } if (!media) individual_send_receive = 0; } if (!individual_send_receive) { if (session->media_list->send && !session->media_list->receive) strncat_printf(sdp, "a=sendonly\r\n"); if (!session->media_list->send && session->media_list->receive) strncat_printf(sdp, "a=recvonly\r\n"); if (!session->media_list->send && !session->media_list->receive) strncat_printf(sdp, "a=inactive\r\n"); } /* media */ osmo_cc_session_for_each_media(session->media_list, media) { strncat_printf(sdp, "m=%s %u %s", osmo_cc_session_media_type2string(media->description.type) ? : media->description.type_name, media->description.port_local, osmo_cc_session_media_proto2string(media->description.proto) ? : media->description.proto_name); osmo_cc_session_for_each_codec(media->codec_list, codec) strncat_printf(sdp, " %u", codec->payload_type_local); strncat_printf(sdp, "\r\n"); /* don't list rtpmap when session was canceled by setting port to 0 */ if (media->description.port_local == 0) continue; if (individual_connection_data) strncat_printf(sdp, "c=%s %s %s\r\n", osmo_cc_session_nettype2string(media->connection_data_local.nettype), osmo_cc_session_addrtype2string(media->connection_data_local.addrtype), media->connection_data_local.address); osmo_cc_session_for_each_codec(media->codec_list, codec) { strncat_printf(sdp, "a=rtpmap:%u %s/%d", codec->payload_type_local, codec->payload_name, codec->payload_rate); if (codec->payload_channels >= 2) strncat_printf(sdp, "/%d", codec->payload_channels); strncat_printf(sdp, "\r\n"); } if (individual_send_receive) { if (media->send && !media->receive) strncat_printf(sdp, "a=sendonly\r\n"); if (!media->send && media->receive) strncat_printf(sdp, "a=recvonly\r\n"); if (!media->send && !media->receive) strncat_printf(sdp, "a=inactive\r\n"); } } /* check for overflow and return */ if (strlen(sdp) == sizeof(sdp) - 1) { PDEBUG(DCC, DEBUG_ERROR, "Fatal error: Allocated SDP buffer with %d bytes is too small, please fix!\n", (int)sizeof(sdp)); return NULL; } return sdp; } /* separate a word from string that is delimited with one or more space characters */ static char *wordsep(char **text_p) { char *text = *text_p; static char word[256]; int i; /* no text */ if (text == NULL || *text == '\0') return NULL; /* skip spaces before text */ while (*text && *text <= ' ') text++; /* copy content */ i = 0; while (*text > ' ' && i < (int)sizeof(word)) word[i++] = *text++; word[i] = '\0'; /* set next */ *text_p = text; return word; } /* * codecs and their default values * * if format is -1, payload type is dynamic * if rate is 0, rate may be any rate */ struct codec_defaults { int fmt; char *name; uint32_t rate; int channels; } codec_defaults[] = { { 0, "PCMU", 8000, 1 }, { 3, "GSM", 8000, 1 }, { 4, "G723", 8000, 1 }, { 5, "DVI4", 8000, 1 }, { 6, "DVI4", 16000, 1 }, { 7, "LPC", 8000, 1 }, { 8, "PCMA", 8000, 1 }, { 9, "G722", 8000, 1 }, { 10, "L16", 44100, 2 }, { 11, "L16", 44100, 1 }, { 12, "QCELP", 8000, 1 }, { 13, "CN", 8000, 1 }, { 14, "MPA", 90000, 1 }, { 15, "G728", 8000, 1 }, { 16, "DVI4", 11025, 1 }, { 17, "DVI4", 22050, 1 }, { 18, "G729", 8000, 1 }, { 25, "CELB", 90000, 0 }, { 26, "JPEG", 90000, 0 }, { 28, "nv", 90000, 0 }, { 31, "H261", 90000, 0 }, { 32, "MPV", 90000, 0 }, { 33, "MP2T", 90000, 0 }, { 34, "H263", 90000, 0 }, { -1, NULL, 0, 0 }, }; static void complete_codec_by_fmt(uint8_t fmt, const char **name, uint32_t *rate, int *channels) { int i; for (i = 0; codec_defaults[i].name; i++) { if (codec_defaults[i].fmt == fmt) break; } if (!codec_defaults[i].name) return; free((char *)*name); *name = strdup(codec_defaults[i].name); *rate = codec_defaults[i].rate; *channels = codec_defaults[i].channels; } int osmo_cc_payload_type_by_attrs(uint8_t *fmt, const char *name, uint32_t *rate, int *channels) { int i; for (i = 0; codec_defaults[i].name; i++) { if (!strcmp(codec_defaults[i].name, name) && (*rate == 0 || codec_defaults[i].rate == *rate) && (*channels == 0 || codec_defaults[i].channels == *channels)) break; } if (!codec_defaults[i].name) return -EINVAL; *fmt = codec_defaults[i].fmt; *rate = codec_defaults[i].rate; *channels = codec_defaults[i].channels; return 0; } /* parses data and codec list from SDP * * sdp = given SDP text * return: SDP session description structure */ struct osmo_cc_session *osmo_cc_session_parsesdp(osmo_cc_session_config_t *conf, void *priv, const char *_sdp) { char buffer[strlen(_sdp) + 1], *sdp = buffer; char *line, *p, *word, *next_word; int line_no = 0; struct osmo_cc_session_connection_data ccd, *cd; int csend = 1, creceive = 1; /* common default */ struct osmo_cc_session *session = NULL; struct osmo_cc_session_media *media = NULL; struct osmo_cc_session_codec *codec = NULL; /* prepare data */ strcpy(sdp, _sdp); memset(&ccd, 0, sizeof(ccd)); /* create SDP session description */ session = osmo_cc_new_session(conf, priv, NULL, NULL, NULL, osmo_cc_session_nettype_inet, osmo_cc_session_addrtype_ipv4, "127.0.0.1", NULL, 0); // values will be replaced by local definitions during negotiation /* check every line of SDP and parse its data */ while(*sdp) { if ((p = strchr(sdp, '\r'))) { *p++ = '\0'; if (*p == '\n') p++; line = sdp; sdp = p; } else if ((p = strchr(sdp, '\n'))) { *p++ = '\0'; line = sdp; sdp = p; } else { line = sdp; sdp = strchr(sdp, '\0'); } next_word = line + 2; line_no++; if (line[0] == '\0') continue; if (line[1] != '=') { PDEBUG(DCC, DEBUG_NOTICE, "SDP line %d = '%s' is garbage, expecting '=' as second character.\n", line_no, line); continue; } switch(line[0]) { case 'v': PDEBUG(DCC, DEBUG_DEBUG, " -> Version: %s\n", next_word); if (atoi(next_word) != 0) { PDEBUG(DCC, DEBUG_NOTICE, "SDP line %d = '%s' describes unsupported version.\n", line_no, line); osmo_cc_free_session(session); return NULL; } break; case 'o': PDEBUG(DCC, DEBUG_DEBUG, " -> Originator: %s\n", next_word); /* Originator */ word = wordsep(&next_word); if (!word) break; free((char *)session->origin_remote.username); // if already set session->origin_remote.username = strdup(word); word = wordsep(&next_word); if (!word) break; free((char *)session->origin_remote.sess_id); // if already set session->origin_remote.sess_id = strdup(word); word = wordsep(&next_word); if (!word) break; free((char *)session->origin_remote.sess_version); // if already set session->origin_remote.sess_version = strdup(word); word = wordsep(&next_word); if (!word) break; free((char *)session->origin_remote.nettype); // if already set session->origin_remote.nettype = strdup(word); word = wordsep(&next_word); if (!word) break; free((char *)session->origin_remote.addrtype); // if already set session->origin_remote.addrtype = strdup(word); word = wordsep(&next_word); if (!word) break; free((char *)session->origin_remote.unicast_address); // if already set session->origin_remote.unicast_address = strdup(word); break; case 's': /* Session Name */ PDEBUG(DCC, DEBUG_DEBUG, " -> Session Name: %s\n", next_word); free((char *)session->name); // if already set session->name = strdup(next_word); break; case 'c': /* Connection Data */ PDEBUG(DCC, DEBUG_DEBUG, " -> Connection Data: %s\n", next_word); if (media) cd = &media->connection_data_remote; else cd = &ccd; /* network type */ if (!(word = wordsep(&next_word))) break; if (!strcmp(word, "IN")) cd->nettype = osmo_cc_session_nettype_inet; else { PDEBUG(DCC, DEBUG_NOTICE, "Unsupported network type '%s' in SDP line %d = '%s'\n", word, line_no, line); break; } /* address type */ if (!(word = wordsep(&next_word))) break; if (!strcmp(word, "IP4")) { cd->addrtype = osmo_cc_session_addrtype_ipv4; PDEBUG(DCC, DEBUG_DEBUG, " -> Address Type = IPv4\n"); } else if (!strcmp(word, "IP6")) { cd->addrtype = osmo_cc_session_addrtype_ipv6; PDEBUG(DCC, DEBUG_DEBUG, " -> Address Type = IPv6\n"); } else { PDEBUG(DCC, DEBUG_NOTICE, "Unsupported address type '%s' in SDP line %d = '%s'\n", word, line_no, line); break; } /* connection address */ if (!(word = wordsep(&next_word))) break; if ((p = strchr(word, '/'))) *p++ = '\0'; free((char *)cd->address); // in case of multiple lines of 'c' cd->address = strdup(word); PDEBUG(DCC, DEBUG_DEBUG, " -> Address = %s\n", word); break; case 'm': /* Media Description */ PDEBUG(DCC, DEBUG_DEBUG, " -> Media Description: %s\n", next_word); /* add media description */ media = osmo_cc_add_media(session, 0, 0, NULL, 0, 0, 0, csend, creceive, NULL, 0); /* copy common connection data from common connection, if exists */ cd = &media->connection_data_remote; memcpy(cd, &ccd, sizeof(*cd)); /* media type */ if (!(word = wordsep(&next_word))) break; if (!strcmp(word, "audio")) media->description.type = osmo_cc_session_media_type_audio; else if (!strcmp(word, "video")) media->description.type = osmo_cc_session_media_type_video; else { media->description.type = osmo_cc_session_media_type_unknown; media->description.type_name = strdup(word); PDEBUG(DCC, DEBUG_DEBUG, "Unsupported media type in SDP line %d = '%s'\n", line_no, line); } /* port */ if (!(word = wordsep(&next_word))) break; media->description.port_remote = atoi(word); /* proto */ if (!(word = wordsep(&next_word))) break; if (!strcmp(word, "RTP/AVP")) media->description.proto = osmo_cc_session_media_proto_rtp; else { media->description.proto = osmo_cc_session_media_proto_unknown; media->description.proto_name = strdup(word); PDEBUG(DCC, DEBUG_NOTICE, "Unsupported protocol type in SDP line %d = '%s'\n", line_no, line); break; } /* create codec description for each codec and link */ while ((word = wordsep(&next_word))) { /* create codec */ codec = osmo_cc_add_codec(media, NULL, 0, 1, NULL, NULL, 0); /* fmt */ codec->payload_type_remote = atoi(word); complete_codec_by_fmt(codec->payload_type_remote, &codec->payload_name, &codec->payload_rate, &codec->payload_channels); PDEBUG(DCC, DEBUG_DEBUG, " -> payload type = %d\n", codec->payload_type_remote); if (codec->payload_name) PDEBUG(DCC, DEBUG_DEBUG, " -> payload name = %s\n", codec->payload_name); if (codec->payload_rate) PDEBUG(DCC, DEBUG_DEBUG, " -> payload rate = %d\n", codec->payload_rate); if (codec->payload_channels) PDEBUG(DCC, DEBUG_DEBUG, " -> payload channels = %d\n", codec->payload_channels); } break; case 'a': PDEBUG(DCC, DEBUG_DEBUG, " -> Attribute: %s\n", next_word); word = wordsep(&next_word); if (!strcmp(word, "sendrecv")) { if (media) { media->receive = 1; media->send = 1; } else { creceive = 1; csend = 1; } break; } else if (!strcmp(word, "recvonly")) { if (media) { media->receive = 1; media->send = 0; } else { creceive = 1; csend = 0; } break; } else if (!strcmp(word, "sendonly")) { if (media) { media->receive = 0; media->send = 1; } else { creceive = 0; csend = 1; } break; } else if (!strcmp(word, "inactive")) { if (media) { media->receive = 0; media->send = 0; } else { creceive = 0; csend = 0; } break; } else if (!media) { PDEBUG(DCC, DEBUG_NOTICE, "Attribute without previously defined media in SDP line %d = '%s'\n", line_no, line); break; } if (!strncmp(word, "rtpmap:", 7)) { int fmt = atoi(word + 7); osmo_cc_session_for_each_codec(media->codec_list, codec) { if (codec->payload_type_remote == fmt) break; } if (!codec) { PDEBUG(DCC, DEBUG_NOTICE, "Attribute without previously defined codec in SDP line %d = '%s'\n", line_no, line); break; } PDEBUG(DCC, DEBUG_DEBUG, " -> (rtpmap) payload type = %d\n", codec->payload_type_remote); if (!(word = wordsep(&next_word))) goto rtpmap_done; if ((p = strchr(word, '/'))) *p++ = '\0'; free((char *)codec->payload_name); // in case it is already set above codec->payload_name = strdup(word); PDEBUG(DCC, DEBUG_DEBUG, " -> (rtpmap) payload name = %s\n", codec->payload_name); if (!(word = p)) goto rtpmap_done; if ((p = strchr(word, '/'))) *p++ = '\0'; codec->payload_rate = atoi(word); PDEBUG(DCC, DEBUG_DEBUG, " -> (rtpmap) payload rate = %d\n", codec->payload_rate); if (!(word = p)) { /* if no channel is given and no default was specified, we must set 1 channel */ if (!codec->payload_channels) codec->payload_channels = 1; goto rtpmap_done; } codec->payload_channels = atoi(word); PDEBUG(DCC, DEBUG_DEBUG, " -> (rtpmap) payload channels = %d\n", codec->payload_channels); rtpmap_done: if (!codec->payload_name || !codec->payload_rate || !codec->payload_channels) { PDEBUG(DCC, DEBUG_NOTICE, "Broken 'rtpmap' definition in SDP line %d = '%s' Skipping codec!\n", line_no, line); osmo_cc_free_codec(codec); } } break; } } /* if something is incomplete, abort here */ if (osmo_cc_session_check(session, 1)) { PDEBUG(DCC, DEBUG_NOTICE, "Parsing SDP failed.\n"); osmo_cc_free_session(session); return NULL; } return session; } void osmo_cc_debug_sdp(const char *_sdp) { const unsigned char *sdp = (const unsigned char *)_sdp; char text[256]; int i; while (*sdp) { for (i = 0; *sdp > 0 && *sdp >= 32 && i < (int)sizeof(text) - 1; i++) text[i] = *sdp++; text[i] = '\0'; PDEBUG(DCC, DEBUG_DEBUG, " | %s\n", text); while (*sdp > 0 && *sdp < 32) sdp++; } }