这里写自定义目录标题
-
- [1. 为什么需要ngx_http_flv_module](#1. 为什么需要ngx_http_flv_module)
- [2. 配置指令](#2. 配置指令)
- [3. 加载ngx_http_flv_module 模块](#3. 加载ngx_http_flv_module 模块)
- [4. 源码分析](#4. 源码分析)
-
- [4.1 指令分析](#4.1 指令分析)
- [4.2 ngx_http_flv_handler处理函数](#4.2 ngx_http_flv_handler处理函数)
- [5. 如何请求flv进行验证](#5. 如何请求flv进行验证)
- [6. 思考](#6. 思考)
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块中添加以下指令:
c
复制代码
flv
即可开启flv流媒体模块。
3. 加载ngx_http_flv_module 模块
在configure的时候需要添加ngx_http_flv_module来将其编译进来,使用如下命令:
bash
复制代码
./configure --with-http_flv_module
然后在nginx.conf 中添加以下配置,如:
bash
复制代码
location / {
flv;
root html;
}
4. 源码分析
4.1 指令分析
c
复制代码
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函数,如下:
c
复制代码
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函数。
下面对该函数的实现过程进行详细解析:
c
复制代码
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进行验证
可以通过以下命令行,例如:
Bash
复制代码
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切片回源的流媒体播放功能吧,敬请期待。