Nginx RTMP源码分析--HTTP-FLV的实现

HTTP-FLV 协议简介

HTTP-FLV:通过HTTP协议传输FLV封装格式的流媒体数据。HTTP协议中content-length字段表明客户端需要接收的body长度,在HTTP-FLV中,媒体服务器不设置content-length,客户端会持续接收数据,直到链接断开。
HTTP-FLV相比RTMP协议的优势:

  1. 防火墙限制:部分防火墙会屏蔽RTMP的1935端口,但是HTTP协议不会被屏蔽。
  2. 简单:HTTP是最广泛的协议。
  3. 调度:根据默认的HTTP 302即可实现拉流调度。在SRS中有针对RTMP的302调度,但是这并不是通用的标准协议,对于各家CDN厂商对接存在诸多问题。但是HTTP协议的302是标准协议,各家CDN都支持。

基于nginx-rtmp-module实现HTTP-FLV,关键部分主要是两点:

  1. 由http request生成rtmp session;
  2. 对于http-flv的connection,修改收发数据的逻辑,匹配HTTP协议。

因此,HTTP FLV的实现需要同时注册HTTP和RTMP模块。

注册RTMP模块

static ngx_int_t
ngx_rtmp_flv_live_index_postconfiguration(ngx_conf_t *cf)
{
    next_play = ngx_rtmp_play;
    ngx_rtmp_play = ngx_http_flv_live_play;

    next_close_stream = ngx_rtmp_close_stream;
    ngx_rtmp_close_stream = ngx_http_flv_live_close_stream;

    http_flv_live_next_play = next_play;
    http_flv_live_next_close_stream = next_close_stream;

    return NGX_OK;
}

HTTP FLV模块只有在play时使用,因此注册的RTMP模块只需将处理函数加入到next_play和next_close_stream函数链表中即可。

注册HTTP模块

生成RTMP Session

  1. 将ngx_http_flv_live_handler注册到NGX_HTTP_CONTENT_PHASE阶段,ngx_http_flv_live_handler是负责处理http request的回调。
    h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_http_flv_live_handler;

  1. 判断请求类型是否是GET,判断http version;
  2. 解析出请求的URI,获取到application和stream name,查找RTMP的configure配置,判断是否匹配;

    last = r->uri.data + r->uri.len;
    p = ngx_strlchr(r->uri.data + 1, last, '/');
    q = ngx_strnstr(r->uri.data, ".flv", r->uri.len);
    if(p == NULL || q == NULL) {
        return NULL;
    }
    ctx->app.data = r->uri.data + 1;
    ctx->app.len = p - r->uri.data - 1;
    ctx->stream.data = p + 1;
    ctx->stream.len = q - p - 1;

    cmcf = ngx_rtmp_core_main_conf;
    if (cmcf == NULL) {
        return NULL;
    }
    appmatch = 0;
    cscf = cmcf->servers.elts;
    for (m = 0; m < cmcf->servers.nelts; ++m, ++cscf) {
        cacf = (*cscf)->applications.elts;
        for (n = 0; n < (*cscf)->applications.nelts; ++n, ++cacf) {
            if((*cacf)->name.len == ctx->app.len && 
                ngx_strncmp((char*)(ctx->app.data), 
                    (char*)((*cacf)->name.data), ctx->app.len) == 0) 
            {
                appmatch = 1;
            }
        }
    }
    if(appmatch == 0) {
        return NULL;
    }

  1. 获取ngx_rtmp_addr_conf_t addrconf;

    ls = ngx_cycle->listening.elts;
    for (n = 0; n < ngx_cycle->listening.nelts; ++n, ++ls) {
        if (ls->handler == ngx_rtmp_init_connection) {
            local_sockaddr = r->connection->local_sockaddr;
            sa_family = local_sockaddr->sa_family;
...
...
            switch (sa_family) {
...
...
            rport = ls->servers;

            if (rport->naddrs > 1) {
                /**
                 * listen xxx.xxx.xxx.xxx:port
                 * listen port
                 **/
                switch (sa_family) {

#if (NGX_HAVE_INET6)
                case AF_INET6:
...
                    addr_conf = &addr6[i].conf;
...
                    break;
#endif

                default:
...
                    addr_conf = &addr[i].conf;
                }
            } else {
                switch (sa_family) {

#if (NGX_HAVE_INET6)
                case AF_INET6:
...
                        addr_conf = &addr6[0].conf;
...
                    break;
#endif

                default:
...
                        addr_conf = &addr[0].conf;
...
                    }
                }
            }

            if (!addr_match) {
                addr_match = 1;
                continue;
            }

            break;
        }
    }

  1. 根据上一步获取的绑定地址信息和HTTP请求对应的connection,通过ngx_rtmp_init_session()生成rtmp session;
    c = r->connection;
    data = c->data;
    s = ngx_rtmp_init_session(c, addr_conf);
    c->data = data;
  1. 重置connection的发送数据和接收数据的handler,这一步非常重要,因为后面是发送的RTMP数据。
    c->write->handler = ngx_rtmp_send;
    c->read->handler = ngx_rtmp_recv;

  1. 调用ngx_rtmp_play,执行next_play函数链表,执行rtmp的play逻辑。

