1451 lines
52 KiB
C++
Executable File
1451 lines
52 KiB
C++
Executable File
/**********
|
|
This library is free software; you can redistribute it and/or modify it under
|
|
the terms of the GNU Lesser General Public License as published by the
|
|
Free Software Foundation; either version 2.1 of the License, or (at your
|
|
option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.)
|
|
|
|
This library 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 Lesser General Public License for
|
|
more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public License
|
|
along with this library; if not, write to the Free Software Foundation, Inc.,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
**********/
|
|
// "liveMedia"
|
|
// Copyright (c) 1996-2016 Live Networks, Inc. All rights reserved.
|
|
// A data structure that represents a session that consists of
|
|
// potentially multiple (audio and/or video) sub-sessions
|
|
// Implementation
|
|
|
|
//#include "liveMedia.hh"
|
|
#include "Locale.hh"
|
|
#include "GroupsockHelper.hh"
|
|
#include <ctype.h>
|
|
|
|
////////// MediaSession //////////
|
|
|
|
MediaSession* MediaSession::createNew(UsageEnvironment& env,
|
|
char const* sdpDescription) {
|
|
MediaSession* newSession = new MediaSession(env);
|
|
if (newSession != NULL) {
|
|
if (!newSession->initializeWithSDP(sdpDescription)) {
|
|
delete newSession;
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
return newSession;
|
|
}
|
|
|
|
Boolean MediaSession::lookupByName(UsageEnvironment& env,
|
|
char const* instanceName,
|
|
MediaSession*& resultSession) {
|
|
resultSession = NULL; // unless we succeed
|
|
|
|
Medium* medium;
|
|
if (!Medium::lookupByName(env, instanceName, medium)) return False;
|
|
|
|
if (!medium->isMediaSession()) {
|
|
env.setResultMsg(instanceName, " is not a 'MediaSession' object");
|
|
return False;
|
|
}
|
|
|
|
resultSession = (MediaSession*)medium;
|
|
return True;
|
|
}
|
|
|
|
MediaSession::MediaSession(UsageEnvironment& env)
|
|
: Medium(env),
|
|
fSubsessionsHead(NULL), fSubsessionsTail(NULL),
|
|
fConnectionEndpointName(NULL),
|
|
fMaxPlayStartTime(0.0f), fMaxPlayEndTime(0.0f), fAbsStartTime(NULL), fAbsEndTime(NULL),
|
|
fScale(1.0f), fSpeed(1.0f),
|
|
fMediaSessionType(NULL), fSessionName(NULL), fSessionDescription(NULL), fControlPath(NULL) {
|
|
fSourceFilterAddr.s_addr = 0;
|
|
|
|
// Get our host name, and use this for the RTCP CNAME:
|
|
const unsigned maxCNAMElen = 100;
|
|
char CNAME[maxCNAMElen+1];
|
|
#ifndef CRIS
|
|
gethostname((char*)CNAME, maxCNAMElen);
|
|
#else
|
|
// "gethostname()" isn't defined for this platform
|
|
sprintf(CNAME, "unknown host %d", (unsigned)(our_random()*0x7FFFFFFF));
|
|
#endif
|
|
CNAME[maxCNAMElen] = '\0'; // just in case
|
|
fCNAME = strDup(CNAME);
|
|
}
|
|
|
|
MediaSession::~MediaSession() {
|
|
delete fSubsessionsHead;
|
|
delete[] fCNAME;
|
|
delete[] fConnectionEndpointName;
|
|
delete[] fAbsStartTime; delete[] fAbsEndTime;
|
|
delete[] fMediaSessionType;
|
|
delete[] fSessionName;
|
|
delete[] fSessionDescription;
|
|
delete[] fControlPath;
|
|
}
|
|
|
|
Boolean MediaSession::isMediaSession() const {
|
|
return True;
|
|
}
|
|
|
|
MediaSubsession* MediaSession::createNewMediaSubsession() {
|
|
// default implementation:
|
|
return new MediaSubsession(*this);
|
|
}
|
|
|
|
Boolean MediaSession::initializeWithSDP(char const* sdpDescription) {
|
|
if (sdpDescription == NULL) return False;
|
|
|
|
// Begin by processing all SDP lines until we see the first "m="
|
|
char const* sdpLine = sdpDescription;
|
|
char const* nextSDPLine;
|
|
while (1) {
|
|
if (!parseSDPLine(sdpLine, nextSDPLine)) return False;
|
|
//##### We should really check for the correct SDP version (v=0)
|
|
if (sdpLine[0] == 'm') break;
|
|
sdpLine = nextSDPLine;
|
|
if (sdpLine == NULL) break; // there are no m= lines at all
|
|
|
|
// Check for various special SDP lines that we understand:
|
|
if (parseSDPLine_s(sdpLine)) continue;
|
|
if (parseSDPLine_i(sdpLine)) continue;
|
|
if (parseSDPLine_c(sdpLine)) continue;
|
|
if (parseSDPAttribute_control(sdpLine)) continue;
|
|
if (parseSDPAttribute_range(sdpLine)) continue;
|
|
if (parseSDPAttribute_type(sdpLine)) continue;
|
|
if (parseSDPAttribute_source_filter(sdpLine)) continue;
|
|
}
|
|
|
|
while (sdpLine != NULL) {
|
|
// We have a "m=" line, representing a new sub-session:
|
|
MediaSubsession* subsession = createNewMediaSubsession();
|
|
if (subsession == NULL) {
|
|
envir().setResultMsg("Unable to create new MediaSubsession");
|
|
return False;
|
|
}
|
|
|
|
// Parse the line as "m=<medium_name> <client_portNum> RTP/AVP <fmt>"
|
|
// or "m=<medium_name> <client_portNum>/<num_ports> RTP/AVP <fmt>"
|
|
// (Should we be checking for >1 payload format number here?)#####
|
|
char* mediumName = strDupSize(sdpLine); // ensures we have enough space
|
|
char const* protocolName = NULL;
|
|
unsigned payloadFormat;
|
|
if ((sscanf(sdpLine, "m=%s %hu RTP/AVP %u",
|
|
mediumName, &subsession->fClientPortNum, &payloadFormat) == 3 ||
|
|
sscanf(sdpLine, "m=%s %hu/%*u RTP/AVP %u",
|
|
mediumName, &subsession->fClientPortNum, &payloadFormat) == 3)
|
|
&& payloadFormat <= 127) {
|
|
protocolName = "RTP";
|
|
} else if ((sscanf(sdpLine, "m=%s %hu UDP %u",
|
|
mediumName, &subsession->fClientPortNum, &payloadFormat) == 3 ||
|
|
sscanf(sdpLine, "m=%s %hu udp %u",
|
|
mediumName, &subsession->fClientPortNum, &payloadFormat) == 3 ||
|
|
sscanf(sdpLine, "m=%s %hu RAW/RAW/UDP %u",
|
|
mediumName, &subsession->fClientPortNum, &payloadFormat) == 3)
|
|
&& payloadFormat <= 127) {
|
|
// This is a RAW UDP source
|
|
protocolName = "UDP";
|
|
} else {
|
|
// This "m=" line is bad; output an error message saying so:
|
|
char* sdpLineStr;
|
|
if (nextSDPLine == NULL) {
|
|
sdpLineStr = (char*)sdpLine;
|
|
} else {
|
|
sdpLineStr = strDup(sdpLine);
|
|
sdpLineStr[nextSDPLine-sdpLine] = '\0';
|
|
}
|
|
envir() << "Bad SDP \"m=\" line: " << sdpLineStr << "\n";
|
|
if (sdpLineStr != (char*)sdpLine) delete[] sdpLineStr;
|
|
|
|
delete[] mediumName;
|
|
delete subsession;
|
|
|
|
// Skip the following SDP lines, up until the next "m=":
|
|
while (1) {
|
|
sdpLine = nextSDPLine;
|
|
if (sdpLine == NULL) break; // we've reached the end
|
|
if (!parseSDPLine(sdpLine, nextSDPLine)) return False;
|
|
|
|
if (sdpLine[0] == 'm') break; // we've reached the next subsession
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Insert this subsession at the end of the list:
|
|
if (fSubsessionsTail == NULL) {
|
|
fSubsessionsHead = fSubsessionsTail = subsession;
|
|
} else {
|
|
fSubsessionsTail->setNext(subsession);
|
|
fSubsessionsTail = subsession;
|
|
}
|
|
|
|
subsession->serverPortNum = subsession->fClientPortNum; // by default
|
|
|
|
char const* mStart = sdpLine;
|
|
subsession->fSavedSDPLines = strDup(mStart);
|
|
|
|
subsession->fMediumName = strDup(mediumName);
|
|
delete[] mediumName;
|
|
subsession->fProtocolName = strDup(protocolName);
|
|
subsession->fRTPPayloadFormat = payloadFormat;
|
|
|
|
// Process the following SDP lines, up until the next "m=":
|
|
while (1) {
|
|
sdpLine = nextSDPLine;
|
|
if (sdpLine == NULL) break; // we've reached the end
|
|
if (!parseSDPLine(sdpLine, nextSDPLine)) return False;
|
|
|
|
if (sdpLine[0] == 'm') break; // we've reached the next subsession
|
|
|
|
// Check for various special SDP lines that we understand:
|
|
if (subsession->parseSDPLine_c(sdpLine)) continue;
|
|
if (subsession->parseSDPLine_b(sdpLine)) continue;
|
|
if (subsession->parseSDPAttribute_rtpmap(sdpLine)) continue;
|
|
if (subsession->parseSDPAttribute_rtcpmux(sdpLine)) continue;
|
|
if (subsession->parseSDPAttribute_control(sdpLine)) continue;
|
|
if (subsession->parseSDPAttribute_range(sdpLine)) continue;
|
|
if (subsession->parseSDPAttribute_fmtp(sdpLine)) continue;
|
|
if (subsession->parseSDPAttribute_source_filter(sdpLine)) continue;
|
|
if (subsession->parseSDPAttribute_x_dimensions(sdpLine)) continue;
|
|
if (subsession->parseSDPAttribute_framerate(sdpLine)) continue;
|
|
|
|
// (Later, check for malformed lines, and other valid SDP lines#####)
|
|
}
|
|
if (sdpLine != NULL) subsession->fSavedSDPLines[sdpLine-mStart] = '\0';
|
|
|
|
// If we don't yet know the codec name, try looking it up from the
|
|
// list of static payload types:
|
|
if (subsession->fCodecName == NULL) {
|
|
subsession->fCodecName
|
|
= lookupPayloadFormat(subsession->fRTPPayloadFormat,
|
|
subsession->fRTPTimestampFrequency,
|
|
subsession->fNumChannels);
|
|
if (subsession->fCodecName == NULL) {
|
|
char typeStr[20];
|
|
sprintf(typeStr, "%d", subsession->fRTPPayloadFormat);
|
|
envir().setResultMsg("Unknown codec name for RTP payload type ",
|
|
typeStr);
|
|
return False;
|
|
}
|
|
}
|
|
|
|
// If we don't yet know this subsession's RTP timestamp frequency
|
|
// (because it uses a dynamic payload type and the corresponding
|
|
// SDP "rtpmap" attribute erroneously didn't specify it),
|
|
// then guess it now:
|
|
if (subsession->fRTPTimestampFrequency == 0) {
|
|
subsession->fRTPTimestampFrequency
|
|
= guessRTPTimestampFrequency(subsession->fMediumName,
|
|
subsession->fCodecName);
|
|
}
|
|
}
|
|
|
|
return True;
|
|
}
|
|
|
|
Boolean MediaSession::parseSDPLine(char const* inputLine,
|
|
char const*& nextLine){
|
|
// Begin by finding the start of the next line (if any):
|
|
nextLine = NULL;
|
|
for (char const* ptr = inputLine; *ptr != '\0'; ++ptr) {
|
|
if (*ptr == '\r' || *ptr == '\n') {
|
|
// We found the end of the line
|
|
++ptr;
|
|
while (*ptr == '\r' || *ptr == '\n') ++ptr;
|
|
nextLine = ptr;
|
|
if (nextLine[0] == '\0') nextLine = NULL; // special case for end
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Then, check that this line is a SDP line of the form <char>=<etc>
|
|
// (However, we also accept blank lines in the input.)
|
|
if (inputLine[0] == '\r' || inputLine[0] == '\n') return True;
|
|
if (strlen(inputLine) < 2 || inputLine[1] != '='
|
|
|| inputLine[0] < 'a' || inputLine[0] > 'z') {
|
|
envir().setResultMsg("Invalid SDP line: ", inputLine);
|
|
return False;
|
|
}
|
|
|
|
return True;
|
|
}
|
|
|
|
static char* parseCLine(char const* sdpLine) {
|
|
char* resultStr = NULL;
|
|
char* buffer = strDupSize(sdpLine); // ensures we have enough space
|
|
if (sscanf(sdpLine, "c=IN IP4 %[^/\r\n]", buffer) == 1) {
|
|
// Later, handle the optional /<ttl> and /<numAddresses> #####
|
|
resultStr = strDup(buffer);
|
|
}
|
|
delete[] buffer;
|
|
|
|
return resultStr;
|
|
}
|
|
|
|
Boolean MediaSession::parseSDPLine_s(char const* sdpLine) {
|
|
// Check for "s=<session name>" line
|
|
char* buffer = strDupSize(sdpLine);
|
|
Boolean parseSuccess = False;
|
|
|
|
if (sscanf(sdpLine, "s=%[^\r\n]", buffer) == 1) {
|
|
delete[] fSessionName; fSessionName = strDup(buffer);
|
|
parseSuccess = True;
|
|
}
|
|
delete[] buffer;
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
Boolean MediaSession::parseSDPLine_i(char const* sdpLine) {
|
|
// Check for "i=<session description>" line
|
|
char* buffer = strDupSize(sdpLine);
|
|
Boolean parseSuccess = False;
|
|
|
|
if (sscanf(sdpLine, "i=%[^\r\n]", buffer) == 1) {
|
|
delete[] fSessionDescription; fSessionDescription = strDup(buffer);
|
|
parseSuccess = True;
|
|
}
|
|
delete[] buffer;
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
Boolean MediaSession::parseSDPLine_c(char const* sdpLine) {
|
|
// Check for "c=IN IP4 <connection-endpoint>"
|
|
// or "c=IN IP4 <connection-endpoint>/<ttl+numAddresses>"
|
|
// (Later, do something with <ttl+numAddresses> also #####)
|
|
char* connectionEndpointName = parseCLine(sdpLine);
|
|
if (connectionEndpointName != NULL) {
|
|
delete[] fConnectionEndpointName;
|
|
fConnectionEndpointName = connectionEndpointName;
|
|
return True;
|
|
}
|
|
|
|
return False;
|
|
}
|
|
|
|
Boolean MediaSession::parseSDPAttribute_type(char const* sdpLine) {
|
|
// Check for a "a=type:broadcast|meeting|moderated|test|H.332|recvonly" line:
|
|
Boolean parseSuccess = False;
|
|
|
|
char* buffer = strDupSize(sdpLine);
|
|
if (sscanf(sdpLine, "a=type: %[^ ]", buffer) == 1) {
|
|
delete[] fMediaSessionType;
|
|
fMediaSessionType = strDup(buffer);
|
|
parseSuccess = True;
|
|
}
|
|
delete[] buffer;
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
Boolean MediaSession::parseSDPAttribute_control(char const* sdpLine) {
|
|
// Check for a "a=control:<control-path>" line:
|
|
Boolean parseSuccess = False;
|
|
|
|
char* controlPath = strDupSize(sdpLine); // ensures we have enough space
|
|
if (sscanf(sdpLine, "a=control: %s", controlPath) == 1) {
|
|
parseSuccess = True;
|
|
delete[] fControlPath; fControlPath = strDup(controlPath);
|
|
}
|
|
delete[] controlPath;
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
static Boolean parseRangeAttribute(char const* sdpLine, double& startTime, double& endTime) {
|
|
return sscanf(sdpLine, "a=range: npt = %lg - %lg", &startTime, &endTime) == 2;
|
|
}
|
|
|
|
static Boolean parseRangeAttribute(char const* sdpLine, char*& absStartTime, char*& absEndTime) {
|
|
size_t len = strlen(sdpLine) + 1;
|
|
char* as = new char[len];
|
|
char* ae = new char[len];
|
|
int sscanfResult = sscanf(sdpLine, "a=range: clock = %[^-\r\n]-%[^\r\n]", as, ae);
|
|
if (sscanfResult == 2) {
|
|
absStartTime = as;
|
|
absEndTime = ae;
|
|
} else if (sscanfResult == 1) {
|
|
absStartTime = as;
|
|
delete[] ae;
|
|
} else {
|
|
delete[] as; delete[] ae;
|
|
return False;
|
|
}
|
|
|
|
return True;
|
|
}
|
|
|
|
Boolean MediaSession::parseSDPAttribute_range(char const* sdpLine) {
|
|
// Check for a "a=range:npt=<startTime>-<endTime>" line:
|
|
// (Later handle other kinds of "a=range" attributes also???#####)
|
|
Boolean parseSuccess = False;
|
|
|
|
double playStartTime;
|
|
double playEndTime;
|
|
if (parseRangeAttribute(sdpLine, playStartTime, playEndTime)) {
|
|
parseSuccess = True;
|
|
if (playStartTime > fMaxPlayStartTime) {
|
|
fMaxPlayStartTime = playStartTime;
|
|
}
|
|
if (playEndTime > fMaxPlayEndTime) {
|
|
fMaxPlayEndTime = playEndTime;
|
|
}
|
|
} else if (parseRangeAttribute(sdpLine, _absStartTime(), _absEndTime())) {
|
|
parseSuccess = True;
|
|
}
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
static Boolean parseSourceFilterAttribute(char const* sdpLine,
|
|
struct in_addr& sourceAddr) {
|
|
// Check for a "a=source-filter:incl IN IP4 <something> <source>" line.
|
|
// Note: At present, we don't check that <something> really matches
|
|
// one of our multicast addresses. We also don't support more than
|
|
// one <source> #####
|
|
Boolean result = False; // until we succeed
|
|
char* sourceName = strDupSize(sdpLine); // ensures we have enough space
|
|
do {
|
|
if (sscanf(sdpLine, "a=source-filter: incl IN IP4 %*s %s",
|
|
sourceName) != 1) break;
|
|
|
|
// Now, convert this name to an address, if we can:
|
|
NetAddressList addresses(sourceName);
|
|
if (addresses.numAddresses() == 0) break;
|
|
|
|
netAddressBits sourceAddrBits
|
|
= *(netAddressBits*)(addresses.firstAddress()->data());
|
|
if (sourceAddrBits == 0) break;
|
|
|
|
sourceAddr.s_addr = sourceAddrBits;
|
|
result = True;
|
|
} while (0);
|
|
|
|
delete[] sourceName;
|
|
return result;
|
|
}
|
|
|
|
Boolean MediaSession
|
|
::parseSDPAttribute_source_filter(char const* sdpLine) {
|
|
return parseSourceFilterAttribute(sdpLine, fSourceFilterAddr);
|
|
}
|
|
|
|
char* MediaSession::lookupPayloadFormat(unsigned char rtpPayloadType,
|
|
unsigned& freq, unsigned& nCh) {
|
|
// Look up the codec name and timestamp frequency for known (static)
|
|
// RTP payload formats.
|
|
char const* temp = NULL;
|
|
switch (rtpPayloadType) {
|
|
case 0: {temp = "PCMU"; freq = 8000; nCh = 1; break;}
|
|
case 2: {temp = "G726-32"; freq = 8000; nCh = 1; break;}
|
|
case 3: {temp = "GSM"; freq = 8000; nCh = 1; break;}
|
|
case 4: {temp = "G723"; freq = 8000; nCh = 1; break;}
|
|
case 5: {temp = "DVI4"; freq = 8000; nCh = 1; break;}
|
|
case 6: {temp = "DVI4"; freq = 16000; nCh = 1; break;}
|
|
case 7: {temp = "LPC"; freq = 8000; nCh = 1; break;}
|
|
case 8: {temp = "PCMA"; freq = 8000; nCh = 1; break;}
|
|
case 9: {temp = "G722"; freq = 8000; nCh = 1; break;}
|
|
case 10: {temp = "L16"; freq = 44100; nCh = 2; break;}
|
|
case 11: {temp = "L16"; freq = 44100; nCh = 1; break;}
|
|
case 12: {temp = "QCELP"; freq = 8000; nCh = 1; break;}
|
|
case 14: {temp = "MPA"; freq = 90000; nCh = 1; break;}
|
|
// 'number of channels' is actually encoded in the media stream
|
|
case 15: {temp = "G728"; freq = 8000; nCh = 1; break;}
|
|
case 16: {temp = "DVI4"; freq = 11025; nCh = 1; break;}
|
|
case 17: {temp = "DVI4"; freq = 22050; nCh = 1; break;}
|
|
case 18: {temp = "G729"; freq = 8000; nCh = 1; break;}
|
|
case 25: {temp = "CELB"; freq = 90000; nCh = 1; break;}
|
|
case 26: {temp = "JPEG"; freq = 90000; nCh = 1; break;}
|
|
case 28: {temp = "NV"; freq = 90000; nCh = 1; break;}
|
|
case 31: {temp = "H261"; freq = 90000; nCh = 1; break;}
|
|
case 32: {temp = "MPV"; freq = 90000; nCh = 1; break;}
|
|
case 33: {temp = "MP2T"; freq = 90000; nCh = 1; break;}
|
|
case 34: {temp = "H263"; freq = 90000; nCh = 1; break;}
|
|
};
|
|
|
|
return strDup(temp);
|
|
}
|
|
|
|
unsigned MediaSession::guessRTPTimestampFrequency(char const* mediumName,
|
|
char const* codecName) {
|
|
// By default, we assume that audio sessions use a frequency of 8000,
|
|
// video sessions use a frequency of 90000,
|
|
// and text sessions use a frequency of 1000.
|
|
// Begin by checking for known exceptions to this rule
|
|
// (where the frequency is known unambiguously (e.g., not like "DVI4"))
|
|
if (strcmp(codecName, "L16") == 0) return 44100;
|
|
if (strcmp(codecName, "MPA") == 0
|
|
|| strcmp(codecName, "MPA-ROBUST") == 0
|
|
|| strcmp(codecName, "X-MP3-DRAFT-00") == 0) return 90000;
|
|
|
|
// Now, guess default values:
|
|
if (strcmp(mediumName, "video") == 0) return 90000;
|
|
else if (strcmp(mediumName, "text") == 0) return 1000;
|
|
return 8000; // for "audio", and any other medium
|
|
}
|
|
|
|
char* MediaSession::absStartTime() const {
|
|
if (fAbsStartTime != NULL) return fAbsStartTime;
|
|
|
|
// If a subsession has an 'absolute' start time, then use that:
|
|
MediaSubsessionIterator iter(*this);
|
|
MediaSubsession* subsession;
|
|
while ((subsession = iter.next()) != NULL) {
|
|
if (subsession->_absStartTime() != NULL) return subsession->_absStartTime();
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
char* MediaSession::absEndTime() const {
|
|
if (fAbsEndTime != NULL) return fAbsEndTime;
|
|
|
|
// If a subsession has an 'absolute' end time, then use that:
|
|
MediaSubsessionIterator iter(*this);
|
|
MediaSubsession* subsession;
|
|
while ((subsession = iter.next()) != NULL) {
|
|
if (subsession->_absEndTime() != NULL) return subsession->_absEndTime();
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
Boolean MediaSession
|
|
::initiateByMediaType(char const* mimeType,
|
|
MediaSubsession*& resultSubsession,
|
|
int useSpecialRTPoffset) {
|
|
// Look through this session's subsessions for media that match "mimeType"
|
|
resultSubsession = NULL;
|
|
MediaSubsessionIterator iter(*this);
|
|
MediaSubsession* subsession;
|
|
while ((subsession = iter.next()) != NULL) {
|
|
Boolean wasAlreadyInitiated = subsession->readSource() != NULL;
|
|
if (!wasAlreadyInitiated) {
|
|
// Try to create a source for this subsession:
|
|
if (!subsession->initiate(useSpecialRTPoffset)) return False;
|
|
}
|
|
|
|
// Make sure the source's MIME type is one that we handle:
|
|
if (strcmp(subsession->readSource()->MIMEtype(), mimeType) != 0) {
|
|
if (!wasAlreadyInitiated) subsession->deInitiate();
|
|
continue;
|
|
}
|
|
|
|
resultSubsession = subsession;
|
|
break; // use this
|
|
}
|
|
|
|
if (resultSubsession == NULL) {
|
|
envir().setResultMsg("Session has no usable media subsession");
|
|
return False;
|
|
}
|
|
|
|
return True;
|
|
}
|
|
|
|
|
|
////////// MediaSubsessionIterator //////////
|
|
|
|
MediaSubsessionIterator::MediaSubsessionIterator(MediaSession const& session)
|
|
: fOurSession(session) {
|
|
reset();
|
|
}
|
|
|
|
MediaSubsessionIterator::~MediaSubsessionIterator() {
|
|
}
|
|
|
|
MediaSubsession* MediaSubsessionIterator::next() {
|
|
MediaSubsession* result = fNextPtr;
|
|
|
|
if (fNextPtr != NULL) fNextPtr = fNextPtr->fNext;
|
|
|
|
return result;
|
|
}
|
|
|
|
void MediaSubsessionIterator::reset() {
|
|
fNextPtr = fOurSession.fSubsessionsHead;
|
|
}
|
|
|
|
|
|
////////// SDPAttribute definition //////////
|
|
|
|
class SDPAttribute {
|
|
public:
|
|
SDPAttribute(char const* strValue, Boolean valueIsHexadecimal);
|
|
virtual ~SDPAttribute();
|
|
|
|
char const* strValue() const { return fStrValue; }
|
|
char const* strValueToLower() const { return fStrValueToLower; }
|
|
int intValue() const { return fIntValue; }
|
|
Boolean valueIsHexadecimal() const { return fValueIsHexadecimal; }
|
|
|
|
private:
|
|
char* fStrValue;
|
|
char* fStrValueToLower;
|
|
int fIntValue;
|
|
Boolean fValueIsHexadecimal;
|
|
};
|
|
|
|
|
|
////////// MediaSubsession //////////
|
|
|
|
MediaSubsession::MediaSubsession(MediaSession& parent)
|
|
: serverPortNum(0), sink(NULL), miscPtr(NULL),
|
|
fParent(parent), fNext(NULL),
|
|
fConnectionEndpointName(NULL),
|
|
fClientPortNum(0), fRTPPayloadFormat(0xFF),
|
|
fSavedSDPLines(NULL), fMediumName(NULL), fCodecName(NULL), fProtocolName(NULL),
|
|
fRTPTimestampFrequency(0), fMultiplexRTCPWithRTP(False), fControlPath(NULL),
|
|
fSourceFilterAddr(parent.sourceFilterAddr()), fBandwidth(0),
|
|
fPlayStartTime(0.0), fPlayEndTime(0.0), fAbsStartTime(NULL), fAbsEndTime(NULL),
|
|
fVideoWidth(0), fVideoHeight(0), fVideoFPS(0), fNumChannels(1), fScale(1.0f), fNPT_PTS_Offset(0.0f),
|
|
fAttributeTable(HashTable::create(STRING_HASH_KEYS)),
|
|
fRTPSocket(NULL), fRTCPSocket(NULL),
|
|
fRTPSource(NULL), fRTCPInstance(NULL), fReadSource(NULL),
|
|
fReceiveRawMP3ADUs(False), fReceiveRawJPEGFrames(False),
|
|
fSessionId(NULL) {
|
|
rtpInfo.seqNum = 0; rtpInfo.timestamp = 0; rtpInfo.infoIsNew = False;
|
|
|
|
// A few attributes have unusual default values. Set these now:
|
|
setAttribute("profile-level-id", "0", True/*value is hexadecimal*/); // used with "video/H264"
|
|
// This won't work for MPEG-4 (unless the value is <10), because for MPEG-4, the value
|
|
// is assumed to be a decimal string, not a hexadecimal string. NEED TO FIX #####
|
|
setAttribute("profile-id", "1"); // used with "video/H265"
|
|
setAttribute("level-id", "93"); // used with "video/H265"
|
|
setAttribute("interop-constraints", "B00000000000"); // used with "video/H265"
|
|
}
|
|
|
|
MediaSubsession::~MediaSubsession() {
|
|
deInitiate();
|
|
|
|
delete[] fConnectionEndpointName; delete[] fSavedSDPLines;
|
|
delete[] fMediumName; delete[] fCodecName; delete[] fProtocolName;
|
|
delete[] fControlPath;
|
|
delete[] fAbsStartTime; delete[] fAbsEndTime;
|
|
delete[] fSessionId;
|
|
|
|
// Empty and delete our 'attributes table':
|
|
SDPAttribute* attr;
|
|
while ((attr = (SDPAttribute*)fAttributeTable->RemoveNext()) != NULL) {
|
|
delete attr;
|
|
}
|
|
delete fAttributeTable;
|
|
|
|
delete fNext;
|
|
}
|
|
|
|
void MediaSubsession::addFilter(FramedFilter* filter){
|
|
fReadSource = filter;
|
|
}
|
|
|
|
double MediaSubsession::playStartTime() const {
|
|
if (fPlayStartTime > 0) return fPlayStartTime;
|
|
|
|
return fParent.playStartTime();
|
|
}
|
|
|
|
double MediaSubsession::playEndTime() const {
|
|
if (fPlayEndTime > 0) return fPlayEndTime;
|
|
|
|
return fParent.playEndTime();
|
|
}
|
|
|
|
char* MediaSubsession::absStartTime() const {
|
|
if (fAbsStartTime != NULL) return fAbsStartTime;
|
|
|
|
return fParent.absStartTime();
|
|
}
|
|
|
|
char* MediaSubsession::absEndTime() const {
|
|
if (fAbsEndTime != NULL) return fAbsEndTime;
|
|
|
|
return fParent.absEndTime();
|
|
}
|
|
|
|
static Boolean const honorSDPPortChoice
|
|
#ifdef IGNORE_UNICAST_SDP_PORTS
|
|
= False;
|
|
#else
|
|
= True;
|
|
#endif
|
|
|
|
Boolean MediaSubsession::initiate(int useSpecialRTPoffset) {
|
|
if (fReadSource != NULL) return True; // has already been initiated
|
|
|
|
do {
|
|
if (fCodecName == NULL) {
|
|
env().setResultMsg("Codec is unspecified");
|
|
break;
|
|
}
|
|
|
|
// Create RTP and RTCP 'Groupsocks' on which to receive incoming data.
|
|
// (Groupsocks will work even for unicast addresses)
|
|
struct in_addr tempAddr;
|
|
tempAddr.s_addr = connectionEndpointAddress();
|
|
// This could get changed later, as a result of a RTSP "SETUP"
|
|
|
|
if (fClientPortNum != 0 && (honorSDPPortChoice || IsMulticastAddress(tempAddr.s_addr))) {
|
|
// The sockets' port numbers were specified for us. Use these:
|
|
Boolean const protocolIsRTP = strcmp(fProtocolName, "RTP") == 0;
|
|
if (protocolIsRTP && !fMultiplexRTCPWithRTP) {
|
|
fClientPortNum = fClientPortNum&~1;
|
|
// use an even-numbered port for RTP, and the next (odd-numbered) port for RTCP
|
|
}
|
|
if (isSSM()) {
|
|
fRTPSocket = new Groupsock(env(), tempAddr, fSourceFilterAddr, fClientPortNum);
|
|
} else {
|
|
fRTPSocket = new Groupsock(env(), tempAddr, fClientPortNum, 255);
|
|
}
|
|
if (fRTPSocket == NULL) {
|
|
env().setResultMsg("Failed to create RTP socket");
|
|
break;
|
|
}
|
|
|
|
if (protocolIsRTP) {
|
|
if (fMultiplexRTCPWithRTP) {
|
|
// Use the RTP 'groupsock' object for RTCP as well:
|
|
fRTCPSocket = fRTPSocket;
|
|
} else {
|
|
// Set our RTCP port to be the RTP port + 1:
|
|
portNumBits const rtcpPortNum = fClientPortNum|1;
|
|
if (isSSM()) {
|
|
fRTCPSocket = new Groupsock(env(), tempAddr, fSourceFilterAddr, rtcpPortNum);
|
|
} else {
|
|
fRTCPSocket = new Groupsock(env(), tempAddr, rtcpPortNum, 255);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Port numbers were not specified in advance, so we use ephemeral port numbers.
|
|
// Create sockets until we get a port-number pair (even: RTP; even+1: RTCP).
|
|
// (However, if we're multiplexing RTCP with RTP, then we create only one socket,
|
|
// and the port number can be even or odd.)
|
|
// We need to make sure that we don't keep trying to use the same bad port numbers over
|
|
// and over again, so we store bad sockets in a table, and delete them all when we're done.
|
|
HashTable* socketHashTable = HashTable::create(ONE_WORD_HASH_KEYS);
|
|
if (socketHashTable == NULL) break;
|
|
Boolean success = False;
|
|
NoReuse dummy(env());
|
|
// ensures that our new ephemeral port number won't be one that's already in use
|
|
|
|
while (1) {
|
|
// Create a new socket:
|
|
if (isSSM()) {
|
|
fRTPSocket = new Groupsock(env(), tempAddr, fSourceFilterAddr, 0);
|
|
} else {
|
|
fRTPSocket = new Groupsock(env(), tempAddr, 0, 255);
|
|
}
|
|
if (fRTPSocket == NULL) {
|
|
env().setResultMsg("MediaSession::initiate(): unable to create RTP and RTCP sockets");
|
|
break;
|
|
}
|
|
|
|
// Get the client port number:
|
|
Port clientPort(0);
|
|
if (!getSourcePort(env(), fRTPSocket->socketNum(), clientPort)) {
|
|
break;
|
|
}
|
|
fClientPortNum = ntohs(clientPort.num());
|
|
|
|
if (fMultiplexRTCPWithRTP) {
|
|
// Use this RTP 'groupsock' object for RTCP as well:
|
|
fRTCPSocket = fRTPSocket;
|
|
success = True;
|
|
break;
|
|
}
|
|
|
|
// To be usable for RTP, the client port number must be even:
|
|
if ((fClientPortNum&1) != 0) { // it's odd
|
|
// Record this socket in our table, and keep trying:
|
|
unsigned key = (unsigned)fClientPortNum;
|
|
Groupsock* existing = (Groupsock*)socketHashTable->Add((char const*)key, fRTPSocket);
|
|
delete existing; // in case it wasn't NULL
|
|
continue;
|
|
}
|
|
|
|
// Make sure we can use the next (i.e., odd) port number, for RTCP:
|
|
portNumBits rtcpPortNum = fClientPortNum|1;
|
|
if (isSSM()) {
|
|
fRTCPSocket = new Groupsock(env(), tempAddr, fSourceFilterAddr, rtcpPortNum);
|
|
} else {
|
|
fRTCPSocket = new Groupsock(env(), tempAddr, rtcpPortNum, 255);
|
|
}
|
|
if (fRTCPSocket != NULL && fRTCPSocket->socketNum() >= 0) {
|
|
// Success! Use these two sockets.
|
|
success = True;
|
|
break;
|
|
} else {
|
|
// We couldn't create the RTCP socket (perhaps that port number's already in use elsewhere?).
|
|
delete fRTCPSocket; fRTCPSocket = NULL;
|
|
|
|
// Record the first socket in our table, and keep trying:
|
|
unsigned key = (unsigned)fClientPortNum;
|
|
Groupsock* existing = (Groupsock*)socketHashTable->Add((char const*)key, fRTPSocket);
|
|
delete existing; // in case it wasn't NULL
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Clean up the socket hash table (and contents):
|
|
Groupsock* oldGS;
|
|
while ((oldGS = (Groupsock*)socketHashTable->RemoveNext()) != NULL) {
|
|
delete oldGS;
|
|
}
|
|
delete socketHashTable;
|
|
|
|
if (!success) break; // a fatal error occurred trying to create the RTP and RTCP sockets; we can't continue
|
|
}
|
|
|
|
// Try to use a big receive buffer for RTP - at least 0.1 second of
|
|
// specified bandwidth and at least 50 KB
|
|
unsigned rtpBufSize = fBandwidth * 25 / 2; // 1 kbps * 0.1 s = 12.5 bytes
|
|
if (rtpBufSize < 50 * 1024)
|
|
rtpBufSize = 50 * 1024;
|
|
increaseReceiveBufferTo(env(), fRTPSocket->socketNum(), rtpBufSize);
|
|
|
|
if (isSSM() && fRTCPSocket != NULL) {
|
|
// Special case for RTCP SSM: Send RTCP packets back to the source via unicast:
|
|
fRTCPSocket->changeDestinationParameters(fSourceFilterAddr,0,~0);
|
|
}
|
|
|
|
// Create "fRTPSource" and "fReadSource":
|
|
if (!createSourceObjects(useSpecialRTPoffset)) break;
|
|
|
|
if (fReadSource == NULL) {
|
|
env().setResultMsg("Failed to create read source");
|
|
break;
|
|
}
|
|
|
|
// Finally, create our RTCP instance. (It starts running automatically)
|
|
if (fRTPSource != NULL && fRTCPSocket != NULL) {
|
|
// If bandwidth is specified, use it and add 5% for RTCP overhead.
|
|
// Otherwise make a guess at 500 kbps.
|
|
unsigned totSessionBandwidth
|
|
= fBandwidth ? fBandwidth + fBandwidth / 20 : 500;
|
|
fRTCPInstance = RTCPInstance::createNew(env(), fRTCPSocket,
|
|
totSessionBandwidth,
|
|
(unsigned char const*)
|
|
fParent.CNAME(),
|
|
NULL /* we're a client */,
|
|
fRTPSource);
|
|
if (fRTCPInstance == NULL) {
|
|
env().setResultMsg("Failed to create RTCP instance");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return True;
|
|
} while (0);
|
|
|
|
deInitiate();
|
|
fClientPortNum = 0;
|
|
return False;
|
|
}
|
|
|
|
void MediaSubsession::deInitiate() {
|
|
Medium::close(fRTCPInstance); fRTCPInstance = NULL;
|
|
|
|
Medium::close(fReadSource); // this is assumed to also close fRTPSource
|
|
fReadSource = NULL; fRTPSource = NULL;
|
|
|
|
delete fRTPSocket;
|
|
if (fRTCPSocket != fRTPSocket) delete fRTCPSocket;
|
|
fRTPSocket = NULL; fRTCPSocket = NULL;
|
|
}
|
|
|
|
Boolean MediaSubsession::setClientPortNum(unsigned short portNum) {
|
|
if (fReadSource != NULL) {
|
|
env().setResultMsg("A read source has already been created");
|
|
return False;
|
|
}
|
|
|
|
fClientPortNum = portNum;
|
|
return True;
|
|
}
|
|
|
|
char const* MediaSubsession::attrVal_str(char const* attrName) const {
|
|
SDPAttribute* attr = (SDPAttribute*)(fAttributeTable->Lookup(attrName));
|
|
if (attr == NULL) return "";
|
|
|
|
return attr->strValue();
|
|
}
|
|
|
|
char const* MediaSubsession::attrVal_strToLower(char const* attrName) const {
|
|
SDPAttribute* attr = (SDPAttribute*)(fAttributeTable->Lookup(attrName));
|
|
if (attr == NULL) return "";
|
|
|
|
return attr->strValueToLower();
|
|
}
|
|
|
|
unsigned MediaSubsession::attrVal_int(char const* attrName) const {
|
|
SDPAttribute* attr = (SDPAttribute*)(fAttributeTable->Lookup(attrName));
|
|
if (attr == NULL) return 0;
|
|
|
|
return attr->intValue();
|
|
}
|
|
|
|
char const* MediaSubsession::fmtp_config() const {
|
|
char const* result = attrVal_str("config");
|
|
if (result[0] == '\0') result = attrVal_str("configuration");
|
|
|
|
return result;
|
|
}
|
|
|
|
netAddressBits MediaSubsession::connectionEndpointAddress() const {
|
|
do {
|
|
// Get the endpoint name from with us, or our parent session:
|
|
char const* endpointString = connectionEndpointName();
|
|
if (endpointString == NULL) {
|
|
endpointString = parentSession().connectionEndpointName();
|
|
}
|
|
if (endpointString == NULL) break;
|
|
|
|
// Now, convert this name to an address, if we can:
|
|
NetAddressList addresses(endpointString);
|
|
if (addresses.numAddresses() == 0) break;
|
|
|
|
return *(netAddressBits*)(addresses.firstAddress()->data());
|
|
} while (0);
|
|
|
|
// No address known:
|
|
return 0;
|
|
}
|
|
|
|
void MediaSubsession::setDestinations(netAddressBits defaultDestAddress) {
|
|
// Get the destination address from the connection endpoint name
|
|
// (This will be 0 if it's not known, in which case we use the default)
|
|
netAddressBits destAddress = connectionEndpointAddress();
|
|
if (destAddress == 0) destAddress = defaultDestAddress;
|
|
struct in_addr destAddr; destAddr.s_addr = destAddress;
|
|
|
|
// The destination TTL remains unchanged:
|
|
int destTTL = ~0; // means: don't change
|
|
|
|
if (fRTPSocket != NULL) {
|
|
Port destPort(serverPortNum);
|
|
fRTPSocket->changeDestinationParameters(destAddr, destPort, destTTL);
|
|
}
|
|
if (fRTCPSocket != NULL && !isSSM() && !fMultiplexRTCPWithRTP) {
|
|
// Note: For SSM sessions, the dest address for RTCP was already set.
|
|
Port destPort(serverPortNum+1);
|
|
fRTCPSocket->changeDestinationParameters(destAddr, destPort, destTTL);
|
|
}
|
|
}
|
|
|
|
void MediaSubsession::setSessionId(char const* sessionId) {
|
|
delete[] fSessionId;
|
|
fSessionId = strDup(sessionId);
|
|
}
|
|
|
|
double MediaSubsession::getNormalPlayTime(struct timeval const& presentationTime) {
|
|
if (rtpSource() == NULL || rtpSource()->timestampFrequency() == 0) return 0.0; // no RTP source, or bad freq!
|
|
|
|
// First, check whether our "RTPSource" object has already been synchronized using RTCP.
|
|
// If it hasn't, then - as a special case - we need to use the RTP timestamp to compute the NPT.
|
|
if (!rtpSource()->hasBeenSynchronizedUsingRTCP()) {
|
|
if (!rtpInfo.infoIsNew) return 0.0; // the "rtpInfo" structure has not been filled in
|
|
u_int32_t timestampOffset = rtpSource()->curPacketRTPTimestamp() - rtpInfo.timestamp;
|
|
double nptOffset = (timestampOffset/(double)(rtpSource()->timestampFrequency()))*scale();
|
|
double npt = playStartTime() + nptOffset;
|
|
|
|
return npt;
|
|
} else {
|
|
// Common case: We have been synchronized using RTCP. This means that the "presentationTime" parameter
|
|
// will be accurate, and so we should use this to compute the NPT.
|
|
double ptsDouble = (double)(presentationTime.tv_sec + presentationTime.tv_usec/1000000.0);
|
|
|
|
if (rtpInfo.infoIsNew) {
|
|
// This is the first time we've been called with a synchronized presentation time since the "rtpInfo"
|
|
// structure was last filled in. Use this "presentationTime" to compute "fNPT_PTS_Offset":
|
|
if (seqNumLT(rtpSource()->curPacketRTPSeqNum(), rtpInfo.seqNum)) return -0.1; // sanity check; ignore old packets
|
|
u_int32_t timestampOffset = rtpSource()->curPacketRTPTimestamp() - rtpInfo.timestamp;
|
|
double nptOffset = (timestampOffset/(double)(rtpSource()->timestampFrequency()))*scale();
|
|
double npt = playStartTime() + nptOffset;
|
|
fNPT_PTS_Offset = npt - ptsDouble*scale();
|
|
rtpInfo.infoIsNew = False; // for next time
|
|
|
|
return npt;
|
|
} else {
|
|
// Use the precomputed "fNPT_PTS_Offset" to compute the NPT from the PTS:
|
|
if (fNPT_PTS_Offset == 0.0) return 0.0; // error: The "rtpInfo" structure was apparently never filled in
|
|
return (double)(ptsDouble*scale() + fNPT_PTS_Offset);
|
|
}
|
|
}
|
|
}
|
|
|
|
void MediaSubsession
|
|
::setAttribute(char const* name, char const* value, Boolean valueIsHexadecimal) {
|
|
// Replace any existing attribute record with this name (except that the 'valueIsHexadecimal'
|
|
// property will be inherited from it, if it exists).
|
|
SDPAttribute* oldAttr = (SDPAttribute*)fAttributeTable->Lookup(name);
|
|
if (oldAttr != NULL) {
|
|
valueIsHexadecimal = oldAttr->valueIsHexadecimal();
|
|
fAttributeTable->Remove(name);
|
|
delete oldAttr;
|
|
}
|
|
|
|
SDPAttribute* newAttr = new SDPAttribute(value, valueIsHexadecimal);
|
|
(void)fAttributeTable->Add(name, newAttr);
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPLine_c(char const* sdpLine) {
|
|
// Check for "c=IN IP4 <connection-endpoint>"
|
|
// or "c=IN IP4 <connection-endpoint>/<ttl+numAddresses>"
|
|
// (Later, do something with <ttl+numAddresses> also #####)
|
|
char* connectionEndpointName = parseCLine(sdpLine);
|
|
if (connectionEndpointName != NULL) {
|
|
delete[] fConnectionEndpointName;
|
|
fConnectionEndpointName = connectionEndpointName;
|
|
return True;
|
|
}
|
|
|
|
return False;
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPLine_b(char const* sdpLine) {
|
|
// Check for "b=<bwtype>:<bandwidth>" line
|
|
// RTP applications are expected to use bwtype="AS"
|
|
return sscanf(sdpLine, "b=AS:%u", &fBandwidth) == 1;
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPAttribute_rtpmap(char const* sdpLine) {
|
|
// Check for a "a=rtpmap:<fmt> <codec>/<freq>" line:
|
|
// (Also check without the "/<freq>"; RealNetworks omits this)
|
|
// Also check for a trailing "/<numChannels>".
|
|
Boolean parseSuccess = False;
|
|
|
|
unsigned rtpmapPayloadFormat;
|
|
char* codecName = strDupSize(sdpLine); // ensures we have enough space
|
|
unsigned rtpTimestampFrequency = 0;
|
|
unsigned numChannels = 1;
|
|
if (sscanf(sdpLine, "a=rtpmap: %u %[^/]/%u/%u",
|
|
&rtpmapPayloadFormat, codecName, &rtpTimestampFrequency,
|
|
&numChannels) == 4
|
|
|| sscanf(sdpLine, "a=rtpmap: %u %[^/]/%u",
|
|
&rtpmapPayloadFormat, codecName, &rtpTimestampFrequency) == 3
|
|
|| sscanf(sdpLine, "a=rtpmap: %u %s",
|
|
&rtpmapPayloadFormat, codecName) == 2) {
|
|
parseSuccess = True;
|
|
if (rtpmapPayloadFormat == fRTPPayloadFormat) {
|
|
// This "rtpmap" matches our payload format, so set our
|
|
// codec name and timestamp frequency:
|
|
// (First, make sure the codec name is upper case)
|
|
{
|
|
Locale l("POSIX");
|
|
for (char* p = codecName; *p != '\0'; ++p) *p = toupper(*p);
|
|
}
|
|
delete[] fCodecName; fCodecName = strDup(codecName);
|
|
fRTPTimestampFrequency = rtpTimestampFrequency;
|
|
fNumChannels = numChannels;
|
|
}
|
|
}
|
|
delete[] codecName;
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPAttribute_rtcpmux(char const* sdpLine) {
|
|
if (strncmp(sdpLine, "a=rtcp-mux", 10) == 0) {
|
|
fMultiplexRTCPWithRTP = True;
|
|
return True;
|
|
}
|
|
|
|
return False;
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPAttribute_control(char const* sdpLine) {
|
|
// Check for a "a=control:<control-path>" line:
|
|
Boolean parseSuccess = False;
|
|
|
|
char* controlPath = strDupSize(sdpLine); // ensures we have enough space
|
|
if (sscanf(sdpLine, "a=control: %s", controlPath) == 1) {
|
|
parseSuccess = True;
|
|
delete[] fControlPath; fControlPath = strDup(controlPath);
|
|
}
|
|
delete[] controlPath;
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPAttribute_range(char const* sdpLine) {
|
|
// Check for a "a=range:npt=<startTime>-<endTime>" line:
|
|
// (Later handle other kinds of "a=range" attributes also???#####)
|
|
Boolean parseSuccess = False;
|
|
|
|
double playStartTime;
|
|
double playEndTime;
|
|
if (parseRangeAttribute(sdpLine, playStartTime, playEndTime)) {
|
|
parseSuccess = True;
|
|
if (playStartTime > fPlayStartTime) {
|
|
fPlayStartTime = playStartTime;
|
|
if (playStartTime > fParent.playStartTime()) {
|
|
fParent.playStartTime() = playStartTime;
|
|
}
|
|
}
|
|
if (playEndTime > fPlayEndTime) {
|
|
fPlayEndTime = playEndTime;
|
|
if (playEndTime > fParent.playEndTime()) {
|
|
fParent.playEndTime() = playEndTime;
|
|
}
|
|
}
|
|
} else if (parseRangeAttribute(sdpLine, _absStartTime(), _absEndTime())) {
|
|
parseSuccess = True;
|
|
}
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPAttribute_fmtp(char const* sdpLine) {
|
|
// Check for a "a=fmtp:" line:
|
|
// Later: Check that payload format number matches; #####
|
|
do {
|
|
if (strncmp(sdpLine, "a=fmtp:", 7) != 0) break; sdpLine += 7;
|
|
while (isdigit(*sdpLine)) ++sdpLine;
|
|
|
|
// The remaining "sdpLine" should be a sequence of
|
|
// <name>=<value>;
|
|
// or
|
|
// <name>;
|
|
// parameter assignments. Look at each of these.
|
|
unsigned const sdpLineLen = strlen(sdpLine);
|
|
char* nameStr = new char[sdpLineLen+1];
|
|
char* valueStr = new char[sdpLineLen+1];
|
|
|
|
while (*sdpLine != '\0' && *sdpLine != '\r' && *sdpLine != '\n') {
|
|
int sscanfResult = sscanf(sdpLine, " %[^=; \t\r\n] = %[^; \t\r\n]", nameStr, valueStr);
|
|
if (sscanfResult >= 1) {
|
|
// <name> or <name>=<value>
|
|
// Convert <name> to lower-case, to ease comparison:
|
|
Locale l("POSIX");
|
|
for (char* c = nameStr; *c != '\0'; ++c) *c = tolower(*c);
|
|
|
|
if (sscanfResult == 1) {
|
|
// <name>
|
|
setAttribute(nameStr);
|
|
} else {
|
|
// <name>=<value>
|
|
setAttribute(nameStr, valueStr);
|
|
}
|
|
}
|
|
|
|
// Move to the next parameter assignment string:
|
|
while (*sdpLine != '\0' && *sdpLine != '\r' && *sdpLine != '\n' && *sdpLine != ';') ++sdpLine;
|
|
while (*sdpLine == ';') ++sdpLine;
|
|
}
|
|
delete[] nameStr; delete[] valueStr;
|
|
return True;
|
|
} while (0);
|
|
|
|
return False;
|
|
}
|
|
|
|
Boolean MediaSubsession
|
|
::parseSDPAttribute_source_filter(char const* sdpLine) {
|
|
return parseSourceFilterAttribute(sdpLine, fSourceFilterAddr);
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPAttribute_x_dimensions(char const* sdpLine) {
|
|
// Check for a "a=x-dimensions:<width>,<height>" line:
|
|
Boolean parseSuccess = False;
|
|
|
|
int width, height;
|
|
if (sscanf(sdpLine, "a=x-dimensions:%d,%d", &width, &height) == 2) {
|
|
parseSuccess = True;
|
|
fVideoWidth = (unsigned short)width;
|
|
fVideoHeight = (unsigned short)height;
|
|
}
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
Boolean MediaSubsession::parseSDPAttribute_framerate(char const* sdpLine) {
|
|
// Check for a "a=framerate: <fps>" or "a=x-framerate: <fps>" line:
|
|
Boolean parseSuccess = False;
|
|
|
|
float frate;
|
|
int rate;
|
|
if (sscanf(sdpLine, "a=framerate: %f", &frate) == 1 || sscanf(sdpLine, "a=framerate:%f", &frate) == 1) {
|
|
parseSuccess = True;
|
|
fVideoFPS = (unsigned)frate;
|
|
} else if (sscanf(sdpLine, "a=x-framerate: %d", &rate) == 1) {
|
|
parseSuccess = True;
|
|
fVideoFPS = (unsigned)rate;
|
|
}
|
|
|
|
return parseSuccess;
|
|
}
|
|
|
|
Boolean MediaSubsession::createSourceObjects(int useSpecialRTPoffset) {
|
|
// do {
|
|
// // First, check "fProtocolName"
|
|
// if (strcmp(fProtocolName, "UDP") == 0) {
|
|
// // A UDP-packetized stream (*not* a RTP stream)
|
|
// fReadSource = BasicUDPSource::createNew(env(), fRTPSocket);
|
|
// fRTPSource = NULL; // Note!
|
|
//
|
|
// if (strcmp(fCodecName, "MP2T") == 0) { // MPEG-2 Transport Stream
|
|
// fReadSource = MPEG2TransportStreamFramer::createNew(env(), fReadSource);
|
|
// // this sets "durationInMicroseconds" correctly, based on the PCR values
|
|
// }
|
|
// } else {
|
|
// // Check "fCodecName" against the set of codecs that we support,
|
|
// // and create our RTP source accordingly
|
|
// // (Later make this code more efficient, as this set grows #####)
|
|
// // (Also, add more fmts that can be implemented by SimpleRTPSource#####)
|
|
// Boolean createSimpleRTPSource = False; // by default; can be changed below
|
|
// Boolean doNormalMBitRule = False; // default behavior if "createSimpleRTPSource" is True
|
|
// if (strcmp(fCodecName, "QCELP") == 0) { // QCELP audio
|
|
// fReadSource =
|
|
// QCELPAudioRTPSource::createNew(env(), fRTPSocket, fRTPSource,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// // Note that fReadSource will differ from fRTPSource in this case
|
|
// } else if (strcmp(fCodecName, "AMR") == 0) { // AMR audio (narrowband)
|
|
// fReadSource =
|
|
// AMRAudioRTPSource::createNew(env(), fRTPSocket, fRTPSource,
|
|
// fRTPPayloadFormat, False /*isWideband*/,
|
|
// fNumChannels, attrVal_bool("octet-align"),
|
|
// attrVal_unsigned("interleaving"),
|
|
// attrVal_bool("robust-sorting"),
|
|
// attrVal_bool("crc"));
|
|
// // Note that fReadSource will differ from fRTPSource in this case
|
|
// } else if (strcmp(fCodecName, "AMR-WB") == 0) { // AMR audio (wideband)
|
|
// fReadSource =
|
|
// AMRAudioRTPSource::createNew(env(), fRTPSocket, fRTPSource,
|
|
// fRTPPayloadFormat, True /*isWideband*/,
|
|
// fNumChannels, attrVal_bool("octet-align"),
|
|
// attrVal_unsigned("interleaving"),
|
|
// attrVal_bool("robust-sorting"),
|
|
// attrVal_bool("crc"));
|
|
// // Note that fReadSource will differ from fRTPSource in this case
|
|
// } else if (strcmp(fCodecName, "MPA") == 0) { // MPEG-1 or 2 audio
|
|
// fReadSource = fRTPSource
|
|
// = MPEG1or2AudioRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "MPA-ROBUST") == 0) { // robust MP3 audio
|
|
// fReadSource = fRTPSource
|
|
// = MP3ADURTPSource::createNew(env(), fRTPSocket, fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// if (fRTPSource == NULL) break;
|
|
//
|
|
// if (!fReceiveRawMP3ADUs) {
|
|
// // Add a filter that deinterleaves the ADUs after depacketizing them:
|
|
// MP3ADUdeinterleaver* deinterleaver
|
|
// = MP3ADUdeinterleaver::createNew(env(), fRTPSource);
|
|
// if (deinterleaver == NULL) break;
|
|
//
|
|
// // Add another filter that converts these ADUs to MP3 frames:
|
|
// fReadSource = MP3FromADUSource::createNew(env(), deinterleaver);
|
|
// }
|
|
// } else if (strcmp(fCodecName, "X-MP3-DRAFT-00") == 0) {
|
|
// // a non-standard variant of "MPA-ROBUST" used by RealNetworks
|
|
// // (one 'ADU'ized MP3 frame per packet; no headers)
|
|
// fRTPSource
|
|
// = SimpleRTPSource::createNew(env(), fRTPSocket, fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency,
|
|
// "audio/MPA-ROBUST" /*hack*/);
|
|
// if (fRTPSource == NULL) break;
|
|
//
|
|
// // Add a filter that converts these ADUs to MP3 frames:
|
|
// fReadSource = MP3FromADUSource::createNew(env(), fRTPSource,
|
|
// False /*no ADU header*/);
|
|
// } else if (strcmp(fCodecName, "MP4A-LATM") == 0) { // MPEG-4 LATM audio
|
|
// fReadSource = fRTPSource
|
|
// = MPEG4LATMAudioRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "VORBIS") == 0) { // Vorbis audio
|
|
// fReadSource = fRTPSource
|
|
// = VorbisAudioRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "THEORA") == 0) { // Theora video
|
|
// fReadSource = fRTPSource
|
|
// = TheoraVideoRTPSource::createNew(env(), fRTPSocket, fRTPPayloadFormat);
|
|
// } else if (strcmp(fCodecName, "VP8") == 0) { // VP8 video
|
|
// fReadSource = fRTPSource
|
|
// = VP8VideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "VP9") == 0) { // VP9 video
|
|
// fReadSource = fRTPSource
|
|
// = VP9VideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "AC3") == 0 || strcmp(fCodecName, "EAC3") == 0) { // AC3 audio
|
|
// fReadSource = fRTPSource
|
|
// = AC3AudioRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "MP4V-ES") == 0) { // MPEG-4 Elementary Stream video
|
|
// fReadSource = fRTPSource
|
|
// = MPEG4ESVideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "MPEG4-GENERIC") == 0) {
|
|
// fReadSource = fRTPSource
|
|
// = MPEG4GenericRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency,
|
|
// fMediumName, attrVal_strToLower("mode"),
|
|
// attrVal_unsigned("sizelength"),
|
|
// attrVal_unsigned("indexlength"),
|
|
// attrVal_unsigned("indexdeltalength"));
|
|
// } else if (strcmp(fCodecName, "MPV") == 0) { // MPEG-1 or 2 video
|
|
// fReadSource = fRTPSource
|
|
// = MPEG1or2VideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "MP2T") == 0) { // MPEG-2 Transport Stream
|
|
// fRTPSource = SimpleRTPSource::createNew(env(), fRTPSocket, fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency, "video/MP2T",
|
|
// 0, False);
|
|
// fReadSource = MPEG2TransportStreamFramer::createNew(env(), fRTPSource);
|
|
// // this sets "durationInMicroseconds" correctly, based on the PCR values
|
|
// } else if (strcmp(fCodecName, "H261") == 0) { // H.261
|
|
// fReadSource = fRTPSource
|
|
// = H261VideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "H263-1998") == 0 ||
|
|
// strcmp(fCodecName, "H263-2000") == 0) { // H.263+
|
|
// fReadSource = fRTPSource
|
|
// = H263plusVideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "H264") == 0) {
|
|
// fReadSource = fRTPSource
|
|
// = H264VideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "H265") == 0) {
|
|
// Boolean expectDONFields = attrVal_unsigned("sprop-depack-buf-nalus") > 0;
|
|
// fReadSource = fRTPSource
|
|
// = H265VideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// expectDONFields,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "DV") == 0) {
|
|
// fReadSource = fRTPSource
|
|
// = DVVideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency);
|
|
// } else if (strcmp(fCodecName, "JPEG") == 0) { // motion JPEG
|
|
// if (fReceiveRawJPEGFrames) {
|
|
// // Special case (used when proxying JPEG/RTP streams): Receive each JPEG/RTP packet, including the special RTP headers:
|
|
// fReadSource = fRTPSource
|
|
// = SimpleRTPSource::createNew(env(), fRTPSocket, fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency, "video/JPEG",
|
|
// 0/*special offset*/, False/*doNormalMBitRule => ignore the 'M' bit*/);
|
|
// } else {
|
|
// // Normal case: Receive each JPEG frame as a complete, displayable JPEG image:
|
|
// fReadSource = fRTPSource
|
|
// = JPEGVideoRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency,
|
|
// videoWidth(),
|
|
// videoHeight());
|
|
// }
|
|
// } else if (strcmp(fCodecName, "X-QT") == 0
|
|
// || strcmp(fCodecName, "X-QUICKTIME") == 0) {
|
|
// // Generic QuickTime streams, as defined in
|
|
// // <http://developer.apple.com/quicktime/icefloe/dispatch026.html>
|
|
// char* mimeType
|
|
// = new char[strlen(mediumName()) + strlen(codecName()) + 2] ;
|
|
// sprintf(mimeType, "%s/%s", mediumName(), codecName());
|
|
// fReadSource = fRTPSource
|
|
// = QuickTimeGenericRTPSource::createNew(env(), fRTPSocket,
|
|
// fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency,
|
|
// mimeType);
|
|
// delete[] mimeType;
|
|
// } else if ( strcmp(fCodecName, "PCMU") == 0 // PCM u-law audio
|
|
// || strcmp(fCodecName, "GSM") == 0 // GSM audio
|
|
// || strcmp(fCodecName, "DVI4") == 0 // DVI4 (IMA ADPCM) audio
|
|
// || strcmp(fCodecName, "PCMA") == 0 // PCM a-law audio
|
|
// || strcmp(fCodecName, "MP1S") == 0 // MPEG-1 System Stream
|
|
// || strcmp(fCodecName, "MP2P") == 0 // MPEG-2 Program Stream
|
|
// || strcmp(fCodecName, "L8") == 0 // 8-bit linear audio
|
|
// || strcmp(fCodecName, "L16") == 0 // 16-bit linear audio
|
|
// || strcmp(fCodecName, "L20") == 0 // 20-bit linear audio (RFC 3190)
|
|
// || strcmp(fCodecName, "L24") == 0 // 24-bit linear audio (RFC 3190)
|
|
// || strcmp(fCodecName, "G722") == 0 // G.722 audio (RFC 3551)
|
|
// || strcmp(fCodecName, "G726-16") == 0 // G.726, 16 kbps
|
|
// || strcmp(fCodecName, "G726-24") == 0 // G.726, 24 kbps
|
|
// || strcmp(fCodecName, "G726-32") == 0 // G.726, 32 kbps
|
|
// || strcmp(fCodecName, "G726-40") == 0 // G.726, 40 kbps
|
|
// || strcmp(fCodecName, "SPEEX") == 0 // SPEEX audio
|
|
// || strcmp(fCodecName, "ILBC") == 0 // iLBC audio
|
|
// || strcmp(fCodecName, "OPUS") == 0 // Opus audio
|
|
// || strcmp(fCodecName, "T140") == 0 // T.140 text (RFC 4103)
|
|
// || strcmp(fCodecName, "DAT12") == 0 // 12-bit nonlinear audio (RFC 3190)
|
|
// || strcmp(fCodecName, "VND.ONVIF.METADATA") == 0 // 'ONVIF' 'metadata' (a XML document)
|
|
// ) {
|
|
// createSimpleRTPSource = True;
|
|
// useSpecialRTPoffset = 0;
|
|
// if (strcmp(fCodecName, "VND.ONVIF.METADATA") == 0) {
|
|
// // This RTP payload format uses the RTP "M" bit to indicate the end of the content (a XML document):
|
|
// doNormalMBitRule = True;
|
|
// }
|
|
// } else if (useSpecialRTPoffset >= 0) {
|
|
// // We don't know this RTP payload format, but try to receive
|
|
// // it using a 'SimpleRTPSource' with the specified header offset:
|
|
// createSimpleRTPSource = True;
|
|
// } else {
|
|
// env().setResultMsg("RTP payload format unknown or not supported");
|
|
// break;
|
|
// }
|
|
//
|
|
// if (createSimpleRTPSource) {
|
|
// char* mimeType
|
|
// = new char[strlen(mediumName()) + strlen(codecName()) + 2] ;
|
|
// sprintf(mimeType, "%s/%s", mediumName(), codecName());
|
|
// fReadSource = fRTPSource
|
|
// = SimpleRTPSource::createNew(env(), fRTPSocket, fRTPPayloadFormat,
|
|
// fRTPTimestampFrequency, mimeType,
|
|
// (unsigned)useSpecialRTPoffset,
|
|
// doNormalMBitRule);
|
|
// delete[] mimeType;
|
|
// }
|
|
// }
|
|
//
|
|
// return True;
|
|
// } while (0);
|
|
|
|
return False; // an error occurred
|
|
}
|
|
|
|
|
|
////////// SDPAttribute implementation //////////
|
|
|
|
SDPAttribute::SDPAttribute(char const* strValue, Boolean valueIsHexadecimal)
|
|
: fStrValue(strDup(strValue)), fStrValueToLower(NULL), fValueIsHexadecimal(valueIsHexadecimal) {
|
|
if (fStrValue == NULL) {
|
|
// No value was given for this attribute, so consider it to be a Boolean, with value True:
|
|
fIntValue = 1;
|
|
} else {
|
|
// Create a 'tolower' version of "fStrValue", in case it's needed:
|
|
Locale l("POSIX");
|
|
size_t strSize;
|
|
|
|
fStrValueToLower = strDupSize(fStrValue, strSize);
|
|
for (unsigned i = 0; i < strSize-1; ++i) fStrValueToLower[i] = tolower(fStrValue[i]);
|
|
fStrValueToLower[strSize-1] = '\0';
|
|
|
|
// Try to parse "fStrValueToLower" as an integer. If we can't, assume an integer value of 0:
|
|
if (sscanf(fStrValueToLower, valueIsHexadecimal ? "%x" : "%d", &fIntValue) != 1) {
|
|
fIntValue = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
SDPAttribute::~SDPAttribute() {
|
|
delete[] fStrValue;
|
|
delete[] fStrValueToLower;
|
|
}
|