/* 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 #include #include #include #include #include #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; iswfHash.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; }