| 1 | /********** |
| 2 | This library is free software; you can redistribute it and/or modify it under |
| 3 | the terms of the GNU Lesser General Public License as published by the |
| 4 | Free Software Foundation; either version 3 of the License, or (at your |
| 5 | option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.) |
| 6 | |
| 7 | This library is distributed in the hope that it will be useful, but WITHOUT |
| 8 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 9 | FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for |
| 10 | more details. |
| 11 | |
| 12 | You should have received a copy of the GNU Lesser General Public License |
| 13 | along with this library; if not, write to the Free Software Foundation, Inc., |
| 14 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| 15 | **********/ |
| 16 | // "liveMedia" |
| 17 | // Copyright (c) 1996-2020 Live Networks, Inc. All rights reserved. |
| 18 | // A server that supports both RTSP, and HTTP streaming (using Apple's "HTTP Live Streaming" protocol) |
| 19 | // Implementation |
| 20 | |
| 21 | #include "RTSPServer.hh" |
| 22 | #include "RTSPServerSupportingHTTPStreaming.hh" |
| 23 | #include "RTSPCommon.hh" |
| 24 | #ifndef _WIN32_WCE |
| 25 | #include <sys/stat.h> |
| 26 | #endif |
| 27 | #include <time.h> |
| 28 | |
| 29 | RTSPServerSupportingHTTPStreaming* |
| 30 | RTSPServerSupportingHTTPStreaming::createNew(UsageEnvironment& env, Port rtspPort, |
| 31 | UserAuthenticationDatabase* authDatabase, unsigned reclamationTestSeconds) { |
| 32 | int ourSocket = setUpOurSocket(env, rtspPort); |
| 33 | if (ourSocket == -1) return NULL; |
| 34 | |
| 35 | return new RTSPServerSupportingHTTPStreaming(env, ourSocket, rtspPort, authDatabase, reclamationTestSeconds); |
| 36 | } |
| 37 | |
| 38 | RTSPServerSupportingHTTPStreaming |
| 39 | ::RTSPServerSupportingHTTPStreaming(UsageEnvironment& env, int ourSocket, Port rtspPort, |
| 40 | UserAuthenticationDatabase* authDatabase, unsigned reclamationTestSeconds) |
| 41 | : RTSPServer(env, ourSocket, rtspPort, authDatabase, reclamationTestSeconds) { |
| 42 | } |
| 43 | |
| 44 | RTSPServerSupportingHTTPStreaming::~RTSPServerSupportingHTTPStreaming() { |
| 45 | } |
| 46 | |
| 47 | GenericMediaServer::ClientConnection* |
| 48 | RTSPServerSupportingHTTPStreaming::createNewClientConnection(int clientSocket, struct sockaddr_in clientAddr) { |
| 49 | return new RTSPClientConnectionSupportingHTTPStreaming(*this, clientSocket, clientAddr); |
| 50 | } |
| 51 | |
| 52 | RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming |
| 53 | ::RTSPClientConnectionSupportingHTTPStreaming(RTSPServer& ourServer, int clientSocket, struct sockaddr_in clientAddr) |
| 54 | : RTSPClientConnection(ourServer, clientSocket, clientAddr), |
| 55 | fClientSessionId(0), fStreamSource(NULL), fPlaylistSource(NULL), fTCPSink(NULL) { |
| 56 | } |
| 57 | |
| 58 | RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming::~RTSPClientConnectionSupportingHTTPStreaming() { |
| 59 | Medium::close(fPlaylistSource); |
| 60 | Medium::close(fStreamSource); |
| 61 | Medium::close(fTCPSink); |
| 62 | } |
| 63 | |
| 64 | static char const* (char const* fileName) { |
| 65 | static char buf[200]; |
| 66 | buf[0] = '\0'; // by default, return an empty string |
| 67 | |
| 68 | #ifndef _WIN32_WCE |
| 69 | struct stat sb; |
| 70 | int statResult = stat(fileName, &sb); |
| 71 | if (statResult == 0) { |
| 72 | strftime(buf, sizeof buf, "Last-Modified: %a, %b %d %Y %H:%M:%S GMT\r\n" , gmtime((const time_t*)&sb.st_mtime)); |
| 73 | } |
| 74 | #endif |
| 75 | |
| 76 | return buf; |
| 77 | } |
| 78 | |
| 79 | void RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming |
| 80 | ::handleHTTPCmd_StreamingGET(char const* urlSuffix, char const* /*fullRequestStr*/) { |
| 81 | // If "urlSuffix" ends with "?segment=<offset-in-seconds>,<duration-in-seconds>", then strip this off, and send the |
| 82 | // specified segment. Otherwise, construct and send a playlist that consists of segments from the specified file. |
| 83 | do { |
| 84 | char const* questionMarkPos = strrchr(urlSuffix, '?'); |
| 85 | if (questionMarkPos == NULL) break; |
| 86 | unsigned offsetInSeconds, durationInSeconds; |
| 87 | if (sscanf(questionMarkPos, "?segment=%u,%u" , &offsetInSeconds, &durationInSeconds) != 2) break; |
| 88 | |
| 89 | char* streamName = strDup(urlSuffix); |
| 90 | streamName[questionMarkPos-urlSuffix] = '\0'; |
| 91 | |
| 92 | do { |
| 93 | ServerMediaSession* session = fOurServer.lookupServerMediaSession(streamName); |
| 94 | if (session == NULL) { |
| 95 | handleHTTPCmd_notFound(); |
| 96 | break; |
| 97 | } |
| 98 | |
| 99 | // We can't send multi-subsession streams over HTTP (because there's no defined way to multiplex more than one subsession). |
| 100 | // Therefore, use the first (and presumed only) substream: |
| 101 | ServerMediaSubsessionIterator iter(*session); |
| 102 | ServerMediaSubsession* subsession = iter.next(); |
| 103 | if (subsession == NULL) { |
| 104 | // Treat an 'empty' ServerMediaSession the same as one that doesn't exist at all: |
| 105 | handleHTTPCmd_notFound(); |
| 106 | break; |
| 107 | } |
| 108 | |
| 109 | // Call "getStreamParameters()" to create the stream's source. (Because we're not actually streaming via RTP/RTCP, most |
| 110 | // of the parameters to the call are dummy.) |
| 111 | ++fClientSessionId; |
| 112 | Port clientRTPPort(0), clientRTCPPort(0), serverRTPPort(0), serverRTCPPort(0); |
| 113 | netAddressBits destinationAddress = 0; |
| 114 | u_int8_t destinationTTL = 0; |
| 115 | Boolean isMulticast = False; |
| 116 | void* streamToken; |
| 117 | subsession->getStreamParameters(fClientSessionId, 0, clientRTPPort,clientRTCPPort, -1,0,0, destinationAddress,destinationTTL, isMulticast, serverRTPPort,serverRTCPPort, streamToken); |
| 118 | |
| 119 | // Seek the stream source to the desired place, with the desired duration, and (as a side effect) get the number of bytes: |
| 120 | double dOffsetInSeconds = (double)offsetInSeconds; |
| 121 | u_int64_t numBytes; |
| 122 | subsession->seekStream(fClientSessionId, streamToken, dOffsetInSeconds, (double)durationInSeconds, numBytes); |
| 123 | unsigned numTSBytesToStream = (unsigned)numBytes; |
| 124 | |
| 125 | if (numTSBytesToStream == 0) { |
| 126 | // For some reason, we do not know the size of the requested range. We can't handle this request: |
| 127 | handleHTTPCmd_notSupported(); |
| 128 | break; |
| 129 | } |
| 130 | |
| 131 | // Construct our response: |
| 132 | snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| 133 | "HTTP/1.1 200 OK\r\n" |
| 134 | "%s" |
| 135 | "Server: LIVE555 Streaming Media v%s\r\n" |
| 136 | "%s" |
| 137 | "Content-Length: %d\r\n" |
| 138 | "Content-Type: text/plain; charset=ISO-8859-1\r\n" |
| 139 | "\r\n" , |
| 140 | dateHeader(), |
| 141 | LIVEMEDIA_LIBRARY_VERSION_STRING, |
| 142 | lastModifiedHeader(streamName), |
| 143 | numTSBytesToStream); |
| 144 | // Send the response now, because we're about to add more data (from the source): |
| 145 | send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0); |
| 146 | fResponseBuffer[0] = '\0'; // We've already sent the response. This tells the calling code not to send it again. |
| 147 | |
| 148 | // Ask the media source to deliver - to the TCP sink - the desired data: |
| 149 | if (fStreamSource != NULL) { // sanity check |
| 150 | if (fTCPSink != NULL) fTCPSink->stopPlaying(); |
| 151 | Medium::close(fStreamSource); |
| 152 | } |
| 153 | fStreamSource = subsession->getStreamSource(streamToken); |
| 154 | if (fStreamSource != NULL) { |
| 155 | if (fTCPSink == NULL) fTCPSink = TCPStreamSink::createNew(envir(), fClientOutputSocket); |
| 156 | fTCPSink->startPlaying(*fStreamSource, afterStreaming, this); |
| 157 | } |
| 158 | } while(0); |
| 159 | |
| 160 | delete[] streamName; |
| 161 | return; |
| 162 | } while (0); |
| 163 | |
| 164 | // "urlSuffix" does not end with "?segment=<offset-in-seconds>,<duration-in-seconds>". |
| 165 | // Construct and send a playlist that describes segments from the specified file. |
| 166 | |
| 167 | // First, make sure that the named file exists, and is streamable: |
| 168 | ServerMediaSession* session = fOurServer.lookupServerMediaSession(urlSuffix); |
| 169 | if (session == NULL) { |
| 170 | handleHTTPCmd_notFound(); |
| 171 | return; |
| 172 | } |
| 173 | |
| 174 | // To be able to construct a playlist for the requested file, we need to know its duration: |
| 175 | float duration = session->duration(); |
| 176 | if (duration <= 0.0) { |
| 177 | // We can't handle this request: |
| 178 | handleHTTPCmd_notSupported(); |
| 179 | return; |
| 180 | } |
| 181 | |
| 182 | // Now, construct the playlist. It will consist of a prefix, one or more media file specifications, and a suffix: |
| 183 | unsigned const maxIntLen = 10; // >= the maximum possible strlen() of an integer in the playlist |
| 184 | char const* const playlistPrefixFmt = |
| 185 | "#EXTM3U\r\n" |
| 186 | "#EXT-X-ALLOW-CACHE:YES\r\n" |
| 187 | "#EXT-X-MEDIA-SEQUENCE:0\r\n" |
| 188 | "#EXT-X-TARGETDURATION:%d\r\n" ; |
| 189 | unsigned const playlistPrefixFmt_maxLen = strlen(playlistPrefixFmt) + maxIntLen; |
| 190 | |
| 191 | char const* const playlistMediaFileSpecFmt = |
| 192 | "#EXTINF:%d,\r\n" |
| 193 | "%s?segment=%d,%d\r\n" ; |
| 194 | unsigned const playlistMediaFileSpecFmt_maxLen = strlen(playlistMediaFileSpecFmt) + maxIntLen + strlen(urlSuffix) + 2*maxIntLen; |
| 195 | |
| 196 | char const* const playlistSuffixFmt = |
| 197 | "#EXT-X-ENDLIST\r\n" ; |
| 198 | unsigned const playlistSuffixFmt_maxLen = strlen(playlistSuffixFmt); |
| 199 | |
| 200 | // Figure out the 'target duration' that will produce a playlist that will fit in our response buffer. (But make it at least 10s.) |
| 201 | unsigned const playlistMaxSize = 10000; |
| 202 | unsigned const mediaFileSpecsMaxSize = playlistMaxSize - (playlistPrefixFmt_maxLen + playlistSuffixFmt_maxLen); |
| 203 | unsigned const maxNumMediaFileSpecs = mediaFileSpecsMaxSize/playlistMediaFileSpecFmt_maxLen; |
| 204 | |
| 205 | unsigned targetDuration = (unsigned)(duration/maxNumMediaFileSpecs + 1); |
| 206 | if (targetDuration < 10) targetDuration = 10; |
| 207 | |
| 208 | char* playlist = new char[playlistMaxSize]; |
| 209 | char* s = playlist; |
| 210 | sprintf(s, playlistPrefixFmt, targetDuration); |
| 211 | s += strlen(s); |
| 212 | |
| 213 | unsigned durSoFar = 0; |
| 214 | while (1) { |
| 215 | unsigned dur = targetDuration < duration ? targetDuration : (unsigned)duration; |
| 216 | duration -= dur; |
| 217 | sprintf(s, playlistMediaFileSpecFmt, dur, urlSuffix, durSoFar, dur); |
| 218 | s += strlen(s); |
| 219 | if (duration < 1.0) break; |
| 220 | |
| 221 | durSoFar += dur; |
| 222 | } |
| 223 | |
| 224 | sprintf(s, playlistSuffixFmt); |
| 225 | s += strlen(s); |
| 226 | unsigned playlistLen = s - playlist; |
| 227 | |
| 228 | // Construct our response: |
| 229 | snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| 230 | "HTTP/1.1 200 OK\r\n" |
| 231 | "%s" |
| 232 | "Server: LIVE555 Streaming Media v%s\r\n" |
| 233 | "%s" |
| 234 | "Content-Length: %d\r\n" |
| 235 | "Content-Type: application/vnd.apple.mpegurl\r\n" |
| 236 | "\r\n" , |
| 237 | dateHeader(), |
| 238 | LIVEMEDIA_LIBRARY_VERSION_STRING, |
| 239 | lastModifiedHeader(urlSuffix), |
| 240 | playlistLen); |
| 241 | |
| 242 | // Send the response header now, because we're about to add more data (the playlist): |
| 243 | send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0); |
| 244 | fResponseBuffer[0] = '\0'; // We've already sent the response. This tells the calling code not to send it again. |
| 245 | |
| 246 | // Then, send the playlist. Because it's large, we don't do so using "send()", because that might not send it all at once. |
| 247 | // Instead, we stream the playlist over the TCP socket: |
| 248 | if (fPlaylistSource != NULL) { // sanity check |
| 249 | if (fTCPSink != NULL) fTCPSink->stopPlaying(); |
| 250 | Medium::close(fPlaylistSource); |
| 251 | } |
| 252 | fPlaylistSource = ByteStreamMemoryBufferSource::createNew(envir(), (u_int8_t*)playlist, playlistLen); |
| 253 | if (fTCPSink == NULL) fTCPSink = TCPStreamSink::createNew(envir(), fClientOutputSocket); |
| 254 | fTCPSink->startPlaying(*fPlaylistSource, afterStreaming, this); |
| 255 | } |
| 256 | |
| 257 | void RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming::afterStreaming(void* clientData) { |
| 258 | RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming* clientConnection |
| 259 | = (RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming*)clientData; |
| 260 | // Arrange to delete the 'client connection' object: |
| 261 | if (clientConnection->fRecursionCount > 0) { |
| 262 | // We're still in the midst of handling a request |
| 263 | clientConnection->fIsActive = False; // will cause the object to get deleted at the end of handling the request |
| 264 | } else { |
| 265 | // We're no longer handling a request; delete the object now: |
| 266 | delete clientConnection; |
| 267 | } |
| 268 | } |
| 269 | |