/********** 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 .) 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 #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 "\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://[[:]@][:][/]" 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 "[:]@" 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 (and perhaps ). 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 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 . // (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 ), 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