269 lines
11 KiB
C++
269 lines
11 KiB
C++
|
/**********
|
||
|
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-2016 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;
|
||
|
}
|
||
|
}
|