mistserver/lib/sdp_media.cpp
2021-10-19 22:29:40 +02:00

1158 lines
37 KiB
C++

#include "defines.h"
#include "sdp_media.h"
#include <algorithm>
#include <cstdarg>
namespace SDP{
std::string codecRTP2Mist(const std::string &codec){
if (codec == "H265"){
return "HEVC";
}else if (codec == "H264"){
return "H264";
}else if (codec == "VP8"){
return "VP8";
}else if (codec == "AC3"){
return "AC3";
}else if (codec == "PCMA"){
return "ALAW";
}else if (codec == "PCMU"){
return "ULAW";
}else if (codec == "L8"){
return "PCM";
}else if (codec == "L16"){
return "PCM";
}else if (codec == "L20"){
return "PCM";
}else if (codec == "L24"){
return "PCM";
}else if (codec == "MPA"){
// can also be MP2, the data should be inspected.
return "MP3";
}else if (codec == "MPEG4-GENERIC"){
return "AAC";
}else if (codec == "OPUS"){
return "opus";
}else if (codec == "ULPFEC"){
return "";
}else if (codec == "RED"){
return "";
}
ERROR_MSG("%s support not implemented", codec.c_str());
return "";
}
std::string codecMist2RTP(const std::string &codec){
if (codec == "HEVC"){
return "H265";
}else if (codec == "H264"){
return "H264";
}else if (codec == "VP8"){
return "VP8";
}else if (codec == "AC3"){
return "AC3";
}else if (codec == "ALAW"){
return "PCMA";
}else if (codec == "ULAW"){
return "PCMU";
}else if (codec == "PCM"){
return "L24";
}else if (codec == "MP2"){
return "MPA";
}else if (codec == "MP3"){
return "MPA";
}else if (codec == "AAC"){
return "MPEG4-GENERIC";
}else if (codec == "opus"){
return "OPUS";
}else if (codec == "ULPFEC"){
return "";
}else if (codec == "RED"){
return "";
}
ERROR_MSG("%s support not implemented", codec.c_str());
BACKTRACE;
return "";
}
static std::vector<std::string>
sdp_split(const std::string &str, const std::string &delim,
bool keepEmpty); // Split a string on the given delimeter and return a vector with the parts.
static bool sdp_extract_payload_type(const std::string &str,
uint64_t &result); // Extract the payload number from a SDP line that
// starts like: `a=something:[payloadtype]`.
static bool sdp_get_name_value_from_varval(const std::string &str, std::string &var,
std::string &value); // Extracts the `name` and `value` from a string like `<name>=<value>`.
// The `name` will always be converted to lowercase!.
static void
sdp_get_all_name_values_from_string(const std::string &str, std::map<std::string, std::string> &result); // Extracts all the name/value pairs from a string like:
// `<name>=<value>;<name>=<value>`. The `name` part will
// always be converted to lowercase.
static bool sdp_get_attribute_value(const std::string &str,
std::string &result); // Extract an "attribute" value, which is formatted
// like: `a=something:<attribute-value>`
static std::string string_to_upper(const std::string &str);
MediaFormat::MediaFormat(){
payloadType = SDP_PAYLOAD_TYPE_NONE;
associatedPayloadType = SDP_PAYLOAD_TYPE_NONE;
audioSampleRate = 0;
audioNumChannels = 0;
audioBitSize = 0;
videoRate = 0;
}
/// \TODO what criteria do you (Jaron) want to use?
MediaFormat::operator bool() const{
if (payloadType == SDP_PAYLOAD_TYPE_NONE){return false;}
if (encodingName.empty()){return false;}
return true;
}
uint32_t MediaFormat::getAudioSampleRate() const{
if (0 != audioSampleRate){return audioSampleRate;}
if (payloadType != SDP_PAYLOAD_TYPE_NONE){
switch (payloadType){
case 0:{
return 8000;
}
case 8:{
return 8000;
}
case 10:{
return 44100;
}
case 11:{
return 44100;
}
}
}
return 0;
}
uint32_t MediaFormat::getAudioNumChannels() const{
if (0 != audioNumChannels){return audioNumChannels;}
if (payloadType != SDP_PAYLOAD_TYPE_NONE){
switch (payloadType){
case 0:{
return 1;
}
case 8:{
return 1;
}
case 10:{
return 2;
}
case 11:{
return 1;
}
}
}
return 0;
}
uint32_t MediaFormat::getAudioBitSize() const{
if (0 != audioBitSize){return audioBitSize;}
if (payloadType != SDP_PAYLOAD_TYPE_NONE){
switch (payloadType){
case 10:{
return 16;
}
case 11:{
return 16;
}
}
}
if (encodingName == "L8"){return 8;}
if (encodingName == "L16"){return 16;}
if (encodingName == "L20"){return 20;}
if (encodingName == "L24"){return 24;}
return 0;
}
uint32_t MediaFormat::getVideoRate() const{
if (0 != videoRate){return videoRate;}
if (encodingName == "H264"){
return 90000;
}else if (encodingName == "H265"){
return 90000;
}else if (encodingName == "VP8"){
return 90000;
}else if (encodingName == "vp9"){
return 90000;
}
return 0;
}
/// \todo Maybe we should create one member `rate` which is used by audio and video (?)
uint32_t MediaFormat::getVideoOrAudioRate() const{
uint32_t r = getAudioSampleRate();
if (0 == r){r = getVideoRate();}
return r;
}
std::string MediaFormat::getFormatParameterForName(const std::string &name) const{
std::string name_lower = name;
std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(), ::tolower);
std::map<std::string, std::string>::const_iterator it = formatParameters.find(name_lower);
if (it == formatParameters.end()){return "";}
return it->second;
}
uint64_t MediaFormat::getPayloadType() const{return payloadType;}
int32_t MediaFormat::getPacketizationModeForH264(){
if (encodingName != "H264"){
ERROR_MSG("Encoding is not H264.");
return -1;
}
std::string val = getFormatParameterForName("packetization-mode");
if (val.empty()){
WARN_MSG(
"No packetization-mode found for this format. We default to packetization-mode = 0.");
return 0;
}
std::stringstream ss;
ss << val;
int32_t pm = 0;
ss >> pm;
return pm;
}
std::string MediaFormat::getProfileLevelIdForH264(){
if (encodingName != "H264"){
ERROR_MSG("Encoding is not H264, cannot get profile-level-id.");
return "";
}
return getFormatParameterForName("profile-level-id");
}
Media::Media(){
framerate = 0.0;
supportsRTCPMux = false;
supportsRTCPReducedSize = false;
candidatePort = 0;
SSRC = 0;
}
/// \TODO what other checks do you want to perform?
Media::operator bool() const{
if (formats.size() == 0){return false;}
if (type.empty()){return false;}
return true;
}
/// Parses a SDP media line like `m=video 9 UDP/TLS/RTP/SAVPF
/// 96 97 98 99 100 101 102` For each payloadtype it will
/// create a MediaFormat entry and initializes it with some
/// default settings.
bool Media::parseMediaLine(const std::string &line){
// split and verify
std::vector<std::string> words = sdp_split(line, " ", false);
if (!words.size()){
ERROR_MSG("Invalid media line.");
return false;
}
// check if we're dealing with audio or video.
if (words[0] == "m=audio"){
type = "audio";
}else if (words[0] == "m=video"){
type = "video";
}else{
ERROR_MSG("Unhandled media type: `%s`.", words[0].c_str());
return false;
}
// proto: UDP/TLS/RTP/SAVP
proto = words[2];
// create static and dynamic tracks.
for (size_t i = 3; i < words.size(); ++i){
SDP::MediaFormat format;
format.payloadType = JSON::Value(words[i]).asInt();
formats[format.payloadType] = format;
if (!payloadTypes.empty()){payloadTypes += " ";}
payloadTypes += words[i];
}
return true;
}
bool Media::parseRtpMapLine(const std::string &line){
MediaFormat *format = getFormatForSdpLine(line);
if (NULL == format){
ERROR_MSG(
"Cannot parse the a=rtpmap line because we did not find the track for the payload type.");
return false;
}
// convert <encoding-name> to fullcaps
std::string mediaType = line.substr(line.find(' ', 8) + 1);
std::string encodingName = mediaType.substr(0, mediaType.find('/'));
for (unsigned int i = 0; i < encodingName.size(); ++i){
if (encodingName[i] <= 122 && encodingName[i] >= 97){encodingName[i] -= 32;}
}
format->encodingName = encodingName;
format->rtpmap = line.substr(line.find("=") + 1);
// extract audio info
if (type == "audio"){
std::string extraInfo = mediaType.substr(mediaType.find('/') + 1);
size_t lastSlash = extraInfo.find('/');
if (lastSlash != std::string::npos){
format->audioSampleRate = atoll(extraInfo.substr(0, lastSlash).c_str());
format->audioNumChannels = atoll(extraInfo.substr(lastSlash + 1).c_str());
}else{
format->audioSampleRate = atoll(extraInfo.c_str());
format->audioNumChannels = 1;
}
}
return true;
}
bool Media::parseRtspControlLine(const std::string &line){
if (line.substr(0, 10) != "a=control:"){
ERROR_MSG(
"Cannot parse the given rtsp control url line because it's incorrectly formatted: `%s`.",
line.c_str());
return false;
}
control = line.substr(10);
if (control.empty()){
ERROR_MSG("Failed to parse the rtsp control line.");
return false;
}
return true;
}
bool Media::parseFrameRateLine(const std::string &line){
if (line.substr(0, 12) != "a=framerate:"){
ERROR_MSG("Cannot parse the `a=framerate:` line because it's incorrectly formatted: `%s`.", line.c_str());
return false;
}
framerate = atof(line.c_str() + 12) * 1000;
return true;
}
/// Parses a line like:
/// `a=fmtp:97
/// streamtype=5;profile-level-id=2;mode=AAC-hbr;config=1408;sizelength=13;indexlength=3;indexdeltalength=3;bitrate=32000`
/// `a=fmtp:96
/// packetization-mode=1;profile-level-id=4d0029;sprop-parameter-sets=Z00AKeKQCADDYC3AQEBpB4kRUA==,aO48gA==`
/// `a=fmtp:97 apt=96`
bool Media::parseFormatParametersLine(const std::string &line){
MediaFormat *format = getFormatForSdpLine(line);
if (!format){
ERROR_MSG("No format found for the given `a=fmtp:` line. The payload type (<fmt>) should be "
"part of the media line.");
return false;
}
// start parsing the parameters after the first <space> character.
size_t start = line.find(" ");
if (start == std::string::npos){
ERROR_MSG(
"Invalid formatted a=fmtp line. No space between format and data. `a=fmtp:<fmt> <data>`");
return false;
}
start = start + 1;
sdp_get_all_name_values_from_string(line.substr(start), format->formatParameters);
// When this format is associated with another format
// which is the case for RTX, we store the associated
// payload type too. `apt` means "Associated Payload Type".
if (format->formatParameters.count("apt") != 0){
std::stringstream ss(format->formatParameters["apt"]);
ss >> format->associatedPayloadType;
}
return true;
}
bool Media::parseRtcpFeedbackLine(const std::string &line){
// does this feedback mechanism apply to all or only a specific format?
MediaFormat *format = NULL;
size_t num_formats = 0;
if (line.substr(0, 11) == "a=rtcp-fb:*"){
num_formats = formats.size();
format = &formats[0];
}else{
num_formats = 1;
format = getFormatForSdpLine(line);
}
// make sure we found something valid.
if (!format){
ERROR_MSG("No format found for the given `a=rtcp-fb` line. The payload type (<fmt>) should "
"be part of the media line.");
return false;
}
if (num_formats == 0){
ERROR_MSG("num_formats is 0. Seems like no format has been added. Invalid media line in SDP "
"maybe?");
return false;
}
std::string fb = line.substr(11);
if (fb.empty()){
ERROR_MSG("The given `a=rtcp-fb` line doesn't contain a rtcp-fb-val.");
return false;
}
// add the feedback mechanism to the found format(s)
for (size_t i = 0; i < num_formats; ++i){format[i].rtcpFormats.insert(fb);}
return true;
}
/// Extracts the fingerpint hash and value, from a line like:
/// a=fingerprint:sha-256
/// C7:98:6F:A9:55:75:C0:73:F2:EB:CF:14:B8:6E:58:FE:A5:F1:B0:C7:41:B7:BA:D3:4A:CF:7E:5C:69:8B:FA:F4
bool Media::parseFingerprintLine(const std::string &sdpLine){
// extract the <hash> type.
size_t start = sdpLine.find(":");
if (start == std::string::npos){
ERROR_MSG("Invalid `a=fingerprint:<hash> <value>` line, no `:` found.");
return false;
}
size_t end = sdpLine.find(" ", start);
if (end == std::string::npos){
ERROR_MSG("Invalid `a=fingerprint:<hash> <value>` line, no <space> found after `:`.");
return false;
}
if (end <= start){
ERROR_MSG("Invalid `a=fingerpint:<hash> <value>` line. Space before the `:` found.");
return false;
}
fingerprintHash = sdpLine.substr(start, end - start);
fingerprintValue = sdpLine.substr(end);
return true;
}
bool Media::parseSSRCLine(const std::string &sdpLine){
if (0 != SSRC){
// We set our SSRC to the first one that we found.
return true;
}
size_t firstSpace = sdpLine.find(" ");
if (firstSpace == std::string::npos){
ERROR_MSG("Failed to parse the `a=ssrc:<ssrc>` line.");
return false;
}
if (firstSpace < 7){
ERROR_MSG("We found an invalid space position. Cannot get SSRC.");
return false;
}
std::string ssrcStr = sdpLine.substr(7, firstSpace - 7);
std::stringstream ss;
ss << ssrcStr;
ss >> SSRC;
return true;
}
MediaFormat *Media::getFormatForSdpLine(const std::string &sdpLine){
uint64_t payloadType = 0;
if (!sdp_extract_payload_type(sdpLine, payloadType)){
ERROR_MSG("Cannot get track for the given SDP line: %s", sdpLine.c_str());
return NULL;
}
return getFormatForPayloadType(payloadType);
}
MediaFormat *Media::getFormatForPayloadType(uint64_t &payloadType){
std::map<uint64_t, MediaFormat>::iterator it = formats.find(payloadType);
if (it == formats.end()){
ERROR_MSG("No format found for payload type: %u.", payloadType);
return NULL;
}
return &it->second;
}
// This will check if there is a `SDP::MediaFormat` with a
// codec that matches the given codec name. Note that we will
// convert the given `encName` into fullcaps as SDP stores all
// codecs in caps.
MediaFormat *Media::getFormatForEncodingName(const std::string &encName){
std::string encNameCaps = codecMist2RTP(encName);
std::map<uint64_t, MediaFormat>::iterator it = formats.begin();
while (it != formats.end()){
MediaFormat &mf = it->second;
if (mf.encodingName == encNameCaps){return &mf;}
++it;
}
return NULL;
}
std::vector<SDP::MediaFormat *> Media::getFormatsForEncodingName(const std::string &encName){
std::string encNameCaps = string_to_upper(encName);
std::vector<MediaFormat *> result;
std::map<uint64_t, MediaFormat>::iterator it = formats.begin();
while (it != formats.end()){
MediaFormat &mf = it->second;
if (mf.encodingName == encNameCaps){result.push_back(&mf);}
++it;
}
return result;
}
MediaFormat *Media::getRetransMissionFormatForPayloadType(uint64_t pt){
std::vector<SDP::MediaFormat *> rtxFormats = getFormatsForEncodingName("RTX");
if (rtxFormats.size() == 0){return NULL;}
for (size_t i = 0; i < rtxFormats.size(); ++i){
if (rtxFormats[i]->associatedPayloadType == pt){return rtxFormats[i];}
}
return NULL;
}
std::string Media::getIcePwdForFormat(const MediaFormat &fmt){
if (!fmt.icePwd.empty()){return fmt.icePwd;}
return icePwd;
}
uint32_t Media::getSSRC() const{return SSRC;}
// Get the `SDP::Media*` for a given type, e.g. audio or video.
Media *Session::getMediaForType(const std::string &type){
size_t n = medias.size();
for (size_t i = 0; i < n; ++i){
if (medias[i].type == type){return &medias[i];}
}
return NULL;
}
/// Get the `SDP::MediaFormat` which represents the format and
/// e.g. encoding, rtp attributes for a specific codec (H264, OPUS, etc.)
///
/// @param mediaType `video` or `audio`
/// @param encodingName Encoding name in fullcaps, e.g. `H264`, `OPUS`, etc.
MediaFormat *Session::getMediaFormatByEncodingName(const std::string &mediaType, const std::string &encodingName){
SDP::Media *media = getMediaForType(mediaType);
if (!media){
WARN_MSG("No SDP::Media found for media type %s.", mediaType.c_str());
return NULL;
}
SDP::MediaFormat *mediaFormat = media->getFormatForEncodingName(encodingName);
if (!mediaFormat){
WARN_MSG("No SDP::MediaFormat found for encoding name %s.", encodingName.c_str());
return NULL;
}
return mediaFormat;
}
bool Session::hasReceiveOnlyMedia(){
size_t numMedias = medias.size();
for (size_t i = 0; i < numMedias; ++i){
if (medias[i].direction == "recvonly"){return true;}
}
return false;
}
bool Session::parseSDP(const std::string &sdp){
if (sdp.empty()){
FAIL_MSG("Requested to parse an empty SDP.");
return false;
}
SDP::Media *currMedia = NULL;
std::stringstream ss(sdp);
std::string line;
while (std::getline(ss, line, '\n')){
// validate line
if (!line.empty() && *line.rbegin() == '\r'){line.erase(line.size() - 1, 1);}
if (line.empty()){
continue;
}
// Parse session (or) media data.
else if (line.substr(0, 2) == "m="){
SDP::Media media;
if (!media.parseMediaLine(line)){
ERROR_MSG("Failed to parse the m= line.");
return false;
}
medias.push_back(media);
currMedia = &medias.back();
// set properties which can be global and may be overwritten per stream
currMedia->iceUFrag = iceUFrag;
currMedia->icePwd = icePwd;
}
// the lines below assume that we found a media line already.
if (!currMedia){continue;}
// parse properties we need later.
if (line.substr(0, 8) == "a=rtpmap"){
currMedia->parseRtpMapLine(line);
}else if (line.substr(0, 10) == "a=control:"){
currMedia->parseRtspControlLine(line);
}else if (line.substr(0, 12) == "a=framerate:"){
currMedia->parseFrameRateLine(line);
}else if (line.substr(0, 7) == "a=fmtp:"){
currMedia->parseFormatParametersLine(line);
}else if (line.substr(0, 11) == "a=rtcp-fb:"){
currMedia->parseRtcpFeedbackLine(line);
}else if (line.substr(0, 10) == "a=sendonly"){
currMedia->direction = "sendonly";
}else if (line.substr(0, 10) == "a=sendrecv"){
currMedia->direction = "sendrecv";
}else if (line.substr(0, 10) == "a=recvonly"){
currMedia->direction = "recvonly";
}else if (line.substr(0, 11) == "a=ice-ufrag"){
sdp_get_attribute_value(line, currMedia->iceUFrag);
}else if (line.substr(0, 9) == "a=ice-pwd"){
sdp_get_attribute_value(line, currMedia->icePwd);
}else if (line.substr(0, 10) == "a=rtcp-mux"){
currMedia->supportsRTCPMux = true;
}else if (line.substr(0, 10) == "a=rtcp-rsize"){
currMedia->supportsRTCPReducedSize = true;
}else if (line.substr(0, 7) == "a=setup"){
sdp_get_attribute_value(line, currMedia->setupMethod);
}else if (line.substr(0, 13) == "a=fingerprint"){
currMedia->parseFingerprintLine(line);
}else if (line.substr(0, 6) == "a=mid:"){
sdp_get_attribute_value(line, currMedia->mediaID);
}else if (line.substr(0, 7) == "a=ssrc:"){
currMedia->parseSSRCLine(line);
}
}// while
return true;
}
static std::vector<std::string> sdp_split(const std::string &str, const std::string &delim, bool keepEmpty){
std::vector<std::string> strings;
std::ostringstream word;
for (size_t n = 0; n < str.size(); ++n){
if (std::string::npos == delim.find(str[n])){
word << str[n];
}else{
if (false == word.str().empty() || true == keepEmpty){strings.push_back(word.str());}
word.str("");
}
}
if (false == word.str().empty() || true == keepEmpty){strings.push_back(word.str());}
return strings;
}
static bool sdp_extract_payload_type(const std::string &str, uint64_t &result){
// extract payload type.
size_t start_pos = str.find_first_of(':');
size_t end_pos = str.find_first_of(' ', start_pos);
if (start_pos == std::string::npos || end_pos == std::string::npos || (start_pos + 1) >= end_pos){
FAIL_MSG("Invalid `a=rtpmap` line. Has not payload type.");
return false;
}
// make sure payload type was part of the media line and is supported.
result = JSON::Value(str.substr(start_pos + 1, end_pos - (start_pos + 1))).asInt();
return true;
}
// Extract the `name` and `value` from a string like
// `<name>=<value>`. This function will return on success,
// when it extract the `name` and `value` and returns false
// when the given input string doesn't contains a
// `<name>=<value>` pair. This function is for example used
// when parsing the `a=fmtp:<fmt>` line.
//
// Note that we will always convert the `var` to lowercase.
static bool sdp_get_name_value_from_varval(const std::string &str, std::string &var, std::string &value){
if (str.empty()){
ERROR_MSG("Cannot get `name` and `value` from string because the given string is empty. "
"String is: `%s`",
str.c_str());
return false;
}
size_t pos = str.find("=");
if (pos == std::string::npos){
WARN_MSG("Cannot get `name` and `value` from string becuase it doesn't contain a `=` sign. "
"String is: `%s`. Returning the string as is.",
str.c_str());
value = str;
return true;
}
var = str.substr(0, pos);
value = str.substr(pos + 1, str.length() - pos);
std::transform(var.begin(), var.end(), var.begin(), ::tolower);
return true;
}
// This function will extract name=value pairs from a string
// which are separated by a ";" delmiter. In the future we
// might want to pass this delimiter as an parameter.
// Currently this is used to parse a `a=fmtp` line.
static void sdp_get_all_name_values_from_string(const std::string &str,
std::map<std::string, std::string> &result){
std::string varval;
std::string name;
std::string value;
size_t start = 0;
size_t end = str.find(";");
while (end != std::string::npos){
varval = str.substr(start, end - start);
if (sdp_get_name_value_from_varval(varval, name, value)){result[name] = value;}
start = end + 1;
end = str.find(";", start);
}
// the last element needs to read separately
varval = str.substr(start, end);
if (sdp_get_name_value_from_varval(varval, name, value)){result[name] = value;}
}
// Extract an "attribute" value, which is formatted like:
// `a=something:<attribute-value>`
static bool sdp_get_attribute_value(const std::string &str, std::string &result){
if (str.empty()){
ERROR_MSG("Cannot get attribute value because the given string is empty.");
return false;
}
size_t start = str.find(":");
if (start == std::string::npos){
ERROR_MSG("Cannot get attribute value because we did not find the : character in %s.", str.c_str());
return false;
}
result = str.substr(start + 1, result.length() - start);
return true;
}
Answer::Answer()
: isVideoEnabled(false), isAudioEnabled(false), candidatePort(0),
videoLossPrevention(SDP_LOSS_PREVENTION_NONE){}
bool Answer::parseOffer(const std::string &sdp){
if (!sdpOffer.parseSDP(sdp)){
FAIL_MSG("Cannot parse given offer SDP.");
return false;
}
return true;
}
bool Answer::hasVideo(){
SDP::Media *m = sdpOffer.getMediaForType("video");
return (m != NULL) ? true : false;
}
bool Answer::hasAudio(){
SDP::Media *m = sdpOffer.getMediaForType("audio");
return (m != NULL) ? true : false;
}
bool Answer::enableVideo(const std::string &codecName){
if (!enableMedia("video", codecName, answerVideoMedia, answerVideoFormat)){
DONTEVEN_MSG("Failed to enable video.");
return false;
}
isVideoEnabled = true;
return true;
}
bool Answer::enableAudio(const std::string &codecName){
if (!enableMedia("audio", codecName, answerAudioMedia, answerAudioFormat)){
DONTEVEN_MSG("Not enabling audio.");
return false;
}
isAudioEnabled = true;
return true;
}
void Answer::setCandidate(const std::string &ip, uint16_t port){
if (ip.empty()){WARN_MSG("Given candidate IP is empty. It's fine if you want to unset it.");}
candidateIP = ip;
candidatePort = port;
}
void Answer::setFingerprint(const std::string &fingerprintSha){
if (fingerprintSha.empty()){
WARN_MSG(
"Given fingerprint is empty; fine when you want to unset it; otherwise check your code.");
}
fingerprint = fingerprintSha;
}
void Answer::setDirection(const std::string &dir){
if (dir.empty()){WARN_MSG("Given direction string is empty; fine if you want to unset.");}
direction = dir;
}
bool Answer::setupVideoDTSCTrack(DTSC::Track &result){
if (!isVideoEnabled){
FAIL_MSG("Video is disabled; cannot setup DTSC::Track.");
return false;
}
result.codec = codecRTP2Mist(answerVideoFormat.encodingName);
if (result.codec.empty()){
FAIL_MSG("Failed to convert the format codec into one that MistServer understands. %s.",
answerVideoFormat.encodingName.c_str());
return false;
}
result.type = "video";
result.rate = answerVideoFormat.getVideoRate();
result.trackID = answerVideoFormat.payloadType;
return true;
}
bool Answer::setupAudioDTSCTrack(DTSC::Track &result){
if (!isAudioEnabled){
FAIL_MSG("Audio is disabled; cannot setup DTSC::Track.");
return false;
}
result.codec = codecRTP2Mist(answerAudioFormat.encodingName);
if (result.codec.empty()){
FAIL_MSG("Failed to convert the format codec into one that MistServer understands. %s.",
answerAudioFormat.encodingName.c_str());
return false;
}
result.type = "audio";
result.rate = answerAudioFormat.getAudioSampleRate();
result.channels = answerAudioFormat.getAudioNumChannels();
result.size = answerAudioFormat.getAudioBitSize();
result.trackID = answerAudioFormat.payloadType;
return true;
}
std::string Answer::toString(){
if (direction.empty()){
FAIL_MSG("Cannot create SDP answer; direction not set. call setDirection().");
return "";
}
if (candidateIP.empty()){
FAIL_MSG("Cannot create SDP answer; candidate not set. call setCandidate().");
return "";
}
if (fingerprint.empty()){
FAIL_MSG("Cannot create SDP answer; fingerprint not set. call setFingerpint().");
return "";
}
std::string result;
output.clear();
// session
addLine("v=0");
addLine("o=- %s 0 IN IP4 0.0.0.0", generateSessionId().c_str());
addLine("s=-");
addLine("t=0 0");
addLine("a=ice-lite");
// session: bundle (audio and video use same candidate)
if (isVideoEnabled && isAudioEnabled){
if (answerVideoMedia.mediaID.empty()){
FAIL_MSG("video media has no media id; necessary for BUNDLE.");
return "";
}
if (answerAudioMedia.mediaID.empty()){
FAIL_MSG("audio media has no media id; necessary for BUNDLE.");
return "";
}
std::string bundled;
for (size_t i = 0; i < sdpOffer.medias.size(); ++i){
if (sdpOffer.medias[i].type == "audio" || sdpOffer.medias[i].type == "video"){
if (!bundled.empty()){bundled += " ";}
bundled += sdpOffer.medias[i].mediaID;
}
}
addLine("a=group:BUNDLE %s", bundled.c_str());
}
// add medias (order is important)
for (size_t i = 0; i < sdpOffer.medias.size(); ++i){
SDP::Media &mediaOffer = sdpOffer.medias[i];
std::string type = mediaOffer.type;
SDP::Media *media = NULL;
SDP::MediaFormat *fmtMedia = NULL;
SDP::MediaFormat *fmtRED = NULL;
SDP::MediaFormat *fmtULPFEC = NULL;
bool isEnabled = false;
std::vector<uint8_t> supportedPayloadTypes;
if (type != "audio" && type != "video"){continue;}
// port = 9 (default), port = 0 (disable this media)
if (type == "audio"){
isEnabled = isAudioEnabled;
media = &answerAudioMedia;
fmtMedia = &answerAudioFormat;
}else if (type == "video"){
isEnabled = isVideoEnabled;
media = &answerVideoMedia;
fmtMedia = &answerVideoFormat;
fmtRED = media->getFormatForEncodingName("RED");
fmtULPFEC = media->getFormatForEncodingName("ULPFEC");
}
if (!media){
WARN_MSG("No media found.");
continue;
}
if (!fmtMedia){
WARN_MSG("No format found.");
continue;
}
// we collect all supported payload types (e.g. RED and
// ULPFEC have their own payload type). We then serialize
// them payload types into a string that is used with the
// `m=` line to indicate we have support for these.
supportedPayloadTypes.push_back((uint8_t)fmtMedia->payloadType);
if ((videoLossPrevention & SDP_LOSS_PREVENTION_ULPFEC) && fmtRED && fmtULPFEC){
supportedPayloadTypes.push_back(fmtRED->payloadType);
supportedPayloadTypes.push_back(fmtULPFEC->payloadType);
}
std::stringstream ss;
size_t nels = supportedPayloadTypes.size();
for (size_t k = 0; k < nels; ++k){
ss << (int)supportedPayloadTypes[k];
if ((k + 1) < nels){ss << " ";}
}
std::string payloadTypes = ss.str();
if (isEnabled){
addLine("m=%s 9 UDP/TLS/RTP/SAVPF %s", type.c_str(), payloadTypes.c_str());
}else{
addLine("m=%s %u UDP/TLS/RTP/SAVPF %s", type.c_str(), 0, mediaOffer.payloadTypes.c_str());
}
addLine("c=IN IP4 0.0.0.0");
if (!isEnabled){
// We have to add the direction otherwise we'll receive an error
// like "Answer tried to set recv when offer did not set send"
// from Firefox.
addLine("a=%s", direction.c_str());
continue;
}
addLine("a=rtcp:9");
addLine("a=%s", direction.c_str());
addLine("a=setup:passive");
addLine("a=fingerprint:sha-256 %s", fingerprint.c_str());
addLine("a=ice-ufrag:%s", fmtMedia->iceUFrag.c_str());
addLine("a=ice-pwd:%s", fmtMedia->icePwd.c_str());
addLine("a=rtcp-mux");
addLine("a=rtcp-rsize");
addLine("a=%s", fmtMedia->rtpmap.c_str());
// BEGIN FEC/RTX: testing with just FEC or RTX
if ((videoLossPrevention & SDP_LOSS_PREVENTION_ULPFEC) && fmtRED && fmtULPFEC){
addLine("a=rtpmap:%u ulpfec/90000", fmtULPFEC->payloadType);
addLine("a=rtpmap:%u red/90000", fmtRED->payloadType);
}
if (videoLossPrevention & SDP_LOSS_PREVENTION_NACK){
addLine("a=rtcp-fb:%u nack", fmtMedia->payloadType);
}
// END FEC/RTX
if (type == "video"){addLine("a=rtcp-fb:%u goog-remb", fmtMedia->payloadType);}
if (!media->mediaID.empty()){addLine("a=mid:%s", media->mediaID.c_str());}
if (fmtMedia->encodingName == "H264"){
std::string usedProfile = fmtMedia->getFormatParameterForName("profile-level-id");
if (usedProfile != "42e01f"){
WARN_MSG("The selected profile-level-id was not 42e01f. We rewrite it into this because "
"that's what we support atm.");
usedProfile = "42e01f";
}
addLine("a=fmtp:%u profile-level-id=%s;level-asymmetry-allowed=1;packetization-mode=1",
fmtMedia->payloadType, usedProfile.c_str());
}else if (fmtMedia->encodingName == "OPUS"){
addLine("a=fmtp:%u minptime=10;useinbandfec=1", fmtMedia->payloadType);
}
addLine("a=candidate:1 1 udp 2130706431 %s %u typ host", candidateIP.c_str(), candidatePort);
addLine("a=end-of-candidates");
}
// combine all the generated lines.
size_t nlines = output.size();
for (size_t i = 0; i < nlines; ++i){result += output[i] + "\r\n";}
return result;
}
void Answer::addLine(const std::string &fmt, ...){
char buffer[1024] ={};
va_list args;
va_start(args, fmt);
vsnprintf(buffer, sizeof(buffer), fmt.c_str(), args);
va_end(args);
output.push_back(buffer);
}
// This function will check if the offer you passed into
// `parseOffer()` contains a media line for the given
// `type`. When found we also check if it contains a codec for
// the given `codecName`. When both are found copy it to the
// given `outMedia` and `outFormat` (which are our answer
// objects. We also generate the values for ice-pwd and
// ice-ufrag which are used during STUN.
//
// @param codecName (string) Can be a comma separated
// string with codecs that you
// support; we select the first
// one that we find.
bool Answer::enableMedia(const std::string &type, const std::string &codecList,
SDP::Media &outMedia, SDP::MediaFormat &outFormat){
Media *media = sdpOffer.getMediaForType(type);
if (!media){
INFO_MSG("Cannot enable %s codec; offer doesn't have %s media.", codecList.c_str(), type.c_str());
return false;
}
std::vector<std::string> codecs = splitString(codecList, ',');
if (codecs.size() == 0){
FAIL_MSG("Failed to split the given codecList.");
return false;
}
// ok, this is a bit ugly sorry for that... but when H264 was
// requested we have to check if the packetization mode is
// what we support. Firefox does packetization-mode 0 and 1
// and provides both formats in their SDP. It may happen that
// an SDP contains multiple format-specs for H264
SDP::MediaFormat *format = NULL;
for (size_t i = 0; i < codecs.size(); ++i){
std::string codec = codecMist2RTP(codecs[i]);
std::vector<SDP::MediaFormat *> formats = media->getFormatsForEncodingName(codec);
for (size_t j = 0; j < formats.size(); ++j){
if (codec == "H264"){
if (formats[j]->getPacketizationModeForH264() != 1){
MEDIUM_MSG(
"Skipping this H264 format because it uses a packetization mode we don't support.");
format = NULL;
continue;
}
if (formats[j]->getProfileLevelIdForH264() != "42e01f"){
MEDIUM_MSG(
"Skipping this H264 format because it uses an unsupported profile-level-id.");
format = NULL;
continue;
}
}
format = formats[j];
break;
}
if (format){break;}
}
if (!format){
FAIL_MSG("Cannot enable %s; codec not found %s.", type.c_str(), codecList.c_str());
return false;
}
INFO_MSG("Enabling media for codec: %s", format->encodingName.c_str());
outMedia = *media;
outFormat = *format;
outFormat.rtcpFormats.clear();
outFormat.icePwd = generateIcePwd();
outFormat.iceUFrag = generateIceUFrag();
return true;
}
std::string Answer::generateSessionId(){
srand(time(NULL));
uint64_t id = Util::getMicros();
id += rand();
std::stringstream ss;
ss << id;
return ss.str();
}
std::string Answer::generateIceUFrag(){return generateRandomString(4);}
std::string Answer::generateIcePwd(){return generateRandomString(22);}
std::string Answer::generateRandomString(const int len){
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
std::string s;
for (int i = 0; i < len; ++i){s.push_back(alphanum[rand() % (sizeof(alphanum) - 1)]);}
return s;
}
std::vector<std::string> Answer::splitString(const std::string &str, char delim){
std::stringstream ss(str);
std::string segment;
std::vector<std::string> result;
while (std::getline(ss, segment, delim)){result.push_back(segment);}
return result;
}
static std::string string_to_upper(const std::string &str){
std::string result = str;
char *p = (char *)result.c_str();
while (*p != 0){
if (*p >= 'a' && *p <= 'z'){*p = *p & ~0x20;}
p++;
}
return result;
}
}// namespace SDP