2009 lines
82 KiB
C++
Executable File
2009 lines
82 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 generic RTSP client
|
|
// Implementation
|
|
|
|
#include "RTSPClient.hh"
|
|
#include "RTSPCommon.hh"
|
|
#include "Base64.hh"
|
|
#include "Locale.hh"
|
|
#include <GroupsockHelper.hh>
|
|
#include "ourMD5.hh"
|
|
|
|
////////// RTSPClient implementation //////////
|
|
|
|
RTSPClient* RTSPClient::createNew(UsageEnvironment& env, char const* rtspURL,
|
|
int verbosityLevel,
|
|
char const* applicationName,
|
|
portNumBits tunnelOverHTTPPortNum,
|
|
int socketNumToServer) {
|
|
return new RTSPClient(env, rtspURL,
|
|
verbosityLevel, applicationName, tunnelOverHTTPPortNum, socketNumToServer);
|
|
}
|
|
|
|
unsigned RTSPClient::sendDescribeCommand(responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "DESCRIBE", responseHandler));
|
|
}
|
|
|
|
unsigned RTSPClient::sendOptionsCommand(responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (authenticator != NULL) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "OPTIONS", responseHandler));
|
|
}
|
|
|
|
unsigned RTSPClient::sendAnnounceCommand(char const* sdpDescription, responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "ANNOUNCE", responseHandler, NULL, NULL, False, 0.0, 0.0, 0.0, sdpDescription));
|
|
}
|
|
|
|
unsigned RTSPClient::sendSetupCommand(MediaSubsession& subsession, responseHandler* responseHandler,
|
|
Boolean streamOutgoing, Boolean streamUsingTCP, Boolean forceMulticastOnUnspecified,
|
|
Authenticator* authenticator) {
|
|
if (fTunnelOverHTTPPortNum != 0) streamUsingTCP = True; // RTSP-over-HTTP tunneling uses TCP (by definition)
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
|
|
u_int32_t booleanFlags = 0;
|
|
if (streamUsingTCP) booleanFlags |= 0x1;
|
|
if (streamOutgoing) booleanFlags |= 0x2;
|
|
if (forceMulticastOnUnspecified) booleanFlags |= 0x4;
|
|
return sendRequest(new RequestRecord(++fCSeq, "SETUP", responseHandler, NULL, &subsession, booleanFlags));
|
|
}
|
|
|
|
unsigned RTSPClient::sendPlayCommand(MediaSession& session, responseHandler* responseHandler,
|
|
double start, double end, float scale,
|
|
Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
sendDummyUDPPackets(session); // hack to improve NAT traversal
|
|
return sendRequest(new RequestRecord(++fCSeq, "PLAY", responseHandler, &session, NULL, 0, start, end, scale));
|
|
}
|
|
|
|
unsigned RTSPClient::sendPlayCommand(MediaSubsession& subsession, responseHandler* responseHandler,
|
|
double start, double end, float scale,
|
|
Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
sendDummyUDPPackets(subsession); // hack to improve NAT traversal
|
|
return sendRequest(new RequestRecord(++fCSeq, "PLAY", responseHandler, NULL, &subsession, 0, start, end, scale));
|
|
}
|
|
|
|
unsigned RTSPClient::sendPlayCommand(MediaSession& session, responseHandler* responseHandler,
|
|
char const* absStartTime, char const* absEndTime, float scale,
|
|
Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
sendDummyUDPPackets(session); // hack to improve NAT traversal
|
|
return sendRequest(new RequestRecord(++fCSeq, responseHandler, absStartTime, absEndTime, scale, &session, NULL));
|
|
}
|
|
|
|
unsigned RTSPClient::sendPlayCommand(MediaSubsession& subsession, responseHandler* responseHandler,
|
|
char const* absStartTime, char const* absEndTime, float scale,
|
|
Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
sendDummyUDPPackets(subsession); // hack to improve NAT traversal
|
|
return sendRequest(new RequestRecord(++fCSeq, responseHandler, absStartTime, absEndTime, scale, NULL, &subsession));
|
|
}
|
|
|
|
unsigned RTSPClient::sendPauseCommand(MediaSession& session, responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "PAUSE", responseHandler, &session));
|
|
}
|
|
|
|
unsigned RTSPClient::sendPauseCommand(MediaSubsession& subsession, responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "PAUSE", responseHandler, NULL, &subsession));
|
|
}
|
|
|
|
unsigned RTSPClient::sendRecordCommand(MediaSession& session, responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "RECORD", responseHandler, &session));
|
|
}
|
|
|
|
unsigned RTSPClient::sendRecordCommand(MediaSubsession& subsession, responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "RECORD", responseHandler, NULL, &subsession));
|
|
}
|
|
|
|
unsigned RTSPClient::sendTeardownCommand(MediaSession& session, responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "TEARDOWN", responseHandler, &session));
|
|
}
|
|
|
|
unsigned RTSPClient::sendTeardownCommand(MediaSubsession& subsession, responseHandler* responseHandler, Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
return sendRequest(new RequestRecord(++fCSeq, "TEARDOWN", responseHandler, NULL, &subsession));
|
|
}
|
|
|
|
unsigned RTSPClient::sendSetParameterCommand(MediaSession& session, responseHandler* responseHandler,
|
|
char const* parameterName, char const* parameterValue,
|
|
Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
char* paramString = new char[strlen(parameterName) + strlen(parameterValue) + 10];
|
|
sprintf(paramString, "%s: %s\r\n", parameterName, parameterValue);
|
|
unsigned result = sendRequest(new RequestRecord(++fCSeq, "SET_PARAMETER", responseHandler, &session, NULL, False, 0.0, 0.0, 0.0, paramString));
|
|
delete[] paramString;
|
|
return result;
|
|
}
|
|
|
|
unsigned RTSPClient::sendGetParameterCommand(MediaSession& session, responseHandler* responseHandler, char const* parameterName,
|
|
Authenticator* authenticator) {
|
|
if (fCurrentAuthenticator < authenticator) fCurrentAuthenticator = *authenticator;
|
|
|
|
// We assume that:
|
|
// parameterName is NULL means: Send no body in the request.
|
|
// parameterName is "" means: Send only \r\n in the request body.
|
|
// parameterName is non-empty means: Send "<parameterName>\r\n" as the request body.
|
|
unsigned parameterNameLen = parameterName == NULL ? 0 : strlen(parameterName);
|
|
char* paramString = new char[parameterNameLen + 3]; // the 3 is for \r\n + the '\0' byte
|
|
if (parameterName == NULL) {
|
|
paramString[0] = '\0';
|
|
} else {
|
|
sprintf(paramString, "%s\r\n", parameterName);
|
|
}
|
|
unsigned result = sendRequest(new RequestRecord(++fCSeq, "GET_PARAMETER", responseHandler, &session, NULL, False, 0.0, 0.0, 0.0, paramString));
|
|
delete[] paramString;
|
|
return result;
|
|
}
|
|
|
|
void RTSPClient::sendDummyUDPPackets(MediaSession& session, unsigned numDummyPackets) {
|
|
MediaSubsessionIterator iter(session);
|
|
MediaSubsession* subsession;
|
|
|
|
while ((subsession = iter.next()) != NULL) {
|
|
sendDummyUDPPackets(*subsession, numDummyPackets);
|
|
}
|
|
}
|
|
|
|
void RTSPClient::sendDummyUDPPackets(MediaSubsession& subsession, unsigned numDummyPackets) {
|
|
// Hack: To increase the likelihood of UDP packets from the server reaching us,
|
|
// if we're behind a NAT, send a few 'dummy' UDP packets to the server now.
|
|
// (We do this on both our RTP port and our RTCP port.)
|
|
Groupsock* gs1 = NULL; Groupsock* gs2 = NULL;
|
|
if (subsession.rtpSource() != NULL) gs1 = subsession.rtpSource()->RTPgs();
|
|
if (subsession.rtcpInstance() != NULL) gs2 = subsession.rtcpInstance()->RTCPgs();
|
|
u_int32_t const dummy = 0xFEEDFACE;
|
|
for (unsigned i = 0; i < numDummyPackets; ++i) {
|
|
if (gs1 != NULL) gs1->output(envir(), (unsigned char*)&dummy, sizeof dummy);
|
|
if (gs2 != NULL) gs2->output(envir(), (unsigned char*)&dummy, sizeof dummy);
|
|
}
|
|
}
|
|
|
|
void RTSPClient::setSpeed(MediaSession& session, float speed) {
|
|
// Optionally set download speed for session to be used later on PLAY command:
|
|
// The user should call this function after the MediaSession is instantiated, but before the
|
|
// first "sendPlayCommand()" is called.
|
|
if (&session != NULL) {
|
|
session.speed() = speed;
|
|
MediaSubsessionIterator iter(session);
|
|
MediaSubsession* subsession;
|
|
|
|
while ((subsession = iter.next()) != NULL) {
|
|
subsession->speed() = speed;
|
|
}
|
|
}
|
|
}
|
|
|
|
Boolean RTSPClient::changeResponseHandler(unsigned cseq, responseHandler* newResponseHandler) {
|
|
// Look for the matching request record in each of our 'pending requests' queues:
|
|
RequestRecord* request;
|
|
if ((request = fRequestsAwaitingConnection.findByCSeq(cseq)) != NULL
|
|
|| (request = fRequestsAwaitingHTTPTunneling.findByCSeq(cseq)) != NULL
|
|
|| (request = fRequestsAwaitingResponse.findByCSeq(cseq)) != NULL) {
|
|
request->handler() = newResponseHandler;
|
|
return True;
|
|
}
|
|
|
|
return False;
|
|
}
|
|
|
|
Boolean RTSPClient::lookupByName(UsageEnvironment& env,
|
|
char const* instanceName,
|
|
RTSPClient*& resultClient) {
|
|
resultClient = NULL; // unless we succeed
|
|
|
|
Medium* medium;
|
|
if (!Medium::lookupByName(env, instanceName, medium)) return False;
|
|
|
|
if (!medium->isRTSPClient()) {
|
|
env.setResultMsg(instanceName, " is not a RTSP client");
|
|
return False;
|
|
}
|
|
|
|
resultClient = (RTSPClient*)medium;
|
|
return True;
|
|
}
|
|
|
|
static void copyUsernameOrPasswordStringFromURL(char* dest, char const* src, unsigned len) {
|
|
// Normally, we just copy from the source to the destination. However, if the source contains
|
|
// %-encoded characters, then we decode them while doing the copy:
|
|
while (len > 0) {
|
|
int nBefore = 0;
|
|
int nAfter = 0;
|
|
|
|
if (*src == '%' && len >= 3 && sscanf(src+1, "%n%2hhx%n", &nBefore, dest, &nAfter) == 1) {
|
|
unsigned codeSize = nAfter - nBefore; // should be 1 or 2
|
|
|
|
++dest;
|
|
src += (1 + codeSize);
|
|
len -= (1 + codeSize);
|
|
} else {
|
|
*dest++ = *src++;
|
|
--len;
|
|
}
|
|
}
|
|
*dest = '\0';
|
|
}
|
|
|
|
Boolean RTSPClient::parseRTSPURL(UsageEnvironment& env, char const* url,
|
|
char*& username, char*& password,
|
|
NetAddress& address,
|
|
portNumBits& portNum,
|
|
char const** urlSuffix) {
|
|
do {
|
|
// Parse the URL as "rtsp://[<username>[:<password>]@]<server-address-or-name>[:<port>][/<stream-name>]"
|
|
char const* prefix = "rtsp://";
|
|
unsigned const prefixLength = 7;
|
|
if (_strncasecmp(url, prefix, prefixLength) != 0) {
|
|
env.setResultMsg("URL is not of the form \"", prefix, "\"");
|
|
break;
|
|
}
|
|
|
|
unsigned const parseBufferSize = 100;
|
|
char parseBuffer[parseBufferSize];
|
|
char const* from = &url[prefixLength];
|
|
|
|
// Check whether "<username>[:<password>]@" occurs next.
|
|
// We do this by checking whether '@' appears before the end of the URL, or before the first '/'.
|
|
username = password = NULL; // default return values
|
|
char const* colonPasswordStart = NULL;
|
|
char const* p;
|
|
for (p = from; *p != '\0' && *p != '/'; ++p) {
|
|
if (*p == ':' && colonPasswordStart == NULL) {
|
|
colonPasswordStart = p;
|
|
} else if (*p == '@') {
|
|
// We found <username> (and perhaps <password>). Copy them into newly-allocated result strings:
|
|
if (colonPasswordStart == NULL) colonPasswordStart = p;
|
|
|
|
char const* usernameStart = from;
|
|
unsigned usernameLen = colonPasswordStart - usernameStart;
|
|
username = new char[usernameLen + 1] ; // allow for the trailing '\0'
|
|
copyUsernameOrPasswordStringFromURL(username, usernameStart, usernameLen);
|
|
|
|
char const* passwordStart = colonPasswordStart;
|
|
if (passwordStart < p) ++passwordStart; // skip over the ':'
|
|
unsigned passwordLen = p - passwordStart;
|
|
password = new char[passwordLen + 1]; // allow for the trailing '\0'
|
|
copyUsernameOrPasswordStringFromURL(password, passwordStart, passwordLen);
|
|
|
|
from = p + 1; // skip over the '@'
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Next, parse <server-address-or-name>
|
|
char* to = &parseBuffer[0];
|
|
unsigned i;
|
|
for (i = 0; i < parseBufferSize; ++i) {
|
|
if (*from == '\0' || *from == ':' || *from == '/') {
|
|
// We've completed parsing the address
|
|
*to = '\0';
|
|
break;
|
|
}
|
|
*to++ = *from++;
|
|
}
|
|
if (i == parseBufferSize) {
|
|
env.setResultMsg("URL is too long");
|
|
break;
|
|
}
|
|
|
|
NetAddressList addresses(parseBuffer);
|
|
if (addresses.numAddresses() == 0) {
|
|
env.setResultMsg("Failed to find network address for \"",
|
|
parseBuffer, "\"");
|
|
break;
|
|
}
|
|
address = *(addresses.firstAddress());
|
|
|
|
portNum = 554; // default value
|
|
char nextChar = *from;
|
|
if (nextChar == ':') {
|
|
int portNumInt;
|
|
if (sscanf(++from, "%d", &portNumInt) != 1) {
|
|
env.setResultMsg("No port number follows ':'");
|
|
break;
|
|
}
|
|
if (portNumInt < 1 || portNumInt > 65535) {
|
|
env.setResultMsg("Bad port number");
|
|
break;
|
|
}
|
|
portNum = (portNumBits)portNumInt;
|
|
while (*from >= '0' && *from <= '9') ++from; // skip over port number
|
|
}
|
|
|
|
// The remainder of the URL is the suffix:
|
|
if (urlSuffix != NULL) *urlSuffix = from;
|
|
|
|
return True;
|
|
} while (0);
|
|
|
|
return False;
|
|
}
|
|
|
|
void RTSPClient::setUserAgentString(char const* userAgentName) {
|
|
if (userAgentName == NULL) return;
|
|
|
|
// Change the existing user agent header string:
|
|
char const* const formatStr = "User-Agent: %s\r\n";
|
|
unsigned const headerSize = strlen(formatStr) + strlen(userAgentName);
|
|
delete[] fUserAgentHeaderStr;
|
|
fUserAgentHeaderStr = new char[headerSize];
|
|
sprintf(fUserAgentHeaderStr, formatStr, userAgentName);
|
|
fUserAgentHeaderStrLen = strlen(fUserAgentHeaderStr);
|
|
}
|
|
|
|
unsigned RTSPClient::responseBufferSize = 20000; // default value; you can reassign this in your application if you need to
|
|
|
|
RTSPClient::RTSPClient(UsageEnvironment& env, char const* rtspURL,
|
|
int verbosityLevel, char const* applicationName,
|
|
portNumBits tunnelOverHTTPPortNum, int socketNumToServer)
|
|
: Medium(env),
|
|
desiredMaxIncomingPacketSize(0), fVerbosityLevel(verbosityLevel), fCSeq(1),
|
|
fAllowBasicAuthentication(True), fServerAddress(0),
|
|
fTunnelOverHTTPPortNum(tunnelOverHTTPPortNum),
|
|
fUserAgentHeaderStr(NULL), fUserAgentHeaderStrLen(0),
|
|
fInputSocketNum(-1), fOutputSocketNum(-1), fBaseURL(NULL), fTCPStreamIdCount(0),
|
|
fLastSessionId(NULL), fSessionTimeoutParameter(0), fSessionCookieCounter(0), fHTTPTunnelingConnectionIsPending(False) {
|
|
setBaseURL(rtspURL);
|
|
|
|
fResponseBuffer = new char[responseBufferSize+1];
|
|
resetResponseBuffer();
|
|
|
|
if (socketNumToServer >= 0) {
|
|
// This socket number is (assumed to be) already connected to the server.
|
|
// Use it, and arrange to handle responses to requests sent on it:
|
|
fInputSocketNum = fOutputSocketNum = socketNumToServer;
|
|
envir().taskScheduler().setBackgroundHandling(fInputSocketNum, SOCKET_READABLE|SOCKET_EXCEPTION,
|
|
(TaskScheduler::BackgroundHandlerProc*)&incomingDataHandler, this);
|
|
}
|
|
|
|
// Set the "User-Agent:" header to use in each request:
|
|
char const* const libName = "LIVE555 Streaming Media v";
|
|
char const* const libVersionStr = LIVEMEDIA_LIBRARY_VERSION_STRING;
|
|
char const* libPrefix; char const* libSuffix;
|
|
if (applicationName == NULL || applicationName[0] == '\0') {
|
|
applicationName = libPrefix = libSuffix = "";
|
|
} else {
|
|
libPrefix = " (";
|
|
libSuffix = ")";
|
|
}
|
|
unsigned userAgentNameSize
|
|
= strlen(applicationName) + strlen(libPrefix) + strlen(libName) + strlen(libVersionStr) + strlen(libSuffix) + 1;
|
|
char* userAgentName = new char[userAgentNameSize];
|
|
sprintf(userAgentName, "%s%s%s%s%s", applicationName, libPrefix, libName, libVersionStr, libSuffix);
|
|
setUserAgentString(userAgentName);
|
|
delete[] userAgentName;
|
|
}
|
|
|
|
RTSPClient::~RTSPClient() {
|
|
RTPInterface::clearServerRequestAlternativeByteHandler(envir(), fInputSocketNum); // in case we were receiving RTP-over-TCP
|
|
reset();
|
|
|
|
delete[] fResponseBuffer;
|
|
delete[] fUserAgentHeaderStr;
|
|
}
|
|
|
|
void RTSPClient::reset() {
|
|
resetTCPSockets();
|
|
resetResponseBuffer();
|
|
fServerAddress = 0;
|
|
|
|
setBaseURL(NULL);
|
|
|
|
fCurrentAuthenticator.reset();
|
|
|
|
delete[] fLastSessionId; fLastSessionId = NULL;
|
|
}
|
|
|
|
void RTSPClient::setBaseURL(char const* url) {
|
|
delete[] fBaseURL; fBaseURL = strDup(url);
|
|
}
|
|
|
|
int RTSPClient::grabSocket() {
|
|
int inputSocket = fInputSocketNum;
|
|
fInputSocketNum = -1;
|
|
|
|
return inputSocket;
|
|
}
|
|
|
|
unsigned RTSPClient::sendRequest(RequestRecord* request) {
|
|
char* cmd = NULL;
|
|
do {
|
|
Boolean connectionIsPending = False;
|
|
if (!fRequestsAwaitingConnection.isEmpty()) {
|
|
// A connection is currently pending (with at least one enqueued request). Enqueue this request also:
|
|
connectionIsPending = True;
|
|
} else if (fInputSocketNum < 0) { // we need to open a connection
|
|
int connectResult = openConnection();
|
|
if (connectResult < 0) break; // an error occurred
|
|
else if (connectResult == 0) {
|
|
// A connection is pending
|
|
connectionIsPending = True;
|
|
} // else the connection succeeded. Continue sending the command.
|
|
}
|
|
if (connectionIsPending) {
|
|
fRequestsAwaitingConnection.enqueue(request);
|
|
return request->cseq();
|
|
}
|
|
|
|
// If requested (and we're not already doing it, or have done it), set up the special protocol for tunneling RTSP-over-HTTP:
|
|
if (fTunnelOverHTTPPortNum != 0 && strcmp(request->commandName(), "GET") != 0 && fOutputSocketNum == fInputSocketNum) {
|
|
if (!setupHTTPTunneling1()) break;
|
|
fRequestsAwaitingHTTPTunneling.enqueue(request);
|
|
return request->cseq();
|
|
}
|
|
|
|
// Construct and send the command:
|
|
|
|
// First, construct command-specific headers that we need:
|
|
|
|
char* cmdURL = fBaseURL; // by default
|
|
Boolean cmdURLWasAllocated = False;
|
|
|
|
char const* protocolStr = "RTSP/1.0"; // by default
|
|
|
|
char* extraHeaders = (char*)""; // by default
|
|
Boolean extraHeadersWereAllocated = False;
|
|
|
|
char* contentLengthHeader = (char*)""; // by default
|
|
Boolean contentLengthHeaderWasAllocated = False;
|
|
|
|
if (!setRequestFields(request,
|
|
cmdURL, cmdURLWasAllocated,
|
|
protocolStr,
|
|
extraHeaders, extraHeadersWereAllocated)) {
|
|
break;
|
|
}
|
|
|
|
char const* contentStr = request->contentStr(); // by default
|
|
if (contentStr == NULL) contentStr = "";
|
|
unsigned contentStrLen = strlen(contentStr);
|
|
if (contentStrLen > 0) {
|
|
char const* contentLengthHeaderFmt =
|
|
"Content-Length: %d\r\n";
|
|
unsigned contentLengthHeaderSize = strlen(contentLengthHeaderFmt)
|
|
+ 20 /* max int len */;
|
|
contentLengthHeader = new char[contentLengthHeaderSize];
|
|
sprintf(contentLengthHeader, contentLengthHeaderFmt, contentStrLen);
|
|
contentLengthHeaderWasAllocated = True;
|
|
}
|
|
|
|
char* authenticatorStr = createAuthenticatorString(request->commandName(), fBaseURL);
|
|
|
|
char const* const cmdFmt =
|
|
"%s %s %s\r\n"
|
|
"CSeq: %d\r\n"
|
|
"%s"
|
|
"%s"
|
|
"%s"
|
|
"%s"
|
|
"\r\n"
|
|
"%s";
|
|
unsigned cmdSize = strlen(cmdFmt)
|
|
+ strlen(request->commandName()) + strlen(cmdURL) + strlen(protocolStr)
|
|
+ 20 /* max int len */
|
|
+ strlen(authenticatorStr)
|
|
+ fUserAgentHeaderStrLen
|
|
+ strlen(extraHeaders)
|
|
+ strlen(contentLengthHeader)
|
|
+ contentStrLen;
|
|
cmd = new char[cmdSize];
|
|
sprintf(cmd, cmdFmt,
|
|
request->commandName(), cmdURL, protocolStr,
|
|
request->cseq(),
|
|
authenticatorStr,
|
|
fUserAgentHeaderStr,
|
|
extraHeaders,
|
|
contentLengthHeader,
|
|
contentStr);
|
|
delete[] authenticatorStr;
|
|
if (cmdURLWasAllocated) delete[] cmdURL;
|
|
if (extraHeadersWereAllocated) delete[] extraHeaders;
|
|
if (contentLengthHeaderWasAllocated) delete[] contentLengthHeader;
|
|
|
|
if (fVerbosityLevel >= 1) envir() << "Sending request: " << cmd << "\n";
|
|
|
|
if (fTunnelOverHTTPPortNum != 0 && strcmp(request->commandName(), "GET") != 0 && strcmp(request->commandName(), "POST") != 0) {
|
|
// When we're tunneling RTSP-over-HTTP, we Base-64-encode the request before we send it.
|
|
// (However, we don't do this for the HTTP "GET" and "POST" commands that we use to set up the tunnel.)
|
|
char* origCmd = cmd;
|
|
cmd = base64Encode(origCmd, strlen(cmd));
|
|
if (fVerbosityLevel >= 1) envir() << "\tThe request was base-64 encoded to: " << cmd << "\n\n";
|
|
delete[] origCmd;
|
|
}
|
|
|
|
if (send(fOutputSocketNum, cmd, strlen(cmd), 0) < 0) {
|
|
char const* errFmt = "%s send() failed: ";
|
|
unsigned const errLength = strlen(errFmt) + strlen(request->commandName());
|
|
char* err = new char[errLength];
|
|
sprintf(err, errFmt, request->commandName());
|
|
envir().setResultErrMsg(err);
|
|
delete[] err;
|
|
break;
|
|
}
|
|
|
|
// The command send succeeded, so enqueue the request record, so that its response (when it comes) can be handled.
|
|
// However, note that we do not expect a response to a POST command with RTSP-over-HTTP, so don't enqueue that.
|
|
int cseq = request->cseq();
|
|
|
|
if (fTunnelOverHTTPPortNum == 0 || strcmp(request->commandName(), "POST") != 0) {
|
|
fRequestsAwaitingResponse.enqueue(request);
|
|
} else {
|
|
delete request;
|
|
}
|
|
|
|
delete[] cmd;
|
|
return cseq;
|
|
} while (0);
|
|
|
|
// An error occurred, so call the response handler immediately (indicating the error):
|
|
delete[] cmd;
|
|
handleRequestError(request);
|
|
delete request;
|
|
return 0;
|
|
}
|
|
|
|
static char* createSessionString(char const* sessionId) {
|
|
char* sessionStr;
|
|
if (sessionId != NULL) {
|
|
sessionStr = new char[20+strlen(sessionId)];
|
|
sprintf(sessionStr, "Session: %s\r\n", sessionId);
|
|
} else {
|
|
sessionStr = strDup("");
|
|
}
|
|
return sessionStr;
|
|
}
|
|
|
|
// Add support for faster download thru "speed:" option on PLAY
|
|
static char* createSpeedString(float speed) {
|
|
char buf[100];
|
|
if (speed == 1.0f ) {
|
|
// This is the default value; we don't need a "Speed:" header:
|
|
buf[0] = '\0';
|
|
} else {
|
|
sprintf(buf, "Speed: %.3f\r\n",speed);
|
|
}
|
|
|
|
return strDup(buf);
|
|
}
|
|
|
|
static char* createScaleString(float scale, float currentScale) {
|
|
char buf[100];
|
|
if (scale == 1.0f && currentScale == 1.0f) {
|
|
// This is the default value; we don't need a "Scale:" header:
|
|
buf[0] = '\0';
|
|
} else {
|
|
Locale l("C", Numeric);
|
|
sprintf(buf, "Scale: %f\r\n", scale);
|
|
}
|
|
|
|
return strDup(buf);
|
|
}
|
|
|
|
static char* createRangeString(double start, double end, char const* absStartTime, char const* absEndTime) {
|
|
char buf[100];
|
|
|
|
if (absStartTime != NULL) {
|
|
// Create a "Range:" header that specifies 'absolute' time values:
|
|
|
|
if (absEndTime == NULL) {
|
|
// There's no end time:
|
|
snprintf(buf, sizeof buf, "Range: clock=%s-\r\n", absStartTime);
|
|
} else {
|
|
// There's both a start and an end time; include them both in the "Range:" hdr
|
|
snprintf(buf, sizeof buf, "Range: clock=%s-%s\r\n", absStartTime, absEndTime);
|
|
}
|
|
} else {
|
|
// Create a "Range:" header that specifies relative (i.e., NPT) time values:
|
|
|
|
if (start < 0) {
|
|
// We're resuming from a PAUSE; there's no "Range:" header at all
|
|
buf[0] = '\0';
|
|
} else if (end < 0) {
|
|
// There's no end time:
|
|
Locale l("C", Numeric);
|
|
sprintf(buf, "Range: npt=%.3f-\r\n", start);
|
|
} else {
|
|
// There's both a start and an end time; include them both in the "Range:" hdr
|
|
Locale l("C", Numeric);
|
|
sprintf(buf, "Range: npt=%.3f-%.3f\r\n", start, end);
|
|
}
|
|
}
|
|
|
|
return strDup(buf);
|
|
}
|
|
|
|
Boolean RTSPClient::setRequestFields(RequestRecord* request,
|
|
char*& cmdURL, Boolean& cmdURLWasAllocated,
|
|
char const*& protocolStr,
|
|
char*& extraHeaders, Boolean& extraHeadersWereAllocated
|
|
) {
|
|
// Set various fields that will appear in our outgoing request, depending upon the particular command that we are sending.
|
|
|
|
if (strcmp(request->commandName(), "DESCRIBE") == 0) {
|
|
extraHeaders = (char*)"Accept: application/sdp\r\n";
|
|
} else if (strcmp(request->commandName(), "OPTIONS") == 0) {
|
|
// If we're currently part of a session, create a "Session:" header (in case the server wants this to indicate
|
|
// client 'liveness); this makes up our 'extra headers':
|
|
extraHeaders = createSessionString(fLastSessionId);
|
|
extraHeadersWereAllocated = True;
|
|
} else if (strcmp(request->commandName(), "ANNOUNCE") == 0) {
|
|
extraHeaders = (char*)"Content-Type: application/sdp\r\n";
|
|
} else if (strcmp(request->commandName(), "SETUP") == 0) {
|
|
MediaSubsession& subsession = *request->subsession();
|
|
Boolean streamUsingTCP = (request->booleanFlags()&0x1) != 0;
|
|
Boolean streamOutgoing = (request->booleanFlags()&0x2) != 0;
|
|
Boolean forceMulticastOnUnspecified = (request->booleanFlags()&0x4) != 0;
|
|
|
|
char const *prefix, *separator, *suffix;
|
|
constructSubsessionURL(subsession, prefix, separator, suffix);
|
|
|
|
char const* transportFmt;
|
|
if (strcmp(subsession.protocolName(), "UDP") == 0) {
|
|
suffix = "";
|
|
transportFmt = "Transport: RAW/RAW/UDP%s%s%s=%d-%d\r\n";
|
|
} else {
|
|
transportFmt = "Transport: RTP/AVP%s%s%s=%d-%d\r\n";
|
|
}
|
|
|
|
cmdURL = new char[strlen(prefix) + strlen(separator) + strlen(suffix) + 1];
|
|
cmdURLWasAllocated = True;
|
|
sprintf(cmdURL, "%s%s%s", prefix, separator, suffix);
|
|
|
|
// Construct a "Transport:" header.
|
|
char const* transportTypeStr;
|
|
char const* modeStr = streamOutgoing ? ";mode=receive" : "";
|
|
// Note: I think the above is nonstandard, but DSS wants it this way
|
|
char const* portTypeStr;
|
|
portNumBits rtpNumber, rtcpNumber;
|
|
if (streamUsingTCP) { // streaming over the RTSP connection
|
|
transportTypeStr = "/TCP;unicast";
|
|
portTypeStr = ";interleaved";
|
|
rtpNumber = fTCPStreamIdCount++;
|
|
rtcpNumber = fTCPStreamIdCount++;
|
|
} else { // normal RTP streaming
|
|
unsigned connectionAddress = subsession.connectionEndpointAddress();
|
|
Boolean requestMulticastStreaming
|
|
= IsMulticastAddress(connectionAddress) || (connectionAddress == 0 && forceMulticastOnUnspecified);
|
|
transportTypeStr = requestMulticastStreaming ? ";multicast" : ";unicast";
|
|
portTypeStr = requestMulticastStreaming ? ";port" : ";client_port";
|
|
rtpNumber = subsession.clientPortNum();
|
|
if (rtpNumber == 0) {
|
|
envir().setResultMsg("Client port number unknown\n");
|
|
delete[] cmdURL;
|
|
return False;
|
|
}
|
|
rtcpNumber = subsession.rtcpIsMuxed() ? rtpNumber : rtpNumber + 1;
|
|
}
|
|
unsigned transportSize = strlen(transportFmt)
|
|
+ strlen(transportTypeStr) + strlen(modeStr) + strlen(portTypeStr) + 2*5 /* max port len */;
|
|
char* transportStr = new char[transportSize];
|
|
sprintf(transportStr, transportFmt,
|
|
transportTypeStr, modeStr, portTypeStr, rtpNumber, rtcpNumber);
|
|
|
|
// When sending more than one "SETUP" request, include a "Session:" header in the 2nd and later commands:
|
|
char* sessionStr = createSessionString(fLastSessionId);
|
|
|
|
// Optionally include a "Blocksize:" string:
|
|
char* blocksizeStr = createBlocksizeString(streamUsingTCP);
|
|
|
|
// The "Transport:" and "Session:" (if present) and "Blocksize:" (if present) headers
|
|
// make up the 'extra headers':
|
|
extraHeaders = new char[transportSize + strlen(sessionStr) + strlen(blocksizeStr)];
|
|
extraHeadersWereAllocated = True;
|
|
sprintf(extraHeaders, "%s%s%s", transportStr, sessionStr, blocksizeStr);
|
|
delete[] transportStr; delete[] sessionStr; delete[] blocksizeStr;
|
|
} else if (strcmp(request->commandName(), "GET") == 0 || strcmp(request->commandName(), "POST") == 0) {
|
|
// We will be sending a HTTP (not a RTSP) request.
|
|
// Begin by re-parsing our RTSP URL, to get the stream name (which we'll use as our 'cmdURL'
|
|
// in the subsequent request), and the server address (which we'll use in a "Host:" header):
|
|
char* username;
|
|
char* password;
|
|
NetAddress destAddress;
|
|
portNumBits urlPortNum;
|
|
if (!parseRTSPURL(envir(), fBaseURL, username, password, destAddress, urlPortNum, (char const**)&cmdURL)) return False;
|
|
if (cmdURL[0] == '\0') cmdURL = (char*)"/";
|
|
delete[] username;
|
|
delete[] password;
|
|
netAddressBits serverAddress = *(netAddressBits*)(destAddress.data());
|
|
AddressString serverAddressString(serverAddress);
|
|
|
|
protocolStr = "HTTP/1.1";
|
|
|
|
if (strcmp(request->commandName(), "GET") == 0) {
|
|
// Create a 'session cookie' string, using MD5:
|
|
struct {
|
|
struct timeval timestamp;
|
|
unsigned counter;
|
|
} seedData;
|
|
gettimeofday(&seedData.timestamp, NULL);
|
|
seedData.counter = ++fSessionCookieCounter;
|
|
our_MD5Data((unsigned char*)(&seedData), sizeof seedData, fSessionCookie);
|
|
// DSS seems to require that the 'session cookie' string be 22 bytes long:
|
|
fSessionCookie[23] = '\0';
|
|
|
|
char const* const extraHeadersFmt =
|
|
"Host: %s\r\n"
|
|
"x-sessioncookie: %s\r\n"
|
|
"Accept: application/x-rtsp-tunnelled\r\n"
|
|
"Pragma: no-cache\r\n"
|
|
"Cache-Control: no-cache\r\n";
|
|
unsigned extraHeadersSize = strlen(extraHeadersFmt)
|
|
+ strlen(serverAddressString.val())
|
|
+ strlen(fSessionCookie);
|
|
extraHeaders = new char[extraHeadersSize];
|
|
extraHeadersWereAllocated = True;
|
|
sprintf(extraHeaders, extraHeadersFmt,
|
|
serverAddressString.val(),
|
|
fSessionCookie);
|
|
} else { // "POST"
|
|
char const* const extraHeadersFmt =
|
|
"Host: %s\r\n"
|
|
"x-sessioncookie: %s\r\n"
|
|
"Content-Type: application/x-rtsp-tunnelled\r\n"
|
|
"Pragma: no-cache\r\n"
|
|
"Cache-Control: no-cache\r\n"
|
|
"Content-Length: 32767\r\n"
|
|
"Expires: Sun, 9 Jan 1972 00:00:00 GMT\r\n";
|
|
unsigned extraHeadersSize = strlen(extraHeadersFmt)
|
|
+ strlen(serverAddressString.val())
|
|
+ strlen(fSessionCookie);
|
|
extraHeaders = new char[extraHeadersSize];
|
|
extraHeadersWereAllocated = True;
|
|
sprintf(extraHeaders, extraHeadersFmt,
|
|
serverAddressString.val(),
|
|
fSessionCookie);
|
|
}
|
|
} else { // "PLAY", "PAUSE", "TEARDOWN", "RECORD", "SET_PARAMETER", "GET_PARAMETER"
|
|
// First, make sure that we have a RTSP session in progress
|
|
if (fLastSessionId == NULL) {
|
|
envir().setResultMsg("No RTSP session is currently in progress\n");
|
|
return False;
|
|
}
|
|
|
|
char const* sessionId;
|
|
float originalScale;
|
|
if (request->session() != NULL) {
|
|
// Session-level operation
|
|
cmdURL = (char*)sessionURL(*request->session());
|
|
|
|
sessionId = fLastSessionId;
|
|
originalScale = request->session()->scale();
|
|
} else {
|
|
// Media-level operation
|
|
char const *prefix, *separator, *suffix;
|
|
constructSubsessionURL(*request->subsession(), prefix, separator, suffix);
|
|
cmdURL = new char[strlen(prefix) + strlen(separator) + strlen(suffix) + 1];
|
|
cmdURLWasAllocated = True;
|
|
sprintf(cmdURL, "%s%s%s", prefix, separator, suffix);
|
|
|
|
sessionId = request->subsession()->sessionId();
|
|
originalScale = request->subsession()->scale();
|
|
}
|
|
|
|
if (strcmp(request->commandName(), "PLAY") == 0) {
|
|
// Create possible "Session:", "Scale:", "Speed:", and "Range:" headers;
|
|
// these make up the 'extra headers':
|
|
char* sessionStr = createSessionString(sessionId);
|
|
char* scaleStr = createScaleString(request->scale(), originalScale);
|
|
float speed = request->session() != NULL ? request->session()->speed() : request->subsession()->speed();
|
|
char* speedStr = createSpeedString(speed);
|
|
char* rangeStr = createRangeString(request->start(), request->end(), request->absStartTime(), request->absEndTime());
|
|
extraHeaders = new char[strlen(sessionStr) + strlen(scaleStr) + strlen(speedStr) + strlen(rangeStr) + 1];
|
|
extraHeadersWereAllocated = True;
|
|
sprintf(extraHeaders, "%s%s%s%s", sessionStr, scaleStr, speedStr, rangeStr);
|
|
delete[] sessionStr; delete[] scaleStr; delete[] speedStr; delete[] rangeStr;
|
|
} else {
|
|
// Create a "Session:" header; this makes up our 'extra headers':
|
|
extraHeaders = createSessionString(sessionId);
|
|
extraHeadersWereAllocated = True;
|
|
}
|
|
}
|
|
|
|
return True;
|
|
}
|
|
|
|
Boolean RTSPClient::isRTSPClient() const {
|
|
return True;
|
|
}
|
|
|
|
void RTSPClient::resetTCPSockets() {
|
|
if (fInputSocketNum >= 0) {
|
|
envir().taskScheduler().disableBackgroundHandling(fInputSocketNum);
|
|
::closeSocket(fInputSocketNum);
|
|
if (fOutputSocketNum != fInputSocketNum) {
|
|
envir().taskScheduler().disableBackgroundHandling(fOutputSocketNum);
|
|
::closeSocket(fOutputSocketNum);
|
|
}
|
|
}
|
|
fInputSocketNum = fOutputSocketNum = -1;
|
|
}
|
|
|
|
void RTSPClient::resetResponseBuffer() {
|
|
fResponseBytesAlreadySeen = 0;
|
|
fResponseBufferBytesLeft = responseBufferSize;
|
|
}
|
|
|
|
int RTSPClient::openConnection() {
|
|
do {
|
|
// Set up a connection to the server. Begin by parsing the URL:
|
|
|
|
char* username;
|
|
char* password;
|
|
NetAddress destAddress;
|
|
portNumBits urlPortNum;
|
|
char const* urlSuffix;
|
|
if (!parseRTSPURL(envir(), fBaseURL, username, password, destAddress, urlPortNum, &urlSuffix)) break;
|
|
portNumBits destPortNum = fTunnelOverHTTPPortNum == 0 ? urlPortNum : fTunnelOverHTTPPortNum;
|
|
if (username != NULL || password != NULL) {
|
|
fCurrentAuthenticator.setUsernameAndPassword(username, password);
|
|
delete[] username;
|
|
delete[] password;
|
|
}
|
|
|
|
// We don't yet have a TCP socket (or we used to have one, but it got closed). Set it up now.
|
|
fInputSocketNum = fOutputSocketNum = setupStreamSocket(envir(), 0);
|
|
if (fInputSocketNum < 0) break;
|
|
ignoreSigPipeOnSocket(fInputSocketNum); // so that servers on the same host that get killed don't also kill us
|
|
|
|
// Connect to the remote endpoint:
|
|
fServerAddress = *(netAddressBits*)(destAddress.data());
|
|
int connectResult = connectToServer(fInputSocketNum, destPortNum);
|
|
if (connectResult < 0) break;
|
|
else if (connectResult > 0) {
|
|
// The connection succeeded. Arrange to handle responses to requests sent on it:
|
|
envir().taskScheduler().setBackgroundHandling(fInputSocketNum, SOCKET_READABLE|SOCKET_EXCEPTION,
|
|
(TaskScheduler::BackgroundHandlerProc*)&incomingDataHandler, this);
|
|
}
|
|
return connectResult;
|
|
} while (0);
|
|
|
|
resetTCPSockets();
|
|
return -1;
|
|
}
|
|
|
|
int RTSPClient::connectToServer(int socketNum, portNumBits remotePortNum) {
|
|
MAKE_SOCKADDR_IN(remoteName, fServerAddress, htons(remotePortNum));
|
|
if (fVerbosityLevel >= 1) {
|
|
envir() << "Opening connection to " << AddressString(remoteName).val() << ", port " << remotePortNum << "...\n";
|
|
}
|
|
if (connect(socketNum, (struct sockaddr*) &remoteName, sizeof remoteName) != 0) {
|
|
int const err = envir().getErrno();
|
|
if (err == EINPROGRESS || err == EWOULDBLOCK) {
|
|
// The connection is pending; we'll need to handle it later. Wait for our socket to be 'writable', or have an exception.
|
|
envir().taskScheduler().setBackgroundHandling(socketNum, SOCKET_WRITABLE|SOCKET_EXCEPTION,
|
|
(TaskScheduler::BackgroundHandlerProc*)&connectionHandler, this);
|
|
return 0;
|
|
}
|
|
envir().setResultErrMsg("connect() failed: ");
|
|
if (fVerbosityLevel >= 1) envir() << "..." << envir().getResultMsg() << "\n";
|
|
return -1;
|
|
}
|
|
if (fVerbosityLevel >= 1) envir() << "...local connection opened\n";
|
|
|
|
return 1;
|
|
}
|
|
|
|
char* RTSPClient::createAuthenticatorString(char const* cmd, char const* url) {
|
|
Authenticator& auth = fCurrentAuthenticator; // alias, for brevity
|
|
if (auth.realm() != NULL && auth.username() != NULL && auth.password() != NULL) {
|
|
// We have a filled-in authenticator, so use it:
|
|
char* authenticatorStr;
|
|
if (auth.nonce() != NULL) { // Digest authentication
|
|
char const* const authFmt =
|
|
"Authorization: Digest username=\"%s\", realm=\"%s\", "
|
|
"nonce=\"%s\", uri=\"%s\", response=\"%s\"\r\n";
|
|
char const* response = auth.computeDigestResponse(cmd, url);
|
|
unsigned authBufSize = strlen(authFmt)
|
|
+ strlen(auth.username()) + strlen(auth.realm())
|
|
+ strlen(auth.nonce()) + strlen(url) + strlen(response);
|
|
authenticatorStr = new char[authBufSize];
|
|
sprintf(authenticatorStr, authFmt,
|
|
auth.username(), auth.realm(),
|
|
auth.nonce(), url, response);
|
|
auth.reclaimDigestResponse(response);
|
|
} else { // Basic authentication
|
|
char const* const authFmt = "Authorization: Basic %s\r\n";
|
|
|
|
unsigned usernamePasswordLength = strlen(auth.username()) + 1 + strlen(auth.password());
|
|
char* usernamePassword = new char[usernamePasswordLength+1];
|
|
sprintf(usernamePassword, "%s:%s", auth.username(), auth.password());
|
|
|
|
char* response = base64Encode(usernamePassword, usernamePasswordLength);
|
|
unsigned const authBufSize = strlen(authFmt) + strlen(response) + 1;
|
|
authenticatorStr = new char[authBufSize];
|
|
sprintf(authenticatorStr, authFmt, response);
|
|
delete[] response; delete[] usernamePassword;
|
|
}
|
|
|
|
return authenticatorStr;
|
|
}
|
|
|
|
// We don't have a (filled-in) authenticator.
|
|
return strDup("");
|
|
}
|
|
|
|
char* RTSPClient::createBlocksizeString(Boolean streamUsingTCP) {
|
|
char* blocksizeStr;
|
|
u_int16_t maxPacketSize = desiredMaxIncomingPacketSize;
|
|
|
|
// Allow for the RTP header (if streaming over TCP)
|
|
// or the IP/UDP/RTP headers (if streaming over UDP):
|
|
u_int16_t const headerAllowance = streamUsingTCP ? 12 : 50/*conservative*/;
|
|
if (maxPacketSize < headerAllowance) {
|
|
maxPacketSize = 0;
|
|
} else {
|
|
maxPacketSize -= headerAllowance;
|
|
}
|
|
|
|
if (maxPacketSize > 0) {
|
|
blocksizeStr = new char[25]; // more than enough space
|
|
sprintf(blocksizeStr, "Blocksize: %u\r\n", maxPacketSize);
|
|
} else {
|
|
blocksizeStr = strDup("");
|
|
}
|
|
return blocksizeStr;
|
|
}
|
|
|
|
void RTSPClient::handleRequestError(RequestRecord* request) {
|
|
int resultCode = -envir().getErrno();
|
|
if (resultCode == 0) {
|
|
// Choose some generic error code instead:
|
|
#if defined(__WIN32__) || defined(_WIN32) || defined(_QNX4)
|
|
resultCode = -WSAENOTCONN;
|
|
#else
|
|
resultCode = -ENOTCONN;
|
|
#endif
|
|
}
|
|
if (request->handler() != NULL) (*request->handler())(this, resultCode, strDup(envir().getResultMsg()));
|
|
}
|
|
|
|
Boolean RTSPClient
|
|
::parseResponseCode(char const* line, unsigned& responseCode, char const*& responseString) {
|
|
if (sscanf(line, "RTSP/%*s%u", &responseCode) != 1 &&
|
|
sscanf(line, "HTTP/%*s%u", &responseCode) != 1) return False;
|
|
// Note: We check for HTTP responses as well as RTSP responses, both in order to setup RTSP-over-HTTP tunneling,
|
|
// and so that we get back a meaningful error if the client tried to mistakenly send a RTSP command to a HTTP-only server.
|
|
|
|
// Use everything after the RTSP/* (or HTTP/*) as the response string:
|
|
responseString = line;
|
|
while (responseString[0] != '\0' && responseString[0] != ' ' && responseString[0] != '\t') ++responseString;
|
|
while (responseString[0] != '\0' && (responseString[0] == ' ' || responseString[0] == '\t')) ++responseString; // skip whitespace
|
|
|
|
return True;
|
|
}
|
|
|
|
void RTSPClient::handleIncomingRequest() {
|
|
// Parse the request string into command name and 'CSeq', then 'handle' the command (by responding that we don't support it):
|
|
char cmdName[RTSP_PARAM_STRING_MAX];
|
|
char urlPreSuffix[RTSP_PARAM_STRING_MAX];
|
|
char urlSuffix[RTSP_PARAM_STRING_MAX];
|
|
char cseq[RTSP_PARAM_STRING_MAX];
|
|
char sessionId[RTSP_PARAM_STRING_MAX];
|
|
unsigned contentLength;
|
|
if (!parseRTSPRequestString(fResponseBuffer, fResponseBytesAlreadySeen,
|
|
cmdName, sizeof cmdName,
|
|
urlPreSuffix, sizeof urlPreSuffix,
|
|
urlSuffix, sizeof urlSuffix,
|
|
cseq, sizeof cseq,
|
|
sessionId, sizeof sessionId,
|
|
contentLength)) {
|
|
return;
|
|
} else {
|
|
if (fVerbosityLevel >= 1) {
|
|
envir() << "Received incoming RTSP request: " << fResponseBuffer << "\n";
|
|
}
|
|
char tmpBuf[2*RTSP_PARAM_STRING_MAX];
|
|
snprintf((char*)tmpBuf, sizeof tmpBuf,
|
|
"RTSP/1.0 405 Method Not Allowed\r\nCSeq: %s\r\n\r\n", cseq);
|
|
send(fOutputSocketNum, tmpBuf, strlen(tmpBuf), 0);
|
|
}
|
|
}
|
|
|
|
Boolean RTSPClient::checkForHeader(char const* line, char const* headerName, unsigned headerNameLength, char const*& headerParams) {
|
|
if (_strncasecmp(line, headerName, headerNameLength) != 0) return False;
|
|
|
|
// The line begins with the desired header name. Trim off any whitespace, and return the header parameters:
|
|
unsigned paramIndex = headerNameLength;
|
|
while (line[paramIndex] != '\0' && (line[paramIndex] == ' ' || line[paramIndex] == '\t')) ++paramIndex;
|
|
if (line[paramIndex] == '\0') return False; // the header is assumed to be bad if it has no parameters
|
|
|
|
headerParams = &line[paramIndex];
|
|
return True;
|
|
}
|
|
|
|
Boolean RTSPClient::parseTransportParams(char const* paramsStr,
|
|
char*& serverAddressStr, portNumBits& serverPortNum,
|
|
unsigned char& rtpChannelId, unsigned char& rtcpChannelId) {
|
|
// Initialize the return parameters to 'not found' values:
|
|
serverAddressStr = NULL;
|
|
serverPortNum = 0;
|
|
rtpChannelId = rtcpChannelId = 0xFF;
|
|
if (paramsStr == NULL) return False;
|
|
|
|
char* foundServerAddressStr = NULL;
|
|
Boolean foundServerPortNum = False;
|
|
portNumBits clientPortNum = 0;
|
|
Boolean foundClientPortNum = False;
|
|
Boolean foundChannelIds = False;
|
|
unsigned rtpCid, rtcpCid;
|
|
Boolean isMulticast = True; // by default
|
|
char* foundDestinationStr = NULL;
|
|
portNumBits multicastPortNumRTP, multicastPortNumRTCP;
|
|
Boolean foundMulticastPortNum = False;
|
|
|
|
// Run through each of the parameters, looking for ones that we handle:
|
|
char const* fields = paramsStr;
|
|
char* field = strDupSize(fields);
|
|
while (sscanf(fields, "%[^;]", field) == 1) {
|
|
if (sscanf(field, "server_port=%hu", &serverPortNum) == 1) {
|
|
foundServerPortNum = True;
|
|
} else if (sscanf(field, "client_port=%hu", &clientPortNum) == 1) {
|
|
foundClientPortNum = True;
|
|
} else if (_strncasecmp(field, "source=", 7) == 0) {
|
|
delete[] foundServerAddressStr;
|
|
foundServerAddressStr = strDup(field+7);
|
|
} else if (sscanf(field, "interleaved=%u-%u", &rtpCid, &rtcpCid) == 2) {
|
|
rtpChannelId = (unsigned char)rtpCid;
|
|
rtcpChannelId = (unsigned char)rtcpCid;
|
|
foundChannelIds = True;
|
|
} else if (strcmp(field, "unicast") == 0) {
|
|
isMulticast = False;
|
|
} else if (_strncasecmp(field, "destination=", 12) == 0) {
|
|
delete[] foundDestinationStr;
|
|
foundDestinationStr = strDup(field+12);
|
|
} else if (sscanf(field, "port=%hu-%hu", &multicastPortNumRTP, &multicastPortNumRTCP) == 2 ||
|
|
sscanf(field, "port=%hu", &multicastPortNumRTP) == 1) {
|
|
foundMulticastPortNum = True;
|
|
}
|
|
|
|
fields += strlen(field);
|
|
while (fields[0] == ';') ++fields; // skip over all leading ';' chars
|
|
if (fields[0] == '\0') break;
|
|
}
|
|
delete[] field;
|
|
|
|
// If we're multicast, and have a "destination=" (multicast) address, then use this
|
|
// as the 'server' address (because some weird servers don't specify the multicast
|
|
// address earlier, in the "DESCRIBE" response's SDP:
|
|
if (isMulticast && foundDestinationStr != NULL && foundMulticastPortNum) {
|
|
delete[] foundServerAddressStr;
|
|
serverAddressStr = foundDestinationStr;
|
|
serverPortNum = multicastPortNumRTP;
|
|
return True;
|
|
}
|
|
delete[] foundDestinationStr;
|
|
|
|
// We have a valid "Transport:" header if any of the following are true:
|
|
// - We saw a "interleaved=" field, indicating RTP/RTCP-over-TCP streaming, or
|
|
// - We saw a "server_port=" field, or
|
|
// - We saw a "client_port=" field.
|
|
// If we didn't also see a "server_port=" field, then the server port is assumed to be the same as the client port.
|
|
if (foundChannelIds || foundServerPortNum || foundClientPortNum) {
|
|
if (foundClientPortNum && !foundServerPortNum) {
|
|
serverPortNum = clientPortNum;
|
|
}
|
|
serverAddressStr = foundServerAddressStr;
|
|
return True;
|
|
}
|
|
|
|
delete[] foundServerAddressStr;
|
|
return False;
|
|
}
|
|
|
|
Boolean RTSPClient::parseScaleParam(char const* paramStr, float& scale) {
|
|
Locale l("C", Numeric);
|
|
return sscanf(paramStr, "%f", &scale) == 1;
|
|
}
|
|
|
|
Boolean RTSPClient::parseSpeedParam(char const* paramStr, float& speed) {
|
|
Locale l("C", Numeric);
|
|
return sscanf(paramStr, "%f", &speed) >= 1;
|
|
}
|
|
|
|
Boolean RTSPClient::parseRTPInfoParams(char const*& paramsStr, u_int16_t& seqNum, u_int32_t& timestamp) {
|
|
if (paramsStr == NULL || paramsStr[0] == '\0') return False;
|
|
while (paramsStr[0] == ',') ++paramsStr;
|
|
|
|
// "paramsStr" now consists of a ';'-separated list of parameters, ending with ',' or '\0'.
|
|
char* field = strDupSize(paramsStr);
|
|
|
|
Boolean sawSeq = False, sawRtptime = False;
|
|
while (sscanf(paramsStr, "%[^;,]", field) == 1) {
|
|
if (sscanf(field, "seq=%hu", &seqNum) == 1) {
|
|
sawSeq = True;
|
|
} else if (sscanf(field, "rtptime=%u", ×tamp) == 1) {
|
|
sawRtptime = True;
|
|
}
|
|
|
|
paramsStr += strlen(field);
|
|
if (paramsStr[0] == '\0' || paramsStr[0] == ',') break;
|
|
// ASSERT: paramsStr[0] == ';'
|
|
++paramsStr; // skip over the ';'
|
|
}
|
|
|
|
delete[] field;
|
|
// For the "RTP-Info:" parameters to be useful to us, we need to have seen both the "seq=" and "rtptime=" parameters:
|
|
return sawSeq && sawRtptime;
|
|
}
|
|
|
|
Boolean RTSPClient::handleSETUPResponse(MediaSubsession& subsession, char const* sessionParamsStr, char const* transportParamsStr,
|
|
Boolean streamUsingTCP) {
|
|
char* sessionId = new char[responseBufferSize]; // ensures we have enough space
|
|
Boolean success = False;
|
|
do {
|
|
// Check for a session id:
|
|
if (sessionParamsStr == NULL || sscanf(sessionParamsStr, "%[^;]", sessionId) != 1) {
|
|
envir().setResultMsg("Missing or bad \"Session:\" header");
|
|
break;
|
|
}
|
|
subsession.setSessionId(sessionId);
|
|
delete[] fLastSessionId; fLastSessionId = strDup(sessionId);
|
|
|
|
// Also look for an optional "; timeout = " parameter following this:
|
|
char const* afterSessionId = sessionParamsStr + strlen(sessionId);
|
|
int timeoutVal;
|
|
if (sscanf(afterSessionId, "; timeout = %d", &timeoutVal) == 1) {
|
|
fSessionTimeoutParameter = timeoutVal;
|
|
}
|
|
|
|
// Parse the "Transport:" header parameters:
|
|
char* serverAddressStr;
|
|
portNumBits serverPortNum;
|
|
unsigned char rtpChannelId, rtcpChannelId;
|
|
if (!parseTransportParams(transportParamsStr, serverAddressStr, serverPortNum, rtpChannelId, rtcpChannelId)) {
|
|
envir().setResultMsg("Missing or bad \"Transport:\" header");
|
|
break;
|
|
}
|
|
delete[] subsession.connectionEndpointName();
|
|
subsession.connectionEndpointName() = serverAddressStr;
|
|
subsession.serverPortNum = serverPortNum;
|
|
subsession.rtpChannelId = rtpChannelId;
|
|
subsession.rtcpChannelId = rtcpChannelId;
|
|
|
|
if (streamUsingTCP) {
|
|
// Tell the subsession to receive RTP (and send/receive RTCP) over the RTSP stream:
|
|
if (subsession.rtpSource() != NULL) {
|
|
subsession.rtpSource()->setStreamSocket(fInputSocketNum, subsession.rtpChannelId);
|
|
// So that we continue to receive & handle RTSP commands and responses from the server
|
|
subsession.rtpSource()->enableRTCPReports() = False;
|
|
// To avoid confusing the server (which won't start handling RTP/RTCP-over-TCP until "PLAY"), don't send RTCP "RR"s yet
|
|
}
|
|
if (subsession.rtcpInstance() != NULL) subsession.rtcpInstance()->setStreamSocket(fInputSocketNum, subsession.rtcpChannelId);
|
|
RTPInterface::setServerRequestAlternativeByteHandler(envir(), fInputSocketNum, handleAlternativeRequestByte, this);
|
|
} else {
|
|
// Normal case.
|
|
// Set the RTP and RTCP sockets' destination address and port from the information in the SETUP response (if present):
|
|
netAddressBits destAddress = subsession.connectionEndpointAddress();
|
|
if (destAddress == 0) destAddress = fServerAddress;
|
|
subsession.setDestinations(destAddress);
|
|
}
|
|
|
|
success = True;
|
|
} while (0);
|
|
|
|
delete[] sessionId;
|
|
return success;
|
|
}
|
|
|
|
Boolean RTSPClient::handlePLAYResponse(MediaSession& session, MediaSubsession& subsession,
|
|
char const* scaleParamsStr, char const* speedParamsStr,
|
|
char const* rangeParamsStr, char const* rtpInfoParamsStr) {
|
|
Boolean scaleOK = False, rangeOK = False, speedOK = False;
|
|
do {
|
|
if (&session != NULL) {
|
|
// The command was on the whole session
|
|
if (scaleParamsStr != NULL && !parseScaleParam(scaleParamsStr, session.scale())) break;
|
|
scaleOK = True;
|
|
if (speedParamsStr != NULL && !parseSpeedParam(speedParamsStr, session.speed())) break;
|
|
speedOK = True;
|
|
Boolean startTimeIsNow;
|
|
if (rangeParamsStr != NULL &&
|
|
!parseRangeParam(rangeParamsStr,
|
|
session.playStartTime(), session.playEndTime(),
|
|
session._absStartTime(), session._absEndTime(),
|
|
startTimeIsNow)) break;
|
|
rangeOK = True;
|
|
|
|
MediaSubsessionIterator iter(session);
|
|
MediaSubsession* subsession;
|
|
while ((subsession = iter.next()) != NULL) {
|
|
u_int16_t seqNum; u_int32_t timestamp;
|
|
subsession->rtpInfo.infoIsNew = False;
|
|
if (parseRTPInfoParams(rtpInfoParamsStr, seqNum, timestamp)) {
|
|
subsession->rtpInfo.seqNum = seqNum;
|
|
subsession->rtpInfo.timestamp = timestamp;
|
|
subsession->rtpInfo.infoIsNew = True;
|
|
}
|
|
|
|
if (subsession->rtpSource() != NULL) subsession->rtpSource()->enableRTCPReports() = True; // start sending RTCP "RR"s now
|
|
}
|
|
} else {
|
|
// The command was on a subsession
|
|
if (scaleParamsStr != NULL && !parseScaleParam(scaleParamsStr, subsession.scale())) break;
|
|
scaleOK = True;
|
|
if (speedParamsStr != NULL && !parseSpeedParam(speedParamsStr, session.speed())) break;
|
|
speedOK = True;
|
|
Boolean startTimeIsNow;
|
|
if (rangeParamsStr != NULL &&
|
|
!parseRangeParam(rangeParamsStr,
|
|
subsession._playStartTime(), subsession._playEndTime(),
|
|
subsession._absStartTime(), subsession._absEndTime(),
|
|
startTimeIsNow)) break;
|
|
rangeOK = True;
|
|
|
|
u_int16_t seqNum; u_int32_t timestamp;
|
|
subsession.rtpInfo.infoIsNew = False;
|
|
if (parseRTPInfoParams(rtpInfoParamsStr, seqNum, timestamp)) {
|
|
subsession.rtpInfo.seqNum = seqNum;
|
|
subsession.rtpInfo.timestamp = timestamp;
|
|
subsession.rtpInfo.infoIsNew = True;
|
|
}
|
|
|
|
if (subsession.rtpSource() != NULL) subsession.rtpSource()->enableRTCPReports() = True; // start sending RTCP "RR"s now
|
|
}
|
|
|
|
return True;
|
|
} while (0);
|
|
|
|
// An error occurred:
|
|
if (!scaleOK) {
|
|
envir().setResultMsg("Bad \"Scale:\" header");
|
|
} else if (!speedOK) {
|
|
envir().setResultMsg("Bad \"Speed:\" header");
|
|
} else if (!rangeOK) {
|
|
envir().setResultMsg("Bad \"Range:\" header");
|
|
} else {
|
|
envir().setResultMsg("Bad \"RTP-Info:\" header");
|
|
}
|
|
return False;
|
|
}
|
|
|
|
Boolean RTSPClient::handleTEARDOWNResponse(MediaSession& /*session*/, MediaSubsession& /*subsession*/) {
|
|
// Because we don't expect to always get a response to "TEARDOWN", we don't need to do anything if we do get one:
|
|
return True;
|
|
}
|
|
|
|
Boolean RTSPClient::handleGET_PARAMETERResponse(char const* parameterName, char*& resultValueString, char* resultValueStringEnd) {
|
|
do {
|
|
// If "parameterName" is non-empty, it may be (possibly followed by ':' and whitespace) at the start of the result string:
|
|
if (parameterName != NULL && parameterName[0] != '\0') {
|
|
if (parameterName[1] == '\0') break; // sanity check; there should have been \r\n at the end of "parameterName"
|
|
|
|
unsigned parameterNameLen = strlen(parameterName);
|
|
// ASSERT: parameterNameLen >= 2;
|
|
parameterNameLen -= 2; // because of the trailing \r\n
|
|
if (resultValueString + parameterNameLen > resultValueStringEnd) break; // not enough space
|
|
if (_strncasecmp(resultValueString, parameterName, parameterNameLen) == 0) {
|
|
resultValueString += parameterNameLen;
|
|
// ASSERT: resultValueString <= resultValueStringEnd
|
|
if (resultValueString == resultValueStringEnd) break;
|
|
|
|
if (resultValueString[0] == ':') ++resultValueString;
|
|
while (resultValueString < resultValueStringEnd
|
|
&& (resultValueString[0] == ' ' || resultValueString[0] == '\t')) {
|
|
++resultValueString;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The rest of "resultValueStr" should be our desired result, but first trim off any \r and/or \n characters at the end:
|
|
char saved = *resultValueStringEnd;
|
|
*resultValueStringEnd = '\0';
|
|
unsigned resultLen = strlen(resultValueString);
|
|
*resultValueStringEnd = saved;
|
|
|
|
while (resultLen > 0 && (resultValueString[resultLen-1] == '\r' || resultValueString[resultLen-1] == '\n')) --resultLen;
|
|
resultValueString[resultLen] = '\0';
|
|
|
|
return True;
|
|
} while (0);
|
|
|
|
// An error occurred:
|
|
envir().setResultMsg("Bad \"GET_PARAMETER\" response");
|
|
return False;
|
|
}
|
|
|
|
Boolean RTSPClient::handleAuthenticationFailure(char const* paramsStr) {
|
|
if (paramsStr == NULL) return False; // There was no "WWW-Authenticate:" header; we can't proceed.
|
|
|
|
// Fill in "fCurrentAuthenticator" with the information from the "WWW-Authenticate:" header:
|
|
Boolean realmHasChanged = False; // by default
|
|
Boolean isStale = False; // by default
|
|
char* realm = strDupSize(paramsStr);
|
|
char* nonce = strDupSize(paramsStr);
|
|
char* stale = strDupSize(paramsStr);
|
|
Boolean success = True;
|
|
if (sscanf(paramsStr, "Digest realm=\"%[^\"]\", nonce=\"%[^\"]\", stale=%[a-zA-Z]", realm, nonce, stale) == 3) {
|
|
realmHasChanged = fCurrentAuthenticator.realm() == NULL || strcmp(fCurrentAuthenticator.realm(), realm) != 0;
|
|
isStale = _strncasecmp(stale, "true", 4) == 0;
|
|
fCurrentAuthenticator.setRealmAndNonce(realm, nonce);
|
|
} else if (sscanf(paramsStr, "Digest realm=\"%[^\"]\", nonce=\"%[^\"]\"", realm, nonce) == 2) {
|
|
realmHasChanged = fCurrentAuthenticator.realm() == NULL || strcmp(fCurrentAuthenticator.realm(), realm) != 0;
|
|
fCurrentAuthenticator.setRealmAndNonce(realm, nonce);
|
|
} else if (sscanf(paramsStr, "Basic realm=\"%[^\"]\"", realm) == 1 && fAllowBasicAuthentication) {
|
|
realmHasChanged = fCurrentAuthenticator.realm() == NULL || strcmp(fCurrentAuthenticator.realm(), realm) != 0;
|
|
fCurrentAuthenticator.setRealmAndNonce(realm, NULL); // Basic authentication
|
|
} else {
|
|
success = False; // bad "WWW-Authenticate:" header
|
|
}
|
|
delete[] realm; delete[] nonce; delete[] stale;
|
|
|
|
if (success) {
|
|
if ((!realmHasChanged && !isStale) || fCurrentAuthenticator.username() == NULL || fCurrentAuthenticator.password() == NULL) {
|
|
// We already tried with the same realm (and a non-stale nonce),
|
|
// or don't have a username and/or password, so the new "WWW-Authenticate:" header
|
|
// information won't help us. We remain unauthenticated.
|
|
success = False;
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
Boolean RTSPClient::resendCommand(RequestRecord* request) {
|
|
if (fVerbosityLevel >= 1) envir() << "Resending...\n";
|
|
if (request != NULL && strcmp(request->commandName(), "GET") != 0) request->cseq() = ++fCSeq;
|
|
return sendRequest(request) != 0;
|
|
}
|
|
|
|
char const* RTSPClient::sessionURL(MediaSession const& session) const {
|
|
char const* url = session.controlPath();
|
|
if (url == NULL || strcmp(url, "*") == 0) url = fBaseURL;
|
|
|
|
return url;
|
|
}
|
|
|
|
void RTSPClient::handleAlternativeRequestByte(void* rtspClient, u_int8_t requestByte) {
|
|
((RTSPClient*)rtspClient)->handleAlternativeRequestByte1(requestByte);
|
|
}
|
|
|
|
void RTSPClient::handleAlternativeRequestByte1(u_int8_t requestByte) {
|
|
if (requestByte == 0xFF) {
|
|
// Hack: The new handler of the input TCP socket encountered an error reading it. Indicate this:
|
|
handleResponseBytes(-1);
|
|
} else if (requestByte == 0xFE) {
|
|
// Another hack: The new handler of the input TCP socket no longer needs it, so take back control:
|
|
envir().taskScheduler().setBackgroundHandling(fInputSocketNum, SOCKET_READABLE|SOCKET_EXCEPTION,
|
|
(TaskScheduler::BackgroundHandlerProc*)&incomingDataHandler, this);
|
|
} else {
|
|
// Normal case:
|
|
fResponseBuffer[fResponseBytesAlreadySeen] = requestByte;
|
|
handleResponseBytes(1);
|
|
}
|
|
}
|
|
|
|
static Boolean isAbsoluteURL(char const* url) {
|
|
// Assumption: "url" is absolute if it contains a ':', before any
|
|
// occurrence of '/'
|
|
while (*url != '\0' && *url != '/') {
|
|
if (*url == ':') return True;
|
|
++url;
|
|
}
|
|
|
|
return False;
|
|
}
|
|
|
|
void RTSPClient::constructSubsessionURL(MediaSubsession const& subsession,
|
|
char const*& prefix,
|
|
char const*& separator,
|
|
char const*& suffix) {
|
|
// Figure out what the URL describing "subsession" will look like.
|
|
// The URL is returned in three parts: prefix; separator; suffix
|
|
//##### NOTE: This code doesn't really do the right thing if "sessionURL()"
|
|
// doesn't end with a "/", and "subsession.controlPath()" is relative.
|
|
// The right thing would have been to truncate "sessionURL()" back to the
|
|
// rightmost "/", and then add "subsession.controlPath()".
|
|
// In practice, though, each "DESCRIBE" response typically contains
|
|
// a "Content-Base:" header that consists of "sessionURL()" followed by
|
|
// a "/", in which case this code ends up giving the correct result.
|
|
// However, we should really fix this code to do the right thing, and
|
|
// also check for and use the "Content-Base:" header appropriately. #####
|
|
prefix = sessionURL(subsession.parentSession());
|
|
if (prefix == NULL) prefix = "";
|
|
|
|
suffix = subsession.controlPath();
|
|
if (suffix == NULL) suffix = "";
|
|
|
|
if (isAbsoluteURL(suffix)) {
|
|
prefix = separator = "";
|
|
} else {
|
|
unsigned prefixLen = strlen(prefix);
|
|
separator = (prefixLen == 0 || prefix[prefixLen-1] == '/' || suffix[0] == '/') ? "" : "/";
|
|
}
|
|
}
|
|
|
|
Boolean RTSPClient::setupHTTPTunneling1() {
|
|
// Set up RTSP-over-HTTP tunneling, as described in
|
|
// http://developer.apple.com/quicktime/icefloe/dispatch028.html and http://images.apple.com/br/quicktime/pdf/QTSS_Modules.pdf
|
|
if (fVerbosityLevel >= 1) {
|
|
envir() << "Requesting RTSP-over-HTTP tunneling (on port " << fTunnelOverHTTPPortNum << ")\n\n";
|
|
}
|
|
|
|
// Begin by sending a HTTP "GET", to set up the server->client link. Continue when we handle the response:
|
|
return sendRequest(new RequestRecord(1, "GET", responseHandlerForHTTP_GET)) != 0;
|
|
}
|
|
|
|
void RTSPClient::responseHandlerForHTTP_GET(RTSPClient* rtspClient, int responseCode, char* responseString) {
|
|
if (rtspClient != NULL) rtspClient->responseHandlerForHTTP_GET1(responseCode, responseString);
|
|
}
|
|
|
|
void RTSPClient::responseHandlerForHTTP_GET1(int responseCode, char* responseString) {
|
|
RequestRecord* request;
|
|
do {
|
|
delete[] responseString; // we don't need it (but are responsible for deleting it)
|
|
if (responseCode != 0) break; // The HTTP "GET" failed.
|
|
|
|
// Having successfully set up (using the HTTP "GET" command) the server->client link, set up a second TCP connection
|
|
// (to the same server & port as before) for the client->server link. All future output will be to this new socket.
|
|
fOutputSocketNum = setupStreamSocket(envir(), 0);
|
|
if (fOutputSocketNum < 0) break;
|
|
ignoreSigPipeOnSocket(fOutputSocketNum); // so that servers on the same host that killed don't also kill us
|
|
|
|
fHTTPTunnelingConnectionIsPending = True;
|
|
int connectResult = connectToServer(fOutputSocketNum, fTunnelOverHTTPPortNum);
|
|
if (connectResult < 0) break; // an error occurred
|
|
else if (connectResult == 0) {
|
|
// A connection is pending. Continue setting up RTSP-over-HTTP when the connection completes.
|
|
// First, move the pending requests to the 'awaiting connection' queue:
|
|
while ((request = fRequestsAwaitingHTTPTunneling.dequeue()) != NULL) {
|
|
fRequestsAwaitingConnection.enqueue(request);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// The connection succeeded. Continue setting up RTSP-over-HTTP:
|
|
if (!setupHTTPTunneling2()) break;
|
|
|
|
// RTSP-over-HTTP tunneling succeeded. Resume the pending request(s):
|
|
while ((request = fRequestsAwaitingHTTPTunneling.dequeue()) != NULL) {
|
|
sendRequest(request);
|
|
}
|
|
return;
|
|
} while (0);
|
|
|
|
// An error occurred. Dequeue the pending request(s), and tell them about the error:
|
|
fHTTPTunnelingConnectionIsPending = False;
|
|
resetTCPSockets(); // do this now, in case an error handler deletes "this"
|
|
RequestQueue requestQueue(fRequestsAwaitingHTTPTunneling);
|
|
while ((request = requestQueue.dequeue()) != NULL) {
|
|
handleRequestError(request);
|
|
delete request;
|
|
}
|
|
}
|
|
|
|
Boolean RTSPClient::setupHTTPTunneling2() {
|
|
fHTTPTunnelingConnectionIsPending = False;
|
|
|
|
// Send a HTTP "POST", to set up the client->server link. (Note that we won't see a reply to the "POST".)
|
|
return sendRequest(new RequestRecord(1, "POST", NULL)) != 0;
|
|
}
|
|
|
|
void RTSPClient::connectionHandler(void* instance, int /*mask*/) {
|
|
RTSPClient* client = (RTSPClient*)instance;
|
|
client->connectionHandler1();
|
|
}
|
|
|
|
void RTSPClient::connectionHandler1() {
|
|
// Restore normal handling on our sockets:
|
|
envir().taskScheduler().disableBackgroundHandling(fOutputSocketNum);
|
|
envir().taskScheduler().setBackgroundHandling(fInputSocketNum, SOCKET_READABLE|SOCKET_EXCEPTION,
|
|
(TaskScheduler::BackgroundHandlerProc*)&incomingDataHandler, this);
|
|
|
|
// Move all requests awaiting connection into a new, temporary queue, to clear "fRequestsAwaitingConnection"
|
|
// (so that "sendRequest()" doesn't get confused by "fRequestsAwaitingConnection" being nonempty, and enqueue them all over again).
|
|
RequestQueue tmpRequestQueue(fRequestsAwaitingConnection);
|
|
RequestRecord* request;
|
|
|
|
// Find out whether the connection succeeded or failed:
|
|
do {
|
|
int err = 0;
|
|
SOCKLEN_T len = sizeof err;
|
|
if (getsockopt(fInputSocketNum, SOL_SOCKET, SO_ERROR, (char*)&err, &len) < 0 || err != 0) {
|
|
envir().setResultErrMsg("Connection to server failed: ", err);
|
|
if (fVerbosityLevel >= 1) envir() << "..." << envir().getResultMsg() << "\n";
|
|
break;
|
|
}
|
|
|
|
// The connection succeeded. If the connection came about from an attempt to set up RTSP-over-HTTP, finish this now:
|
|
if (fVerbosityLevel >= 1) envir() << "...remote connection opened\n";
|
|
if (fHTTPTunnelingConnectionIsPending && !setupHTTPTunneling2()) break;
|
|
|
|
// Resume sending all pending requests:
|
|
while ((request = tmpRequestQueue.dequeue()) != NULL) {
|
|
sendRequest(request);
|
|
}
|
|
return;
|
|
} while (0);
|
|
|
|
// An error occurred. Tell all pending requests about the error:
|
|
resetTCPSockets(); // do this now, in case an error handler deletes "this"
|
|
while ((request = tmpRequestQueue.dequeue()) != NULL) {
|
|
handleRequestError(request);
|
|
delete request;
|
|
}
|
|
}
|
|
|
|
void RTSPClient::incomingDataHandler(void* instance, int /*mask*/) {
|
|
RTSPClient* client = (RTSPClient*)instance;
|
|
client->incomingDataHandler1();
|
|
}
|
|
|
|
void RTSPClient::incomingDataHandler1() {
|
|
struct sockaddr_in dummy; // 'from' address - not used
|
|
|
|
int bytesRead = readSocket(envir(), fInputSocketNum, (unsigned char*)&fResponseBuffer[fResponseBytesAlreadySeen], fResponseBufferBytesLeft, dummy);
|
|
handleResponseBytes(bytesRead);
|
|
}
|
|
|
|
static char* getLine(char* startOfLine) {
|
|
// returns the start of the next line, or NULL if none. Note that this modifies the input string to add '\0' characters.
|
|
for (char* ptr = startOfLine; *ptr != '\0'; ++ptr) {
|
|
// Check for the end of line: \r\n (but also accept \r or \n by itself):
|
|
if (*ptr == '\r' || *ptr == '\n') {
|
|
// We found the end of the line
|
|
if (*ptr == '\r') {
|
|
*ptr++ = '\0';
|
|
if (*ptr == '\n') ++ptr;
|
|
} else {
|
|
*ptr++ = '\0';
|
|
}
|
|
return ptr;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void RTSPClient::handleResponseBytes(int newBytesRead) {
|
|
do {
|
|
if (newBytesRead >= 0 && (unsigned)newBytesRead < fResponseBufferBytesLeft) break; // data was read OK; process it below
|
|
|
|
if (newBytesRead >= (int)fResponseBufferBytesLeft) {
|
|
// We filled up our response buffer. Treat this as an error (for the first response handler):
|
|
envir().setResultMsg("RTSP response was truncated. Increase \"RTSPClient::responseBufferSize\"");
|
|
}
|
|
|
|
// An error occurred while reading our TCP socket. Call all pending response handlers, indicating this error.
|
|
// (However, the "RTSP response was truncated" error is applied to the first response handler only.)
|
|
resetResponseBuffer();
|
|
RequestRecord* request;
|
|
if (newBytesRead > 0) { // The "RTSP response was truncated" error
|
|
if ((request = fRequestsAwaitingResponse.dequeue()) != NULL) {
|
|
handleRequestError(request);
|
|
delete request;
|
|
}
|
|
} else {
|
|
RequestQueue requestQueue(fRequestsAwaitingResponse);
|
|
resetTCPSockets(); // do this now, in case an error handler deletes "this"
|
|
|
|
while ((request = requestQueue.dequeue()) != NULL) {
|
|
handleRequestError(request);
|
|
delete request;
|
|
}
|
|
}
|
|
return;
|
|
} while (0);
|
|
|
|
fResponseBufferBytesLeft -= newBytesRead;
|
|
fResponseBytesAlreadySeen += newBytesRead;
|
|
fResponseBuffer[fResponseBytesAlreadySeen] = '\0';
|
|
if (fVerbosityLevel >= 1 && newBytesRead > 1) envir() << "Received " << newBytesRead << " new bytes of response data.\n";
|
|
|
|
unsigned numExtraBytesAfterResponse = 0;
|
|
Boolean responseSuccess = False; // by default
|
|
do {
|
|
// Data was read OK. Look through the data that we've read so far, to see if it contains <CR><LF><CR><LF>.
|
|
// (If not, wait for more data to arrive.)
|
|
Boolean endOfHeaders = False;
|
|
char const* ptr = fResponseBuffer;
|
|
if (fResponseBytesAlreadySeen > 3) {
|
|
char const* const ptrEnd = &fResponseBuffer[fResponseBytesAlreadySeen-3];
|
|
while (ptr < ptrEnd) {
|
|
if (*ptr++ == '\r' && *ptr++ == '\n' && *ptr++ == '\r' && *ptr++ == '\n') {
|
|
// This is it
|
|
endOfHeaders = True;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!endOfHeaders) return; // subsequent reads will be needed to get the complete response
|
|
|
|
// Now that we have the complete response headers (ending with <CR><LF><CR><LF>), parse them to get the response code, CSeq,
|
|
// and various other header parameters. To do this, we first make a copy of the received header data, because we'll be
|
|
// modifying it by adding '\0' bytes.
|
|
char* headerDataCopy;
|
|
unsigned responseCode = 200;
|
|
char const* responseStr = NULL;
|
|
RequestRecord* foundRequest = NULL;
|
|
char const* sessionParamsStr = NULL;
|
|
char const* transportParamsStr = NULL;
|
|
char const* scaleParamsStr = NULL;
|
|
char const* speedParamsStr = NULL;
|
|
char const* rangeParamsStr = NULL;
|
|
char const* rtpInfoParamsStr = NULL;
|
|
char const* wwwAuthenticateParamsStr = NULL;
|
|
char const* publicParamsStr = NULL;
|
|
char* bodyStart = NULL;
|
|
unsigned numBodyBytes = 0;
|
|
responseSuccess = False;
|
|
do {
|
|
headerDataCopy = new char[responseBufferSize];
|
|
strncpy(headerDataCopy, fResponseBuffer, fResponseBytesAlreadySeen);
|
|
headerDataCopy[fResponseBytesAlreadySeen] = '\0';
|
|
|
|
char* lineStart;
|
|
char* nextLineStart = headerDataCopy;
|
|
do {
|
|
lineStart = nextLineStart;
|
|
nextLineStart = getLine(lineStart);
|
|
} while (lineStart[0] == '\0' && nextLineStart != NULL); // skip over any blank lines at the start
|
|
if (!parseResponseCode(lineStart, responseCode, responseStr)) {
|
|
// This does not appear to be a RTSP response; perhaps it's a RTSP request instead?
|
|
handleIncomingRequest();
|
|
break; // we're done with this data
|
|
}
|
|
|
|
// Scan through the headers, handling the ones that we're interested in:
|
|
Boolean reachedEndOfHeaders;
|
|
unsigned cseq = 0;
|
|
unsigned contentLength = 0;
|
|
|
|
while (1) {
|
|
reachedEndOfHeaders = True; // by default; may get changed below
|
|
lineStart = nextLineStart;
|
|
if (lineStart == NULL) break;
|
|
|
|
nextLineStart = getLine(lineStart);
|
|
if (lineStart[0] == '\0') break; // this is a blank line
|
|
reachedEndOfHeaders = False;
|
|
|
|
char const* headerParamsStr;
|
|
if (checkForHeader(lineStart, "CSeq:", 5, headerParamsStr)) {
|
|
if (sscanf(headerParamsStr, "%u", &cseq) != 1 || cseq <= 0) {
|
|
envir().setResultMsg("Bad \"CSeq:\" header: \"", lineStart, "\"");
|
|
break;
|
|
}
|
|
// Find the handler function for "cseq":
|
|
RequestRecord* request;
|
|
while ((request = fRequestsAwaitingResponse.dequeue()) != NULL) {
|
|
if (request->cseq() < cseq) { // assumes that the CSeq counter will never wrap around
|
|
// We never received (and will never receive) a response for this handler, so delete it:
|
|
if (fVerbosityLevel >= 1 && strcmp(request->commandName(), "POST") != 0) {
|
|
envir() << "WARNING: The server did not respond to our \"" << request->commandName() << "\" request (CSeq: "
|
|
<< request->cseq() << "). The server appears to be buggy (perhaps not handling pipelined requests properly).\n";
|
|
}
|
|
delete request;
|
|
} else if (request->cseq() == cseq) {
|
|
// This is the handler that we want. Remove its record, but remember it, so that we can later call its handler:
|
|
foundRequest = request;
|
|
break;
|
|
} else { // request->cseq() > cseq
|
|
// No handler was registered for this response, so ignore it.
|
|
break;
|
|
}
|
|
}
|
|
} else if (checkForHeader(lineStart, "Content-Length:", 15, headerParamsStr)) {
|
|
if (sscanf(headerParamsStr, "%u", &contentLength) != 1) {
|
|
envir().setResultMsg("Bad \"Content-Length:\" header: \"", lineStart, "\"");
|
|
break;
|
|
}
|
|
} else if (checkForHeader(lineStart, "Content-Base:", 13, headerParamsStr)) {
|
|
setBaseURL(headerParamsStr);
|
|
} else if (checkForHeader(lineStart, "Session:", 8, sessionParamsStr)) {
|
|
} else if (checkForHeader(lineStart, "Transport:", 10, transportParamsStr)) {
|
|
} else if (checkForHeader(lineStart, "Scale:", 6, scaleParamsStr)) {
|
|
} else if (checkForHeader(lineStart, "Speed:", 6, speedParamsStr)) {
|
|
} else if (checkForHeader(lineStart, "Range:", 6, rangeParamsStr)) {
|
|
} else if (checkForHeader(lineStart, "RTP-Info:", 9, rtpInfoParamsStr)) {
|
|
} else if (checkForHeader(lineStart, "WWW-Authenticate:", 17, headerParamsStr)) {
|
|
// If we've already seen a "WWW-Authenticate:" header, then we replace it with this new one only if
|
|
// the new one specifies "Digest" authentication:
|
|
if (wwwAuthenticateParamsStr == NULL || _strncasecmp(headerParamsStr, "Digest", 6) == 0) {
|
|
wwwAuthenticateParamsStr = headerParamsStr;
|
|
}
|
|
} else if (checkForHeader(lineStart, "Public:", 7, publicParamsStr)) {
|
|
} else if (checkForHeader(lineStart, "Allow:", 6, publicParamsStr)) {
|
|
// Note: we accept "Allow:" instead of "Public:", so that "OPTIONS" requests made to HTTP servers will work.
|
|
} else if (checkForHeader(lineStart, "Location:", 9, headerParamsStr)) {
|
|
setBaseURL(headerParamsStr);
|
|
} else if (checkForHeader(lineStart, "com.ses.streamID:", 16, headerParamsStr)) {
|
|
// Replace the tail of the 'base URL' with the value of this header parameter:
|
|
char* oldBaseURLTail = strrchr(fBaseURL, '/');
|
|
if (oldBaseURLTail != NULL) {
|
|
unsigned newBaseURLLen
|
|
= (oldBaseURLTail - fBaseURL) + 8/* for "/stream=" */ + strlen(headerParamsStr+1);
|
|
char* newBaseURL = new char[newBaseURLLen + 1];
|
|
// Note: We couldn't use "asprintf()", because some compilers don't support it
|
|
sprintf(newBaseURL, "%.*s/stream=%s",
|
|
(int)(oldBaseURLTail - fBaseURL), fBaseURL, headerParamsStr+1);
|
|
setBaseURL(newBaseURL);
|
|
delete[] newBaseURL;
|
|
}
|
|
}
|
|
}
|
|
if (!reachedEndOfHeaders) break; // an error occurred
|
|
|
|
if (foundRequest == NULL) {
|
|
// Hack: The response didn't have a "CSeq:" header; assume it's for our most recent request:
|
|
foundRequest = fRequestsAwaitingResponse.dequeue();
|
|
}
|
|
|
|
// If we saw a "Content-Length:" header, then make sure that we have the amount of data that it specified:
|
|
unsigned bodyOffset = nextLineStart == NULL ? fResponseBytesAlreadySeen : nextLineStart - headerDataCopy;
|
|
bodyStart = &fResponseBuffer[bodyOffset];
|
|
numBodyBytes = fResponseBytesAlreadySeen - bodyOffset;
|
|
if (contentLength > numBodyBytes) {
|
|
// We need to read more data. First, make sure we have enough space for it:
|
|
unsigned numExtraBytesNeeded = contentLength - numBodyBytes;
|
|
unsigned remainingBufferSize = responseBufferSize - fResponseBytesAlreadySeen;
|
|
if (numExtraBytesNeeded > remainingBufferSize) {
|
|
char tmpBuf[200];
|
|
sprintf(tmpBuf, "Response buffer size (%d) is too small for \"Content-Length:\" %d (need a buffer size of >= %d bytes\n",
|
|
responseBufferSize, contentLength, fResponseBytesAlreadySeen + numExtraBytesNeeded);
|
|
envir().setResultMsg(tmpBuf);
|
|
break;
|
|
}
|
|
|
|
if (fVerbosityLevel >= 1) {
|
|
envir() << "Have received " << fResponseBytesAlreadySeen << " total bytes of a "
|
|
<< (foundRequest != NULL ? foundRequest->commandName() : "(unknown)")
|
|
<< " RTSP response; awaiting " << numExtraBytesNeeded << " bytes more.\n";
|
|
}
|
|
delete[] headerDataCopy;
|
|
if (foundRequest != NULL) fRequestsAwaitingResponse.putAtHead(foundRequest);// put our request record back; we need it again
|
|
return; // We need to read more data
|
|
}
|
|
|
|
// We now have a complete response (including all bytes specified by the "Content-Length:" header, if any).
|
|
char* responseEnd = bodyStart + contentLength;
|
|
numExtraBytesAfterResponse = &fResponseBuffer[fResponseBytesAlreadySeen] - responseEnd;
|
|
|
|
if (fVerbosityLevel >= 1) {
|
|
char saved = *responseEnd;
|
|
*responseEnd = '\0';
|
|
envir() << "Received a complete "
|
|
<< (foundRequest != NULL ? foundRequest->commandName() : "(unknown)")
|
|
<< " response:\n" << fResponseBuffer << "\n";
|
|
if (numExtraBytesAfterResponse > 0) envir() << "\t(plus " << numExtraBytesAfterResponse << " additional bytes)\n";
|
|
*responseEnd = saved;
|
|
}
|
|
|
|
if (foundRequest != NULL) {
|
|
Boolean needToResendCommand = False; // by default...
|
|
if (responseCode == 200) {
|
|
// Do special-case response handling for some commands:
|
|
if (strcmp(foundRequest->commandName(), "SETUP") == 0) {
|
|
if (!handleSETUPResponse(*foundRequest->subsession(), sessionParamsStr, transportParamsStr, foundRequest->booleanFlags()&0x1)) break;
|
|
} else if (strcmp(foundRequest->commandName(), "PLAY") == 0) {
|
|
if (!handlePLAYResponse(*foundRequest->session(), *foundRequest->subsession(), scaleParamsStr, speedParamsStr, rangeParamsStr, rtpInfoParamsStr)) break;
|
|
} else if (strcmp(foundRequest->commandName(), "TEARDOWN") == 0) {
|
|
if (!handleTEARDOWNResponse(*foundRequest->session(), *foundRequest->subsession())) break;
|
|
} else if (strcmp(foundRequest->commandName(), "GET_PARAMETER") == 0) {
|
|
if (!handleGET_PARAMETERResponse(foundRequest->contentStr(), bodyStart, responseEnd)) break;
|
|
}
|
|
} else if (responseCode == 401 && handleAuthenticationFailure(wwwAuthenticateParamsStr)) {
|
|
// We need to resend the command, with an "Authorization:" header:
|
|
needToResendCommand = True;
|
|
|
|
if (strcmp(foundRequest->commandName(), "GET") == 0) {
|
|
// Note: If a HTTP "GET" command (for RTSP-over-HTTP tunneling) returns "401 Unauthorized", then we resend it
|
|
// (with an "Authorization:" header), just as we would for a RTSP command. However, we do so using a new TCP connection,
|
|
// because some servers close the original connection after returning the "401 Unauthorized".
|
|
resetTCPSockets(); // forces the opening of a new connection for the resent command
|
|
}
|
|
} else if (responseCode == 301 || responseCode == 302) { // redirection
|
|
resetTCPSockets(); // because we need to connect somewhere else next
|
|
needToResendCommand = True;
|
|
}
|
|
|
|
if (needToResendCommand) {
|
|
resetResponseBuffer();
|
|
(void)resendCommand(foundRequest);
|
|
delete[] headerDataCopy;
|
|
return; // without calling our response handler; the response to the resent command will do that
|
|
}
|
|
}
|
|
|
|
responseSuccess = True;
|
|
} while (0);
|
|
|
|
// If we have a handler function for this response, call it.
|
|
// But first, reset our response buffer, in case the handler goes to the event loop, and we end up getting called recursively:
|
|
if (numExtraBytesAfterResponse > 0) {
|
|
// An unusual case; usually due to having received pipelined responses. Move the extra bytes to the front of the buffer:
|
|
char* responseEnd = &fResponseBuffer[fResponseBytesAlreadySeen - numExtraBytesAfterResponse];
|
|
|
|
// But first: A hack to save a copy of the response 'body', in case it's needed below for "resultString":
|
|
numBodyBytes -= numExtraBytesAfterResponse;
|
|
if (numBodyBytes > 0) {
|
|
char saved = *responseEnd;
|
|
*responseEnd = '\0';
|
|
bodyStart = strDup(bodyStart);
|
|
*responseEnd = saved;
|
|
}
|
|
|
|
memmove(fResponseBuffer, responseEnd, numExtraBytesAfterResponse);
|
|
fResponseBytesAlreadySeen = numExtraBytesAfterResponse;
|
|
fResponseBufferBytesLeft = responseBufferSize - numExtraBytesAfterResponse;
|
|
fResponseBuffer[numExtraBytesAfterResponse] = '\0';
|
|
} else {
|
|
resetResponseBuffer();
|
|
}
|
|
if (foundRequest != NULL && foundRequest->handler() != NULL) {
|
|
int resultCode;
|
|
char* resultString;
|
|
if (responseSuccess) {
|
|
if (responseCode == 200) {
|
|
resultCode = 0;
|
|
resultString = numBodyBytes > 0 ? strDup(bodyStart) : strDup(publicParamsStr);
|
|
// Note: The "strDup(bodyStart)" call assumes that the body is encoded without interior '\0' bytes
|
|
} else {
|
|
resultCode = responseCode;
|
|
resultString = strDup(responseStr);
|
|
envir().setResultMsg(responseStr);
|
|
}
|
|
(*foundRequest->handler())(this, resultCode, resultString);
|
|
} else {
|
|
// An error occurred parsing the response, so call the handler, indicating an error:
|
|
handleRequestError(foundRequest);
|
|
}
|
|
}
|
|
delete foundRequest;
|
|
delete[] headerDataCopy;
|
|
if (numExtraBytesAfterResponse > 0 && numBodyBytes > 0) delete[] bodyStart;
|
|
} while (numExtraBytesAfterResponse > 0 && responseSuccess);
|
|
}
|
|
|
|
|
|
////////// RTSPClient::RequestRecord implementation //////////
|
|
|
|
RTSPClient::RequestRecord::RequestRecord(unsigned cseq, char const* commandName, responseHandler* handler,
|
|
MediaSession* session, MediaSubsession* subsession, u_int32_t booleanFlags,
|
|
double start, double end, float scale, char const* contentStr)
|
|
: fNext(NULL), fCSeq(cseq), fCommandName(commandName), fSession(session), fSubsession(subsession), fBooleanFlags(booleanFlags),
|
|
fStart(start), fEnd(end), fAbsStartTime(NULL), fAbsEndTime(NULL), fScale(scale), fContentStr(strDup(contentStr)), fHandler(handler) {
|
|
}
|
|
|
|
RTSPClient::RequestRecord::RequestRecord(unsigned cseq, responseHandler* handler,
|
|
char const* absStartTime, char const* absEndTime, float scale,
|
|
MediaSession* session, MediaSubsession* subsession)
|
|
: fNext(NULL), fCSeq(cseq), fCommandName("PLAY"), fSession(session), fSubsession(subsession), fBooleanFlags(0),
|
|
fStart(0.0f), fEnd(-1.0f), fAbsStartTime(strDup(absStartTime)), fAbsEndTime(strDup(absEndTime)), fScale(scale),
|
|
fContentStr(NULL), fHandler(handler) {
|
|
}
|
|
|
|
RTSPClient::RequestRecord::~RequestRecord() {
|
|
// Delete the rest of the list first:
|
|
delete fNext;
|
|
|
|
delete[] fAbsStartTime; delete[] fAbsEndTime;
|
|
delete[] fContentStr;
|
|
}
|
|
|
|
|
|
////////// RTSPClient::RequestQueue implementation //////////
|
|
|
|
RTSPClient::RequestQueue::RequestQueue()
|
|
: fHead(NULL), fTail(NULL) {
|
|
}
|
|
|
|
RTSPClient::RequestQueue::RequestQueue(RequestQueue& origQueue)
|
|
: fHead(NULL), fTail(NULL) {
|
|
RequestRecord* request;
|
|
while ((request = origQueue.dequeue()) != NULL) {
|
|
enqueue(request);
|
|
}
|
|
}
|
|
|
|
RTSPClient::RequestQueue::~RequestQueue() {
|
|
delete fHead;
|
|
}
|
|
|
|
void RTSPClient::RequestQueue::enqueue(RequestRecord* request) {
|
|
if (fTail == NULL) {
|
|
fHead = request;
|
|
} else {
|
|
fTail->next() = request;
|
|
}
|
|
fTail = request;
|
|
}
|
|
|
|
RTSPClient::RequestRecord* RTSPClient::RequestQueue::dequeue() {
|
|
RequestRecord* request = fHead;
|
|
if (fHead == fTail) {
|
|
fHead = NULL;
|
|
fTail = NULL;
|
|
} else {
|
|
fHead = fHead->next();
|
|
}
|
|
if (request != NULL) request->next() = NULL;
|
|
return request;
|
|
}
|
|
|
|
void RTSPClient::RequestQueue::putAtHead(RequestRecord* request) {
|
|
request->next() = fHead;
|
|
fHead = request;
|
|
if (fTail == NULL) {
|
|
fTail = request;
|
|
}
|
|
}
|
|
|
|
RTSPClient::RequestRecord* RTSPClient::RequestQueue::findByCSeq(unsigned cseq) {
|
|
RequestRecord* request;
|
|
for (request = fHead; request != NULL; request = request->next()) {
|
|
if (request->cseq() == cseq) return request;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
|
|
#ifndef OMIT_REGISTER_HANDLING
|
|
////////// HandlerServerForREGISTERCommand implementation /////////
|
|
|
|
HandlerServerForREGISTERCommand* HandlerServerForREGISTERCommand
|
|
::createNew(UsageEnvironment& env, onRTSPClientCreationFunc* creationFunc, Port ourPort,
|
|
UserAuthenticationDatabase* authDatabase, int verbosityLevel, char const* applicationName) {
|
|
int ourSocket = setUpOurSocket(env, ourPort);
|
|
if (ourSocket == -1) return NULL;
|
|
|
|
return new HandlerServerForREGISTERCommand(env, creationFunc, ourSocket, ourPort, authDatabase, verbosityLevel, applicationName);
|
|
}
|
|
|
|
HandlerServerForREGISTERCommand
|
|
::HandlerServerForREGISTERCommand(UsageEnvironment& env, onRTSPClientCreationFunc* creationFunc, int ourSocket, Port ourPort,
|
|
UserAuthenticationDatabase* authDatabase, int verbosityLevel, char const* applicationName)
|
|
: RTSPServer(env, ourSocket, ourPort, authDatabase, 30/*small reclamationTestSeconds*/),
|
|
fCreationFunc(creationFunc), fVerbosityLevel(verbosityLevel), fApplicationName(strDup(applicationName)) {
|
|
}
|
|
|
|
HandlerServerForREGISTERCommand::~HandlerServerForREGISTERCommand() {
|
|
delete[] fApplicationName;
|
|
}
|
|
|
|
RTSPClient* HandlerServerForREGISTERCommand
|
|
::createNewRTSPClient(char const* rtspURL, int verbosityLevel, char const* applicationName, int socketNumToServer) {
|
|
// Default implementation: create a basic "RTSPClient":
|
|
return RTSPClient::createNew(envir(), rtspURL, verbosityLevel, applicationName, 0, socketNumToServer);
|
|
}
|
|
|
|
char const* HandlerServerForREGISTERCommand::allowedCommandNames() {
|
|
return "OPTIONS, REGISTER";
|
|
}
|
|
|
|
Boolean HandlerServerForREGISTERCommand::weImplementREGISTER(char const* proxyURLSuffix, char*& responseStr) {
|
|
responseStr = NULL;
|
|
return True;
|
|
}
|
|
|
|
void HandlerServerForREGISTERCommand::implementCmd_REGISTER(char const* url, char const* urlSuffix, int socketToRemoteServer,
|
|
Boolean deliverViaTCP, char const* /*proxyURLSuffix*/) {
|
|
// Create a new "RTSPClient" object, and call our 'creation function' with it:
|
|
RTSPClient* newRTSPClient = createNewRTSPClient(url, fVerbosityLevel, fApplicationName, socketToRemoteServer);
|
|
|
|
if (fCreationFunc != NULL) (*fCreationFunc)(newRTSPClient, deliverViaTCP);
|
|
}
|
|
#endif
|