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 | |