| /********** |
| 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-2015 Live Networks, Inc. All rights reserved. |
| // A server that supports both RTSP, and HTTP streaming (using Apple's "HTTP Live Streaming" protocol) |
| // Implementation |
| |
| #include "RTSPServer.hh" |
| #include "RTSPServerSupportingHTTPStreaming.hh" |
| #include "RTSPCommon.hh" |
| #ifndef _WIN32_WCE |
| #include <sys/stat.h> |
| #endif |
| #include <time.h> |
| |
| RTSPServerSupportingHTTPStreaming* |
| RTSPServerSupportingHTTPStreaming::createNew(UsageEnvironment& env, Port rtspPort, |
| UserAuthenticationDatabase* authDatabase, unsigned reclamationTestSeconds) { |
| int ourSocket = setUpOurSocket(env, rtspPort); |
| if (ourSocket == -1) return NULL; |
| |
| return new RTSPServerSupportingHTTPStreaming(env, ourSocket, rtspPort, authDatabase, reclamationTestSeconds); |
| } |
| |
| RTSPServerSupportingHTTPStreaming |
| ::RTSPServerSupportingHTTPStreaming(UsageEnvironment& env, int ourSocket, Port rtspPort, |
| UserAuthenticationDatabase* authDatabase, unsigned reclamationTestSeconds) |
| : RTSPServer(env, ourSocket, rtspPort, authDatabase, reclamationTestSeconds) { |
| } |
| |
| RTSPServerSupportingHTTPStreaming::~RTSPServerSupportingHTTPStreaming() { |
| } |
| |
| GenericMediaServer::ClientConnection* |
| RTSPServerSupportingHTTPStreaming::createNewClientConnection(int clientSocket, struct sockaddr_in clientAddr) { |
| return new RTSPClientConnectionSupportingHTTPStreaming(*this, clientSocket, clientAddr); |
| } |
| |
| RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming |
| ::RTSPClientConnectionSupportingHTTPStreaming(RTSPServer& ourServer, int clientSocket, struct sockaddr_in clientAddr) |
| : RTSPClientConnection(ourServer, clientSocket, clientAddr), |
| fClientSessionId(0), fStreamSource(NULL), fPlaylistSource(NULL), fTCPSink(NULL) { |
| } |
| |
| RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming::~RTSPClientConnectionSupportingHTTPStreaming() { |
| Medium::close(fPlaylistSource); |
| Medium::close(fStreamSource); |
| Medium::close(fTCPSink); |
| } |
| |
| static char const* lastModifiedHeader(char const* fileName) { |
| static char buf[200]; |
| buf[0] = '\0'; // by default, return an empty string |
| |
| #ifndef _WIN32_WCE |
| struct stat sb; |
| int statResult = stat(fileName, &sb); |
| if (statResult == 0) { |
| strftime(buf, sizeof buf, "Last-Modified: %a, %b %d %Y %H:%M:%S GMT\r\n", gmtime((const time_t*)&sb.st_mtime)); |
| } |
| #endif |
| |
| return buf; |
| } |
| |
| void RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming |
| ::handleHTTPCmd_StreamingGET(char const* urlSuffix, char const* /*fullRequestStr*/) { |
| // If "urlSuffix" ends with "?segment=<offset-in-seconds>,<duration-in-seconds>", then strip this off, and send the |
| // specified segment. Otherwise, construct and send a playlist that consists of segments from the specified file. |
| do { |
| char const* questionMarkPos = strrchr(urlSuffix, '?'); |
| if (questionMarkPos == NULL) break; |
| unsigned offsetInSeconds, durationInSeconds; |
| if (sscanf(questionMarkPos, "?segment=%u,%u", &offsetInSeconds, &durationInSeconds) != 2) break; |
| |
| char* streamName = strDup(urlSuffix); |
| streamName[questionMarkPos-urlSuffix] = '\0'; |
| |
| do { |
| ServerMediaSession* session = fOurServer.lookupServerMediaSession(streamName); |
| if (session == NULL) { |
| handleHTTPCmd_notFound(); |
| break; |
| } |
| |
| // We can't send multi-subsession streams over HTTP (because there's no defined way to multiplex more than one subsession). |
| // Therefore, use the first (and presumed only) substream: |
| ServerMediaSubsessionIterator iter(*session); |
| ServerMediaSubsession* subsession = iter.next(); |
| if (subsession == NULL) { |
| // Treat an 'empty' ServerMediaSession the same as one that doesn't exist at all: |
| handleHTTPCmd_notFound(); |
| break; |
| } |
| |
| // Call "getStreamParameters()" to create the stream's source. (Because we're not actually streaming via RTP/RTCP, most |
| // of the parameters to the call are dummy.) |
| ++fClientSessionId; |
| Port clientRTPPort(0), clientRTCPPort(0), serverRTPPort(0), serverRTCPPort(0); |
| netAddressBits destinationAddress = 0; |
| u_int8_t destinationTTL = 0; |
| Boolean isMulticast = False; |
| void* streamToken; |
| subsession->getStreamParameters(fClientSessionId, 0, clientRTPPort,clientRTCPPort, -1,0,0, destinationAddress,destinationTTL, isMulticast, serverRTPPort,serverRTCPPort, streamToken); |
| |
| // Seek the stream source to the desired place, with the desired duration, and (as a side effect) get the number of bytes: |
| double dOffsetInSeconds = (double)offsetInSeconds; |
| u_int64_t numBytes; |
| subsession->seekStream(fClientSessionId, streamToken, dOffsetInSeconds, (double)durationInSeconds, numBytes); |
| unsigned numTSBytesToStream = (unsigned)numBytes; |
| |
| if (numTSBytesToStream == 0) { |
| // For some reason, we do not know the size of the requested range. We can't handle this request: |
| handleHTTPCmd_notSupported(); |
| break; |
| } |
| |
| // Construct our response: |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "HTTP/1.1 200 OK\r\n" |
| "%s" |
| "Server: LIVE555 Streaming Media v%s\r\n" |
| "%s" |
| "Content-Length: %d\r\n" |
| "Content-Type: text/plain; charset=ISO-8859-1\r\n" |
| "\r\n", |
| dateHeader(), |
| LIVEMEDIA_LIBRARY_VERSION_STRING, |
| lastModifiedHeader(streamName), |
| numTSBytesToStream); |
| // Send the response now, because we're about to add more data (from the source): |
| send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0); |
| fResponseBuffer[0] = '\0'; // We've already sent the response. This tells the calling code not to send it again. |
| |
| // Ask the media source to deliver - to the TCP sink - the desired data: |
| if (fStreamSource != NULL) { // sanity check |
| if (fTCPSink != NULL) fTCPSink->stopPlaying(); |
| Medium::close(fStreamSource); |
| } |
| fStreamSource = subsession->getStreamSource(streamToken); |
| if (fStreamSource != NULL) { |
| if (fTCPSink == NULL) fTCPSink = TCPStreamSink::createNew(envir(), fClientOutputSocket); |
| fTCPSink->startPlaying(*fStreamSource, afterStreaming, this); |
| } |
| } while(0); |
| |
| delete[] streamName; |
| return; |
| } while (0); |
| |
| // "urlSuffix" does not end with "?segment=<offset-in-seconds>,<duration-in-seconds>". |
| // Construct and send a playlist that describes segments from the specified file. |
| |
| // First, make sure that the named file exists, and is streamable: |
| ServerMediaSession* session = fOurServer.lookupServerMediaSession(urlSuffix); |
| if (session == NULL) { |
| handleHTTPCmd_notFound(); |
| return; |
| } |
| |
| // To be able to construct a playlist for the requested file, we need to know its duration: |
| float duration = session->duration(); |
| if (duration <= 0.0) { |
| // We can't handle this request: |
| handleHTTPCmd_notSupported(); |
| return; |
| } |
| |
| // Now, construct the playlist. It will consist of a prefix, one or more media file specifications, and a suffix: |
| unsigned const maxIntLen = 10; // >= the maximum possible strlen() of an integer in the playlist |
| char const* const playlistPrefixFmt = |
| "#EXTM3U\r\n" |
| "#EXT-X-ALLOW-CACHE:YES\r\n" |
| "#EXT-X-MEDIA-SEQUENCE:0\r\n" |
| "#EXT-X-TARGETDURATION:%d\r\n"; |
| unsigned const playlistPrefixFmt_maxLen = strlen(playlistPrefixFmt) + maxIntLen; |
| |
| char const* const playlistMediaFileSpecFmt = |
| "#EXTINF:%d,\r\n" |
| "%s?segment=%d,%d\r\n"; |
| unsigned const playlistMediaFileSpecFmt_maxLen = strlen(playlistMediaFileSpecFmt) + maxIntLen + strlen(urlSuffix) + 2*maxIntLen; |
| |
| char const* const playlistSuffixFmt = |
| "#EXT-X-ENDLIST\r\n"; |
| unsigned const playlistSuffixFmt_maxLen = strlen(playlistSuffixFmt); |
| |
| // Figure out the 'target duration' that will produce a playlist that will fit in our response buffer. (But make it at least 10s.) |
| unsigned const playlistMaxSize = 10000; |
| unsigned const mediaFileSpecsMaxSize = playlistMaxSize - (playlistPrefixFmt_maxLen + playlistSuffixFmt_maxLen); |
| unsigned const maxNumMediaFileSpecs = mediaFileSpecsMaxSize/playlistMediaFileSpecFmt_maxLen; |
| |
| unsigned targetDuration = (unsigned)(duration/maxNumMediaFileSpecs + 1); |
| if (targetDuration < 10) targetDuration = 10; |
| |
| char* playlist = new char[playlistMaxSize]; |
| char* s = playlist; |
| sprintf(s, playlistPrefixFmt, targetDuration); |
| s += strlen(s); |
| |
| unsigned durSoFar = 0; |
| while (1) { |
| unsigned dur = targetDuration < duration ? targetDuration : (unsigned)duration; |
| duration -= dur; |
| sprintf(s, playlistMediaFileSpecFmt, dur, urlSuffix, durSoFar, dur); |
| s += strlen(s); |
| if (duration < 1.0) break; |
| |
| durSoFar += dur; |
| } |
| |
| sprintf(s, playlistSuffixFmt); |
| s += strlen(s); |
| unsigned playlistLen = s - playlist; |
| |
| // Construct our response: |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "HTTP/1.1 200 OK\r\n" |
| "%s" |
| "Server: LIVE555 Streaming Media v%s\r\n" |
| "%s" |
| "Content-Length: %d\r\n" |
| "Content-Type: application/vnd.apple.mpegurl\r\n" |
| "\r\n", |
| dateHeader(), |
| LIVEMEDIA_LIBRARY_VERSION_STRING, |
| lastModifiedHeader(urlSuffix), |
| playlistLen); |
| |
| // Send the response header now, because we're about to add more data (the playlist): |
| send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0); |
| fResponseBuffer[0] = '\0'; // We've already sent the response. This tells the calling code not to send it again. |
| |
| // Then, send the playlist. Because it's large, we don't do so using "send()", because that might not send it all at once. |
| // Instead, we stream the playlist over the TCP socket: |
| if (fPlaylistSource != NULL) { // sanity check |
| if (fTCPSink != NULL) fTCPSink->stopPlaying(); |
| Medium::close(fPlaylistSource); |
| } |
| fPlaylistSource = ByteStreamMemoryBufferSource::createNew(envir(), (u_int8_t*)playlist, playlistLen); |
| if (fTCPSink == NULL) fTCPSink = TCPStreamSink::createNew(envir(), fClientOutputSocket); |
| fTCPSink->startPlaying(*fPlaylistSource, afterStreaming, this); |
| } |
| |
| void RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming::afterStreaming(void* clientData) { |
| RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming* clientConnection |
| = (RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming*)clientData; |
| // Arrange to delete the 'client connection' object: |
| if (clientConnection->fRecursionCount > 0) { |
| // We're still in the midst of handling a request |
| clientConnection->fIsActive = False; // will cause the object to get deleted at the end of handling the request |
| } else { |
| // We're no longer handling a request; delete the object now: |
| delete clientConnection; |
| } |
| } |