1 ngx_rtmp_record_module模块功能描述
本模块主要是直播转点播录制的功能,跟点播这个相关的模块还有
ngx_rtmp_play_module,
ngx_rtmp_flv_module,
ngx_rtmp_mp4_module
ngx_rtmp_control_module 这个模块提供可以控制录制的开始和结束
本模块只分析record模块,其他点播相关后续再分析。
1.1 配置描述
本模块提供的配置相对来说比较多,录制方式也多样化,以下只说明录制的方式。
每个配置具体描述可以参考:https://github.com/arut/nginx-rtmp-module/wiki/Directives#record说的比较详细
按录制方式区分:
按时间间隔录制: record_interval 15s/m
按帧数个数录制: record_max_frames 128
按文件大小录制: record_max_size 10M
按录制文件的类型可以分以下三类:
只录制音频 recorder audio
只录制视频 recorder video
只录制关键帧 recorder keyframes
录制音视频 recorder all
按录制方式:(recorder 是支持配置多个参数的)
自动录制 recorder all
手动录制 recorder all manual
手动录制方式需要rtmp_control配置项配合使用:
http {
server {
listen 80;
location /control {
rtmp_control all;
}
}
}
rtmp {
server {
listen 1935;
application live {
live on;
recorder rec1 {
record all manual;
record_suffix all.flv;
record_path /tmp/rec;
record_unique on;
}
}
}
}
请求方式:http://localhost/control/record/start?app=live&name=test&rec=rec1"
http://localhost/control/record/stop?app=live&name=test&rec=rec1"
2 record源码剖析
主要从录制音视频消息handler注册,相关数据结构解析,以及录制写文件这三方面分析。
整体录制过程主要是这个两个函数:
ngx_rtmp_record_node_av 接受音视频消息,然后进行各种录制判断
ngx_rtmp_record_write_frame 按flv tag的形式写文件。
1)先写flv header---tag header---tag body---tag size
2)按帧个数以及按文件大小进行文件分割
ngx_rtmp_record_node_close 关闭文件操作以及相应关闭文件的变量重新置位成关闭状态
ngx_rtmp_record_node_open 打开文件的初始化操作,文件名以及各种判断录制条件变量的初始化
其他一些函数不做具体分析:
ngx_rtmp_record_find主要是提供control模块根据录制机名字查找配置录制机id
ngx_rtmp_record_close 执行关闭文件操作提供control模块结束录制
ngx_rtmp_record_open 提供control模块开启录制
ngx_rtmp_record_write_header 写flv header
2.1 录制核心函数ngx_rtmp_record_node_av
录制的主要逻辑是在ngx_rtmp_record_node_av函数当中。在ngx_rtmp_record_av当中调用,这个函数是在解析配置完之后,在ngx_rtmp_module_t中postconfiguration注
册的,在整个rtmp模块当中这个阶段会注册rtmp消息事件的回调。
static ngx_int_t
ngx_rtmp_record_postconfiguration(ngx_conf_t *cf)
{
ngx_rtmp_core_main_conf_t *cmcf;
ngx_rtmp_handler_pt *h;
ngx_rtmp_record_done = ngx_rtmp_record_done_init;
cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module);
/* 注册音频和视频的消息回调处理 */
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;
······
}
2.2 相关数据结构
ngx_rtmp_record_mask主要是recorder录制配置开关值对应整数类型只和control的类似
#define NGX_RTMP_RECORD_OFF 0x01
#define NGX_RTMP_RECORD_AUDIO 0x02
#define NGX_RTMP_RECORD_VIDEO 0x04
#define NGX_RTMP_RECORD_KEYFRAMES 0x08
#define NGX_RTMP_RECORD_MANUAL 0x10
static ngx_conf_bitmask_t ngx_rtmp_record_mask[] = {
{ ngx_string("off"), NGX_RTMP_RECORD_OFF },
{ ngx_string("all"), NGX_RTMP_RECORD_AUDIO |
NGX_RTMP_RECORD_VIDEO },
{ ngx_string("audio"), NGX_RTMP_RECORD_AUDIO },
{ ngx_string("video"), NGX_RTMP_RECORD_VIDEO },
{ ngx_string("keyframes"), NGX_RTMP_RECORD_KEYFRAMES },
{ ngx_string("manual"), NGX_RTMP_RECORD_MANUAL },
{ ngx_null_string, 0 }
};
ngx_rtmp_record_ctx_t是整个录制模块的上下文信息,主要保存了所有录制机的上下文以及流名和一些rtmp请求的参数信息
typedef struct {
ngx_array_t rec; /* ngx_rtmp_record_rec_ctx_t */
u_char name[NGX_RTMP_MAX_NAME];
u_char args[NGX_RTMP_MAX_ARGS];
} ngx_rtmp_record_ctx_t;
ngx_rtmp_record_rec_ctx_t是每个录制机的上下文结构,一个录制上下文包过若干个子的录制机。对应配置里面其实可以看出来,app有很多字的字rec block模块
typedef struct {
ngx_rtmp_record_app_conf_t *conf; /* 每个子的录制机的配置文件 */
ngx_file_t file; /* 打开的文件描述信息 */
ngx_uint_t nframes; /* 按帧数缓存的话,统计帧的个数 */
uint32_t epoch, time_shift;
ngx_time_t last; /* 打开文件的时间,带秒和毫秒的 */
time_t timestamp; /* 文件打开时的时间, 以时间后缀的文件名的时候有用 */
unsigned failed:1; /* */
unsigned initialized:1; /* 开始写文件的初始化,主要写flv header的工作 */
unsigned aac_header_sent:1; /* 是否已经写入aac音频头 */
unsigned avc_header_sent:1; /* 是否已经写入avc视频头 */
unsigned video_key_sent:1; /* 每次写入视频帧先按关键帧开始写 */
unsigned audio:1;
unsigned video:1;
} ngx_rtmp_record_rec_ctx_t;
2.3 录制核心逻辑大致流程
接下来分析ngx_rtmp_record_node_av函数,整体逻辑主要是按不同录制方式写flv文件。由于不同录制方式和录制类型比较多,导致逻辑相对来说比较复杂一点,
主要是各种if判断条件分支比较多。
static ngx_int_t
ngx_rtmp_record_node_av(ngx_rtmp_session_t *s, ngx_rtmp_record_rec_ctx_t *rctx,
ngx_rtmp_header_t *h, ngx_chain_t *in)
{
ngx_time_t next;
ngx_rtmp_header_t ch;
ngx_rtmp_codec_ctx_t *codec_ctx;
ngx_int_t keyframe, brkframe;
ngx_rtmp_record_app_conf_t *rracf;
rracf = rctx->conf;
/* 录制是否开启,默认是关闭的 */
if (rracf->flags & NGX_RTMP_RECORD_OFF) {
ngx_rtmp_record_node_close(s, rctx);
return NGX_OK;
}
/* 判断当前帧是不是关键帧 */
keyframe = (h->type == NGX_RTMP_MSG_VIDEO)
? (ngx_rtmp_get_video_frame_type(in) == NGX_RTMP_VIDEO_KEY_FRAME)
: 0;
/* 这个变量理解起来比较绕。仔细分析一下
* 如果当前帧是视频帧,brkframe为keyframe,关键帧为1 非关键帧0
* 如果是音频帧,再区分录制类型(视频100,音频10,音视频110,关键帧1000, 手动录制1 0000) 只有类型为音频和关键帧才会为1,其他为0
* 如果按录制音视频的情况。当前帧是视频只有关键帧才为1,音频帧为0
* 如果按纯视频录制,只有关键帧来了才为1,非关键帧为0
* 如果按纯音频录制,这个一直为1
* 如果按关键帧录制,这个一直为1
* /
brkframe = (h->type == NGX_RTMP_MSG_VIDEO)
? keyframe
: (rracf->flags & NGX_RTMP_RECORD_VIDEO) == 0;
/* (rracf->flags & NGX_RTMP_RECORD_MANUAL) == 0 表示没有开启手动录制的方式
* 没有开启手动录制的情况才会进入
* /
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);
}
}
/* brkframe 分析手动录制音视频的情况,只有视频为关键帧的时候才为1,音频为0
* 说明开起了手动录制 并且帧个数为0只有是关键帧才会继续往下走进行录制,非关键帧都不写文件
* 此种情况保证从关键帧开始录制
*/
if ((rracf->flags & NGX_RTMP_RECORD_MANUAL) &&
!brkframe && rctx->nframes == 0)
{
return NGX_OK;
}
/* 未打开文件描述,不进行录制 */
if (rctx->file.fd == NGX_INVALID_FILE) {
return NGX_OK;
}
/* 录制方式是不录制音频(有可能是录制纯视频或者关键帧那种情况),来了音频不录制 */
if (h->type == NGX_RTMP_MSG_AUDIO &&
(rracf->flags & NGX_RTMP_RECORD_AUDIO) == 0)
{
return NGX_OK;
}
/* (rracf->flags & NGX_RTMP_RECORD_VIDEO) == 0 为true表示不按纯视频录制
* (rracf->flags & NGX_RTMP_RECORD_KEYFRAMES) == 0 不按关键帧录制
* 此种情况表示不按纯视频且纯关键帧录制方式但是当前帧是非关键帧的情况不写文件(有可能是只录制纯音频的请,视频帧来了不录制)
* /
if (h->type == NGX_RTMP_MSG_VIDEO &&
(rracf->flags & NGX_RTMP_RECORD_VIDEO) == 0 &&
((rracf->flags & NGX_RTMP_RECORD_KEYFRAMES) == 0 || !keyframe))
{
return NGX_OK;
}
/* 写文件之前先初始化下,一般写一下flv 文件头 */
if (!rctx->initialized) {
rctx->initialized = 1;
rctx->epoch = h->timestamp - rctx->time_shift;
if (rctx->file.offset == 0 &&
ngx_rtmp_record_write_header(&rctx->file) != NGX_OK)
{
ngx_rtmp_record_node_close(s, rctx);
return NGX_OK;
}
}
codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module);
if (codec_ctx) {
ch = *h;
/* AAC header 写aac sequence header 每个文件只写一次aac_header_sent变量控制的 */
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;
}
/* AVC header 写h264 sequence 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;
}
}
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;
}
}
/* 将每帧数据写入文件 */
return ngx_rtmp_record_write_frame(s, rctx, h, in, 1);
}
以上主要是处理了按录制间隔方式录制、手动录制控制以及录制写之前优先保证先写音视频头以及录制视频的时候先从关键帧的位置开始录制
static ngx_int_t
ngx_rtmp_record_write_frame(ngx_rtmp_session_t *s,
ngx_rtmp_record_rec_ctx_t *rctx,
ngx_rtmp_header_t *h, ngx_chain_t *in,
ngx_int_t inc_nframes)
{
u_char hdr[11], *p, *ph;
uint32_t timestamp, tag_size;
ngx_rtmp_record_app_conf_t *rracf;
rracf = rctx->conf;
ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: %V frame: mlen=%uD",
&rracf->id, h->mlen);
if (h->type == NGX_RTMP_MSG_VIDEO) {
rctx->video = 1;
} else {
rctx->audio = 1;
}
timestamp = h->timestamp - rctx->epoch;
if ((int32_t) timestamp < 0) {
ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0,
"record: %V cut timestamp=%D", &rracf->id, timestamp);
timestamp = 0;
}
/* 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;
/* tag header 写入文件 */
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;
}
/* 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;
}
/* 将每帧内容数据写入文件即tag body */
if (ngx_write_file(&rctx->file, in->buf->pos, in->buf->last
- in->buf->pos, rctx->file.offset)
== NGX_ERROR)
{
return NGX_ERROR;
}
}
/* write tag size */
ph = hdr;
p = (u_char*)&tag_size;
*ph++ = p[3];
*ph++ = p[2];
*ph++ = p[1];
*ph++ = p[0];
/* 将当前帧的tag_size 值写入文件 */
if (ngx_write_file(&rctx->file, hdr, ph - hdr,
rctx->file.offset)
== NGX_ERROR)
{
return NGX_ERROR;
}
/* 统计帧的个数 */
rctx->nframes += inc_nframes;
/* 主要是处理从文件大小和帧个数 方式录制
* 1)达到配置rracf->max_size指定文件大小之后,分割文件,写入的文件大小根据写的文件偏移位置来区分
* 2)达到指定最大帧数之后,重新分割文件
* /
if ((rracf->max_size && rctx->file.offset >= (ngx_int_t) rracf->max_size) ||
(rracf->max_frames && rctx->nframes >= rracf->max_frames))
{
ngx_rtmp_record_node_close(s, rctx);
}
return NGX_OK;
}
本模块难点就是理解ngx_rtmp_record_node_av函数当中各种录制方式的变量判断。
整体可以这样理解:
按推流的数据可以分为:
纯音频
纯视频
音视频
按录制方式
手动录制和自动录制
按时间间隔录制
按文件大小录制
按帧个数录制
反正归纳起来就是这几种情况组合起来的一些逻辑判断,以及保存成flv文件注意“先写flv文件头”,“其次写音视频头”, 再就是视频从关键帧开始写。
本模块改进
1)可以增加gop的方式录制
2)增加些MP4文件格式
3)目前只是写本地文件,有的是需要存到存储上面的,所以需要一些文件上传的功能