mirror of https://bitbucket.org/ausocean/av.git
1212 lines
29 KiB
C
1212 lines
29 KiB
C
/* HTTP-RTMP Stream Gateway
|
|
* Copyright (C) 2009 Andrej Stepanchuk
|
|
* Copyright (C) 2009-2010 Howard Chu
|
|
*
|
|
* This Program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2, or (at your option)
|
|
* any later version.
|
|
*
|
|
* This Program 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with RTMPDump; see the file COPYING. If not, write to
|
|
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
#include <signal.h>
|
|
#include <getopt.h>
|
|
|
|
#include <assert.h>
|
|
|
|
#include "librtmp/rtmp_sys.h"
|
|
#include "librtmp/log.h"
|
|
|
|
#include "thread.h"
|
|
|
|
#define RD_SUCCESS 0
|
|
#define RD_FAILED 1
|
|
#define RD_INCOMPLETE 2
|
|
|
|
#define PACKET_SIZE 1024*1024
|
|
|
|
#ifdef WIN32
|
|
#define InitSockets() {\
|
|
WORD version; \
|
|
WSADATA wsaData; \
|
|
\
|
|
version = MAKEWORD(1,1); \
|
|
WSAStartup(version, &wsaData); }
|
|
|
|
#define CleanupSockets() WSACleanup()
|
|
#else
|
|
#define InitSockets()
|
|
#define CleanupSockets()
|
|
#endif
|
|
|
|
enum
|
|
{
|
|
STREAMING_ACCEPTING,
|
|
STREAMING_IN_PROGRESS,
|
|
STREAMING_STOPPING,
|
|
STREAMING_STOPPED
|
|
};
|
|
|
|
typedef struct
|
|
{
|
|
int socket;
|
|
int state;
|
|
|
|
} STREAMING_SERVER;
|
|
|
|
STREAMING_SERVER *httpServer = 0; // server structure pointer
|
|
|
|
STREAMING_SERVER *startStreaming(const char *address, int port);
|
|
void stopStreaming(STREAMING_SERVER * server);
|
|
|
|
typedef struct
|
|
{
|
|
AVal hostname;
|
|
int rtmpport;
|
|
int protocol;
|
|
int bLiveStream; // is it a live stream? then we can't seek/resume
|
|
|
|
long int timeout; // timeout connection after 120 seconds
|
|
uint32_t bufferTime;
|
|
|
|
char *rtmpurl;
|
|
AVal fullUrl;
|
|
AVal playpath;
|
|
AVal swfUrl;
|
|
AVal tcUrl;
|
|
AVal pageUrl;
|
|
AVal app;
|
|
AVal auth;
|
|
AVal swfHash;
|
|
AVal flashVer;
|
|
AVal token;
|
|
AVal subscribepath;
|
|
AVal usherToken; //Justin.tv auth token
|
|
AVal sockshost;
|
|
AMFObject extras;
|
|
int edepth;
|
|
uint32_t swfSize;
|
|
int swfAge;
|
|
int swfVfy;
|
|
|
|
uint32_t dStartOffset;
|
|
uint32_t dStopOffset;
|
|
|
|
#ifdef CRYPTO
|
|
unsigned char hash[RTMP_SWF_HASHLEN];
|
|
#endif
|
|
} RTMP_REQUEST;
|
|
|
|
#define STR2AVAL(av,str) av.av_val = str; av.av_len = strlen(av.av_val)
|
|
|
|
int
|
|
parseAMF(AMFObject *obj, const char *arg, int *depth)
|
|
{
|
|
AMFObjectProperty prop = {{0,0}};
|
|
int i;
|
|
char *p;
|
|
|
|
if (arg[1] == ':')
|
|
{
|
|
p = (char *)arg+2;
|
|
switch(arg[0])
|
|
{
|
|
case 'B':
|
|
prop.p_type = AMF_BOOLEAN;
|
|
prop.p_vu.p_number = atoi(p);
|
|
break;
|
|
case 'S':
|
|
prop.p_type = AMF_STRING;
|
|
STR2AVAL(prop.p_vu.p_aval,p);
|
|
break;
|
|
case 'N':
|
|
prop.p_type = AMF_NUMBER;
|
|
prop.p_vu.p_number = strtod(p, NULL);
|
|
break;
|
|
case 'Z':
|
|
prop.p_type = AMF_NULL;
|
|
break;
|
|
case 'O':
|
|
i = atoi(p);
|
|
if (i)
|
|
{
|
|
prop.p_type = AMF_OBJECT;
|
|
}
|
|
else
|
|
{
|
|
(*depth)--;
|
|
return 0;
|
|
}
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
else if (arg[2] == ':' && arg[0] == 'N')
|
|
{
|
|
p = strchr(arg+3, ':');
|
|
if (!p || !*depth)
|
|
return -1;
|
|
prop.p_name.av_val = (char *)arg+3;
|
|
prop.p_name.av_len = p - (arg+3);
|
|
|
|
p++;
|
|
switch(arg[1])
|
|
{
|
|
case 'B':
|
|
prop.p_type = AMF_BOOLEAN;
|
|
prop.p_vu.p_number = atoi(p);
|
|
break;
|
|
case 'S':
|
|
prop.p_type = AMF_STRING;
|
|
STR2AVAL(prop.p_vu.p_aval,p);
|
|
break;
|
|
case 'N':
|
|
prop.p_type = AMF_NUMBER;
|
|
prop.p_vu.p_number = strtod(p, NULL);
|
|
break;
|
|
case 'O':
|
|
prop.p_type = AMF_OBJECT;
|
|
break;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
else
|
|
return -1;
|
|
|
|
if (*depth)
|
|
{
|
|
AMFObject *o2;
|
|
for (i=0; i<*depth; i++)
|
|
{
|
|
o2 = &obj->o_props[obj->o_num-1].p_vu.p_object;
|
|
obj = o2;
|
|
}
|
|
}
|
|
AMF_AddProp(obj, &prop);
|
|
if (prop.p_type == AMF_OBJECT)
|
|
(*depth)++;
|
|
return 0;
|
|
}
|
|
|
|
/* this request is formed from the parameters and used to initialize a new request,
|
|
* thus it is a default settings list. All settings can be overriden by specifying the
|
|
* parameters in the GET request. */
|
|
RTMP_REQUEST defaultRTMPRequest;
|
|
|
|
int ParseOption(char opt, char *arg, RTMP_REQUEST * req);
|
|
|
|
#ifdef _DEBUG
|
|
uint32_t debugTS = 0;
|
|
|
|
int pnum = 0;
|
|
|
|
FILE *netstackdump = NULL;
|
|
FILE *netstackdump_read = NULL;
|
|
#endif
|
|
|
|
/* inplace http unescape. This is possible .. strlen(unescaped_string) <= strlen(esacped_string) */
|
|
void
|
|
http_unescape(char *data)
|
|
{
|
|
char hex[3];
|
|
char *stp;
|
|
int src_x = 0;
|
|
int dst_x = 0;
|
|
|
|
int length = (int) strlen(data);
|
|
hex[2] = 0;
|
|
|
|
while (src_x < length)
|
|
{
|
|
if (strncmp(data + src_x, "%", 1) == 0 && src_x + 2 < length)
|
|
{
|
|
//
|
|
// Since we encountered a '%' we know this is an escaped character
|
|
//
|
|
hex[0] = data[src_x + 1];
|
|
hex[1] = data[src_x + 2];
|
|
data[dst_x] = (char) strtol(hex, &stp, 16);
|
|
dst_x += 1;
|
|
src_x += 3;
|
|
}
|
|
else if (src_x != dst_x)
|
|
{
|
|
//
|
|
// This doesn't need to be unescaped. If we didn't unescape anything previously
|
|
// there is no need to copy the string either
|
|
//
|
|
data[dst_x] = data[src_x];
|
|
src_x += 1;
|
|
dst_x += 1;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// This doesn't need to be unescaped, however we need to copy the string
|
|
//
|
|
src_x += 1;
|
|
dst_x += 1;
|
|
}
|
|
}
|
|
data[dst_x] = '\0';
|
|
}
|
|
|
|
TFTYPE
|
|
controlServerThread(void *unused)
|
|
{
|
|
char ich;
|
|
while (1)
|
|
{
|
|
ich = getchar();
|
|
switch (ich)
|
|
{
|
|
case 'q':
|
|
RTMP_LogPrintf("Exiting\n");
|
|
stopStreaming(httpServer);
|
|
exit(0);
|
|
break;
|
|
default:
|
|
RTMP_LogPrintf("Unknown command \'%c\', ignoring\n", ich);
|
|
}
|
|
}
|
|
TFRET();
|
|
}
|
|
|
|
/*
|
|
ssize_t readHTTPLine(int sockfd, char *buffer, size_t length)
|
|
{
|
|
size_t i=0;
|
|
|
|
while(i < length-1) {
|
|
char c;
|
|
int n = read(sockfd, &c, 1);
|
|
|
|
if(n == 0)
|
|
break;
|
|
|
|
buffer[i] = c;
|
|
i++;
|
|
|
|
if(c == '\n')
|
|
break;
|
|
}
|
|
buffer[i]='\0';
|
|
i++;
|
|
|
|
return i;
|
|
}
|
|
|
|
int isHTTPRequestEOF(char *line, size_t length)
|
|
{
|
|
if(length < 2)
|
|
return TRUE;
|
|
|
|
if(line[0]=='\r' && line[1]=='\n')
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
*/
|
|
|
|
void processTCPrequest(STREAMING_SERVER * server, // server socket and state (our listening socket)
|
|
int sockfd // client connection socket
|
|
)
|
|
{
|
|
char buf[512] = { 0 }; // answer buffer
|
|
char header[2048] = { 0 }; // request header
|
|
char *filename = NULL; // GET request: file name //512 not enuf
|
|
char *buffer = NULL; // stream buffer
|
|
char *ptr = NULL; // header pointer
|
|
int len;
|
|
|
|
size_t nRead = 0;
|
|
|
|
char srvhead[] = "\r\nServer: HTTP-RTMP Stream Server " RTMPDUMP_VERSION "\r\n";
|
|
|
|
char *status = "404 Not Found";
|
|
|
|
server->state = STREAMING_IN_PROGRESS;
|
|
|
|
RTMP rtmp = { 0 };
|
|
uint32_t dSeek = 0; // can be used to start from a later point in the stream
|
|
|
|
// reset RTMP options to defaults specified upon invokation of streams
|
|
RTMP_REQUEST req;
|
|
memcpy(&req, &defaultRTMPRequest, sizeof(RTMP_REQUEST));
|
|
|
|
// timeout for http requests
|
|
fd_set fds;
|
|
struct timeval tv;
|
|
|
|
memset(&tv, 0, sizeof(struct timeval));
|
|
tv.tv_sec = 5;
|
|
|
|
// go through request lines
|
|
//do {
|
|
FD_ZERO(&fds);
|
|
FD_SET(sockfd, &fds);
|
|
|
|
if (select(sockfd + 1, &fds, NULL, NULL, &tv) <= 0)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "Request timeout/select failed, ignoring request");
|
|
goto quit;
|
|
}
|
|
else
|
|
{
|
|
nRead = recv(sockfd, header, 2047, 0);
|
|
header[2047] = '\0';
|
|
|
|
RTMP_Log(RTMP_LOGDEBUG, "%s: header: %s", __FUNCTION__, header);
|
|
|
|
if (strstr(header, "Range: bytes=") != 0)
|
|
{
|
|
// TODO check range starts from 0 and asking till the end.
|
|
RTMP_LogPrintf("%s, Range request not supported\n", __FUNCTION__);
|
|
len = sprintf(buf, "HTTP/1.0 416 Requested Range Not Satisfiable%s\r\n",
|
|
srvhead);
|
|
send(sockfd, buf, len, 0);
|
|
goto quit;
|
|
}
|
|
|
|
if (strncmp(header, "GET", 3) == 0 && nRead > 4)
|
|
{
|
|
filename = header + 4;
|
|
|
|
// filter " HTTP/..." from end of request
|
|
char *p = filename;
|
|
while (*p != '\0')
|
|
{
|
|
if (*p == ' ')
|
|
{
|
|
*p = '\0';
|
|
break;
|
|
}
|
|
p++;
|
|
}
|
|
}
|
|
}
|
|
//} while(!isHTTPRequestEOF(header, nRead));
|
|
|
|
// if we got a filename from the GET method
|
|
if (filename != NULL)
|
|
{
|
|
RTMP_Log(RTMP_LOGDEBUG, "%s: Request header: %s", __FUNCTION__, filename);
|
|
if (filename[0] == '/')
|
|
{ // if its not empty, is it /?
|
|
ptr = filename + 1;
|
|
|
|
// parse parameters
|
|
if (*ptr == '?')
|
|
{
|
|
ptr++;
|
|
int len = strlen(ptr);
|
|
|
|
while (len >= 2)
|
|
{
|
|
char ich = *ptr;
|
|
ptr++;
|
|
if (*ptr != '=')
|
|
goto filenotfound; // long parameters not (yet) supported
|
|
|
|
ptr++;
|
|
len -= 2;
|
|
|
|
// get position of the next '&'
|
|
char *temp;
|
|
|
|
unsigned int nArgLen = len;
|
|
if ((temp = strstr(ptr, "&")) != 0)
|
|
{
|
|
nArgLen = temp - ptr;
|
|
}
|
|
|
|
char *arg = (char *) malloc((nArgLen + 1) * sizeof(char));
|
|
memcpy(arg, ptr, nArgLen * sizeof(char));
|
|
arg[nArgLen] = '\0';
|
|
|
|
//RTMP_Log(RTMP_LOGDEBUG, "%s: unescaping parameter: %s", __FUNCTION__, arg);
|
|
http_unescape(arg);
|
|
|
|
RTMP_Log(RTMP_LOGDEBUG, "%s: parameter: %c, arg: %s", __FUNCTION__,
|
|
ich, arg);
|
|
|
|
ptr += nArgLen + 1;
|
|
len -= nArgLen + 1;
|
|
|
|
if (!ParseOption(ich, arg, &req))
|
|
{
|
|
status = "400 unknown option";
|
|
goto filenotfound;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
goto filenotfound;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RTMP_LogPrintf("%s: No request header received/unsupported method\n",
|
|
__FUNCTION__);
|
|
}
|
|
|
|
// do necessary checks right here to make sure the combined request of default values and GET parameters is correct
|
|
if (!req.hostname.av_len && !req.fullUrl.av_len)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR,
|
|
"You must specify a hostname (--host) or url (-r \"rtmp://host[:port]/playpath\") containing a hostname");
|
|
status = "400 Missing Hostname";
|
|
goto filenotfound;
|
|
}
|
|
if (req.playpath.av_len == 0 && !req.fullUrl.av_len)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR,
|
|
"You must specify a playpath (--playpath) or url (-r \"rtmp://host[:port]/playpath\") containing a playpath");
|
|
status = "400 Missing Playpath";
|
|
goto filenotfound;;
|
|
}
|
|
|
|
if (req.protocol == RTMP_PROTOCOL_UNDEFINED && !req.fullUrl.av_len)
|
|
{
|
|
RTMP_Log(RTMP_LOGWARNING,
|
|
"You haven't specified a protocol (--protocol) or rtmp url (-r), using default protocol RTMP");
|
|
req.protocol = RTMP_PROTOCOL_RTMP;
|
|
}
|
|
if (req.rtmpport == -1 && !req.fullUrl.av_len)
|
|
{
|
|
RTMP_Log(RTMP_LOGWARNING,
|
|
"You haven't specified a port (--port) or rtmp url (-r), using default port");
|
|
req.rtmpport = 0;
|
|
}
|
|
if (req.rtmpport == 0 && !req.fullUrl.av_len)
|
|
{
|
|
if (req.protocol & RTMP_FEATURE_SSL)
|
|
req.rtmpport = 443;
|
|
else if (req.protocol & RTMP_FEATURE_HTTP)
|
|
req.rtmpport = 80;
|
|
else
|
|
req.rtmpport = 1935;
|
|
}
|
|
|
|
if (req.tcUrl.av_len == 0)
|
|
{
|
|
char str[512] = { 0 };
|
|
req.tcUrl.av_len = snprintf(str, 511, "%s://%.*s:%d/%.*s",
|
|
RTMPProtocolStringsLower[req.protocol], req.hostname.av_len,
|
|
req.hostname.av_val, req.rtmpport, req.app.av_len, req.app.av_val);
|
|
req.tcUrl.av_val = (char *) malloc(req.tcUrl.av_len + 1);
|
|
strcpy(req.tcUrl.av_val, str);
|
|
}
|
|
|
|
if (req.swfVfy)
|
|
{
|
|
#ifdef CRYPTO
|
|
if (RTMP_HashSWF(req.swfUrl.av_val, &req.swfSize, req.hash, req.swfAge) == 0)
|
|
{
|
|
req.swfHash.av_val = (char *)req.hash;
|
|
req.swfHash.av_len = RTMP_SWF_HASHLEN;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// after validation of the http request send response header
|
|
len = sprintf(buf, "HTTP/1.0 200 OK%sContent-Type: video/flv\r\n\r\n", srvhead);
|
|
send(sockfd, buf, len, 0);
|
|
|
|
// send the packets
|
|
buffer = (char *) calloc(PACKET_SIZE, 1);
|
|
|
|
// User defined seek offset
|
|
if (req.dStartOffset > 0)
|
|
{
|
|
if (req.bLiveStream)
|
|
RTMP_Log(RTMP_LOGWARNING,
|
|
"Can't seek in a live stream, ignoring --seek option");
|
|
else
|
|
dSeek += req.dStartOffset;
|
|
}
|
|
|
|
if (dSeek != 0)
|
|
{
|
|
RTMP_LogPrintf("Starting at TS: %d ms\n", dSeek);
|
|
}
|
|
|
|
RTMP_Log(RTMP_LOGDEBUG, "Setting buffer time to: %dms", req.bufferTime);
|
|
RTMP_Init(&rtmp);
|
|
RTMP_SetBufferMS(&rtmp, req.bufferTime);
|
|
if (!req.fullUrl.av_len)
|
|
{
|
|
RTMP_SetupStream(&rtmp, req.protocol, &req.hostname, req.rtmpport, &req.sockshost,
|
|
&req.playpath, &req.tcUrl, &req.swfUrl, &req.pageUrl, &req.app, &req.auth, &req.swfHash, req.swfSize, &req.flashVer, &req.subscribepath, &req.usherToken, dSeek, req.dStopOffset,
|
|
req.bLiveStream, req.timeout);
|
|
}
|
|
else
|
|
{
|
|
if (RTMP_SetupURL(&rtmp, req.fullUrl.av_val) == FALSE)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "Couldn't parse URL: %s", req.fullUrl.av_val);
|
|
return;
|
|
}
|
|
}
|
|
/* backward compatibility, we always sent this as true before */
|
|
if (req.auth.av_len)
|
|
rtmp.Link.lFlags |= RTMP_LF_AUTH;
|
|
|
|
rtmp.Link.extras = req.extras;
|
|
rtmp.Link.token = req.token;
|
|
rtmp.m_read.timestamp = dSeek;
|
|
|
|
RTMP_LogPrintf("Connecting ... port: %d, app: %s\n", req.rtmpport, req.app.av_val);
|
|
if (!RTMP_Connect(&rtmp, NULL))
|
|
{
|
|
RTMP_LogPrintf("%s, failed to connect!\n", __FUNCTION__);
|
|
}
|
|
else
|
|
{
|
|
unsigned long size = 0;
|
|
double percent = 0;
|
|
double duration = 0.0;
|
|
|
|
int nWritten = 0;
|
|
int nRead = 0;
|
|
|
|
do
|
|
{
|
|
nRead = RTMP_Read(&rtmp, buffer, PACKET_SIZE);
|
|
|
|
if (nRead > 0)
|
|
{
|
|
if ((nWritten = send(sockfd, buffer, nRead, 0)) < 0)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "%s, sending failed, error: %d", __FUNCTION__,
|
|
GetSockError());
|
|
goto cleanup; // we are in STREAMING_IN_PROGRESS, so we'll go to STREAMING_ACCEPTING
|
|
}
|
|
|
|
size += nRead;
|
|
|
|
//RTMP_LogPrintf("write %dbytes (%.1f KB)\n", nRead, nRead/1024.0);
|
|
if (duration <= 0) // if duration unknown try to get it from the stream (onMetaData)
|
|
duration = RTMP_GetDuration(&rtmp);
|
|
|
|
if (duration > 0)
|
|
{
|
|
percent =
|
|
((double) (dSeek + rtmp.m_read.timestamp)) / (duration *
|
|
1000.0) * 100.0;
|
|
percent = ((double) (int) (percent * 10.0)) / 10.0;
|
|
RTMP_LogStatus("\r%.3f KB / %.2f sec (%.1f%%)",
|
|
(double) size / 1024.0,
|
|
(double) (rtmp.m_read.timestamp) / 1000.0, percent);
|
|
}
|
|
else
|
|
{
|
|
RTMP_LogStatus("\r%.3f KB / %.2f sec", (double) size / 1024.0,
|
|
(double) (rtmp.m_read.timestamp) / 1000.0);
|
|
}
|
|
}
|
|
#ifdef _DEBUG
|
|
else
|
|
{
|
|
RTMP_Log(RTMP_LOGDEBUG, "zero read!");
|
|
}
|
|
#endif
|
|
}
|
|
while (server->state == STREAMING_IN_PROGRESS && nRead > -1
|
|
&& RTMP_IsConnected(&rtmp) && nWritten >= 0);
|
|
}
|
|
cleanup:
|
|
RTMP_LogPrintf("Closing connection... ");
|
|
RTMP_Close(&rtmp);
|
|
RTMP_LogPrintf("done!\n\n");
|
|
|
|
quit:
|
|
if (buffer)
|
|
{
|
|
free(buffer);
|
|
buffer = NULL;
|
|
}
|
|
|
|
if (sockfd)
|
|
closesocket(sockfd);
|
|
|
|
if (server->state == STREAMING_IN_PROGRESS)
|
|
server->state = STREAMING_ACCEPTING;
|
|
|
|
return;
|
|
|
|
filenotfound:
|
|
RTMP_LogPrintf("%s, %s, %s\n", __FUNCTION__, status, filename);
|
|
len = sprintf(buf, "HTTP/1.0 %s%s\r\n", status, srvhead);
|
|
send(sockfd, buf, len, 0);
|
|
goto quit;
|
|
}
|
|
|
|
TFTYPE
|
|
serverThread(void *arg)
|
|
{
|
|
STREAMING_SERVER *server = arg;
|
|
server->state = STREAMING_ACCEPTING;
|
|
|
|
while (server->state == STREAMING_ACCEPTING)
|
|
{
|
|
struct sockaddr_in addr;
|
|
socklen_t addrlen = sizeof(struct sockaddr_in);
|
|
int sockfd =
|
|
accept(server->socket, (struct sockaddr *) &addr, &addrlen);
|
|
|
|
if (sockfd > 0)
|
|
{
|
|
// Create a new process and transfer the control to that
|
|
RTMP_Log(RTMP_LOGDEBUG, "%s: accepted connection from %s\n", __FUNCTION__,
|
|
inet_ntoa(addr.sin_addr));
|
|
processTCPrequest(server, sockfd);
|
|
RTMP_Log(RTMP_LOGDEBUG, "%s: processed request\n", __FUNCTION__);
|
|
}
|
|
else
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "%s: accept failed", __FUNCTION__);
|
|
}
|
|
}
|
|
server->state = STREAMING_STOPPED;
|
|
TFRET();
|
|
}
|
|
|
|
STREAMING_SERVER *
|
|
startStreaming(const char *address, int port)
|
|
{
|
|
struct sockaddr_in addr;
|
|
int sockfd;
|
|
STREAMING_SERVER *server;
|
|
|
|
sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
if (sockfd == -1)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "%s, couldn't create socket", __FUNCTION__);
|
|
return 0;
|
|
}
|
|
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_addr.s_addr = inet_addr(address); //htonl(INADDR_ANY);
|
|
addr.sin_port = htons(port);
|
|
|
|
if (bind(sockfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_in)) ==
|
|
-1)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "%s, TCP bind failed for port number: %d", __FUNCTION__,
|
|
port);
|
|
return 0;
|
|
}
|
|
|
|
if (listen(sockfd, 10) == -1)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "%s, listen failed", __FUNCTION__);
|
|
closesocket(sockfd);
|
|
return 0;
|
|
}
|
|
|
|
server = (STREAMING_SERVER *) calloc(1, sizeof(STREAMING_SERVER));
|
|
server->socket = sockfd;
|
|
|
|
ThreadCreate(serverThread, server);
|
|
|
|
return server;
|
|
}
|
|
|
|
void
|
|
stopStreaming(STREAMING_SERVER * server)
|
|
{
|
|
assert(server);
|
|
|
|
if (server->state != STREAMING_STOPPED)
|
|
{
|
|
if (server->state == STREAMING_IN_PROGRESS)
|
|
{
|
|
server->state = STREAMING_STOPPING;
|
|
|
|
// wait for streaming threads to exit
|
|
while (server->state != STREAMING_STOPPED)
|
|
msleep(1);
|
|
}
|
|
|
|
if (closesocket(server->socket))
|
|
RTMP_Log(RTMP_LOGERROR, "%s: Failed to close listening socket, error %d",
|
|
__FUNCTION__, GetSockError());
|
|
|
|
server->state = STREAMING_STOPPED;
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
sigIntHandler(int sig)
|
|
{
|
|
RTMP_ctrlC = TRUE;
|
|
RTMP_LogPrintf("Caught signal: %d, cleaning up, just a second...\n", sig);
|
|
if (httpServer)
|
|
stopStreaming(httpServer);
|
|
signal(SIGINT, SIG_DFL);
|
|
}
|
|
|
|
#define HEX2BIN(a) (((a)&0x40)?((a)&0xf)+9:((a)&0xf))
|
|
int hex2bin(char *str, char **hex)
|
|
{
|
|
char *ptr;
|
|
int i, l = strlen(str);
|
|
|
|
if (l & 1)
|
|
return 0;
|
|
|
|
*hex = malloc(l/2);
|
|
ptr = *hex;
|
|
if (!ptr)
|
|
return 0;
|
|
|
|
for (i=0; i<l; i+=2)
|
|
*ptr++ = (HEX2BIN(str[i]) << 4) | HEX2BIN(str[i+1]);
|
|
return l/2;
|
|
}
|
|
|
|
// this will parse RTMP related options as needed
|
|
// excludes the following options: h, d, g
|
|
|
|
// Return values: true (option parsing ok)
|
|
// false (option not parsed/invalid)
|
|
int
|
|
ParseOption(char opt, char *arg, RTMP_REQUEST * req)
|
|
{
|
|
switch (opt)
|
|
{
|
|
#ifdef CRYPTO
|
|
case 'w':
|
|
{
|
|
int res = hex2bin(arg, &req->swfHash.av_val);
|
|
if (!res || res != RTMP_SWF_HASHLEN)
|
|
{
|
|
req->swfHash.av_val = NULL;
|
|
RTMP_Log(RTMP_LOGWARNING,
|
|
"Couldn't parse swf hash hex string, not hexstring or not %d bytes, ignoring!", RTMP_SWF_HASHLEN);
|
|
}
|
|
req->swfHash.av_len = RTMP_SWF_HASHLEN;
|
|
break;
|
|
}
|
|
case 'x':
|
|
{
|
|
int size = atoi(arg);
|
|
if (size <= 0)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "SWF Size must be at least 1, ignoring\n");
|
|
}
|
|
else
|
|
{
|
|
req->swfSize = size;
|
|
}
|
|
break;
|
|
}
|
|
case 'W':
|
|
{
|
|
STR2AVAL(req->swfUrl, arg);
|
|
req->swfVfy = 1;
|
|
}
|
|
break;
|
|
case 'X':
|
|
{
|
|
int num = atoi(arg);
|
|
if (num < 0)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "SWF Age must be non-negative, ignoring\n");
|
|
}
|
|
else
|
|
{
|
|
req->swfAge = num;
|
|
}
|
|
break;
|
|
}
|
|
#endif
|
|
case 'b':
|
|
{
|
|
int32_t bt = atol(arg);
|
|
if (bt < 0)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR,
|
|
"Buffer time must be greater than zero, ignoring the specified value %d!",
|
|
bt);
|
|
}
|
|
else
|
|
{
|
|
req->bufferTime = bt;
|
|
}
|
|
break;
|
|
}
|
|
case 'v':
|
|
req->bLiveStream = TRUE; // no seeking or resuming possible!
|
|
break;
|
|
case 'd':
|
|
STR2AVAL(req->subscribepath, arg);
|
|
break;
|
|
case 'n':
|
|
STR2AVAL(req->hostname, arg);
|
|
break;
|
|
case 'c':
|
|
req->rtmpport = atoi(arg);
|
|
break;
|
|
case 'l':
|
|
{
|
|
int protocol = atoi(arg);
|
|
if (protocol < RTMP_PROTOCOL_RTMP || protocol > RTMP_PROTOCOL_RTMPTS)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "Unknown protocol specified: %d, using default",
|
|
protocol);
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
req->protocol = protocol;
|
|
}
|
|
break;
|
|
}
|
|
case 'y':
|
|
STR2AVAL(req->playpath, arg);
|
|
break;
|
|
case 'r':
|
|
{
|
|
req->rtmpurl = arg;
|
|
|
|
AVal parsedHost, parsedPlaypath, parsedApp;
|
|
unsigned int parsedPort = 0;
|
|
int parsedProtocol = RTMP_PROTOCOL_UNDEFINED;
|
|
|
|
if (!RTMP_ParseURL
|
|
(req->rtmpurl, &parsedProtocol, &parsedHost, &parsedPort,
|
|
&parsedPlaypath, &parsedApp))
|
|
{
|
|
RTMP_Log(RTMP_LOGWARNING, "Couldn't parse the specified url (%s)!", arg);
|
|
}
|
|
else
|
|
{
|
|
if (!req->hostname.av_len)
|
|
req->hostname = parsedHost;
|
|
if (req->rtmpport == -1)
|
|
req->rtmpport = parsedPort;
|
|
if (req->playpath.av_len == 0 && parsedPlaypath.av_len)
|
|
{
|
|
req->playpath = parsedPlaypath;
|
|
}
|
|
if (req->protocol == RTMP_PROTOCOL_UNDEFINED)
|
|
req->protocol = parsedProtocol;
|
|
if (req->app.av_len == 0 && parsedApp.av_len)
|
|
{
|
|
req->app = parsedApp;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'i':
|
|
STR2AVAL(req->fullUrl, arg);
|
|
break;
|
|
case 's':
|
|
STR2AVAL(req->swfUrl, arg);
|
|
break;
|
|
case 't':
|
|
STR2AVAL(req->tcUrl, arg);
|
|
break;
|
|
case 'p':
|
|
STR2AVAL(req->pageUrl, arg);
|
|
break;
|
|
case 'a':
|
|
STR2AVAL(req->app, arg);
|
|
break;
|
|
case 'f':
|
|
STR2AVAL(req->flashVer, arg);
|
|
break;
|
|
case 'u':
|
|
STR2AVAL(req->auth, arg);
|
|
break;
|
|
case 'C':
|
|
parseAMF(&req->extras, arg, &req->edepth);
|
|
break;
|
|
case 'm':
|
|
req->timeout = atoi(arg);
|
|
break;
|
|
case 'A':
|
|
req->dStartOffset = (int)(atof(arg) * 1000.0);
|
|
//printf("dStartOffset = %d\n", dStartOffset);
|
|
break;
|
|
case 'B':
|
|
req->dStopOffset = (int)(atof(arg) * 1000.0);
|
|
//printf("dStartOffset = %d\n", dStartOffset);
|
|
break;
|
|
case 'T':
|
|
STR2AVAL(req->token, arg);
|
|
break;
|
|
case 'S':
|
|
STR2AVAL(req->sockshost, arg);
|
|
case 'q':
|
|
RTMP_debuglevel = RTMP_LOGCRIT;
|
|
break;
|
|
case 'V':
|
|
RTMP_debuglevel = RTMP_LOGDEBUG;
|
|
break;
|
|
case 'z':
|
|
RTMP_debuglevel = RTMP_LOGALL;
|
|
break;
|
|
case 'j':
|
|
STR2AVAL(req->usherToken, arg);
|
|
break;
|
|
default:
|
|
RTMP_LogPrintf("unknown option: %c, arg: %s\n", opt, arg);
|
|
return FALSE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
int nStatus = RD_SUCCESS;
|
|
|
|
// http streaming server
|
|
char DEFAULT_HTTP_STREAMING_DEVICE[] = "0.0.0.0"; // 0.0.0.0 is any device
|
|
|
|
char *httpStreamingDevice = DEFAULT_HTTP_STREAMING_DEVICE; // streaming device, default 0.0.0.0
|
|
int nHttpStreamingPort = 80; // port
|
|
|
|
RTMP_LogPrintf("HTTP-RTMP Stream Gateway %s\n", RTMPDUMP_VERSION);
|
|
RTMP_LogPrintf("(c) 2010 Andrej Stepanchuk, Howard Chu; license: GPL\n\n");
|
|
|
|
// init request
|
|
memset(&defaultRTMPRequest, 0, sizeof(RTMP_REQUEST));
|
|
|
|
defaultRTMPRequest.rtmpport = -1;
|
|
defaultRTMPRequest.protocol = RTMP_PROTOCOL_UNDEFINED;
|
|
defaultRTMPRequest.bLiveStream = FALSE; // is it a live stream? then we can't seek/resume
|
|
|
|
defaultRTMPRequest.timeout = 120; // timeout connection after 120 seconds
|
|
defaultRTMPRequest.bufferTime = 20 * 1000;
|
|
|
|
defaultRTMPRequest.swfAge = 30;
|
|
|
|
int opt;
|
|
struct option longopts[] = {
|
|
{"help", 0, NULL, 'h'},
|
|
{"url", 1, NULL, 'i'},
|
|
{"host", 1, NULL, 'n'},
|
|
{"port", 1, NULL, 'c'},
|
|
{"socks", 1, NULL, 'S'},
|
|
{"protocol", 1, NULL, 'l'},
|
|
{"playpath", 1, NULL, 'y'},
|
|
{"rtmp", 1, NULL, 'r'},
|
|
{"swfUrl", 1, NULL, 's'},
|
|
{"tcUrl", 1, NULL, 't'},
|
|
{"pageUrl", 1, NULL, 'p'},
|
|
{"app", 1, NULL, 'a'},
|
|
#ifdef CRYPTO
|
|
{"swfhash", 1, NULL, 'w'},
|
|
{"swfsize", 1, NULL, 'x'},
|
|
{"swfVfy", 1, NULL, 'W'},
|
|
{"swfAge", 1, NULL, 'X'},
|
|
#endif
|
|
{"auth", 1, NULL, 'u'},
|
|
{"conn", 1, NULL, 'C'},
|
|
{"flashVer", 1, NULL, 'f'},
|
|
{"live", 0, NULL, 'v'},
|
|
//{"flv", 1, NULL, 'o'},
|
|
//{"resume", 0, NULL, 'e'},
|
|
{"timeout", 1, NULL, 'm'},
|
|
{"buffer", 1, NULL, 'b'},
|
|
//{"skip", 1, NULL, 'k'},
|
|
{"device", 1, NULL, 'D'},
|
|
{"sport", 1, NULL, 'g'},
|
|
{"subscribe", 1, NULL, 'd'},
|
|
{"start", 1, NULL, 'A'},
|
|
{"stop", 1, NULL, 'B'},
|
|
{"token", 1, NULL, 'T'},
|
|
{"debug", 0, NULL, 'z'},
|
|
{"quiet", 0, NULL, 'q'},
|
|
{"verbose", 0, NULL, 'V'},
|
|
{"jtv", 1, NULL, 'j'},
|
|
{0, 0, 0, 0}
|
|
};
|
|
|
|
signal(SIGINT, sigIntHandler);
|
|
#ifndef WIN32
|
|
signal(SIGPIPE, SIG_IGN);
|
|
#endif
|
|
|
|
InitSockets();
|
|
|
|
while ((opt =
|
|
getopt_long(argc, argv,
|
|
"hvqVzr:s:t:i:p:a:f:u:n:c:l:y:m:d:D:A:B:T:g:w:x:W:X:S:j:", longopts,
|
|
NULL)) != -1)
|
|
{
|
|
switch (opt)
|
|
{
|
|
case 'h':
|
|
RTMP_LogPrintf
|
|
("\nThis program serves media content streamed from RTMP onto HTTP.\n\n");
|
|
RTMP_LogPrintf("--help|-h Prints this help screen.\n");
|
|
RTMP_LogPrintf
|
|
("--url|-i url URL with options included (e.g. rtmp://host[:port]/path swfUrl=url tcUrl=url)\n");
|
|
RTMP_LogPrintf
|
|
("--rtmp|-r url URL (e.g. rtmp://host[:port]/path)\n");
|
|
RTMP_LogPrintf
|
|
("--host|-n hostname Overrides the hostname in the rtmp url\n");
|
|
RTMP_LogPrintf
|
|
("--port|-c port Overrides the port in the rtmp url\n");
|
|
RTMP_LogPrintf
|
|
("--socks|-S host:port Use the specified SOCKS proxy\n");
|
|
RTMP_LogPrintf
|
|
("--protocol|-l Overrides the protocol in the rtmp url (0 - RTMP, 2 - RTMPE)\n");
|
|
RTMP_LogPrintf
|
|
("--playpath|-y Overrides the playpath parsed from rtmp url\n");
|
|
RTMP_LogPrintf("--swfUrl|-s url URL to player swf file\n");
|
|
RTMP_LogPrintf
|
|
("--tcUrl|-t url URL to played stream (default: \"rtmp://host[:port]/app\")\n");
|
|
RTMP_LogPrintf("--pageUrl|-p url Web URL of played programme\n");
|
|
RTMP_LogPrintf("--app|-a app Name of target app in server\n");
|
|
#ifdef CRYPTO
|
|
RTMP_LogPrintf
|
|
("--swfhash|-w hexstring SHA256 hash of the decompressed SWF file (32 bytes)\n");
|
|
RTMP_LogPrintf
|
|
("--swfsize|-x num Size of the decompressed SWF file, required for SWFVerification\n");
|
|
RTMP_LogPrintf
|
|
("--swfVfy|-W url URL to player swf file, compute hash/size automatically\n");
|
|
RTMP_LogPrintf
|
|
("--swfAge|-X days Number of days to use cached SWF hash before refreshing\n");
|
|
#endif
|
|
RTMP_LogPrintf
|
|
("--auth|-u string Authentication string to be appended to the connect string\n");
|
|
RTMP_LogPrintf
|
|
("--conn|-C type:data Arbitrary AMF data to be appended to the connect string\n");
|
|
RTMP_LogPrintf
|
|
(" B:boolean(0|1), S:string, N:number, O:object-flag(0|1),\n");
|
|
RTMP_LogPrintf
|
|
(" Z:(null), NB:name:boolean, NS:name:string, NN:name:number\n");
|
|
RTMP_LogPrintf
|
|
("--flashVer|-f string Flash version string (default: \"%s\")\n",
|
|
RTMP_DefaultFlashVer.av_val);
|
|
RTMP_LogPrintf
|
|
("--live|-v Get a live stream, no --resume (seeking) of live streams possible\n");
|
|
RTMP_LogPrintf
|
|
("--subscribe|-d string Stream name to subscribe to (otherwise defaults to playpath if live is specified)\n");
|
|
RTMP_LogPrintf
|
|
("--timeout|-m num Timeout connection num seconds (default: %lu)\n",
|
|
defaultRTMPRequest.timeout);
|
|
RTMP_LogPrintf
|
|
("--start|-A num Start at num seconds into stream (not valid when using --live)\n");
|
|
RTMP_LogPrintf
|
|
("--stop|-B num Stop at num seconds into stream\n");
|
|
RTMP_LogPrintf
|
|
("--token|-T key Key for SecureToken response\n");
|
|
RTMP_LogPrintf
|
|
("--jtv|-j JSON Authentication token for Justin.tv legacy servers\n");
|
|
RTMP_LogPrintf
|
|
("--buffer|-b Buffer time in milliseconds (default: %u)\n\n",
|
|
defaultRTMPRequest.bufferTime);
|
|
|
|
RTMP_LogPrintf
|
|
("--device|-D Streaming device ip address (default: %s)\n",
|
|
DEFAULT_HTTP_STREAMING_DEVICE);
|
|
RTMP_LogPrintf
|
|
("--sport|-g Streaming port (default: %d)\n\n",
|
|
nHttpStreamingPort);
|
|
RTMP_LogPrintf
|
|
("--quiet|-q Suppresses all command output.\n");
|
|
RTMP_LogPrintf("--verbose|-V Verbose command output.\n");
|
|
RTMP_LogPrintf("--debug|-z Debug level command output.\n");
|
|
RTMP_LogPrintf
|
|
("If you don't pass parameters for swfUrl, pageUrl, or auth these properties will not be included in the connect ");
|
|
RTMP_LogPrintf("packet.\n\n");
|
|
return RD_SUCCESS;
|
|
break;
|
|
// streaming server specific options
|
|
case 'D':
|
|
if (inet_addr(optarg) == INADDR_NONE)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR,
|
|
"Invalid binding address (requested address %s), ignoring",
|
|
optarg);
|
|
}
|
|
else
|
|
{
|
|
httpStreamingDevice = optarg;
|
|
}
|
|
break;
|
|
case 'g':
|
|
{
|
|
int port = atoi(optarg);
|
|
if (port < 0 || port > 65535)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR,
|
|
"Streaming port out of range (requested port %d), ignoring\n",
|
|
port);
|
|
}
|
|
else
|
|
{
|
|
nHttpStreamingPort = port;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
//RTMP_LogPrintf("unknown option: %c\n", opt);
|
|
if (!ParseOption(opt, optarg, &defaultRTMPRequest))
|
|
return RD_FAILED;
|
|
break;
|
|
}
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
netstackdump = fopen("netstackdump", "wb");
|
|
netstackdump_read = fopen("netstackdump_read", "wb");
|
|
#endif
|
|
|
|
// start text UI
|
|
ThreadCreate(controlServerThread, 0);
|
|
|
|
// start http streaming
|
|
if ((httpServer =
|
|
startStreaming(httpStreamingDevice, nHttpStreamingPort)) == 0)
|
|
{
|
|
RTMP_Log(RTMP_LOGERROR, "Failed to start HTTP server, exiting!");
|
|
return RD_FAILED;
|
|
}
|
|
RTMP_LogPrintf("Streaming on http://%s:%d\n", httpStreamingDevice,
|
|
nHttpStreamingPort);
|
|
|
|
while (httpServer->state != STREAMING_STOPPED)
|
|
{
|
|
sleep(1);
|
|
}
|
|
RTMP_Log(RTMP_LOGDEBUG, "Done, exiting...");
|
|
|
|
CleanupSockets();
|
|
|
|
#ifdef _DEBUG
|
|
if (netstackdump != 0)
|
|
fclose(netstackdump);
|
|
if (netstackdump_read != 0)
|
|
fclose(netstackdump_read);
|
|
#endif
|
|
return nStatus;
|
|
}
|