简介
本章介绍RTMP的录制模块。Nginx RTMP中,直播流可以被录制为FLV格式的文件。
录制模块配置解析
record指令指定了应该被准确录制的内容:
- off:不开启录制;
- all:音频和视频录制;
- audio:只录制音频
- video:只录制视频;
- keyframes:只录制视频关键帧;
- manual:从不自动开启录制,使用录制接口控制开始/结束录制。
record_path
指定录制的FLV文件的路径
record_path /tmp/rec;
record_suffix
设置录制文件后缀名,默认是“.flv”
record_suffix _recorded.flv;
录制的后缀也可以是strftime格式的模式。以下指令将产生格式为mystream-24-Apr-13-18:23:38.flv的文件。
record_suffix -%d-%b-%y-%T.flv;
record_unique
如果开启record_unique,则将当前的时间戳添加到录制文件的文件名中。否则,每次进行新的录制的时候都将写入相同的录制文件。默认是关闭状态。
record_append
切换文件追加模式。打开录制后,会将新数据追加到旧文件中,或者在丢失时创建新数据。 文件中的旧数据和新数据之间没有时间间隔。默认为关闭。
record_lock
打开时,当前录制的文件将通过fcntl调用锁定。可以从其他地方进行检查以找出正在录制的文件。 默认为关闭。
在FreeBSD上,您可以使用flock工具进行检查。在Linux上,flock和fcntl是无关的,因此您只需要编写一个简单的脚本来检查文件锁定状态即可。下面是一个简单的python检测脚本。
#!/usr/bin/python
import fcntl, sys
sys.stderr.close()
fcntl.lockf(open(sys.argv[1], "a"), fcntl.LOCK_EX|fcntl.LOCK_NB)
record_max_size
设置最大录制文件大小。
record_max_frames
设置每个录制文件的最大视频帧数。
record_interval
在此配置的时间后重新开始录制。默认情况下关闭。零表示两次录制之间没有延迟。 如果record_unique关闭,则所有记录片段都将写入同一文件。 否则,将附加时间戳,这会使文件不同(给定record_interval大于1秒)。
recorder
创建录制块。 可以在单个application块中创建多个录制块。可以在recorder {}块中指定所有上述与记录有关的指令。所有设置都继承自更高级别。
record_notify
当特定的录制块开始或停止录制文件时,切换向发布者发送NetStream.Record.Start和NetStream.Record.Stop状态消息(onStatus)。状态描述字段保存录制块名称(默认录制块为空)。默认情况下关闭。
FLV格式分析
由于RTMP的录制是保存为FLV格式的文件,所有我们首先需要对FLV的封装格式有所了解。本节中我们将知悉FLV是如何封装音视频数据的。
源码分析
ngx_rtmp_record_postconfiguration
首先来看ngx_rtmp_record_postconfiguration函数。
h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]);
*h = ngx_rtmp_record_av;
h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]);
*h = ngx_rtmp_record_av;
在这个函数里面,我们可以看到录制模块中对于接收到音视频数据的处理都是交由ngx_rtmp_record_av来完成的,也意味着对录制的处理是在ngx_rtmp_record_av函数中。
next_publish = ngx_rtmp_publish;
ngx_rtmp_publish = ngx_rtmp_record_publish;
next_close_stream = ngx_rtmp_close_stream;
ngx_rtmp_close_stream = ngx_rtmp_record_close_stream;
next_stream_begin = ngx_rtmp_stream_begin;
ngx_rtmp_stream_begin = ngx_rtmp_record_stream_begin;
next_stream_eof = ngx_rtmp_stream_eof;
ngx_rtmp_stream_eof = ngx_rtmp_record_stream_eof;
第二步是将录制模块加入到next_publish、next_close_stream函数链中。
ngx_rtmp_record_publish
由于ngx_rtmp_record_publish在next_publish函数链中,所以当有直播推流时会触发这个函数执行。
if (s->auto_pushed) {
goto next;
}
racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module);
if (racf == NULL || racf->rec.nelts == 0) {
goto next;
}
1、判断是否是本机自动转推的流,如果是,则直接跳过; 2、判断是否存在录制模块,并且已经配置了record{}块;
if (ngx_rtmp_record_init(s) != NGX_OK) {
return NGX_ERROR;
}
3、初始化此RTMP会话的录制。
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module);
ngx_memcpy(ctx->name, v->name, sizeof(ctx->name));
ngx_memcpy(ctx->args, v->args, sizeof(ctx->args));
/* terminate name on /../ */
for (p = ctx->name; *p; ++p) {
if (ngx_path_separator(p[0]) &&
p[1] == '.' && p[2] == '.' &&
ngx_path_separator(p[3]))
{
*p = 0;
break;
}
}
4、获取当前的录制路径。
ngx_rtmp_record_start(s);
5、开始录制推流的RTMP会话。
ngx_rtmp_record_init
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module);
if (ctx) {
return NGX_OK;
}
1、判断RTMP会话是否已经设置了录制模块的ctx,如果已经设置了,就不需要进行以下的设置步骤;否则需要创建并设置RTMP会话的录制模块ctx。
racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module);
if (racf == NULL || racf->rec.nelts == 0) {
return NGX_OK;
}
2、判断是否存在录制模块,并且已经配置了record{}块;
ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_record_ctx_t));
if (ctx == NULL) {
return NGX_ERROR;
}
ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_record_module);
3、申请创建RTMP会话的录制模块的ctx,并将其设置给该RTMP会话。
if (ngx_array_init(&ctx->rec, s->connection->pool, racf->rec.nelts,
sizeof(ngx_rtmp_record_rec_ctx_t))
!= NGX_OK)
{
return NGX_ERROR;
}
rracf = racf->rec.elts;
rctx = ngx_array_push_n(&ctx->rec, racf->rec.nelts);
if (rctx == NULL) {
return NGX_ERROR;
}
for (n = 0; n < racf->rec.nelts; ++n, ++rracf, ++rctx) {
ngx_memzero(rctx, sizeof(*rctx));
rctx->conf = *rracf;
rctx->file.fd = NGX_INVALID_FILE;
}
4、racf->rec.elts是配置文件中所有的record{}配置项的数量,对于每一个record{}配置,RTMP会话都需要设置rctx,以保存不同的录制规则。
ngx_rtmp_record_start
ngx_rtmp_record_start是开始录制的接口。
racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module);
if (racf == NULL || racf->rec.nelts == 0) {
return;
}
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module);
if (ctx == NULL) {
return;
}
ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: start");
1、判断当前RTMP会话是否存在了record模块,并且配置了record{}块。 2、判断当前RTMP会话是否设置了record模块的ctx。
rctx = ctx->rec.elts;
for (n = 0; n < ctx->rec.nelts; ++n, ++rctx) {
if (rctx->conf->flags & (NGX_RTMP_RECORD_OFF|NGX_RTMP_RECORD_MANUAL)) {
continue;
}
ngx_rtmp_record_node_open(s, rctx);
}
3、针对每一个配置的record{}块,判断其record指令参数,如果record指令是关闭的,或者是使用录制控制接口进行控制的,那么久直接跳过,否则就会进行ngx_rtmp_record_node_open打开录制文件。
ngx_rtmp_record_node_open
ngx_rtmp_record_node_open的作用是打开文件供录制数据的写入,并且根据需求是否在文件起始位置写入FLV头信息。
if (rctx->file.fd != NGX_INVALID_FILE) {
return NGX_AGAIN;
}
1、如果文件ID已经被打开,则跳过。
rctx->conf = rracf;
rctx->last = *ngx_cached_time;
rctx->timestamp = ngx_cached_time->sec;
ngx_rtmp_record_make_path(s, rctx, &path);
2、根据时间戳,设置录制的路径。
mode = rracf->append ? NGX_FILE_RDWR : NGX_FILE_WRONLY;
create_mode = rracf->append ? NGX_FILE_CREATE_OR_OPEN : NGX_FILE_TRUNCATE;
ngx_memzero(&rctx->file, sizeof(rctx->file));
rctx->file.offset = 0;
rctx->file.log = s->connection->log;
rctx->file.fd = ngx_open_file(path.data, mode, create_mode,
NGX_FILE_DEFAULT_ACCESS);
ngx_str_set(&rctx->file.name, "recorded");
3、判断录制的模式是否为追加,然后配置打开文件的参数。
if (rctx->file.fd == NGX_INVALID_FILE) {
err = ngx_errno;
if (err != NGX_ENOENT) {
ngx_log_error(NGX_LOG_CRIT, s->connection->log, err,
"record: %V failed to open file '%V'",
&rracf->id, &path);
}
ngx_rtmp_record_notify_error(s, rctx);
return NGX_OK;
}
4、如果打开文件失败,上报错误并返回。
#if !(NGX_WIN32)
if (rracf->lock_file) {
err = ngx_lock_fd(rctx->file.fd);
if (err) {
ngx_log_error(NGX_LOG_CRIT, s->connection->log, err,
"record: %V lock failed", &rracf->id);
}
}
#endif
5、对于非windows系统,如果配置record_lock打开时,当前录制的文件将通过fcntl调用锁定。
if (rracf->notify) {
ngx_rtmp_send_status(s, "NetStream.Record.Start", "status",
rracf->id.data ? (char *) rracf->id.data : "");
}
6、如果配置record_notify,在开始录制打开文件时会向发布者发送NetStream.Record.Start信令。
if (rracf->append) {
file_size = 0;
timestamp = 0;
#if (NGX_WIN32)
{
LONG lo, hi;
lo = 0;
hi = 0;
lo = SetFilePointer(rctx->file.fd, lo, &hi, FILE_END);
file_size = (lo == INVALID_SET_FILE_POINTER ?
(off_t) -1 : (off_t) hi << 32 | (off_t) lo);
}
#else
file_size = lseek(rctx->file.fd, 0, SEEK_END);
#endif
if (file_size == (off_t) -1) {
ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno,
"record: %V seek failed", &rracf->id);
goto done;
}
if (file_size < 4) {
goto done;
}
if (ngx_read_file(&rctx->file, buf, 4, file_size - 4) != 4) {
ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno,
"record: %V tag size read failed", &rracf->id);
goto done;
}
p = (u_char *) &tag_size;
p[0] = buf[3];
p[1] = buf[2];
p[2] = buf[1];
p[3] = buf[0];
if (tag_size == 0 || tag_size + 4 > file_size) {
file_size = 0;
goto done;
}
if (ngx_read_file(&rctx->file, buf, 8, file_size - tag_size - 4) != 8)
{
ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno,
"record: %V tag read failed", &rracf->id);
goto done;
}
p = (u_char *) &mlen;
p[0] = buf[3];
p[1] = buf[2];
p[2] = buf[1];
p[3] = 0;
if (tag_size != mlen + 11) {
ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno,
"record: %V tag size mismatch: "
"tag_size=%uD, mlen=%uD", &rracf->id, tag_size, mlen);
goto done;
}
p = (u_char *) ×tamp;
p[3] = buf[7];
p[0] = buf[6];
p[1] = buf[5];
p[2] = buf[4];
done:
rctx->file.offset = file_size;
rctx->time_shift = timestamp;
ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: append offset=%O, time=%uD, tag_size=%uD",
file_size, timestamp, tag_size);
}
7、如果配置了record_append录制追加模式。通过seek到文件末尾,然后读取flv最后一个音视频帧的timestamp信息,将该timestamp作为起始时间戳。
ngx_rtmp_record_av
对于接收到的每一个音视频帧,都会调用ngx_rtmp_record_av进入录制逻辑。
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module);
if (ctx == NULL) {
return NGX_OK;
}
rctx = ctx->rec.elts;
for (n = 0; n < ctx->rec.nelts; ++n, ++rctx) {
ngx_rtmp_record_node_av(s, rctx, h, in);
}
1、判断当前会话是否设置了录制模块的ctx,如果没有配置,直接返回。对于设置完成的会话,会根据配置文件中record{}项的数量,分别调用ngx_rtmp_record_node_av。
ngx_rtmp_record_node_av
rracf = rctx->conf;
if (rracf->flags & NGX_RTMP_RECORD_OFF) {
ngx_rtmp_record_node_close(s, rctx);
return NGX_OK;
}
1、如果配置中是关闭了录制,就调用ngx_rtmp_record_node_close,将当前会话的录制ctx关闭。
keyframe = (h->type == NGX_RTMP_MSG_VIDEO)
? (ngx_rtmp_get_video_frame_type(in) == NGX_RTMP_VIDEO_KEY_FRAME)
: 0;
2、获取当前帧是否是视频的关键帧。
brkframe = (h->type == NGX_RTMP_MSG_VIDEO)
? keyframe
: (rracf->flags & NGX_RTMP_RECORD_VIDEO) == 0;
3、当前是视频帧时,brkframe = keyframe的值;对于非视频帧,当前record配置需要录制视频数据时为0 ,否则为1。
if (brkframe && (rracf->flags & NGX_RTMP_RECORD_MANUAL) == 0) {
if (rracf->interval != (ngx_msec_t) NGX_CONF_UNSET) {
next = rctx->last;
next.msec += rracf->interval;
next.sec += (next.msec / 1000);
next.msec %= 1000;
if (ngx_cached_time->sec > next.sec ||
(ngx_cached_time->sec == next.sec &&
ngx_cached_time->msec > next.msec))
{
ngx_rtmp_record_node_close(s, rctx);
ngx_rtmp_record_node_open(s, rctx);
}
} else if (!rctx->failed) {
ngx_rtmp_record_node_open(s, rctx);
}
}
4、打开录制文件
codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module);
if(codec_ctx) {
}
5、获取RTMP会话的编码信息。
/* AAC header */
if (!rctx->aac_header_sent && codec_ctx->aac_header &&
(rracf->flags & NGX_RTMP_RECORD_AUDIO))
{
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: %V writing AAC header", &rracf->id);
ch.type = NGX_RTMP_MSG_AUDIO;
ch.mlen = ngx_rtmp_record_get_chain_mlen(codec_ctx->aac_header);
if (ngx_rtmp_record_write_frame(s, rctx, &ch,
codec_ctx->aac_header, 0)
!= NGX_OK)
{
return NGX_OK;
}
rctx->aac_header_sent = 1;
}
6、如果需要录制音频数据,判断音频AAC头是否已经写入录制文件,如果没有写入,就调用ngx_rtmp_record_write_frame将音频AAC的编码信息帧写入到录制文件中。
/* AVC header */
if (!rctx->avc_header_sent && codec_ctx->avc_header &&
(rracf->flags & (NGX_RTMP_RECORD_VIDEO|
NGX_RTMP_RECORD_KEYFRAMES)))
{
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: %V writing AVC header", &rracf->id);
ch.type = NGX_RTMP_MSG_VIDEO;
ch.mlen = ngx_rtmp_record_get_chain_mlen(codec_ctx->avc_header);
if (ngx_rtmp_record_write_frame(s, rctx, &ch,
codec_ctx->avc_header, 0)
!= NGX_OK)
{
return NGX_OK;
}
rctx->avc_header_sent = 1;
}
7、同理,如果需要录制视频数据,判断视频AVC头是否已经写入录制文件,如果没有写入,就调用ngx_rtmp_record_write_frame将视频AVC的编码信息帧写入到录制文件中。
if (h->type == NGX_RTMP_MSG_VIDEO) {
if (codec_ctx && codec_ctx->video_codec_id == NGX_RTMP_VIDEO_H264 &&
!rctx->avc_header_sent)
{
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: %V skipping until H264 header", &rracf->id);
return NGX_OK;
}
if (ngx_rtmp_get_video_frame_type(in) == NGX_RTMP_VIDEO_KEY_FRAME &&
((codec_ctx && codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264) ||
!ngx_rtmp_is_codec_header(in)))
{
rctx->video_key_sent = 1;
}
if (!rctx->video_key_sent) {
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: %V skipping until keyframe", &rracf->id);
return NGX_OK;
}
} else {
if (codec_ctx && codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC &&
!rctx->aac_header_sent)
{
ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: %V skipping until AAC header", &rracf->id);
return NGX_OK;
}
}
8、判断是否需要跳过当前接收到的帧数据。对于视频帧,如果还没有接收到视频编码头信息或者还没有接收到关键帧,就会跳过当前的视频帧数据;对应音频帧,如果还没有收到音频的AAC编码信息,也一样会跳过当前的音频帧。
ngx_rtmp_record_write_frame(s, rctx, h, in, 1);
9、最后,将需要录制的音视频帧数据调用ngx_rtmp_record_write_frame写入录制文件中。
ngx_rtmp_record_write_frame
/* write tag header */
ph = hdr;
*ph++ = (u_char)h->type;
p = (u_char*)&h->mlen;
*ph++ = p[2];
*ph++ = p[1];
*ph++ = p[0];
p = (u_char*)×tamp;
*ph++ = p[2];
*ph++ = p[1];
*ph++ = p[0];
*ph++ = p[3];
*ph++ = 0;
*ph++ = 0;
*ph++ = 0;
tag_size = (ph - hdr) + h->mlen;
if (ngx_write_file(&rctx->file, hdr, ph - hdr, rctx->file.offset)
== NGX_ERROR)
{
ngx_rtmp_record_notify_error(s, rctx);
ngx_close_file(rctx->file.fd);
return NGX_ERROR;
}
1、计算FLV tag的头信息,包括长度,时间戳;并计算当前tag的大小。 然后将tag头信息写入文件中。
/* write tag body
* FIXME: NGINX
* ngx_write_chain seems to fit best
* but it suffers from uncontrollable
* allocations.
* we're left with plain writing */
for(; in; in = in->next) {
if (in->buf->pos == in->buf->last) {
continue;
}
if (ngx_write_file(&rctx->file, in->buf->pos, in->buf->last
- in->buf->pos, rctx->file.offset)
== NGX_ERROR)
{
return NGX_ERROR;
}
}
2、在tag头写入录制文件后,将数据写入。音视频数据都保存在ngx_chain_t in中。
/* write tag size */
ph = hdr;
p = (u_char*)&tag_size;
*ph++ = p[3];
*ph++ = p[2];
*ph++ = p[1];
*ph++ = p[0];
if (ngx_write_file(&rctx->file, hdr, ph - hdr,
rctx->file.offset)
== NGX_ERROR)
{
return NGX_ERROR;
}
3、最后将tag大小写入文件中。
小结
本章介绍了Nginx RTMP的录制模块,我们可以清楚的知道RTMP直播流是如何录制成文件的全过程。首先我们了解了FLV的封装格式,因为RTMP的录制文件是FLV格式的。其次,我们知道应该如何设置配置文件,用来生成录制文件;最后,我们知道了录制中对每一个音视频帧数据的处理过程。
|