From 840b62bf7152f06494eb5d5a7a580cbf0587a7bb Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 27 Sep 2012 14:24:51 +0400 Subject: [PATCH 01/37] initial implementation of control http module --- config | 2 + ngx_rtmp_control_module.c | 310 ++++++++++++++++++++++++++++++++++++++ ngx_rtmp_record_module.c | 22 +++ ngx_rtmp_record_module.h | 4 + 4 files changed, 338 insertions(+) create mode 100644 ngx_rtmp_control_module.c diff --git a/config b/config index 0714a51..b2d2114 100644 --- a/config +++ b/config @@ -22,6 +22,7 @@ CORE_MODULES="$CORE_MODULES HTTP_MODULES="$HTTP_MODULES \ ngx_rtmp_stat_module \ + ngx_rtmp_control_module \ " @@ -45,6 +46,7 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $ngx_addon_dir/ngx_rtmp_mp4_module.c \ $ngx_addon_dir/ngx_rtmp_netcall_module.c \ $ngx_addon_dir/ngx_rtmp_stat_module.c \ + $ngx_addon_dir/ngx_rtmp_control_module.c \ $ngx_addon_dir/ngx_rtmp_relay_module.c \ $ngx_addon_dir/ngx_rtmp_bandwidth.c \ $ngx_addon_dir/ngx_rtmp_exec_module.c \ diff --git a/ngx_rtmp_control_module.c b/ngx_rtmp_control_module.c new file mode 100644 index 0000000..5538942 --- /dev/null +++ b/ngx_rtmp_control_module.c @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2012 Roman Arutyunyan + */ + + +#include +#include + +#include "ngx_rtmp.h" +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_record_module.h" + + +static ngx_int_t ngx_rtmp_control_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_control_create_loc_conf(ngx_conf_t *cf); +static char * ngx_rtmp_control_merge_loc_conf(ngx_conf_t *cf, + void *parent, void *child); + + +#define NGX_RTMP_CONTROL_ALL 0xff +#define NGX_RTMP_CONTROL_RECORD 0x01 + +/* + * global: stat-{bufs-{total,free,used}, total bytes in/out, bw in/out} - cscf +*/ + + +typedef struct { + ngx_uint_t control; +} ngx_rtmp_control_loc_conf_t; + + +static ngx_conf_bitmask_t ngx_rtmp_control_masks[] = { + { ngx_string("all"), NGX_RTMP_CONTROL_ALL }, + { ngx_string("record"), NGX_RTMP_CONTROL_RECORD }, + { ngx_null_string, 0 } +}; + + +static ngx_command_t ngx_rtmp_control_commands[] = { + + { ngx_string("rtmp_control"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, + ngx_conf_set_bitmask_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_rtmp_control_loc_conf_t, control), + ngx_rtmp_control_masks }, + + ngx_null_command +}; + + +static ngx_http_module_t ngx_rtmp_control_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_control_postconfiguration, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_rtmp_control_create_loc_conf, /* create location configuration */ + ngx_rtmp_control_merge_loc_conf, /* merge location configuration */ +}; + + +ngx_module_t ngx_rtmp_control_module = { + NGX_MODULE_V1, + &ngx_rtmp_control_module_ctx, /* module context */ + ngx_rtmp_control_commands, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +/* /record arguments: + * srv - server index (optional) + * app - application name + * name - stream name + * rec - recorder name + */ + + +static ngx_int_t +ngx_rtmp_control_record(ngx_http_request_t *r) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_core_srv_conf_t **pcscf, *cscf; + ngx_rtmp_core_app_conf_t **pcacf, *cacf; + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_live_stream_t *ls; + ngx_rtmp_live_ctx_t *lctx; + ngx_rtmp_record_app_conf_t *racf; + ngx_rtmp_session_t *s; + ngx_uint_t sn, rn, n; + ngx_str_t srv, app, rec, name; + size_t len; + ngx_str_t msg; + ngx_chain_t cl; + ngx_buf_t *b; + + sn = 0; + if (ngx_http_arg(r, (u_char *) "srv", sizeof("srv") - 1, &srv) == NGX_OK) { + sn = ngx_atoi(srv.data, srv.len); + } + + if (ngx_http_arg(r, (u_char *) "app", sizeof("app") - 1, &app) != NGX_OK) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "rtmp_control: app not specified"); + ngx_str_set(&msg, "Application not specified"); + goto error; + } + + ngx_memzero(&rec, sizeof(rec)); + ngx_http_arg(r, (u_char *) "rec", sizeof("rec") - 1, &rec); + + ngx_memzero(&name, sizeof(name)); + ngx_http_arg(r, (u_char *) "name", sizeof("name") - 1, &name); + + cmcf = ngx_rtmp_core_main_conf; + if (cmcf == NULL) { + ngx_str_set(&msg, "Missing main RTMP conf"); + goto error; + } + + /* find server */ + if (sn >= cmcf->servers.nelts) { + ngx_str_set(&msg, "Server index out of range"); + goto error; + } + + pcscf = cmcf->servers.elts; + pcscf += sn; + cscf = *pcscf; + + /* find application */ + pcacf = cscf->applications.elts; + cacf = NULL; + + for (n = 0; n < cscf->applications.nelts; ++n, ++pcacf) { + if ((*pcacf)->name.len == app.len && + ngx_strncmp((*pcacf)->name.data, app.data, app.len) == 0) + { + cacf = *pcacf; + break; + } + } + + if (cacf == NULL) { + ngx_str_set(&msg, "Application not found"); + goto error; + } + + lacf = cacf->app_conf[ngx_rtmp_live_module.ctx_index]; + racf = cacf->app_conf[ngx_rtmp_record_module.ctx_index]; + + /* find live stream by name */ + for (ls = lacf->streams[ngx_hash_key(name.data, name.len)]; ls; + ls = ls->next) + { + len = ngx_strlen(ls->name); + + if (name.len == len && ngx_strncmp(name.data, ls->name, name.len) + == 0) + { + break; + } + } + + if (ls == NULL) { + ngx_str_set(&msg, "Live stream not found"); + goto error; + } + + /* find publisher context */ + for (lctx = ls->ctx; lctx; lctx = lctx->next) { + if (lctx->flags & NGX_RTMP_LIVE_PUBLISHING) { + break; + } + } + + if (lctx == NULL) { + ngx_str_set(&msg, "No publisher"); + goto error; + } + + s = lctx->session; + + /* find recorder */ + rn = ngx_rtmp_record_find(racf, &rec); + if (rn == NGX_CONF_UNSET_UINT) { + ngx_str_set(&msg, "Recorder not found"); + goto error; + } + + if (r->uri.len == sizeof("/record/start") - 1 && + ngx_strncmp(r->uri.data, "/record/start", r->uri.len) == 0) + { + ngx_rtmp_record_open(s, rn, NULL); + + } else if (r->uri.len == sizeof("/record/stop") - 1 && + ngx_strncmp(r->uri.data, "/record/stop", r->uri.len) == 0) + { + ngx_rtmp_record_close(s, rn, NULL); + + } else { + ngx_str_set(&msg, "Undefined subrequest"); + goto error; + } + + r->header_only = 1; + r->headers_out.status = NGX_HTTP_OK; + + return ngx_http_send_header(r); + +error: + r->headers_out.status = NGX_HTTP_INTERNAL_SERVER_ERROR; + r->headers_out.content_length_n = msg.len; + + b = ngx_calloc_buf(r->pool); + if (b == NULL) { + return NGX_ERROR; + } + + ngx_memzero(&cl, sizeof(cl)); + cl.buf = b; + + b->start = b->pos = msg.data; + b->end = b->last = msg.data + msg.len; + b->memory = 1; + b->last_buf = 1; + + ngx_http_send_header(r); + + return ngx_http_output_filter(r, &cl); +} + + +static ngx_int_t +ngx_rtmp_control_handler(ngx_http_request_t *r) +{ + ngx_rtmp_control_loc_conf_t *llcf; + + llcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_control_module); + if (llcf->control == 0) { + return NGX_DECLINED; + } + + if (llcf->control & NGX_RTMP_CONTROL_RECORD && + ngx_strncmp(r->uri.data, "/record/", sizeof("/record/") - 1) == 0) + { + return ngx_rtmp_control_record(r); + } + + return NGX_DECLINED; +} + + +static void * +ngx_rtmp_control_create_loc_conf(ngx_conf_t *cf) +{ + ngx_rtmp_control_loc_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_control_loc_conf_t)); + if (conf == NULL) { + return NULL; + } + + conf->control = 0; + + return conf; +} + + +static char * +ngx_rtmp_control_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_control_loc_conf_t *prev = parent; + ngx_rtmp_control_loc_conf_t *conf = child; + + ngx_conf_merge_bitmask_value(conf->control, prev->control, 0); + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_control_postconfiguration(ngx_conf_t *cf) +{ + ngx_http_handler_pt *h; + ngx_http_core_main_conf_t *cmcf; + + cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); + + h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + *h = ngx_rtmp_control_handler; + + return NGX_OK; +} diff --git a/ngx_rtmp_record_module.c b/ngx_rtmp_record_module.c index 419a302..141a09a 100644 --- a/ngx_rtmp_record_module.c +++ b/ngx_rtmp_record_module.c @@ -307,6 +307,28 @@ ngx_rtmp_record_close(ngx_rtmp_session_t *s, ngx_uint_t n, ngx_str_t *path) } +ngx_uint_t +ngx_rtmp_record_find(ngx_rtmp_record_app_conf_t *racf, ngx_str_t *id) +{ + ngx_rtmp_record_app_conf_t **pracf, *rracf; + ngx_uint_t n; + + pracf = racf->rec.elts; + + for (n = 0; n < racf->rec.nelts; ++n, ++pracf) { + rracf = *pracf; + + if (rracf->id.len == id->len && + ngx_strncmp(rracf->id.data, id->data, id->len) == 0) + { + return n; + } + } + + return NGX_CONF_UNSET_UINT; +} + + /* This funcion returns pointer to a static buffer */ static void ngx_rtmp_record_make_path(ngx_rtmp_session_t *s, diff --git a/ngx_rtmp_record_module.h b/ngx_rtmp_record_module.h index 6b7d417..ea79b7e 100644 --- a/ngx_rtmp_record_module.h +++ b/ngx_rtmp_record_module.h @@ -50,6 +50,10 @@ typedef struct { } ngx_rtmp_record_ctx_t; +ngx_uint_t ngx_rtmp_record_find(ngx_rtmp_record_app_conf_t *racf, + ngx_str_t *id); + + /* Manual recording control, * 'n' is record node index in config array. * Note: these functions allocate path in static buffer */ From 3d6d65f7c7df3a491e86a04615b9dd67f67f1608 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 27 Sep 2012 14:39:46 +0400 Subject: [PATCH 02/37] minor fixes in control module --- ngx_rtmp_control_module.c | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/ngx_rtmp_control_module.c b/ngx_rtmp_control_module.c index 5538942..4fa4913 100644 --- a/ngx_rtmp_control_module.c +++ b/ngx_rtmp_control_module.c @@ -20,10 +20,6 @@ static char * ngx_rtmp_control_merge_loc_conf(ngx_conf_t *cf, #define NGX_RTMP_CONTROL_ALL 0xff #define NGX_RTMP_CONTROL_RECORD 0x01 -/* - * global: stat-{bufs-{total,free,used}, total bytes in/out, bw in/out} - cscf -*/ - typedef struct { ngx_uint_t control; @@ -40,11 +36,11 @@ static ngx_conf_bitmask_t ngx_rtmp_control_masks[] = { static ngx_command_t ngx_rtmp_control_commands[] = { { ngx_string("rtmp_control"), - NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, - ngx_conf_set_bitmask_slot, - NGX_HTTP_LOC_CONF_OFFSET, - offsetof(ngx_rtmp_control_loc_conf_t, control), - ngx_rtmp_control_masks }, + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, + ngx_conf_set_bitmask_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_rtmp_control_loc_conf_t, control), + ngx_rtmp_control_masks }, ngx_null_command }; @@ -222,7 +218,7 @@ ngx_rtmp_control_record(ngx_http_request_t *r) return ngx_http_send_header(r); error: - r->headers_out.status = NGX_HTTP_INTERNAL_SERVER_ERROR; + r->headers_out.status = NGX_HTTP_BAD_REQUEST; r->headers_out.content_length_n = msg.len; b = ngx_calloc_buf(r->pool); From b70d37edb1274dcc5d28d4f2aa064169fb6e30c5 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 27 Sep 2012 21:05:52 +0400 Subject: [PATCH 03/37] improved manual recorder: keyframes, repeated open/close --- ngx_rtmp_record_module.c | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/ngx_rtmp_record_module.c b/ngx_rtmp_record_module.c index 141a09a..56042ce 100644 --- a/ngx_rtmp_record_module.c +++ b/ngx_rtmp_record_module.c @@ -27,7 +27,7 @@ static char * ngx_rtmp_record_merge_app_conf(ngx_conf_t *cf, void *parent, void *child); 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_rtmp_header_t *h, ngx_chain_t *in, ngx_int_t inc_nframes); static ngx_int_t ngx_rtmp_record_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_chain_t *in); static ngx_int_t ngx_rtmp_record_node_av(ngx_rtmp_session_t *s, @@ -168,8 +168,6 @@ ngx_rtmp_record_create_app_conf(ngx_conf_t *cf) racf->unique = NGX_CONF_UNSET; racf->url = NGX_CONF_UNSET_PTR; - ngx_str_set(&racf->id, "default"); - if (ngx_array_init(&racf->rec, cf->pool, 1, sizeof(void *)) != NGX_OK) { return NULL; } @@ -259,6 +257,7 @@ ngx_int_t ngx_rtmp_record_open(ngx_rtmp_session_t *s, ngx_uint_t n, ngx_str_t *path) { ngx_rtmp_record_rec_ctx_t *rctx; + ngx_int_t rc; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "record: #%ui manual open", n); @@ -269,8 +268,9 @@ ngx_rtmp_record_open(ngx_rtmp_session_t *s, ngx_uint_t n, ngx_str_t *path) return NGX_ERROR; } - if (ngx_rtmp_record_node_open(s, rctx) != NGX_OK) { - return NGX_ERROR; + rc = ngx_rtmp_record_node_open(s, rctx); + if (rc != NGX_OK) { + return rc; } if (path) { @@ -285,6 +285,7 @@ ngx_int_t ngx_rtmp_record_close(ngx_rtmp_session_t *s, ngx_uint_t n, ngx_str_t *path) { ngx_rtmp_record_rec_ctx_t *rctx; + ngx_int_t rc; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "record: #%ui manual close", n); @@ -295,8 +296,9 @@ ngx_rtmp_record_close(ngx_rtmp_session_t *s, ngx_uint_t n, ngx_str_t *path) return NGX_ERROR; } - if (ngx_rtmp_record_node_close(s, rctx) != NGX_OK) { - return NGX_ERROR; + rc = ngx_rtmp_record_node_close(s, rctx); + if (rc != NGX_OK) { + return rc; } if (path) { @@ -384,7 +386,7 @@ ngx_rtmp_record_node_open(ngx_rtmp_session_t *s, rracf = rctx->conf; if (rctx->file.fd != NGX_INVALID_FILE) { - return NGX_OK; + return NGX_AGAIN; } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, @@ -548,7 +550,7 @@ ngx_rtmp_record_node_close(ngx_rtmp_session_t *s, rracf = rctx->conf; if (rctx->file.fd == NGX_INVALID_FILE) { - return NGX_OK; + return NGX_AGAIN; } if (ngx_close_file(rctx->file.fd) == NGX_FILE_ERROR) { @@ -611,7 +613,8 @@ next: 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_rtmp_header_t *h, ngx_chain_t *in, + ngx_int_t inc_nframes) { u_char hdr[11], *p, *ph; uint32_t timestamp, tag_size; @@ -688,7 +691,7 @@ ngx_rtmp_record_write_frame(ngx_rtmp_session_t *s, return NGX_ERROR; } - ++rctx->nframes; + rctx->nframes += inc_nframes; /* watch max size */ if ((rracf->max_size && rctx->file.offset >= (ngx_int_t) rracf->max_size) || @@ -785,6 +788,12 @@ ngx_rtmp_record_node_av(ngx_rtmp_session_t *s, ngx_rtmp_record_rec_ctx_t *rctx, } } + if ((rracf->flags & NGX_RTMP_RECORD_MANUAL) && + !brkframe && rctx->nframes == 0) + { + return NGX_OK; + } + if (rctx->file.fd == NGX_INVALID_FILE) { return NGX_OK; } @@ -837,7 +846,8 @@ ngx_rtmp_record_node_av(ngx_rtmp_session_t *s, ngx_rtmp_record_rec_ctx_t *rctx, 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) + if (ngx_rtmp_record_write_frame(s, rctx, &ch, + codec_ctx->aac_header, 0) != NGX_OK) { return NGX_OK; @@ -855,7 +865,8 @@ ngx_rtmp_record_node_av(ngx_rtmp_session_t *s, ngx_rtmp_record_rec_ctx_t *rctx, 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) + if (ngx_rtmp_record_write_frame(s, rctx, &ch, + codec_ctx->avc_header, 0) != NGX_OK) { return NGX_OK; @@ -864,7 +875,7 @@ ngx_rtmp_record_node_av(ngx_rtmp_session_t *s, ngx_rtmp_record_rec_ctx_t *rctx, } } - return ngx_rtmp_record_write_frame(s, rctx, h, in); + return ngx_rtmp_record_write_frame(s, rctx, h, in, 1); } From 0cd7883a438a19f76d3d29d1f465516d1ec97bb1 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 27 Sep 2012 21:06:46 +0400 Subject: [PATCH 04/37] implemented control section/method parsers; added returning file path --- ngx_rtmp_control_module.c | 111 ++++++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 23 deletions(-) diff --git a/ngx_rtmp_control_module.c b/ngx_rtmp_control_module.c index 4fa4913..1346d81 100644 --- a/ngx_rtmp_control_module.c +++ b/ngx_rtmp_control_module.c @@ -86,22 +86,23 @@ ngx_module_t ngx_rtmp_control_module = { static ngx_int_t -ngx_rtmp_control_record(ngx_http_request_t *r) +ngx_rtmp_control_record(ngx_http_request_t *r, ngx_str_t *method) { + ngx_rtmp_record_app_conf_t *racf; ngx_rtmp_core_main_conf_t *cmcf; ngx_rtmp_core_srv_conf_t **pcscf, *cscf; ngx_rtmp_core_app_conf_t **pcacf, *cacf; ngx_rtmp_live_app_conf_t *lacf; ngx_rtmp_live_stream_t *ls; ngx_rtmp_live_ctx_t *lctx; - ngx_rtmp_record_app_conf_t *racf; ngx_rtmp_session_t *s; - ngx_uint_t sn, rn, n; - ngx_str_t srv, app, rec, name; - size_t len; - ngx_str_t msg; ngx_chain_t cl; + ngx_uint_t sn, rn, n; + ngx_str_t srv, app, rec, name, path; + ngx_str_t msg; ngx_buf_t *b; + ngx_int_t rc; + size_t len; sn = 0; if (ngx_http_arg(r, (u_char *) "srv", sizeof("srv") - 1, &srv) == NGX_OK) { @@ -159,8 +160,8 @@ ngx_rtmp_control_record(ngx_http_request_t *r) racf = cacf->app_conf[ngx_rtmp_record_module.ctx_index]; /* find live stream by name */ - for (ls = lacf->streams[ngx_hash_key(name.data, name.len)]; ls; - ls = ls->next) + for (ls = lacf->streams[ngx_hash_key(name.data, name.len) % lacf->nbuckets]; + ls; ls = ls->next) { len = ngx_strlen(ls->name); @@ -197,25 +198,53 @@ ngx_rtmp_control_record(ngx_http_request_t *r) goto error; } - if (r->uri.len == sizeof("/record/start") - 1 && - ngx_strncmp(r->uri.data, "/record/start", r->uri.len) == 0) - { - ngx_rtmp_record_open(s, rn, NULL); + ngx_memzero(&path, sizeof(path)); - } else if (r->uri.len == sizeof("/record/stop") - 1 && - ngx_strncmp(r->uri.data, "/record/stop", r->uri.len) == 0) + if (method->len == sizeof("start") - 1 && + ngx_strncmp(method->data, "start", method->len) == 0) { - ngx_rtmp_record_close(s, rn, NULL); + rc = ngx_rtmp_record_open(s, rn, &path); + + } else if (method->len == sizeof("stop") - 1 && + ngx_strncmp(method->data, "stop", method->len) == 0) + { + rc = ngx_rtmp_record_close(s, rn, &path); } else { - ngx_str_set(&msg, "Undefined subrequest"); + ngx_str_set(&msg, "Undefined method"); goto error; } - r->header_only = 1; - r->headers_out.status = NGX_HTTP_OK; + if (rc == NGX_ERROR) { + ngx_str_set(&msg, "Recorder error"); + goto error; + } - return ngx_http_send_header(r); + if (rc == NGX_AGAIN) { + /* already opened/closed */ + ngx_str_null(&path); + r->header_only = 1; + } + + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = path.len; + + b = ngx_create_temp_buf(r->pool, path.len); + if (b == NULL) { + return NGX_ERROR; + } + + ngx_memzero(&cl, sizeof(cl)); + cl.buf = b; + + b->last = ngx_cpymem(b->pos, path.data, path.len); + + b->memory = 1; + b->last_buf = 1; + + ngx_http_send_header(r); + + return ngx_http_output_filter(r, &cl); error: r->headers_out.status = NGX_HTTP_BAD_REQUEST; @@ -244,18 +273,54 @@ static ngx_int_t ngx_rtmp_control_handler(ngx_http_request_t *r) { ngx_rtmp_control_loc_conf_t *llcf; + ngx_str_t section, method; + u_char *p; + ngx_uint_t n; llcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_control_module); if (llcf->control == 0) { return NGX_DECLINED; } - if (llcf->control & NGX_RTMP_CONTROL_RECORD && - ngx_strncmp(r->uri.data, "/record/", sizeof("/record/") - 1) == 0) - { - return ngx_rtmp_control_record(r); + /* uri format: .../section/method?args */ + ngx_memzero(§ion, sizeof(section)); + ngx_memzero(&method, sizeof(method)); + + for (n = r->uri.len; n; --n) { + p = &r->uri.data[n - 1]; + + if (*p != '/') { + continue; + } + + if (method.data) { + section.data = p + 1; + section.len = method.data - section.data - 1; + break; + } + + method.data = p + 1; + method.len = r->uri.data + r->uri.len - method.data; } + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, r->connection->log, 0, + "rtmp_control: section='%V' method='%V'", + §ion, &method); + + +#define NGX_RTMP_CONTROL_SECTION(flag, secname) \ + if (llcf->control & NGX_RTMP_CONTROL_##flag && \ + section.len == sizeof(#secname) - 1 && \ + ngx_strncmp(section.data, #secname, sizeof(#secname) - 1) == 0) \ + { \ + return ngx_rtmp_control_##secname(r, &method); \ + } + + NGX_RTMP_CONTROL_SECTION(RECORD, record); + +#undef NGX_RTMP_CONTROL_SECTION + + return NGX_DECLINED; } From e630c913918b9a663a3416deaedb5795cd9dd978 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 27 Sep 2012 21:18:49 +0400 Subject: [PATCH 05/37] fixed formatting --- ngx_rtmp_record_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_rtmp_record_module.c b/ngx_rtmp_record_module.c index 56042ce..bb3eff9 100644 --- a/ngx_rtmp_record_module.c +++ b/ngx_rtmp_record_module.c @@ -791,7 +791,7 @@ ngx_rtmp_record_node_av(ngx_rtmp_session_t *s, ngx_rtmp_record_rec_ctx_t *rctx, if ((rracf->flags & NGX_RTMP_RECORD_MANUAL) && !brkframe && rctx->nframes == 0) { - return NGX_OK; + return NGX_OK; } if (rctx->file.fd == NGX_INVALID_FILE) { From 0b757e8a49480a88f5e051f6590f25e6c4de3288 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 27 Sep 2012 21:25:35 +0400 Subject: [PATCH 06/37] fixed http output --- ngx_rtmp_control_module.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/ngx_rtmp_control_module.c b/ngx_rtmp_control_module.c index 1346d81..227b677 100644 --- a/ngx_rtmp_control_module.c +++ b/ngx_rtmp_control_module.c @@ -238,8 +238,6 @@ ngx_rtmp_control_record(ngx_http_request_t *r, ngx_str_t *method) cl.buf = b; b->last = ngx_cpymem(b->pos, path.data, path.len); - - b->memory = 1; b->last_buf = 1; ngx_http_send_header(r); From 87686029aa2cfddfea765c626be7161981583482 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Sun, 30 Sep 2012 01:42:02 +0400 Subject: [PATCH 07/37] fixed exec terminate crash; added closing io descriptors in child prior to exec --- ngx_rtmp_enotify_module.c | 14 +++++++++++++- ngx_rtmp_exec_module.c | 35 ++++++++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/ngx_rtmp_enotify_module.c b/ngx_rtmp_enotify_module.c index 2023a85..0e7be8a 100644 --- a/ngx_rtmp_enotify_module.c +++ b/ngx_rtmp_enotify_module.c @@ -243,7 +243,7 @@ static ngx_int_t ngx_rtmp_enotify_exec(ngx_rtmp_session_t *s, ngx_rtmp_enotify_conf_t *ec) { #ifndef NGX_WIN32 - int pid; + int pid, fd; ngx_str_t a, *arg; char **args; ngx_uint_t n; @@ -258,6 +258,18 @@ ngx_rtmp_enotify_exec(ngx_rtmp_session_t *s, ngx_rtmp_enotify_conf_t *ec) case 0: /* child */ + fd = open("/dev/null", O_RDWR); + + if (fd != -1) { + close(0); + close(1); + close(2); + + dup(fd); + dup(fd); + dup(fd); + } + args = ngx_palloc(s->connection->pool, (ec->args.nelts + 2) * sizeof(char *)); if (args == NULL) { diff --git a/ngx_rtmp_exec_module.c b/ngx_rtmp_exec_module.c index 1069256..02f7781 100644 --- a/ngx_rtmp_exec_module.c +++ b/ngx_rtmp_exec_module.c @@ -254,15 +254,22 @@ ngx_rtmp_exec_child_dead(ngx_event_t *ev) static ngx_int_t ngx_rtmp_exec_kill(ngx_rtmp_session_t *s, ngx_rtmp_exec_t *e, ngx_int_t term) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "exec: terminating child %ui", - (ngx_int_t)e->pid); - if (e->respawn_evt.timer_set) { ngx_del_timer(&e->respawn_evt); } - ngx_del_event(&e->read_evt, NGX_READ_EVENT, 0); + if (e->read_evt.active) { + ngx_del_event(&e->read_evt, NGX_READ_EVENT, 0); + } + + if (e->active == 0) { + return NGX_OK; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "exec: terminating child %ui", + (ngx_int_t)e->pid); + e->active = 0; close(e->pipefd); @@ -288,7 +295,7 @@ ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n) #ifndef NGX_WIN32 ngx_rtmp_exec_app_conf_t *eacf; ngx_rtmp_exec_ctx_t *ctx; - int pid; + int pid, fd; int pipefd[2]; int ret; ngx_rtmp_exec_conf_t *ec; @@ -337,6 +344,18 @@ ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n) case 0: /* child */ + fd = open("/dev/null", O_RDWR); + + if (fd != -1) { + close(0); + close(1); + close(2); + + dup(fd); + dup(fd); + dup(fd); + } + args = ngx_palloc(s->connection->pool, (ec->args.nelts + 2) * sizeof(char *)); if (args == NULL) { @@ -411,9 +430,7 @@ ngx_rtmp_exec_delete_stream(ngx_rtmp_session_t *s, ngx_rtmp_delete_stream_t *v) e = ctx->execs; for (n = 0; n < eacf->execs.nelts; ++n, ++e) { - if (e->active) { - ngx_rtmp_exec_kill(s, e, 1); - } + ngx_rtmp_exec_kill(s, e, 1); } next: From 01d3b8857c0033872a2b530ac9c4d52071c6eba2 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 1 Oct 2012 14:37:30 +0400 Subject: [PATCH 08/37] changed capture codecs to h264/speex --- test/rtmp-publisher/RtmpPublisher.mxml | 9 +++++++++ test/rtmp-publisher/RtmpPublisher.swf | Bin 46156 -> 46375 bytes 2 files changed, 9 insertions(+) diff --git a/test/rtmp-publisher/RtmpPublisher.mxml b/test/rtmp-publisher/RtmpPublisher.mxml index 6d4f209..f07fc24 100644 --- a/test/rtmp-publisher/RtmpPublisher.mxml +++ b/test/rtmp-publisher/RtmpPublisher.mxml @@ -18,6 +18,7 @@ private var microphone:Microphone; private var connection:NetConnection; private var stream:NetStream; + private var h264Settings:H264VideoStreamSettings; private function toggleFeedListener(event:MouseEvent):void { if(toggleFeed.label == 'Start Feed') { @@ -69,6 +70,9 @@ stream = new NetStream(connection); stream.attachCamera(camera); stream.attachAudio(microphone); + h264Settings = new H264VideoStreamSettings(); + h264Settings.setProfileLevel(H264Profile.BASELINE, H264Level.LEVEL_1_2); + stream.videoStreamSettings = h264Settings; break; } } @@ -88,6 +92,11 @@ camera = Camera.getCamera(); microphone = Microphone.getMicrophone(); + microphone.setSilenceLevel(0); + microphone.codec = "Speex"; + microphone.encodeQuality = 6; + camera.setMode(704, 576, 25); + camera.setQuality(131072, 70); connection = new NetConnection(); connection.connect(streamer); connection.addEventListener(NetStatusEvent.NET_STATUS, netStatusHander); diff --git a/test/rtmp-publisher/RtmpPublisher.swf b/test/rtmp-publisher/RtmpPublisher.swf index a3396cf81482ad8f1fdc42a8ae1cbdc8b942cf3f..cfa6f8533427b9d20dd0750f4e4ba39c06ca922a 100644 GIT binary patch literal 46375 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

