自学内容网 自学内容网

nginx flv模块的使用和源码分析

1. 为什么需要ngx_http_flv_module

  毋庸多说,就是为了提供在线的http flv流媒体播放服务。在若干年前,adobe flash player风靡的年代,可以说http flv是最流行的在线流媒体点播的解决方案,甚至很多直播也用http flv来实现,可以和RTMP实时流媒体协议的实时性相当。
   可是若干年过去了,随着H5的兴起,新的视频编码器的出现,adobe flash player已经被各大浏览器彻底抛弃了,虽然http flv也逐渐式微了,但是因为flv格式有着格式简单高效的特点,很容易进行流化处理,所以用http flv协议进行流媒体播放还一直生生不息。
   那么如何用nginx搭建一个http flv的流媒体服务器呢?这里就要用到ngx_http_flv_module了。

2. 配置指令

   ngx_http_flv_module 的配置非常方便。
  只要在nginx.conf的location块中添加以下指令:
flv
即可开启flv流媒体模块。

3. 加载ngx_http_flv_module 模块

在configure的时候需要添加ngx_http_flv_module来将其编译进来,使用如下命令:
 ./configure --with-http_flv_module
    然后在nginx.conf 中添加以下配置,如:
location / {
    flv;
    root html;
}

4. 源码分析

4.1 指令分析

static ngx_command_t  ngx_http_flv_commands[] = {

    { ngx_string("flv"),
      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
      ngx_http_flv,
      0,
      0,
      NULL },

      ngx_null_command
};
从以上代码知道,flv指令在location中使用,后面不带参数,并且用ngx_http_flv指令分析函数进行解析。
下面来看看ngx_http_flv函数,如下:
static char *
ngx_http_flv(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t  *clcf;

    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = ngx_http_flv_handler;

    return NGX_CONF_OK;
}
代码非常简单,就是在nginx 的http content phase阶段将处理回调函数挂进去,这里挂进去的钩子函数即ngx_http_flv_handler。将回调函数挂到content phase有两种方法,不过大都采用本模块使用的方法,另外也可以王ngx_htt_core_main_conf_t全局结构体的phases[NGX_HTTP_CONTENT_PHASE]动态数组添加回调函数来实现。

4.2 ngx_http_flv_handler处理函数

   当用户请求的url匹配到nginx.conf中的某个开启了flv的location后,在content phase阶段,就会触发调用ngx_http_flv_handler函数。
    下面对该函数的实现过程进行详细解析:
static ngx_int_t
ngx_http_flv_handler(ngx_http_request_t *r)
{
    u_char                    *last;
    off_t                      start, len;
    size_t                     root;
    ngx_int_t                  rc;
    ngx_uint_t                 level, i;
    ngx_str_t                  path, value;
    ngx_log_t                 *log;
    ngx_buf_t                 *b;
    ngx_chain_t                out[2];
    ngx_open_file_info_t       of;
    ngx_http_core_loc_conf_t  *clcf;
    /* 仅支持HTTP GET/HEAD方法,否则返回http 405错误码 */
    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
        return NGX_HTTP_NOT_ALLOWED;
    }
/* 请求地址不能以/结尾,意味着没有指定特定的文件,则返回NGX_DECLINED,
   即跳过本模块进行处理 */
    if (r->uri.data[r->uri.len - 1] == '/') {
        return NGX_DECLINED;
    }
/* 因为是流媒体服务,没有需要处理HTTP请求的body部分,所以需要丢弃请求的body内容 */
    rc = ngx_http_discard_request_body(r);

    if (rc != NGX_OK) {
        return rc;
    }
/* 将请求的url地址映射到本地硬盘上的文件路径, 
   其中path为返回值:即映射出来的本地硬盘上的文件路径
   其中root为返回值:即当前location对应的根目录的字符串长度
   返回值是指向path字符串的结尾符\0的指针 */
    last = ngx_http_map_uri_to_path(r, &path, &root, 0);
    if (last == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    log = r->connection->log;

    path.len = last - path.data;

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, log, 0,
                   "http flv filename: \"%V\"", &path);

    clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
