On Sat, 16 Jun 2012, Samuel Pitoiset wrote:

This adds two protocols, but one of them is an internal implementation
detail just used as an abstraction layer/generalization in the code. The
RTMPT protocol implementation uses rtmphttp:// as an alternative to the
tcp:// protocol. This allows moving most of the lower level logic out
from the higher level generic rtmp code.
---
- Fix missing proper dependecies in configure
- Fix capacity of the output buffer in rtmp_http_write()
- Fix a case of ==
- Return EIO when off == sizeof(client_id)
Changelog                |    1 +
configure                |    4 +
doc/general.texi         |    1 +
doc/protocols.texi       |    8 ++
libavformat/Makefile     |    1 +
libavformat/allformats.c |    2 +
libavformat/rtmphttp.c   |  220 ++++++++++++++++++++++++++++++++++++++++++++++
libavformat/rtmpproto.c  |   31 ++++++-
libavformat/version.h    |    4 +-
9 files changed, 267 insertions(+), 5 deletions(-)
create mode 100644 libavformat/rtmphttp.c

diff --git a/Changelog b/Changelog
index b80ff88..4288aa3 100644
--- a/Changelog
+++ b/Changelog
@@ -25,6 +25,7 @@ version <next>:
  be used with -of old.
- Indeo Audio decoder
- channelsplit audio filter
+- RTMPT protocol support


version 0.8:
diff --git a/configure b/configure
index 4bb2030..d614366 100755
--- a/configure
+++ b/configure
@@ -1511,6 +1511,10 @@ mmsh_protocol_select="http_protocol"
mmst_protocol_deps="network"
rtmp_protocol_deps="!librtmp_protocol"
rtmp_protocol_select="tcp_protocol"
+rtmphttp_protocol_deps="!librtmp_protocol"

Strictly speaking, this isn't needed, you can build this protocol just fine even if you enable librtmp. It's useless in that case though, so perhaps it's better this way anyway.

+rtmphttp_protocol_select="http_protocol"
+rtmpt_protocol_deps="!librtmp_protocol"
+rtmpt_protocol_select="rtmphttp_protocol"
rtp_protocol_select="udp_protocol"
sctp_protocol_deps="network netinet_sctp_h"
tcp_protocol_deps="network"
diff --git a/doc/general.texi b/doc/general.texi
index 354067d..1c81774 100644
--- a/doc/general.texi
+++ b/doc/general.texi
@@ -811,6 +811,7 @@ performance on systems without hardware floating point 
support).
@item MMS          @tab X
@item pipe         @tab X
@item RTMP         @tab X
+@item RTMPT        @tab X
@item RTP          @tab X
@item TCP          @tab X
@item UDP          @tab X
diff --git a/doc/protocols.texi b/doc/protocols.texi
index 8492033..0b4f1b1 100644
--- a/doc/protocols.texi
+++ b/doc/protocols.texi
@@ -243,6 +243,14 @@ For example to read with @command{avplay} a multimedia 
resource named
avplay rtmp://myserver/vod/sample
@end example

+@section rtmpt
+
+Real-Time Messaging Protocol tunneled through HTTP.
+
+The Real-Time Messaging Protocol tunneled through HTTP (RTMPT) is used
+for streaming multimedia content within HTTP requests to traverse
+firewalls.
+
@section rtmp, rtmpe, rtmps, rtmpt, rtmpte

Real-Time Messaging Protocol and its variants supported through
diff --git a/libavformat/Makefile b/libavformat/Makefile
index ca4f7a0..b8d93cd 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -345,6 +345,7 @@ OBJS-$(CONFIG_MMST_PROTOCOL)             += mmst.o mms.o 
asf.o
OBJS-$(CONFIG_MD5_PROTOCOL)              += md5proto.o
OBJS-$(CONFIG_PIPE_PROTOCOL)             += file.o
OBJS-$(CONFIG_RTMP_PROTOCOL)             += rtmpproto.o rtmppkt.o
+OBJS-$(CONFIG_RTMPHTTP_PROTOCOL)         += rtmphttp.o rtmpproto.o rtmppkt.o
OBJS-$(CONFIG_RTP_PROTOCOL)              += rtpproto.o
OBJS-$(CONFIG_SCTP_PROTOCOL)             += sctp.o
OBJS-$(CONFIG_TCP_PROTOCOL)              += tcp.o
diff --git a/libavformat/allformats.c b/libavformat/allformats.c
index 1320a28..69f27ab 100644
--- a/libavformat/allformats.c
+++ b/libavformat/allformats.c
@@ -256,6 +256,8 @@ void av_register_all(void)
    REGISTER_PROTOCOL (MD5,  md5);
    REGISTER_PROTOCOL (PIPE, pipe);
    REGISTER_PROTOCOL (RTMP, rtmp);
+    REGISTER_PROTOCOL (RTMPT, rtmpt);
+    REGISTER_PROTOCOL (RTMPHTTP, rtmphttp);
    REGISTER_PROTOCOL (RTP, rtp);
    REGISTER_PROTOCOL (SCTP, sctp);
    REGISTER_PROTOCOL (TCP, tcp);
