直播音视频数据分发
我知道,在直播过程中会有成千上万的用户同时观看,对应到Nginx RTMP到实现中,就是支持一个publisher,同时有多个subscriber进行拉流播放。本节我们将重点介绍ngx_rtmp_live_module模块是如何实现将直播音视频数据分发给多个subscriber的。
注册音视频数据回调处理函数
static ngx_int_t
ngx_rtmp_live_postconfiguration(ngx_conf_t *cf)
{
......
/* register raw event handlers */
h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]);
*h = ngx_rtmp_live_av;
h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]);
*h = ngx_rtmp_live_av;
......
}
ngx_rtmp_live_module在postconfiguration阶段,将ngx_rtmp_live_av添加进音频和视频事件的处理函数链中。当推流端开始推流直播时,RTMP端ngx_rtmp_receive_message会接收到音视频数据,会依次调用cmcf->events中已经注册的事件,会执行本模块端ngx_rtmp_live_av函数。
ngx_rtmp_live_av函数分析
- 判断是否打开直播配置,并且输入的ngx_chain_t是有效的。
lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module);
if (lacf == NULL) {
return NGX_ERROR;
}
if (!lacf->live || in == NULL || in->buf == NULL) {
return NGX_OK;
}
- 向推流端发送直播开始指令
if (!ctx->stream->active) {
ngx_rtmp_live_start(s);
}
- 设置publisher推流idle事件
if (ctx->idle_evt.timer_set) {
ngx_add_timer(&ctx->idle_evt, lacf->idle_timeout);
}
如果在配置文件中设置了publisher的idle的超时时间,就会设置idle的定时器。在每次接收到音视频数据时,更新idle定时器。如果在超时时间内没有接收到数据,就会触发定时器事件,执行相应的idle处理函数,即ngx_rtmp_live_idle。
- 发送消息时,需要使用的优先级
prio = (h->type == NGX_RTMP_MSG_VIDEO ?
ngx_rtmp_get_video_frame_type(in) : 0);
- 设置chunk stream数据
csidx = !(lacf->interleave || h->type == NGX_RTMP_MSG_VIDEO);
cs = &ctx->cs[csidx];
ngx_memzero(&ch, sizeof(ch));
ch.timestamp = h->timestamp;
ch.msid = NGX_RTMP_MSID;
ch.csid = cs->csid;
ch.type = h->type;
lh = ch;
if (cs->active) {
lh.timestamp = cs->timestamp;
}
clh = lh;
clh.type = (h->type == NGX_RTMP_MSG_AUDIO ? NGX_RTMP_MSG_VIDEO :
NGX_RTMP_MSG_AUDIO);
cs->active = 1;
cs->timestamp = ch.timestamp;
- 预处理
rpkt = ngx_rtmp_append_shared_bufs(cscf, NULL, in);
ngx_rtmp_prepare_message(s, &ch, &lh, rpkt);
codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module);
if (codec_ctx) {
if (h->type == NGX_RTMP_MSG_AUDIO) {
header = codec_ctx->aac_header;
if (lacf->interleave) {
coheader = codec_ctx->avc_header;
}
if (codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC &&
ngx_rtmp_is_codec_header(in))
{
prio = 0;
mandatory = 1;
}
} else {
header = codec_ctx->avc_header;
if (lacf->interleave) {
coheader = codec_ctx->aac_header;
}
if (codec_ctx->video_codec_id == NGX_RTMP_VIDEO_H264 &&
ngx_rtmp_is_codec_header(in))
{
prio = 0;
mandatory = 1;
}
}
if (codec_ctx->meta) {
meta = codec_ctx->meta;
meta_version = codec_ctx->meta_version;
}
}
1、将输入的in,拷贝一份到rpkt。
2、使用ngx_rtmp_prepare_message对rpkt进行处理,主要是对rtmp的数据信息头进行处理。
3、获取ngx_rtmp_codec_module中解析的codec信息。并判断当前转发的数据是不是codec数据。
4、对于header和coheader,只有在配置lacf->sync(即上节中介绍的用于同步音视频信息的超时时间),才会被使用到。并且在lacf->interleave为true时,即配置了在同一个chunk stream中传输时,音频头和视频头会被一起发送到subscriber,否则,先接收到音频就先转发音频头,先接收到视频就先转发视频头。这时,将mandatory=1,即这个数据包是必须要发送成功的。
- 向每个subscriber转发数据
/* broadcast to all subscribers */
for (pctx = ctx->stream->ctx; pctx; pctx = pctx->next) {
if (pctx == ctx || pctx->paused) {
continue;
}
ss = pctx->session;
cs = &pctx->cs[csidx];
/* send metadata */
if (meta && meta_version != pctx->meta_version) {
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0,
"live: meta");
if (ngx_rtmp_send_message(ss, meta, 0) == NGX_OK) {
pctx->meta_version = meta_version;
}
}
......
......
/* send relative packet */
ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0,
"live: rel %s packet delta=%uD",
type_s, delta);
if (ngx_rtmp_send_message(ss, rpkt, prio) != NGX_OK) {
++pctx->ndropped;
cs->dropped += delta;
if (mandatory) {
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0,
"live: mandatory packet failed");
ngx_rtmp_finalize_session(ss);
}
continue;
}
cs->timestamp += delta;
++peers;
ss->current_time = cs->timestamp;
}
1、遍历stream对应的ngx_rtmp_live_ctx_t ctx链表。上节中我们介绍了一个直播流的publisher和subscriber是由ngx_rtmp_live_ctx_t ctx链表串联起来的,因此遍历ctx链表,可以获取到每个subscriber。
2、判断当前遍历到到节点是否是publiser自身,或者这个节点已经是paused状态。如果是,则跳过。
3、如果是向这个subscriber首次转发音视频数据,则先发送meta信息。
4、将当前到音视频数据rpkt调用ngx_rtmp_send_message发送出去。如果发送失败,进行drop数据统计,如果mandatory=1,即这个rpkt是必须确保发送成功的数据,则关闭这个subscriber连接session。
5、中间省略的部分是当配置了sync时,当dropped超过sync的值时,从重新发送video/audio的codec信息。
- 更新统计信息
ngx_rtmp_update_bandwidth(&ctx->stream->bw_in, h->mlen);
ngx_rtmp_update_bandwidth(&ctx->stream->bw_out, h->mlen * peers);
ngx_rtmp_update_bandwidth(h->type == NGX_RTMP_MSG_AUDIO ?
&ctx->stream->bw_in_audio :
&ctx->stream->bw_in_video,
h->mlen);