通过以上步骤,我们成功的由一个HTTP Requset生成一个RTMP Session,并将Session加入到rtmp_live_module的保存流信息的结构体中。

HTTP-FLV的数据发送和接收的逻辑

HTTP-FLV不需要处理接收逻辑,对于接收到数据,直接丢弃返回即可。重点是对发送packet的封装。

  • 发送HTTP FLV头信息
    创建处理HTTP FLV到连接成功后,首先需要发送视频头信息,主要参考HTTP协议和FLV封装格式协议。由于传输的是直播视频数据,不需要设置Content-Length。
HTTP HEADER Value
Content-Type video/x-flv
Connection keep-alive
Expires -1
Transfer-Encoding chunked

发送FLV头信息。

    /**
     * |F|L|V|ver|00000101|header_size|0|0|0|0|, ngx_http_flv_module.c
     * for more details, please refer to http://www.adobe.com/devnet/f4v.html
     **/
    u_char flv_header[] = "FLV\x1\0\0\0\0\x9\0\0\0\0";

  • 数据接收

在通过HTTP Request构建RTMP Session时,我们已经设置了Connection读读事件c->read->handler = ngx_rtmp_recv。在ngx_rtmp_recv对HTTP连接接收到的数据不做任何处理。

    if(s->signature == NGX_HTTP_MODULE) {
        r = c->data;
        ctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module);
        s = ctx->s;
    }
  • 数据发送

由于RTMP和HTTP-FLV的metadata和音视频数据的封装格式不同,所以对于RTMP和HTTP-FLV分别对应不同的数据处理函数。

区域 类型 说明
Reserved Unsigned Bit[2] 两位,FMS保留,为0
Filter Unsigned Bit[1] 一位,通常为0
TagType Unsigned Bit[5] 五位,tag类型,音频:0x08,视频:0x09,脚本:0x12
DataSize Unsigned Int24 3个字节,数据区长度
TimeStamp Unsigned Int24 3个字节,毫秒
TimeStampExtended Unsigned Int8 1个字节,时间戳扩展高8位
StreamsID Unsigned Int24 3个字节,为0
TagData TagType=0x08,Audio头信息,TagType=0x09,Video头信息,TagType=0x12,脚本信息
    /* type, 5bits */
    *pos++ = (u_char) (h->type & 0x1f);

    /* data length, 3B */
    p = (u_char *) &data_size;
    *pos++ = p[2];
    *pos++ = p[1];
    *pos++ = p[0];

    /* timestamp, 3B + ext, 1B */
    p = (u_char *) &h->timestamp;
    *pos++ = p[2];
    *pos++ = p[1];
    *pos++ = p[0];
    *pos++ = p[3];

    /* streamId, 3B, always be 0 */
    *pos++ = 0;
    *pos++ = 0;
    *pos++ = 0;

在ngx_rtmp_send中,对于HTTP FLV的Session,从中获取到连接connection。

    if(s->signature == NGX_HTTP_MODULE) {
        r = c->data;
        ctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module);
        s = ctx->s;
    }

  • ngx_rtmp_send_shared_packet处理
    HTTP-FLV不需要发送RTMP指令类消息。
static ngx_int_t
ngx_rtmp_send_shared_packet(ngx_rtmp_session_t *s, ngx_chain_t *cl)
{
...
    /*http flv request*/
    if (s->signature == NGX_HTTP_MODULE) {
        ngx_rtmp_free_shared_chain(cscf, cl);
        return NGX_OK;
    }
...
}

play结束

在stream结束播放后,需要释放HTTP Request(ngx_http_free_request)和RTMP Session(ngx_rtmp_finalize_session)。

你可能感兴趣的