ngx_rtmp_live_module 概述
ngx_rtmp_live_module模块负责直播音视频数据的转发。它将从publisher推流上来的音视频数据,转发给每个player进行播放。本章主要包括以下两个方面:
live module数据结构分析
Nginx RTMP服务器支持多个直播同时推流,并且支持每个直播有多个客户端进行播放观看。那么RTMP是如何将这些直播流组织在一起的?
直播流主要依赖以下三个struct组织在一起:ngx_rtmp_live_ctx_s、ngx_rtmp_live_stream_s、ngx_rtmp_live_app_conf_t,我们按照源码中的顺序依次介绍。
ngx_rtmp_live_ctx_s
struct ngx_rtmp_live_ctx_s {
ngx_rtmp_session_t *session; /*rtmp session*/
ngx_rtmp_live_stream_t *stream; /*当前直播流*/
ngx_rtmp_live_ctx_t *next; /*相同流名称的直播流通过next链表串联起来*/
ngx_uint_t ndropped; /*丢包数量*/
ngx_rtmp_live_chunk_stream_t cs[2]; /*当前转发的音视频包相关数据*/
ngx_uint_t meta_version; /*meta版本号*/
ngx_event_t idle_evt; /*推流IDLE定时器timer*/
unsigned active:1; /*推流状态*/
unsigned publishing:1; /*判断是否publish流*/
unsigned silent:1; /*是否本地relay*/
unsigned paused:1; /*暂停*/
};
ngx_rtmp_live_stream_s
struct ngx_rtmp_live_stream_s {
u_char name[NGX_RTMP_MAX_NAME];/*直播流名称*/
ngx_rtmp_live_stream_t *next; /*不同的直播流通过next链表串联起来*/
ngx_rtmp_live_ctx_t *ctx; /*相同名称的直播流通过ctx串联起来*/
ngx_rtmp_bandwidth_t bw_in; /*入口带宽*/
ngx_rtmp_bandwidth_t bw_in_audio; /*音频入口带宽*/
ngx_rtmp_bandwidth_t bw_in_video; /*视频入口带宽*/
ngx_rtmp_bandwidth_t bw_out; /*出口带宽*/
ngx_msec_t epoch; /*直播流创建时间*/
unsigned active:1; /*直播流状态*/
unsigned publishing:1; /*是否已经开始推流*/
};
ngx_rtmp_live_app_conf_t
typedef struct {
ngx_int_t nbuckets; /*根据流名称进行hash时,bucket的数量*/
ngx_rtmp_live_stream_t **streams; /*每个bucket中,对应ngx_rtmp_live_stream_t*数组*/
ngx_flag_t live;
ngx_flag_t meta;
ngx_msec_t sync;
ngx_msec_t idle_timeout;
ngx_flag_t atc;
ngx_flag_t interleave;
ngx_flag_t wait_key;
ngx_flag_t wait_video;
ngx_flag_t publish_notify;
ngx_flag_t play_restart;
ngx_flag_t idle_streams;
ngx_msec_t buflen;
ngx_pool_t *pool; /*app conf的内存池*/
ngx_rtmp_live_stream_t *free_streams; /*空闲的streams*/
} ngx_rtmp_live_app_conf_t;
ngx_rtmp_live_app_conf_t中的字段对应的是Nginx配置文件RTMP application域中的配置项。
live
直播开关,on或off。
meta
设置metadata的发送模式。默认值:on
on:订阅者接收包含预定义字段的重新构建的metadata数据包,包含宽高等。
copy:订阅者接收推流端精确的metadata副本,包含标准字段和自定义字段。
off:关闭向订阅者发送任何metadata。
sync timeout
用于同步音视频流。如果订阅者的带宽不足以按照推流端的发送速率接收数据,服务器就会丢弃一些帧。这会导致同步问题。当时间戳差异超过设置当同步参数时,将发送一个绝对帧来修复差异。默认值是300毫秒。
idle_timeout
idle_timeout是drop_idle_publisher设置的值。当推流端在一定时间没有发送音视频数据时,会关闭连接。
interleave
在此模式下,音频和视频数据在同一RTMP块流上传输。
wait_key
确保视频流从关键帧开始
wait_video
在第一个视频帧到来前,忽略所有的音频帧。和wait_key同时使用,确保订阅者首先接收到关键帧。可以调整编码器的gop减少延迟。
publish_notify
向订阅者发送NetStream.Play.PublishNotify和NetStream.Play.UnpublishNotify。
play_restart
on:nginx-rtmp在publisher每次开始和结束推流时都向每个订阅者发送NetStream.Play.Start和NetStream.Play.Stop。
off:每个订阅者只有在开始和结束播放时,才会接收到NetStream.Play.Start和NetStream.Play.Stop。
idle_streams
如果禁用,nginx rtmp则禁止订阅者连接到空闲或不存在的直播流,并在推流断开连接时断开所有订阅者的连接。
流的组织结构
根据以上的结构体,可以构建出一个类似树的结构体,来组织直播服务器上的所有直播流。如图所示:
添加直播流
本节我们介绍rtmp live模块是如何将新的直播流加入到系统中的。
ngx_rtmp_live_publish
ngx_rtmp_live_join
ngx_rtmp_live_play
ngx_rtmp_live_get_stream
从源码中函数的调用关系可以看出,添加新的直播流由publish和play触发。ngx_rtmp_live_publish和ngx_rtmp_live_play中加入新的直播流的处理逻辑基本一致,只是角色不同,ngx_rtmp_live_publish是将直播流作为publisher加入,ngx_rtmp_live_ply是将直播流作为subscriber加入。添加直播流的主要处理流程是在ngx_rtmp_live_join中完成的。
ngx_rtmp_live_join函数分析
- 获取rtmp session的ngx_rtmp_live_module模块上下文。
判断ngx_rtmp_live_ctx_t ctx是否为NULL,如果为NULL,则新创建一个ngx_rtmp_live_ctx_t结构,并调用ngx_rtmp_set_ctx进行设置。
lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module);
if (lacf == NULL) {
return;
}
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module);
if (ctx && ctx->stream) {
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"live: already joined");
return;
}
if (ctx == NULL) {
ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_live_ctx_t));
ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_live_module);
}
- 获取rtmp session的ngx_rtmp_live_stream_t结构。
stream = ngx_rtmp_live_get_stream(s, name, publisher || lacf->idle_streams);
- 对于subscriber,判断是否已经开始推流并且是否允许存在空闲的播放流,如果都为false,则关闭subscriber的连接。
if (stream == NULL ||
!(publisher || (*stream)->publishing || lacf->idle_streams))
{
ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
"live: stream not found");
ngx_rtmp_send_status(s, "NetStream.Play.StreamNotFound", "error",
"No such stream");
ngx_rtmp_finalize_session(s);
return;
}
- 对于publisher,判断是否已经存在推流。Nginx RTMP默认在同一个worker进程,同一个application下不允许同一个流名称存在多个publisher,即禁止重复推流。
if (publisher) {
if ((*stream)->publishing) {
ngx_log_error(NGX_LOG_ERR, s->connection->log, 0,
"live: already publishing");
ngx_rtmp_send_status(s, "NetStream.Publish.BadName", "error",
"Already publishing");
return;
}
(*stream)->publishing = 1;
}
- 将新创建的ngx_rtmp_live_ctx_t加入到stream的ctx链表中。
ctx->stream = *stream;
ctx->publishing = publisher;
ctx->next = (*stream)->ctx;
(*stream)->ctx = ctx;
- 其他处理
if (lacf->buflen) {
s->out_buffer = 1;
}
ctx->cs[0].csid = NGX_RTMP_CSID_VIDEO;
ctx->cs[1].csid = NGX_RTMP_CSID_AUDIO;
if (!ctx->publishing && ctx->stream->active) {
ngx_rtmp_live_start(s);
}
ngx_rtmp_live_get_stream函数分析
- 根据流名称哈希到对应到ngx_rtmp_live_app_conf_t到streams数组的bucket。获取该bucket中的ngx_rtmp_live_stream_t链表(可能存在多个不同的直播流)。
lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module);
if (lacf == NULL) {
return NULL;
}
len = ngx_strlen(name);
stream = &lacf->streams[ngx_hash_key(name, len) % lacf->nbuckets];
- 和链表中的stream逐一比较流名称。如果匹配到相同流名称到stream,则直接返回。
for (; *stream; stream = &(*stream)->next) {
if (ngx_strcmp(name, (*stream)->name) == 0) {
return stream;
}
}
- 判断是否需要新创建stream,如果是publisher,允许新创建stream,对于subscriber,如果配置idle_streams为true,也允许新创建stream。
if (!create) {
return NULL;
}
- 从空闲free_streams获取stream,或者直接从内存池中创建新的stream,并对stream进行初始化。
if (lacf->free_streams) {
*stream = lacf->free_streams;
lacf->free_streams = lacf->free_streams->next;
} else {
*stream = ngx_palloc(lacf->pool, sizeof(ngx_rtmp_live_stream_t));
}
ngx_memzero(*stream, sizeof(ngx_rtmp_live_stream_t));
ngx_memcpy((*stream)->name, name,
ngx_min(sizeof((*stream)->name) - 1, len));
(*stream)->epoch = ngx_current_msec;