]> git.sesse.net Git - ffmpeg/blobdiff - libavformat/rtmpproto.c
avf: introduce nobuffer option
[ffmpeg] / libavformat / rtmpproto.c
index 42856dbf64bc8b8876a56ba3732a4e2da4d3cf87..501e0eddc74b0a33c8ec2c7bafa18d3427f16f8e 100644 (file)
@@ -37,6 +37,7 @@
 
 #include "flv.h"
 #include "rtmp.h"
+#include "rtmpcrypt.h"
 #include "rtmppkt.h"
 #include "url.h"
 
@@ -89,9 +90,11 @@ typedef struct RTMPContext {
     char*         tcurl;                      ///< url of the target stream
     char*         flashver;                   ///< version of the flash plugin
     char*         swfurl;                     ///< url of the swf player
+    char*         pageurl;                    ///< url of the web page
     int           server_bw;                  ///< server bandwidth
     int           client_buffer_time;         ///< client buffer time in ms
     int           flush_interval;             ///< number of packets flushed in the same request (RTMPT only)
+    int           encrypted;                  ///< use an encrypted connection (RTMPE only)
 } RTMPContext;
 
 #define PLAYER_KEY_OPEN_PART_LEN 30   ///< length of partial key used for first client digest signing
@@ -230,6 +233,11 @@ static int gen_connect(URLContext *s, RTMPContext *rt)
         ff_amf_write_number(&p, 252.0);
         ff_amf_write_field_name(&p, "videoFunction");
         ff_amf_write_number(&p, 1.0);
+
+        if (rt->pageurl) {
+            ff_amf_write_field_name(&p, "pageUrl");
+            ff_amf_write_string(&p, rt->pageurl);
+        }
     }
     ff_amf_write_object_end(&p);
 
@@ -507,6 +515,12 @@ static int gen_pong(URLContext *s, RTMPContext *rt, RTMPPacket *ppkt)
     uint8_t *p;
     int ret;
 
+    if (ppkt->data_size < 6) {
+        av_log(s, AV_LOG_ERROR, "Too short ping packet (%d)\n",
+               ppkt->data_size);
+        return AVERROR_INVALIDDATA;
+    }
+
     if ((ret = ff_rtmp_packet_create(&pkt, RTMP_NETWORK_CHANNEL, RTMP_PT_PING,
                                      ppkt->timestamp + 1, 6)) < 0)
         return ret;
@@ -632,21 +646,34 @@ int ff_rtmp_calc_digest(const uint8_t *src, int len, int gap,
     return 0;
 }
 
+int ff_rtmp_calc_digest_pos(const uint8_t *buf, int off, int mod_val,
+                            int add_val)
+{
+    int i, digest_pos = 0;
+
+    for (i = 0; i < 4; i++)
+        digest_pos += buf[i + off];
+    digest_pos = digest_pos % mod_val + add_val;
+
+    return digest_pos;
+}
+
 /**
  * Put HMAC-SHA2 digest of packet data (except for the bytes where this digest
  * will be stored) into that packet.
  *
  * @param buf handshake data (1536 bytes)
+ * @param encrypted use an encrypted connection (RTMPE)
  * @return offset to the digest inside input data
  */
