diff --git a/ngx_rtmp.h b/ngx_rtmp.h index 69953cf..d86a59c 100644 --- a/ngx_rtmp.h +++ b/ngx_rtmp.h @@ -424,6 +424,19 @@ ngx_int_t ngx_rtmp_receive_amf(ngx_rtmp_session_t *s, ngx_chain_t *in, ngx_rtmp_amf_elt_t *elts, size_t nelts); +/* Frame types */ +#define NGX_RTMP_VIDEO_KEY_FRAME 1 +#define NGX_RTMP_VIDEO_INTER_FRAME 2 +#define NGX_RTMP_VIDEO_DISPOSABLE_FRAME 3 + + +static inline ngx_int_t +ngx_rtmp_get_video_frame_type(ngx_chain_t *in) +{ + return (in->buf->pos[0] & 0xf0) >> 4; +} + + extern ngx_uint_t ngx_rtmp_max_module; extern ngx_module_t ngx_rtmp_core_module; diff --git a/ngx_rtmp_live_module.c b/ngx_rtmp_live_module.c index db6d3db..77db968 100644 --- a/ngx_rtmp_live_module.c +++ b/ngx_rtmp_live_module.c @@ -251,19 +251,9 @@ next: } -#define NGX_RTMP_VIDEO_KEY_FRAME 1 -#define NGX_RTMP_VIDEO_INTER_FRAME 2 -#define NGX_RTMP_VIDEO_DISPOSABLE_FRAME 3 #define NGX_RTMP_AUDIO_FRAME NGX_RTMP_VIDEO_KEY_FRAME -static ngx_int_t -ngx_rtmp_get_video_frame_type(ngx_chain_t *in) -{ - return (in->buf->pos[0] & 0xf0) >> 4; -} - - static ngx_int_t ngx_rtmp_live_send_abs_message(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_chain_t *in) diff --git a/ngx_rtmp_record_module.c b/ngx_rtmp_record_module.c index cae7438..939e1ca 100644 --- a/ngx_rtmp_record_module.c +++ b/ngx_rtmp_record_module.c @@ -13,6 +13,9 @@ static ngx_rtmp_publish_pt next_publish; static ngx_rtmp_delete_stream_pt next_delete_stream; +static ngx_int_t ngx_rtmp_record_open(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_record_close(ngx_rtmp_session_t *s); + static char * ngx_rtmp_notify_on_record_done(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); @@ -23,28 +26,78 @@ static char * ngx_rtmp_record_merge_app_conf(ngx_conf_t *cf, typedef struct { - ngx_str_t root; + ngx_uint_t flags; + ngx_str_t path; size_t max_size; + size_t max_frames; + ngx_msec_t interval; ngx_url_t *url; } ngx_rtmp_record_app_conf_t; +typedef struct { + ngx_file_t file; + u_char path[NGX_MAX_PATH + 1]; + ngx_uint_t nframes; + uint32_t epoch; + ngx_time_t last; +} ngx_rtmp_record_ctx_t; + + +#define NGX_RTMP_RECORD_OFF 0x01 +#define NGX_RTMP_RECORD_AUDIO 0x02 +#define NGX_RTMP_RECORD_VIDEO 0x04 +#define NGX_RTMP_RECORD_KEYFRAMES 0x08 + + +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_null_string, 0 } +}; + + static ngx_command_t ngx_rtmp_record_commands[] = { { ngx_string("record"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_conf_set_bitmask_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, flags), + ngx_rtmp_record_mask }, + + { ngx_string("record_path"), NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, ngx_conf_set_str_slot, NGX_RTMP_APP_CONF_OFFSET, - offsetof(ngx_rtmp_record_app_conf_t, root), + offsetof(ngx_rtmp_record_app_conf_t, path), NULL }, - { ngx_string("record_size"), + { ngx_string("record_max_size"), NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, ngx_conf_set_size_slot, NGX_RTMP_APP_CONF_OFFSET, offsetof(ngx_rtmp_record_app_conf_t, max_size), NULL }, + { ngx_string("record_max_frames"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, max_frames), + NULL }, + + { ngx_string("record_interval"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, interval), + NULL }, + { ngx_string("on_record_done"), NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, ngx_rtmp_notify_on_record_done, @@ -84,15 +137,6 @@ ngx_module_t ngx_rtmp_record_module = { }; -typedef struct { - ngx_file_t file; - ngx_str_t path; - ngx_int_t counter; - ngx_time_t last; - uint32_t epoch; -} ngx_rtmp_record_ctx_t; - - static void * ngx_rtmp_record_create_app_conf(ngx_conf_t *cf) { @@ -104,6 +148,8 @@ ngx_rtmp_record_create_app_conf(ngx_conf_t *cf) } racf->max_size = NGX_CONF_UNSET; + racf->max_frames = NGX_CONF_UNSET; + racf->interval = NGX_CONF_UNSET; return racf; } @@ -115,9 +161,12 @@ ngx_rtmp_record_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) ngx_rtmp_record_app_conf_t *prev = parent; ngx_rtmp_record_app_conf_t *conf = child; - ngx_conf_merge_str_value(conf->root, prev->root, ""); - ngx_conf_merge_size_value(conf->max_size, prev->max_size, - (size_t)NGX_CONF_UNSET); + ngx_conf_merge_bitmask_value(conf->flags, prev->flags, + (NGX_CONF_BITMASK_SET|NGX_RTMP_RECORD_OFF)); + ngx_conf_merge_str_value(conf->path, prev->path, ""); + ngx_conf_merge_size_value(conf->max_size, prev->max_size, 0); + ngx_conf_merge_size_value(conf->max_frames, prev->max_frames, 0); + ngx_conf_merge_msec_value(conf->interval, prev->interval, 0); return NGX_CONF_OK; } @@ -148,17 +197,56 @@ ngx_rtmp_record_write_header(ngx_file_t *file) } +static ngx_int_t +ngx_rtmp_record_open(ngx_rtmp_session_t *s) +{ + ngx_rtmp_record_ctx_t *ctx; + ngx_err_t err; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + if (ctx == NULL || ctx->file.fd != NGX_INVALID_FILE) { + return NGX_ERROR; + } + + /* open file */ + ctx->nframes = 0; + ngx_memzero(&ctx->file, sizeof(ctx->file)); + ctx->file.offset = 0; + ctx->file.log = s->connection->log; + ctx->file.fd = ngx_open_file(ctx->path, NGX_FILE_WRONLY, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + if (ctx->file.fd == NGX_INVALID_FILE) { + err = ngx_errno; + if (err != NGX_ENOENT) { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, err, + "record: failed to open file" " \"%s\" failed", + ctx->path); + } + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: opened '%s'", ctx->path); + + if (ngx_rtmp_record_write_header(&ctx->file) != NGX_OK) { + ngx_rtmp_record_close(s); + return NGX_ERROR; + } + + return NGX_OK; +} + + static ngx_int_t ngx_rtmp_record_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) { ngx_rtmp_record_app_conf_t *racf; ngx_rtmp_record_ctx_t *ctx; - ngx_time_t *tod; - ngx_err_t err; + u_char *p, *l; racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module); - if (racf == NULL || racf->root.len == 0) { + if (racf == NULL || racf->flags & NGX_RTMP_RECORD_OFF) { goto next; } @@ -170,59 +258,31 @@ ngx_rtmp_record_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) if (ctx == NULL) { return NGX_ERROR; } + ctx->file.fd = NGX_INVALID_FILE; ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_record_module); } - if (ctx->path.len) { - ngx_log_error(NGX_LOG_INFO, s->connection->log, NGX_ERROR, - "record: already recording"); - return NGX_ERROR; - } - - /* create file name */ - tod = ngx_timeofday(); - ctx->path.data = ngx_pcalloc(s->connection->pool, NGX_MAX_PATH + 1); - if (ctx->path.data == NULL) { - return NGX_ERROR; - } - - if (tod->sec == ctx->last.sec - && tod->msec == ctx->last.msec) - { - ++ctx->counter; - } else { - ctx->counter = 0; - } - - ctx->last = *tod; - - /* TODO: can use 'name' here - * but need to check for bad symbols first - * it comes right from user */ - ctx->path.len = ngx_snprintf(ctx->path.data, NGX_MAX_PATH, - "%V/rec-%T.%M.%d.flv", &racf->root, tod->sec, tod->msec, - ctx->counter) - ctx->path.data; - ctx->path.data[ctx->path.len] = 0; - - /* open file */ - ngx_memzero(&ctx->file, sizeof(ctx->file)); - ctx->file.log = s->connection->log; - ctx->file.fd = ngx_open_file(ctx->path.data, NGX_FILE_WRONLY, - NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); - if (ctx->file.fd == NGX_INVALID_FILE) { - err = ngx_errno; - if (err != NGX_ENOENT) { - ngx_log_error(NGX_LOG_CRIT, s->connection->log, err, - "record: failed to open file" " \"%V\" failed", - ctx->path); - } + /* create file path */ + p = ctx->path; + l = ctx->path + sizeof(ctx->path) - 1; + p = ngx_cpymem(p, racf->path.data, + ngx_min(racf->path.len, (size_t)(l - p - 1))); + *p++ = '/'; + if (l - p <= 4) { return NGX_ERROR; } + p = (u_char *)ngx_escape_uri(p, v->name, ngx_min(ngx_strlen(v->name), + (size_t)(l - p - 4)), NGX_ESCAPE_URI_COMPONENT); + *p++ = '.'; + *p++ = 'f'; + *p++ = 'l'; + *p++ = 'v'; + *p = 0; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "record: opened '%V'", &ctx->path); + "record: path '%s'", ctx->path); - if (ngx_rtmp_record_write_header(&ctx->file) != NGX_OK) { + if (ngx_rtmp_record_open(s) != NGX_OK) { return NGX_ERROR; } @@ -235,16 +295,17 @@ static ngx_chain_t * ngx_rtmp_record_notify_create(ngx_rtmp_session_t *s, void *arg, ngx_pool_t *pool) { - ngx_str_t *path = arg; - ngx_rtmp_record_app_conf_t *racf; + ngx_rtmp_record_ctx_t *ctx; ngx_chain_t *hl, *cl, *pl; ngx_buf_t *b; + size_t len; racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module); - if (path == NULL) { - return NGX_OK; + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + if (ctx == NULL) { + return NULL; } /* common variables */ @@ -261,9 +322,11 @@ ngx_rtmp_record_notify_create(ngx_rtmp_session_t *s, void *arg, return NULL; } + len = ngx_strlen(ctx->path); + b = ngx_create_temp_buf(pool, sizeof("&call=record_done") + - sizeof("&path=") + path->len * 3); + sizeof("&path=") + len * 3); if (b == NULL) { return NULL; } @@ -274,7 +337,7 @@ ngx_rtmp_record_notify_create(ngx_rtmp_session_t *s, void *arg, sizeof("&call=record_done") - 1); b->last = ngx_cpymem(b->last, (u_char*)"&path=", sizeof("&path=") - 1); - b->last = (u_char*)ngx_escape_uri(b->last, path->data, path->len, 0); + b->last = (u_char*)ngx_escape_uri(b->last, ctx->path, len, 0); /* HTTP header */ hl = ngx_rtmp_netcall_http_format_header(racf->url, pool, @@ -294,7 +357,7 @@ ngx_rtmp_record_notify_create(ngx_rtmp_session_t *s, void *arg, static ngx_int_t -ngx_rtmp_record_notify(ngx_rtmp_session_t *s, ngx_str_t *path) +ngx_rtmp_record_notify(ngx_rtmp_session_t *s) { ngx_rtmp_record_app_conf_t *racf; ngx_rtmp_netcall_init_t ci; @@ -308,7 +371,6 @@ ngx_rtmp_record_notify(ngx_rtmp_session_t *s, ngx_str_t *path) ci.url = racf->url; ci.create = ngx_rtmp_record_notify_create; - ci.arg = path; return ngx_rtmp_netcall_create(s, &ci); } @@ -319,27 +381,25 @@ ngx_rtmp_record_close(ngx_rtmp_session_t *s) { ngx_rtmp_record_ctx_t *ctx; ngx_err_t err; - ngx_str_t path; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); - if (ctx == NULL || ctx->path.len == 0) { + if (ctx == NULL || ctx->file.fd == NGX_INVALID_FILE) { return NGX_OK; } - path = ctx->path; - ngx_str_null(&ctx->path); - if (ngx_close_file(ctx->file.fd) == NGX_FILE_ERROR) { err = ngx_errno; ngx_log_error(NGX_LOG_CRIT, s->connection->log, err, "record: error closing file"); } + ctx->file.fd = NGX_INVALID_FILE; + ctx->last = *ngx_cached_time; ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "record: closed"); - return ngx_rtmp_record_notify(s, &path); + return ngx_rtmp_record_notify(s); } @@ -364,12 +424,50 @@ ngx_rtmp_record_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_rtmp_record_app_conf_t *racf; u_char hdr[11], *p, *ph; uint32_t timestamp, tag_size; + ngx_time_t next; c = s->connection; - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); - if (racf == NULL || ctx == NULL || ctx->path.len == 0) { + if (racf == NULL || ctx == NULL || racf->flags & NGX_RTMP_RECORD_OFF) { + return NGX_OK; + } + + if (ctx->file.fd == NGX_INVALID_FILE && racf->interval + && (ctx->last.sec || ctx->last.msec)) + { + next = ctx->last; + next.msec += racf->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)) + { + if (ngx_rtmp_record_open(s) != NGX_OK) { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, 0, + "record: '%s' failed", ctx->path); + } + } + } + + if (ctx->file.fd == NGX_INVALID_FILE) { + return NGX_OK; + } + + /* filter frames */ + if (h->type == NGX_RTMP_MSG_AUDIO && + (racf->flags & NGX_RTMP_RECORD_AUDIO) == 0) + { + return NGX_OK; + } + + if (h->type == NGX_RTMP_MSG_VIDEO && + (racf->flags & NGX_RTMP_RECORD_VIDEO) == 0 && + ((racf->flags & NGX_RTMP_RECORD_KEYFRAMES) == 0 + || ngx_rtmp_get_video_frame_type(in) != NGX_RTMP_VIDEO_KEY_FRAME)) + { return NGX_OK; } @@ -441,12 +539,14 @@ ngx_rtmp_record_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, return NGX_ERROR; } + ++ctx->nframes; + /* watch max size */ - if (racf->max_size != (size_t)NGX_CONF_UNSET - && ctx->file.offset >= (ngx_int_t)racf->max_size) + if ((racf->max_size && ctx->file.offset >= (ngx_int_t)racf->max_size) + || (racf->max_frames && ctx->nframes >= racf->max_frames)) { ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "record: closed on size limit"); + "record: closed"); ngx_rtmp_record_close(s); }