1/**********
2This library is free software; you can redistribute it and/or modify it under
3the terms of the GNU Lesser General Public License as published by the
4Free Software Foundation; either version 3 of the License, or (at your
5option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.)
6
7This library is distributed in the hope that it will be useful, but WITHOUT
8ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
9FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for
10more details.
11
12You should have received a copy of the GNU Lesser General Public License
13along with this library; if not, write to the Free Software Foundation, Inc.,
1451 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
29RTSPServerSupportingHTTPStreaming*
30RTSPServerSupportingHTTPStreaming::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
38RTSPServerSupportingHTTPStreaming
39::RTSPServerSupportingHTTPStreaming(UsageEnvironment& env, int ourSocket, Port rtspPort,
40 UserAuthenticationDatabase* authDatabase, unsigned reclamationTestSeconds)
41 : RTSPServer(env, ourSocket, rtspPort, authDatabase, reclamationTestSeconds) {
42}
43
44RTSPServerSupportingHTTPStreaming::~RTSPServerSupportingHTTPStreaming() {
45}
46
47GenericMediaServer::ClientConnection*
48RTSPServerSupportingHTTPStreaming::createNewClientConnection(int clientSocket, struct sockaddr_in clientAddr) {
49 return new RTSPClientConnectionSupportingHTTPStreaming(*this, clientSocket, clientAddr);
50}
51
52RTSPServerSupportingHTTPStreaming::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
58RTSPServerSupportingHTTPStreaming::RTSPClientConnectionSupportingHTTPStreaming::~RTSPClientConnectionSupportingHTTPStreaming() {
59 Medium::close(fPlaylistSource);
60 Medium::close(fStreamSource);
61 Medium::close(fTCPSink);
62}
63
64static char const* lastModifiedHeader(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
79void 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
257void 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