>C`3-ILvcz@8SD5_l%b9m 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 LoErUCc@$lKCKG|7 literal 46156 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 M$Rp;dYY@it&d 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�$pCd-Oh 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 A=Kufz From ce7ae837b3d988d9617ed56edf169af0a21d47ec Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 3 Oct 2012 09:33:43 +0400 Subject: [PATCH 09/37] fixed compilation (dup result) --- ngx_rtmp_enotify_module.c | 12 +++--------- ngx_rtmp_exec_module.c | 12 +++--------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/ngx_rtmp_enotify_module.c b/ngx_rtmp_enotify_module.c index 0e7be8a..a81b8e2 100644 --- a/ngx_rtmp_enotify_module.c +++ b/ngx_rtmp_enotify_module.c @@ -260,15 +260,9 @@ ngx_rtmp_enotify_exec(ngx_rtmp_session_t *s, ngx_rtmp_enotify_conf_t *ec) /* child */ fd = open("/dev/null", O_RDWR); - if (fd != -1) { - close(0); - close(1); - close(2); - - dup(fd); - dup(fd); - dup(fd); - } + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); args = ngx_palloc(s->connection->pool, (ec->args.nelts + 2) * sizeof(char *)); diff --git a/ngx_rtmp_exec_module.c b/ngx_rtmp_exec_module.c index 02f7781..6e9a405 100644 --- a/ngx_rtmp_exec_module.c +++ b/ngx_rtmp_exec_module.c @@ -346,15 +346,9 @@ ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n) /* child */ fd = open("/dev/null", O_RDWR); - if (fd != -1) { - close(0); - close(1); - close(2); - - dup(fd); - dup(fd); - dup(fd); - } + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); args = ngx_palloc(s->connection->pool, (ec->args.nelts + 2) * sizeof(char *)); From 83aa7b9e89926b2875268c22d505a5b928ac140e Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 4 Oct 2012 09:26:00 +0400 Subject: [PATCH 10/37] fixed mp4 b-frame delay --- ngx_rtmp_mp4_module.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 92b35bb..b39db1d 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -1931,7 +1931,7 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) ngx_rtmp_mp4_track_t *t; ngx_rtmp_mp4_cursor_t *cr; uint32_t buflen, end_timestamp, sched, - timestamp, last_timestamp; + timestamp, last_timestamp, rdelay; ssize_t ret; u_char fhdr[5]; size_t fhdr_size; @@ -2059,10 +2059,13 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) if (t->header) { fhdr_size = 5; + + rdelay = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->delay); + ngx_rtmp_mp4_buffer[1] = 1; - ngx_rtmp_mp4_buffer[2] = cr->delay & 0xf00; - ngx_rtmp_mp4_buffer[3] = cr->delay & 0x0f0; - ngx_rtmp_mp4_buffer[4] = cr->delay & 0x00f; + ngx_rtmp_mp4_buffer[2] = (rdelay >> 16) & 0xff; + ngx_rtmp_mp4_buffer[3] = (rdelay >> 8) & 0xff; + ngx_rtmp_mp4_buffer[4] = rdelay & 0xff; } } else { /* NGX_RTMP_MSG_AUDIO */ From cc632eb6b683eacdced09bfbe905e510779d5efc Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 4 Oct 2012 11:25:51 +0400 Subject: [PATCH 11/37] refactored & restyled access module; implemented access inheritance from higher config levels --- ngx_rtmp_access_module.c | 142 ++++++++++++++++++++++++--------------- 1 file changed, 87 insertions(+), 55 deletions(-) diff --git a/ngx_rtmp_access_module.c b/ngx_rtmp_access_module.c index 5d939ef..ae9a58b 100644 --- a/ngx_rtmp_access_module.c +++ b/ngx_rtmp_access_module.c @@ -46,9 +46,9 @@ typedef struct { typedef struct { - ngx_array_t *rules; /* array of ngx_rtmp_access_rule_t */ + ngx_array_t rules; /* array of ngx_rtmp_access_rule_t */ #if (NGX_HAVE_INET6) - ngx_array_t *rules6; /* array of ngx_rtmp_access_rule6_t */ + ngx_array_t rules6; /* array of ngx_rtmp_access_rule6_t */ #endif } ngx_rtmp_access_app_conf_t; @@ -111,13 +111,67 @@ ngx_rtmp_access_create_app_conf(ngx_conf_t *cf) return NULL; } + if (ngx_array_init(&aacf->rules, cf->pool, 1, + sizeof(ngx_rtmp_access_rule_t)) + != NGX_OK) + { + return NULL; + } + +#if (NGX_HAVE_INET6) + if (ngx_array_init(&aacf->rules6, cf->pool, 1, + sizeof(ngx_rtmp_access_rule6_t)) + != NGX_OK) + { + return NULL; + } +#endif + return aacf; } +static ngx_int_t +ngx_rtmp_access_merge_rules(ngx_array_t *prev, ngx_array_t *rules) +{ + void *p; + + if (prev->nelts == 0) { + return NGX_OK; + } + + if (rules->nelts == 0) { + *rules = *prev; + return NGX_OK; + } + + p = ngx_array_push_n(rules, prev->nelts); + if (p == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(p, prev->elts, prev->size * prev->nelts); + + return NGX_OK; +} + + static char * ngx_rtmp_access_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) { + ngx_rtmp_access_app_conf_t *prev = parent; + ngx_rtmp_access_app_conf_t *conf = child; + + if (ngx_rtmp_access_merge_rules(&prev->rules, &conf->rules) != NGX_OK) { + return NGX_CONF_ERROR; + } + +#if (NGX_HAVE_INET6) + if (ngx_rtmp_access_merge_rules(&prev->rules6, &conf->rules6) != NGX_OK) { + return NGX_CONF_ERROR; + } +#endif + return NGX_CONF_OK; } @@ -127,7 +181,7 @@ ngx_rtmp_access_found(ngx_rtmp_session_t *s, ngx_uint_t deny) { if (deny) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "access forbidden by rule"); + "access forbidden by rule"); return NGX_ERROR; } @@ -136,23 +190,22 @@ ngx_rtmp_access_found(ngx_rtmp_session_t *s, ngx_uint_t deny) static ngx_int_t -ngx_rtmp_access_inet(ngx_rtmp_session_t *s, - ngx_rtmp_access_app_conf_t *ascf, - in_addr_t addr, ngx_uint_t flag) +ngx_rtmp_access_inet(ngx_rtmp_session_t *s, in_addr_t addr, ngx_uint_t flag) { ngx_uint_t i; ngx_rtmp_access_rule_t *rule; + ngx_rtmp_access_app_conf_t *ascf; - rule = ascf->rules->elts; - for (i = 0; i < ascf->rules->nelts; i++) { + ascf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_access_module); + + rule = ascf->rules.elts; + for (i = 0; i < ascf->rules.nelts; i++) { ngx_log_debug3(NGX_LOG_DEBUG_HTTP, s->connection->log, 0, "access: %08XD %08XD %08XD", addr, rule[i].mask, rule[i].addr); - if ((addr & rule[i].mask) == rule[i].addr - && flag & rule[i].flags) - { + if ((addr & rule[i].mask) == rule[i].addr && (flag & rule[i].flags)) { return ngx_rtmp_access_found(s, rule[i].deny); } } @@ -164,16 +217,17 @@ ngx_rtmp_access_inet(ngx_rtmp_session_t *s, #if (NGX_HAVE_INET6) static ngx_int_t -ngx_rtmp_access_inet6(ngx_rtmp_session_t *s, - ngx_rtmp_access_app_conf_t *ascf, - u_char *p, ngx_uint_t flag) +ngx_rtmp_access_inet6(ngx_rtmp_session_t *s, u_char *p, ngx_uint_t flag) { ngx_uint_t n; ngx_uint_t i; ngx_rtmp_access_rule6_t *rule6; + ngx_rtmp_access_app_conf_t *ascf; - rule6 = ascf->rules6->elts; - for (i = 0; i < ascf->rules6->nelts; i++) { + ascf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_access_module); + + rule6 = ascf->rules6.elts; + for (i = 0; i < ascf->rules6.nelts; i++) { #if (NGX_DEBUG) { @@ -223,10 +277,9 @@ ngx_rtmp_access(ngx_rtmp_session_t *s, ngx_uint_t flag) #endif ascf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_access_module); - if (ascf == NULL) { ngx_log_debug0(NGX_LOG_DEBUG_HTTP, s->connection->log, 0, - "access: NULL app conf"); + "access: NULL app conf"); return NGX_ERROR; } @@ -238,12 +291,8 @@ ngx_rtmp_access(ngx_rtmp_session_t *s, ngx_uint_t flag) switch (s->connection->sockaddr->sa_family) { case AF_INET: - if (ascf->rules) { - sin = (struct sockaddr_in *) s->connection->sockaddr; - return ngx_rtmp_access_inet(s, ascf, - sin->sin_addr.s_addr, flag); - } - break; + sin = (struct sockaddr_in *) s->connection->sockaddr; + return ngx_rtmp_access_inet(s, sin->sin_addr.s_addr, flag); #if (NGX_HAVE_INET6) @@ -251,23 +300,20 @@ ngx_rtmp_access(ngx_rtmp_session_t *s, ngx_uint_t flag) sin6 = (struct sockaddr_in6 *) s->connection->sockaddr; p = sin6->sin6_addr.s6_addr; - if (ascf->rules && IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) { - addr = p[12] << 24; + if (IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) { + addr = p[12] << 24; addr += p[13] << 16; addr += p[14] << 8; addr += p[15]; - return ngx_rtmp_access_inet(s, ascf, htonl(addr), flag); + return ngx_rtmp_access_inet(s, htonl(addr), flag); } - if (ascf->rules6) { - return ngx_rtmp_access_inet6(s, ascf, p, flag); - } + return ngx_rtmp_access_inet6(s, p, flag); #endif } return NGX_OK; - } @@ -295,22 +341,23 @@ ngx_rtmp_access_rule(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) flags = 0; if (cf->args->nelts == 2) { + flags = NGX_RTMP_ACCESS_PUBLISH | NGX_RTMP_ACCESS_PLAY; } else { for(; n < cf->args->nelts - 1; ++n) { - if (value[n].len == sizeof("publish") - 1 - && ngx_strcmp(value[1].data, "publish") == 0) + if (value[n].len == sizeof("publish") - 1 && + ngx_strcmp(value[1].data, "publish") == 0) { flags |= NGX_RTMP_ACCESS_PUBLISH; continue; } - if (value[n].len == sizeof("play") - 1 - && ngx_strcmp(value[1].data, "play") == 0) + if (value[n].len == sizeof("play") - 1 && + ngx_strcmp(value[1].data, "play") == 0) { flags |= NGX_RTMP_ACCESS_PLAY; continue; @@ -318,7 +365,7 @@ ngx_rtmp_access_rule(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) } ngx_log_error(NGX_LOG_ERR, cf->log, 0, - "unexpected access specified: '%V'", &value[n]); + "unexpected access specified: '%V'", &value[n]); return NGX_CONF_ERROR; } } @@ -331,13 +378,14 @@ ngx_rtmp_access_rule(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) if (rc == NGX_ERROR) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, - "invalid parameter \"%V\"", &value[1]); + "invalid parameter \"%V\"", &value[1]); return NGX_CONF_ERROR; } if (rc == NGX_DONE) { ngx_conf_log_error(NGX_LOG_WARN, cf, 0, - "low address bits of %V are meaningless", &value[1]); + "low address bits of %V are meaningless", + &value[1]); } } @@ -347,14 +395,6 @@ ngx_rtmp_access_rule(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) case AF_INET6: case 0: /* all */ - if (ascf->rules6 == NULL) { - ascf->rules6 = ngx_array_create(cf->pool, 4, - sizeof(ngx_rtmp_access_rule6_t)); - if (ascf->rules6 == NULL) { - return NGX_CONF_ERROR; - } - } - rule6 = ngx_array_push(ascf->rules6); if (rule6 == NULL) { return NGX_CONF_ERROR; @@ -374,15 +414,7 @@ ngx_rtmp_access_rule(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) default: /* AF_INET */ - if (ascf->rules == NULL) { - ascf->rules = ngx_array_create(cf->pool, 4, - sizeof(ngx_rtmp_access_rule_t)); - if (ascf->rules == NULL) { - return NGX_CONF_ERROR; - } - } - - rule = ngx_array_push(ascf->rules); + rule = ngx_array_push(&ascf->rules); if (rule == NULL) { return NGX_CONF_ERROR; } From 6e55e3a77fe2dab1fcf6f2e4a9c4b1e616738d42 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 4 Oct 2012 14:00:18 +0400 Subject: [PATCH 12/37] fixed compilation --- ngx_rtmp_access_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_rtmp_access_module.c b/ngx_rtmp_access_module.c index ae9a58b..a229aa2 100644 --- a/ngx_rtmp_access_module.c +++ b/ngx_rtmp_access_module.c @@ -395,7 +395,7 @@ ngx_rtmp_access_rule(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) case AF_INET6: case 0: /* all */ - rule6 = ngx_array_push(ascf->rules6); + rule6 = ngx_array_push(&ascf->rules6); if (rule6 == NULL) { return NGX_CONF_ERROR; } From 0740b046d6e45e93eb8d0ae29fdb475ece4d3252 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 4 Oct 2012 22:18:51 +0400 Subject: [PATCH 13/37] recorder now stops at closeStream instead of deleteStream --- ngx_rtmp_record_module.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ngx_rtmp_record_module.c b/ngx_rtmp_record_module.c index 419a302..6147731 100644 --- a/ngx_rtmp_record_module.c +++ b/ngx_rtmp_record_module.c @@ -16,7 +16,7 @@ ngx_rtmp_record_done_pt ngx_rtmp_record_done; static ngx_rtmp_publish_pt next_publish; -static ngx_rtmp_delete_stream_pt next_delete_stream; +static ngx_rtmp_close_stream_pt next_close_stream; static char *ngx_rtmp_record_recorder(ngx_conf_t *cf, ngx_command_t *cmd, @@ -558,8 +558,8 @@ ngx_rtmp_record_node_close(ngx_rtmp_session_t *s, static ngx_int_t -ngx_rtmp_record_delete_stream(ngx_rtmp_session_t *s, - ngx_rtmp_delete_stream_t *v) +ngx_rtmp_record_close_stream(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v) { ngx_rtmp_record_ctx_t *ctx; ngx_rtmp_record_rec_ctx_t *rctx; @@ -572,7 +572,7 @@ ngx_rtmp_record_delete_stream(ngx_rtmp_session_t *s, } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "record: delete_stream %ui nodes", + "record: close_stream %ui nodes", ctx->rec.nelts); rctx = ctx->rec.elts; @@ -582,7 +582,7 @@ ngx_rtmp_record_delete_stream(ngx_rtmp_session_t *s, } next: - return next_delete_stream(s, v); + return next_close_stream(s, v); } @@ -953,8 +953,8 @@ ngx_rtmp_record_postconfiguration(ngx_conf_t *cf) next_publish = ngx_rtmp_publish; ngx_rtmp_publish = ngx_rtmp_record_publish; - next_delete_stream = ngx_rtmp_delete_stream; - ngx_rtmp_delete_stream = ngx_rtmp_record_delete_stream; + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_record_close_stream; return NGX_OK; } From fb824371e7bfb8394b30b094bcb9121d3125fba3 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 5 Oct 2012 10:11:30 +0400 Subject: [PATCH 14/37] fixed vod stopping: added NetStream.Play.Complete meta message --- ngx_rtmp.h | 2 ++ ngx_rtmp_play_module.c | 1 + ngx_rtmp_send.c | 57 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/ngx_rtmp.h b/ngx_rtmp.h index ab598c4..aa7432a 100644 --- a/ngx_rtmp.h +++ b/ngx_rtmp.h @@ -501,6 +501,8 @@ ngx_int_t ngx_rtmp_receive_amf(ngx_rtmp_session_t *s, ngx_chain_t *in, /* AMF status sender */ ngx_int_t ngx_rtmp_send_status(ngx_rtmp_session_t *s, char *code, char* level, char *desc); +ngx_int_t ngx_rtmp_send_play_status(ngx_rtmp_session_t *s, char *code, + char* level, ngx_uint_t duration, ngx_uint_t bytes); /* Frame types */ diff --git a/ngx_rtmp_play_module.c b/ngx_rtmp_play_module.c index 5c0315a..3d1ed91 100644 --- a/ngx_rtmp_play_module.c +++ b/ngx_rtmp_play_module.c @@ -161,6 +161,7 @@ ngx_rtmp_play_send(ngx_event_t *e) ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID); + ngx_rtmp_send_play_status(s, "NetStream.Play.Complete", "status", 0, 0); ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stopped"); } diff --git a/ngx_rtmp_send.c b/ngx_rtmp_send.c index eab33bd..89d77b0 100644 --- a/ngx_rtmp_send.c +++ b/ngx_rtmp_send.c @@ -346,3 +346,60 @@ ngx_rtmp_send_status(ngx_rtmp_session_t *s, char *code, char* level, char *desc) return ngx_rtmp_send_amf(s, &h, out_elts, sizeof(out_elts) / sizeof(out_elts[0])); } + + +ngx_int_t +ngx_rtmp_send_play_status(ngx_rtmp_session_t *s, char *code, char* level, + ngx_uint_t duration, ngx_uint_t bytes) +{ + ngx_rtmp_header_t h; + static double dduration; + static double dbytes; + + static ngx_rtmp_amf_elt_t out_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("code"), + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + NULL, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("duration"), + &dduration, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("bytes"), + &dbytes, 0 }, + }; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "onPlayStatus", 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + out_inf, + sizeof(out_inf) }, + }; + + + out_inf[0].data = code; + out_inf[1].data = level; + + dduration = duration; + dbytes = bytes; + + memset(&h, 0, sizeof(h)); + + h.type = NGX_RTMP_MSG_AMF_META; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; + + return ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} From 2ef9075f2b2fb4a782f330e8cb194e5714c509be Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 5 Oct 2012 15:36:05 +0400 Subject: [PATCH 15/37] refactored seek & pause in vod --- ngx_rtmp_cmd_module.c | 118 ++++------------------------------------- ngx_rtmp_flv_module.c | 36 ++++++++++--- ngx_rtmp_mp4_module.c | 96 ++++++++++++++++++++++++++++----- ngx_rtmp_play_module.c | 96 +++++++++++++++++++++++---------- ngx_rtmp_play_module.h | 6 ++- ngx_rtmp_send.c | 1 + 6 files changed, 195 insertions(+), 158 deletions(-) diff --git a/ngx_rtmp_cmd_module.c b/ngx_rtmp_cmd_module.c index af63ed1..39cbe1f 100644 --- a/ngx_rtmp_cmd_module.c +++ b/ngx_rtmp_cmd_module.c @@ -1003,71 +1003,17 @@ ngx_rtmp_cmd_pause_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, static ngx_int_t ngx_rtmp_cmd_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) { - ngx_rtmp_header_t h; - - static double trans; - - static ngx_rtmp_amf_elt_t out_inf[] = { - - { NGX_RTMP_AMF_STRING, - ngx_string("code"), - "NetStream.Pause.Notify", 0 }, - - { NGX_RTMP_AMF_STRING, - ngx_string("level"), - "status", 0 }, - - { NGX_RTMP_AMF_STRING, - ngx_string("description"), - "Paused.", 0 }, - }; - - static ngx_rtmp_amf_elt_t out_elts[] = { - - { NGX_RTMP_AMF_STRING, - ngx_null_string, - "onStatus", 0 }, - - { NGX_RTMP_AMF_NUMBER, - ngx_null_string, - &trans, 0 }, - - { NGX_RTMP_AMF_NULL, - ngx_null_string, - NULL, 0 }, - - { NGX_RTMP_AMF_OBJECT, - ngx_null_string, - out_inf, sizeof(out_inf) }, - }; - ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, - "pause: state='%i' position=%i", - v->pause, (ngx_int_t) v->position); - - /* send onStatus reply */ - ngx_memzero(&h, sizeof(h)); - h.type = NGX_RTMP_MSG_AMF_CMD; - h.csid = NGX_RTMP_CMD_CSID_AMF; - h.msid = NGX_RTMP_CMD_MSID; + "pause: state='%i' position=%i", + v->pause, (ngx_int_t) v->position); if (v->pause) { - out_inf[0].data = "NetStream.Pause.Notify"; - out_inf[2].data = "Paused."; - return ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_CMD_MSID) != NGX_OK - || ngx_rtmp_send_amf(s, &h, out_elts, - sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK - ? NGX_ERROR - : NGX_OK; + return ngx_rtmp_send_status(s, "NetStream.Pause.Notify", "status", + "Paused"); + } else { + return ngx_rtmp_send_status(s, "NetStream.Unpause.Notify", "status", + "Unpaused"); } - - out_inf[0].data = "NetStream.Unpause.Notify"; - out_inf[2].data = "Unpaused."; - return ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_CMD_MSID) != NGX_OK - || ngx_rtmp_send_amf(s, &h, out_elts, - sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK - ? NGX_ERROR - : NGX_OK; } @@ -1124,57 +1070,13 @@ ngx_rtmp_cmd_seek_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, static ngx_int_t ngx_rtmp_cmd_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) { - ngx_rtmp_header_t h; - - static double trans; - - static ngx_rtmp_amf_elt_t out_inf[] = { - - { NGX_RTMP_AMF_STRING, - ngx_string("code"), - "NetStream.Play.Reset", 0 }, - - { NGX_RTMP_AMF_STRING, - ngx_string("level"), - "status", 0 }, - - { NGX_RTMP_AMF_STRING, - ngx_string("description"), - "Paused.", 0 }, - }; - - static ngx_rtmp_amf_elt_t out_elts[] = { - - { NGX_RTMP_AMF_STRING, - ngx_null_string, - "onStatus", 0 }, - - { NGX_RTMP_AMF_NUMBER, - ngx_null_string, - &trans, 0 }, - - { NGX_RTMP_AMF_NULL, - ngx_null_string, - NULL, 0 }, - - { NGX_RTMP_AMF_OBJECT, - ngx_null_string, - out_inf, sizeof(out_inf) }, - }; - ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, - "seek: offset=%i", (ngx_int_t) v->offset); - - /* send onStatus reply */ - ngx_memzero(&h, sizeof(h)); - h.type = NGX_RTMP_MSG_AMF_CMD; - h.csid = NGX_RTMP_CMD_CSID_AMF; - h.msid = NGX_RTMP_CMD_MSID; + "seek: offset=%i", (ngx_int_t) v->offset); return (ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_CMD_MSID) != NGX_OK || ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_CMD_MSID) != NGX_OK - || ngx_rtmp_send_amf(s, &h, out_elts, - sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK) + || ngx_rtmp_send_status(s, "NetStream.Seek.Notify", "status", + "Seeking") ? NGX_ERROR : NGX_OK; } diff --git a/ngx_rtmp_flv_module.c b/ngx_rtmp_flv_module.c index 9e8163e..3d751c3 100644 --- a/ngx_rtmp_flv_module.c +++ b/ngx_rtmp_flv_module.c @@ -13,10 +13,12 @@ static void ngx_rtmp_flv_read_meta(ngx_rtmp_session_t *s, ngx_file_t *f); static ngx_int_t ngx_rtmp_flv_timestamp_to_offset(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_int_t timestamp); static ngx_int_t ngx_rtmp_flv_init(ngx_rtmp_session_t *s, ngx_file_t *f); -static ngx_int_t ngx_rtmp_flv_start(ngx_rtmp_session_t *s, ngx_file_t *f, +static ngx_int_t ngx_rtmp_flv_start(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_flv_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t offset); static ngx_int_t ngx_rtmp_flv_stop(ngx_rtmp_session_t *s, ngx_file_t *f); -static ngx_int_t ngx_rtmp_flv_send(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_flv_send(ngx_rtmp_session_t *s, ngx_file_t *f, + ngx_uint_t *ts); typedef struct { @@ -389,7 +391,7 @@ ngx_rtmp_flv_read_meta(ngx_rtmp_session_t *s, ngx_file_t *f) static ngx_int_t -ngx_rtmp_flv_send(ngx_rtmp_session_t *s, ngx_file_t *f) +ngx_rtmp_flv_send(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t *ts) { ngx_rtmp_flv_ctx_t *ctx; uint32_t last_timestamp; @@ -567,7 +569,28 @@ ngx_rtmp_flv_init(ngx_rtmp_session_t *s, ngx_file_t *f) static ngx_int_t -ngx_rtmp_flv_start(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) +ngx_rtmp_flv_start(ngx_rtmp_session_t *s, ngx_file_t *f) +{ + ngx_rtmp_flv_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_flv_module); + + if (ctx == NULL) { + return NGX_OK; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: start"); + + ctx->offset = -1; + ctx->msg_mask = 0; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_flv_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) { ngx_rtmp_flv_ctx_t *ctx; @@ -578,11 +601,9 @@ ngx_rtmp_flv_start(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "flv: start timestamp=%ui", timestamp); + "flv: seek timestamp=%ui", timestamp); ctx->start_timestamp = timestamp; - ctx->offset = -1; - ctx->msg_mask = 0; return NGX_OK; } @@ -635,6 +656,7 @@ ngx_rtmp_flv_postconfiguration(ngx_conf_t *cf) fmt->init = ngx_rtmp_flv_init; fmt->start = ngx_rtmp_flv_start; + fmt->seek = ngx_rtmp_flv_seek; fmt->stop = ngx_rtmp_flv_stop; fmt->send = ngx_rtmp_flv_send; diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index b39db1d..0c95849 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -9,12 +9,15 @@ static ngx_int_t ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf); -static ngx_int_t ngx_rtmp_mp4_init(ngx_rtmp_session_t *s, ngx_file_t *f); -static ngx_int_t ngx_rtmp_mp4_done(ngx_rtmp_session_t *s, ngx_file_t *f); -static ngx_int_t ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f, - ngx_uint_t offset); -static ngx_int_t ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s, ngx_file_t *f); -static ngx_int_t ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_mp4_init(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_mp4_done(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s, ngx_file_t *f, + ngx_uint_t offset); +static ngx_int_t ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f, + ngx_uint_t *ts); +static ngx_int_t ngx_rtmp_mp4_reset(ngx_rtmp_session_t *s); #pragma pack(push,4) @@ -1921,7 +1924,7 @@ ngx_rtmp_mp4_seek_track(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, static ngx_int_t -ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) +ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t *ts) { ngx_rtmp_mp4_ctx_t *ctx; ngx_buf_t in_buf; @@ -1962,6 +1965,7 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) sched = 0; active = 0; + last_timestamp = 0; end_timestamp = ctx->start_timestamp + (ngx_current_msec - ctx->epoch) + buflen; @@ -2123,7 +2127,17 @@ next: return sched; } - return active ? NGX_OK : NGX_DONE; + if (active) { + return NGX_OK; + } + + if (ts) { + *ts = last_timestamp; + } + + /*ngx_rtmp_mp4_reset(s);*/ + + return NGX_DONE; } @@ -2227,7 +2241,7 @@ ngx_rtmp_mp4_done(ngx_rtmp_session_t *s, ngx_file_t *f) static ngx_int_t -ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) +ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) { ngx_rtmp_mp4_ctx_t *ctx; ngx_uint_t n; @@ -2239,7 +2253,7 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: start timestamp=%ui", timestamp); + "mp4: seek timestamp=%ui", timestamp); for (n = 0; n < ctx->ntracks; ++n) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, @@ -2248,9 +2262,52 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) ngx_rtmp_mp4_seek_track(s, &ctx->tracks[n], timestamp); } - ctx->epoch = ngx_current_msec; ctx->start_timestamp = timestamp; + return ngx_rtmp_mp4_reset(s); +} + + +static ngx_int_t +ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f) +{ + ngx_rtmp_mp4_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL) { + return NGX_OK; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: start timestamp=%uD", ctx->start_timestamp); + + ctx->epoch = ngx_current_msec; + + return NGX_OK;/*ngx_rtmp_mp4_reset(s);*/ +} + + +static ngx_int_t +ngx_rtmp_mp4_reset(ngx_rtmp_session_t *s) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_track_t *t; + ngx_uint_t n; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL) { + return NGX_OK; + } + + t = &ctx->tracks[0]; + for (n = 0; n < ctx->ntracks; ++n, ++t) { + cr = &t->cursor; + cr->not_first = 0; + } + return NGX_OK; } @@ -2258,10 +2315,20 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) static ngx_int_t ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s, ngx_file_t *f) { - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: stop"); + ngx_rtmp_mp4_ctx_t *ctx; - return NGX_OK; + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL) { + return NGX_OK; + } + + ctx->start_timestamp += (ngx_current_msec - ctx->epoch); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: stop timestamp=%uD", ctx->start_timestamp); + + return NGX_OK;/*ngx_rtmp_mp4_reset(s);*/ } @@ -2294,6 +2361,7 @@ ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf) fmt->init = ngx_rtmp_mp4_init; fmt->done = ngx_rtmp_mp4_done; + fmt->seek = ngx_rtmp_mp4_seek; fmt->start = ngx_rtmp_mp4_start; fmt->stop = ngx_rtmp_mp4_stop; fmt->send = ngx_rtmp_mp4_send; diff --git a/ngx_rtmp_play_module.c b/ngx_rtmp_play_module.c index 3d1ed91..3c1b18b 100644 --- a/ngx_rtmp_play_module.c +++ b/ngx_rtmp_play_module.c @@ -19,10 +19,18 @@ static ngx_int_t ngx_rtmp_play_postconfiguration(ngx_conf_t *cf); static void * ngx_rtmp_play_create_app_conf(ngx_conf_t *cf); static char * ngx_rtmp_play_merge_app_conf(ngx_conf_t *cf, void *parent, void *child); -static ngx_int_t ngx_rtmp_play_init(ngx_rtmp_session_t *s); -static ngx_int_t ngx_rtmp_play_done(ngx_rtmp_session_t *s); -static ngx_int_t ngx_rtmp_play_start(ngx_rtmp_session_t *s, double timestamp); -static ngx_int_t ngx_rtmp_play_stop(ngx_rtmp_session_t *s); + +static ngx_int_t ngx_rtmp_play_do_init(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_play_do_done(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_play_do_start(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_play_do_stop(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_play_do_seek(ngx_rtmp_session_t *s, + ngx_uint_t timestamp); + +static ngx_int_t ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v); +static ngx_int_t ngx_rtmp_play_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v); +static ngx_int_t ngx_rtmp_play_pause(ngx_rtmp_session_t *s, + ngx_rtmp_pause_t *v); static void ngx_rtmp_play_send(ngx_event_t *e); @@ -122,6 +130,7 @@ ngx_rtmp_play_send(ngx_event_t *e) ngx_rtmp_session_t *s = e->data; ngx_rtmp_play_ctx_t *ctx; ngx_int_t rc; + ngx_uint_t ts; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); @@ -129,7 +138,9 @@ ngx_rtmp_play_send(ngx_event_t *e) return; } - rc = ctx->fmt->send(s, &ctx->file); + ts = 0; + + rc = ctx->fmt->send(s, &ctx->file, &ts); if (rc > 0) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, @@ -161,13 +172,14 @@ ngx_rtmp_play_send(ngx_event_t *e) ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID); - ngx_rtmp_send_play_status(s, "NetStream.Play.Complete", "status", 0, 0); + ngx_rtmp_send_play_status(s, "NetStream.Play.Complete", "status", ts, 0); + ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stopped"); } static ngx_int_t -ngx_rtmp_play_init(ngx_rtmp_session_t *s) +ngx_rtmp_play_do_init(ngx_rtmp_session_t *s) { ngx_rtmp_play_ctx_t *ctx; @@ -188,7 +200,7 @@ ngx_rtmp_play_init(ngx_rtmp_session_t *s) static ngx_int_t -ngx_rtmp_play_done(ngx_rtmp_session_t *s) +ngx_rtmp_play_do_done(ngx_rtmp_session_t *s) { ngx_rtmp_play_ctx_t *ctx; @@ -209,10 +221,9 @@ ngx_rtmp_play_done(ngx_rtmp_session_t *s) static ngx_int_t -ngx_rtmp_play_start(ngx_rtmp_session_t *s, double timestamp) +ngx_rtmp_play_do_start(ngx_rtmp_session_t *s) { ngx_rtmp_play_ctx_t *ctx; - ngx_uint_t ts; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); @@ -220,27 +231,53 @@ ngx_rtmp_play_start(ngx_rtmp_session_t *s, double timestamp) return NGX_ERROR; } - ngx_rtmp_play_stop(s); - - ts = (timestamp > 0 ? (ngx_uint_t) timestamp : 0); - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: start timestamp=%ui", ts); + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: start"); if (ctx->fmt && ctx->fmt->start && - ctx->fmt->start(s, &ctx->file, ts) != NGX_OK) + ctx->fmt->start(s, &ctx->file) != NGX_OK) { return NGX_ERROR; } ngx_post_event((&ctx->send_evt), &ngx_posted_events); + ctx->playing = 1; + return NGX_OK; } static ngx_int_t -ngx_rtmp_play_stop(ngx_rtmp_session_t *s) +ngx_rtmp_play_do_seek(ngx_rtmp_session_t *s, ngx_uint_t timestamp) +{ + ngx_rtmp_play_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: seek timestamp=%ui", timestamp); + + if (ctx->fmt && ctx->fmt->seek && + ctx->fmt->seek(s, &ctx->file, timestamp) != NGX_OK) + { + return NGX_ERROR; + } + + if (ctx->playing) { + ngx_post_event((&ctx->send_evt), &ngx_posted_events); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_play_do_stop(ngx_rtmp_session_t *s) { ngx_rtmp_play_ctx_t *ctx; @@ -267,6 +304,8 @@ ngx_rtmp_play_stop(ngx_rtmp_session_t *s) return NGX_ERROR; } + ctx->playing = 0; + return NGX_OK; } @@ -285,9 +324,9 @@ ngx_rtmp_play_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "play: close_stream"); - ngx_rtmp_play_stop(s); + ngx_rtmp_play_do_stop(s); - ngx_rtmp_play_done(s); + ngx_rtmp_play_do_done(s); if (ctx->file.fd != NGX_INVALID_FILE) { ngx_close_file(ctx->file.fd); @@ -309,10 +348,7 @@ ngx_rtmp_play_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) goto next; } - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: seek offset=%f", v->offset); - - ngx_rtmp_play_start(s, v->offset); + ngx_rtmp_play_do_seek(s, v->offset); next: return next_seek(s, v); @@ -335,9 +371,9 @@ ngx_rtmp_play_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) (ngx_int_t) v->pause, v->position); if (v->pause) { - ngx_rtmp_play_stop(s); + ngx_rtmp_play_do_stop(s); } else { - ngx_rtmp_play_start(s, v->position); + ngx_rtmp_play_do_start(s); /*TODO: v->position? */ } next: @@ -476,11 +512,15 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) ngx_rtmp_send_user_recorded(s, 1); - if (ngx_rtmp_play_init(s) != NGX_OK) { + if (ngx_rtmp_play_do_init(s) != NGX_OK) { return NGX_ERROR; } - if (ngx_rtmp_play_start(s, v->start) != NGX_OK) { + if (ngx_rtmp_play_do_seek(s, v->start < 0 ? 0 : v->start) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_play_do_start(s) != NGX_OK) { return NGX_ERROR; } diff --git a/ngx_rtmp_play_module.h b/ngx_rtmp_play_module.h index 77d7856..8bb9363 100644 --- a/ngx_rtmp_play_module.h +++ b/ngx_rtmp_play_module.h @@ -15,11 +15,13 @@ typedef ngx_int_t (*ngx_rtmp_play_init_pt) (ngx_rtmp_session_t *s, typedef ngx_int_t (*ngx_rtmp_play_done_pt) (ngx_rtmp_session_t *s, ngx_file_t *f); typedef ngx_int_t (*ngx_rtmp_play_start_pt) (ngx_rtmp_session_t *s, + ngx_file_t *f); +typedef ngx_int_t (*ngx_rtmp_play_seek_pt) (ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t offs); typedef ngx_int_t (*ngx_rtmp_play_stop_pt) (ngx_rtmp_session_t *s, ngx_file_t *f); typedef ngx_int_t (*ngx_rtmp_play_send_pt) (ngx_rtmp_session_t *s, - ngx_file_t *f); + ngx_file_t *f, ngx_uint_t *ts); typedef struct { @@ -30,6 +32,7 @@ typedef struct { ngx_rtmp_play_init_pt init; ngx_rtmp_play_done_pt done; ngx_rtmp_play_start_pt start; + ngx_rtmp_play_seek_pt seek; ngx_rtmp_play_stop_pt stop; ngx_rtmp_play_send_pt send; } ngx_rtmp_play_fmt_t; @@ -39,6 +42,7 @@ typedef struct { ngx_file_t file; ngx_rtmp_play_fmt_t *fmt; ngx_event_t send_evt; + unsigned playing:1; } ngx_rtmp_play_ctx_t; diff --git a/ngx_rtmp_send.c b/ngx_rtmp_send.c index 89d77b0..261c405 100644 --- a/ngx_rtmp_send.c +++ b/ngx_rtmp_send.c @@ -399,6 +399,7 @@ ngx_rtmp_send_play_status(ngx_rtmp_session_t *s, char *code, char* level, h.type = NGX_RTMP_MSG_AMF_META; h.csid = NGX_RTMP_CSID_AMF; h.msid = NGX_RTMP_MSID; + h.timestamp = duration; return ngx_rtmp_send_amf(s, &h, out_elts, sizeof(out_elts) / sizeof(out_elts[0])); From bff1c355ec7db875972da6682e2553d65ac847ad Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 5 Oct 2012 15:46:57 +0400 Subject: [PATCH 16/37] added record_notify feature --- ngx_rtmp_record_module.c | 20 ++++++++++++++++++++ ngx_rtmp_record_module.h | 1 + 2 files changed, 21 insertions(+) diff --git a/ngx_rtmp_record_module.c b/ngx_rtmp_record_module.c index 6147731..c7242bc 100644 --- a/ngx_rtmp_record_module.c +++ b/ngx_rtmp_record_module.c @@ -111,6 +111,14 @@ static ngx_command_t ngx_rtmp_record_commands[] = { offsetof(ngx_rtmp_record_app_conf_t, interval), NULL }, + { ngx_string("record_notify"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF| + NGX_RTMP_REC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, notify), + NULL }, + { ngx_string("recorder"), NGX_RTMP_APP_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE1, ngx_rtmp_record_recorder, @@ -166,6 +174,7 @@ ngx_rtmp_record_create_app_conf(ngx_conf_t *cf) racf->max_frames = NGX_CONF_UNSET; racf->interval = NGX_CONF_UNSET; racf->unique = NGX_CONF_UNSET; + racf->notify = NGX_CONF_UNSET; racf->url = NGX_CONF_UNSET_PTR; ngx_str_set(&racf->id, "default"); @@ -190,6 +199,7 @@ ngx_rtmp_record_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) 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_value(conf->unique, prev->unique, 0); + ngx_conf_merge_value(conf->notify, prev->notify, 0); ngx_conf_merge_msec_value(conf->interval, prev->interval, (ngx_msec_t) NGX_CONF_UNSET); ngx_conf_merge_bitmask_value(conf->flags, prev->flags, 0); @@ -397,6 +407,11 @@ ngx_rtmp_record_node_open(ngx_rtmp_session_t *s, ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "record: %V opened '%V'", &rracf->id, &path); + if (rracf->notify) { + ngx_rtmp_send_status(s, "NetStream.Record.Start", "status", + rracf->id.data ? (char *) rracf->id.data : ""); + } + return NGX_OK; } @@ -540,6 +555,11 @@ ngx_rtmp_record_node_close(ngx_rtmp_session_t *s, ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "record: %V closed", &rracf->id); + if (rracf->notify) { + ngx_rtmp_send_status(s, "NetStream.Record.Stop", "status", + rracf->id.data ? (char *) rracf->id.data : ""); + } + app_conf = s->app_conf; if (rracf->rec_conf) { diff --git a/ngx_rtmp_record_module.h b/ngx_rtmp_record_module.h index 6b7d417..ab4f4dd 100644 --- a/ngx_rtmp_record_module.h +++ b/ngx_rtmp_record_module.h @@ -26,6 +26,7 @@ typedef struct { ngx_msec_t interval; ngx_str_t suffix; ngx_flag_t unique; + ngx_flag_t notify; ngx_url_t *url; void **rec_conf; From e5d61f2c62b7303c4708bcce78984b7e5105b92c Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Tue, 9 Oct 2012 18:22:14 +0400 Subject: [PATCH 17/37] added 'busy' directive --- ngx_rtmp.h | 1 + ngx_rtmp_core_module.c | 9 +++++++++ ngx_rtmp_handler.c | 7 +++++++ 3 files changed, 17 insertions(+) diff --git a/ngx_rtmp.h b/ngx_rtmp.h index aa7432a..7d9c7f3 100644 --- a/ngx_rtmp.h +++ b/ngx_rtmp.h @@ -287,6 +287,7 @@ typedef struct ngx_rtmp_core_srv_conf_s { size_t max_message; ngx_flag_t play_time_fix; ngx_flag_t publish_time_fix; + ngx_flag_t busy; size_t out_queue; size_t out_cork; diff --git a/ngx_rtmp_core_module.c b/ngx_rtmp_core_module.c index eac72a7..e300e8b 100644 --- a/ngx_rtmp_core_module.c +++ b/ngx_rtmp_core_module.c @@ -127,6 +127,13 @@ static ngx_command_t ngx_rtmp_core_commands[] = { offsetof(ngx_rtmp_core_srv_conf_t, out_cork), NULL }, + { ngx_string("busy"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, busy), + NULL }, + /* time fixes are needed for flash clients */ { ngx_string("play_time_fix"), NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, @@ -232,6 +239,7 @@ ngx_rtmp_core_create_srv_conf(ngx_conf_t *cf) conf->out_cork = NGX_CONF_UNSET; conf->play_time_fix = NGX_CONF_UNSET; conf->publish_time_fix = NGX_CONF_UNSET; + conf->busy = NGX_CONF_UNSET; return conf; } @@ -258,6 +266,7 @@ ngx_rtmp_core_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) conf->out_queue / 8); ngx_conf_merge_value(conf->play_time_fix, prev->play_time_fix, 1); ngx_conf_merge_value(conf->publish_time_fix, prev->publish_time_fix, 1); + ngx_conf_merge_value(conf->busy, prev->busy, 0); if (prev->pool == NULL) { prev->pool = ngx_create_pool(4096, &cf->cycle->new_log); diff --git a/ngx_rtmp_handler.c b/ngx_rtmp_handler.c index 79465f7..f0a0c83 100644 --- a/ngx_rtmp_handler.c +++ b/ngx_rtmp_handler.c @@ -164,6 +164,13 @@ ngx_rtmp_ping(ngx_event_t *pev) return; } + if (cscf->busy) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "ping: not busy between pings"); + ngx_rtmp_finalize_session(s); + return; + } + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "ping: schedule %Mms", cscf->ping_timeout); From ebb20b848e5585664e978be21fde2243cce4459b Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 12 Oct 2012 20:45:47 +0400 Subject: [PATCH 18/37] implemented MPEG-TS sample re-arranging in HLS stream to fix crackles because of low-resolution timestamp in RTMP vs MPEG-TS --- hls/ngx_rtmp_hls_module.c | 39 +++++++++++++++++++++++++++++++++++++++ ngx_rtmp_codec_module.c | 25 ++++++++++++++++++++++--- ngx_rtmp_codec_module.h | 1 + 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index f7ecbfb..5073800 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -73,6 +73,9 @@ typedef struct { ngx_int_t out_astream; int8_t nal_bytes; + int64_t aframe_base; + int64_t aframe_num; + AVFormatContext *out_format; } ngx_rtmp_hls_ctx_t; @@ -82,6 +85,7 @@ typedef struct { ngx_flag_t hls; ngx_msec_t fraglen; ngx_msec_t muxdelay; + ngx_msec_t sync; ngx_msec_t playlen; size_t nfrags; ngx_rtmp_hls_ctx_t **ctx; @@ -127,6 +131,13 @@ static ngx_command_t ngx_rtmp_hls_commands[] = { offsetof(ngx_rtmp_hls_app_conf_t, muxdelay), NULL }, + { ngx_string("hls_sync"), + 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_hls_app_conf_t, sync), + NULL }, + ngx_null_command }; @@ -909,6 +920,7 @@ ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_codec_ctx_t *codec_ctx; AVPacket packet; + int64_t dts, ddts; static u_char buffer[NGX_RTMP_HLS_BUFSIZE]; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); @@ -940,6 +952,31 @@ ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, packet.data = buffer; packet.size = ngx_rtmp_hls_chain2buffer(buffer, sizeof(buffer), in, 1); + if (hacf->sync && codec_ctx->sample_rate) { + + /* TODO: We assume here AAC frame size is 1024 + * Need to handle AAC frames with frame size of 960 */ + + dts = ctx->aframe_base + ctx->aframe_num * 90000 * 1024 / + codec_ctx->sample_rate; + ddts = dts - packet.dts; + + if (ddts > (int64_t) hacf->sync * 90 || + ddts < (int64_t) hacf->sync * -90) + { + ctx->aframe_base = packet.dts; + ctx->aframe_num = 0; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: sync breakup ddts=%L (%.5fs)", + ddts, ddts / 90000.); + } else { + packet.dts = dts; + } + + ctx->aframe_num++; + } + if (codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC) { if (packet.size == 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, @@ -1119,6 +1156,7 @@ ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf) conf->hls = NGX_CONF_UNSET; conf->fraglen = NGX_CONF_UNSET; conf->muxdelay = NGX_CONF_UNSET; + conf->sync = NGX_CONF_UNSET; conf->playlen = NGX_CONF_UNSET; conf->nbuckets = 1024; @@ -1135,6 +1173,7 @@ ngx_rtmp_hls_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_value(conf->hls, prev->hls, 0); ngx_conf_merge_msec_value(conf->fraglen, prev->fraglen, 5000); ngx_conf_merge_msec_value(conf->muxdelay, prev->muxdelay, 700); + ngx_conf_merge_msec_value(conf->sync, prev->sync, 0); ngx_conf_merge_msec_value(conf->playlen, prev->playlen, 30000); ngx_conf_merge_str_value(conf->path, prev->path, ""); conf->ctx = ngx_pcalloc(cf->pool, diff --git a/ngx_rtmp_codec_module.c b/ngx_rtmp_codec_module.c index 25ef0a9..778d23f 100644 --- a/ngx_rtmp_codec_module.c +++ b/ngx_rtmp_codec_module.c @@ -162,6 +162,11 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, static ngx_uint_t sample_rates[] = { 5512, 11025, 22050, 44100 }; + static ngx_uint_t aac_sample_rates[] = + { 96000, 88200, 64000, 48000, + 44100, 32000, 24000, 22050, + 16000, 12000, 11025, 8000, + 7350, 0, 0, 0 }; if (h->type != NGX_RTMP_MSG_AUDIO && h->type != NGX_RTMP_MSG_VIDEO) { return NGX_OK; @@ -183,7 +188,10 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ctx->audio_codec_id = (fmt & 0xf0) >> 4; ctx->audio_channels = (fmt & 0x01) + 1; ctx->sample_size = (fmt & 0x02) ? 2 : 1; - ctx->sample_rate = sample_rates[(fmt & 0x0c) >> 2]; + + if (ctx->aac_sample_rate == 0) { + ctx->sample_rate = sample_rates[(fmt & 0x0c) >> 2]; + } } else { ctx->video_codec_id = (fmt & 0x0f); } @@ -207,8 +215,19 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, header = &ctx->aac_header; pheader = &ctx->aac_pheader; version = &ctx->aac_version; - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "codec: AAC header arrived"); + + /* 1 byte RTMP audio header + * 1 byte AAC conf/data + * 3 bytes until 18-22 bits of AAC header */ + if (in->buf->last - in->buf->pos >= 5) { + ctx->aac_sample_rate = aac_sample_rates[ + (in->buf->pos[4] >> 2) & 0xff]; + ctx->sample_rate = ctx->aac_sample_rate; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "codec: AAC header arrived, sample_rate=%u", + ctx->aac_sample_rate); } } else { if (ctx->video_codec_id == NGX_RTMP_VIDEO_H264) { diff --git a/ngx_rtmp_codec_module.h b/ngx_rtmp_codec_module.h index 4a95e32..d02f100 100644 --- a/ngx_rtmp_codec_module.h +++ b/ngx_rtmp_codec_module.h @@ -55,6 +55,7 @@ typedef struct { ngx_uint_t video_codec_id; ngx_uint_t audio_data_rate; ngx_uint_t audio_codec_id; + ngx_uint_t aac_sample_rate; ngx_uint_t sample_rate; /* 5512, 11025, 22050, 44100 */ ngx_uint_t sample_size; /* 1=8bit, 2=16bit */ ngx_uint_t audio_channels; /* 1, 2 */ From 67e7f4818f78d7dfba964e51e134aecddc8ce10a Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 12 Oct 2012 21:08:12 +0400 Subject: [PATCH 19/37] fixed logging --- ngx_rtmp_codec_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_rtmp_codec_module.c b/ngx_rtmp_codec_module.c index 778d23f..91cf30a 100644 --- a/ngx_rtmp_codec_module.c +++ b/ngx_rtmp_codec_module.c @@ -226,7 +226,7 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "codec: AAC header arrived, sample_rate=%u", + "codec: AAC header arrived, sample_rate=%ui", ctx->aac_sample_rate); } } else { From 5cd5f83d8d919bca5ec2827785606df837c30189 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 12 Oct 2012 21:10:39 +0400 Subject: [PATCH 20/37] fixed AAC real sample rate parsing --- ngx_rtmp_codec_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_rtmp_codec_module.c b/ngx_rtmp_codec_module.c index 91cf30a..6ad5659 100644 --- a/ngx_rtmp_codec_module.c +++ b/ngx_rtmp_codec_module.c @@ -221,7 +221,7 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, * 3 bytes until 18-22 bits of AAC header */ if (in->buf->last - in->buf->pos >= 5) { ctx->aac_sample_rate = aac_sample_rates[ - (in->buf->pos[4] >> 2) & 0xff]; + (in->buf->pos[4] >> 2) & 0xf]; ctx->sample_rate = ctx->aac_sample_rate; } From 5aac8b5cb32401f379f055df94f4e12636dea260 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Sat, 13 Oct 2012 09:28:35 +0400 Subject: [PATCH 21/37] fixed AAC header parser --- ngx_rtmp_codec_module.c | 51 +++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/ngx_rtmp_codec_module.c b/ngx_rtmp_codec_module.c index 6ad5659..657edc0 100644 --- a/ngx_rtmp_codec_module.c +++ b/ngx_rtmp_codec_module.c @@ -158,7 +158,8 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_chain_t **header, **pheader; uint8_t fmt; ngx_rtmp_header_t ch, lh; - ngx_uint_t *version; + ngx_uint_t *version, idx; + u_char *p; static ngx_uint_t sample_rates[] = { 5512, 11025, 22050, 44100 }; @@ -216,12 +217,48 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, pheader = &ctx->aac_pheader; version = &ctx->aac_version; - /* 1 byte RTMP audio header - * 1 byte AAC conf/data - * 3 bytes until 18-22 bits of AAC header */ - if (in->buf->last - in->buf->pos >= 5) { - ctx->aac_sample_rate = aac_sample_rates[ - (in->buf->pos[4] >> 2) & 0xf]; + if (in->buf->last - in->buf->pos > 3) { + p = in->buf->pos + 2; + + /* MPEG-4 Audio Specific Config + + 5 bits: object type + if (object type == 31) + 6 bits + 32: object type + --->4 bits: frequency index + if (frequency index == 15) + 24 bits: frequency + 4 bits: channel configuration + var bits: AOT Specific Config + */ + + if ((p[0] >> 3) == 0x1f) { + idx = (p[1] >> 1) & 0x0f; + } else { + idx = ((p[0] << 1) & 0x0f) | (p[1] >> 7); + } + +#ifdef NGX_DEBUG + { + u_char buf[256], *p, *pp; + u_char hex[] = "01234567890abcdef"; + + for (pp = buf, p = in->buf->pos; + p < in->buf->last && pp < buf + sizeof(buf) - 1; + ++p) + { + *pp++ = hex[*p >> 4]; + *pp++ = hex[*p & 0x0f]; + } + + *pp = 0; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "codec: AAC header: %s", buf); + } +#endif + + ctx->aac_sample_rate = aac_sample_rates[idx]; ctx->sample_rate = ctx->aac_sample_rate; } From a4c790e0ee901f6cd631c9af8adca731f0e3aed5 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 15 Oct 2012 09:43:22 +0400 Subject: [PATCH 22/37] fixed compilation --- ngx_rtmp_cmd_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ngx_rtmp_cmd_module.c b/ngx_rtmp_cmd_module.c index 39cbe1f..6b389af 100644 --- a/ngx_rtmp_cmd_module.c +++ b/ngx_rtmp_cmd_module.c @@ -1076,7 +1076,7 @@ ngx_rtmp_cmd_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) return (ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_CMD_MSID) != NGX_OK || ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_CMD_MSID) != NGX_OK || ngx_rtmp_send_status(s, "NetStream.Seek.Notify", "status", - "Seeking") + "Seeking")) ? NGX_ERROR : NGX_OK; } From 21174b2ee743c9ea1a0bccb46436c56c871febd3 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Tue, 16 Oct 2012 14:09:52 +0400 Subject: [PATCH 23/37] fixed pts assignment while synchronizing hls --- hls/ngx_rtmp_hls_module.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index 5073800..6ac2c4b 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -947,7 +947,6 @@ ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, /* write to file */ av_init_packet(&packet); packet.dts = h->timestamp * 90L; - packet.pts = packet.dts; packet.stream_index = ctx->out_astream; packet.data = buffer; packet.size = ngx_rtmp_hls_chain2buffer(buffer, sizeof(buffer), in, 1); @@ -961,6 +960,10 @@ ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, codec_ctx->sample_rate; ddts = dts - packet.dts; + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: sync stat ddts=%L (%.5fs)", + ddts, ddts / 90000.); + if (ddts > (int64_t) hacf->sync * 90 || ddts < (int64_t) hacf->sync * -90) { @@ -977,6 +980,8 @@ ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ctx->aframe_num++; } + packet.pts = packet.dts; + if (codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC) { if (packet.size == 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, @@ -1160,7 +1165,7 @@ ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf) conf->playlen = NGX_CONF_UNSET; conf->nbuckets = 1024; - return conf; + return conf; } From 795c1538a3602d5eedf7617c6d13c775721a66b9 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 18 Oct 2012 11:08:30 +0400 Subject: [PATCH 24/37] added option for choosing exec kill signal --- ngx_rtmp_exec_module.c | 69 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/ngx_rtmp_exec_module.c b/ngx_rtmp_exec_module.c index 6e9a405..cdabc36 100644 --- a/ngx_rtmp_exec_module.c +++ b/ngx_rtmp_exec_module.c @@ -38,6 +38,7 @@ typedef struct { ngx_array_t execs; /* ngx_rtmp_exec_conf_t */ ngx_msec_t respawn_timeout; ngx_flag_t respawn; + ngx_int_t kill_signal; } ngx_rtmp_exec_app_conf_t; @@ -59,6 +60,8 @@ typedef struct { } ngx_rtmp_exec_ctx_t; +static char *ngx_rtmp_exec_kill_signal(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); static ngx_int_t ngx_rtmp_exec_kill(ngx_rtmp_session_t *s, ngx_rtmp_exec_t *e, ngx_int_t term); static ngx_int_t ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n); @@ -87,6 +90,13 @@ static ngx_command_t ngx_rtmp_exec_commands[] = { offsetof(ngx_rtmp_exec_app_conf_t, respawn_timeout), NULL }, + { ngx_string("exec_kill_signal"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_exec_kill_signal, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + ngx_null_command }; @@ -165,6 +175,7 @@ ngx_rtmp_exec_create_app_conf(ngx_conf_t *cf) eacf->respawn = NGX_CONF_UNSET; eacf->respawn_timeout = NGX_CONF_UNSET; + eacf->kill_signal = NGX_CONF_UNSET; if (ngx_array_init(&eacf->execs, cf->pool, 1, sizeof(ngx_rtmp_exec_conf_t)) != NGX_OK) @@ -187,6 +198,7 @@ ngx_rtmp_exec_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_value(conf->respawn, prev->respawn, 1); ngx_conf_merge_msec_value(conf->respawn_timeout, prev->respawn_timeout, 5000); + ngx_conf_merge_value(conf->kill_signal, prev->kill_signal, SIGKILL); if (prev->execs.nelts) { ec = ngx_array_push_n(&conf->execs, prev->execs.nelts); @@ -254,6 +266,10 @@ ngx_rtmp_exec_child_dead(ngx_event_t *ev) static ngx_int_t ngx_rtmp_exec_kill(ngx_rtmp_session_t *s, ngx_rtmp_exec_t *e, ngx_int_t term) { + ngx_rtmp_exec_app_conf_t *eacf; + + eacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_exec_module); + if (e->respawn_evt.timer_set) { ngx_del_timer(&e->respawn_evt); } @@ -277,7 +293,7 @@ ngx_rtmp_exec_kill(ngx_rtmp_session_t *s, ngx_rtmp_exec_t *e, ngx_int_t term) return NGX_OK; } - if (kill(e->pid, SIGKILL) == -1) { + if (kill(e->pid, eacf->kill_signal) == -1) { ngx_log_error(NGX_LOG_INFO, s->connection->log, ngx_errno, "exec: kill failed pid=%i", (ngx_int_t)e->pid); } else { @@ -513,6 +529,57 @@ ngx_rtmp_exec_exec(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) } +static char * +ngx_rtmp_exec_kill_signal(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_exec_app_conf_t *eacf; + ngx_str_t *value; + + eacf = ngx_rtmp_conf_get_module_app_conf(cf, ngx_rtmp_exec_module); + value = cf->args->elts; + value++; + + eacf->kill_signal = ngx_atoi(value->data, value->len); + if (eacf->kill_signal != NGX_ERROR) { + return NGX_CONF_OK; + } + +#define NGX_RMTP_EXEC_SIGNAL(name) \ + if (value->len == sizeof(#name) - 1 && \ + ngx_strncasecmp(value->data, (u_char *) #name, value->len) == 0) \ + { \ + eacf->kill_signal = SIG##name; \ + return NGX_CONF_OK; \ + } + + /* POSIX.1-1990 signals */ + + NGX_RMTP_EXEC_SIGNAL(HUP); + NGX_RMTP_EXEC_SIGNAL(INT); + NGX_RMTP_EXEC_SIGNAL(QUIT); + NGX_RMTP_EXEC_SIGNAL(ILL); + NGX_RMTP_EXEC_SIGNAL(ABRT); + NGX_RMTP_EXEC_SIGNAL(FPE); + NGX_RMTP_EXEC_SIGNAL(KILL); + NGX_RMTP_EXEC_SIGNAL(SEGV); + NGX_RMTP_EXEC_SIGNAL(PIPE); + NGX_RMTP_EXEC_SIGNAL(ALRM); + NGX_RMTP_EXEC_SIGNAL(TERM); + NGX_RMTP_EXEC_SIGNAL(USR1); + NGX_RMTP_EXEC_SIGNAL(USR2); + NGX_RMTP_EXEC_SIGNAL(CHLD); + NGX_RMTP_EXEC_SIGNAL(CONT); + NGX_RMTP_EXEC_SIGNAL(STOP); + NGX_RMTP_EXEC_SIGNAL(TSTP); + NGX_RMTP_EXEC_SIGNAL(TTIN); + NGX_RMTP_EXEC_SIGNAL(TTOU); + +#undef NGX_RMTP_EXEC_SIGNAL + + return "unknown signal"; +} + + static ngx_int_t ngx_rtmp_exec_postconfiguration(ngx_conf_t *cf) { From ff1c7ecec56dca2220380cb658d40fbca29030d8 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 18 Oct 2012 22:15:27 +0400 Subject: [PATCH 25/37] explicitly close file descriptors in child to escape epoll signaling wrong events --- ngx_rtmp_enotify_module.c | 2 +- ngx_rtmp_exec_module.c | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/ngx_rtmp_enotify_module.c b/ngx_rtmp_enotify_module.c index a81b8e2..f6c3858 100644 --- a/ngx_rtmp_enotify_module.c +++ b/ngx_rtmp_enotify_module.c @@ -242,7 +242,7 @@ ngx_rtmp_enotify_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) static ngx_int_t ngx_rtmp_enotify_exec(ngx_rtmp_session_t *s, ngx_rtmp_enotify_conf_t *ec) { -#ifndef NGX_WIN32 +#if !(NGX_WIN32) int pid, fd; ngx_str_t a, *arg; char **args; diff --git a/ngx_rtmp_exec_module.c b/ngx_rtmp_exec_module.c index cdabc36..b3b8308 100644 --- a/ngx_rtmp_exec_module.c +++ b/ngx_rtmp_exec_module.c @@ -308,10 +308,10 @@ ngx_rtmp_exec_kill(ngx_rtmp_session_t *s, ngx_rtmp_exec_t *e, ngx_int_t term) static ngx_int_t ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n) { -#ifndef NGX_WIN32 +#if !(NGX_WIN32) ngx_rtmp_exec_app_conf_t *eacf; ngx_rtmp_exec_ctx_t *ctx; - int pid, fd; + int pid, fd, maxfd; int pipefd[2]; int ret; ngx_rtmp_exec_conf_t *ec; @@ -325,6 +325,8 @@ ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n) ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); e = ctx->execs + n; + ngx_memzero(e, sizeof(*e)); + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "exec: starting child '%V'", &ec->cmd); @@ -360,6 +362,17 @@ ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n) case 0: /* child */ + + /* close all descriptors but pipe write end */ + maxfd = sysconf(_SC_OPEN_MAX); + for (fd = 0; fd < maxfd; ++fd) { + if (fd == pipefd[1]) { + continue; + } + + close(fd); + } + fd = open("/dev/null", O_RDWR); dup2(fd, STDIN_FILENO); From a3fde6ea536de702e14f43ea9c8ae7157fe5e7be Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 18 Oct 2012 23:37:44 +0400 Subject: [PATCH 26/37] added closing descriptors in enotify module --- ngx_rtmp_enotify_module.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ngx_rtmp_enotify_module.c b/ngx_rtmp_enotify_module.c index f6c3858..4acea5f 100644 --- a/ngx_rtmp_enotify_module.c +++ b/ngx_rtmp_enotify_module.c @@ -243,7 +243,7 @@ static ngx_int_t ngx_rtmp_enotify_exec(ngx_rtmp_session_t *s, ngx_rtmp_enotify_conf_t *ec) { #if !(NGX_WIN32) - int pid, fd; + int pid, fd, maxfd; ngx_str_t a, *arg; char **args; ngx_uint_t n; @@ -258,6 +258,13 @@ ngx_rtmp_enotify_exec(ngx_rtmp_session_t *s, ngx_rtmp_enotify_conf_t *ec) case 0: /* child */ + + /* close all descriptors */ + maxfd = sysconf(_SC_OPEN_MAX); + for (fd = 0; fd < maxfd; ++fd) { + close(fd); + } + fd = open("/dev/null", O_RDWR); dup2(fd, STDIN_FILENO); From cb4623aa4f7e6c2d1cb4c9c9f55ea6107f3a3985 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Sat, 20 Oct 2012 17:40:21 +0400 Subject: [PATCH 27/37] removed legacy constants --- ngx_rtmp_cmd_module.c | 44 ++++++++++++++++++++--------------------- ngx_rtmp_codec_module.c | 10 +++++----- ngx_rtmp_flv_module.c | 6 +++--- ngx_rtmp_live_module.c | 6 +++--- ngx_rtmp_streams.h | 10 ---------- 5 files changed, 33 insertions(+), 43 deletions(-) diff --git a/ngx_rtmp_cmd_module.c b/ngx_rtmp_cmd_module.c index af63ed1..ffd03a7 100644 --- a/ngx_rtmp_cmd_module.c +++ b/ngx_rtmp_cmd_module.c @@ -214,7 +214,7 @@ ngx_rtmp_cmd_connect(ngx_rtmp_session_t *s, ngx_rtmp_connect_t *v) s->connected = 1; ngx_memzero(&h, sizeof(h)); - h.csid = NGX_RTMP_CMD_CSID_AMF_INI; + h.csid = NGX_RTMP_CSID_AMF_INI; h.type = NGX_RTMP_MSG_AMF_CMD; @@ -321,10 +321,10 @@ ngx_rtmp_cmd_create_stream(ngx_rtmp_session_t *s, ngx_rtmp_create_stream_t *v) }; trans = v->trans; - stream = NGX_RTMP_CMD_MSID; + stream = NGX_RTMP_MSID; ngx_memzero(&h, sizeof(h)); - h.csid = NGX_RTMP_CMD_CSID_AMF_INI; + h.csid = NGX_RTMP_CSID_AMF_INI; h.type = NGX_RTMP_MSG_AMF_CMD; ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, @@ -369,7 +369,7 @@ ngx_rtmp_cmd_close_stream_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, static ngx_int_t ngx_rtmp_cmd_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) { - ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_CMD_MSID); + ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID); /* Whatever happens return OK * since we should be careful with destruction */ @@ -537,8 +537,8 @@ ngx_rtmp_cmd_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) /* send onStatus reply */ memset(&h, 0, sizeof(h)); h.type = NGX_RTMP_MSG_AMF_CMD; - h.csid = NGX_RTMP_CMD_CSID_AMF; - h.msid = NGX_RTMP_CMD_MSID; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; if (ngx_rtmp_send_amf(s, &h, out_elts, sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK) @@ -630,8 +630,8 @@ ngx_rtmp_cmd_fcpublish(ngx_rtmp_session_t *s, ngx_rtmp_fcpublish_t *v) /* send onFCPublish reply */ memset(&h, 0, sizeof(h)); h.type = NGX_RTMP_MSG_AMF_CMD; - h.csid = NGX_RTMP_CMD_CSID_AMF; - h.msid = NGX_RTMP_CMD_MSID; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; if (ngx_rtmp_send_amf(s, &h, out_elts, sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK) @@ -818,15 +818,15 @@ ngx_rtmp_cmd_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) /* send onStatus reply */ memset(&h, 0, sizeof(h)); h.type = NGX_RTMP_MSG_AMF_CMD; - h.csid = NGX_RTMP_CMD_CSID_AMF; - h.msid = NGX_RTMP_CMD_MSID; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; /* - if (ngx_rtmp_send_user_recorded(s, NGX_RTMP_CMD_MSID) != NGX_OK) { + if (ngx_rtmp_send_user_recorded(s, NGX_RTMP_MSID) != NGX_OK) { return NGX_ERROR; }*/ - if (ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_CMD_MSID) != NGX_OK) { + if (ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_MSID) != NGX_OK) { return NGX_ERROR; } @@ -943,8 +943,8 @@ ngx_rtmp_cmd_fcsubscribe(ngx_rtmp_session_t *s, ngx_rtmp_fcsubscribe_t *v) /* send onFCSubscribe reply */ memset(&h, 0, sizeof(h)); h.type = NGX_RTMP_MSG_AMF_CMD; - h.csid = NGX_RTMP_CMD_CSID_AMF; - h.msid = NGX_RTMP_CMD_MSID; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; if (ngx_rtmp_send_amf(s, &h, out_elts, sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK) @@ -1048,13 +1048,13 @@ ngx_rtmp_cmd_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) /* send onStatus reply */ ngx_memzero(&h, sizeof(h)); h.type = NGX_RTMP_MSG_AMF_CMD; - h.csid = NGX_RTMP_CMD_CSID_AMF; - h.msid = NGX_RTMP_CMD_MSID; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; if (v->pause) { out_inf[0].data = "NetStream.Pause.Notify"; out_inf[2].data = "Paused."; - return ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_CMD_MSID) != NGX_OK + return ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID) != NGX_OK || ngx_rtmp_send_amf(s, &h, out_elts, sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK ? NGX_ERROR @@ -1063,7 +1063,7 @@ ngx_rtmp_cmd_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) out_inf[0].data = "NetStream.Unpause.Notify"; out_inf[2].data = "Unpaused."; - return ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_CMD_MSID) != NGX_OK + return ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_MSID) != NGX_OK || ngx_rtmp_send_amf(s, &h, out_elts, sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK ? NGX_ERROR @@ -1168,11 +1168,11 @@ ngx_rtmp_cmd_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) /* send onStatus reply */ ngx_memzero(&h, sizeof(h)); h.type = NGX_RTMP_MSG_AMF_CMD; - h.csid = NGX_RTMP_CMD_CSID_AMF; - h.msid = NGX_RTMP_CMD_MSID; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; - return (ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_CMD_MSID) != NGX_OK - || ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_CMD_MSID) != NGX_OK + return (ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID) != NGX_OK + || ngx_rtmp_send_user_stream_begin(s, NGX_RTMP_MSID) != NGX_OK || ngx_rtmp_send_amf(s, &h, out_elts, sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK) ? NGX_ERROR diff --git a/ngx_rtmp_codec_module.c b/ngx_rtmp_codec_module.c index 657edc0..a8d23b9 100644 --- a/ngx_rtmp_codec_module.c +++ b/ngx_rtmp_codec_module.c @@ -290,11 +290,11 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, /* equal headers; timeout diff is zero */ ngx_memzero(&ch, sizeof(ch)); - ch.msid = NGX_RTMP_LIVE_MSID; + ch.msid = NGX_RTMP_MSID; ch.type = h->type; ch.csid = (h->type == NGX_RTMP_MSG_VIDEO - ? NGX_RTMP_LIVE_CSID_VIDEO - : NGX_RTMP_LIVE_CSID_AUDIO); + ? NGX_RTMP_CSID_VIDEO + : NGX_RTMP_CSID_AUDIO); lh = ch; *header = ngx_rtmp_append_shared_bufs(cscf, NULL, in); *pheader = ngx_rtmp_append_shared_bufs(cscf, NULL, in); @@ -428,8 +428,8 @@ ngx_rtmp_codec_update_meta(ngx_rtmp_session_t *s) } ngx_memzero(&h, sizeof(h)); - h.csid = NGX_RTMP_LIVE_CSID_META; - h.msid = NGX_RTMP_LIVE_MSID; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; h.type = NGX_RTMP_MSG_AMF_META; ngx_rtmp_prepare_message(s, &h, NULL, ctx->meta); diff --git a/ngx_rtmp_flv_module.c b/ngx_rtmp_flv_module.c index 9e8163e..17fc050 100644 --- a/ngx_rtmp_flv_module.c +++ b/ngx_rtmp_flv_module.c @@ -343,8 +343,8 @@ ngx_rtmp_flv_read_meta(ngx_rtmp_session_t *s, ngx_file_t *f) ngx_memzero(&h, sizeof(h)); h.type = NGX_RTMP_MSG_AMF_META; - h.msid = NGX_RTMP_LIVE_MSID; - h.csid = NGX_RTMP_LIVE_CSID_META; + h.msid = NGX_RTMP_MSID; + h.csid = NGX_RTMP_CSID_AMF; size = 0; ngx_rtmp_rmemcpy(&size, ngx_rtmp_flv_header + 1, 3); @@ -431,7 +431,7 @@ ngx_rtmp_flv_send(ngx_rtmp_session_t *s, ngx_file_t *f) /* parse header fields */ ngx_memzero(&h, sizeof(h)); - h.msid = NGX_RTMP_LIVE_MSID; + h.msid = NGX_RTMP_MSID; h.type = ngx_rtmp_flv_header[0]; size = 0; diff --git a/ngx_rtmp_live_module.c b/ngx_rtmp_live_module.c index bca2c03..a406146 100644 --- a/ngx_rtmp_live_module.c +++ b/ngx_rtmp_live_module.c @@ -337,18 +337,18 @@ ngx_rtmp_live_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_memzero(&ch, sizeof(ch)); ngx_memzero(&lh, sizeof(lh)); ch.timestamp = h->timestamp; - ch.msid = NGX_RTMP_LIVE_MSID; + ch.msid = NGX_RTMP_MSID; ch.type = h->type; lh.msid = ch.msid; if (h->type == NGX_RTMP_MSG_VIDEO) { prio = ngx_rtmp_get_video_frame_type(in); - ch.csid = NGX_RTMP_LIVE_CSID_VIDEO; + ch.csid = NGX_RTMP_CSID_VIDEO; lh.timestamp = ctx->last_video; ctx->last_video = ch.timestamp; } else { /* audio priority is the same as video key frame's */ prio = NGX_RTMP_VIDEO_KEY_FRAME; - ch.csid = NGX_RTMP_LIVE_CSID_AUDIO; + ch.csid = NGX_RTMP_CSID_AUDIO; lh.timestamp = ctx->last_audio; ctx->last_audio = ch.timestamp; } diff --git a/ngx_rtmp_streams.h b/ngx_rtmp_streams.h index bf1ec7b..9f15475 100644 --- a/ngx_rtmp_streams.h +++ b/ngx_rtmp_streams.h @@ -15,14 +15,4 @@ #define NGX_RTMP_CSID_VIDEO 7 -/*legacy*/ -#define NGX_RTMP_CMD_CSID_AMF_INI NGX_RTMP_CSID_AMF_INI -#define NGX_RTMP_CMD_CSID_AMF NGX_RTMP_CSID_AMF -#define NGX_RTMP_CMD_MSID NGX_RTMP_MSID -#define NGX_RTMP_LIVE_CSID_META NGX_RTMP_CSID_AMF -#define NGX_RTMP_LIVE_CSID_AUDIO NGX_RTMP_CSID_AUDIO -#define NGX_RTMP_LIVE_CSID_VIDEO NGX_RTMP_CSID_VIDEO -#define NGX_RTMP_LIVE_MSID NGX_RTMP_MSID - - #endif /* _NGX_RTMP_STREAMS_H_INCLUDED_ */ From c86e30fd270103b00c244f136c8780fb3a51b921 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Sun, 21 Oct 2012 05:56:58 +0400 Subject: [PATCH 28/37] added deps to config --- config | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/config b/config index b2d2114..752b1b7 100644 --- a/config +++ b/config @@ -26,6 +26,22 @@ HTTP_MODULES="$HTTP_MODULES \ " +NGX_ADDON_DEPS="$NGX_ADDON_DEPS \ + $ngx_addon_dir/ngx_rtmp_amf.h \ + $ngx_addon_dir/ngx_rtmp_bandwidth.h \ + $ngx_addon_dir/ngx_rtmp_cmd_module.h \ + $ngx_addon_dir/ngx_rtmp_codec_module.h \ + $ngx_addon_dir/ngx_rtmp_eval.h \ + $ngx_addon_dir/ngx_rtmp.h \ + $ngx_addon_dir/ngx_rtmp_live_module.h \ + $ngx_addon_dir/ngx_rtmp_netcall_module.h \ + $ngx_addon_dir/ngx_rtmp_play_module.h \ + $ngx_addon_dir/ngx_rtmp_record_module.h \ + $ngx_addon_dir/ngx_rtmp_relay_module.h \ + $ngx_addon_dir/ngx_rtmp_streams.h \ + " + + NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $ngx_addon_dir/ngx_rtmp.c \ $ngx_addon_dir/ngx_rtmp_init.c \ From f65f07deb32565b144e22faece57638f8961d62f Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Tue, 23 Oct 2012 10:46:44 +0400 Subject: [PATCH 29/37] implemented connection dropped in control module --- ngx_rtmp_control_module.c | 342 ++++++++++++++++++++++++++++---------- 1 file changed, 251 insertions(+), 91 deletions(-) diff --git a/ngx_rtmp_control_module.c b/ngx_rtmp_control_module.c index 227b677..ff7bcd1 100644 --- a/ngx_rtmp_control_module.c +++ b/ngx_rtmp_control_module.c @@ -17,8 +17,22 @@ static char * ngx_rtmp_control_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); +typedef struct { + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_core_app_conf_t *cacf; +} ngx_rtmp_control_core_t; + + +typedef struct { + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_live_stream_t *ls; +} ngx_rtmp_control_live_t; + + #define NGX_RTMP_CONTROL_ALL 0xff #define NGX_RTMP_CONTROL_RECORD 0x01 +#define NGX_RTMP_CONTROL_DROP 0x02 typedef struct { @@ -27,9 +41,10 @@ typedef struct { static ngx_conf_bitmask_t ngx_rtmp_control_masks[] = { - { ngx_string("all"), NGX_RTMP_CONTROL_ALL }, - { ngx_string("record"), NGX_RTMP_CONTROL_RECORD }, - { ngx_null_string, 0 } + { ngx_string("all"), NGX_RTMP_CONTROL_ALL }, + { ngx_string("record"), NGX_RTMP_CONTROL_RECORD }, + { ngx_string("drop"), NGX_RTMP_CONTROL_DROP }, + { ngx_null_string, 0 } }; @@ -77,6 +92,131 @@ ngx_module_t ngx_rtmp_control_module = { }; +static ngx_int_t +ngx_rtmp_control_output_error(ngx_http_request_t *r, const char *msg) +{ + size_t len; + ngx_buf_t *b; + ngx_chain_t cl; + + len = ngx_strlen(msg); + + r->headers_out.status = NGX_HTTP_BAD_REQUEST; + r->headers_out.content_length_n = len; + + b = ngx_calloc_buf(r->pool); + if (b == NULL) { + return NGX_ERROR; + } + + ngx_memzero(&cl, sizeof(cl)); + cl.buf = b; + + b->start = b->pos = (u_char *) msg; + b->end = b->last = (u_char *) msg + len; + b->memory = 1; + b->last_buf = 1; + + ngx_http_send_header(r); + + return ngx_http_output_filter(r, &cl); +} + + +static const char * +ngx_rtmp_control_parse_core(ngx_http_request_t *r, + ngx_rtmp_control_core_t *core) +{ + ngx_str_t srv, app; + ngx_uint_t sn, n; + ngx_rtmp_core_srv_conf_t **pcscf; + ngx_rtmp_core_app_conf_t **pcacf; + + + core->cmcf = ngx_rtmp_core_main_conf; + if (core->cmcf == NULL) { + return "Missing main RTMP conf"; + } + + /* find server */ + sn = 0; + + if (ngx_http_arg(r, (u_char *) "srv", sizeof("srv") - 1, &srv) == NGX_OK) { + sn = ngx_atoi(srv.data, srv.len); + } + + if (sn >= core->cmcf->servers.nelts) { + return "Server index out of range"; + } + + pcscf = core->cmcf->servers.elts; + pcscf += sn; + + core->cscf = *pcscf; + + /* find application */ + if (ngx_http_arg(r, (u_char *) "app", sizeof("app") - 1, &app) != NGX_OK) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, + "rtmp_control: app not specified"); + return "Application not specified"; + } + + core->cacf = NULL; + + pcacf = core->cscf->applications.elts; + + for (n = 0; n < core->cscf->applications.nelts; ++n, ++pcacf) { + if ((*pcacf)->name.len == app.len && + ngx_strncmp((*pcacf)->name.data, app.data, app.len) == 0) + { + core->cacf = *pcacf; + break; + } + } + + if (core->cacf == NULL) { + return "Application not found"; + } + + return NGX_CONF_OK; +} + + +static const char * +ngx_rtmp_control_parse_live(ngx_http_request_t *r, + ngx_rtmp_control_core_t *core, + ngx_rtmp_control_live_t *live) +{ + ngx_str_t name; + size_t len; + + ngx_memzero(&name, sizeof(name)); + ngx_http_arg(r, (u_char *) "name", sizeof("name") - 1, &name); + + live->lacf = core->cacf->app_conf[ngx_rtmp_live_module.ctx_index]; + + /* find live stream by name */ + for (live->ls = live->lacf->streams[ngx_hash_key(name.data, name.len) % + live->lacf->nbuckets]; + live->ls; live->ls = live->ls->next) + { + len = ngx_strlen(live->ls->name); + + if (name.len == len && ngx_strncmp(name.data, live->ls->name, name.len) + == 0) + { + break; + } + } + + if (live->ls == NULL) { + return "Live stream not found"; + } + + return NGX_CONF_OK; +} + + /* /record arguments: * srv - server index (optional) * app - application name @@ -88,116 +228,55 @@ ngx_module_t ngx_rtmp_control_module = { static ngx_int_t ngx_rtmp_control_record(ngx_http_request_t *r, ngx_str_t *method) { + ngx_rtmp_control_core_t core; + ngx_rtmp_control_live_t live; ngx_rtmp_record_app_conf_t *racf; - ngx_rtmp_core_main_conf_t *cmcf; - ngx_rtmp_core_srv_conf_t **pcscf, *cscf; - ngx_rtmp_core_app_conf_t **pcacf, *cacf; - ngx_rtmp_live_app_conf_t *lacf; - ngx_rtmp_live_stream_t *ls; ngx_rtmp_live_ctx_t *lctx; ngx_rtmp_session_t *s; ngx_chain_t cl; - ngx_uint_t sn, rn, n; - ngx_str_t srv, app, rec, name, path; - ngx_str_t msg; + ngx_uint_t rn; + ngx_str_t rec, path; ngx_buf_t *b; ngx_int_t rc; - size_t len; + const char *msg; - sn = 0; - if (ngx_http_arg(r, (u_char *) "srv", sizeof("srv") - 1, &srv) == NGX_OK) { - sn = ngx_atoi(srv.data, srv.len); - } - - if (ngx_http_arg(r, (u_char *) "app", sizeof("app") - 1, &app) != NGX_OK) { - ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, - "rtmp_control: app not specified"); - ngx_str_set(&msg, "Application not specified"); + msg = ngx_rtmp_control_parse_core(r, &core); + if (msg != NGX_CONF_OK) { goto error; } - ngx_memzero(&rec, sizeof(rec)); - ngx_http_arg(r, (u_char *) "rec", sizeof("rec") - 1, &rec); - - ngx_memzero(&name, sizeof(name)); - ngx_http_arg(r, (u_char *) "name", sizeof("name") - 1, &name); - - cmcf = ngx_rtmp_core_main_conf; - if (cmcf == NULL) { - ngx_str_set(&msg, "Missing main RTMP conf"); - goto error; - } - - /* find server */ - if (sn >= cmcf->servers.nelts) { - ngx_str_set(&msg, "Server index out of range"); - goto error; - } - - pcscf = cmcf->servers.elts; - pcscf += sn; - cscf = *pcscf; - - /* find application */ - pcacf = cscf->applications.elts; - cacf = NULL; - - for (n = 0; n < cscf->applications.nelts; ++n, ++pcacf) { - if ((*pcacf)->name.len == app.len && - ngx_strncmp((*pcacf)->name.data, app.data, app.len) == 0) - { - cacf = *pcacf; - break; - } - } - - if (cacf == NULL) { - ngx_str_set(&msg, "Application not found"); - goto error; - } - - lacf = cacf->app_conf[ngx_rtmp_live_module.ctx_index]; - racf = cacf->app_conf[ngx_rtmp_record_module.ctx_index]; - - /* find live stream by name */ - for (ls = lacf->streams[ngx_hash_key(name.data, name.len) % lacf->nbuckets]; - ls; ls = ls->next) - { - len = ngx_strlen(ls->name); - - if (name.len == len && ngx_strncmp(name.data, ls->name, name.len) - == 0) - { - break; - } - } - - if (ls == NULL) { - ngx_str_set(&msg, "Live stream not found"); + msg = ngx_rtmp_control_parse_live(r, &core, &live); + if (msg != NGX_CONF_OK) { goto error; } /* find publisher context */ - for (lctx = ls->ctx; lctx; lctx = lctx->next) { + for (lctx = live.ls->ctx; lctx; lctx = lctx->next) { if (lctx->flags & NGX_RTMP_LIVE_PUBLISHING) { break; } } if (lctx == NULL) { - ngx_str_set(&msg, "No publisher"); + msg = "No publisher"; goto error; } s = lctx->session; /* find recorder */ + ngx_memzero(&rec, sizeof(rec)); + ngx_http_arg(r, (u_char *) "rec", sizeof("rec") - 1, &rec); + + racf = core.cacf->app_conf[ngx_rtmp_record_module.ctx_index]; + rn = ngx_rtmp_record_find(racf, &rec); if (rn == NGX_CONF_UNSET_UINT) { - ngx_str_set(&msg, "Recorder not found"); + msg = "Recorder not found"; goto error; } + /* call the method */ ngx_memzero(&path, sizeof(path)); if (method->len == sizeof("start") - 1 && @@ -211,12 +290,12 @@ ngx_rtmp_control_record(ngx_http_request_t *r, ngx_str_t *method) rc = ngx_rtmp_record_close(s, rn, &path); } else { - ngx_str_set(&msg, "Undefined method"); + msg = "Undefined method"; goto error; } if (rc == NGX_ERROR) { - ngx_str_set(&msg, "Recorder error"); + msg = "Recorder error"; goto error; } @@ -245,25 +324,105 @@ ngx_rtmp_control_record(ngx_http_request_t *r, ngx_str_t *method) return ngx_http_output_filter(r, &cl); error: - r->headers_out.status = NGX_HTTP_BAD_REQUEST; - r->headers_out.content_length_n = msg.len; + return ngx_rtmp_control_output_error(r, msg); +} + + +static ngx_int_t +ngx_rtmp_control_drop(ngx_http_request_t *r, ngx_str_t *method) +{ + ngx_rtmp_control_core_t core; + ngx_rtmp_control_live_t live; + ngx_rtmp_live_ctx_t *lctx; + ngx_str_t addr, *paddr; + const char *msg; + ngx_uint_t ndropped; + size_t len; + u_char *p; + ngx_buf_t *b; + ngx_chain_t cl; + + msg = ngx_rtmp_control_parse_core(r, &core); + if (msg != NGX_CONF_OK) { + goto error; + } + + msg = ngx_rtmp_control_parse_live(r, &core, &live); + if (msg != NGX_CONF_OK) { + goto error; + } + + ndropped = 0; + + if (method->len == sizeof("publisher") - 1 && + ngx_strncmp(method->data, "publisher", method->len) == 0) + { + for (lctx = live.ls->ctx; lctx; lctx = lctx->next) { + if (lctx->flags & NGX_RTMP_LIVE_PUBLISHING) { + ngx_rtmp_finalize_session(lctx->session); + ++ndropped; + break; + } + } + + } else if (method->len == sizeof("client") - 1 && + ngx_strncmp(method->data, "client", method->len) == 0) + { + ngx_memzero(&addr, sizeof(addr)); + ngx_http_arg(r, (u_char *) "addr", sizeof("addr") - 1, &addr); + + for (lctx = live.ls->ctx; lctx; lctx = lctx->next) { + if (addr.len && lctx->session && lctx->session->connection) { + paddr = &lctx->session->connection->addr_text; + if (paddr->len != addr.len || + ngx_strncmp(paddr->data, addr.data, addr.len)) + { + continue; + } + } + + ngx_rtmp_finalize_session(lctx->session); + ++ndropped; + } + + } else { + msg = "Undefined method"; + goto error; + } + + /* output ndropped */ + + len = NGX_OFF_T_LEN; + + p = ngx_palloc(r->connection->pool, len); + if (p == NULL) { + return NGX_ERROR; + } + + len = (size_t) (ngx_snprintf(p, len, "%ui", ndropped) - p); + + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = len; b = ngx_calloc_buf(r->pool); if (b == NULL) { return NGX_ERROR; } + b->start = b->pos = p; + b->end = b->last = p + len; + b->temporary = 1; + b->last_buf = 1; + ngx_memzero(&cl, sizeof(cl)); cl.buf = b; - b->start = b->pos = msg.data; - b->end = b->last = msg.data + msg.len; - b->memory = 1; - b->last_buf = 1; - ngx_http_send_header(r); return ngx_http_output_filter(r, &cl); + +error: + return ngx_rtmp_control_output_error(r, msg); } @@ -315,6 +474,7 @@ ngx_rtmp_control_handler(ngx_http_request_t *r) } NGX_RTMP_CONTROL_SECTION(RECORD, record); + NGX_RTMP_CONTROL_SECTION(DROP, drop); #undef NGX_RTMP_CONTROL_SECTION From eacfd8fac4c0d9aa8fada93cf641d52b03da3502 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Tue, 23 Oct 2012 20:21:24 +0400 Subject: [PATCH 30/37] this version compiles ok --- ngx_rtmp_netcall_module.c | 95 ++++++++---- ngx_rtmp_netcall_module.h | 10 +- ngx_rtmp_notify_module.c | 29 ++-- ngx_rtmp_play_module.c | 295 +++++++++++++++++++++++++++++++++++++- ngx_rtmp_play_module.h | 3 + 5 files changed, 380 insertions(+), 52 deletions(-) diff --git a/ngx_rtmp_netcall_module.c b/ngx_rtmp_netcall_module.c index 01a01b4..384c90b 100644 --- a/ngx_rtmp_netcall_module.c +++ b/ngx_rtmp_netcall_module.c @@ -20,12 +20,9 @@ static void ngx_rtmp_netcall_recv(ngx_event_t *rev); static void ngx_rtmp_netcall_send(ngx_event_t *wev); -ngx_str_t ngx_rtmp_netcall_content_type_urlencoded = - ngx_string("application/x-www-form-urlencoded"); - - typedef struct { ngx_msec_t timeout; + size_t bufsize; ngx_log_t *log; } ngx_rtmp_netcall_app_conf_t; @@ -38,11 +35,13 @@ typedef struct ngx_rtmp_netcall_session_s { void *arg; ngx_rtmp_netcall_handle_pt handle; ngx_rtmp_netcall_filter_pt filter; + ngx_rtmp_netcall_sink_pt sink; ngx_chain_t *in; ngx_chain_t *inlast; ngx_chain_t *out; ngx_msec_t timeout; - ngx_int_t detached; + unsigned detached:1; + size_t bufsize; } ngx_rtmp_netcall_session_t; @@ -60,6 +59,13 @@ static ngx_command_t ngx_rtmp_netcall_commands[] = { offsetof(ngx_rtmp_netcall_app_conf_t, timeout), NULL }, + { ngx_string("netcall_buffer"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_netcall_app_conf_t, bufsize), + NULL }, + ngx_null_command }; @@ -103,6 +109,8 @@ ngx_rtmp_netcall_create_app_conf(ngx_conf_t *cf) } nacf->timeout = NGX_CONF_UNSET_MSEC; + nacf->bufsize = NGX_CONF_UNSET_SIZE; + nacf->log = &cf->cycle->new_log; return nacf; @@ -116,6 +124,7 @@ ngx_rtmp_netcall_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) ngx_rtmp_netcall_app_conf_t *conf = child; ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 10000); + ngx_conf_merge_size_value(conf->bufsize, prev->bufsize, 1024); return NGX_CONF_OK; } @@ -219,9 +228,11 @@ ngx_rtmp_netcall_create(ngx_rtmp_session_t *s, ngx_rtmp_netcall_init_t *ci) } cs->timeout = cacf->timeout; + cs->bufsize = cacf->bufsize; cs->url = ci->url; cs->session = s; cs->filter = ci->filter; + cs->sink = ci->sink; cs->handle = ci->handle; if (cs->handle == NULL) { cs->detached = 1; @@ -281,6 +292,7 @@ ngx_rtmp_netcall_close(ngx_connection_t *cc) ngx_pool_t *pool; ngx_rtmp_session_t *s; ngx_rtmp_netcall_ctx_t *ctx; + ngx_buf_t *b; cs = cc->data; @@ -294,6 +306,14 @@ ngx_rtmp_netcall_close(ngx_connection_t *cc) s = cs->session; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_netcall_module); + if (cs->in && cs->sink) { + cs->sink(cs->session, cs->in); + + b = cs->in->buf; + b->pos = b->last = b->start; + + } + for(css = &ctx->cs; *css; css = &((*css)->next)) { if (*css == cs) { *css = cs->next; @@ -301,9 +321,7 @@ ngx_rtmp_netcall_close(ngx_connection_t *cc) } } - if (cs->handle && - cs->handle(s, cs->arg, cs->in) != NGX_OK) - { + if (cs->handle && cs->handle(s, cs->arg, cs->in) != NGX_OK) { ngx_rtmp_finalize_session(s); } } @@ -352,29 +370,43 @@ ngx_rtmp_netcall_recv(ngx_event_t *rev) for ( ;; ) { - if (cs->inlast == NULL - || cs->inlast->buf->last == cs->inlast->buf->end) + if (cs->inlast == NULL || + cs->inlast->buf->last == cs->inlast->buf->end) { - cl = ngx_alloc_chain_link(cc->pool); - if (cl == NULL) { - ngx_rtmp_netcall_close(cc); - return; - } - cl->next = NULL; + if (cs->in && cs->sink) { + if (!cs->detached) { + if (cs->sink(cs->session, cs->in) != NGX_OK) { + ngx_rtmp_netcall_close(cc); + return; + } + } - cl->buf = ngx_create_temp_buf(cc->pool, 1024); - if (cl->buf == NULL) { - ngx_rtmp_netcall_close(cc); - return; - } + b = cs->in->buf; + b->pos = b->last = b->start; - if (cs->in == NULL) { - cs->in = cl; } else { - cs->inlast->next = cl; - } + cl = ngx_alloc_chain_link(cc->pool); + if (cl == NULL) { + ngx_rtmp_netcall_close(cc); + return; + } - cs->inlast = cl; + cl->next = NULL; + + cl->buf = ngx_create_temp_buf(cc->pool, cs->bufsize); + if (cl->buf == NULL) { + ngx_rtmp_netcall_close(cc); + return; + } + + if (cs->in == NULL) { + cs->in = cl; + } else { + cs->inlast->next = cl; + } + + cs->inlast = cl; + } } b = cs->inlast->buf; @@ -459,8 +491,9 @@ ngx_rtmp_netcall_send(ngx_event_t *wev) ngx_chain_t * -ngx_rtmp_netcall_http_format_header(ngx_url_t *url, ngx_pool_t *pool, - size_t content_length, ngx_str_t *content_type) +ngx_rtmp_netcall_http_format_header(ngx_str_t *uri, ngx_str_t *host, + ngx_pool_t *pool, size_t content_length, + ngx_str_t *content_type) { ngx_chain_t *cl; ngx_buf_t *b; @@ -480,8 +513,8 @@ ngx_rtmp_netcall_http_format_header(ngx_url_t *url, ngx_pool_t *pool, } b = ngx_create_temp_buf(pool, sizeof(rq_tmpl) - + url->uri.len - + url->host.len + + uri->len + + host->len + content_type->len + 5); @@ -492,7 +525,7 @@ ngx_rtmp_netcall_http_format_header(ngx_url_t *url, ngx_pool_t *pool, cl->buf = b; b->last = ngx_snprintf(b->last, b->end - b->last, rq_tmpl, - &url->uri, &url->host, content_type, content_length); + uri, host, content_type, content_length); return cl; } diff --git a/ngx_rtmp_netcall_module.h b/ngx_rtmp_netcall_module.h index b621e6b..f6ea286 100644 --- a/ngx_rtmp_netcall_module.h +++ b/ngx_rtmp_netcall_module.h @@ -15,6 +15,8 @@ typedef ngx_chain_t * (*ngx_rtmp_netcall_create_pt)(ngx_rtmp_session_t *s, void *arg, ngx_pool_t *pool); typedef ngx_int_t (*ngx_rtmp_netcall_filter_pt)(ngx_chain_t *in); +typedef ngx_int_t (*ngx_rtmp_netcall_sink_pt)(ngx_rtmp_session_t *s, + ngx_chain_t *in); typedef ngx_int_t (*ngx_rtmp_netcall_handle_pt)(ngx_rtmp_session_t *s, void *arg, ngx_chain_t *in); @@ -32,6 +34,7 @@ typedef struct { ngx_url_t *url; ngx_rtmp_netcall_create_pt create; ngx_rtmp_netcall_filter_pt filter; + ngx_rtmp_netcall_sink_pt sink; ngx_rtmp_netcall_handle_pt handle; void *arg; size_t argsize; @@ -41,14 +44,13 @@ typedef struct { ngx_int_t ngx_rtmp_netcall_create(ngx_rtmp_session_t *s, ngx_rtmp_netcall_init_t *ci); -extern ngx_str_t ngx_rtmp_netcall_content_type_urlencoded; - /* HTTP handling */ ngx_chain_t * ngx_rtmp_netcall_http_format_session(ngx_rtmp_session_t *s, ngx_pool_t *pool); -ngx_chain_t * ngx_rtmp_netcall_http_format_header(ngx_url_t *url, - ngx_pool_t *pool, size_t content_length, ngx_str_t *content_type); +ngx_chain_t * ngx_rtmp_netcall_http_format_header(ngx_str_t *uri, + ngx_str_t *host, ngx_pool_t *pool, size_t content_length, + ngx_str_t *content_type); ngx_chain_t * ngx_rtmp_netcall_http_skip_header(ngx_chain_t *in); diff --git a/ngx_rtmp_notify_module.c b/ngx_rtmp_notify_module.c index 361cb78..3d29e77 100644 --- a/ngx_rtmp_notify_module.c +++ b/ngx_rtmp_notify_module.c @@ -28,6 +28,10 @@ static ngx_int_t ngx_rtmp_notify_done(ngx_rtmp_session_t *s, char *cbname, ngx_url_t *url); +ngx_str_t ngx_rtmp_notify_urlencoded = + ngx_string("application/x-www-form-urlencoded"); + + #define NGX_RTMP_NOTIFY_PUBLISHING 0x01 #define NGX_RTMP_NOTIFY_PLAYING 0x02 @@ -190,6 +194,7 @@ ngx_rtmp_notify_publish_create(ngx_rtmp_session_t *s, void *arg, ngx_chain_t *hl, *cl, *pl; ngx_buf_t *b; ngx_str_t *addr_text; + ngx_url_t *url; size_t name_len, type_len, args_len; nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); @@ -244,9 +249,10 @@ ngx_rtmp_notify_publish_create(ngx_rtmp_session_t *s, void *arg, } /* HTTP header */ - hl = ngx_rtmp_netcall_http_format_header(nacf->url[NGX_RTMP_NOTIFY_PUBLISH], + url = nacf->url[NGX_RTMP_NOTIFY_PUBLISH]; + hl = ngx_rtmp_netcall_http_format_header(&url->uri, &url->host, pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), - &ngx_rtmp_netcall_content_type_urlencoded); + &ngx_rtmp_notify_urlencoded); if (hl == NULL) { return NULL; @@ -270,6 +276,7 @@ ngx_rtmp_notify_play_create(ngx_rtmp_session_t *s, void *arg, ngx_chain_t *hl, *cl, *pl; ngx_buf_t *b; ngx_str_t *addr_text; + ngx_url_t *url; size_t name_len, args_len; nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); @@ -324,9 +331,10 @@ ngx_rtmp_notify_play_create(ngx_rtmp_session_t *s, void *arg, } /* HTTP header */ - hl = ngx_rtmp_netcall_http_format_header(nacf->url[NGX_RTMP_NOTIFY_PLAY], + url = nacf->url[NGX_RTMP_NOTIFY_PLAY]; + hl = ngx_rtmp_netcall_http_format_header(&url->uri, &url->host, pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), - &ngx_rtmp_netcall_content_type_urlencoded); + &ngx_rtmp_notify_urlencoded); if (hl == NULL) { return NULL; @@ -401,9 +409,9 @@ ngx_rtmp_notify_done_create(ngx_rtmp_session_t *s, void *arg, } /* HTTP header */ - hl = ngx_rtmp_netcall_http_format_header(ds->url, pool, - cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), - &ngx_rtmp_netcall_content_type_urlencoded); + hl = ngx_rtmp_netcall_http_format_header(&ds->url->uri, &ds->url->host, + pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), + &ngx_rtmp_notify_urlencoded); if (hl == NULL) { return NULL; @@ -428,6 +436,7 @@ ngx_rtmp_notify_record_done_create(ngx_rtmp_session_t *s, void *arg, ngx_chain_t *hl, *cl, *pl; ngx_buf_t *b; ngx_str_t *addr_text; + ngx_url_t *url; size_t name_len, args_len; nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); @@ -489,10 +498,10 @@ ngx_rtmp_notify_record_done_create(ngx_rtmp_session_t *s, void *arg, } /* HTTP header */ - hl = ngx_rtmp_netcall_http_format_header( - nacf->url[NGX_RTMP_NOTIFY_RECORD_DONE], + url = nacf->url[NGX_RTMP_NOTIFY_RECORD_DONE]; + hl = ngx_rtmp_netcall_http_format_header(&url->uri, &url->host, pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), - &ngx_rtmp_netcall_content_type_urlencoded); + &ngx_rtmp_notify_urlencoded); if (hl == NULL) { return NULL; diff --git a/ngx_rtmp_play_module.c b/ngx_rtmp_play_module.c index 3d1ed91..792a386 100644 --- a/ngx_rtmp_play_module.c +++ b/ngx_rtmp_play_module.c @@ -5,6 +5,7 @@ #include "ngx_rtmp_play_module.h" #include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_netcall_module.h" #include "ngx_rtmp_streams.h" @@ -14,6 +15,8 @@ static ngx_rtmp_seek_pt next_seek; static ngx_rtmp_pause_pt next_pause; +static char *ngx_rtmp_play_url(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); static void *ngx_rtmp_play_create_main_conf(ngx_conf_t *cf); static ngx_int_t ngx_rtmp_play_postconfiguration(ngx_conf_t *cf); static void * ngx_rtmp_play_create_app_conf(ngx_conf_t *cf); @@ -24,15 +27,29 @@ static ngx_int_t ngx_rtmp_play_done(ngx_rtmp_session_t *s); static ngx_int_t ngx_rtmp_play_start(ngx_rtmp_session_t *s, double timestamp); static ngx_int_t ngx_rtmp_play_stop(ngx_rtmp_session_t *s); static void ngx_rtmp_play_send(ngx_event_t *e); +static ngx_int_t ngx_rtmp_play_open(ngx_rtmp_session_t *s, double start); +static ngx_int_t ngx_rtmp_play_remote_handle(ngx_rtmp_session_t *s, + void *arg, ngx_chain_t *in); +static ngx_chain_t * ngx_rtmp_play_remote_create(ngx_rtmp_session_t *s, + void *arg, ngx_pool_t *pool); +static ngx_int_t ngx_rtmp_play_open_remote(ngx_rtmp_session_t *s, + ngx_rtmp_play_t *v); static ngx_command_t ngx_rtmp_play_commands[] = { { ngx_string("play"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_play_url, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("play_temp_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_play_app_conf_t, root), + offsetof(ngx_rtmp_play_app_conf_t, temp_path), NULL }, ngx_null_command @@ -111,6 +128,7 @@ ngx_rtmp_play_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) ngx_rtmp_play_app_conf_t *conf = child; ngx_conf_merge_str_value(conf->root, prev->root, ""); + ngx_conf_merge_str_value(conf->temp_path, prev->temp_path, "/tmp"); return NGX_CONF_OK; } @@ -352,7 +370,6 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) ngx_rtmp_play_app_conf_t *pacf; ngx_rtmp_play_ctx_t *ctx; u_char *p; - ngx_event_t *e; ngx_rtmp_play_fmt_t *fmt, **pfmt; ngx_str_t *pfx, *sfx; ngx_str_t name; @@ -457,17 +474,46 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) p = ngx_snprintf(path, sizeof(path), "%V/%V%V", &pacf->root, &name, sfx); *p = 0; + ctx->file.fd = NGX_INVALID_FILE; + + if (pacf->url) { + return ngx_rtmp_play_open_remote(s, v); + } + + /* open local */ + ctx->file.fd = ngx_open_file(path, NGX_FILE_RDONLY, NGX_FILE_OPEN, NGX_FILE_DEFAULT_ACCESS); if (ctx->file.fd == NGX_INVALID_FILE) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, "play: error opening file '%s'", path); return NGX_ERROR; } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: opened file '%s'", path); + "play: opened local file '%s'", path); + + if (ngx_rtmp_play_open(s, v->start) != NGX_OK) { + return NGX_ERROR; + } + +next: + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_play_open(ngx_rtmp_session_t *s, double start) +{ + ngx_rtmp_play_ctx_t *ctx; + ngx_event_t *e; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx->file.fd == NGX_INVALID_FILE) { + return NGX_ERROR; + } e = &ctx->send_evt; e->data = s; @@ -480,12 +526,247 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) return NGX_ERROR; } - if (ngx_rtmp_play_start(s, v->start) != NGX_OK) { + if (ngx_rtmp_play_start(s, start) != NGX_OK) { return NGX_ERROR; } -next: - return next_play(s, v); + return NGX_OK; +} + + +static ngx_chain_t * +ngx_rtmp_play_remote_create(ngx_rtmp_session_t *s, void *arg, ngx_pool_t *pool) +{ + ngx_rtmp_play_t *v = arg; + + ngx_rtmp_play_app_conf_t *pacf; + ngx_chain_t *hl, *cl, *pl; + ngx_buf_t *b; + ngx_str_t *addr_text, uri; + u_char *p; + size_t name_len, args_len; + static ngx_str_t text_plain = ngx_string("text/plain"); + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + /* common variables */ + cl = ngx_rtmp_netcall_http_format_session(s, pool); + + if (cl == NULL) { + return NULL; + } + + /* publish variables */ + pl = ngx_alloc_chain_link(pool); + + if (pl == NULL) { + return NULL; + } + + name_len = ngx_strlen(v->name); + args_len = ngx_strlen(v->args); + addr_text = &s->connection->addr_text; + + b = ngx_create_temp_buf(pool, + sizeof("&addr=") + addr_text->len + + 1 + args_len); + if (b == NULL) { + return NULL; + } + + pl->buf = b; + + b->last = ngx_cpymem(b->last, (u_char*)"&addr=", sizeof("&addr=") -1); + b->last = (u_char*)ngx_escape_uri(b->last, addr_text->data, + addr_text->len, 0); + + if (args_len) { + *b->last++ = '&'; + b->last = (u_char *)ngx_cpymem(b->last, v->args, args_len); + } + + /* create uri */ + uri.len = pacf->url->uri.len + name_len; + uri.data = ngx_palloc(pool, uri.len); + + p = ngx_cpymem(uri.data, pacf->url->uri.data, pacf->url->uri.len); + ngx_memcpy(p, v->name, name_len); + + /* HTTP header */ + hl = ngx_rtmp_netcall_http_format_header(&uri, &pacf->url->host, + pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), + &text_plain); + + if (hl == NULL) { + return NULL; + } + + hl->next = cl; + cl->next = pl; + pl->next = NULL; + + return hl; +} + + +static ngx_int_t +ngx_rtmp_play_remote_handle(ngx_rtmp_session_t *s, void *arg, ngx_chain_t *in) +{ + ngx_rtmp_play_t *v = arg; + + if (ngx_rtmp_play_open(s, v->start) != NGX_OK) { + return NGX_ERROR; + } + + return next_play(s, (ngx_rtmp_play_t *)arg); +} + + +static ngx_int_t +ngx_rtmp_play_remote_sink(ngx_rtmp_session_t *s, ngx_chain_t *in) +{ + ngx_rtmp_play_ctx_t *ctx; + ngx_buf_t *b; + ngx_int_t rc; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + /* skip HTTP header */ + for (; in && ctx->ncrs != 2; in = in->next) { + b = in->buf; + + for (; b->pos != b->last && ctx->ncrs != 2; ++b->pos) { + switch (*b->pos) { + case '\n': + ++ctx->ncrs; + break; + case '\r': + break; + default: + ctx->ncrs = 0; + } + } + } + + /* write to temp file */ + for (; in; in = in->next) { + b = in->buf; + + if (b->pos == b->last) { + continue; + } + + rc = ngx_write_fd(ctx->file.fd, b->pos, b->last - b->pos); + + if (rc == NGX_ERROR) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, ngx_errno, + "play: error writing to temp file"); + return NGX_ERROR; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_play_open_remote(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_play_app_conf_t *pacf; + ngx_rtmp_play_ctx_t *ctx; + ngx_rtmp_netcall_init_t ci; + u_char *p; + ngx_err_t err; + static u_char path[NGX_MAX_PATH]; + static ngx_uint_t counter; + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + for ( ;; ) { + p = ngx_snprintf(path, sizeof(path), "%V/nginx-rtmp-play%ui", + &pacf->temp_path, counter++); + *p = 0; + + ctx->file.fd = ngx_open_tempfile(path, 0, 0); + + if (ctx->file.fd != NGX_INVALID_FILE) { + break; + } + + err = ngx_errno; + + if (err != NGX_EEXIST) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, err, + "play: failed to create temp file"); + + return NGX_ERROR; + } + } + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = pacf->url;; + ci.create = ngx_rtmp_play_remote_create; + ci.sink = ngx_rtmp_play_remote_sink; + ci.handle = ngx_rtmp_play_remote_handle; + ci.arg = v; + ci.argsize = sizeof(*v); + + return ngx_rtmp_netcall_create(s, &ci); +} + + +static char * +ngx_rtmp_play_url(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_play_app_conf_t *pacf; + ngx_str_t url; + ngx_url_t *u; + size_t add; + ngx_str_t *value; + + value = cf->args->elts; + + if (ngx_strncasecmp(value[1].data, (u_char *) "http://", 7)) { + + /* local file */ + + pacf->root = value[1]; + return NGX_CONF_OK; + } + + /* http case */ + + url = value[1]; + + add = sizeof("http://") - 1; + + url.data += add; + url.len -= add; + + u = ngx_pcalloc(cf->pool, sizeof(ngx_url_t)); + if (u == NULL) { + return NGX_CONF_ERROR; + } + + u->url.len = url.len; + u->url.data = url.data; + u->default_port = 80; + u->uri_part = 1; + + if (ngx_parse_url(cf->pool, u) != NGX_OK) { + if (u->err) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "%s in url \"%V\"", u->err, &u->url); + } + return NGX_CONF_ERROR; + } + + pacf->url = u; + + return NGX_CONF_OK; } diff --git a/ngx_rtmp_play_module.h b/ngx_rtmp_play_module.h index 77d7856..cb768d1 100644 --- a/ngx_rtmp_play_module.h +++ b/ngx_rtmp_play_module.h @@ -39,11 +39,14 @@ typedef struct { ngx_file_t file; ngx_rtmp_play_fmt_t *fmt; ngx_event_t send_evt; + ngx_uint_t ncrs; } ngx_rtmp_play_ctx_t; typedef struct { ngx_str_t root; + ngx_str_t temp_path; + ngx_url_t *url; } ngx_rtmp_play_app_conf_t; From b8424c9b635764993acd6c6c24bae7d7c81df714 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Tue, 23 Oct 2012 21:59:31 +0400 Subject: [PATCH 31/37] vod-http is now working --- ngx_rtmp_netcall_module.c | 25 ++++++++-- ngx_rtmp_netcall_module.h | 9 ++-- ngx_rtmp_notify_module.c | 20 ++++---- ngx_rtmp_play_module.c | 97 +++++++++++++++++++-------------------- ngx_rtmp_play_module.h | 1 + 5 files changed, 85 insertions(+), 67 deletions(-) diff --git a/ngx_rtmp_netcall_module.c b/ngx_rtmp_netcall_module.c index 384c90b..1f52b32 100644 --- a/ngx_rtmp_netcall_module.c +++ b/ngx_rtmp_netcall_module.c @@ -491,15 +491,17 @@ ngx_rtmp_netcall_send(ngx_event_t *wev) ngx_chain_t * -ngx_rtmp_netcall_http_format_header(ngx_str_t *uri, ngx_str_t *host, - ngx_pool_t *pool, size_t content_length, - ngx_str_t *content_type) +ngx_rtmp_netcall_http_format_header(ngx_int_t method, ngx_str_t *uri, + ngx_str_t *host, ngx_pool_t *pool, + size_t content_length, + ngx_str_t *content_type) { ngx_chain_t *cl; ngx_buf_t *b; + const char *method_s; static char rq_tmpl[] = - "POST %V HTTP/1.0\r\n" + "%s %V HTTP/1.0\r\n" "Host: %V\r\n" "Content-Type: %V\r\n" "Connection: Close\r\n" @@ -513,6 +515,7 @@ ngx_rtmp_netcall_http_format_header(ngx_str_t *uri, ngx_str_t *host, } b = ngx_create_temp_buf(pool, sizeof(rq_tmpl) + + sizeof("POST") - 1 /* longest method */ + uri->len + host->len + content_type->len @@ -523,9 +526,21 @@ ngx_rtmp_netcall_http_format_header(ngx_str_t *uri, ngx_str_t *host, } cl->buf = b; + cl->next = NULL; + + switch (method) { + case NGX_RTMP_NETCALL_HTTP_GET: + method_s = "GET"; + break; + case NGX_RTMP_NETCALL_HTTP_POST: + method_s = "POST"; + break; + default: + return NULL; + } b->last = ngx_snprintf(b->last, b->end - b->last, rq_tmpl, - uri, host, content_type, content_length); + method_s, uri, host, content_type, content_length); return cl; } diff --git a/ngx_rtmp_netcall_module.h b/ngx_rtmp_netcall_module.h index f6ea286..c6c03ab 100644 --- a/ngx_rtmp_netcall_module.h +++ b/ngx_rtmp_netcall_module.h @@ -20,6 +20,9 @@ typedef ngx_int_t (*ngx_rtmp_netcall_sink_pt)(ngx_rtmp_session_t *s, typedef ngx_int_t (*ngx_rtmp_netcall_handle_pt)(ngx_rtmp_session_t *s, void *arg, ngx_chain_t *in); +#define NGX_RTMP_NETCALL_HTTP_GET 1 +#define NGX_RTMP_NETCALL_HTTP_POST 2 + /* If handle is NULL then netcall is created detached * which means it's completely independent of RTMP @@ -48,9 +51,9 @@ ngx_int_t ngx_rtmp_netcall_create(ngx_rtmp_session_t *s, /* HTTP handling */ ngx_chain_t * ngx_rtmp_netcall_http_format_session(ngx_rtmp_session_t *s, ngx_pool_t *pool); -ngx_chain_t * ngx_rtmp_netcall_http_format_header(ngx_str_t *uri, - ngx_str_t *host, ngx_pool_t *pool, size_t content_length, - ngx_str_t *content_type); +ngx_chain_t * ngx_rtmp_netcall_http_format_header(ngx_int_t method, + ngx_str_t *uri, ngx_str_t *host, ngx_pool_t *pool, + size_t content_length, ngx_str_t *content_type); ngx_chain_t * ngx_rtmp_netcall_http_skip_header(ngx_chain_t *in); diff --git a/ngx_rtmp_notify_module.c b/ngx_rtmp_notify_module.c index 3d29e77..263b0fa 100644 --- a/ngx_rtmp_notify_module.c +++ b/ngx_rtmp_notify_module.c @@ -220,7 +220,7 @@ ngx_rtmp_notify_publish_create(ngx_rtmp_session_t *s, void *arg, b = ngx_create_temp_buf(pool, sizeof("&call=publish") + - sizeof("&addr=") + addr_text->len + + sizeof("&addr=") + addr_text->len *3 + sizeof("&name=") + name_len * 3 + sizeof("&type=") + type_len * 3 + 1 + args_len); @@ -250,7 +250,8 @@ ngx_rtmp_notify_publish_create(ngx_rtmp_session_t *s, void *arg, /* HTTP header */ url = nacf->url[NGX_RTMP_NOTIFY_PUBLISH]; - hl = ngx_rtmp_netcall_http_format_header(&url->uri, &url->host, + hl = ngx_rtmp_netcall_http_format_header(NGX_RTMP_NETCALL_HTTP_POST, + &url->uri, &url->host, pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), &ngx_rtmp_notify_urlencoded); @@ -301,7 +302,7 @@ ngx_rtmp_notify_play_create(ngx_rtmp_session_t *s, void *arg, b = ngx_create_temp_buf(pool, sizeof("&call=play") + - sizeof("&addr=") + addr_text->len + + sizeof("&addr=") + addr_text->len * 3 + sizeof("&name=") + name_len * 3 + sizeof("&start=&duration=&reset=") + NGX_OFF_T_LEN * 3 + 1 + args_len); @@ -332,7 +333,8 @@ ngx_rtmp_notify_play_create(ngx_rtmp_session_t *s, void *arg, /* HTTP header */ url = nacf->url[NGX_RTMP_NOTIFY_PLAY]; - hl = ngx_rtmp_netcall_http_format_header(&url->uri, &url->host, + hl = ngx_rtmp_netcall_http_format_header(NGX_RTMP_NETCALL_HTTP_POST, + &url->uri, &url->host, pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), &ngx_rtmp_notify_urlencoded); @@ -382,7 +384,7 @@ ngx_rtmp_notify_done_create(ngx_rtmp_session_t *s, void *arg, b = ngx_create_temp_buf(pool, sizeof("&call=") + cbname_len + - sizeof("&addr=") + addr_text->len + + sizeof("&addr=") + addr_text->len * 3 + sizeof("&name=") + name_len * 3 + 1 + args_len); if (b == NULL) { @@ -409,7 +411,8 @@ ngx_rtmp_notify_done_create(ngx_rtmp_session_t *s, void *arg, } /* HTTP header */ - hl = ngx_rtmp_netcall_http_format_header(&ds->url->uri, &ds->url->host, + hl = ngx_rtmp_netcall_http_format_header(NGX_RTMP_NETCALL_HTTP_POST, + &ds->url->uri, &ds->url->host, pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), &ngx_rtmp_notify_urlencoded); @@ -464,7 +467,7 @@ ngx_rtmp_notify_record_done_create(ngx_rtmp_session_t *s, void *arg, b = ngx_create_temp_buf(pool, sizeof("&call=record_done") + sizeof("&recorder=") + v->recorder.len + - sizeof("&addr=") + addr_text->len + + sizeof("&addr=") + addr_text->len *3 + sizeof("&name=") + name_len * 3 + sizeof("&path=") + v->path.len * 3 + + 1 + args_len); @@ -499,7 +502,8 @@ ngx_rtmp_notify_record_done_create(ngx_rtmp_session_t *s, void *arg, /* HTTP header */ url = nacf->url[NGX_RTMP_NOTIFY_RECORD_DONE]; - hl = ngx_rtmp_netcall_http_format_header(&url->uri, &url->host, + hl = ngx_rtmp_netcall_http_format_header(NGX_RTMP_NETCALL_HTTP_POST, + &url->uri, &url->host, pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), &ngx_rtmp_notify_urlencoded); diff --git a/ngx_rtmp_play_module.c b/ngx_rtmp_play_module.c index 792a386..6ec21d5 100644 --- a/ngx_rtmp_play_module.c +++ b/ngx_rtmp_play_module.c @@ -381,7 +381,7 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); - if (pacf == NULL || pacf->root.len == 0) { + if (pacf == NULL || (pacf->root.len == 0 && pacf->url == NULL)) { goto next; } @@ -458,8 +458,11 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) goto next; } + ctx->file.fd = NGX_INVALID_FILE; + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, - "play: %V", &ctx->fmt->name); + "play %s: %V", pacf->url ? "remote" : "local", + &ctx->fmt->name); sfx = &ctx->fmt->sfx; @@ -471,17 +474,26 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) sfx = &nosfx; } - p = ngx_snprintf(path, sizeof(path), "%V/%V%V", &pacf->root, &name, sfx); - *p = 0; - - ctx->file.fd = NGX_INVALID_FILE; - + /* remote? */ if (pacf->url) { + ctx->name.data = ngx_palloc(s->connection->pool, name.len + sfx->len); + if (ctx->name.data == NULL) { + return NGX_ERROR; + } + + p = ngx_sprintf(ctx->name.data, "%V%V", &name, sfx); + *p = 0; + + ctx->name.len = p - ctx->name.data; + return ngx_rtmp_play_open_remote(s, v); } /* open local */ + p = ngx_snprintf(path, sizeof(path), "%V/%V%V", &pacf->root, &name, sfx); + *p = 0; + ctx->file.fd = ngx_open_file(path, NGX_FILE_RDONLY, NGX_FILE_OPEN, NGX_FILE_DEFAULT_ACCESS); @@ -540,71 +552,50 @@ ngx_rtmp_play_remote_create(ngx_rtmp_session_t *s, void *arg, ngx_pool_t *pool) ngx_rtmp_play_t *v = arg; ngx_rtmp_play_app_conf_t *pacf; - ngx_chain_t *hl, *cl, *pl; - ngx_buf_t *b; + ngx_rtmp_play_ctx_t *ctx; + ngx_chain_t *hl; ngx_str_t *addr_text, uri; u_char *p; - size_t name_len, args_len; + size_t args_len, len; static ngx_str_t text_plain = ngx_string("text/plain"); pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); - /* common variables */ - cl = ngx_rtmp_netcall_http_format_session(s, pool); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); - if (cl == NULL) { - return NULL; - } - - /* publish variables */ - pl = ngx_alloc_chain_link(pool); - - if (pl == NULL) { - return NULL; - } - - name_len = ngx_strlen(v->name); args_len = ngx_strlen(v->args); addr_text = &s->connection->addr_text; - b = ngx_create_temp_buf(pool, - sizeof("&addr=") + addr_text->len + - 1 + args_len); - if (b == NULL) { + len = pacf->url->uri.len + ctx->name.len + + sizeof("?addr=") + addr_text->len * 3 + + 1 + args_len; + + uri.data = ngx_palloc(pool, len); + if (uri.data == NULL) { return NULL; } - pl->buf = b; - - b->last = ngx_cpymem(b->last, (u_char*)"&addr=", sizeof("&addr=") -1); - b->last = (u_char*)ngx_escape_uri(b->last, addr_text->data, - addr_text->len, 0); + p= uri.data; + p = ngx_cpymem(p, pacf->url->uri.data, pacf->url->uri.len); + p = ngx_cpymem(p, ctx->name.data, ctx->name.len); + p = ngx_cpymem(p, (u_char*)"?addr=", sizeof("&addr=") -1); + p = (u_char*)ngx_escape_uri(p, addr_text->data, addr_text->len, 0); if (args_len) { - *b->last++ = '&'; - b->last = (u_char *)ngx_cpymem(b->last, v->args, args_len); + *p++ = '&'; + p = (u_char *) ngx_cpymem(p, v->args, args_len); } - /* create uri */ - uri.len = pacf->url->uri.len + name_len; - uri.data = ngx_palloc(pool, uri.len); - - p = ngx_cpymem(uri.data, pacf->url->uri.data, pacf->url->uri.len); - ngx_memcpy(p, v->name, name_len); + uri.len = p - uri.data; /* HTTP header */ - hl = ngx_rtmp_netcall_http_format_header(&uri, &pacf->url->host, - pool, cl->buf->last - cl->buf->pos + (pl->buf->last - pl->buf->pos), - &text_plain); + hl = ngx_rtmp_netcall_http_format_header(NGX_RTMP_NETCALL_HTTP_GET, + &uri, &pacf->url->host, pool, 0, &text_plain); if (hl == NULL) { return NULL; } - hl->next = cl; - cl->next = pl; - pl->next = NULL; - return hl; } @@ -632,20 +623,23 @@ ngx_rtmp_play_remote_sink(ngx_rtmp_session_t *s, ngx_chain_t *in) ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); /* skip HTTP header */ - for (; in && ctx->ncrs != 2; in = in->next) { + while (in && ctx->ncrs != 2) { b = in->buf; for (; b->pos != b->last && ctx->ncrs != 2; ++b->pos) { switch (*b->pos) { case '\n': ++ctx->ncrs; - break; case '\r': break; default: ctx->ncrs = 0; } } + + if (b->pos == b->last) { + in = in->next; + } } /* write to temp file */ @@ -721,7 +715,8 @@ ngx_rtmp_play_open_remote(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) static char * ngx_rtmp_play_url(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { - ngx_rtmp_play_app_conf_t *pacf; + ngx_rtmp_play_app_conf_t *pacf = conf; + ngx_str_t url; ngx_url_t *u; size_t add; diff --git a/ngx_rtmp_play_module.h b/ngx_rtmp_play_module.h index cb768d1..5505009 100644 --- a/ngx_rtmp_play_module.h +++ b/ngx_rtmp_play_module.h @@ -40,6 +40,7 @@ typedef struct { ngx_rtmp_play_fmt_t *fmt; ngx_event_t send_evt; ngx_uint_t ncrs; + ngx_str_t name; } ngx_rtmp_play_ctx_t; From 4adc5f7487b0bb27cf027c3e792b00821ad30ada Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Tue, 23 Oct 2012 22:07:54 +0400 Subject: [PATCH 32/37] fixed uri in vod-http --- ngx_rtmp_play_module.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ngx_rtmp_play_module.c b/ngx_rtmp_play_module.c index 6ec21d5..72eaf7a 100644 --- a/ngx_rtmp_play_module.c +++ b/ngx_rtmp_play_module.c @@ -566,7 +566,7 @@ ngx_rtmp_play_remote_create(ngx_rtmp_session_t *s, void *arg, ngx_pool_t *pool) args_len = ngx_strlen(v->args); addr_text = &s->connection->addr_text; - len = pacf->url->uri.len + ctx->name.len + + len = pacf->url->uri.len + 1 + ctx->name.len + sizeof("?addr=") + addr_text->len * 3 + 1 + args_len; @@ -575,9 +575,14 @@ ngx_rtmp_play_remote_create(ngx_rtmp_session_t *s, void *arg, ngx_pool_t *pool) return NULL; } - p= uri.data; + p = uri.data; p = ngx_cpymem(p, pacf->url->uri.data, pacf->url->uri.len); + + if (p == uri.data || p[-1] != '/') { + *p++ = '/'; + } + p = ngx_cpymem(p, ctx->name.data, ctx->name.len); p = ngx_cpymem(p, (u_char*)"?addr=", sizeof("&addr=") -1); p = (u_char*)ngx_escape_uri(p, addr_text->data, addr_text->len, 0); From 58246359382dd62a9da67a822011369d19161681 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 24 Oct 2012 14:51:18 +0400 Subject: [PATCH 33/37] update TODO --- TODO | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/TODO b/TODO index 9c04cd7..e8fcd3f 100644 --- a/TODO +++ b/TODO @@ -1,17 +1,12 @@ -- Auto-pushing pulled stream +- option to start live streaming with a keyframe -- Pull secondary address support +- interleaved mode -- Binary search in play module +- vod seamless pause -- More Wiki docs +- DNS round-robin url -Style: -====== +- a/v merge from different sources -- Move out & merge stream ids from live & cmd modules - -- Clean cmd module: - * make shortcuts for status messages - * move protocol message sending to live/play modules +- multiple streams perconnection From 98d959ac5374bd14123f53318e6c8a8d481ffd92 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 24 Oct 2012 17:36:24 +0400 Subject: [PATCH 34/37] fixed flv seeking --- ngx_rtmp_flv_module.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ngx_rtmp_flv_module.c b/ngx_rtmp_flv_module.c index e3b92ce..cea72ec 100644 --- a/ngx_rtmp_flv_module.c +++ b/ngx_rtmp_flv_module.c @@ -604,6 +604,8 @@ ngx_rtmp_flv_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) "flv: seek timestamp=%ui", timestamp); ctx->start_timestamp = timestamp; + ctx->offset = -1; + ctx->msg_mask = 0; return NGX_OK; } From 924f2c1b56d95c0094435befa2b2d5c78ea39cf6 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 24 Oct 2012 18:50:42 +0400 Subject: [PATCH 35/37] added resetting epoch in seek --- ngx_rtmp_flv_module.c | 5 ++++- ngx_rtmp_mp4_module.c | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ngx_rtmp_flv_module.c b/ngx_rtmp_flv_module.c index cea72ec..148bac8 100644 --- a/ngx_rtmp_flv_module.c +++ b/ngx_rtmp_flv_module.c @@ -44,6 +44,7 @@ typedef struct { #define NGX_RTMP_FLV_BUFFER (1024*1024) #define NGX_RTMP_FLV_DEFAULT_BUFLEN 1000 +#define NGX_RTMP_FLV_BUFLEN_ADDON 1000 #define NGX_RTMP_FLV_TAG_HEADER 11 #define NGX_RTMP_FLV_DATA_OFFSET 13 @@ -526,7 +527,8 @@ next: return NGX_OK; } - buflen = (s->buflen ? s->buflen : NGX_RTMP_FLV_DEFAULT_BUFLEN); + buflen = (s->buflen ? s->buflen + NGX_RTMP_FLV_BUFLEN_ADDON: + NGX_RTMP_FLV_DEFAULT_BUFLEN); end_timestamp = (ngx_current_msec - ctx->epoch) + ctx->start_timestamp + buflen; @@ -604,6 +606,7 @@ ngx_rtmp_flv_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) "flv: seek timestamp=%ui", timestamp); ctx->start_timestamp = timestamp; + ctx->epoch = ngx_current_msec; ctx->offset = -1; ctx->msg_mask = 0; diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 0c95849..cd466b0 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -196,6 +196,7 @@ ngx_rtmp_mp4_from_rtmp_timestamp(ngx_rtmp_mp4_track_t *t, uint32_t ts) #define NGX_RTMP_MP4_DEFAULT_BUFLEN 1000 +#define NGX_RTMP_MP4_BUFLEN_ADDON 1000 static u_char ngx_rtmp_mp4_buffer[1024*1024]; @@ -1959,7 +1960,8 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t *ts) return rc; } - buflen = (s->buflen ? s->buflen : NGX_RTMP_MP4_DEFAULT_BUFLEN); + buflen = (s->buflen ? s->buflen + NGX_RTMP_MP4_BUFLEN_ADDON: + NGX_RTMP_MP4_DEFAULT_BUFLEN); t = ctx->tracks; @@ -2263,6 +2265,7 @@ ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) } ctx->start_timestamp = timestamp; + ctx->epoch = ngx_current_msec; return ngx_rtmp_mp4_reset(s); } From d143af86de071f077fed174bf14e12c312aaaabe Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 24 Oct 2012 21:59:33 +0400 Subject: [PATCH 36/37] fixed mp4 chunk offset seek; implemented keyframe lookup when seeking mp4 --- ngx_rtmp_mp4_module.c | 54 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index cd466b0..fbb2661 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -1240,7 +1240,8 @@ ngx_rtmp_mp4_seek_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, return NGX_ERROR; } - cr->time_count = (timestamp - cr->timestamp) / ngx_rtmp_r32(te->sample_delta); + cr->time_count = (timestamp - cr->timestamp) / + ngx_rtmp_r32(te->sample_delta); cr->timestamp += ngx_rtmp_r32(te->sample_delta) * cr->time_count; cr->pos += cr->time_count; @@ -1523,13 +1524,20 @@ static ngx_int_t ngx_rtmp_mp4_seek_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) { ngx_rtmp_mp4_cursor_t *cr; + ngx_uint_t pos; cr = &t->cursor; + if (cr->chunk_count > cr->pos) { + return NGX_ERROR; + } + if (t->sizes) { if (t->sizes->sample_size) { cr->size = ngx_rtmp_r32(t->sizes->sample_size); + cr->offset += cr->size * cr->chunk_count; + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: track#%ui seek size fix=%uz", t->id, cr->size); @@ -1546,6 +1554,10 @@ ngx_rtmp_mp4_seek_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) return NGX_ERROR; } + for (pos = 1; pos <= cr->chunk_count; ++pos) { + cr->offset += ngx_rtmp_r32(t->sizes->entries[cr->pos - pos]); + } + cr->size_pos = cr->pos; cr->size = ngx_rtmp_r32(t->sizes->entries[cr->size_pos]); @@ -1624,6 +1636,7 @@ ngx_rtmp_mp4_seek_key(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) { ngx_rtmp_mp4_cursor_t *cr; uint32_t *ke; + ngx_int_t dpos; cr = &t->cursor; @@ -1632,7 +1645,7 @@ ngx_rtmp_mp4_seek_key(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) } while (cr->key_pos < ngx_rtmp_r32(t->keys->entry_count)) { - if (ngx_rtmp_r32(t->keys->entries[cr->key_pos]) >= cr->pos) { + if (ngx_rtmp_r32(t->keys->entries[cr->key_pos]) > cr->pos) { break; } @@ -1648,7 +1661,18 @@ ngx_rtmp_mp4_seek_key(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) } ke = &t->keys->entries[cr->key_pos]; - cr->key = (cr->pos + 1 == ngx_rtmp_r32(*ke)); + /*cr->key = (cr->pos + 1 == ngx_rtmp_r32(*ke));*/ + + /* distance to the next keyframe */ + dpos = ngx_rtmp_r32(*ke) - cr->pos - 1; + cr->key = 1; + + /* TODO: range version needed */ + for (; dpos > 0; --dpos) { + ngx_rtmp_mp4_next_time(s, t); + } + +/* cr->key = (cr->pos + 1 == ngx_rtmp_r32(*ke));*/ ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: track#%ui seek key[%ui/%uD][%ui/%uD]=%s", @@ -2246,6 +2270,7 @@ static ngx_int_t ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) { ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; ngx_uint_t n; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); @@ -2258,6 +2283,29 @@ ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) "mp4: seek timestamp=%ui", timestamp); for (n = 0; n < ctx->ntracks; ++n) { + t = &ctx->tracks[n]; + + if (t->type != NGX_RTMP_MSG_VIDEO) { + continue; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek video", n); + + ngx_rtmp_mp4_seek_track(s, t, timestamp); + + timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(t, t->cursor.timestamp); + + break; + } + + for (n = 0; n < ctx->ntracks; ++n) { + t = &ctx->tracks[n]; + + if (t->type == NGX_RTMP_MSG_VIDEO) { + continue; + } + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: track#%ui seek", n); From d3ce27a96c13db641cc7aee0653739403a2c4c3a Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 25 Oct 2012 18:17:08 +0400 Subject: [PATCH 37/37] fixed late publishing --- ngx_rtmp_live_module.c | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/ngx_rtmp_live_module.c b/ngx_rtmp_live_module.c index 45207ac..84ba6db 100644 --- a/ngx_rtmp_live_module.c +++ b/ngx_rtmp_live_module.c @@ -202,18 +202,20 @@ ngx_rtmp_live_join(ngx_rtmp_session_t *s, u_char *name, } ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); - if (ctx == NULL) { - ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_live_ctx_t)); - ctx->session = s; - ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_live_module); - } - - if (ctx->stream) { + 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); + } + + ngx_memzero(ctx, sizeof(*ctx)); + ctx->session = s; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "live: join '%s'", name); @@ -378,6 +380,12 @@ ngx_rtmp_live_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ctx->last_audio = ch.timestamp; last_offset = offsetof(ngx_rtmp_live_ctx_t, last_audio); } + + if ((ctx->msg_mask & (1 << h->type)) == 0) { + lh.timestamp = ch.timestamp; + ctx->msg_mask |= (1 << h->type); + } + lh.csid = ch.csid; diff_timestamp = ch.timestamp - lh.timestamp;