ngx_rtmp_control_module
RTMP控制模块主要包括录制指令、关闭指令、重定向指令三个控制命令。和其他RTMP有所不同的地方是:RTMP控制模块是注册在HTTP模块下的,它是通过在HTTP配置域下进行控制配置,并通过HTTP Get请求来实现外部对RTMP服务的控制功能。
源码分析
注册HTTP回调
static char *
ngx_rtmp_control(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_rtmp_control_handler;
return ngx_conf_set_bitmask_slot(cf, cmd, conf);
}
通过ngx_rtmp_control将ngx_rtmp_control_handler注册到HTTP服务上,当接收到HTTP客户端请求时,进行相应的指令操作并返回结果。
ngx_rtmp_control_handler回调函数解析
一、解析HTTP请求的URI,获取到section和method。section和method的具体含义如下表所示:
section | method | 说明 |
---|---|---|
record | start | 录制开始 |
record | stop | 录制结束 |
drop | publisher | 关闭推流 |
drop | subscriber | 关闭拉流 |
drop | client | 关闭匹配的所有客户端连接 |
redirect | publisher | 推流重定向到的新流名称 |
redirect | subscriber | 拉流重定向到的新流名称 |
redirect | client | 客户端连接重定向到的新流名称 |
/* uri format: .../section/method?args */
ngx_str_null(§ion);
ngx_str_null(&method);
for (n = r->uri.len; n; --n) {
p = &r->uri.data[n - 1];
if (*p != '/') {
continue;
}
if (method.data) {
section.data = p + 1;
section.len = method.data - section.data - 1;
break;
}
method.data = p + 1;
method.len = r->uri.data + r->uri.len - method.data;
}
二、使用宏定义来简化代码。
其实可以将代码扩展开来如下,以record(录制)为例进行说明:
if (llcf->control & NGX_RTMP_CONTROL_RECORD &&
section.len == sizeof("record") - 1 &&
ngx_strncmp(section.data, "record", sizeof("record") - 1) == 0)
{
return ngx_rtmp_control_record(r, &method);
}
#define NGX_RTMP_CONTROL_SECTION(flag, secname) \
if (llcf->control & NGX_RTMP_CONTROL_##flag && \
section.len == sizeof(#secname) - 1 && \
ngx_strncmp(section.data, #secname, sizeof(#secname) - 1) == 0) \
{ \
return ngx_rtmp_control_##secname(r, &method); \
}
NGX_RTMP_CONTROL_SECTION(RECORD, record);
NGX_RTMP_CONTROL_SECTION(DROP, drop);
NGX_RTMP_CONTROL_SECTION(REDIRECT, redirect);
三、录制
录制请求的格式参考官方wiki如下:
http://server.com/control/record/start|stop?srv=SRV&app=APP&name=NAME&rec=REC
查找RTMP配置的所有Application及其所有的直播流,直到匹配上URI(r)中的参数,然后调用ngx_rtmp_control_record_handler函数进行具体的录制指令的执行。
msg = ngx_rtmp_control_walk(r, ngx_rtmp_control_record_handler);
现在我们首先看看在查找到对应的直播RTMP会话后,录制指令是如何执行的:
1、解析URI中的rec,然后通过ngx_rtmp_record_find查找在RTMP的配置项中的录制块名称。
if (ngx_http_arg(r, (u_char *) "rec", sizeof("rec") - 1, &rec) != NGX_OK) {
rec.len = 0;
}
rn = ngx_rtmp_record_find(racf, &rec);
if (rn == NGX_CONF_UNSET_UINT) {
return "Recorder not found";
}
2、解析出method的具体指令是开始录制或者结束录制,然后分别调用不同的录制函数。
if (ctx->method.len == sizeof("start") - 1 &&
ngx_strncmp(ctx->method.data, "start", ctx->method.len) == 0)
{
rc = ngx_rtmp_record_open(s, rn, &ctx->path);
} else if (ctx->method.len == sizeof("stop") - 1 &&
ngx_strncmp(ctx->method.data, "stop", ctx->method.len) == 0)
{
rc = ngx_rtmp_record_close(s, rn, &ctx->path);
} else {
return "Undefined method";
}
我们可以看到,对于method参数传入start,则执行打开录制的函数;如果method参数是stop,则执行关闭录制的函数。至于函数ngx_rtmp_record_open和函数ngx_rtmp_record_close的具体内容,我们已经在录制模块中介绍了,有疑问的读者可以阅读参考第N章RTMP录制模块。
四、关闭RTMP会话
关闭RTMP会话的格式参考官方wiki如下:
http://server.com/control/drop/publisher|subscriber|client?
srv=SRV&app=APP&name=NAME&addr=ADDR&clientid=CLIENTID
method包含三种:publisher,subscriber和client。首先是判断获取到的method类型,分别对ctx->filter设置不同的参数:
publisher:NGX_RTMP_CONTROL_FILTER_PUBLISHER
subscriber:NGX_RTMP_CONTROL_FILTER_SUBSCRIBER
client:NGX_RTMP_CONTROL_FILTER_CLIENT
设置ctx->filter的意义是在ngx_rtmp_control_walk_session函数中查找RTMP会话,并根据ctx->filter对RTMP会话进行筛选,选择出合适的RTMP会话进行下一步操作。
if (ctx->method.len == sizeof("publisher") - 1 &&
ngx_memcmp(ctx->method.data, "publisher", ctx->method.len) == 0)
{
ctx->filter = NGX_RTMP_CONTROL_FILTER_PUBLISHER;
} else if (ctx->method.len == sizeof("subscriber") - 1 &&
ngx_memcmp(ctx->method.data, "subscriber", ctx->method.len)
== 0)
{
ctx->filter = NGX_RTMP_CONTROL_FILTER_SUBSCRIBER;
} else if (method->len == sizeof("client") - 1 &&
ngx_memcmp(ctx->method.data, "client", ctx->method.len) == 0)
{
ctx->filter = NGX_RTMP_CONTROL_FILTER_CLIENT;
} else {
msg = "Undefined filter";
goto error;
}
第二步是调用ngx_rtmp_control_walk查找所有符合条件的RTMP会话,然后调用设置的回调函数ngx_rtmp_control_drop_handler运行。
msg = ngx_rtmp_control_walk(r, ngx_rtmp_control_drop_handler);
if (msg != NGX_CONF_OK) {
goto error;
}
我们可以看到ngx_rtmp_control_drop_handler回调中只有一步简单的操作,就是将RTMP会话关闭。
ngx_rtmp_control_ctx_t *ctx;
ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module);
ngx_rtmp_finalize_session(s);
++ctx->count;
最后是将control/drop的操作结果给出HTTP响应信息。
。。。。。。
ngx_http_send_header(r);
return ngx_http_output_filter(r, &cl);
五、重定向流
官方wiki说明:主要是多了有一个newname,代表重定向的新流名称。
http://server.com/control/redirect/publisher|subscriber|client?
srv=SRV&app=APP&name=NAME&addr=ADDR&clientid=CLIENTID&newname=NEWNAME
第一步同drop流类似,也是首先判断method参数,并设置ctx->filter。
ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module);
if (ctx->method.len == sizeof("publisher") - 1 &&
ngx_memcmp(ctx->method.data, "publisher", ctx->method.len) == 0)
{
ctx->filter = NGX_RTMP_CONTROL_FILTER_PUBLISHER;
} else if (ctx->method.len == sizeof("subscriber") - 1 &&
ngx_memcmp(ctx->method.data, "subscriber", ctx->method.len)
== 0)
{
ctx->filter = NGX_RTMP_CONTROL_FILTER_SUBSCRIBER;
} else if (ctx->method.len == sizeof("client") - 1 &&
ngx_memcmp(ctx->method.data, "client", ctx->method.len) == 0)
{
ctx->filter = NGX_RTMP_CONTROL_FILTER_CLIENT;
} else {
msg = "Undefined filter";
goto error;
}
第二步是调用ngx_rtmp_control_walk遍历寻找指定的RTMP会话,并对其调用ngx_rtmp_control_redirect_handler进行重定向新RTMP会话。
msg = ngx_rtmp_control_walk(r, ngx_rtmp_control_redirect_handler);
if (msg != NGX_CONF_OK) {
goto error;
}
最后是返回HTTP操作的响应结果
ngx_http_send_header(r);
return ngx_http_output_filter(r, &cl);
ngx_rtmp_control_redirect_handler中,首先会调用 ngx_rtmp_close_stream(s, &vc);来关闭旧的流名称,但是其并没有调用ngx_rtmp_finalize_session(s)将RTMP会话进行实际关闭,这样可以看出其实际上是在逻辑上关闭。我们知道ngx_rtmp_close_stream其实是一个函数链表,对本节中最重要的两个函数是next_close_stream = ngx_rtmp_close_stream;
将旧的流在逻辑上关闭后,针对不同的ctx->filter类型,决定执行ngx_rtmp_publish或ngx_rtmp_play。
查找流
接下来我们看本章中的另一个重点源码内容,就是ngx_rtmp_control_walk函数。它的主要作用是查找到符合要求的相应的RTMP会话,然后调用ngx_rtmp_control_handler_t执行指令。ngx_rtmp_control_handler_t就是我们上面介绍的三种指令类型的回调函数,我们再重温一遍:录制回调ngx_rtmp_control_record_handler,关闭会话回调ngx_rtmp_control_drop_handler,重定向回调ngx_rtmp_control_redirect_handler。
首先是获取HTTP请求中的srv参数,用来匹配nginx.conf中rtmp配置的server。默认是配置的第一块server{}。
if (ngx_http_arg(r, (u_char *) "srv", sizeof("srv") - 1, &srv) == NGX_OK) {
sn = ngx_atoi(srv.data, srv.len);
}
然后在server{}块中,查找名称和app参数相同的application{}块。
if (ngx_http_arg(r, (u_char *) "app", sizeof("app") - 1, &app) != NGX_OK) {
app.len = 0;
}
pcacf = cscf->applications.elts;
for (n = 0; n < cscf->applications.nelts; ++n, ++pcacf) {
if (app.len && ((*pcacf)->name.len != app.len ||
ngx_strncmp((*pcacf)->name.data, app.data, app.len)))
{
continue;
}
s = ngx_rtmp_control_walk_app(r, *pcacf);
if (s != NGX_CONF_OK) {
return s;
}
}
下一步就是获取HTTP请求中是否携带name参数。如果没有携带name参数,则会遍历此application中所有的RTMP会话,找到符合下一项的(例如:addr、clientid等),这一点需要特别注意,control指令不携带RTMP会话名称将会使application中所有符合其他条件的RTMP会话被执行,不携带RTMP会话名称对于线上服务可能是非常危险的。
if (ngx_http_arg(r, (u_char *) "name", sizeof("name") - 1, &name) != NGX_OK)
{
for (n = 0; n < (ngx_uint_t) lacf->nbuckets; ++n) {
for (ls = lacf->streams[n]; ls; ls = ls->next) {
s = ngx_rtmp_control_walk_stream(r, ls);
if (s != NGX_CONF_OK) {
return s;
}
}
}
return NGX_CONF_OK;
}
如果携带有RTMP会话名称,会从streams数组中获取到匹配上的RTMP会话链。然后执行ngx_rtmp_control_walk_stream。
for (ls = lacf->streams[ngx_hash_key(name.data, name.len) % lacf->nbuckets];
ls; ls = ls->next)
{
len = ngx_strlen(ls->name);
if (name.len != len || ngx_strncmp(name.data, ls->name, name.len)) {
continue;
}
s = ngx_rtmp_control_walk_stream(r, ls);
if (s != NGX_CONF_OK) {
return s;
}
}
ngx_rtmp_control_walk_stream的本质是遍历lctx链表,然后执行ngx_rtmp_control_walk_session判断RTMP会话是否匹配。
在ngx_rtmp_control_walk_session函数中,会获取HTTP请求中的addr和clientid,判断是否匹配;并判断指令是对推流有效还是对拉流有效。最后将匹配到的接口添加到ctx->sessions数组中。交由上层回调函数使用。
ss = ngx_array_push(&ctx->sessions);
本章小结
通过本章的介绍,相信大家对于RTMP中如何执行指令有了比较清楚的了解。大家明白了录制指令、关闭会话指令、重定向指令的具体实现是什么样子的,大家在以后具体的使用中会有更深刻的理解。