-static int rtmp_handshake_imprint_with_digest(uint8_t *buf)
+static int rtmp_handshake_imprint_with_digest(uint8_t *buf, int encrypted)
 {
-    int i, digest_pos = 0;
-    int ret;
+    int ret, digest_pos;
 
-    for (i = 8; i < 12; i++)
-        digest_pos += buf[i];
-    digest_pos = (digest_pos % 728) + 12;
+    if (encrypted)
+        digest_pos = ff_rtmp_calc_digest_pos(buf, 772, 728, 776);
+    else
+        digest_pos = ff_rtmp_calc_digest_pos(buf, 8, 728, 12);
 
     ret = ff_rtmp_calc_digest(buf, RTMP_HANDSHAKE_PACKET_SIZE, digest_pos,
                               rtmp_player_key, PLAYER_KEY_OPEN_PART_LEN,
@@ -666,13 +693,10 @@ static int rtmp_handshake_imprint_with_digest(uint8_t *buf)
  */
 static int rtmp_validate_digest(uint8_t *buf, int off)
 {
-    int i, digest_pos = 0;
     uint8_t digest[32];
-    int ret;
+    int ret, digest_pos;
 
-    for (i = 0; i < 4; i++)
-        digest_pos += buf[i + off];
-    digest_pos = (digest_pos % 728) + off + 4;
+    digest_pos = ff_rtmp_calc_digest_pos(buf, off, 728, off + 4);
 
     ret = ff_rtmp_calc_digest(buf, RTMP_HANDSHAKE_PACKET_SIZE, digest_pos,
                               rtmp_server_key, SERVER_KEY_OPEN_PART_LEN,
@@ -706,8 +730,8 @@ static int rtmp_handshake(URLContext *s, RTMPContext *rt)
     uint8_t serverdata[RTMP_HANDSHAKE_PACKET_SIZE+1];
     int i;
     int server_pos, client_pos;
-    uint8_t digest[32];
-    int ret;
+    uint8_t digest[32], signature[32];
+    int ret, type = 0;
 
     av_log(s, AV_LOG_DEBUG, "Handshaking...\n");
 
@@ -715,7 +739,24 @@ static int rtmp_handshake(URLContext *s, RTMPContext *rt)
     // generate handshake packet - 1536 bytes of pseudorandom data
     for (i = 9; i <= RTMP_HANDSHAKE_PACKET_SIZE; i++)
         tosend[i] = av_lfg_get(&rnd) >> 24;
-    client_pos = rtmp_handshake_imprint_with_digest(tosend + 1);
+
+    if (rt->encrypted && CONFIG_FFRTMPCRYPT_PROTOCOL) {
+        /* When the client wants to use RTMPE, we have to change the command
+         * byte to 0x06 which means to use encrypted data and we have to set
+         * the flash version to at least 9.0.115.0. */
+        tosend[0] = 6;
+        tosend[5] = 128;
+        tosend[6] = 0;
+        tosend[7] = 3;
+        tosend[8] = 2;
+
+        /* Initialize the Diffie-Hellmann context and generate the public key
+         * to send to the server. */
+        if ((ret = ff_rtmpe_gen_pub_key(rt->stream, tosend + 1)) < 0)
+            return ret;
+    }
+
+    client_pos = rtmp_handshake_imprint_with_digest(tosend + 1, rt->encrypted);
     if (client_pos < 0)
         return client_pos;
 
@@ -737,6 +778,7 @@ static int rtmp_handshake(URLContext *s, RTMPContext *rt)
         return ret;
     }
 
+    av_log(s, AV_LOG_DEBUG, "Type answer %d\n", serverdata[0]);
     av_log(s, AV_LOG_DEBUG, "Server version %d.%d.%d.%d\n",
            serverdata[5], serverdata[6], serverdata[7], serverdata[8]);
 
@@ -746,6 +788,7 @@ static int rtmp_handshake(URLContext *s, RTMPContext *rt)
             return server_pos;
 
         if (!server_pos) {
+            type = 1;
             server_pos = rtmp_validate_digest(serverdata + 1, 8);
             if (server_pos < 0)
                 return server_pos;
@@ -763,11 +806,22 @@ static int rtmp_handshake(URLContext *s, RTMPContext *rt)
             return ret;
 
         ret = ff_rtmp_calc_digest(clientdata, RTMP_HANDSHAKE_PACKET_SIZE - 32,
-                                  0, digest, 32, digest);
+                                  0, digest, 32, signature);
         if (ret < 0)
             return ret;
 
-        if (memcmp(digest, clientdata + RTMP_HANDSHAKE_PACKET_SIZE - 32, 32)) {
+        if (rt->encrypted && CONFIG_FFRTMPCRYPT_PROTOCOL) {
+            /* Compute the shared secret key sent by the server and initialize
+             * the RC4 encryption. */
+            if ((ret = ff_rtmpe_compute_secret_key(rt->stream, serverdata + 1,
+                                                   tosend + 1, type)) < 0)
+                return ret;
+
+            /* Encrypt the signature received by the server. */
+            ff_rtmpe_encrypt_sig(rt->stream, signature, digest, serverdata[0]);
+        }
+
+        if (memcmp(signature, clientdata + RTMP_HANDSHAKE_PACKET_SIZE - 32, 32)) {
             av_log(s, AV_LOG_ERROR, "Signature mismatch\n");
             return AVERROR(EIO);
         }
@@ -786,88 +840,163 @@ static int rtmp_handshake(URLContext *s, RTMPContext *rt)
         if (ret < 0)
             return ret;
 
+        if (rt->encrypted && CONFIG_FFRTMPCRYPT_PROTOCOL) {
+            /* Encrypt the signature to be send to the server. */
+            ff_rtmpe_encrypt_sig(rt->stream, tosend +
+                                 RTMP_HANDSHAKE_PACKET_SIZE - 32, digest,
+                                 serverdata[0]);
+        }
+
         // write reply back to the server
         if ((ret = ffurl_write(rt->stream, tosend,
                                RTMP_HANDSHAKE_PACKET_SIZE)) < 0)
             return ret;
+
+        if (rt->encrypted && CONFIG_FFRTMPCRYPT_PROTOCOL) {
+            /* Set RC4 keys for encryption and update the keystreams. */
+            if ((ret = ff_rtmpe_update_keystream(rt->stream)) < 0)
+                return ret;
+        }
     } else {
+        if (rt->encrypted && CONFIG_FFRTMPCRYPT_PROTOCOL) {
+            /* Compute the shared secret key sent by the server and initialize
+             * the RC4 encryption. */
+            if ((ret = ff_rtmpe_compute_secret_key(rt->stream, serverdata + 1,
+                            tosend + 1, 1)) < 0)
+                return ret;
+
+            if (serverdata[0] == 9) {
+                /* Encrypt the signature received by the server. */
+                ff_rtmpe_encrypt_sig(rt->stream, signature, digest,
+                                     serverdata[0]);
+            }
+        }
+
         if ((ret = ffurl_write(rt->stream, serverdata + 1,
                                RTMP_HANDSHAKE_PACKET_SIZE)) < 0)
             return ret;
+
+        if (rt->encrypted && CONFIG_FFRTMPCRYPT_PROTOCOL) {
+            /* Set RC4 keys for encryption and update the keystreams. */
+            if ((ret = ff_rtmpe_update_keystream(rt->stream)) < 0)
+                return ret;
+        }
     }
 
     return 0;
 }
 
-/**
- * Parse received packet and possibly perform some action depending on
- * the packet contents.
- * @return 0 for no errors, negative values for serious errors which prevent
- *         further communications, positive values for uncritical errors
- */
-static int rtmp_parse_result(URLContext *s, RTMPContext *rt, RTMPPacket *pkt)
+static int handle_chunk_size(URLContext *s, RTMPPacket *pkt)
 {
+    RTMPContext *rt = s->priv_data;
+    int ret;
+
+    if (pkt->data_size < 4) {
+        av_log(s, AV_LOG_ERROR,
+               "Too short chunk size change packet (%d)\n",
+               pkt->data_size);
+        return AVERROR_INVALIDDATA;
+    }
+
+    if (!rt->is_input) {
+        if ((ret = ff_rtmp_packet_write(rt->stream, pkt, rt->chunk_size,
+                                        rt->prev_pkt[1])) < 0)
+            return ret;
+    }
+
+    rt->chunk_size = AV_RB32(pkt->data);
+    if (rt->chunk_size <= 0) {
+        av_log(s, AV_LOG_ERROR, "Incorrect chunk size %d\n", rt->chunk_size);
+        return AVERROR_INVALIDDATA;
+    }
+    av_log(s, AV_LOG_DEBUG, "New chunk size = %d\n", rt->chunk_size);
+
+    return 0;
+}
+
+static int handle_ping(URLContext *s, RTMPPacket *pkt)
+{
+    RTMPContext *rt = s->priv_data;
+    int t, ret;
+
+    if (pkt->data_size < 2) {
+        av_log(s, AV_LOG_ERROR, "Too short ping packet (%d)\n",
+               pkt->data_size);
+        return AVERROR_INVALIDDATA;
+    }
+
+    t = AV_RB16(pkt->data);
+    if (t == 6) {
+        if ((ret = gen_pong(s, rt, pkt)) < 0)
+            return ret;
+    }
+
+    return 0;
+}
+
+static int handle_client_bw(URLContext *s, RTMPPacket *pkt)
+{
+    RTMPContext *rt = s->priv_data;
+
+    if (pkt->data_size < 4) {
+        av_log(s, AV_LOG_ERROR,
+               "Client bandwidth report packet is less than 4 bytes long (%d)\n",
+               pkt->data_size);
+        return AVERROR_INVALIDDATA;
+    }
+
+    rt->client_report_size = AV_RB32(pkt->data);
+    if (rt->client_report_size <= 0) {
+        av_log(s, AV_LOG_ERROR, "Incorrect client bandwidth %d\n",
+                rt->client_report_size);
+        return AVERROR_INVALIDDATA;
+
+    }
+    av_log(s, AV_LOG_DEBUG, "Client bandwidth = %d\n", rt->client_report_size);
+    rt->client_report_size >>= 1;
+
+    return 0;
+}
+
+static int handle_server_bw(URLContext *s, RTMPPacket *pkt)
+{
+    RTMPContext *rt = s->priv_data;
+
+    if (pkt->data_size < 4) {
+        av_log(s, AV_LOG_ERROR,
+               "Too short server bandwidth report packet (%d)\n",
+               pkt->data_size);
+        return AVERROR_INVALIDDATA;
+    }
+
+    rt->server_bw = AV_RB32(pkt->data);
+    if (rt->server_bw <= 0) {
+        av_log(s, AV_LOG_ERROR, "Incorrect server bandwidth %d\n",
+               rt->server_bw);
+        return AVERROR_INVALIDDATA;
+    }
+    av_log(s, AV_LOG_DEBUG, "Server bandwidth = %d\n", rt->server_bw);
+
+    return 0;
+}
+
+static int handle_invoke(URLContext *s, RTMPPacket *pkt)
+{
+    RTMPContext *rt = s->priv_data;
     int i, t;
     const uint8_t *data_end = pkt->data + pkt->data_size;
     int ret;
 
-#ifdef DEBUG
-    ff_rtmp_packet_dump(s, pkt);
-#endif
+    //TODO: check for the messages sent for wrong state?
+    if (!memcmp(pkt->data, "\002\000\006_error", 9)) {
+        uint8_t tmpstr[256];
 
-    switch (pkt->type) {
-    case RTMP_PT_CHUNK_SIZE:
-        if (pkt->data_size != 4) {
-            av_log(s, AV_LOG_ERROR,
-                   "Chunk size change packet is not 4 bytes long (%d)\n", pkt->data_size);
-            return -1;
-        }
-        if (!rt->is_input)
-            if ((ret = ff_rtmp_packet_write(rt->stream, pkt, rt->chunk_size,
-                                            rt->prev_pkt[1])) < 0)
-                return ret;
-        rt->chunk_size = AV_RB32(pkt->data);
-        if (rt->chunk_size <= 0) {
-            av_log(s, AV_LOG_ERROR, "Incorrect chunk size %d\n", rt->chunk_size);
-            return -1;
-        }
-        av_log(s, AV_LOG_DEBUG, "New chunk size = %d\n", rt->chunk_size);
-        break;
-    case RTMP_PT_PING:
-        t = AV_RB16(pkt->data);
-        if (t == 6)
-            if ((ret = gen_pong(s, rt, pkt)) < 0)
-                return ret;
-        break;
-    case RTMP_PT_CLIENT_BW:
-        if (pkt->data_size < 4) {
-            av_log(s, AV_LOG_ERROR,
-                   "Client bandwidth report packet is less than 4 bytes long (%d)\n",
-                   pkt->data_size);
-            return -1;
-        }
-        av_log(s, AV_LOG_DEBUG, "Client bandwidth = %d\n", AV_RB32(pkt->data));
-        rt->client_report_size = AV_RB32(pkt->data) >> 1;
-        break;
-    case RTMP_PT_SERVER_BW:
-        rt->server_bw = AV_RB32(pkt->data);
-        if (rt->server_bw <= 0) {
-            av_log(s, AV_LOG_ERROR, "Incorrect server bandwidth %d\n", rt->server_bw);
-            return AVERROR(EINVAL);
-        }
-        av_log(s, AV_LOG_DEBUG, "Server bandwidth = %d\n", rt->server_bw);
-        break;
-    case RTMP_PT_INVOKE:
-        //TODO: check for the messages sent for wrong state?
-        if (!memcmp(pkt->data, "\002\000\006_error", 9)) {
-            uint8_t tmpstr[256];
-
-            if (!ff_amf_get_field_value(pkt->data + 9, data_end,
-                                        "description", tmpstr, sizeof(tmpstr)))
-                av_log(s, AV_LOG_ERROR, "Server error: %s\n",tmpstr);
-            return -1;
-        } else if (!memcmp(pkt->data, "\002\000\007_result", 10)) {
-            switch (rt->state) {
+        if (!ff_amf_get_field_value(pkt->data + 9, data_end,
+                                    "description", tmpstr, sizeof(tmpstr)))
+            av_log(s, AV_LOG_ERROR, "Server error: %s\n",tmpstr);
+        return -1;
+    } else if (!memcmp(pkt->data, "\002\000\007_result", 10)) {
+        switch (rt->state) {
             case STATE_HANDSHAKED:
                 if (!rt->is_input) {
                     if ((ret = gen_release_stream(s, rt)) < 0)
@@ -915,35 +1044,73 @@ static int rtmp_parse_result(URLContext *s, RTMPContext *rt, RTMPPacket *pkt)
                 }
                 rt->state = STATE_READY;
                 break;
-            }
-        } else if (!memcmp(pkt->data, "\002\000\010onStatus", 11)) {
-            const uint8_t* ptr = pkt->data + 11;
-            uint8_t tmpstr[256];
-
-            for (i = 0; i < 2; i++) {
-                t = ff_amf_tag_size(ptr, data_end);
-                if (t < 0)
-                    return 1;
-                ptr += t;
-            }
-            t = ff_amf_get_field_value(ptr, data_end,
-                                       "level", tmpstr, sizeof(tmpstr));
-            if (!t && !strcmp(tmpstr, "error")) {
-                if (!ff_amf_get_field_value(ptr, data_end,
-                                            "description", tmpstr, sizeof(tmpstr)))
-                    av_log(s, AV_LOG_ERROR, "Server error: %s\n",tmpstr);
-                return -1;
-            }
-            t = ff_amf_get_field_value(ptr, data_end,
-                                       "code", tmpstr, sizeof(tmpstr));
-            if (!t && !strcmp(tmpstr, "NetStream.Play.Start")) rt->state = STATE_PLAYING;
-            if (!t && !strcmp(tmpstr, "NetStream.Play.Stop")) rt->state = STATE_STOPPED;
-            if (!t && !strcmp(tmpstr, "NetStream.Play.UnpublishNotify")) rt->state = STATE_STOPPED;
-            if (!t && !strcmp(tmpstr, "NetStream.Publish.Start")) rt->state = STATE_PUBLISHING;
-        } else if (!memcmp(pkt->data, "\002\000\010onBWDone", 11)) {
-            if ((ret = gen_check_bw(s, rt)) < 0)
-                return ret;
         }
+    } else if (!memcmp(pkt->data, "\002\000\010onStatus", 11)) {
+        const uint8_t* ptr = pkt->data + 11;
+        uint8_t tmpstr[256];
+
+        for (i = 0; i < 2; i++) {
+            t = ff_amf_tag_size(ptr, data_end);
+            if (t < 0)
+                return 1;
+            ptr += t;
+        }
+        t = ff_amf_get_field_value(ptr, data_end,
+                                   "level", tmpstr, sizeof(tmpstr));
+        if (!t && !strcmp(tmpstr, "error")) {
+            if (!ff_amf_get_field_value(ptr, data_end,
+                                        "description", tmpstr, sizeof(tmpstr)))
+                av_log(s, AV_LOG_ERROR, "Server error: %s\n",tmpstr);
+            return -1;
+        }
+        t = ff_amf_get_field_value(ptr, data_end,
+                "code", tmpstr, sizeof(tmpstr));
+        if (!t && !strcmp(tmpstr, "NetStream.Play.Start")) rt->state = STATE_PLAYING;
+        if (!t && !strcmp(tmpstr, "NetStream.Play.Stop")) rt->state = STATE_STOPPED;
+        if (!t && !strcmp(tmpstr, "NetStream.Play.UnpublishNotify")) rt->state = STATE_STOPPED;
+        if (!t && !strcmp(tmpstr, "NetStream.Publish.Start")) rt->state = STATE_PUBLISHING;
+    } else if (!memcmp(pkt->data, "\002\000\010onBWDone", 11)) {
+        if ((ret = gen_check_bw(s, rt)) < 0)
+            return ret;
+    }
+
+    return 0;
+}
+
+/**
+ * Parse received packet and possibly perform some action depending on
+ * the packet contents.
+ * @return 0 for no errors, negative values for serious errors which prevent
+ *         further communications, positive values for uncritical errors
+ */
+static int rtmp_parse_result(URLContext *s, RTMPContext *rt, RTMPPacket *pkt)
+{
+    int ret;
+
+#ifdef DEBUG
+    ff_rtmp_packet_dump(s, pkt);
+#endif
+
+    switch (pkt->type) {
+    case RTMP_PT_CHUNK_SIZE:
+        if ((ret = handle_chunk_size(s, pkt)) < 0)
+            return ret;
+        break;
+    case RTMP_PT_PING:
+        if ((ret = handle_ping(s, pkt)) < 0)
+            return ret;
+        break;
+    case RTMP_PT_CLIENT_BW:
+        if ((ret = handle_client_bw(s, pkt)) < 0)
+            return ret;
+        break;
+    case RTMP_PT_SERVER_BW:
+        if ((ret = handle_server_bw(s, pkt)) < 0)
+            return ret;
+        break;
+    case RTMP_PT_INVOKE:
+        if ((ret = handle_invoke(s, pkt)) < 0)
+            return ret;
         break;
     case RTMP_PT_VIDEO:
     case RTMP_PT_AUDIO:
@@ -1116,6 +1283,13 @@ static int rtmp_open(URLContext *s, const char *uri, int flags)
         if (port < 0)
             port = RTMPS_DEFAULT_PORT;
         ff_url_join(buf, sizeof(buf), "tls", NULL, hostname, port, NULL);
+    } else if (!strcmp(proto, "rtmpe") || (!strcmp(proto, "rtmpte"))) {
+        if (!strcmp(proto, "rtmpte"))
+            av_dict_set(&opts, "ffrtmpcrypt_tunneling", "1", 1);
+
+        /* open the encrypted connection */
+        ff_url_join(buf, sizeof(buf), "ffrtmpcrypt", NULL, hostname, port, NULL);
+        rt->encrypted = 1;
     } else {
         /* open the tcp connection */
         if (port < 0)
@@ -1416,9 +1590,10 @@ static const AVOption rtmp_options[] = {
     {"any", "both", 0, AV_OPT_TYPE_CONST, {-2}, 0, 0, DEC, "rtmp_live"},
     {"live", "live stream", 0, AV_OPT_TYPE_CONST, {-1}, 0, 0, DEC, "rtmp_live"},
     {"recorded", "recorded stream", 0, AV_OPT_TYPE_CONST, {0}, 0, 0, DEC, "rtmp_live"},
+    {"rtmp_pageurl", "URL of the web page in which the media was embedded. By default no value will be sent.", OFFSET(pageurl), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC},
     {"rtmp_playpath", "Stream identifier to play or to publish", OFFSET(playpath), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
     {"rtmp_swfurl", "URL of the SWF player. By default no value will be sent", OFFSET(swfurl), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
-    {"rtmp_tcurl", "URL of the target stream. Defaults to rtmp://host[:port]/app.", OFFSET(tcurl), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
+    {"rtmp_tcurl", "URL of the target stream. Defaults to proto://host[:port]/app.", OFFSET(tcurl), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
     { NULL },
 };
 
@@ -1440,6 +1615,24 @@ URLProtocol ff_rtmp_protocol = {
     .priv_data_class= &rtmp_class,
 };
 
+static const AVClass rtmpe_class = {
+    .class_name = "rtmpe",
+    .item_name  = av_default_item_name,
+    .option     = rtmp_options,
+    .version    = LIBAVUTIL_VERSION_INT,
+};
+
+URLProtocol ff_rtmpe_protocol = {
+    .name            = "rtmpe",
+    .url_open        = rtmp_open,
+    .url_read        = rtmp_read,
+    .url_write       = rtmp_write,
+    .url_close       = rtmp_close,
+    .priv_data_size  = sizeof(RTMPContext),
+    .flags           = URL_PROTOCOL_FLAG_NETWORK,
+    .priv_data_class = &rtmpe_class,
+};
+
 static const AVClass rtmps_class = {
     .class_name = "rtmps",
     .item_name  = av_default_item_name,
@@ -1476,6 +1669,24 @@ URLProtocol ff_rtmpt_protocol = {
     .priv_data_class = &rtmpt_class,
 };
 
+static const AVClass rtmpte_class = {
+    .class_name = "rtmpte",
+    .item_name  = av_default_item_name,
+    .option     = rtmp_options,
+    .version    = LIBAVUTIL_VERSION_INT,
+};
+
+URLProtocol ff_rtmpte_protocol = {
+    .name            = "rtmpte",
+    .url_open        = rtmp_open,
+    .url_read        = rtmp_read,
+    .url_write       = rtmp_write,
+    .url_close       = rtmp_close,
+    .priv_data_size  = sizeof(RTMPContext),
+    .flags           = URL_PROTOCOL_FLAG_NETWORK,
+    .priv_data_class = &rtmpte_class,
+};
+
 static const AVClass rtmpts_class = {
     .class_name = "rtmpts",
     .item_name  = av_default_item_name,