/* 以下准备打开flv文件 */
    ngx_memzero(&of, sizeof(ngx_open_file_info_t));

    of.read_ahead = clcf->read_ahead;
    of.directio = clcf->directio;
    of.valid = clcf->open_file_cache_valid;
    of.min_uses = clcf->open_file_cache_min_uses;
    of.errors = clcf->open_file_cache_errors;
    of.events = clcf->open_file_cache_events;

    if (ngx_http_set_disable_symlinks(r, clcf, &path, &of) != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
/* 在nginx的文件缓冲区中打开flv文件 */
    if (ngx_open_cached_file(clcf->open_file_cache, &path, &of, r->pool)
        != NGX_OK)
    {   /* 以下为打开失败的处理逻辑 */
        switch (of.err) {

        case 0:
            return NGX_HTTP_INTERNAL_SERVER_ERROR;

        case NGX_ENOENT:
        case NGX_ENOTDIR:
        case NGX_ENAMETOOLONG:

            level = NGX_LOG_ERR;
            rc = NGX_HTTP_NOT_FOUND;
            break;

        case NGX_EACCES:
#if (NGX_HAVE_OPENAT)
        case NGX_EMLINK:
        case NGX_ELOOP:
#endif

            level = NGX_LOG_ERR;
            rc = NGX_HTTP_FORBIDDEN;
            break;

        default:

            level = NGX_LOG_CRIT;
            rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
            break;
        }

        if (rc != NGX_HTTP_NOT_FOUND || clcf->log_not_found) {
            ngx_log_error(level, log, of.err,
                          "%s \"%s\" failed", of.failed, path.data);
        }

        return rc;
    }
/* 如果打开的不是文件,那么久关闭文件句柄,并返回NGX_DECLINED */
    if (!of.is_file) {

        if (ngx_close_file(of.fd) == NGX_FILE_ERROR) {
            ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
                          ngx_close_file_n " \"%s\" failed", path.data);
        }

        return NGX_DECLINED;
    }

    r->root_tested = !r->error_page;

    start = 0;
    len = of.size;
    i = 1;
/* 根据请求url中的start来获取客户端实际想要的视频内容开始的文件偏移量 */
    if (r->args.len) {

        if (ngx_http_arg(r, (u_char *) "start", 5, &value) == NGX_OK) {

            start = ngx_atoof(value.data, value.len);

            if (start == NGX_ERROR || start >= len) {
                start = 0;
            }
/* 如果不是从头开始请求,那么响应的内容长度按照以下代码进行计算
            if (start) {
                len = sizeof(ngx_flv_header) - 1 + len - start;
                i = 0;/* i = 0 表示不是响应完整内容 */
            }
        }
    }

    log->action = "sending flv to client";
/* 响应的状态设置为200, Content-Length设置为将要响应的文件的长度 */
    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = len;
    r->headers_out.last_modified_time = of.mtime;
/* 设置当前文件的http etag header值 */
    if (ngx_http_set_etag(r) != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
/* 设置文件的Content-Type类型 */
    if (ngx_http_set_content_type(r) != NGX_OK) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
/* 不是响应完整文件内容的情况,需要自己构建一个flv header放入响应缓冲区中 */
    if (i == 0) {
        b = ngx_calloc_buf(r->pool);
        if (b == NULL) {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        b->pos = ngx_flv_header;
        b->last = ngx_flv_header + sizeof(ngx_flv_header) - 1;
        b->memory = 1;

        out[0].buf = b;
        out[0].next = &out[1];/* 将flv头部缓存连接到flv body缓存,构成一个响应缓存链 */
    }

/* 接下去分配一个文件类型的ngx_buf_t缓冲区 */
    b = ngx_calloc_buf(r->pool);
    if (b == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
    if (b->file == NULL) {
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }
/* 设置允许ngx_http_range_filter_module处理Range请求 */
    r->allow_ranges = 1;
/* 发送http响应头 */
    rc = ngx_http_send_header(r);

    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) {
        return rc;
    }
/* 设置文件缓存的起始结束便宜位置 */
    b->file_pos = start;
    b->file_last = of.size;
/* 设置ngx_buf_t的in_file标记,表示是文件缓存 */
    b->in_file = b->file_last ? 1: 0;
    b->last_buf = (r == r->main) ? 1 : 0;  /* 如果是主请求,设置当前ngx_buf_t是最后一个buf */
    b->last_in_chain = 1;/* 设置为当前ngx_buf_t是响应缓存链中的最后一个 */

    b->file->fd = of.fd;/* 设置文件句柄 */
    b->file->name = path;/* 设置文件名 */
    b->file->log = log;
    b->file->directio = of.is_directio;

    out[1].buf = b;
    out[1].next = NULL;/* 设置next为NULL, 表示当前是最后一个ngx_buf_t */
/* 通知nginx框架发送flv内容 */
    return ngx_http_output_filter(r, &out[i]);
}

5. 如何请求flv进行验证

可以通过以下命令行,例如:
curl "http://127.0.0.1:8080/path/test.flv?start=12345" > /dev/null
当然,最好还是用支持flv流媒体播放的播放器,譬如vlc,ffplay来进行播放测试验证。
但是vlc和ffplay无法通过http flv来进行视频拖拽播放,必须实现一个能够支持http flv拖拽功能的播放器,是需要解析flv文件内容,根据metadata知道每一个关键帧的起始未知,然后在拖拽的时候自动对齐到最近的一个关键帧,然后发起一个新的带有start=xxx的http flv播放地址参数的请求进行播放。

6. 思考

  其实FLV流媒体实现的代码还是非常简单的。
  如果希望能够像[nginx slice模块的使用和源码分析](https://editor.csdn.net/md/?articleId=136029381)中提到的那样,对FLV进行切片处理,来实现对cdn的缓存友好性,那怎么来做呢?由于FLV流媒体下载和普通的Range请求下载还是有一定的区别的,肯定需要进行特殊的处理。如果有时间的话我再来参数一下支持FLV切片回源的流媒体播放功能吧,敬请期待。

原文地址:https://blog.csdn.net/bluestn/article/details/136033245

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!