diff --git a/libavformat/rtmphttp.c b/libavformat/rtmphttp.c
new file mode 100644
index 0000000..d9bf028
--- /dev/null
+++ b/libavformat/rtmphttp.c
@@ -0,0 +1,220 @@
+/*
+ * RTMP HTTP network protocol
+ * Copyright (c) 2012 Samuel Pitoiset
+ *
+ * This file is part of Libav.
+ *
+ * Libav 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.
+ *
+ * Libav 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 Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * RTMP HTTP protocol
+ */
+
+#include "libavutil/avstring.h"
+#include "libavutil/intfloat.h"
+#include "libavutil/opt.h"
+#include "internal.h"
+#include "http.h"
+
+#define RTMPT_DEFAULT_PORT 80
+
+/* protocol handler state */
+typedef enum {
+    STATE_START,        ///< client has not done anything yet
+    STATE_OPENED,       ///< client has opened the connection
+    STATE_WAITING,      ///< client is waiting for a server reply
+    STATE_READY,        ///< client is ready for sending requests
+} ClientState;
+
+/* protocol handler context */
+typedef struct RTMP_HTTPContext {
+    URLContext   *stream;           ///< HTTP stream
+    char         host[256];         ///< hostname of the server
+    int          port;              ///< port to connect (default is 80)
+    char         client_id[64];     ///< client ID used for all requests 
except the first one
+    int          seq;               ///< sequence ID used for all requests
+    ClientState  state;             ///< current state
+    uint8_t      *out_data;         ///< output buffer
+    int          out_size;          ///< current output buffer size
+    int          out_capacity;      ///< current output buffer capacity
+} RTMP_HTTPContext;
+
+static int rtmp_http_send_cmd(URLContext *h, const char *cmd)
+{
+    RTMP_HTTPContext *rt = h->priv_data;
+    char uri[2048];
+    uint8_t c;
+    int ret;
+
+    ff_url_join(uri, sizeof(uri), "http", NULL, rt->host, rt->port,
+                "/%s/%s/%d", cmd, rt->client_id, rt->seq++);
+
+    av_opt_set_bin(rt->stream->priv_data, "post_data", rt->out_data,
+                   rt->out_size, 0);
+
+    /* send a new request to the server */
+    if ((ret = ff_http_do_new_request(rt->stream, uri)) < 0)
+        return ret;
+
+    /* re-init output buffer */
+    rt->out_size = 0;
+
+    /* read the first byte which contains the polling interval */
+    if ((ret = ffurl_read(rt->stream, &c, 1)) < 0)
+        return ret;
+
+    return ret;
+}
+
+static int rtmp_http_write(URLContext *h, const uint8_t *buf, int size)
+{
+    RTMP_HTTPContext *rt = h->priv_data;
+    void *ptr;
+
+    if (rt->out_size + size > rt->out_capacity) {
+        rt->out_capacity += (rt->out_size + size) * 2;

Using += here will make it grow a bit faster than what is necessary, a plain = is enough.

+        ptr = av_realloc(rt->out_data, rt->out_capacity);
+        if (!ptr)
+            return AVERROR(ENOMEM);
+        rt->out_data = ptr;
+    }
+
+    memcpy(rt->out_data + rt->out_size, buf, size);
+    rt->out_size += size;
+
+    return size;
+}
+
+static int rtmp_http_read(URLContext *h, uint8_t *buf, int size)
+{
+    RTMP_HTTPContext *rt = h->priv_data;
+    int ret, off = 0;
+
+    /* try to read at least 1 byte of data */
+    do {
+        ret = ffurl_read(rt->stream, buf + off, size);
+        if (ret < 0 && ret != AVERROR_EOF)
+            return ret;
+
+        if (ret == AVERROR_EOF) {
+            /* When the client has reached end of file for the last request,
+             * we have to send a new request if we have buffered data.
+             * Otherwise, we have to send an idle POST. */
+            if (rt->out_size > 0) {
+                if ((ret = rtmp_http_send_cmd(h, "send")) < 0)
+                    return ret;
+            } else {
+                if ((ret = rtmp_http_write(h, "", 1)) < 0)
+                    return ret;
+
+                if ((ret = rtmp_http_send_cmd(h, "idle")) < 0)
+                    return ret;
+            }
+        } else {
+            off  += ret;
+            size -= ret;
+        }
+    } while (off <= 0);
+
+    return off;
+}
+
+static int rtmp_http_close(URLContext *h)
+{
+    RTMP_HTTPContext *rt = h->priv_data;
+    int ret = 0;
+
+    if (rt->state > STATE_OPENED) {
+        // TODO: Add support for the nonblocking variant.
+        if ((ret = rtmp_http_write(h, "", 1)) > 0)
+            ret = rtmp_http_send_cmd(h, "close");
+    }
+
+    av_freep(&rt->out_data);
+    ffurl_close(rt->stream);
+
+    return ret;
+}
+
+static int rtmp_http_open(URLContext *h, const char *uri, int flags)
+{
+    RTMP_HTTPContext *rt = h->priv_data;
+    char headers[1024], url[1024];
+    int ret, off = 0;
+
+    av_url_split(NULL, 0, NULL, 0, rt->host, sizeof(rt->host), &rt->port,
+                 NULL, 0, uri);
+
+    rt->state = STATE_START;
+    if (rt->port < 0)
+        rt->port = RTMPT_DEFAULT_PORT;
+
+    /* This is the first request that is sent to the server in order to
+     * register a client on the server and start a new session. The server
+     * replies with a unique id (usually a number) that is used by the client
+     * for all future requests.
+     * Note: the reply doesn't contain a value for the polling interval.
+     * A successful connect resets the consecutive index that is used
+     * in the URLs. */
+    ff_url_join(url, sizeof(url), "http", NULL, rt->host, rt->port, "/open/1");
+
+    /* alloc the http context */
+    if ((ret = ffurl_alloc(&rt->stream, url, AVIO_FLAG_READ_WRITE, NULL)) < 0)
+        return ret;
+
+    /* set options */
+    snprintf(headers, sizeof(headers),
+             "Cache-Control: no-cache\r\n"
+             "Content-type: application/x-fcs\r\n"
+             "User-Agent: Shockwave Flash\r\n");
+    av_opt_set(rt->stream->priv_data, "headers", headers, 0);
+    av_opt_set(rt->stream->priv_data, "multiple_requests", "1", 0);
+    av_opt_set_bin(rt->stream->priv_data, "post_data", "", 1, 0);
+
+    /* open the http context */
+    if ((ret = ffurl_connect(rt->stream, NULL)) < 0)
+        return ret;

This doesn't free stuff allocated so far in the function.

+
+    /* read the server reply which contains a unique ID */
+    for (;;) {
+        ret = ffurl_read(rt->stream, rt->client_id + off, 
sizeof(rt->client_id) - off);
+        if (ret == AVERROR_EOF)
+            break;
+        if (ret < 0)
+            return ret;
+        if (off == sizeof(rt->client_id))
+            return AVERROR(EIO);
+        off += ret;

Please swap these lines, first off += ret, then check against the end of the buffer. Otherwise, at the end of the buffer, you'll try to do a read of 0 bytes before you reach the "buffer full" check, and I'm not sure that's well defined currently.

+    }
+    while (off > 0 && isspace(rt->client_id[off - 1]))
+        rt->client_id[--off] = '\0';

Almost. If there is no whitespace at all at the end, this won't add any null termination. You could either add a rt->client_id[off] = '\0'; above it - this assumes that off < sizeof(rt->client_id), but you have a check for that in the loop so it should be ok. Another version would be this:

while (off > 0 && isspace(rt->client_id[off - 1])
    off--;
rt->client_id[off] = '\0';

I think that's the cleanest solution.

+    /* client has now opened the http tunneling connection */
+    rt->state = STATE_OPENED;
+
+    return 0;
+}

I think the state variable can be simplified. For example, STATE_START is set at the start of this function, and set to STATE_OPENED at the end if it succeeded. I think the main thing you need to keep track of is whether you have an unfinished request or not.

The things you still need is:

- Nonblocking read. If you don't have any unfinished request, return EAGAIN. If you have an unfinished request, try to read data from the server. If the server returns EOF, it means there's no more data you can immediately return to the caller, thus return EAGAIN. (In blocking read mode, you should instead send queued data or send an idle request here, but for nonblocking reads you just indicate that there's no more data to read.)

- You need to readd a shutdown() function for flushing data to write. You only need to call it in rtmp_write - for reading, it's totally enough when we send queued data in rtmp_http_read. This shutdown/flush function will send the queued data if there is no request ongoing, otherwise just return without doing anything.

The point here is how all these will work together when publishing data. Normally you just do a lot of writes - you queue data to send, then you'll send it via the flush/shutdown call. Now you have an ongoing request, and will not be able to send another request until you have finished this request. Nothing will finish this request since you (previously) don't read anything from the input.

Now the nonblocking read will save you - in most cases, the reply from the server is empty. The nonblocking read will try to read data, will return EOF, return EAGAIN to the caller. Now you don't have any more unfinished request, and you will be able to send more data in a new request the next time.

If the reply from the server isn't empty (once in a while, when the server sends a ping), the nonblocking read will return the first byte of this to the caller, and the normal blocking read will read the rest of the request (possibly sending idle packets if needed and so on).


Also a final missing thing which I've told you before and you've changed a few times but not got really right yet. At the end, you can't send the 'close' request until you have finished any previous unfinished request. Therefore you'll need to read data in nonblocking mode, until there's no more data to return (returning EAGAIN). Only then, you've finished the previous request and can start sending the final 'close' request.

// Martin
_______________________________________________
libav-devel mailing list
libav-devel@libav.org
https://lists.libav.org/mailman/listinfo/libav-devel

Reply via email to