commit 9f28fed00a64cf51dd2655211d59dbf2dbbf56ca Author: mxd Date: Sun May 17 11:34:54 2026 +0800 Upload Modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bab4d51 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2026 mxdyeah + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..493e14e --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +# nginx-modules + +Useful third-party modules for Nginx compilation. + +Most modules are recommended to be built as dynamic modules unless you explicitly require static linking. + +--- + +## ngx_dav_ext_module + +WebDAV extension module for Nginx. + +Requires the built-in `http_dav_module`. + +### Build + +```bash +./configure \ + --with-http_dav_module \ + --add-dynamic-module=/path/to/ngx_dav_ext_module +``` + +### Notes + +You must enable: + +```bash +--with-http_dav_module +``` + +otherwise this module will not compile. + +### License + +BSD 2-Clause License. + +--- + +## ngx_fancyindex + +Fancy directory listing module for Nginx. + +Provides a customizable and visually improved autoindex page. + +### Build + +```bash +./configure \ + --add-dynamic-module=/path/to/ngx_fancyindex +``` + +### Optional + +Can optionally work with: + +```bash +--with-http_addition_module +``` + +### License + +BSD 2-Clause License. + +--- + +## ngx_http_flv_module + +HTTP-FLV / RTMP / HLS streaming server module for Nginx. + +Based on: + +- nginx-rtmp-module + +Provides: + +- RTMP streaming +- HTTP-FLV +- HLS +- Relay +- Recording +- Live streaming support + +### Important + +This module already includes functionality from: + +- nginx-rtmp-module + +Do NOT compile both together. + +### Build + +```bash +./configure \ + --add-module=/path/to/ngx_http_flv_module +``` + +### Notes + +This module is typically built statically due to its deeper integration with the Nginx streaming pipeline. + +### License + +BSD 2-Clause License. + +--- + +## ngx_http_geoip2_module + +GeoIP2 module for Nginx using MaxMind GeoIP2 databases. + +Supports: + +- IPv4 +- IPv6 +- Country / City / ASN lookup + +### Dependencies + +Requires: + +- libmaxminddb + +### Build + +```bash +./configure \ + --add-dynamic-module=/path/to/ngx_http_geoip2_module +``` + +### License + +BSD 2-Clause License. + +--- + +## ngx_http_limit_req_module + +Extended limit request module for Nginx. + +Allows reading and writing request limits based on: + +https://nginx.org/en/docs/http/ngx_http_limit_req_module.html + +### Build + +```bash +./configure \ + --add-module=/path/to/ngx_http_limit_req_module +``` + +### Notes + +This module is recommended to be built statically because it interacts closely with request limiting internals and shared memory zones. + +### License + +BSD 2-Clause License. \ No newline at end of file diff --git a/ngx_dav_ext_module/LICENSE b/ngx_dav_ext_module/LICENSE new file mode 100644 index 0000000..c68a7d4 --- /dev/null +++ b/ngx_dav_ext_module/LICENSE @@ -0,0 +1,22 @@ +Copyright (C) 2012-2018 Roman Arutyunyan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ngx_dav_ext_module/README.rst b/ngx_dav_ext_module/README.rst new file mode 100644 index 0000000..092056e --- /dev/null +++ b/ngx_dav_ext_module/README.rst @@ -0,0 +1,188 @@ +******************** +nginx-dav-ext-module +******************** + +nginx_ WebDAV_ PROPFIND,OPTIONS,LOCK,UNLOCK support. + +.. contents:: + + +About +===== + +The standard ngx_http_dav_module_ provides partial WebDAV_ implementation and +only supports GET,HEAD,PUT,DELETE,MKCOL,COPY,MOVE methods. + +For full WebDAV_ support in nginx_ you need to enable the standard +ngx_http_dav_module_ as well as this module for the missing methods. + + +Build +===== + +Building nginx_ with the module: + +.. code-block:: bash + + # static module + $ ./configure --with-http_dav_module --add-module=/path/to/nginx-dav-ext-module + + # dynamic module + $ ./configure --with-http_dav_module --add-dynamic-module=/path/to/nginx-dav-ext-module + +Trying to compile nginx_ with this module but without ngx_http_dav_module_ will +result in compilation error. + + +Requirements +============ + +- nginx_ version >= 1.13.4 +- ``libxml2`` + ``libxslt`` + +The ``libxslt`` library is technically redundant and is only required since this +combination is supported by nginx_ for the xslt module. +Using builtin nginx mechanisms for linking against third-party libraries +brings certain compatibility benefits. +However this redundancy can be easily eliminated in the ``config`` file. + + +Testing +======= + +The module tests require standard nginx-tests_ and Perl ``HTTP::DAV`` library. + +.. code-block:: bash + + $ export PERL5LIB=/path/to/nginx-tests/lib + $ export TEST_NGINX_BINARY=/path/to/nginx + $ prove t + + +Locking +======= + +- Only the exclusive write locks are supported, which is the only type of locks + described in the WebDAV_ specification. + +- All currently held locks are kept in a list. + Checking if an object is constrained by a lock requires O(n) operations. + A huge number of simultaneously held locks may degrade performance. + Thus it is not recommended to have a large lock timeout which would increase + the number of locks. + + +Directives +========== + +dav_ext_methods +--------------- + +========== ==== +*Syntax:* ``dav_ext_methods [PROPFIND] [OPTIONS] [LOCK] [UNLOCK]`` +*Context:* http, server, location +========== ==== + +Enables support for the specified WebDAV methods in the current scope. + +dav_ext_lock_zone +----------------- + +========== ==== +*Syntax:* ``dav_ext_lock_zone zone=NAME:SIZE [timeout=TIMEOUT]`` +*Context:* http +========== ==== + +Defines a shared zone for WebDAV locks with specified NAME and SIZE. +Also, defines a lock expiration TIMEOUT. +Default lock timeout value is 1 minute. + + +dav_ext_lock +------------ + +========== ==== +*Syntax:* ``dav_ext_lock zone=NAME`` +*Context:* http, server, location +========== ==== + +Enables WebDAV locking in the specified scope. +Locks are stored in the shared zone specified by NAME. +This zone must be defined with the ``dav_ext_lock_zone`` directive. + +Note that even though this directive enables locking capabilities in the +current scope, HTTP methods LOCK and UNLOCK should also be explicitly specified +in the ``dav_ext_methods``. + + +Example 1 +========= + +Simple lockless example:: + + location / { + root /data/www; + + dav_methods PUT DELETE MKCOL COPY MOVE; + dav_ext_methods PROPFIND OPTIONS; + } + + +Example 2 +========= + +WebDAV with locking:: + + http { + dav_ext_lock_zone zone=foo:10m; + + ... + + server { + ... + + location / { + root /data/www; + + dav_methods PUT DELETE MKCOL COPY MOVE; + dav_ext_methods PROPFIND OPTIONS LOCK UNLOCK; + dav_ext_lock zone=foo; + } + } + } + + +Example 3 +========= + +WebDAV with locking which works with MacOS client:: + + http { + dav_ext_lock_zone zone=foo:10m; + + ... + + server { + ... + + location / { + root /data/www; + + # enable creating directories without trailing slash + set $x $uri$request_method; + if ($x ~ [^/]MKCOL$) { + rewrite ^(.*)$ $1/; + } + + dav_methods PUT DELETE MKCOL COPY MOVE; + dav_ext_methods PROPFIND OPTIONS LOCK UNLOCK; + dav_ext_lock zone=foo; + } + } + } + +.. _ngx_http_dav_module: http://nginx.org/en/docs/http/ngx_http_dav_module.html +.. _nginx-tests: http://hg.nginx.org/nginx-tests +.. _nginx: http://nginx.org +.. _WebDAV: https://tools.ietf.org/html/rfc4918 +.. _`RFC4918 If Header`: https://tools.ietf.org/html/rfc4918#section-10.4 diff --git a/ngx_dav_ext_module/config b/ngx_dav_ext_module/config new file mode 100644 index 0000000..91ae1b3 --- /dev/null +++ b/ngx_dav_ext_module/config @@ -0,0 +1,17 @@ +ngx_addon_name=ngx_http_dav_ext_module + +ngx_module_type=HTTP +ngx_module_name=ngx_http_dav_ext_module + +# nginx has robust builtin support for linking against +# libxml2+libxslt. This is definitelty the right way to go if +# building nginx with the xslt module, in which case libxslt will +# be linked anyway. In other cases libxslt is just redundant. +# If that's a big deal, libxml2 can be linked directly: +# ngx_module_libs=-lxml2 + +ngx_module_libs=LIBXSLT + +ngx_module_srcs="$ngx_addon_dir/ngx_http_dav_ext_module.c" + +. auto/module diff --git a/ngx_dav_ext_module/ngx_http_dav_ext_module.c b/ngx_dav_ext_module/ngx_http_dav_ext_module.c new file mode 100644 index 0000000..0d6d067 --- /dev/null +++ b/ngx_dav_ext_module/ngx_http_dav_ext_module.c @@ -0,0 +1,2181 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include +#include + + +#define NGX_HTTP_DAV_EXT_OFF 2 + +#define NGX_HTTP_DAV_EXT_PREALLOCATE 50 + +#define NGX_HTTP_DAV_EXT_NODE_PROPFIND 0x01 +#define NGX_HTTP_DAV_EXT_NODE_PROP 0x02 +#define NGX_HTTP_DAV_EXT_NODE_PROPNAME 0x04 +#define NGX_HTTP_DAV_EXT_NODE_ALLPROP 0x08 + +#define NGX_HTTP_DAV_EXT_PROP_DISPLAYNAME 0x01 +#define NGX_HTTP_DAV_EXT_PROP_GETCONTENTLENGTH 0x02 +#define NGX_HTTP_DAV_EXT_PROP_GETLASTMODIFIED 0x04 +#define NGX_HTTP_DAV_EXT_PROP_RESOURCETYPE 0x08 +#define NGX_HTTP_DAV_EXT_PROP_LOCKDISCOVERY 0x10 +#define NGX_HTTP_DAV_EXT_PROP_SUPPORTEDLOCK 0x20 + +#define NGX_HTTP_DAV_EXT_PROP_ALL 0x7f +#define NGX_HTTP_DAV_EXT_PROP_NAMES 0x80 + + +typedef struct { + ngx_str_t uri; + ngx_str_t name; + time_t mtime; + off_t size; + + time_t lock_expire; + ngx_str_t lock_root; + uint32_t lock_token; + + unsigned dir:1; + unsigned lock_supported:1; + unsigned lock_infinite:1; +} ngx_http_dav_ext_entry_t; + + +typedef struct { + ngx_uint_t nodes; + ngx_uint_t props; +} ngx_http_dav_ext_xml_ctx_t; + + +typedef struct { + ngx_uint_t methods; + ngx_shm_zone_t *shm_zone; +} ngx_http_dav_ext_loc_conf_t; + + +typedef struct { + ngx_queue_t queue; + uint32_t token; + time_t expire; + ngx_uint_t infinite; /* unsigned infinite:1; */ + size_t len; + u_char data[1]; +} ngx_http_dav_ext_node_t; + + +typedef struct { + ngx_queue_t queue; +} ngx_http_dav_ext_lock_sh_t; + + +typedef struct { + time_t timeout; + ngx_slab_pool_t *shpool; + ngx_http_dav_ext_lock_sh_t *sh; +} ngx_http_dav_ext_lock_t; + + +static ngx_int_t ngx_http_dav_ext_precontent_handler(ngx_http_request_t *r); +static ngx_int_t ngx_http_dav_ext_strip_uri(ngx_http_request_t *r, + ngx_str_t *uri); +static ngx_int_t ngx_http_dav_ext_verify_lock(ngx_http_request_t *r, + ngx_str_t *uri, ngx_uint_t delete_lock); +static ngx_http_dav_ext_node_t *ngx_http_dav_ext_lock_lookup( + ngx_http_request_t *r, ngx_http_dav_ext_lock_t *lock, ngx_str_t *uri, + ngx_int_t depth); + +static ngx_int_t ngx_http_dav_ext_content_handler(ngx_http_request_t *r); +static void ngx_http_dav_ext_propfind_handler(ngx_http_request_t *r); +static void ngx_http_dav_ext_propfind_xml_start(void *data, + const xmlChar *localname, const xmlChar *prefix, const xmlChar *uri, + int nb_namespaces, const xmlChar **namespaces, int nb_attributes, + int nb_defaulted, const xmlChar **attributes); +static void ngx_http_dav_ext_propfind_xml_end(void *data, + const xmlChar *localname, const xmlChar *prefix, const xmlChar *uri); +static ngx_int_t ngx_http_dav_ext_propfind(ngx_http_request_t *r, + ngx_uint_t props); +static ngx_int_t ngx_http_dav_ext_set_locks(ngx_http_request_t *r, + ngx_http_dav_ext_entry_t *entry); +static ngx_int_t ngx_http_dav_ext_propfind_response(ngx_http_request_t *r, + ngx_array_t *entries, ngx_uint_t props); +static ngx_int_t ngx_http_dav_ext_lock_handler(ngx_http_request_t *r); +static ngx_int_t ngx_http_dav_ext_lock_response(ngx_http_request_t *r, + ngx_uint_t status, time_t timeout, ngx_uint_t depth, uint32_t token); +static ngx_int_t ngx_http_dav_ext_unlock_handler(ngx_http_request_t *r); + +static ngx_int_t ngx_http_dav_ext_depth(ngx_http_request_t *r, + ngx_int_t default_depth); +static uint32_t ngx_http_dav_ext_lock_token(ngx_http_request_t *r); +static uint32_t ngx_http_dav_ext_if(ngx_http_request_t *r, ngx_str_t *uri); +static uintptr_t ngx_http_dav_ext_format_propfind(ngx_http_request_t *r, + u_char *dst, ngx_http_dav_ext_entry_t *entry, ngx_uint_t props); +static uintptr_t ngx_http_dav_ext_format_lockdiscovery(ngx_http_request_t *r, + u_char *dst, ngx_http_dav_ext_entry_t *entry); +static uintptr_t ngx_http_dav_ext_format_token(u_char *dst, uint32_t token, + ngx_uint_t brackets); + +static ngx_int_t ngx_http_dav_ext_init_zone(ngx_shm_zone_t *shm_zone, + void *data); +static void *ngx_http_dav_ext_create_loc_conf(ngx_conf_t *cf); +static char *ngx_http_dav_ext_merge_loc_conf(ngx_conf_t *cf, void *parent, + void *child); +static char *ngx_http_dav_ext_lock_zone(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_http_dav_ext_lock(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static ngx_int_t ngx_http_dav_ext_init(ngx_conf_t *cf); + + +static ngx_conf_bitmask_t ngx_http_dav_ext_methods_mask[] = { + { ngx_string("off"), NGX_HTTP_DAV_EXT_OFF }, + { ngx_string("propfind"), NGX_HTTP_PROPFIND }, + { ngx_string("options"), NGX_HTTP_OPTIONS }, + { ngx_string("lock"), NGX_HTTP_LOCK }, + { ngx_string("unlock"), NGX_HTTP_UNLOCK }, + { ngx_null_string, 0 } +}; + + +static ngx_command_t ngx_http_dav_ext_commands[] = { + + { ngx_string("dav_ext_methods"), + 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_http_dav_ext_loc_conf_t, methods), + &ngx_http_dav_ext_methods_mask }, + + { ngx_string("dav_ext_lock_zone"), + NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE12, + ngx_http_dav_ext_lock_zone, + 0, + 0, + NULL }, + + { ngx_string("dav_ext_lock"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_http_dav_ext_lock, + NGX_HTTP_LOC_CONF_OFFSET, + 0, + NULL }, + + ngx_null_command +}; + + +static ngx_http_module_t ngx_http_dav_ext_module_ctx = { + NULL, /* preconfiguration */ + ngx_http_dav_ext_init, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_http_dav_ext_create_loc_conf, /* create location configuration */ + ngx_http_dav_ext_merge_loc_conf, /* merge location configuration */ +}; + + +ngx_module_t ngx_http_dav_ext_module = { + NGX_MODULE_V1, + &ngx_http_dav_ext_module_ctx, /* module context */ + ngx_http_dav_ext_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 +}; + + +static ngx_int_t +ngx_http_dav_ext_precontent_handler(ngx_http_request_t *r) +{ + ngx_str_t uri; + ngx_int_t rc; + ngx_uint_t delete_lock; + ngx_table_elt_t *dest; + ngx_http_dav_ext_loc_conf_t *dlcf; + + dlcf = ngx_http_get_module_loc_conf(r, ngx_http_dav_ext_module); + + if (dlcf->shm_zone == NULL) { + return NGX_DECLINED; + } + + if (r->method & (NGX_HTTP_PUT|NGX_HTTP_DELETE|NGX_HTTP_MKCOL|NGX_HTTP_MOVE)) + { + delete_lock = (r->method & (NGX_HTTP_DELETE|NGX_HTTP_MOVE)) ? 1 : 0; + + rc = ngx_http_dav_ext_verify_lock(r, &r->uri, delete_lock); + if (rc != NGX_OK) { + return rc; + } + } + + if (r->method & (NGX_HTTP_MOVE|NGX_HTTP_COPY)) { + dest = r->headers_in.destination; + if (dest == NULL) { + return NGX_DECLINED; + } + + uri.data = dest->value.data; + uri.len = dest->value.len; + + if (ngx_http_dav_ext_strip_uri(r, &uri) != NGX_OK) { + return NGX_DECLINED; + } + + rc = ngx_http_dav_ext_verify_lock(r, &uri, 0); + if (rc != NGX_OK) { + return rc; + } + } + + return NGX_DECLINED; +} + + +static ngx_int_t +ngx_http_dav_ext_strip_uri(ngx_http_request_t *r, ngx_str_t *uri) +{ + u_char *p, *last, *host; + size_t len; + + if (uri->data[0] == '/') { + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext strip uri:\"%V\" unchanged", uri); + return NGX_OK; + } + + len = r->headers_in.server.len; + + if (len == 0) { + goto failed; + } + +#if (NGX_HTTP_SSL) + + if (r->connection->ssl) { + if (ngx_strncmp(uri->data, "https://", sizeof("https://") - 1) != 0) { + goto failed; + } + + host = uri->data + sizeof("https://") - 1; + + } else +#endif + { + if (ngx_strncmp(uri->data, "http://", sizeof("http://") - 1) != 0) { + goto failed; + } + + host = uri->data + sizeof("http://") - 1; + } + + if (ngx_strncmp(host, r->headers_in.server.data, len) != 0) { + goto failed; + } + + last = uri->data + uri->len; + + for (p = host + len; p != last; p++) { + if (*p == '/') { + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext strip uri \"%V\" \"%*s\"", + uri, last - p, p); + + uri->data = p; + uri->len = last - p; + + return NGX_OK; + } + } + +failed: + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext strip uri \"%V\" failed", uri); + + return NGX_DECLINED; +} + + +static ngx_int_t +ngx_http_dav_ext_verify_lock(ngx_http_request_t *r, ngx_str_t *uri, + ngx_uint_t delete_lock) +{ + uint32_t token; + ngx_http_dav_ext_node_t *node; + ngx_http_dav_ext_lock_t *lock; + ngx_http_dav_ext_loc_conf_t *dlcf; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext verify lock \"%V\"", uri); + + token = ngx_http_dav_ext_if(r, uri); + + dlcf = ngx_http_get_module_loc_conf(r, ngx_http_dav_ext_module); + lock = dlcf->shm_zone->data; + + ngx_shmtx_lock(&lock->shpool->mutex); + + node = ngx_http_dav_ext_lock_lookup(r, lock, uri, -1); + if (node == NULL) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return NGX_OK; + } + + if (token == 0) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return 423; /* Locked */ + } + + if (token != node->token) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return NGX_HTTP_PRECONDITION_FAILED; + } + + /* + * RFC4918: + * If a request causes the lock-root of any lock to become an + * unmapped URL, then the lock MUST also be deleted by that request. + */ + + if (delete_lock && node->len == uri->len) { + ngx_queue_remove(&node->queue); + ngx_slab_free_locked(lock->shpool, node); + } + + ngx_shmtx_unlock(&lock->shpool->mutex); + + return NGX_OK; +} + + +static ngx_http_dav_ext_node_t * +ngx_http_dav_ext_lock_lookup(ngx_http_request_t *r, + ngx_http_dav_ext_lock_t *lock, ngx_str_t *uri, ngx_int_t depth) +{ + time_t now; + ngx_queue_t *q; + ngx_http_dav_ext_node_t *node; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext lock lookup \"%V\"", uri); + + if (uri->len == 0) { + return NULL; + } + + now = ngx_time(); + + while (!ngx_queue_empty(&lock->sh->queue)) { + q = ngx_queue_head(&lock->sh->queue); + node = (ngx_http_dav_ext_node_t *) q; + + if (node->expire >= now) { + break; + } + + ngx_queue_remove(q); + ngx_slab_free_locked(lock->shpool, node); + } + + for (q = ngx_queue_head(&lock->sh->queue); + q != ngx_queue_sentinel(&lock->sh->queue); + q = ngx_queue_next(q)) + { + node = (ngx_http_dav_ext_node_t *) q; + + if (uri->len >= node->len) { + if (ngx_memcmp(uri->data, node->data, node->len)) { + continue; + } + + if (uri->len > node->len) { + if (node->data[node->len - 1] != '/') { + continue; + } + + if (!node->infinite + && ngx_strlchr(uri->data + node->len, + uri->data + uri->len - 1, '/')) + { + continue; + } + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext lock found \"%*s\"", + node->len, node->data); + + return node; + } + + /* uri->len < node->len */ + + if (depth >= 0) { + if (ngx_memcmp(node->data, uri->data, uri->len)) { + continue; + } + + if (uri->data[uri->len - 1] != '/') { + continue; + } + + if (depth == 0 + && ngx_strlchr(node->data + uri->len, + node->data + node->len - 1, '/')) + { + continue; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext lock found \"%*s\"", + node->len, node->data); + + return node; + } + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext lock not found"); + + return NULL; +} + + +static ngx_int_t +ngx_http_dav_ext_content_handler(ngx_http_request_t *r) +{ + ngx_int_t rc; + ngx_table_elt_t *h; + ngx_http_dav_ext_loc_conf_t *dlcf; + + dlcf = ngx_http_get_module_loc_conf(r, ngx_http_dav_ext_module); + + if (!(r->method & dlcf->methods)) { + return NGX_DECLINED; + } + + switch (r->method) { + + case NGX_HTTP_PROPFIND: + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext propfind"); + + rc = ngx_http_read_client_request_body(r, + ngx_http_dav_ext_propfind_handler); + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + return rc; + } + + return NGX_DONE; + + case NGX_HTTP_OPTIONS: + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext options"); + + rc = ngx_http_discard_request_body(r); + + if (rc != NGX_OK) { + return rc; + } + + h = ngx_list_push(&r->headers_out.headers); + if (h == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ngx_str_set(&h->key, "DAV"); + h->value.len = 1; + h->value.data = (u_char *) (dlcf->shm_zone ? "2" : "1"); + h->hash = 1; + + h = ngx_list_push(&r->headers_out.headers); + if (h == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + /* XXX */ + ngx_str_set(&h->key, "Allow"); + ngx_str_set(&h->value, + "GET,HEAD,PUT,DELETE,MKCOL,COPY,MOVE,PROPFIND,OPTIONS,LOCK,UNLOCK"); + h->hash = 1; + + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = 0; + + rc = ngx_http_send_header(r); + + if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { + return rc; + } + + return ngx_http_send_special(r, NGX_HTTP_LAST); + + case NGX_HTTP_LOCK: + + if (dlcf->shm_zone == NULL) { + return NGX_HTTP_NOT_ALLOWED; + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext lock"); + + /* + * Body is expected to carry the requested lock type, but + * since we only support write/exclusive locks, we ignore it. + * Ideally we could throw an error if a lock of another type + * is requested, but the amount of work required for that is + * not worth it. + */ + + rc = ngx_http_discard_request_body(r); + + if (rc != NGX_OK) { + return rc; + } + + return ngx_http_dav_ext_lock_handler(r); + + case NGX_HTTP_UNLOCK: + + if (dlcf->shm_zone == NULL) { + return NGX_HTTP_NOT_ALLOWED; + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext unlock"); + + rc = ngx_http_discard_request_body(r); + + if (rc != NGX_OK) { + return rc; + } + + return ngx_http_dav_ext_unlock_handler(r); + } + + return NGX_DECLINED; +} + + +static void +ngx_http_dav_ext_propfind_handler(ngx_http_request_t *r) +{ + off_t len; + ngx_buf_t *b; + ngx_chain_t *cl; + xmlSAXHandler sax; + xmlParserCtxtPtr pctx; + ngx_http_dav_ext_xml_ctx_t xctx; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext propfind handler"); + + ngx_memzero(&xctx, sizeof(ngx_http_dav_ext_xml_ctx_t)); + ngx_memzero(&sax, sizeof(xmlSAXHandler)); + + sax.initialized = XML_SAX2_MAGIC; + sax.startElementNs = ngx_http_dav_ext_propfind_xml_start; + sax.endElementNs = ngx_http_dav_ext_propfind_xml_end; + + pctx = xmlCreatePushParserCtxt(&sax, &xctx, NULL, 0, NULL); + if (pctx == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "xmlCreatePushParserCtxt() failed"); + ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + len = 0; + + for (cl = r->request_body->bufs; cl; cl = cl->next) { + b = cl->buf; + + if (b->in_file) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "PROPFIND client body is in file, " + "you may want to increase client_body_buffer_size"); + xmlFreeParserCtxt(pctx); + ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + if (ngx_buf_special(b)) { + continue; + } + + len += b->last - b->pos; + + if (xmlParseChunk(pctx, (const char *) b->pos, b->last - b->pos, + b->last_buf)) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "xmlParseChunk() failed"); + xmlFreeParserCtxt(pctx); + ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST); + return; + } + } + + xmlFreeParserCtxt(pctx); + + if (len == 0) { + + /* + * For easier debugging treat bodiless requests + * as if they expect all properties. + */ + + xctx.props = NGX_HTTP_DAV_EXT_PROP_ALL; + } + + ngx_http_finalize_request(r, ngx_http_dav_ext_propfind(r, xctx.props)); +} + + +static void +ngx_http_dav_ext_propfind_xml_start(void *data, const xmlChar *localname, + const xmlChar *prefix, const xmlChar *uri, int nb_namespaces, + const xmlChar **namespaces, int nb_attributes, int nb_defaulted, + const xmlChar **attributes) +{ + ngx_http_dav_ext_xml_ctx_t *xctx = data; + + if (ngx_strcmp(localname, "propfind") == 0) { + xctx->nodes ^= NGX_HTTP_DAV_EXT_NODE_PROPFIND; + } + + if (ngx_strcmp(localname, "prop") == 0) { + xctx->nodes ^= NGX_HTTP_DAV_EXT_NODE_PROP; + } + + if (ngx_strcmp(localname, "propname") == 0) { + xctx->nodes ^= NGX_HTTP_DAV_EXT_NODE_PROPNAME; + } + + if (ngx_strcmp(localname, "allprop") == 0) { + xctx->nodes ^= NGX_HTTP_DAV_EXT_NODE_ALLPROP; + } +} + + +static void +ngx_http_dav_ext_propfind_xml_end(void *data, const xmlChar *localname, + const xmlChar *prefix, const xmlChar *uri) +{ + ngx_http_dav_ext_xml_ctx_t *xctx = data; + + if (xctx->nodes & NGX_HTTP_DAV_EXT_NODE_PROPFIND) { + + if (xctx->nodes & NGX_HTTP_DAV_EXT_NODE_PROP) { + if (ngx_strcmp(localname, "displayname") == 0) { + xctx->props |= NGX_HTTP_DAV_EXT_PROP_DISPLAYNAME; + } + + if (ngx_strcmp(localname, "getcontentlength") == 0) { + xctx->props |= NGX_HTTP_DAV_EXT_PROP_GETCONTENTLENGTH; + } + + if (ngx_strcmp(localname, "getlastmodified") == 0) { + xctx->props |= NGX_HTTP_DAV_EXT_PROP_GETLASTMODIFIED; + } + + if (ngx_strcmp(localname, "resourcetype") == 0) { + xctx->props |= NGX_HTTP_DAV_EXT_PROP_RESOURCETYPE; + } + + if (ngx_strcmp(localname, "lockdiscovery") == 0) { + xctx->props |= NGX_HTTP_DAV_EXT_PROP_LOCKDISCOVERY; + } + + if (ngx_strcmp(localname, "supportedlock") == 0) { + xctx->props |= NGX_HTTP_DAV_EXT_PROP_SUPPORTEDLOCK; + } + } + + if (xctx->nodes & NGX_HTTP_DAV_EXT_NODE_PROPNAME) { + xctx->props |= NGX_HTTP_DAV_EXT_PROP_NAMES; + } + + if (xctx->nodes & NGX_HTTP_DAV_EXT_NODE_ALLPROP) { + xctx->props = NGX_HTTP_DAV_EXT_PROP_ALL; + } + } + + ngx_http_dav_ext_propfind_xml_start(data, localname, prefix, uri, + 0, NULL, 0, 0, NULL); +} + + +static ngx_int_t +ngx_http_dav_ext_propfind(ngx_http_request_t *r, ngx_uint_t props) +{ + size_t root, allocated; + u_char *p, *last, *filename; + ngx_int_t rc; + ngx_err_t err; + ngx_str_t path, name; + ngx_dir_t dir; + ngx_uint_t depth; + ngx_array_t entries; + ngx_file_info_t fi; + ngx_http_dav_ext_entry_t *entry; + + if (ngx_array_init(&entries, r->pool, 40, sizeof(ngx_http_dav_ext_entry_t)) + != NGX_OK) + { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + rc = ngx_http_dav_ext_depth(r, 0); + + if (rc == NGX_ERROR) { + return NGX_HTTP_BAD_REQUEST; + } + + if (rc == NGX_MAX_INT_T_VALUE) { + + /* + * RFC4918: + * 403 Forbidden - A server MAY reject PROPFIND requests on + * collections with depth header of "Infinity", in which case + * it SHOULD use this error with the precondition code + * 'propfind-finite-depth' inside the error body. + */ + + return NGX_HTTP_FORBIDDEN; + } + + depth = rc; + + last = ngx_http_map_uri_to_path(r, &path, &root, + NGX_HTTP_DAV_EXT_PREALLOCATE); + if (last == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + allocated = path.len; + path.len = last - path.data; + + if (path.len > 1 && path.data[path.len - 1] == '/') { + path.len--; + + } else { + last++; + } + + path.data[path.len] = '\0'; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext propfind path: \"%s\"", path.data); + + if (ngx_file_info(path.data, &fi) == NGX_FILE_ERROR) { + return NGX_HTTP_NOT_FOUND; + } + + if (r->uri.len < 2) { + name = r->uri; + + } else { + name.data = &r->uri.data[r->uri.len - 1]; + name.len = (name.data[0] == '/') ? 0 : 1; + + while (name.data != r->uri.data) { + p = name.data - 1; + if (*p == '/') { + break; + } + + name.data--; + name.len++; + } + } + + entry = ngx_array_push(&entries); + if (entry == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ngx_memzero(entry, sizeof(ngx_http_dav_ext_entry_t)); + + entry->uri = r->uri; + entry->name = name; + entry->dir = ngx_is_dir(&fi); + entry->mtime = ngx_file_mtime(&fi); + entry->size = ngx_file_size(&fi); + + if (ngx_http_dav_ext_set_locks(r, entry) != NGX_OK) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext propfind name:\"%V\", uri:\"%V\"", + &entry->name, &entry->uri); + + if (depth == 0 || !entry->dir) { + return ngx_http_dav_ext_propfind_response(r, &entries, props); + } + + if (ngx_open_dir(&path, &dir) == NGX_ERROR) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, ngx_errno, + ngx_open_dir_n " \"%s\" failed", path.data); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + rc = NGX_OK; + + filename = path.data; + filename[path.len] = '/'; + + for ( ;; ) { + ngx_set_errno(0); + + if (ngx_read_dir(&dir) == NGX_ERROR) { + err = ngx_errno; + + if (err != NGX_ENOMOREFILES) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, err, + ngx_read_dir_n " \"%V\" failed", &path); + rc = NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + break; + } + + name.len = ngx_de_namelen(&dir); + name.data = ngx_de_name(&dir); + + if (name.data[0] == '.') { + continue; + } + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext propfind child path: \"%s\"", name.data); + + entry = ngx_array_push(&entries); + if (entry == NULL) { + rc = NGX_HTTP_INTERNAL_SERVER_ERROR; + break; + } + + ngx_memzero(entry, sizeof(ngx_http_dav_ext_entry_t)); + + if (!dir.valid_info) { + + if (path.len + 1 + name.len + 1 > allocated) { + allocated = path.len + 1 + name.len + 1 + + NGX_HTTP_DAV_EXT_PREALLOCATE; + + filename = ngx_pnalloc(r->pool, allocated); + if (filename == NULL) { + rc = NGX_HTTP_INTERNAL_SERVER_ERROR; + break; + } + + last = ngx_cpystrn(filename, path.data, path.len + 1); + *last++ = '/'; + } + + ngx_cpystrn(last, name.data, name.len + 1); + + if (ngx_de_info(filename, &dir) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_CRIT, r->connection->log, ngx_errno, + ngx_de_info_n " \"%s\" failed", filename); + rc = NGX_HTTP_INTERNAL_SERVER_ERROR; + break; + } + } + + p = ngx_pnalloc(r->pool, name.len); + if (p == NULL) { + rc = NGX_HTTP_INTERNAL_SERVER_ERROR; + break; + } + + ngx_memcpy(p, name.data, name.len); + entry->name.data = p; + entry->name.len = name.len; + + p = ngx_pnalloc(r->pool, r->uri.len + 1 + name.len + 1); + if (p == NULL) { + rc = NGX_HTTP_INTERNAL_SERVER_ERROR; + break; + } + + entry->uri.data = p; + + p = ngx_cpymem(p, r->uri.data, r->uri.len); + if (r->uri.len && r->uri.data[r->uri.len - 1] != '/') { + *p++ = '/'; + } + + p = ngx_cpymem(p, name.data, name.len); + if (ngx_de_is_dir(&dir)) { + *p++ = '/'; + } + + entry->uri.len = p - entry->uri.data; + entry->dir = ngx_de_is_dir(&dir); + entry->mtime = ngx_de_mtime(&dir); + entry->size = ngx_de_size(&dir); + + if (ngx_http_dav_ext_set_locks(r, entry) != NGX_OK) { + rc = NGX_HTTP_INTERNAL_SERVER_ERROR; + break; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext propfind child name:\"%V\", uri:\"%V\"", + &entry->name, &entry->uri); + } + + if (ngx_close_dir(&dir) == NGX_ERROR) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, ngx_errno, + ngx_close_dir_n " \"%V\" failed", &path); + } + + if (rc != NGX_OK) { + return rc; + } + + return ngx_http_dav_ext_propfind_response(r, &entries, props); +} + + +static ngx_int_t +ngx_http_dav_ext_set_locks(ngx_http_request_t *r, + ngx_http_dav_ext_entry_t *entry) +{ + ngx_http_dav_ext_node_t *node; + ngx_http_dav_ext_lock_t *lock; + ngx_http_dav_ext_loc_conf_t *dlcf; + + dlcf = ngx_http_get_module_loc_conf(r, ngx_http_dav_ext_module); + + if (dlcf->shm_zone == NULL) { + entry->lock_supported = 0; + return NGX_OK; + } + + entry->lock_supported = 1; + + lock = dlcf->shm_zone->data; + + ngx_shmtx_lock(&lock->shpool->mutex); + + node = ngx_http_dav_ext_lock_lookup(r, lock, &entry->uri, -1); + if (node == NULL) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return NGX_OK; + } + + entry->lock_infinite = node->infinite ? 1 : 0; + entry->lock_expire = node->expire; + entry->lock_token = node->token; + + entry->lock_root.data = ngx_pnalloc(r->pool, node->len); + if (entry->lock_root.data == NULL) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return NGX_ERROR; + } + + ngx_memcpy(entry->lock_root.data, node->data, node->len); + entry->lock_root.len = node->len; + + ngx_shmtx_unlock(&lock->shpool->mutex); + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_dav_ext_propfind_response(ngx_http_request_t *r, ngx_array_t *entries, + ngx_uint_t props) +{ + size_t len; + u_char *p; + uintptr_t escape; + ngx_buf_t *b; + ngx_int_t rc; + ngx_uint_t n; + ngx_chain_t cl; + ngx_http_dav_ext_entry_t *entry; + + static u_char head[] = + "\n" + "\n"; + + static u_char tail[] = + "\n"; + + entry = entries->elts; + + for (n = 0; n < entries->nelts; n++) { + escape = 2 * ngx_escape_uri(NULL, entry[n].uri.data, entry[n].uri.len, + NGX_ESCAPE_URI); + if (escape == 0) { + continue; + } + + p = ngx_pnalloc(r->pool, entry[n].uri.len + escape); + if (p == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + entry[n].uri.len = (u_char *) ngx_escape_uri(p, entry[n].uri.data, + entry[n].uri.len, + NGX_ESCAPE_URI) + - p; + entry[n].uri.data = p; + } + + len = sizeof(head) - 1 + sizeof(tail) - 1; + + for (n = 0; n < entries->nelts; n++) { + len += ngx_http_dav_ext_format_propfind(r, NULL, &entry[n], props); + } + + b = ngx_create_temp_buf(r->pool, len); + if (b == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + b->last = ngx_cpymem(b->last, head, sizeof(head) - 1); + + for (n = 0; n < entries->nelts; n++) { + b->last = (u_char *) ngx_http_dav_ext_format_propfind(r, b->last, + &entry[n], props); + } + + b->last = ngx_cpymem(b->last, tail, sizeof(tail) - 1); + + b->last_buf = (r == r->main) ? 1 : 0; + b->last_in_chain = 1; + + cl.buf = b; + cl.next = NULL; + + r->headers_out.status = 207; + ngx_str_set(&r->headers_out.status_line, "207 Multi-Status"); + + r->headers_out.content_length_n = b->last - b->pos; + + r->headers_out.content_type_len = sizeof("text/xml") - 1; + ngx_str_set(&r->headers_out.content_type, "text/xml"); + r->headers_out.content_type_lowcase = NULL; + + ngx_str_set(&r->headers_out.charset, "utf-8"); + + rc = ngx_http_send_header(r); + + if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { + return rc; + } + + return ngx_http_output_filter(r, &cl); +} + + +static ngx_int_t +ngx_http_dav_ext_lock_handler(ngx_http_request_t *r) +{ + u_char *last; + size_t n, root; + time_t now; + uint32_t token, new_token; + ngx_fd_t fd; + ngx_int_t rc, depth; + ngx_str_t path; + ngx_uint_t status; + ngx_file_info_t fi; + ngx_http_dav_ext_lock_t *lock; + ngx_http_dav_ext_node_t *node; + ngx_http_dav_ext_loc_conf_t *dlcf; + + if (r->uri.len == 0) { + return NGX_HTTP_BAD_REQUEST; + } + + dlcf = ngx_http_get_module_loc_conf(r, ngx_http_dav_ext_module); + lock = dlcf->shm_zone->data; + + /* + * RFC4918: + * If no Depth header is submitted on a LOCK request, then the request + * MUST act as if a "Depth:infinity" had been submitted. + */ + + rc = ngx_http_dav_ext_depth(r, NGX_MAX_INT_T_VALUE); + + if (rc == NGX_ERROR || rc == 1) { + + /* + * RFC4918: + * Values other than 0 or infinity MUST NOT be used with the Depth + * header on a LOCK method. + */ + + return NGX_HTTP_BAD_REQUEST; + } + + depth = rc; + + token = ngx_http_dav_ext_if(r, &r->uri); + + do { + new_token = ngx_random(); + } while (new_token == 0); + + now = ngx_time(); + + ngx_shmtx_lock(&lock->shpool->mutex); + + node = ngx_http_dav_ext_lock_lookup(r, lock, &r->uri, depth); + + if (node) { + if (token == 0) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return 423; /* Locked */ + } + + if (node->token != token) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return NGX_HTTP_PRECONDITION_FAILED; + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext refresh lock"); + + node->expire = now + lock->timeout; + + ngx_queue_remove(&node->queue); + ngx_queue_insert_tail(&lock->sh->queue, &node->queue); + + ngx_shmtx_unlock(&lock->shpool->mutex); + + return ngx_http_dav_ext_lock_response(r, NGX_HTTP_OK, lock->timeout, + depth, token); + } + + n = sizeof(ngx_http_dav_ext_node_t) + r->uri.len - 1; + + node = ngx_slab_alloc_locked(lock->shpool, n); + if (node == NULL) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ngx_memzero(node, sizeof(ngx_http_dav_ext_node_t)); + + ngx_memcpy(&node->data, r->uri.data, r->uri.len); + + node->len = r->uri.len; + node->token = new_token; + node->expire = now + lock->timeout; + node->infinite = (depth ? 1 : 0); + + ngx_queue_insert_tail(&lock->sh->queue, &node->queue); + + ngx_shmtx_unlock(&lock->shpool->mutex); + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext add lock"); + + last = ngx_http_map_uri_to_path(r, &path, &root, 0); + if (last == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + *last = '\0'; + + status = NGX_HTTP_OK; + + if (ngx_file_info(path.data, &fi) == NGX_FILE_ERROR) { + + /* + * RFC4918: + * A successful lock request to an unmapped URL MUST result in the + * creation of a locked (non-collection) resource with empty content. + */ + + fd = ngx_open_file(path.data, NGX_FILE_RDONLY, NGX_FILE_CREATE_OR_OPEN, + NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + + /* + * RFC4918: + * 409 (Conflict) - A resource cannot be created at the destination + * until one or more intermediate collections have been created. + * The server MUST NOT create those intermediate collections + * automatically. + */ + + ngx_log_error(NGX_LOG_ERR, r->connection->log, ngx_errno, + ngx_open_file_n " \"%s\" failed", path.data); + return NGX_HTTP_CONFLICT; + } + + if (ngx_close_file(fd) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ALERT, r->connection->log, ngx_errno, + ngx_close_file_n " \"%s\" failed", path.data); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + status = NGX_HTTP_CREATED; + } + + return ngx_http_dav_ext_lock_response(r, status, lock->timeout, depth, + new_token); +} + + +static ngx_int_t +ngx_http_dav_ext_lock_response(ngx_http_request_t *r, ngx_uint_t status, + time_t timeout, ngx_uint_t depth, uint32_t token) +{ + size_t len; + time_t now; + u_char *p; + ngx_int_t rc; + ngx_buf_t *b; + ngx_chain_t cl; + ngx_table_elt_t *h; + ngx_http_dav_ext_entry_t entry; + + static u_char head[] = + "\n" + "\n"; + + static u_char tail[] = + "\n"; + + now = ngx_time(); + + ngx_memzero(&entry, sizeof(ngx_http_dav_ext_entry_t)); + + entry.lock_expire = now + timeout; + entry.lock_root = r->uri; + entry.lock_infinite = depth ? 1 : 0; + entry.lock_token = token; + + len = sizeof(head) - 1 + + ngx_http_dav_ext_format_lockdiscovery(r, NULL, &entry) + + sizeof(tail) - 1; + + b = ngx_create_temp_buf(r->pool, len); + if (b == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + b->last = ngx_cpymem(b->last, head, sizeof(head) - 1); + b->last = (u_char *) ngx_http_dav_ext_format_lockdiscovery(r, b->last, + &entry); + b->last = ngx_cpymem(b->last, tail, sizeof(tail) - 1); + + b->last_buf = (r == r->main) ? 1 : 0; + b->last_in_chain = 1; + + cl.buf = b; + cl.next = NULL; + + r->headers_out.status = status; + r->headers_out.content_length_n = b->last - b->pos; + + r->headers_out.content_type_len = sizeof("text/xml") - 1; + ngx_str_set(&r->headers_out.content_type, "text/xml"); + r->headers_out.content_type_lowcase = NULL; + + ngx_str_set(&r->headers_out.charset, "utf-8"); + + h = ngx_list_push(&r->headers_out.headers); + if (h == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ngx_str_set(&h->key, "Lock-Token"); + + p = ngx_pnalloc(r->pool, ngx_http_dav_ext_format_token(NULL, token, 1)); + if (p == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + h->value.data = p; + h->value.len = (u_char *) ngx_http_dav_ext_format_token(p, token, 1) - p; + h->hash = 1; + + rc = ngx_http_send_header(r); + + if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { + return rc; + } + + return ngx_http_output_filter(r, &cl); +} + + +static ngx_int_t +ngx_http_dav_ext_unlock_handler(ngx_http_request_t *r) +{ + uint32_t token; + ngx_http_dav_ext_lock_t *lock; + ngx_http_dav_ext_node_t *node; + ngx_http_dav_ext_loc_conf_t *dlcf; + + token = ngx_http_dav_ext_lock_token(r); + + dlcf = ngx_http_get_module_loc_conf(r, ngx_http_dav_ext_module); + lock = dlcf->shm_zone->data; + + ngx_shmtx_lock(&lock->shpool->mutex); + + node = ngx_http_dav_ext_lock_lookup(r, lock, &r->uri, -1); + + if (node == NULL || node->token != token) { + ngx_shmtx_unlock(&lock->shpool->mutex); + return NGX_HTTP_NO_CONTENT; + } + + ngx_queue_remove(&node->queue); + ngx_slab_free_locked(lock->shpool, node); + + ngx_shmtx_unlock(&lock->shpool->mutex); + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext delete lock"); + + return NGX_HTTP_NO_CONTENT; +} + + +static ngx_int_t +ngx_http_dav_ext_depth(ngx_http_request_t *r, ngx_int_t default_depth) +{ + ngx_table_elt_t *depth; + + depth = r->headers_in.depth; + + if (depth == NULL) { + return default_depth; + } + + if (depth->value.len == 1) { + + if (depth->value.data[0] == '0') { + return 0; + } + + if (depth->value.data[0] == '1') { + return 1; + } + + } else { + + if (depth->value.len == sizeof("infinity") - 1 + && ngx_strcmp(depth->value.data, "infinity") == 0) + { + return NGX_MAX_INT_T_VALUE; + } + } + + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "client sent invalid \"Depth\" header: \"%V\"", + &depth->value); + + return NGX_ERROR; +} + + +static uint32_t +ngx_http_dav_ext_lock_token(ngx_http_request_t *r) +{ + u_char *p, ch; + uint32_t token; + ngx_uint_t i, n; + ngx_list_part_t *part; + ngx_table_elt_t *header; + + static u_char name[] = "lock-token"; + + part = &r->headers_in.headers.part; + header = part->elts; + + for (i = 0; /* void */ ; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + header = part->elts; + i = 0; + } + + for (n = 0; n < sizeof(name) - 1 && n < header[i].key.len; n++) { + ch = header[i].key.data[n]; + + if (ch >= 'A' && ch <= 'Z') { + ch |= 0x20; + } + + if (name[n] != ch) { + break; + } + } + + if (n == sizeof(name) - 1 && n == header[i].key.len) { + p = header[i].value.data; + + if (ngx_strncmp(p, "= '0' && ch <= '9') { + token = token * 16 + (ch - '0'); + continue; + } + + ch = (u_char) (ch | 0x20); + + if (ch >= 'a' && ch <= 'f') { + token = token * 16 + (ch - 'a' + 10); + continue; + } + + return 0; + } + + if (*p != '>') { + return 0; + } + + return token; + } + } + + return 0; +} + + +static uint32_t +ngx_http_dav_ext_if(ngx_http_request_t *r, ngx_str_t *uri) +{ + u_char *p, ch; + uint32_t token; + ngx_str_t tag; + ngx_uint_t i, n; + ngx_list_part_t *part; + ngx_table_elt_t *header; + + static u_char name[] = "if"; + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext if \"%V\"", uri); + + part = &r->headers_in.headers.part; + header = part->elts; + + for (i = 0; /* void */ ; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + header = part->elts; + i = 0; + } + + for (n = 0; n < sizeof(name) - 1 && n < header[i].key.len; n++) { + ch = header[i].key.data[n]; + + if (ch >= 'A' && ch <= 'Z') { + ch |= 0x20; + } + + if (name[n] != ch) { + break; + } + } + + if (n == sizeof(name) - 1 && n == header[i].key.len) { + p = header[i].value.data; + tag = r->uri; + + while (*p != '\0') { + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext if list \"%s\"", p); + + while (*p == ' ') { p++; } + + if (*p == '<') { + tag.data = ++p; + + while (*p != '\0' && *p != '>') { p++; } + + if (*p == '\0') { + break; + } + + tag.len = p++ - tag.data; + + (void) ngx_http_dav_ext_strip_uri(r, &tag); + + while (*p == ' ') { p++; } + } + + if (*p != '(') { + break; + } + + p++; + + if (tag.len == 0 + || tag.len > uri->len + || (tag.len < uri->len && tag.data[tag.len - 1] != '/') + || ngx_memcmp(tag.data, uri->data, tag.len)) + { + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext if tag mismatch \"%V\"", &tag); + + while (*p != '\0' && *p != ')') { p++; } + + if (*p == ')') { + p++; + } + + continue; + } + + while (*p != '\0') { + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext if condition \"%s\"", p); + + while (*p == ' ') { p++; } + + if (ngx_strncmp(p, "Not", 3) == 0) { + p += 3; + while (*p == ' ') { p++; } + goto next; + } + + if (*p == '[') { + p++; + while (*p != '\0' && *p != ']') { p++; } + goto next; + } + + if (ngx_strncmp(p, "= '0' && ch <= '9') { + token = token * 16 + (ch - '0'); + continue; + } + + ch = (u_char) (ch | 0x20); + + if (ch >= 'a' && ch <= 'f') { + token = token * 16 + (ch - 'a' + 10); + continue; + } + + goto next; + } + + if (*p != '>') { + goto next; + } + + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext if token: %uxD", token); + + return token; + + next: + + while (*p != '\0' && *p != ' ' && *p != ')') { p++; } + + if (*p == ')') { + p++; + break; + } + } + } + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "http dav_ext if header mismatch"); + } + } + + return 0; +} + + +static uintptr_t +ngx_http_dav_ext_format_propfind(ngx_http_request_t *r, u_char *dst, + ngx_http_dav_ext_entry_t *entry, ngx_uint_t props) +{ + size_t len; + + static u_char head[] = + "\n" + ""; + + /* uri */ + + static u_char prop[] = + "\n" + "\n" + "\n"; + + /* properties */ + + static u_char tail[] = + "\n" + "HTTP/1.1 200 OK\n" + "\n" + "\n"; + + static u_char names[] = + "\n" + "\n" + "\n" + "\n" + "\n" + "\n"; + + static u_char supportedlock[] = + "\n" + "\n" + "\n" + "\n"; + + if (dst == NULL) { + len = sizeof(head) - 1 + + sizeof(prop) - 1 + + sizeof(tail) - 1; + + len += entry->uri.len + ngx_escape_html(NULL, entry->uri.data, + entry->uri.len); + + if (props & NGX_HTTP_DAV_EXT_PROP_NAMES) { + len += sizeof(names) - 1; + + } else { + len += sizeof("" + "\n" + + "" + "\n" + + "" + "Mon, 28 Sep 1970 06:00:00 GMT" + "\n" + + "" + "" + "\n" + + "\n" + "\n") - 1; + + /* displayname */ + len += entry->name.len + + ngx_escape_html(NULL, entry->name.data, entry->name.len); + + /* getcontentlength */ + len += NGX_OFF_T_LEN; + + /* lockdiscovery */ + len += ngx_http_dav_ext_format_lockdiscovery(r, NULL, entry); + + /* supportedlock */ + if (entry->lock_supported) { + len += sizeof(supportedlock) - 1; + } + } + + return len; + } + + dst = ngx_cpymem(dst, head, sizeof(head) - 1); + dst = (u_char *) ngx_escape_html(dst, entry->uri.data, entry->uri.len); + dst = ngx_cpymem(dst, prop, sizeof(prop) - 1); + + if (props & NGX_HTTP_DAV_EXT_PROP_NAMES) { + dst = ngx_cpymem(dst, names, sizeof(names) - 1); + + } else { + if (props & NGX_HTTP_DAV_EXT_PROP_DISPLAYNAME) { + dst = ngx_cpymem(dst, "", + sizeof("") - 1); + dst = (u_char *) ngx_escape_html(dst, entry->name.data, + entry->name.len); + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + } + + if (props & NGX_HTTP_DAV_EXT_PROP_GETCONTENTLENGTH) { + if (!entry->dir) { + dst = ngx_sprintf(dst, "%O" + "\n", entry->size); + } + } + + if (props & NGX_HTTP_DAV_EXT_PROP_GETLASTMODIFIED) { + dst = ngx_cpymem(dst, "", + sizeof("") - 1); + dst = ngx_http_time(dst, entry->mtime); + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + } + + if (props & NGX_HTTP_DAV_EXT_PROP_RESOURCETYPE) { + dst = ngx_cpymem(dst, "", + sizeof("") - 1); + + if (entry->dir) { + dst = ngx_cpymem(dst, "", + sizeof("") - 1); + } + + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + } + + if (props & NGX_HTTP_DAV_EXT_PROP_LOCKDISCOVERY) { + dst = (u_char *) ngx_http_dav_ext_format_lockdiscovery(r, dst, + entry); + } + + if (props & NGX_HTTP_DAV_EXT_PROP_SUPPORTEDLOCK) { + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + if (entry->lock_supported) { + dst = ngx_cpymem(dst, supportedlock, sizeof(supportedlock) - 1); + } + + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + } + } + + dst = ngx_cpymem(dst, tail, sizeof(tail) - 1); + + return (uintptr_t) dst; +} + + +static uintptr_t +ngx_http_dav_ext_format_lockdiscovery(ngx_http_request_t *r, u_char *dst, + ngx_http_dav_ext_entry_t *entry) +{ + size_t len; + time_t now; + + if (dst == NULL) { + if (entry->lock_token == 0) { + return sizeof("\n") - 1; + } + + len = sizeof("\n" + "\n" + "\n" + "\n" + "infinity\n" + "Second-\n" + "\n" + "\n" + "\n" + "\n") - 1; + + /* timeout */ + len += NGX_TIME_T_LEN; + + /* token */ + len += ngx_http_dav_ext_format_token(NULL, entry->lock_token, 0); + + /* lockroot */ + len += entry->lock_root.len + ngx_escape_html(NULL, + entry->lock_root.data, + entry->lock_root.len); + return len; + } + + if (entry->lock_token == 0) { + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + return (uintptr_t) dst; + } + + now = ngx_time(); + + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + dst = ngx_sprintf(dst, "%s\n", + entry->lock_infinite ? "infinity" : "0"); + + dst = ngx_sprintf(dst, "Second-%T\n", + entry->lock_expire - now); + + dst = ngx_cpymem(dst, "", + sizeof("") - 1); + dst = (u_char *) ngx_http_dav_ext_format_token(dst, entry->lock_token, 0); + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + dst = ngx_cpymem(dst, "", + sizeof("") - 1); + dst = (u_char *) ngx_escape_html(dst, entry->lock_root.data, + entry->lock_root.len); + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + dst = ngx_cpymem(dst, "\n", + sizeof("\n") - 1); + + return (uintptr_t) dst; +} + + +static uintptr_t +ngx_http_dav_ext_format_token(u_char *dst, uint32_t token, ngx_uint_t brackets) +{ + ngx_uint_t n; + + static u_char hex[] = "0123456789abcdef"; + + if (dst == NULL) { + return sizeof("") - 1 + (brackets ? 2 : 0); + } + + if (brackets) { + *dst++ = '<'; + } + + dst = ngx_cpymem(dst, "urn:", 4); + + for (n = 0; n < 4; n++) { + *dst++ = hex[token >> 28]; + *dst++ = hex[(token >> 24) & 0xf]; + token <<= 8; + } + + if (brackets) { + *dst++ = '>'; + } + + return (uintptr_t) dst; +} + + +static ngx_int_t +ngx_http_dav_ext_init_zone(ngx_shm_zone_t *shm_zone, void *data) +{ + ngx_http_dav_ext_lock_t *olock = data; + + size_t len; + ngx_http_dav_ext_lock_t *lock; + + lock = shm_zone->data; + + if (olock) { + lock->sh = olock->sh; + lock->shpool = olock->shpool; + return NGX_OK; + } + + lock->shpool = (ngx_slab_pool_t *) shm_zone->shm.addr; + + if (shm_zone->shm.exists) { + lock->sh = lock->shpool->data; + return NGX_OK; + } + + lock->sh = ngx_slab_alloc(lock->shpool, sizeof(ngx_http_dav_ext_lock_sh_t)); + if (lock->sh == NULL) { + return NGX_ERROR; + } + + lock->shpool->data = lock->sh; + + ngx_queue_init(&lock->sh->queue); + + len = sizeof(" in dav_ext zone \"\"") + shm_zone->shm.name.len; + + lock->shpool->log_ctx = ngx_slab_alloc(lock->shpool, len); + if (lock->shpool->log_ctx == NULL) { + return NGX_ERROR; + } + + ngx_sprintf(lock->shpool->log_ctx, " in dav_ext zone \"%V\"%Z", + &shm_zone->shm.name); + + return NGX_OK; +} + + +static void * +ngx_http_dav_ext_create_loc_conf(ngx_conf_t *cf) +{ + ngx_http_dav_ext_loc_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_dav_ext_loc_conf_t)); + if (conf == NULL) { + return NULL; + } + + /* + * set by ngx_pcalloc(): + * + * conf->shm_zone = NULL; + */ + + return conf; +} + + +static char * +ngx_http_dav_ext_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_http_dav_ext_loc_conf_t *prev = parent; + ngx_http_dav_ext_loc_conf_t *conf = child; + + ngx_conf_merge_bitmask_value(conf->methods, prev->methods, + (NGX_CONF_BITMASK_SET|NGX_HTTP_DAV_EXT_OFF)); + + if (conf->shm_zone == NULL) { + conf->shm_zone = prev->shm_zone; + } + + return NGX_CONF_OK; +} + + +static char * +ngx_http_dav_ext_lock_zone(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + u_char *p; + time_t timeout; + ssize_t size; + ngx_str_t *value, name, s; + ngx_uint_t i; + ngx_shm_zone_t *shm_zone; + ngx_http_dav_ext_lock_t *lock; + + value = cf->args->elts; + + name.len = 0; + size = 0; + timeout = 60; + + for (i = 1; i < cf->args->nelts; i++) { + + if (ngx_strncmp(value[i].data, "zone=", 5) == 0) { + + name.data = value[i].data + 5; + + p = (u_char *) ngx_strchr(name.data, ':'); + + if (p == NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid zone size \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + name.len = p - name.data; + + s.data = p + 1; + s.len = value[i].data + value[i].len - s.data; + + size = ngx_parse_size(&s); + + if (size == NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid zone size \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (size < (ssize_t) (8 * ngx_pagesize)) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "zone \"%V\" is too small", &value[i]); + return NGX_CONF_ERROR; + } + + continue; + } + + if (ngx_strncmp(value[i].data, "timeout=", 8) == 0) { + + s.len = value[i].len - 8; + s.data = value[i].data + 8; + + timeout = ngx_parse_time(&s, 1); + if (timeout == (time_t) NGX_ERROR || timeout == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid timeout value \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + continue; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (name.len == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"%V\" must have \"zone\" parameter", + &cmd->name); + return NGX_CONF_ERROR; + } + + lock = ngx_pcalloc(cf->pool, sizeof(ngx_http_dav_ext_lock_t)); + if (lock == NULL) { + return NGX_CONF_ERROR; + } + + lock->timeout = timeout; + + shm_zone = ngx_shared_memory_add(cf, &name, size, + &ngx_http_dav_ext_module); + if (shm_zone == NULL) { + return NGX_CONF_ERROR; + } + + if (shm_zone->data) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "duplicate zone \"%V\"", &name); + return NGX_CONF_ERROR; + } + + shm_zone->init = ngx_http_dav_ext_init_zone; + shm_zone->data = lock; + + return NGX_CONF_OK; +} + + +static char * +ngx_http_dav_ext_lock(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_dav_ext_loc_conf_t *dlcf = conf; + + ngx_str_t *value, s; + ngx_uint_t i; + ngx_shm_zone_t *shm_zone; + + if (dlcf->shm_zone) { + return "is duplicate"; + } + + value = cf->args->elts; + + shm_zone = NULL; + + for (i = 1; i < cf->args->nelts; i++) { + + if (ngx_strncmp(value[i].data, "zone=", 5) == 0) { + + s.len = value[i].len - 5; + s.data = value[i].data + 5; + + shm_zone = ngx_shared_memory_add(cf, &s, 0, + &ngx_http_dav_ext_module); + if (shm_zone == NULL) { + return NGX_CONF_ERROR; + } + + continue; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + if (shm_zone == NULL) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"%V\" must have \"zone\" parameter", &cmd->name); + return NGX_CONF_ERROR; + } + + dlcf->shm_zone = shm_zone; + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_http_dav_ext_init(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_PRECONTENT_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_dav_ext_precontent_handler; + + h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_dav_ext_content_handler; + + return NGX_OK; +} diff --git a/ngx_dav_ext_module/t/dav_ext.t b/ngx_dav_ext_module/t/dav_ext.t new file mode 100644 index 0000000..00327e2 --- /dev/null +++ b/ngx_dav_ext_module/t/dav_ext.t @@ -0,0 +1,141 @@ +#!/usr/bin/perl + +# (C) Roman Arutyunyan + +# Tests for nginx-dav-ext-module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use HTTP::DAV + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http dav/)->plan(20); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + dav_ext_lock_zone zone=foo:10m timeout=10s; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + dav_methods PUT DELETE MKCOL COPY MOVE; + dav_ext_methods PROPFIND OPTIONS LOCK UNLOCK; + dav_ext_lock zone=foo; + } + } +} + +EOF + +$t->write_file('foo', 'foo'); + +$t->run(); + +############################################################################### + +my $url = "http://127.0.0.1:8080"; + +my $content; + +my $d = HTTP::DAV->new(); +$d->open($url); + +my $d2 = HTTP::DAV->new(); +$d2->open($url); + +#debug: +#$d->DebugLevel(3); +#see /tmp/perldav_debug.txt. + +my $p = $d->propfind('/', 1); +is($p->is_collection, 1, 'propfind dir collection'); +is($p->get_property('displayname'), '/', 'propfind dir displayname'); +is($p->get_uri(), 'http://127.0.0.1:8080/', 'propfind dir uri'); + +$p = $d->propfind('/foo'); +is($p->is_collection, 0, 'propfind file collection'); +is($p->get_property('displayname'), 'foo', 'propfind file displayname'); +is($p->get_uri(), 'http://127.0.0.1:8080/foo', 'propfind file uri'); +is($p->get_property('getcontentlength'), '3', 'propfind file size'); + +$d->lock('/foo'); +is($d->lock('/foo'), 0, 'prevent double lock'); + +$d->unlock('/foo'); +is($d->lock('/foo'), 1, 'relock'); + +$d->lock('/bar'); +$p = $d->propfind('/bar'); +is($p->get_property('displayname'), 'bar', 'lock creates a file'); + +$d->get('/bar', \$content) or $content = 'none'; +is($content, '', 'lock creates an empty file'); + +$content = "bar"; +$d->put(\$content, '/bar'); +$d->get('/bar', \$content) or $content = ''; +is($content, 'bar', 'put lock'); + +$content = "qux"; +$d2->put(\$content, '/bar'); +$d2->get('/bar', \$content) or $content = ''; +isnt($content, 'qux', 'prevent put lock'); + +$d->mkcol('/d/'); +$d->lock('/d/'); +$d->copy('/bar', '/d/bar'); +$d->get('/d/bar', \$content) or $content = ''; +is($content, 'bar', 'copy lock'); + +$d2->copy('/bar', '/d/qux'); +$d2->get('/d/qux', \$content) or $content = ''; +isnt($content, 'bar', 'prevent copy lock'); + +$d2->delete('/d/bar'); +$d2->get('/d/bar', \$content) or $content = ''; +is($content, 'bar', 'prevent delete lock'); + +$d->delete('/d/bar'); +$d->get('/d/bar', \$content) or $content = ''; +is($content, '', 'delete lock'); + +$d->mkcol('/d/c/'); +$p = $d->propfind('/d/c/'); +is($p->is_collection, 1, 'mkcol lock'); + +$d2->mkcol('/d/e/'); +is($d2->propfind('/d/e/'), 0, 'prevent mkcol lock'); + +$d->unlock('/d/'); +$d->lock('/d/', -depth=>"0"); +$content = 'qux'; +$d2->put(\$content, '/d/c/qux'); +$d2->get('/d/c/qux', \$content) or $content = ''; +is($content, 'qux', 'put to a depth-0-locked subdirectory'); + +############################################################################### diff --git a/ngx_fancyindex/.gitattributes b/ngx_fancyindex/.gitattributes new file mode 100644 index 0000000..c710740 --- /dev/null +++ b/ngx_fancyindex/.gitattributes @@ -0,0 +1,4 @@ +/.gitignore export-ignore +/.travis.yml export-ignore +/make-dist export-ignore +t/* text eol=lf diff --git a/ngx_fancyindex/.github/workflows/ci.yml b/ngx_fancyindex/.github/workflows/ci.yml new file mode 100644 index 0000000..374487e --- /dev/null +++ b/ngx_fancyindex/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +--- +name: Build +on: [pull_request] + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-18.04] + compiler: [gcc, clang] + nginx: + # Mainline + - 1.23.3 + # Stable. + - 1.22.1 + # First version with loadable module support. + - 1.9.15 + # Oldest supported version. + - 0.8.55 + dynamic: [0, 1] + exclude: + - nginx: 0.8.55 + dynamic: 1 + - nginx: 0.8.55 + os: macos-latest + - compiler: gcc + os: macos-latest + runs-on: ${{ matrix.os }} + env: + CFLAGS: "-Wno-error" + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Packages + run: | + case $RUNNER_OS in + Linux ) + sudo apt update + sudo apt install -y libpcre3-dev libssl-dev + ;; + * ) + ;; + esac + t/get-pup || echo 'Tests needing pup will be skipped' + - name: Test + env: + CC: ${{ matrix.compiler }} + run: t/build-and-run ${{ matrix.nginx }} ${{ matrix.dynamic }} diff --git a/ngx_fancyindex/.gitignore b/ngx_fancyindex/.gitignore new file mode 100644 index 0000000..ba43cbe --- /dev/null +++ b/ngx_fancyindex/.gitignore @@ -0,0 +1,8 @@ +*.sw[op] +/nginx-* +/t/*.sh +/t/*.out +/t/*.err +/t/pup* +/t/bug*/ +/prefix/ diff --git a/ngx_fancyindex/CHANGELOG.md b/ngx_fancyindex/CHANGELOG.md new file mode 100644 index 0000000..ebeb379 --- /dev/null +++ b/ngx_fancyindex/CHANGELOG.md @@ -0,0 +1,199 @@ +# Change Log +All notable changes to this project will be documented in this file. + +## [Unreleased] + +## [0.5.2] - 2021-10-28 +### Fixed +- Properly escape file names to ensure that file names are never renreded + as HTML. (Patch by Anthony Ryan <>, + [#128](https://github.com/aperezdc/ngx-fancyindex/pull/128).) + +## [0.5.1] - 2020-10-26 +### Fixed +- Properly handle optional second argument to `fancyindex_header` and + `fancyindex_footer` + ([#117](https://github.com/aperezdc/ngx-fancyindex/issues/117)). + +## [0.5.0] - 2020-10-24 +### Added +- New option `fancyindex_show_dotfiles`. (Path by Joshua Shaffer + <>.) +- The `fancyindex_header` and `fancyindex_footer` options now support local + files properly, by means of a `local` flag. (Patches by JoungKyun Kim + <> and Adrián Pérez <>.) + +### Changed +- Improved performance of directory entry sorting, which should be quite + noticeable for directories with thousands of files. (Patch by + [Yuxiang Zhang](https://github.com/z4yx).) +- The minimum Nginx version supported by the module is now 0.8.x. + +### Fixed +- Properly escape square brackets in directory entry names when the module + is built with older versions of Nginx. (Patch by Adrián Pérez + <>.) +- Fix directory entry listing not being shown when using the + [nginx-auth-ldap](https://github.com/kvspb/nginx-auth-ldap) module. (Patch + by JoungKyun Kim <>.) + +## [0.4.4] - 2020-02-19 +### Added +- New option `fancyindex_hide_parent_dir`, which disables generating + links to parent directories in listings. (Patch by Kawai Ryota + <>.) + +### Changed +- Each table row is now separated by a new line (as a matter of fact, + a `CRLF` sequence), which makes it easier to parse output using simple + text tools. (Patch by Anders Trier <>.) +- Some corrections and additions to the README file. (Patches by Nicolas + Carpi <> and David Beitey <>.) + +### Fixed +- Use correct character references for `&` characters in table sorter URLs + within the template (Patch by David Beitey <>.) +- Properly encode filenames when used as URI components. + +## [0.4.3] - 2018-07-03 +### Added +- Table cells now have class names, which allows for better CSS styling. + (Patch by qjqqyy <>.) +- The test suite now can parse and check elements from the HTML returned + by the module, thanks to the [pup](https://github.com/EricChiang/pup) + tool. + +### Fixed +- Sorting by file size now works correctly. + (Patch by qjqqyy <>.) + +## [0.4.2] - 2017-08-19 +### Changed +- Generated HTML from the default template is now proper HTML5, and it should + pass validation (#52). +- File sizes now have decimal positions when using `fancyindex_exact_size off`. + (Patch by Anders Trier <>.) +- Multiple updates to `README.rst` (Patches by Danila Vershinin + <>, Iulian Onofrei, Lilian Besson, and Nick Geoghegan + <>.) + +### Fixed +- Sorting by file size now also works correctly for directories which contain + files of sizes bigger than `INT_MAX`. (#74, fix suggestion by Chris Young.) +- Custom headers which fail to declare an UTF-8 encoding no longer cause table + header arrows to be rendered incorrectly by browsers (#50). +- Fix segmentation fault when opening directories with empty files (#61, patch + by Catgirl <>.) + +## [0.4.1] - 2016-08-18 +### Added +- New `fancyindex_directories_first` configuration directive (enabled by + default), which allows setting whether directories are sorted before other + files. (Patch by Luke Zapart <>.) + +### Fixed +- Fix index files not working when the fancyindex module is in use (#46). + + +## [0.4.0] - 2016-06-08 +### Added +- The module can now be built as a [dynamic + module](https://www.nginx.com/resources/wiki/extending/converting/). + (Patch by Róbert Nagy <>.) +- New configuration directive `fancyindex_show_path`, which allows hiding the + `

` header which contains the current path. + (Patch by Thomas P. <>.) + +### Changed +- Directory and file links in listings now have a title="..." attribute. + (Patch by `@janglapuk` <>.) + +### Fixed +- Fix for hung requests when the module is used along with `ngx_pagespeed`. + (Patch by Otto van der Schaaf <>.) + + +## [0.3.6] - 2016-01-26 +### Added +- New feature: Allow filtering out symbolic links using the + `fancyindex_hide_symlinks` configuration directive. (Idea and prototype + patch by Thomas Wemm.) +- New feature: Allow specifying the format of timestamps using the + `fancyindex_time_format` configuration directive. (Idea suggested by Xiao + Meng <>). + +### Changed +- Listings in top-level directories will not generate a "Parent Directory" + link as first element of the listing. (Patch by Thomas P.) + +### Fixed +- Fix propagation and overriding of the `fancyindex_css_href` setting inside + nested locations. +- Minor changes in the code to allow building cleanly under Windows with + Visual Studio 2013. (Patch by Y. Yuan <>). + + +## [0.3.5] - 2015-02-19 +### Added +- New feature: Allow setting the default sort criterion using the + `fancyindex_default_sort` configuration directive. (Patch by + Алексей Урбанский). +- New feature: Allow changing the maximum length of file names, using + the `fancyindex_name_length` configuration directive. (Patch by + Martin Herkt). + +### Changed +- Renames `NEWS.rst` to `CHANGELOG.md`, which follows the recommendations + from [Keep a Change Log](http://keepachangelog.com/). +- Configuring Nginx without the `http_addition_module` will generate a + warning during configuration, as it is needed for the `fancyindex_footer` + and `fancyindex_header` directives. + + +## [0.3.4] - 2014-09-03 + +### Added +- Viewport is now defined in the generated HTML, which works better + for mobile devices. + +### Changed +- Even-odd row styling moved to the CSS using :nth-child(). This + makes the HTML served to clients smaller. + + +## [0.3.3] - 2013-10-25 + +### Added +- New feature: table headers in the default template are now clickable + to set the sorting criteria and direction of the index entries. + (https://github.com/aperezdc/ngx-fancyindex/issues/7) + + +## [0.3.2] - 2013-06-05 + +### Fixed +- Solved a bug that would leave certain clients stalled forever. +- Improved handling of subrequests for non-builtin headers/footers. + + +## [0.3.1] - 2011-04-04 + +### Added +- `NEWS.rst` file, to act as change log. + + +[Unreleased]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.5.2...HEAD +[0.5.2]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.5.1...v0.5.2 +[0.5.1]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.5.0...v0.5.1 +[0.5.0]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.4.4...v0.5.0 +[0.4.4]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.4.3...v0.4.4 +[0.4.3]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.4.2...v0.4.3 +[0.4.2]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.4.1...v0.4.2 +[0.4.1]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.4.0...v0.4.1 +[0.4.0]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.3.6...v0.4.0 +[0.3.6]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.3.5...v0.3.6 +[0.3.5]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.3.4...v0.3.5 +[0.3.4]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.3.3...v0.3.4 +[0.3.3]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.3.2...v0.3.3 +[0.3.2]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.3.1...v0.3.2 +[0.3.1]: https://github.com/aperezdc/ngx-fancyindex/compare/v0.3...v0.3.1 diff --git a/ngx_fancyindex/HACKING.md b/ngx_fancyindex/HACKING.md new file mode 100644 index 0000000..bf7223a --- /dev/null +++ b/ngx_fancyindex/HACKING.md @@ -0,0 +1,30 @@ +# Fancy Index module Hacking HOW-TO + +## How to modify the template + +The template is in the `template.html` file. Note that comment markers are +used to control how the `template.awk` Awk script generates the C header +which gets ultimately included in the compiled object code. Comment markers +have the `` format. Here `identifier` must be +a valid C identifier. All the text following the marker until the next +marker will be flattened into a C string. + +If the identifier is `NONE` (capitalized) the text from that marker up to +the next marker will be discarded. + + +## Regenerating the C header + +You will need Awk. I hope any decent implementation will do, but the GNU one +is known to work flawlessly. Just do: + + $ awk -f template.awk template.html > template.h + +If your copy of `awk` is not the GNU implementation, you will need to +install it and use `gawk` instead in the command line above. + +This includes macOS where the current built-in `awk` (currently version +20070501 at time of testing on 10.13.6) doesn't apply correctly and causes +characters to be omitted from the output. `gawk` can be installed with a +package manager such as [Homebrew](https://brew.sh) or +[MacPorts](https://ports.macports.org/port/gawk). diff --git a/ngx_fancyindex/LICENSE b/ngx_fancyindex/LICENSE new file mode 100644 index 0000000..9fd66ee --- /dev/null +++ b/ngx_fancyindex/LICENSE @@ -0,0 +1,20 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/ngx_fancyindex/README.rst b/ngx_fancyindex/README.rst new file mode 100644 index 0000000..00fe2ee --- /dev/null +++ b/ngx_fancyindex/README.rst @@ -0,0 +1,324 @@ +======================== +Nginx Fancy Index module +======================== + +.. image:: https://travis-ci.com/aperezdc/ngx-fancyindex.svg?branch=master + :target: https://travis-ci.com/aperezdc/ngx-fancyindex + :alt: Build Status + +.. contents:: + +The Fancy Index module makes possible the generation of file listings, like +the built-in `autoindex `__ +module does, but adding a touch of style. This is possible because the module +allows a certain degree of customization of the generated content: + +* Custom headers, either local or stored remotely. +* Custom footers, either local or stored remotely. +* Add your own CSS style rules. +* Allow choosing to sort elements by name (default), modification time, or + size; both ascending (default), or descending. + +This module is designed to work with Nginx_, a high performance open source web +server written by `Igor Sysoev `__. + + +Requirements +============ + +CentOS, RHEL, Fedora Linux +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For users of the `official stable `__ Nginx repository, `extra packages repository with dynamic modules `__ is available and fancyindex is included. + +Install repository configuration, then the module package:: + + yum -y install https://extras.getpagespeed.com/release-latest.rpm + yum -y install nginx-module-fancyindex + +Then load the module in `/etc/nginx/nginx.conf` using:: + + load_module "modules/ngx_http_fancyindex_module.so"; + +macOS +~~~~~ + +Users can `install Nginx on macOS with MacPorts `__; fancyindex is included:: + + sudo port install nginx + +Other platforms +~~~~~~~~~~~~~~~ + +In most other cases you will need the sources for Nginx_. Any version starting +from the 0.8 series should work. + +In order to use the ``fancyindex_header_`` and ``fancyindex_footer_`` directives +you will also need the `ngx_http_addition_module `_ +built into Nginx. + + +Building +======== + +1. Unpack the Nginx_ sources:: + + $ gunzip -c nginx-?.?.?.tar.gz | tar -xvf - + +2. Unpack the sources for the fancy indexing module:: + + $ gunzip -c nginx-fancyindex-?.?.?.tar.gz | tar -xvf - + +3. Change to the directory which contains the Nginx_ sources, run the + configuration script with the desired options and be sure to put an + ``--add-module`` flag pointing to the directory which contains the source + of the fancy indexing module:: + + $ cd nginx-?.?.? + $ ./configure --add-module=../nginx-fancyindex-?.?.? \ + [--with-http_addition_module] [extra desired options] + + Since version 0.4.0, the module can also be built as a + `dynamic module `_, + using ``--add-dynamic-module=…`` instead and + ``load_module "modules/ngx_http_fancyindex_module.so";`` + in the configuration file + +4. Build and install the software:: + + $ make + + And then, as ``root``:: + + # make install + +5. Configure Nginx_ by using the modules' configuration directives_. + + +Example +======= + +You can test the default built-in style by adding the following lines into +a ``server`` section in your Nginx_ configuration file:: + + location / { + fancyindex on; # Enable fancy indexes. + fancyindex_exact_size off; # Output human-readable file sizes. + } + + +Themes +~~~~~~ + +The following themes demonstrate the level of customization which can be +achieved using the module: + +* `Theme `__ by + `@TheInsomniac `__. Uses custom header and + footer. +* `Theme `__ by + `@Naereen `__. Uses custom header and footer. The + header includes a search field to filter by file name using JavaScript. +* `Theme `__ by + `@fraoustin `__. Responsive theme using + Material Design elements. +* `Theme `__ by + `@alehaa `__. Simple, flat theme based on + Bootstrap 4 and FontAwesome. + + +Directives +========== + +fancyindex +~~~~~~~~~~ +:Syntax: *fancyindex* [*on* | *off*] +:Default: fancyindex off +:Context: http, server, location +:Description: + Enables or disables fancy directory indexes. + +fancyindex_default_sort +~~~~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_default_sort* [*name* | *size* | *date* | *name_desc* | *size_desc* | *date_desc*] +:Default: fancyindex_default_sort name +:Context: http, server, location +:Description: + Defines sorting criterion by default. + +fancyindex_case_sensitive +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_case_sensitive* [*on* | *off*] +:Default: fancyindex_case_sensitive on +:Context: http, server, location +:Description: + If enabled (default setting), sorting by name will be case-sensitive. + If disabled, case will be ignored when sorting by name. + +fancyindex_directories_first +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_directories_first* [*on* | *off*] +:Default: fancyindex_directories_first on +:Context: http, server, location +:Description: + If enabled (default setting), groups directories together and sorts them + before all regular files. If disabled, directories are sorted together with files. + +fancyindex_css_href +~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_css_href uri* +:Default: fancyindex_css_href "" +:Context: http, server, location +:Description: + Allows inserting a link to a CSS style sheet in generated listings. The + provided *uri* parameter will be inserted as-is in a ```` HTML tag. + The link is inserted after the built-in CSS rules, so you can override the + default styles. + +fancyindex_exact_size +~~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_exact_size* [*on* | *off*] +:Default: fancyindex_exact_size on +:Context: http, server, location +:Description: + Defines how to represent file sizes in the directory listing: either + accurately, or rounding off to the kilobyte, the megabyte and the + gigabyte. + +fancyindex_footer +~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_footer path* [*subrequest* | *local*] +:Default: fancyindex_footer "" +:Context: http, server, location +:Description: + Specifies which file should be inserted at the foot of directory listings. + If set to an empty string, the default footer supplied by the module will + be sent. The optional parameter indicates whether the *path* is to be + treated as a URI to load using a *subrequest* (the default), or whether + it refers to a *local* file. + +.. note:: Using this directive needs the ngx_http_addition_module_ built + into Nginx. + +.. warning:: When inserting custom a header/footer, a subrequest will be + issued so potentially any URL can be used as source for them. Although it + will work with external URLs, only using internal ones is supported. + External URLs are totally untested and using them will make Nginx_ block + while waiting for the subrequest to complete. If you feel like external + header/footer is a must-have for you, please + `let me know `__. + +fancyindex_header +~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_header path* [*subrequest* | *local*] +:Default: fancyindex_header "" +:Context: http, server, location +:Description: + Specifies which file should be inserted at the head of directory listings. + If set to an empty string, the default header supplied by the module will + be sent. The optional parameter indicates whether the *path* is to be + treated as a URI to load using a *subrequest* (the default), or whether + it refers to a *local* file. + +.. note:: Using this directive needs the ngx_http_addition_module_ built + into Nginx. + +fancyindex_show_path +~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_show_path* [*on* | *off*] +:Default: fancyindex_show_path on +:Context: http, server, location +:Description: + Whether or not to output the path and the closing

tag after the header. + This is useful when you want to handle the path displaying with a PHP script + for example. + +.. warning:: This directive can be turned off only if a custom header is provided + using fancyindex_header. + +fancyindex_show_dotfiles +~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_show_dotfiles* [*on* | *off*] +:Default: fancyindex_show_dotfiles off +:Context: http, server, location +:Description: + Whether to list files that are preceded with a dot. Normal convention is to + hide these. + +fancyindex_ignore +~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_ignore string1 [string2 [... stringN]]* +:Default: No default. +:Context: http, server, location +:Description: + Specifies a list of file names which will not be shown in generated + listings. If Nginx was built with PCRE support, strings are interpreted as + regular expressions. + +fancyindex_hide_symlinks +~~~~~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_hide_symlinks* [*on* | *off*] +:Default: fancyindex_hide_symlinks off +:Context: http, server, location +:Description: + When enabled, generated listings will not contain symbolic links. + +fancyindex_hide_parent_dir +~~~~~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_hide_parent_dir* [*on* | *off*] +:Default: fancyindex_hide_parent_dir off +:Context: http, server, location +:Description: + When enabled, it will not show the parent directory. + +fancyindex_localtime +~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_localtime* [*on* | *off*] +:Default: fancyindex_localtime off +:Context: http, server, location +:Description: + Enables showing file times as local time. Default is “off” (GMT time). + +fancyindex_time_format +~~~~~~~~~~~~~~~~~~~~~~ +:Syntax: *fancyindex_time_format* string +:Default: fancyindex_time_format "%Y-%b-%d %H:%M" +:Context: http, server, location +:Description: + Format string used for timestamps. The format specifiers are a subset of + those supported by the `strftime `_ + function, and the behavior is locale-independent (for example, day and month + names are always in English). The supported formats are: + + * ``%a``: Abbreviated name of the day of the week. + * ``%A``: Full name of the day of the week. + * ``%b``: Abbreviated month name. + * ``%B``: Full month name. + * ``%d``: Day of the month as a decimal number (range 01 to 31). + * ``%e``: Like ``%d``, the day of the month as a decimal number, but a + leading zero is replaced by a space. + * ``%F``: Equivalent to ``%Y-%m-%d`` (the ISO 8601 date format). + * ``%H``: Hour as a decimal number using a 24-hour clock (range 00 + to 23). + * ``%I``: Hour as a decimal number using a 12-hour clock (range 01 to 12). + * ``%k``: Hour (24-hour clock) as a decimal number (range 0 to 23); + single digits are preceded by a blank. + * ``%l``: Hour (12-hour clock) as a decimal number (range 1 to 12); single + digits are preceded by a blank. + * ``%m``: Month as a decimal number (range 01 to 12). + * ``%M``: Minute as a decimal number (range 00 to 59). + * ``%p``: Either "AM" or "PM" according to the given time value. + * ``%P``: Like ``%p`` but in lowercase: "am" or "pm". + * ``%r``: Time in a.m. or p.m. notation. Equivalent to ``%I:%M:%S %p``. + * ``%R``: Time in 24-hour notation (``%H:%M``). + * ``%S``: Second as a decimal number (range 00 to 60). + * ``%T``: Time in 24-hour notation (``%H:%M:%S``). + * ``%u``: Day of the week as a decimal, range 1 to 7, Monday being 1. + * ``%w``: Day of the week as a decimal, range 0 to 6, Monday being 0. + * ``%y``: Year as a decimal number without a century (range 00 to 99). + * ``%Y``: Year as a decimal number including the century. + + +.. _nginx: https://nginx.org + +.. vim:ft=rst:spell:spelllang=en: diff --git a/ngx_fancyindex/config b/ngx_fancyindex/config new file mode 100644 index 0000000..4ef3809 --- /dev/null +++ b/ngx_fancyindex/config @@ -0,0 +1,20 @@ +# vim:ft=sh: +ngx_addon_name=ngx_http_fancyindex_module + +if [ "$ngx_module_link" = DYNAMIC ] ; then + ngx_module_type=HTTP + ngx_module_name=ngx_http_fancyindex_module + ngx_module_srcs="$ngx_addon_dir/ngx_http_fancyindex_module.c" + ngx_module_deps="$ngx_addon_dir/template.h" + ngx_module_order="$ngx_module_name ngx_http_autoindex_module" + . auto/module +else + # XXX: Insert fancyindex module *after* index module! + # + HTTP_MODULES=`echo "${HTTP_MODULES}" | sed -e \ + 's/ngx_http_index_module/ngx_http_fancyindex_module ngx_http_index_module/'` + NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_fancyindex_module.c" + if [ $HTTP_ADDITION != YES ] ; then + echo " - The 'addition' filter is needed for fancyindex_{header,footer}, but it was disabled" + fi +fi diff --git a/ngx_fancyindex/make-dist b/ngx_fancyindex/make-dist new file mode 100644 index 0000000..fef95da --- /dev/null +++ b/ngx_fancyindex/make-dist @@ -0,0 +1,10 @@ +#! /bin/sh +set -e + +GIT_TAG=$(git describe --tags HEAD) +VERSION=${GIT_TAG#v} +PV="ngx-fancyindex-${VERSION}" + +set -x +git archive --worktree-attributes --prefix="${PV}/" -o "${PV}.tar" "${GIT_TAG}" +xz -f9 "${PV}.tar" diff --git a/ngx_fancyindex/ngx_http_fancyindex_module.c b/ngx_fancyindex/ngx_http_fancyindex_module.c new file mode 100644 index 0000000..d523656 --- /dev/null +++ b/ngx_fancyindex/ngx_http_fancyindex_module.c @@ -0,0 +1,1624 @@ +/* + * ngx_http_fancyindex_module.c + * Copyright © 2007-2016 Adrian Perez + * + * Module used for fancy indexing of directories. Features and differences + * with the stock nginx autoindex module: + * + * - Output is a table instead of a
 element with embedded  links.
+ *  - Header and footer may be added to every generated directory listing.
+ *  - Default header and/or footer are generated if custom ones are not
+ *    configured. Files used for header and footer can only be local path
+ *    names (i.e. you cannot insert the result of a subrequest.)
+ *  - Proper HTML is generated: it should validate both as XHTML 1.0 Strict
+ *    and HTML 4.01.
+ *
+ * Base functionality heavy based upon the stock nginx autoindex module,
+ * which in turn was made by Igor Sysoev, like the majority of nginx.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include 
+#include 
+#include 
+#include 
+
+#include "template.h"
+
+#if defined(__GNUC__) && (__GNUC__ >= 3)
+# define ngx_force_inline __attribute__((__always_inline__))
+#else /* !__GNUC__ */
+# define ngx_force_inline
+#endif /* __GNUC__ */
+
+
+static const char *short_weekday[] = {
+    "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun",
+};
+static const char *long_weekday[] = {
+    "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday",
+};
+static const char *short_month[] = {
+    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+};
+static const char *long_month[] = {
+    "January", "February", "March", "April", "May", "June", "July",
+    "August", "September", "October", "November", "December",
+};
+
+
+#define DATETIME_FORMATS(F_, t) \
+    F_ ('a',  3, "%3s",  short_weekday[((t)->ngx_tm_wday + 6) % 7]) \
+    F_ ('A',  9, "%s",   long_weekday [((t)->ngx_tm_wday + 6) % 7]) \
+    F_ ('b',  3, "%3s",  short_month[(t)->ngx_tm_mon - 1]         ) \
+    F_ ('B',  9, "%s",   long_month [(t)->ngx_tm_mon - 1]         ) \
+    F_ ('d',  2, "%02d", (t)->ngx_tm_mday                         ) \
+    F_ ('e',  2, "%2d",  (t)->ngx_tm_mday                         ) \
+    F_ ('F', 10, "%d-%02d-%02d",                                    \
+                  (t)->ngx_tm_year,                                 \
+                  (t)->ngx_tm_mon,                                  \
+                  (t)->ngx_tm_mday                                ) \
+    F_ ('H',  2, "%02d", (t)->ngx_tm_hour                         ) \
+    F_ ('I',  2, "%02d", ((t)->ngx_tm_hour % 12) + 1              ) \
+    F_ ('k',  2, "%2d",  (t)->ngx_tm_hour                         ) \
+    F_ ('l',  2, "%2d",  ((t)->ngx_tm_hour % 12) + 1              ) \
+    F_ ('m',  2, "%02d", (t)->ngx_tm_mon                          ) \
+    F_ ('M',  2, "%02d", (t)->ngx_tm_min                          ) \
+    F_ ('p',  2, "%2s",  (((t)->ngx_tm_hour < 12) ? "AM" : "PM")  ) \
+    F_ ('P',  2, "%2s",  (((t)->ngx_tm_hour < 12) ? "am" : "pm")  ) \
+    F_ ('r', 11, "%02d:%02d:%02d %2s",                              \
+                 ((t)->ngx_tm_hour % 12) + 1,                       \
+                 (t)->ngx_tm_min,                                   \
+                 (t)->ngx_tm_sec,                                   \
+                 (((t)->ngx_tm_hour < 12) ? "AM" : "PM")          ) \
+    F_ ('R',  5, "%02d:%02d", (t)->ngx_tm_hour, (t)->ngx_tm_min   ) \
+    F_ ('S',  2, "%02d", (t)->ngx_tm_sec                          ) \
+    F_ ('T',  8, "%02d:%02d:%02d",                                  \
+                 (t)->ngx_tm_hour,                                  \
+                 (t)->ngx_tm_min,                                   \
+                 (t)->ngx_tm_sec                                  ) \
+    F_ ('u',  1, "%1d", (((t)->ngx_tm_wday + 6) % 7) + 1          ) \
+    F_ ('w',  1, "%1d", ((t)->ngx_tm_wday + 6) % 7                ) \
+    F_ ('y',  2, "%02d", (t)->ngx_tm_year % 100                   ) \
+    F_ ('Y',  4, "%04d", (t)->ngx_tm_year                         )
+
+
+static size_t
+ngx_fancyindex_timefmt_calc_size (const ngx_str_t *fmt)
+{
+#define DATETIME_CASE(letter, fmtlen, fmt, ...) \
+        case letter: result += (fmtlen); break;
+
+    size_t i, result = 0;
+    for (i = 0; i < fmt->len; i++) {
+        if (fmt->data[i] == '%') {
+            if (++i >= fmt->len) {
+                result++;
+                break;
+            }
+            switch (fmt->data[i]) {
+                DATETIME_FORMATS(DATETIME_CASE,)
+                default:
+                    result++;
+            }
+        } else {
+            result++;
+        }
+    }
+    return result;
+
+#undef DATETIME_CASE
+}
+
+
+static u_char*
+ngx_fancyindex_timefmt (u_char *buffer, const ngx_str_t *fmt, const ngx_tm_t *tm)
+{
+#define DATETIME_CASE(letter, fmtlen, fmt, ...) \
+        case letter: buffer = ngx_snprintf(buffer, fmtlen, fmt, ##__VA_ARGS__); break;
+
+    size_t i;
+    for (i = 0; i < fmt->len; i++) {
+        if (fmt->data[i] == '%') {
+            if (++i >= fmt->len) {
+                *buffer++ = '%';
+                break;
+            }
+            switch (fmt->data[i]) {
+                DATETIME_FORMATS(DATETIME_CASE, tm)
+                default:
+                    *buffer++ = fmt->data[i];
+            }
+        } else {
+            *buffer++ = fmt->data[i];
+        }
+    }
+    return buffer;
+
+#undef DATETIME_CASE
+}
+
+typedef struct {
+    ngx_str_t path;
+    ngx_str_t local;
+} ngx_fancyindex_headerfooter_conf_t;
+
+/**
+ * Configuration structure for the fancyindex module. The configuration
+ * commands defined in the module do fill in the members of this structure.
+ */
+typedef struct {
+    ngx_flag_t enable;         /**< Module is enabled. */
+    ngx_uint_t default_sort;   /**< Default sort criterion. */
+    ngx_flag_t case_sensitive; /**< Case-sensitive name sorting */
+    ngx_flag_t dirs_first;     /**< Group directories together first when sorting */
+    ngx_flag_t localtime;      /**< File mtime dates are sent in local time. */
+    ngx_flag_t exact_size;     /**< Sizes are sent always in bytes. */
+    ngx_flag_t hide_symlinks;  /**< Hide symbolic links in listings. */
+    ngx_flag_t show_path;      /**< Whether to display or not the path + '' after the header */
+    ngx_flag_t hide_parent;    /**< Hide parent directory. */
+    ngx_flag_t show_dot_files; /**< Show files that start with a dot.*/
+
+    ngx_str_t  css_href;       /**< Link to a CSS stylesheet, or empty if none. */
+    ngx_str_t  time_format;    /**< Format used for file timestamps. */
+
+    ngx_array_t *ignore;       /**< List of files to ignore in listings. */
+
+    ngx_fancyindex_headerfooter_conf_t header;
+    ngx_fancyindex_headerfooter_conf_t footer;
+} ngx_http_fancyindex_loc_conf_t;
+
+#define NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME       0
+#define NGX_HTTP_FANCYINDEX_SORT_CRITERION_SIZE       1
+#define NGX_HTTP_FANCYINDEX_SORT_CRITERION_DATE       2
+#define NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME_DESC  3
+#define NGX_HTTP_FANCYINDEX_SORT_CRITERION_SIZE_DESC  4
+#define NGX_HTTP_FANCYINDEX_SORT_CRITERION_DATE_DESC  5
+
+static ngx_conf_enum_t ngx_http_fancyindex_sort_criteria[] = {
+    { ngx_string("name"), NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME },
+    { ngx_string("size"), NGX_HTTP_FANCYINDEX_SORT_CRITERION_SIZE },
+    { ngx_string("date"), NGX_HTTP_FANCYINDEX_SORT_CRITERION_DATE },
+    { ngx_string("name_desc"), NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME_DESC },
+    { ngx_string("size_desc"), NGX_HTTP_FANCYINDEX_SORT_CRITERION_SIZE_DESC },
+    { ngx_string("date_desc"), NGX_HTTP_FANCYINDEX_SORT_CRITERION_DATE_DESC },
+    { ngx_null_string, 0 }
+};
+
+enum {
+    NGX_HTTP_FANCYINDEX_HEADERFOOTER_SUBREQUEST,
+    NGX_HTTP_FANCYINDEX_HEADERFOOTER_LOCAL,
+};
+
+static ngx_uint_t
+headerfooter_kind(const ngx_str_t *value)
+{
+    static const struct {
+        ngx_str_t name;
+        ngx_uint_t value;
+    } values[] = {
+        { ngx_string("subrequest"), NGX_HTTP_FANCYINDEX_HEADERFOOTER_SUBREQUEST },
+        { ngx_string("local"), NGX_HTTP_FANCYINDEX_HEADERFOOTER_LOCAL },
+    };
+
+    unsigned i;
+
+    for (i = 0; i < sizeof(values) / sizeof(values[0]); i++) {
+        if (value->len == values[i].name.len &&
+            ngx_strcasecmp(value->data, values[i].name.data) == 0)
+        {
+            return values[i].value;
+        }
+    }
+
+    return NGX_CONF_UNSET_UINT;
+}
+
+static char*
+ngx_fancyindex_conf_set_headerfooter(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_fancyindex_headerfooter_conf_t *item =
+        (void*) (((char*) conf) + cmd->offset);
+    ngx_str_t *values = cf->args->elts;
+
+    if (item->path.data)
+        return "is duplicate";
+
+    item->path = values[1];
+
+    /* Kind of path. Default is "subrequest". */
+    ngx_uint_t kind = NGX_HTTP_FANCYINDEX_HEADERFOOTER_SUBREQUEST;
+    if (cf->args->nelts == 3) {
+        kind = headerfooter_kind(&values[2]);
+        if (kind == NGX_CONF_UNSET_UINT) {
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
+                               "unknown header/footer kind \"%V\"", &values[2]);
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    if (kind == NGX_HTTP_FANCYINDEX_HEADERFOOTER_LOCAL) {
+        ngx_file_t file;
+        ngx_file_info_t fi;
+        ssize_t n;
+
+        ngx_memzero(&file, sizeof(ngx_file_t));
+        file.log = cf->log;
+        file.fd = ngx_open_file(item->path.data, NGX_FILE_RDONLY, 0, 0);
+        if (file.fd == NGX_INVALID_FILE) {
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
+                               "cannot open file \"%V\"", &values[1]);
+            return NGX_CONF_ERROR;
+        }
+
+        if (ngx_fd_info(file.fd, &fi) == NGX_FILE_ERROR) {
+            ngx_close_file(file.fd);
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
+                               "cannot get info for file \"%V\"", &values[1]);
+            return NGX_CONF_ERROR;
+        }
+
+        item->local.len = ngx_file_size(&fi);
+        item->local.data = ngx_pcalloc(cf->pool, item->local.len + 1);
+        if (item->local.data == NULL) {
+            ngx_close_file(file.fd);
+            return NGX_CONF_ERROR;
+        }
+
+        n = item->local.len;
+        while (n > 0) {
+            ssize_t r = ngx_read_file(&file,
+                                      item->local.data + file.offset,
+                                      n,
+                                      file.offset);
+            if (r == NGX_ERROR) {
+                ngx_close_file(file.fd);
+                ngx_conf_log_error(NGX_LOG_EMERG, cf, ngx_errno,
+                                   "cannot read file \"%V\"", &values[1]);
+                return NGX_CONF_ERROR;
+            }
+
+            n -= r;
+        }
+        item->local.data[item->local.len] = '\0';
+    }
+
+    return NGX_CONF_OK;
+}
+
+#define NGX_HTTP_FANCYINDEX_PREALLOCATE  50
+
+
+/**
+ * Calculates the length of a NULL-terminated string. It is ugly having to
+ * remember to substract 1 from the sizeof result.
+ */
+#define ngx_sizeof_ssz(_s)  (sizeof(_s) - 1)
+
+/**
+ * Compute the length of a statically allocated array
+ */
+#define DIM(x) (sizeof(x)/sizeof(*(x)))
+
+/**
+ * Copy a static zero-terminated string. Useful to output template
+ * string pieces into a temporary buffer.
+ */
+#define ngx_cpymem_ssz(_p, _t) \
+	(ngx_cpymem((_p), (_t), sizeof(_t) - 1))
+
+/**
+ * Copy a ngx_str_t.
+ */
+#define ngx_cpymem_str(_p, _s) \
+	(ngx_cpymem((_p), (_s).data, (_s).len))
+
+/**
+ * Check whether a particular bit is set in a particular value.
+ */
+#define ngx_has_flag(_where, _what) \
+	(((_where) & (_what)) == (_what))
+
+
+
+
+typedef struct {
+    ngx_str_t      name;
+    size_t         utf_len;
+    ngx_uint_t     escape;
+    ngx_uint_t     escape_html;
+    ngx_uint_t     dir;
+    time_t         mtime;
+    off_t          size;
+} ngx_http_fancyindex_entry_t;
+
+
+
+static int ngx_libc_cdecl
+    ngx_http_fancyindex_cmp_entries_name_cs_desc(const void *one, const void *two);
+static int ngx_libc_cdecl
+    ngx_http_fancyindex_cmp_entries_name_ci_desc(const void *one, const void *two);
+static int ngx_libc_cdecl
+    ngx_http_fancyindex_cmp_entries_size_desc(const void *one, const void *two);
+static int ngx_libc_cdecl
+    ngx_http_fancyindex_cmp_entries_mtime_desc(const void *one, const void *two);
+static int ngx_libc_cdecl
+    ngx_http_fancyindex_cmp_entries_name_cs_asc(const void *one, const void *two);
+static int ngx_libc_cdecl
+    ngx_http_fancyindex_cmp_entries_name_ci_asc(const void *one, const void *two);
+static int ngx_libc_cdecl
+    ngx_http_fancyindex_cmp_entries_size_asc(const void *one, const void *two);
+static int ngx_libc_cdecl
+    ngx_http_fancyindex_cmp_entries_mtime_asc(const void *one, const void *two);
+
+static ngx_int_t ngx_http_fancyindex_error(ngx_http_request_t *r,
+    ngx_dir_t *dir, ngx_str_t *name);
+
+static ngx_int_t ngx_http_fancyindex_init(ngx_conf_t *cf);
+
+static void *ngx_http_fancyindex_create_loc_conf(ngx_conf_t *cf);
+
+static char *ngx_http_fancyindex_merge_loc_conf(ngx_conf_t *cf,
+    void *parent, void *child);
+
+static char *ngx_http_fancyindex_ignore(ngx_conf_t    *cf,
+                                        ngx_command_t *cmd,
+                                        void          *conf);
+
+static uintptr_t
+    ngx_fancyindex_escape_filename(u_char *dst, u_char*src, size_t size);
+
+/*
+ * These are used only once per handler invocation. We can tell GCC to
+ * inline them always, if possible (see how ngx_force_inline is defined
+ * above).
+ */
+static ngx_inline ngx_buf_t*
+    make_header_buf(ngx_http_request_t *r, const ngx_str_t css_href)
+    ngx_force_inline;
+
+
+static ngx_command_t  ngx_http_fancyindex_commands[] = {
+
+    { ngx_string("fancyindex"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, enable),
+      NULL },
+
+    { ngx_string("fancyindex_default_sort"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
+      ngx_conf_set_enum_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, default_sort),
+      &ngx_http_fancyindex_sort_criteria },
+
+    { ngx_string("fancyindex_case_sensitive"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, case_sensitive),
+      NULL },
+
+    { ngx_string("fancyindex_directories_first"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, dirs_first),
+      NULL },
+
+    { ngx_string("fancyindex_localtime"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, localtime),
+      NULL },
+
+    { ngx_string("fancyindex_exact_size"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, exact_size),
+      NULL },
+
+    { ngx_string("fancyindex_header"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12,
+      ngx_fancyindex_conf_set_headerfooter,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, header),
+      NULL },
+
+    { ngx_string("fancyindex_footer"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12,
+      ngx_fancyindex_conf_set_headerfooter,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, footer),
+      NULL },
+
+    { ngx_string("fancyindex_css_href"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_str_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, css_href),
+      NULL },
+
+    { ngx_string("fancyindex_ignore"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE,
+      ngx_http_fancyindex_ignore,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      0,
+      NULL },
+
+    { ngx_string("fancyindex_hide_symlinks"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, hide_symlinks),
+      NULL },
+
+    { ngx_string("fancyindex_show_path"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, show_path),
+      NULL },
+
+    { ngx_string("fancyindex_show_dotfiles"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, show_dot_files),
+      NULL },
+
+    { ngx_string("fancyindex_hide_parent_dir"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_flag_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, hide_parent),
+      NULL },
+
+    { ngx_string("fancyindex_time_format"),
+      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG,
+      ngx_conf_set_str_slot,
+      NGX_HTTP_LOC_CONF_OFFSET,
+      offsetof(ngx_http_fancyindex_loc_conf_t, time_format),
+      NULL },
+
+    ngx_null_command
+};
+
+
+static ngx_http_module_t  ngx_http_fancyindex_module_ctx = {
+    NULL,                                  /* preconfiguration */
+    ngx_http_fancyindex_init,              /* postconfiguration */
+
+    NULL,                                  /* create main configuration */
+    NULL,                                  /* init main configuration */
+
+    NULL,                                  /* create server configuration */
+    NULL,                                  /* merge server configuration */
+
+    ngx_http_fancyindex_create_loc_conf,   /* create location configuration */
+    ngx_http_fancyindex_merge_loc_conf     /* merge location configuration */
+};
+
+
+ngx_module_t  ngx_http_fancyindex_module = {
+    NGX_MODULE_V1,
+    &ngx_http_fancyindex_module_ctx,       /* module context */
+    ngx_http_fancyindex_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
+};
+
+
+
+static const ngx_str_t css_href_pre =
+    ngx_string("\n");
+
+
+#ifdef NGX_ESCAPE_URI_COMPONENT
+static inline uintptr_t
+ngx_fancyindex_escape_filename(u_char *dst, u_char *src, size_t size)
+{
+    return ngx_escape_uri(dst, src, size, NGX_ESCAPE_URI_COMPONENT);
+}
+#else /* !NGX_ESCAPE_URI_COMPONENT */
+static uintptr_t
+ngx_fancyindex_escape_filename(u_char *dst, u_char *src, size_t size)
+{
+    /*
+     * The ngx_escape_uri() function will not escape colons or the
+     * ? character, which signals the beginning of the query string.
+     * So we handle those characters ourselves.
+     *
+     * TODO: Get rid of this once ngx_escape_uri() works as expected!
+     */
+
+    u_int escapes = 0;
+    u_char *psrc = src;
+    size_t psize = size;
+
+    while (psize--) {
+        switch (*psrc++) {
+            case ':':
+            case '?':
+            case '[':
+            case ']':
+                escapes++;
+                break;
+        }
+    }
+
+    if (dst == NULL) {
+        return escapes + ngx_escape_uri(NULL, src, size, NGX_ESCAPE_HTML);
+    }
+    else if (escapes == 0) {
+        /* No need to do extra escaping, avoid the temporary buffer */
+        return ngx_escape_uri(dst, src, size, NGX_ESCAPE_HTML);
+    }
+    else {
+        uintptr_t uescapes = ngx_escape_uri(NULL, src, size, NGX_ESCAPE_HTML);
+        size_t bufsz = size + 2 * uescapes;
+
+        /*
+         * GCC and CLANG both support stack-allocated variable length
+         * arrays. Take advantage of that to avoid a malloc-free cycle.
+         */
+#if defined(__GNUC__) || defined(__clang__)
+        u_char cbuf[bufsz];
+        u_char *buf = cbuf;
+#else  /* __GNUC__ || __clang__ */
+        u_char *buf = (u_char*) malloc(sizeof(u_char) * bufsz);
+#endif /* __GNUC__ || __clang__ */
+
+        ngx_escape_uri(buf, src, size, NGX_ESCAPE_HTML);
+
+        while (bufsz--) {
+            switch (*buf) {
+                case ':':
+                    *dst++ = '%';
+                    *dst++ = '3';
+                    *dst++ = 'A';
+                    break;
+                case '?':
+                    *dst++ = '%';
+                    *dst++ = '3';
+                    *dst++ = 'F';
+                    break;
+                case '[':
+                    *dst++ = '%';
+                    *dst++ = '5';
+                    *dst++ = 'B';
+                    break;
+                case ']':
+                    *dst++ = '%';
+                    *dst++ = '5';
+                    *dst++ = 'D';
+                    break;
+                default:
+                    *dst++ = *buf;
+            }
+            buf++;
+        }
+
+#if !defined(__GNUC__) && !defined(__clang__)
+        free(buf);
+#endif /* !__GNUC__ && !__clang__ */
+
+        return escapes + uescapes;
+    }
+}
+#endif /* NGX_ESCAPE_URI_COMPONENT */
+
+
+static ngx_inline ngx_buf_t*
+make_header_buf(ngx_http_request_t *r, const ngx_str_t css_href)
+{
+    ngx_buf_t *b;
+    size_t blen = r->uri.len
+        + ngx_sizeof_ssz(t01_head1)
+        + ngx_sizeof_ssz(t02_head2)
+        + ngx_sizeof_ssz(t03_head3)
+        + ngx_sizeof_ssz(t04_body1)
+        ;
+
+    if (css_href.len) {
+        blen += css_href_pre.len \
+              + css_href.len \
+              + css_href_post.len
+              ;
+    }
+
+    if ((b = ngx_create_temp_buf(r->pool, blen)) == NULL)
+        return NULL;
+
+    b->last = ngx_cpymem_ssz(b->last, t01_head1);
+
+    if (css_href.len) {
+        b->last = ngx_cpymem_str(b->last, css_href_pre);
+        b->last = ngx_cpymem_str(b->last, css_href);
+        b->last = ngx_cpymem_str(b->last, css_href_post);
+    }
+
+    b->last = ngx_cpymem_ssz(b->last, t02_head2);
+    b->last = ngx_cpymem_str(b->last, r->uri);
+    b->last = ngx_cpymem_ssz(b->last, t03_head3);
+    b->last = ngx_cpymem_ssz(b->last, t04_body1);
+
+    return b;
+}
+
+
+static ngx_inline ngx_int_t
+make_content_buf(
+        ngx_http_request_t *r, ngx_buf_t **pb,
+        ngx_http_fancyindex_loc_conf_t *alcf)
+{
+    ngx_http_fancyindex_entry_t *entry;
+
+    int (*sort_cmp_func)(const void *, const void *);
+    const char  *sort_url_args = "";
+
+    off_t        length;
+    size_t       len, root, allocated, escape_html;
+    int64_t      multiplier;
+    u_char      *filename, *last;
+    ngx_tm_t     tm;
+    ngx_array_t  entries;
+    ngx_time_t  *tp;
+    ngx_uint_t   i, j;
+    ngx_str_t    path;
+    ngx_dir_t    dir;
+    ngx_buf_t   *b;
+
+    static const char    *sizes[]  = { "EiB", "PiB", "TiB", "GiB", "MiB", "KiB", "B" };
+    static const int64_t  exbibyte = 1024LL * 1024LL * 1024LL *
+                                     1024LL * 1024LL * 1024LL;
+
+    /*
+     * NGX_DIR_MASK_LEN is lesser than NGX_HTTP_FANCYINDEX_PREALLOCATE
+     */
+    if ((last = ngx_http_map_uri_to_path(r, &path, &root,
+                    NGX_HTTP_FANCYINDEX_PREALLOCATE)) == NULL)
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+
+    allocated = path.len;
+    path.len = last - path.data;
+    if (path.len > 1) {
+        path.len--;
+    }
+    path.data[path.len] = '\0';
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                   "http fancyindex: \"%s\"", path.data);
+
+    if (ngx_open_dir(&path, &dir) == NGX_ERROR) {
+        ngx_int_t rc, err = ngx_errno;
+        ngx_uint_t level;
+
+        if (err == NGX_ENOENT || err == NGX_ENOTDIR || err == NGX_ENAMETOOLONG) {
+            level = NGX_LOG_ERR;
+            rc = NGX_HTTP_NOT_FOUND;
+        } else if (err == NGX_EACCES) {
+            level = NGX_LOG_ERR;
+            rc = NGX_HTTP_FORBIDDEN;
+        } else {
+            level = NGX_LOG_CRIT;
+            rc = NGX_HTTP_INTERNAL_SERVER_ERROR;
+        }
+
+        ngx_log_error(level, r->connection->log, err,
+                ngx_open_dir_n " \"%s\" failed", path.data);
+
+        return rc;
+    }
+
+#if (NGX_SUPPRESS_WARN)
+    /* MSVC thinks 'entries' may be used without having been initialized */
+    ngx_memzero(&entries, sizeof(ngx_array_t));
+#endif /* NGX_SUPPRESS_WARN */
+
+
+    if (ngx_array_init(&entries, r->pool, 40,
+                sizeof(ngx_http_fancyindex_entry_t)) != NGX_OK)
+        return ngx_http_fancyindex_error(r, &dir, &path);
+
+    filename = path.data;
+    filename[path.len] = '/';
+
+    /* Read directory entries and their associated information. */
+    for (;;) {
+        ngx_set_errno(0);
+
+        if (ngx_read_dir(&dir) == NGX_ERROR) {
+            ngx_int_t err = ngx_errno;
+
+            if (err != NGX_ENOMOREFILES) {
+                ngx_log_error(NGX_LOG_CRIT, r->connection->log, err,
+                        ngx_read_dir_n " \"%V\" failed", &path);
+                return ngx_http_fancyindex_error(r, &dir, &path);
+            }
+            break;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                       "http fancyindex file: \"%s\"", ngx_de_name(&dir));
+
+        len = ngx_de_namelen(&dir);
+
+        if (!alcf->show_dot_files && ngx_de_name(&dir)[0] == '.')
+            continue;
+
+        if (alcf->hide_symlinks && ngx_de_is_link (&dir))
+            continue;
+
+#if NGX_PCRE
+        {
+            ngx_str_t str;
+            str.len = len;
+            str.data = ngx_de_name(&dir);
+
+            if (alcf->ignore && ngx_regex_exec_array(alcf->ignore, &str,
+                                                     r->connection->log)
+                != NGX_DECLINED)
+            {
+                continue;
+            }
+        }
+#else /* !NGX_PCRE */
+        if (alcf->ignore) {
+            u_int match_found = 0;
+            ngx_str_t *s = alcf->ignore->elts;
+
+            for (i = 0; i < alcf->ignore->nelts; i++, s++) {
+                if (ngx_strcmp(ngx_de_name(&dir), s->data) == 0) {
+                    match_found = 1;
+                    break;
+                }
+            }
+
+            if (match_found) {
+                continue;
+            }
+        }
+#endif /* NGX_PCRE */
+
+        if (!dir.valid_info) {
+            /* 1 byte for '/' and 1 byte for terminating '\0' */
+            if (path.len + 1 + len + 1 > allocated) {
+                allocated = path.len + 1 + len + 1
+                          + NGX_HTTP_FANCYINDEX_PREALLOCATE;
+
+                if ((filename = ngx_palloc(r->pool, allocated)) == NULL)
+                    return ngx_http_fancyindex_error(r, &dir, &path);
+
+                last = ngx_cpystrn(filename, path.data, path.len + 1);
+                *last++ = '/';
+            }
+
+            ngx_cpystrn(last, ngx_de_name(&dir), len + 1);
+
+            if (ngx_de_info(filename, &dir) == NGX_FILE_ERROR) {
+                ngx_int_t err = ngx_errno;
+
+                if (err != NGX_ENOENT) {
+                    ngx_log_error(NGX_LOG_ERR, r->connection->log, err,
+                            ngx_de_info_n " \"%s\" failed", filename);
+                    continue;
+                }
+
+                if (ngx_de_link_info(filename, &dir) == NGX_FILE_ERROR) {
+                    ngx_log_error(NGX_LOG_CRIT, r->connection->log, ngx_errno,
+                            ngx_de_link_info_n " \"%s\" failed", filename);
+                    return ngx_http_fancyindex_error(r, &dir, &path);
+                }
+            }
+        }
+
+        if ((entry = ngx_array_push(&entries)) == NULL)
+            return ngx_http_fancyindex_error(r, &dir, &path);
+
+        entry->name.len  = len;
+        entry->name.data = ngx_palloc(r->pool, len + 1);
+        if (entry->name.data == NULL)
+            return ngx_http_fancyindex_error(r, &dir, &path);
+
+        ngx_cpystrn(entry->name.data, ngx_de_name(&dir), len + 1);
+        entry->escape = 2 * ngx_fancyindex_escape_filename(NULL,
+                                                           ngx_de_name(&dir),
+                                                           len);
+        entry->escape_html = ngx_escape_html(NULL,
+                                             entry->name.data,
+                                             entry->name.len);
+
+        entry->dir     = ngx_de_is_dir(&dir);
+        entry->mtime   = ngx_de_mtime(&dir);
+        entry->size    = ngx_de_size(&dir);
+        entry->utf_len = (r->headers_out.charset.len == 5 &&
+                ngx_strncasecmp(r->headers_out.charset.data, (u_char*) "utf-8", 5) == 0)
+            ?  ngx_utf8_length(entry->name.data, entry->name.len)
+            : len;
+    }
+
+    if (ngx_close_dir(&dir) == NGX_ERROR) {
+        ngx_log_error(NGX_LOG_ALERT, r->connection->log, ngx_errno,
+                ngx_close_dir_n " \"%s\" failed", &path);
+    }
+
+    /*
+     * Calculate needed buffer length.
+     */
+
+    escape_html = ngx_escape_html(NULL, r->uri.data, r->uri.len);
+
+    if (alcf->show_path)
+        len = r->uri.len + escape_html
+          + ngx_sizeof_ssz(t05_body2)
+          + ngx_sizeof_ssz(t06_list1)
+          + ngx_sizeof_ssz(t_parentdir_entry)
+          + ngx_sizeof_ssz(t07_list2)
+          + ngx_fancyindex_timefmt_calc_size (&alcf->time_format) * entries.nelts
+          ;
+   else
+        len = r->uri.len + escape_html
+          + ngx_sizeof_ssz(t06_list1)
+          + ngx_sizeof_ssz(t_parentdir_entry)
+          + ngx_sizeof_ssz(t07_list2)
+          + ngx_fancyindex_timefmt_calc_size (&alcf->time_format) * entries.nelts
+          ;
+
+    /*
+     * If we are a the root of the webserver (URI =  "/" --> length of 1),
+     * do not display the "Parent Directory" link.
+     */
+    if (r->uri.len == 1) {
+        len -= ngx_sizeof_ssz(t_parentdir_entry);
+    }
+
+    entry = entries.elts;
+    for (i = 0; i < entries.nelts; i++) {
+        /*
+         * Genearated table rows are as follows, unneeded whitespace
+         * is stripped out:
+         *
+         *   
+         *     fname
+         *     sizedate
+         *   
+         */
+        len += ngx_sizeof_ssz("")
+            + entry[i].name.len + entry[i].utf_len + entry[i].escape_html
+            + ngx_sizeof_ssz("")
+            + 20 /* File size */
+            + ngx_sizeof_ssz("")    /* Date prefix */
+            + ngx_sizeof_ssz("\n") /* Date suffix */
+            + 2 /* CR LF */
+            ;
+    }
+
+    if ((b = ngx_create_temp_buf(r->pool, len)) == NULL)
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+
+    /*
+     * Determine the sorting criteria. URL arguments look like:
+     *
+     *    C=x[&O=y]
+     *
+     * Where x={M,S,N} and y={A,D}
+     */
+    if ((r->args.len == 3 || (r->args.len == 7 && r->args.data[3] == '&')) &&
+        r->args.data[0] == 'C' && r->args.data[1] == '=')
+    {
+        /* Determine whether the direction of the sorting */
+        ngx_int_t sort_descending = r->args.len == 7
+                                 && r->args.data[4] == 'O'
+                                 && r->args.data[5] == '='
+                                 && r->args.data[6] == 'D';
+
+        /* Pick the sorting criteria */
+        switch (r->args.data[2]) {
+            case 'M': /* Sort by mtime */
+                if (sort_descending) {
+                    sort_cmp_func = ngx_http_fancyindex_cmp_entries_mtime_desc;
+                    if (alcf->default_sort != NGX_HTTP_FANCYINDEX_SORT_CRITERION_DATE_DESC)
+                        sort_url_args = "?C=M&O=D";
+                }
+                else {
+                    sort_cmp_func = ngx_http_fancyindex_cmp_entries_mtime_asc;
+                    if (alcf->default_sort != NGX_HTTP_FANCYINDEX_SORT_CRITERION_DATE)
+                        sort_url_args = "?C=M&O=A";
+                }
+                break;
+            case 'S': /* Sort by size */
+                if (sort_descending) {
+                    sort_cmp_func = ngx_http_fancyindex_cmp_entries_size_desc;
+                    if (alcf->default_sort != NGX_HTTP_FANCYINDEX_SORT_CRITERION_SIZE_DESC)
+                        sort_url_args = "?C=S&O=D";
+                }
+                else {
+                    sort_cmp_func = ngx_http_fancyindex_cmp_entries_size_asc;
+                        if (alcf->default_sort != NGX_HTTP_FANCYINDEX_SORT_CRITERION_SIZE)
+                    sort_url_args = "?C=S&O=A";
+                }
+                break;
+            case 'N': /* Sort by name */
+            default:
+                if (sort_descending) {
+                    sort_cmp_func = alcf->case_sensitive
+                        ? ngx_http_fancyindex_cmp_entries_name_cs_desc
+                        : ngx_http_fancyindex_cmp_entries_name_ci_desc;
+                    if (alcf->default_sort != NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME_DESC)
+                        sort_url_args = "?C=N&O=D";
+                }
+                else {
+                    sort_cmp_func = alcf->case_sensitive
+                        ? ngx_http_fancyindex_cmp_entries_name_cs_asc
+                        : ngx_http_fancyindex_cmp_entries_name_ci_asc;
+                    if (alcf->default_sort != NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME)
+                        sort_url_args = "?C=N&O=A";
+                }
+                break;
+        }
+    }
+    else {
+        switch (alcf->default_sort) {
+            case NGX_HTTP_FANCYINDEX_SORT_CRITERION_DATE_DESC:
+                sort_cmp_func = ngx_http_fancyindex_cmp_entries_mtime_desc;
+                break;
+            case NGX_HTTP_FANCYINDEX_SORT_CRITERION_DATE:
+                sort_cmp_func = ngx_http_fancyindex_cmp_entries_mtime_asc;
+                break;
+            case NGX_HTTP_FANCYINDEX_SORT_CRITERION_SIZE_DESC:
+                sort_cmp_func = ngx_http_fancyindex_cmp_entries_size_desc;
+                break;
+            case NGX_HTTP_FANCYINDEX_SORT_CRITERION_SIZE:
+                sort_cmp_func = ngx_http_fancyindex_cmp_entries_size_asc;
+                break;
+            case NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME_DESC:
+                sort_cmp_func = alcf->case_sensitive
+                    ? ngx_http_fancyindex_cmp_entries_name_cs_desc
+                    : ngx_http_fancyindex_cmp_entries_name_ci_desc;
+                break;
+            case NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME:
+            default:
+                sort_cmp_func = alcf->case_sensitive
+                    ? ngx_http_fancyindex_cmp_entries_name_cs_asc
+                    : ngx_http_fancyindex_cmp_entries_name_ci_asc;
+                break;
+        }
+    }
+
+    /* Sort entries, if needed */
+    if (entries.nelts > 1) {
+        if (alcf->dirs_first)
+        {
+            ngx_http_fancyindex_entry_t *l, *r;
+
+            l = entry;
+            r = entry + entries.nelts - 1;
+            while (l < r)
+            {
+                while (l < r && l->dir)
+                    l++;
+                while (l < r && !r->dir)
+                    r--;
+                if (l < r) {
+                    /* Now l points a file while r points a directory */
+                    ngx_http_fancyindex_entry_t tmp;
+                    tmp = *l;
+                    *l = *r;
+                    *r = tmp;
+                }
+            }
+            if (r->dir)
+                r++;
+
+            if (r > entry)
+                /* Sort directories */
+                ngx_qsort(entry, (size_t)(r - entry),
+                        sizeof(ngx_http_fancyindex_entry_t), sort_cmp_func);
+            if (r < entry + entries.nelts)
+                /* Sort files */
+                ngx_qsort(r, (size_t)(entry + entries.nelts - r),
+                        sizeof(ngx_http_fancyindex_entry_t), sort_cmp_func);
+        } else {
+            ngx_qsort(entry, (size_t)entries.nelts,
+                    sizeof(ngx_http_fancyindex_entry_t), sort_cmp_func);
+        }
+    }
+
+    /* Display the path, if needed */
+    if (alcf->show_path){
+        b->last = last = (u_char *) ngx_escape_html(b->last, r->uri.data, r->uri.len);
+        b->last = ngx_cpymem_ssz(b->last, t05_body2);
+    }
+
+    /* Open the  tag */
+    b->last = ngx_cpymem_ssz(b->last, t06_list1);
+
+    tp = ngx_timeofday();
+
+    /* "Parent dir" entry, always first if displayed */
+    if (r->uri.len > 1 && alcf->hide_parent == 0) {
+        b->last = ngx_cpymem_ssz(b->last,
+                                 ""
+                                 ""
+                                 ""
+                                 ""
+                                 ""
+                                 CRLF);
+    }
+
+    /* Entries for directories and files */
+    for (i = 0; i < entries.nelts; i++) {
+        b->last = ngx_cpymem_ssz(b->last, "");
+
+        *b->last++ = CR;
+        *b->last++ = LF;
+    }
+
+    /* Output table bottom */
+    b->last = ngx_cpymem_ssz(b->last, t07_list2);
+
+    *pb = b;
+    return NGX_OK;
+}
+
+
+
+static ngx_int_t
+ngx_http_fancyindex_handler(ngx_http_request_t *r)
+{
+    ngx_http_request_t             *sr;
+    ngx_str_t                      *sr_uri;
+    ngx_str_t                       rel_uri;
+    ngx_int_t                       rc;
+    ngx_http_fancyindex_loc_conf_t *alcf;
+    ngx_chain_t                     out[3] = {
+        { NULL, NULL }, { NULL, NULL}, { NULL, NULL }};
+
+
+    if (r->uri.data[r->uri.len - 1] != '/') {
+        return NGX_DECLINED;
+    }
+
+    /* TODO: Win32 */
+#if defined(nginx_version) \
+    && ((nginx_version < 7066) \
+        || ((nginx_version > 8000) && (nginx_version < 8038)))
+    if (r->zero_in_uri) {
+        return NGX_DECLINED;
+    }
+#endif
+
+    if (!(r->method & (NGX_HTTP_GET|NGX_HTTP_HEAD))) {
+        return NGX_DECLINED;
+    }
+
+    alcf = ngx_http_get_module_loc_conf(r, ngx_http_fancyindex_module);
+
+    if (!alcf->enable) {
+        return NGX_DECLINED;
+    }
+
+    if ((rc = make_content_buf(r, &out[0].buf, alcf)) != NGX_OK)
+        return rc;
+
+    out[0].buf->last_in_chain = 1;
+
+    r->headers_out.status = NGX_HTTP_OK;
+    r->headers_out.content_type_len  = ngx_sizeof_ssz("text/html");
+    r->headers_out.content_type.len  = ngx_sizeof_ssz("text/html");
+    r->headers_out.content_type.data = (u_char *) "text/html";
+
+    rc = ngx_http_send_header(r);
+    if (rc == NGX_ERROR || rc > NGX_OK || r->header_only)
+        return rc;
+
+    if (alcf->header.path.len > 0 && alcf->header.local.len == 0) {
+        /* URI is configured, make Nginx take care of with a subrequest. */
+        sr_uri = &alcf->header.path;
+
+        if (*sr_uri->data != '/') {
+            /* Relative path */
+            rel_uri.len  = r->uri.len + alcf->header.path.len;
+            rel_uri.data = ngx_palloc(r->pool, rel_uri.len);
+            if (rel_uri.data == NULL) {
+                return NGX_HTTP_INTERNAL_SERVER_ERROR;
+            }
+            ngx_memcpy(ngx_cpymem(rel_uri.data, r->uri.data, r->uri.len),
+                    alcf->header.path.data, alcf->header.path.len);
+            sr_uri = &rel_uri;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                "http fancyindex: header subrequest \"%V\"", sr_uri);
+
+        rc = ngx_http_subrequest(r, sr_uri, NULL, &sr, NULL, 0);
+        if (rc == NGX_ERROR || rc == NGX_DONE) {
+            ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                    "http fancyindex: header subrequest for \"%V\" failed", sr_uri);
+            return rc;
+        }
+
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                "http fancyindex: header subrequest status = %i",
+                sr->headers_out.status);
+        /* ngx_http_subrequest returns NGX_OK(0), not NGX_HTTP_OK(200) */
+        if (sr->headers_out.status != NGX_OK) {
+            /*
+             * XXX: Should we write a message to the error log just in case
+             * we get something different from a 404?
+             */
+            goto add_builtin_header;
+        }
+    }
+    else {
+add_builtin_header:
+        /* Make space before */
+        out[1].next = out[0].next;
+        out[1].buf  = out[0].buf;
+        /* Chain header buffer */
+        out[0].next = &out[1];
+        if (alcf->header.local.len > 0) {
+            /* Header buffer is local, make a buffer pointing to the data. */
+            out[0].buf = ngx_calloc_buf(r->pool);
+            if (out[0].buf == NULL)
+                return NGX_ERROR;
+            out[0].buf->memory = 1;
+            out[0].buf->pos = alcf->header.local.data;
+            out[0].buf->last = alcf->header.local.data + alcf->header.local.len;
+        } else {
+            /* Prepare a buffer with the contents of the builtin header. */
+            out[0].buf = make_header_buf(r, alcf->css_href);
+        }
+    }
+
+    /* If footer is disabled, chain up footer buffer. */
+    if (alcf->footer.path.len == 0 || alcf->footer.local.len > 0) {
+        ngx_uint_t last = (alcf->header.path.len == 0) ? 2 : 1;
+
+        out[last-1].next = &out[last];
+        out[last].buf = ngx_calloc_buf(r->pool);
+        if (out[last].buf == NULL)
+            return NGX_ERROR;
+
+        out[last].buf->memory = 1;
+        if (alcf->footer.local.len > 0) {
+            out[last].buf->pos = alcf->footer.local.data;
+            out[last].buf->last = alcf->footer.local.data + alcf->footer.local.len;
+        } else {
+            out[last].buf->pos = (u_char*) t08_foot1;
+            out[last].buf->last = (u_char*) t08_foot1 + sizeof(t08_foot1) - 1;
+        }
+
+        out[last-1].buf->last_in_chain = 0;
+        out[last].buf->last_in_chain   = 1;
+        out[last].buf->last_buf        = 1;
+        /* Send everything with a single call :D */
+        return ngx_http_output_filter(r, &out[0]);
+    }
+
+    /*
+     * If we reach here, we were asked to send a custom footer. We need to:
+     * partially send whatever is referenced from out[0] and then send the
+     * footer as a subrequest. If the subrequest fails, we should send the
+     * standard footer as well.
+     */
+    rc = ngx_http_output_filter(r, &out[0]);
+
+    if (rc != NGX_OK && rc != NGX_AGAIN)
+        return NGX_HTTP_INTERNAL_SERVER_ERROR;
+
+    /* URI is configured, make Nginx take care of with a subrequest. */
+    sr_uri = &alcf->footer.path;
+
+    if (*sr_uri->data != '/') {
+        /* Relative path */
+        rel_uri.len  = r->uri.len + alcf->footer.path.len;
+        rel_uri.data = ngx_palloc(r->pool, rel_uri.len);
+        if (rel_uri.data == NULL) {
+            return NGX_HTTP_INTERNAL_SERVER_ERROR;
+        }
+        ngx_memcpy(ngx_cpymem(rel_uri.data, r->uri.data, r->uri.len),
+                alcf->footer.path.data, alcf->footer.path.len);
+        sr_uri = &rel_uri;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+            "http fancyindex: footer subrequest \"%V\"", sr_uri);
+
+    rc = ngx_http_subrequest(r, sr_uri, NULL, &sr, NULL, 0);
+    if (rc == NGX_ERROR || rc == NGX_DONE) {
+        ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+                "http fancyindex: footer subrequest for \"%V\" failed", sr_uri);
+        return rc;
+    }
+
+    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+            "http fancyindex: header subrequest status = %i",
+            sr->headers_out.status);
+
+    /* see above: ngx_http_subrequest resturns NGX_OK (0) not NGX_HTTP_OK (200) */
+    if (sr->headers_out.status != NGX_OK) {
+        /*
+         * XXX: Should we write a message to the error log just in case
+         * we get something different from a 404?
+         */
+        out[0].next = NULL;
+        out[0].buf = ngx_calloc_buf(r->pool);
+        if (out[0].buf == NULL)
+            return NGX_ERROR;
+        out[0].buf->memory = 1;
+        out[0].buf->pos = (u_char*) t08_foot1;
+        out[0].buf->last = (u_char*) t08_foot1 + sizeof(t08_foot1) - 1;
+        out[0].buf->last_in_chain = 1;
+        out[0].buf->last_buf = 1;
+        /* Directly send out the builtin footer */
+        return ngx_http_output_filter(r, &out[0]);
+    }
+
+    return (r != r->main) ? rc : ngx_http_send_special(r, NGX_HTTP_LAST);
+}
+
+
+static int ngx_libc_cdecl
+ngx_http_fancyindex_cmp_entries_name_cs_desc(const void *one, const void *two)
+{
+    ngx_http_fancyindex_entry_t *first = (ngx_http_fancyindex_entry_t *) one;
+    ngx_http_fancyindex_entry_t *second = (ngx_http_fancyindex_entry_t *) two;
+
+    return (int) ngx_strcmp(second->name.data, first->name.data);
+}
+
+
+static int ngx_libc_cdecl
+ngx_http_fancyindex_cmp_entries_name_ci_desc(const void *one, const void *two)
+{
+    ngx_http_fancyindex_entry_t *first = (ngx_http_fancyindex_entry_t *) one;
+    ngx_http_fancyindex_entry_t *second = (ngx_http_fancyindex_entry_t *) two;
+
+    return (int) ngx_strcasecmp(second->name.data, first->name.data);
+}
+
+
+static int ngx_libc_cdecl
+ngx_http_fancyindex_cmp_entries_size_desc(const void *one, const void *two)
+{
+    ngx_http_fancyindex_entry_t *first = (ngx_http_fancyindex_entry_t *) one;
+    ngx_http_fancyindex_entry_t *second = (ngx_http_fancyindex_entry_t *) two;
+
+    return (first->size < second->size) - (first->size > second->size);
+}
+
+
+static int ngx_libc_cdecl
+ngx_http_fancyindex_cmp_entries_mtime_desc(const void *one, const void *two)
+{
+    ngx_http_fancyindex_entry_t *first = (ngx_http_fancyindex_entry_t *) one;
+    ngx_http_fancyindex_entry_t *second = (ngx_http_fancyindex_entry_t *) two;
+
+    return (int) (second->mtime - first->mtime);
+}
+
+
+static int ngx_libc_cdecl
+ngx_http_fancyindex_cmp_entries_name_cs_asc(const void *one, const void *two)
+{
+    ngx_http_fancyindex_entry_t *first = (ngx_http_fancyindex_entry_t *) one;
+    ngx_http_fancyindex_entry_t *second = (ngx_http_fancyindex_entry_t *) two;
+
+    return (int) ngx_strcmp(first->name.data, second->name.data);
+}
+
+
+static int ngx_libc_cdecl
+ngx_http_fancyindex_cmp_entries_name_ci_asc(const void *one, const void *two)
+{
+    ngx_http_fancyindex_entry_t *first = (ngx_http_fancyindex_entry_t *) one;
+    ngx_http_fancyindex_entry_t *second = (ngx_http_fancyindex_entry_t *) two;
+
+    return (int) ngx_strcasecmp(first->name.data, second->name.data);
+}
+
+
+static int ngx_libc_cdecl
+ngx_http_fancyindex_cmp_entries_size_asc(const void *one, const void *two)
+{
+    ngx_http_fancyindex_entry_t *first = (ngx_http_fancyindex_entry_t *) one;
+    ngx_http_fancyindex_entry_t *second = (ngx_http_fancyindex_entry_t *) two;
+
+    return (first->size > second->size) - (first->size < second->size);
+}
+
+
+static int ngx_libc_cdecl
+ngx_http_fancyindex_cmp_entries_mtime_asc(const void *one, const void *two)
+{
+    ngx_http_fancyindex_entry_t *first = (ngx_http_fancyindex_entry_t *) one;
+    ngx_http_fancyindex_entry_t *second = (ngx_http_fancyindex_entry_t *) two;
+
+    return (int) (first->mtime - second->mtime);
+}
+
+
+static ngx_int_t
+ngx_http_fancyindex_error(ngx_http_request_t *r, ngx_dir_t *dir, ngx_str_t *name)
+{
+    if (ngx_close_dir(dir) == NGX_ERROR) {
+        ngx_log_error(NGX_LOG_ALERT, r->connection->log, ngx_errno,
+                      ngx_close_dir_n " \"%V\" failed", name);
+    }
+
+    return NGX_HTTP_INTERNAL_SERVER_ERROR;
+}
+
+
+static void *
+ngx_http_fancyindex_create_loc_conf(ngx_conf_t *cf)
+{
+    ngx_http_fancyindex_loc_conf_t  *conf;
+
+    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_fancyindex_loc_conf_t));
+    if (conf == NULL) {
+        return NGX_CONF_ERROR;
+    }
+
+    /*
+     * Set by ngx_pcalloc:
+     *    conf->header.*.len     = 0
+     *    conf->header.*.data    = NULL
+     *    conf->footer.*.len     = 0
+     *    conf->footer.*.data    = NULL
+     *    conf->css_href.len     = 0
+     *    conf->css_href.data    = NULL
+     *    conf->time_format.len  = 0
+     *    conf->time_format.data = NULL
+     */
+    conf->enable         = NGX_CONF_UNSET;
+    conf->default_sort   = NGX_CONF_UNSET_UINT;
+    conf->case_sensitive = NGX_CONF_UNSET;
+    conf->dirs_first     = NGX_CONF_UNSET;
+    conf->localtime      = NGX_CONF_UNSET;
+    conf->exact_size     = NGX_CONF_UNSET;
+    conf->ignore         = NGX_CONF_UNSET_PTR;
+    conf->hide_symlinks  = NGX_CONF_UNSET;
+    conf->show_path      = NGX_CONF_UNSET;
+    conf->hide_parent    = NGX_CONF_UNSET;
+    conf->show_dot_files = NGX_CONF_UNSET;
+
+    return conf;
+}
+
+
+static char *
+ngx_http_fancyindex_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
+{
+    ngx_http_fancyindex_loc_conf_t *prev = parent;
+    ngx_http_fancyindex_loc_conf_t *conf = child;
+
+    (void) cf; /* unused */
+
+    ngx_conf_merge_value(conf->enable, prev->enable, 0);
+    ngx_conf_merge_uint_value(conf->default_sort, prev->default_sort, NGX_HTTP_FANCYINDEX_SORT_CRITERION_NAME);
+    ngx_conf_merge_value(conf->case_sensitive, prev->case_sensitive, 1);
+    ngx_conf_merge_value(conf->dirs_first, prev->dirs_first, 1);
+    ngx_conf_merge_value(conf->localtime, prev->localtime, 0);
+    ngx_conf_merge_value(conf->exact_size, prev->exact_size, 1);
+    ngx_conf_merge_value(conf->show_path, prev->show_path, 1);
+    ngx_conf_merge_value(conf->show_dot_files, prev->show_dot_files, 0);
+
+    ngx_conf_merge_str_value(conf->header.path, prev->header.path, "");
+    ngx_conf_merge_str_value(conf->header.path, prev->header.local, "");
+    ngx_conf_merge_str_value(conf->footer.path, prev->footer.path, "");
+    ngx_conf_merge_str_value(conf->footer.path, prev->footer.local, "");
+
+    ngx_conf_merge_str_value(conf->css_href, prev->css_href, "");
+    ngx_conf_merge_str_value(conf->time_format, prev->time_format, "%Y-%b-%d %H:%M");
+
+    ngx_conf_merge_ptr_value(conf->ignore, prev->ignore, NULL);
+    ngx_conf_merge_value(conf->hide_symlinks, prev->hide_symlinks, 0);
+    ngx_conf_merge_value(conf->hide_parent, prev->hide_parent, 0);
+
+    /* Just make sure we haven't disabled the show_path directive without providing a custom header */
+    if (conf->show_path == 0 && conf->header.path.len == 0)
+    {
+        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "FancyIndex : cannot set show_path to off without providing a custom header !");
+        return NGX_CONF_ERROR;
+    }
+
+    return NGX_CONF_OK;
+}
+
+
+static char*
+ngx_http_fancyindex_ignore(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
+{
+    ngx_http_fancyindex_loc_conf_t *alcf = conf;
+    ngx_str_t *value;
+
+    (void) cmd; /* unused */
+
+#if (NGX_PCRE)
+    ngx_uint_t          i;
+    ngx_regex_elt_t    *re;
+    ngx_regex_compile_t rc;
+    u_char              errstr[NGX_MAX_CONF_ERRSTR];
+
+    if (alcf->ignore == NGX_CONF_UNSET_PTR) {
+        alcf->ignore = ngx_array_create(cf->pool, 2, sizeof(ngx_regex_elt_t));
+        if (alcf->ignore == NULL) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    value = cf->args->elts;
+
+    ngx_memzero(&rc, sizeof(ngx_regex_compile_t));
+
+    rc.err.data = errstr;
+    rc.err.len  = NGX_MAX_CONF_ERRSTR;
+    rc.pool     = cf->pool;
+
+    for (i = 1; i < cf->args->nelts; i++) {
+        re = ngx_array_push(alcf->ignore);
+        if (re == NULL) {
+            return NGX_CONF_ERROR;
+        }
+
+        rc.pattern = value[i];
+        rc.options = NGX_REGEX_CASELESS;
+
+        if (ngx_regex_compile(&rc) != NGX_OK) {
+            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "%V", &rc.err);
+            return NGX_CONF_ERROR;
+        }
+
+        re->name  = value[i].data;
+        re->regex = rc.regex;
+    }
+
+    return NGX_CONF_OK;
+#else /* !NGX_PCRE */
+    ngx_uint_t i;
+    ngx_str_t *str;
+
+    if (alcf->ignore == NGX_CONF_UNSET_PTR) {
+        alcf->ignore = ngx_array_create(cf->pool, 2, sizeof(ngx_str_t));
+        if (alcf->ignore == NULL) {
+            return NGX_CONF_ERROR;
+        }
+    }
+
+    value = cf->args->elts;
+
+    for (i = 1; i < cf->args->nelts; i++) {
+        str = ngx_array_push(alcf->ignore);
+        if (str == NULL) {
+            return NGX_CONF_ERROR;
+        }
+
+        str->data = value[i].data;
+        str->len  = value[i].len;
+    }
+
+    return NGX_CONF_OK;
+#endif /* NGX_PCRE */
+
+}
+
+
+static ngx_int_t
+ngx_http_fancyindex_init(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_http_fancyindex_handler;
+
+    return NGX_OK;
+}
+
+/* vim:et:sw=4:ts=4:
+ */
diff --git a/ngx_fancyindex/t/00-build-artifacts.test b/ngx_fancyindex/t/00-build-artifacts.test
new file mode 100644
index 0000000..b9bc1ac
--- /dev/null
+++ b/ngx_fancyindex/t/00-build-artifacts.test
@@ -0,0 +1,22 @@
+#! /bin/bash
+cat <<---
+This test checks that the built Nginx either has the dynamic fancyindex
+module available, or that it's not there (for static builds).
+--
+
+readonly nginx_path="${PREFIX}/sbin/nginx"
+readonly so_path="${PREFIX}/modules/ngx_http_fancyindex_module.so"
+
+if [[ ! -x ${nginx_path} ]] ; then
+	fail "executable binary not found at '%s'\n" "${nginx_path}"
+fi
+
+if ${DYNAMIC} ; then
+	if [[ ! -r ${so_path} ]] ; then
+		fail "module not found at '%s'\n" "${so_path}"
+	fi
+else
+	if [[ -r ${so_path} ]] ; then
+		fail "module should not exist at '%s'\n" "${so_path}"
+	fi
+fi
diff --git a/ngx_fancyindex/t/01-smoke-hasindex.test b/ngx_fancyindex/t/01-smoke-hasindex.test
new file mode 100644
index 0000000..19706a4
--- /dev/null
+++ b/ngx_fancyindex/t/01-smoke-hasindex.test
@@ -0,0 +1,7 @@
+#! /bin/bash
+cat <<---
+This test fetches the root directory served by Nginx, which has no index file,
+and checks that the output contains something that resembles a directory index.
+--
+nginx_start
+grep 'Index of' <( fetch )
diff --git a/ngx_fancyindex/t/02-smoke-indexisfancy.test b/ngx_fancyindex/t/02-smoke-indexisfancy.test
new file mode 100644
index 0000000..47972d8
--- /dev/null
+++ b/ngx_fancyindex/t/02-smoke-indexisfancy.test
@@ -0,0 +1,11 @@
+#! /bin/bash
+cat <<---
+This test fetches the root directory served by Nginx, which has no index file,
+and checks that the output contains something that resembles the output from
+the fancyindex module.
+--
+nginx_start
+content=$(fetch --with-headers)
+grep 'Index of /' <<< "${content}"  # It is an index
+grep ''   <<< "${content}"  # It contains a table
+grep '^  Content-Type:[[:space:]]*text/html' <<< "${content}"
diff --git a/ngx_fancyindex/t/03-exact_size_off.test b/ngx_fancyindex/t/03-exact_size_off.test
new file mode 100644
index 0000000..6bf7bcc
--- /dev/null
+++ b/ngx_fancyindex/t/03-exact_size_off.test
@@ -0,0 +1,9 @@
+#! /bin/bash
+cat <<---
+This test checks if the output from using "fancyindex_exact_size off"
+looks sane.
+--
+nginx_start 'fancyindex_exact_size off;'
+content=$(fetch)
+grep -e '[1-9]\.[0-9] KiB'  <<< "${content}"
+grep -E '[0-9]+ B'  <<< "${content}"
diff --git a/ngx_fancyindex/t/04-hasindex-html.test b/ngx_fancyindex/t/04-hasindex-html.test
new file mode 100644
index 0000000..13a0700
--- /dev/null
+++ b/ngx_fancyindex/t/04-hasindex-html.test
@@ -0,0 +1,24 @@
+#! /bin/bash
+cat <<---
+This test fetches the root directory served by Nginx, which has no index
+file, and checks that the output contains a few HTML elements known to
+exist in a directory index.
+--
+use pup
+nginx_start
+
+content=$( fetch )
+
+# Check page title
+[[ $(pup -p title text{} <<< "${content}") = 'Index of /' ]]
+
+# Check table headers
+[[ $(pup -n body table thead th a:first-child <<< "${content}") -eq 3 ]]
+{
+	read -r name_label
+	read -r size_label
+	read -r date_label
+} < <(  pup -p body table thead th a:first-child text{} <<< "${content}" )
+[[ ${name_label} = File\ Name ]]
+[[ ${size_label} = File\ Size ]]
+[[ ${date_label} = Date ]]
diff --git a/ngx_fancyindex/t/05-sort-by-size.test b/ngx_fancyindex/t/05-sort-by-size.test
new file mode 100644
index 0000000..23fade9
--- /dev/null
+++ b/ngx_fancyindex/t/05-sort-by-size.test
@@ -0,0 +1,36 @@
+#! /bin/bash
+cat <<---
+This test validates that the sorting by file size works.
+--
+use pup
+nginx_start
+
+# Ascending sort.
+previous=''
+while read -r size ; do
+	if [[ ${size} = - ]] ; then
+		continue
+	fi
+	if [[ -z ${previous} ]] ; then
+		previous=${size}
+		continue
+	fi
+	[[ ${previous} -le ${size} ]] || fail \
+		'Size %d should be smaller than %d\n' "${previous}" "${size}"
+done < <( fetch '/?C=S&O=A' \
+	    | pup -p body table tbody 'td:nth-child(2)' text{} )
+
+# Descending sort.
+previous=''
+while read -r size ; do
+	if [[ ${size} = - ]] ; then
+		continue
+	fi
+	if [[ -z ${previous} ]] ; then
+		previous=${size}
+		continue
+	fi
+	[[ ${previous} -ge ${size} ]] || fail \
+		'Size %d should be greater than %d\n' "${previous}" "${size}"
+done < <( fetch '/?C=S&O=D' \
+	    | pup -p body table tbody 'td:nth-child(2)' text{} )
diff --git a/ngx_fancyindex/t/06-hide_parent.test b/ngx_fancyindex/t/06-hide_parent.test
new file mode 100644
index 0000000..9e3ad6b
--- /dev/null
+++ b/ngx_fancyindex/t/06-hide_parent.test
@@ -0,0 +1,23 @@
+#! /bin/bash
+cat <<---
+This test checks the output using "fancyindex_hide_parent_dir on".
+--
+use pup
+nginx_start 'fancyindex_hide_parent_dir on;'
+
+content=$( fetch /child-directory/ )
+
+# Check page title
+[[ $(pup -p title text{} <<< "${content}") = "Index of /child-directory/" ]]
+
+# Check table headers
+[[ $(pup -n body table tbody tr:first-child td <<< "${content}") -eq 3 ]]
+{
+	read -r name_label
+	read -r size_label
+	read -r date_label
+} < <(  pup -p body table tbody tr:first-child td text{} <<< "${content}" )
+[[ ${name_label} != Parent\ Directory/ ]]
+[[ ${name_label} = empty-file.txt ]]
+[[ ${size_label} != - ]]
+[[ ${date_label} != - ]]
diff --git a/ngx_fancyindex/t/07-directory-first.test b/ngx_fancyindex/t/07-directory-first.test
new file mode 100644
index 0000000..a7f3080
--- /dev/null
+++ b/ngx_fancyindex/t/07-directory-first.test
@@ -0,0 +1,50 @@
+#! /bin/bash
+cat <<---
+This test checks the output using "fancyindex_directories_first on".
+--
+use pup
+
+for d in "008d" "000d" "004d" ; do
+	mkdir -p "${TESTDIR}/dir_first/${d}"
+done
+for f in "005f" "001f" "003f"; do
+	touch "${TESTDIR}/dir_first/${f}"
+done
+for d in "006d" "002d" ; do
+	mkdir -p "${TESTDIR}/dir_first/${d}"
+done
+
+nginx_start 'fancyindex_directories_first on;'
+previous=''
+cur_type=''
+while read -r name ; do
+	case "$name" in
+	*Parent*)
+		;;
+	*d*)
+		echo "dir $name"
+		[[ "$cur_type" = f ]] && fail 'Directories should come before files'
+		cur_type=d
+		if [[ -z ${previous} ]] ; then
+			previous=${name}
+		else
+			[[ ${previous} < ${name} ]] || fail \
+				'Name %s should come before %s\n' "${previous}" "${name}"
+		fi
+		;;
+	*f*)
+		echo "file $name"
+		[[ -z "$cur_type" ]] && fail 'Directories should come before files'
+		if [[ "$cur_type" = d ]] ; then
+			cur_type=f
+			previous=${name}
+		else
+			[[ ${previous} < ${name} ]] || fail \
+				'Name %s should come before %s\n' "${previous}" "${name}"
+		fi
+		;;
+	esac
+done < <( fetch '/dir_first/' \
+		| pup -p body table tbody 'td:nth-child(1)' text{} )
+
+nginx_is_running || fail "Nginx died"
diff --git a/ngx_fancyindex/t/07-show_dotfiles.test b/ngx_fancyindex/t/07-show_dotfiles.test
new file mode 100644
index 0000000..0ef5529
--- /dev/null
+++ b/ngx_fancyindex/t/07-show_dotfiles.test
@@ -0,0 +1,21 @@
+#! /bin/bash
+cat <<---
+This test checks the option to show dotfiles.
+--
+# Turn it on.
+nginx_start 'fancyindex_show_dotfiles on;'
+on_content=$(fetch /show_dotfiles/)
+nginx_stop
+if [ $(grep '.okay'  <<< "${on_content}") -ne 0 ] ; then
+    exit 1
+fi
+
+# Turn it off.
+nginx_start
+off_content=$(fetch /show_dotfiles/)
+nginx_stop
+if [ $(grep '.okay'  <<< "${on_content}") -eq 0] ; then
+    exit 1
+fi
+
+exit 0
diff --git a/ngx_fancyindex/t/08-local-footer.test b/ngx_fancyindex/t/08-local-footer.test
new file mode 100644
index 0000000..09b1e6c
--- /dev/null
+++ b/ngx_fancyindex/t/08-local-footer.test
@@ -0,0 +1,17 @@
+#! /bin/bash
+cat <<---
+This test checks that a local footer can be included with
+"fancyindex_header ... local".
+--
+use pup
+
+cat > "${TESTDIR}/footer" <yes
+EOF
+
+nginx_start "fancyindex_footer \"${TESTDIR}/footer\" local;"
+
+T=$(fetch / | pup -p body 'div#customfooter' text{})
+[[ $T == yes ]] ||  fail 'Custom header missing'
+
+nginx_is_running || fail 'Nginx died'
diff --git a/ngx_fancyindex/t/09-local-header.test b/ngx_fancyindex/t/09-local-header.test
new file mode 100644
index 0000000..8b101b1
--- /dev/null
+++ b/ngx_fancyindex/t/09-local-header.test
@@ -0,0 +1,17 @@
+#! /bin/bash
+cat <<---
+This test checks that a local header can be included with
+"fancyindex_header ... local".
+--
+use pup
+
+cat > "${TESTDIR}/header" <yes
+EOF
+
+nginx_start "fancyindex_header \"${TESTDIR}/header\" local;"
+
+T=$(fetch / | pup -p body 'div#customheader' text{})
+[[ $T == yes ]] ||  fail 'Custom header missing'
+
+nginx_is_running || fail 'Nginx died'
diff --git a/ngx_fancyindex/t/10-local-headerfooter.test b/ngx_fancyindex/t/10-local-headerfooter.test
new file mode 100644
index 0000000..739fcd6
--- /dev/null
+++ b/ngx_fancyindex/t/10-local-headerfooter.test
@@ -0,0 +1,26 @@
+#! /bin/bash
+cat <<---
+This test checks that both a local header and footer can be included with
+"fancyindex_{header,footer} ... local".
+--
+use pup
+
+cat > "${TESTDIR}/header" <yes
+EOF
+cat > "${TESTDIR}/footer" <yes
+EOF
+
+nginx_start "fancyindex_header \"${TESTDIR}/header\" local;
+             fancyindex_footer \"${TESTDIR}/footer\" local;"
+
+P=$(fetch /)
+
+H=$(pup -p body 'div#customheader' text{} <<< "$P")
+[[ $H == yes ]] ||  fail 'Custom header missing'
+
+F=$(pup -p body 'div#customfooter' text{} <<< "$P")
+[[ $F == yes ]] || fail 'Custom footer missing'
+
+nginx_is_running || fail 'Nginx died'
diff --git a/ngx_fancyindex/t/11-local-footer-nested.test b/ngx_fancyindex/t/11-local-footer-nested.test
new file mode 100644
index 0000000..ce90d20
--- /dev/null
+++ b/ngx_fancyindex/t/11-local-footer-nested.test
@@ -0,0 +1,31 @@
+#! /bin/bash
+cat <<---
+This test checks that local footers are correctly included in the presence of
+directives in nested locations:
+
+	fancyindex_footer  local;
+	location /sub {
+		fancyindex_footer  local;
+	}
+
+--
+use pup
+
+echo '
yes
' > "${TESTDIR}/top-footer" +echo '
yes
' > "${TESTDIR}/sub-footer" + +nginx_start "fancyindex_footer \"${TESTDIR}/top-footer\" local; + location /child-directory { + fancyindex_footer \"${TESTDIR}/sub-footer\" local; + }" + +T=$(fetch /) +echo "$T" > "$TESTDIR/top.html" +[[ $(pup -p body 'div#topfooter' text{} <<< "$T") = yes ]] || fail 'Custom header missing at /' +[[ -z $(pup -p body 'div#subfooter' text{} <<< "$T") ]] || fail 'Wrong header at /' + +T=$(fetch /child-directory/) +[[ $(pup -p body 'div#subfooter' text{} <<< "$T") = yes ]] || fail 'Custom header missing at /sub/' +[[ -z $(pup -p body 'div#topfooter' text{} <<< "$T") ]] || fail 'Wrong header at /sub/' + +nginx_is_running || fail 'Nginx died' diff --git a/ngx_fancyindex/t/12-local-footer-nested.test b/ngx_fancyindex/t/12-local-footer-nested.test new file mode 100644 index 0000000..3ec0e6b --- /dev/null +++ b/ngx_fancyindex/t/12-local-footer-nested.test @@ -0,0 +1,11 @@ +#! /bin/bash +cat <<--- +This test checks that the configuration file is properly parsed if there +is only one parameter passed to the fancyindex_header and fancyindex_footer +configuration directives. +-- + +nginx_start 'fancyindex_header "/header"; + fancyindex_footer "/footer";' + +nginx_is_running || fail 'Nginx died' diff --git a/ngx_fancyindex/t/bug107-filesystem-root-404.test b/ngx_fancyindex/t/bug107-filesystem-root-404.test new file mode 100644 index 0000000..2870910 --- /dev/null +++ b/ngx_fancyindex/t/bug107-filesystem-root-404.test @@ -0,0 +1,9 @@ +#! /bin/bash +cat <<--- +Bug #107: 404 is returned when indexing filesystem root +https://github.com/aperezdc/ngx-fancyindex/issues/107 +-- +nginx_start 'root /;' +content=$(fetch) +grep 'Index of /' <<< "${content}" # It is an index +grep '' <<< "${content}" # It contains a table diff --git a/ngx_fancyindex/t/bug157-saturday-in-long-weekdays.test b/ngx_fancyindex/t/bug157-saturday-in-long-weekdays.test new file mode 100644 index 0000000..a2a0f6c --- /dev/null +++ b/ngx_fancyindex/t/bug157-saturday-in-long-weekdays.test @@ -0,0 +1,27 @@ +#! /bin/bash +cat <<--- +Check whether the Saturday long day name is available. +https://github.com/aperezdc/ngx-fancyindex/issues/157 +-- +use pup +nginx_start 'fancyindex_time_format "%A"; fancyindex_default_sort date;' + +mkdir -p "${TESTDIR}/weekdays" +for (( i=2 ; i <= 8 ; i++ )) ; do + TZ=UTC touch -d "2023-01-0${i}T06:00:00" "${TESTDIR}/weekdays/day$i.txt" +done +ls "${TESTDIR}/weekdays" +content=$(fetch /weekdays/) + +# We need row+1 because the first one is the table header. +dayname=$(pup -p body table tbody \ + 'tr:nth-child(7)' 'td:nth-child(3)' 'text{}' \ + <<< "$content") +[[ $dayname = Saturday ]] || fail 'Sixth day is not Saturday' + +dayname=$(pup -p body table tbody \ + 'tr:nth-child(8)' 'td:nth-child(3)' 'text{}' \ + <<< "$content") +[[ $dayname = Sunday ]] || fail 'Seventh day is not Sunday' + +nginx_is_running || fail 'Nginx died' diff --git a/ngx_fancyindex/t/bug61-empty-file-segfault.test b/ngx_fancyindex/t/bug61-empty-file-segfault.test new file mode 100644 index 0000000..d9c5a40 --- /dev/null +++ b/ngx_fancyindex/t/bug61-empty-file-segfault.test @@ -0,0 +1,16 @@ +#! /bin/bash +cat <<--- +Bug #61: Listing a directory with an empty file crashes Nginx +https://github.com/aperezdc/ngx-fancyindex/issues/61 +-- + +# Prepare an empty directory with an empty file +mkdir -p "${TESTDIR}/bug61" +touch "${TESTDIR}/bug61/bug61.txt" + +nginx_start 'fancyindex_exact_size off;' +content=$(fetch /bug61/) +test -n "${content}" || fail "Empty response" +echo "Response:" +echo "${content}" +nginx_is_running || fail "Nginx died" diff --git a/ngx_fancyindex/t/bug78-case-insensitive.test b/ngx_fancyindex/t/bug78-case-insensitive.test new file mode 100644 index 0000000..5943075 --- /dev/null +++ b/ngx_fancyindex/t/bug78-case-insensitive.test @@ -0,0 +1,8 @@ +#! /bin/bash +cat <<--- +This test checks that case-insensitive sorting works. +-- + +nginx_start 'fancyindex_case_sensitive off;' +content=$(fetch /case-sensitivity/) +grep -A 999 '\' <<< "${content}" | grep '\' # Bob is after alice diff --git a/ngx_fancyindex/t/bug78-case-sensitive.test b/ngx_fancyindex/t/bug78-case-sensitive.test new file mode 100644 index 0000000..ff9037d --- /dev/null +++ b/ngx_fancyindex/t/bug78-case-sensitive.test @@ -0,0 +1,8 @@ +#! /bin/bash +cat <<--- +This test checks that case-sensitive sorting works. +-- + +nginx_start 'fancyindex_case_sensitive on;' +content=$(fetch /case-sensitivity/) +grep -A 999 '\' <<< "${content}" | grep '\' # alice is after Bob diff --git a/ngx_fancyindex/t/bug95-square-brackets.test b/ngx_fancyindex/t/bug95-square-brackets.test new file mode 100644 index 0000000..16e1ddc --- /dev/null +++ b/ngx_fancyindex/t/bug95-square-brackets.test @@ -0,0 +1,19 @@ +#! /bin/bash +cat <<--- +Bug #95: FancyIndex does not encode square brackets +https://github.com/aperezdc/ngx-fancyindex/issues/95 +-- +use pup + +# Prepare a directory with a file that contains square brackets in the name. +mkdir -p "${TESTDIR}/bug95" +touch "${TESTDIR}"/bug95/'bug[95].txt' + +nginx_start +content=$(fetch /bug95/) +test -n "${content}" || fail 'Empty response' + +expected_href='bug%5B95%5D.txt' +obtained_href=$(pup -p body tbody 'tr:nth-child(2)' a 'attr{href}' <<< "${content}") +test "${expected_href}" = "${obtained_href}" || \ + fail 'Expected: %s - Obtained: %s' "${expected_href}" "${obtained_href}" diff --git a/ngx_fancyindex/t/build-and-run b/ngx_fancyindex/t/build-and-run new file mode 100644 index 0000000..b801a3e --- /dev/null +++ b/ngx_fancyindex/t/build-and-run @@ -0,0 +1,36 @@ +#! /bin/bash +set -e + +if [[ $# -lt 1 || $# -gt 2 ]] ; then + echo "Usage: $0 [1]" 1>&2 + exit 1 +fi + +readonly NGINX=$1 + +if [[ $2 -eq 1 ]] ; then + readonly DYNAMIC=$2 +fi + +case $(uname -s) in + Darwin) + JOBS=$(sysctl -n hw.activecpu) + ;; + *) + JOBS=1 + ;; +esac + +cd "$(dirname "$0")/.." +wget -O - http://nginx.org/download/nginx-${NGINX}.tar.gz | tar -xzf - +rm -rf prefix/ +cd nginx-${NGINX} +./configure \ + --add-${DYNAMIC:+dynamic-}module=.. \ + --with-http_addition_module \ + --without-http_rewrite_module \ + --prefix="$(pwd)/../prefix" +make -j"$JOBS" +make install +cd .. +exec ./t/run prefix ${DYNAMIC} diff --git a/ngx_fancyindex/t/case-sensitivity/Bob b/ngx_fancyindex/t/case-sensitivity/Bob new file mode 100644 index 0000000..e69de29 diff --git a/ngx_fancyindex/t/case-sensitivity/alice b/ngx_fancyindex/t/case-sensitivity/alice new file mode 100644 index 0000000..e69de29 diff --git a/ngx_fancyindex/t/child-directory/empty-file.txt b/ngx_fancyindex/t/child-directory/empty-file.txt new file mode 100644 index 0000000..e69de29 diff --git a/ngx_fancyindex/t/get-pup b/ngx_fancyindex/t/get-pup new file mode 100644 index 0000000..46130ee --- /dev/null +++ b/ngx_fancyindex/t/get-pup @@ -0,0 +1,105 @@ +#! /bin/bash +set -e + +declare -r SHASUMS='\ +ec9522193516ad49c78d40a8163f1d92e98866892a11aadb7be584a975026a8a pup_69c02e189c2aaed331061ee436c39e72b830ef32_darwin_amd64.xz +75c27caa0008a9cc639beb7506077ad9f32facbffcc4e815e999eaf9588a527e pup_v0.4.0_darwin_386.zip +c539a697efee2f8e56614a54cb3b215338e00de1f6a7c2fa93144ab6e1db8ebe pup_v0.4.0_darwin_amd64.zip +259eee82c7d7d766f1b8f93a382be21dcfefebc855a9ce8124fd78717f9df439 pup_v0.4.0_dragonfly_amd64.zip +ba0fe5e87a24cab818e5d2efdd7540714ddfb1b7246600135915c666fdf1a601 pup_v0.4.0_freebsd_386.zip +1838ef84ec1f961e8009d19a4d1e6a23b926ee315da3d60c08878f3d69af5692 pup_v0.4.0_freebsd_amd64.zip +6886a9c60a912a810d012610bc3f784f0417999ff7d7df833a0695b9af60395b pup_v0.4.0_freebsd_arm.zip +e486b32ca07552cd3aa713cbf2f9d1b6e210ddb51d34b3090c7643f465828057 pup_v0.4.0_linux_386.zip +ec3d29e9fb375b87ac492c8b546ad6be84b0c0b49dab7ff4c6b582eac71ba01c pup_v0.4.0_linux_amd64.zip +c09b669fa8240f4f869dee7d34ee3c7ea620a0280cee1ea7d559593bcdd062c9 pup_v0.4.0_linux_arm64.zip +ebf70b3c76c02e0202c94af7ef06dcb3ecc866d1b9b84453d43fe01fa5dd5870 pup_v0.4.0_linux_arm.zip +a98a4d1f3c3a103e8ebe1a7aba9cb9d3cb045003208ca6f5f3d54889a225f267 pup_v0.4.0_linux_mips64le.zip +8e471cf6cfa118b2497bb3f42a7a48c52d0096107f748f37216855c8ab94f8e5 pup_v0.4.0_linux_mips64.zip +cfda9375eba65f710e052b1b59893c228c3fc92b0510756bb3f02c25938eee30 pup_v0.4.0_linux_ppc64le.zip +91a1e07ffb2c373d6053252e4de732f5db78c8eace49c6e1a0ef52402ecdf56c pup_v0.4.0_linux_ppc64.zip +fdc9b28a3daac5ad096023e1647292a7eccea6d9b1686f871307dae9f3bd064f pup_v0.4.0_nacl_386.zip +c8d3c9b56783bd5a55446f4580e1835606b2b945da2d1417ed509c5927a5f8bc pup_v0.4.0_nacl_amd64p32.zip +48c068c4353672528c8c3447a536208b0719f1e6d0f8fab8416b38b63ad0c1d9 pup_v0.4.0_nacl_arm.zip +7a27497b2f0be95c51bb2cbc25da12efba682c4f766bc5abc5742e9fc8d1eeb0 pup_v0.4.0_netbsd_386.zip +71a1808eb1b6442aa45d1de9e1c4fca543b2754c1aff5ba3d62b3456f9519691 pup_v0.4.0_netbsd_amd64.zip +928e6691b11c68ae3f28826848a13dc5c1c9673848fe7cf7f80dd76c9fb6e8a6 pup_v0.4.0_netbsd_arm.zip +5aca20a9b3264d2fde5a8d32f213c434edf9570ee6fae18953b8fff09d2976e2 pup_v0.4.0_openbsd_386.zip +e965c6f04b897240d84c60e2c18226deb231a657c5583680f58a61051ff5a100 pup_v0.4.0_openbsd_amd64.zip +30bc88a1e06606f4f3449af9fbf586f97c2e958677460a72bb1a168f67c4911c pup_v0.4.0_openbsd_arm.zip +9d50decf4572292f187cfec84660648d648336bc6109e1f032b1699ba1d28549 pup_v0.4.0_plan9_386.zip +1b2a6bd2388ddd691ca429497d88b2b047ec8dfb7bce9436925cb2f30632bf8e pup_v0.4.0_plan9_amd64.zip +0835de9c10a9e2b3b958b82d148da49eaafc695fe4a018cbaf7bb861b455583f pup_v0.4.0_solaris_amd64.zip +01acae220b69fb1ba8477d0e7f4d7669ef5de147966dc819cf75a845af74c5f3 pup_v0.4.0_windows_386.zip +6755cbd43e94eaf173689e93e914c7056a2249c2977e5b90024fb397f9b45ba4 pup_v0.4.0_windows_amd64.zip +' + +declare -r TDIR=$(dirname "$0") + +case $(uname -m) in + x86_64 | amd64 ) ARCH=amd64 ;; + i[3456]86 ) ARCH=386 ;; + * ) ARCH= ;; +esac + +OS=$(uname -s | tr 'A-Z' 'a-z') +case ${OS} in + linux | freebsd | openbsd | netbsd | darwin ) ;; + * ) OS= ;; +esac + +# The binary of pup 0.4.0 for macOS provided by the original project +# crashes immediately on macOS 10.13 (Darwin 17) and up so use a fork: +# https://github.com/ericchiang/pup/issues/85 +if [[ ${OS} = darwin && $(uname -r | cut -d. -f1) -ge 17 ]] ; then + USE_FORK=1 +else + USE_FORK=0 +fi + +if (( USE_FORK )) ; then + declare -r VERSION=69c02e189c2aaed331061ee436c39e72b830ef32 + declare -r DISTFILE="pup_${VERSION}_${OS}_${ARCH}.xz" + declare -r URL="https://github.com/frioux/pup/releases/download/untagged-${VERSION}/pup.mac.xz" + if ! command -v xz >/dev/null ; then + echo "xz not found" 1>&2 + exit 3 + fi +else + declare -r VERSION=0.4.0 + declare -r DISTFILE="pup_v${VERSION}_${OS}_${ARCH}.zip" + declare -r URL="https://github.com/ericchiang/pup/releases/download/v${VERSION}/${DISTFILE}" +fi + +if [[ -z ${ARCH} || -z ${OS} ]] ; then + echo "pup ${VERSION} is not available for $(uname -s) on $(uname -m)" 1>&2 + exit 1 +fi + +EXPECT_SHA= +while read sum fname ; do + if [[ ${fname} = ${DISTFILE} ]] ; then + EXPECT_SHA=${sum} + break + fi +done <<< "${SHASUMS}" + +wget -cO "${TDIR}/${DISTFILE}" "${URL}" + +read -r _ GOT_SHA < <( openssl sha256 < "${TDIR}/${DISTFILE}" ) +if [[ ${EXPECT_SHA} = ${GOT_SHA} ]] ; then + echo "Checksum for ${DISTFILE} verified :-)" +else + rm -f "${TDIR}/${DISTFILE}" "${TDIR}/pup" + echo "Checksum for ${DISTFILE} does not match :-(" + echo " Expected: ${EXPECT_SHA}" + echo " Got: ${GOT_SHA}" + exit 2 +fi 1>&2 + +rm -f "${TDIR}/pup" + +if (( USE_FORK )) ; then + (cd "${TDIR}" && xz -dk "${DISTFILE}" && mv "${DISTFILE%.*}" pup && chmod a+x pup) +else + unzip "${TDIR}/${DISTFILE}" pup -d "${TDIR}" +fi diff --git a/ngx_fancyindex/t/has-index.test b/ngx_fancyindex/t/has-index.test new file mode 100644 index 0000000..cf34207 --- /dev/null +++ b/ngx_fancyindex/t/has-index.test @@ -0,0 +1,7 @@ +#! /bin/bash +cat <<--- +This test ensures that the "index.html" is returned instead of a directory +listing when fetching a directory which contains an index file. +-- +nginx_start +diff -u "${TESTDIR}/has-index/index.html" <( fetch /has-index/ ) 1>&2 diff --git a/ngx_fancyindex/t/has-index/index.html b/ngx_fancyindex/t/has-index/index.html new file mode 100644 index 0000000..419ae86 --- /dev/null +++ b/ngx_fancyindex/t/has-index/index.html @@ -0,0 +1,10 @@ + + + + + Index file test + + + This is index.html. + + diff --git a/ngx_fancyindex/t/nginx.conf b/ngx_fancyindex/t/nginx.conf new file mode 100644 index 0000000..2b99a3d --- /dev/null +++ b/ngx_fancyindex/t/nginx.conf @@ -0,0 +1,25 @@ +worker_processes 1; + + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + server { + listen 80; + server_name localhost; + location / { + root html; + index index.html index.htm; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + } +} diff --git a/ngx_fancyindex/t/preamble b/ngx_fancyindex/t/preamble new file mode 100644 index 0000000..04e0f7e --- /dev/null +++ b/ngx_fancyindex/t/preamble @@ -0,0 +1,124 @@ +#! /bin/bash +# +# preamble +# Copyright (C) 2016 Adrian Perez +# +# SPDX-License-Identifier: BSD-2-Clause +# + +function nginx_conf_generate () { + if ${DYNAMIC} ; then + echo 'load_module modules/ngx_http_fancyindex_module.so;' + fi + cat <<-EOF + worker_processes 1; + events { worker_connections 1024; } + http { + include mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + server { + server_name localhost; + listen 127.0.0.1:${NGINX_PORT}; + root ${TESTDIR}; + error_page 500 502 503 504 /50x.html; + location = /50x.html { root html; } + location / { + index index.html; + fancyindex on; + $* + } + } + } + EOF +} + +readonly NGINX_CONF="${PREFIX}/conf/nginx.conf" +readonly NGINX_PID="${PREFIX}/logs/nginx.pid" + +case $(uname -s) in + Darwin) + NGINX_PORT=$(netstat -a -n -finet -ptcp | awk '/LISTEN/ { sub(".+\\.", "", $4) ; seen[$4]=1 } + END { p=1025 ; while (seen[p]) p++; print p}') + ;; + *) + NGINX_PORT=$(ss -4Htnl | awk '{ sub("[^:]+:", "", $4) ; seen[$4]=1 } + END { p=1025 ; while (seen[p]) p++; print p}') + ;; +esac +readonly NGINX_PORT + +rm -f "${NGINX_CONF}" "${NGINX_PID}" +mkdir -p "${PREFIX}/logs" + +function pup () { + if [[ -x ${TESTDIR}/pup ]] ; then + "${TESTDIR}/pup" "$@" + else + skip 'Test uses "pup", which is not available' + fi +} + +function use () { + case $1 in + pup ) [[ -x ${TESTDIR}/pup ]] \ + || skip 'Test uses "pup", which is unavailable\n' ;; + * ) warn "Invalid 'use' flag: '%s'\n'" "$1" ;; + esac +} + +function nginx () { + env - PATH="${PATH}" "${PREFIX}/sbin/nginx" "$@" +} + +function nginx_conf () { + nginx_conf_generate "$@" > "${NGINX_CONF}" +} + +function nginx_is_running () { + [[ -r ${NGINX_PID} ]] && kill -0 $(< "${NGINX_PID}") +} + +function nginx_stop () { + if nginx_is_running ; then nginx -s stop ; fi + rm -f "${NGINX_PID}" +} +trap nginx_stop EXIT + +function nginx_start () { + if [[ $# -gt 0 || ! -r ${NGINX_CONF} ]] ; then nginx_conf "$@" ; fi + nginx_stop # Ensure that it is not running. + nginx + local n=0 + while [[ ! -r ${NGINX_PID} && n -lt 20 ]] ; do + sleep 0.1 # Wait until pid exists. + n=$((n+1)) + done +} + +function fetch () { + local -a opts=( -q ) + if [[ $1 = --with-headers ]] ; then + opts+=( -S ) + shift + fi + wget "${opts[@]}" -O- "http://localhost:${NGINX_PORT}${1:-/}" 2>&1 +} + +function skip () { + printf '(--) ' + printf "$@" + exit 111 +} 1>&2 + +function fail () { + printf '(FF) ' + printf "$@" + exit 1 +} 1>&2 + +function warn () { + printf '(WW) ' + printf "$@" +} 1>&2 diff --git a/ngx_fancyindex/t/run b/ngx_fancyindex/t/run new file mode 100644 index 0000000..9988fa2 --- /dev/null +++ b/ngx_fancyindex/t/run @@ -0,0 +1,86 @@ +#!/bin/bash +set -e + +if [[ $# -lt 1 || $# -gt 2 ]] ; then + echo "Usage: $0 [1]" 1>&2 + exit 1 +fi + +# Obtain the absolute path to the tests directory +pushd "$(dirname "$0")" &> /dev/null +readonly T=$(pwd) +popd &> /dev/null +export T + +# Same for the nginx prefix directory +pushd "$1" &> /dev/null +readonly prefix=$(pwd) +popd &> /dev/null + +dynamic=false +if [[ $# -gt 1 && $2 -eq 1 ]] ; then + dynamic=true +fi +readonly dynamic + +declare -a t_pass=( ) +declare -a t_fail=( ) +declare -a t_skip=( ) + +for t in `ls "$T"/*.test | sort -R` ; do + name="t/${t##*/}" + name=${name%.test} + printf "${name} ... " + errfile="${name}.err" + outfile="${name}.out" + shfile="${name}.sh" + cat > "${shfile}" <<-EOF + readonly DYNAMIC=${dynamic} + readonly TESTDIR='$T' + readonly PREFIX='${prefix}' + $(< "$T/preamble") + $(< "$t") + EOF + if bash -e "${shfile}" > "${outfile}" 2> "${errfile}" ; then + t_pass+=( "${name}" ) + printf 'passed\n' + elif [[ $? -eq 111 ]] ; then + t_skip+=( "${name}" ) + printf 'skipped\n' + else + t_fail+=( "${name}" ) + printf 'failed\n' + fi +done + +for name in "${t_fail[@]}" ; do + echo + printf '=== %s.out\n' "${name}" + cat "${name}.out" + echo + printf '=== %s.err\n' "${name}" + cat "${name}.err" + echo +done + +if [[ ${#t_skip[@]} -gt 0 ]] ; then + echo + printf 'Skipped tests:\n' + for name in "${t_skip[@]}" ; do + reason=$(grep '^(\-\-) ' "${name}.err" | head -1) + if [[ -z ${reason} ]] ; then + reason='No reason given' + else + reason=${reason:5} + fi + printf ' - %s: %s\n' "${name}" "${reason:-No reason given}" + done + echo +fi + +printf '=== passed/skipped/failed/total: %d/%d/%d/%d\n' \ + ${#t_pass[@]} ${#t_skip[@]} ${#t_fail[@]} $(( ${#t_pass[@]} + ${#t_fail[@]} )) + +if [[ ${#t_fail[@]} -gt 0 ]] ; then + exit 1 +fi diff --git a/ngx_fancyindex/t/show_dotfiles/.okay b/ngx_fancyindex/t/show_dotfiles/.okay new file mode 100644 index 0000000..e69de29 diff --git a/ngx_fancyindex/template.awk b/ngx_fancyindex/template.awk new file mode 100644 index 0000000..95240e1 --- /dev/null +++ b/ngx_fancyindex/template.awk @@ -0,0 +1,52 @@ +#! /usr/bin/awk -f +# +# Copyright © Adrian Perez +# +# Converts an HTML template into a C header suitable for inclusion. +# Take a look at the HACKING.md file to know how to use it :-) +# +# This code is placed in the public domain. + +BEGIN { + varname = 0; + print "/* Automagically generated, do not edit! */" + vars_count = 0; +} + +/^$/ { + if (varname) print ";"; + if ($3 == "NONE") { + varname = 0; + next; + } + varname = $3; + vars[vars_count++] = varname; + print "static const u_char " varname "[] = \"\""; + next; +} + +/^$/ { + if (!varname) next; + print "\"\\n\""; + next; +} + +{ + if (!varname) next; + # Order matters + gsub(/[\t\v\n\r\f]+/, ""); + gsub(/\\/, "\\\\"); + gsub(/"/, "\\\""); + print "\"" $0 "\"" +} + + +END { + if (varname) print ";"; + print "#define NFI_TEMPLATE_SIZE (0 \\"; + for (var in vars) { + print "\t+ nfi_sizeof_ssz(" vars[var] ") \\"; + } + print "\t)" +} + diff --git a/ngx_fancyindex/template.h b/ngx_fancyindex/template.h new file mode 100644 index 0000000..02ba908 --- /dev/null +++ b/ngx_fancyindex/template.h @@ -0,0 +1,102 @@ +/* Automagically generated, do not edit! */ +static const u_char t01_head1[] = "" +"" +"" +"" +"" +"" +"" +"\n" +; +static const u_char t02_head2[] = "" +"\n" +"Index of " +; +static const u_char t03_head3[] = "" +"" +"\n" +"" +; +static const u_char t04_body1[] = "" +"" +"

Index of " +; +static const u_char t05_body2[] = "" +"

" +"\n" +; +static const u_char t06_list1[] = "" +"
last = ngx_cpymem(b->last, + sort_url_args, + ngx_sizeof_ssz("?C=N&O=A")); + } + b->last = ngx_cpymem_ssz(b->last, + "\">Parent directory/--
last, + entry[i].name.data, + entry[i].name.len); + + b->last += entry[i].name.len + entry[i].escape; + + } else { + b->last = ngx_cpymem_str(b->last, entry[i].name); + } + + if (entry[i].dir) { + *b->last++ = '/'; + if (*sort_url_args) { + b->last = ngx_cpymem(b->last, + sort_url_args, + ngx_sizeof_ssz("?C=x&O=y")); + } + } + + *b->last++ = '"'; + b->last = ngx_cpymem_ssz(b->last, " title=\""); + b->last = (u_char *) ngx_escape_html(b->last, entry[i].name.data, entry[i].name.len); + *b->last++ = '"'; + *b->last++ = '>'; + + len = entry[i].utf_len; + + b->last = (u_char *) ngx_escape_html(b->last, entry[i].name.data, entry[i].name.len); + last = b->last - 3; + + if (entry[i].dir) { + *b->last++ = '/'; + len++; + } + + b->last = ngx_cpymem_ssz(b->last, ""); + + if (alcf->exact_size) { + if (entry[i].dir) { + *b->last++ = '-'; + } else { + b->last = ngx_sprintf(b->last, "%19O", entry[i].size); + } + + } else { + if (entry[i].dir) { + *b->last++ = '-'; + } else { + length = entry[i].size; + multiplier = exbibyte; + + for (j = 0; j < DIM(sizes) - 1 && length < multiplier; j++) + multiplier /= 1024; + + /* If we are showing the filesize in bytes, do not show a decimal */ + if (j == DIM(sizes) - 1) + b->last = ngx_sprintf(b->last, "%O %s", length, sizes[j]); + else + b->last = ngx_sprintf(b->last, "%.1f %s", + (float) length / multiplier, sizes[j]); + } + } + + ngx_gmtime(entry[i].mtime + tp->gmtoff * 60 * alcf->localtime, &tm); + b->last = ngx_cpymem_ssz(b->last, ""); + b->last = ngx_fancyindex_timefmt(b->last, &alcf->time_format, &tm); + b->last = ngx_cpymem_ssz(b->last, "
" +"" +"" +"" +"" +"" +"" +"" +"\n" +"" +; +static const u_char t_parentdir_entry[] = "" +"" +"" +"" +"" +"" +"\n" +; +static const u_char t07_list2[] = "" +"" +"
File Name  ↓ File Size  ↓ Date  ↓ 
Parent directory/--
" +; +static const u_char t08_foot1[] = "" +"" +"" +; +#define NFI_TEMPLATE_SIZE (0 \ + + nfi_sizeof_ssz(t01_head1) \ + + nfi_sizeof_ssz(t02_head2) \ + + nfi_sizeof_ssz(t03_head3) \ + + nfi_sizeof_ssz(t04_body1) \ + + nfi_sizeof_ssz(t05_body2) \ + + nfi_sizeof_ssz(t06_list1) \ + + nfi_sizeof_ssz(t_parentdir_entry) \ + + nfi_sizeof_ssz(t07_list2) \ + + nfi_sizeof_ssz(t08_foot1) \ + ) diff --git a/ngx_fancyindex/template.html b/ngx_fancyindex/template.html new file mode 100644 index 0000000..b2f521b --- /dev/null +++ b/ngx_fancyindex/template.html @@ -0,0 +1,101 @@ + + + + + + + + + + + Index of +<!-- var NONE --> + /path/to/somewhere +<!-- var t03_head3 --> + + + + + +

Index of + + /path/to/somewhere + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File Name  ↓ File Size  ↓ Date  ↓ 
--
test file 1123kBdate
test file 2321MBdate
test file 3666date
+ + + diff --git a/ngx_http_flv_module/.github/CODE_OF_CONDUCT.md b/ngx_http_flv_module/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..77e9612 --- /dev/null +++ b/ngx_http_flv_module/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at winshining@163.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/ngx_http_flv_module/.github/CONTRIBUTING.md b/ngx_http_flv_module/.github/CONTRIBUTING.md new file mode 100644 index 0000000..d91a37b --- /dev/null +++ b/ngx_http_flv_module/.github/CONTRIBUTING.md @@ -0,0 +1,41 @@ +## Guidelines to contribute + +#### **When you find a bug** + +* **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/winshining/nginx-http-flv-module/issues). + +* If there is no issue addressing the problem, [open a new one](https://github.com/winshining/nginx-http-flv-module/issues/new). Be sure to include a **title prefixed by '[bug]' and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. + +#### **Write a patch that fixes a bug** + +* Open a new GitHub pull request with the patch. + +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +* Before submitting, be sure the commit description is prefixed by: + * **[add]** if new features were added. + * **[dev]** if codes were changed. + * **[fix]** if bugs were fixed. + * **[misc]** if some changes were done and bugs were fixed. + +* Ensure that your codes conform to code conventions: + * All files are prefixed by 'ngx\_'. + * Include #ifndef \_FILE\_NAME\_H\_INCLUDED\_, #define \_FILE\_NAME\_H\_INCLUDED\_ and #endif in header files. + * Comments use /* ... */ are preferable. + * It would be better that built-in types appear before customized types. + * There should be no less than 2 spaces between types and variables. + * Variables are aligned by character, not '\*'. + * No more than 80 characters in a single code or comment line. + * Two blank lines between two functions, styles of macro and type definitions are same as functions. + +#### **Add a new feature or change an existing one** + +* Open an issue on GitHub prefixed by '[feature]' until you have collected positive feedback about the change. + +#### **Questions about the source code** + +* Open an issue on GitHub prefixed by '[misc]', describe as clear as possible. + +Thanks! + +Winshining diff --git a/ngx_http_flv_module/.github/FUNDING.yml b/ngx_http_flv_module/.github/FUNDING.yml new file mode 100644 index 0000000..2df81ba --- /dev/null +++ b/ngx_http_flv_module/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ['https://www.paypal.me/ShingWong'] diff --git a/ngx_http_flv_module/.github/ISSUE_TEMPLATE.md b/ngx_http_flv_module/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..c94e4fa --- /dev/null +++ b/ngx_http_flv_module/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,14 @@ +When you meet a bug, please open the issue including a title prefixed by '[bug]' and describe it as follows: +(当你碰到一个 bug,请在提出问题时以 '[bug]' 为前缀写明标题,并且像下面的内容一样描述它): + +### Expected behavior (期望行为) + +### Actual behavior (实际行为) + +### OS and Nginx version (操作系统和 Nginx 版本号) + +### Configuration file (配置文件) + +### Steps to reproduce the behavior (复现问题步骤) + +### Error log if any (错误日志) diff --git a/ngx_http_flv_module/.github/workflows/nginx-http-flv-module.yml b/ngx_http_flv_module/.github/workflows/nginx-http-flv-module.yml new file mode 100644 index 0000000..f7d0abf --- /dev/null +++ b/ngx_http_flv_module/.github/workflows/nginx-http-flv-module.yml @@ -0,0 +1,42 @@ +name: nginx-http-flv-module CI + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + + runs-on: ubuntu-latest + + env: + NGINX_VERSION: nginx-1.28.0 + + steps: + - uses: actions/checkout@v3 + - name: download nginx + working-directory: ../ + run: wget https://nginx.org/download/${{env.NGINX_VERSION}}.tar.gz + - name: uncompress nginx + working-directory: ../ + run: tar zxvf ${{env.NGINX_VERSION}}.tar.gz + - name: configure (build into nginx) + working-directory: ../${{env.NGINX_VERSION}} + run: ./configure --add-module=../nginx-http-flv-module + - name: make + working-directory: ../${{env.NGINX_VERSION}} + run: make + - name: clean + working-directory: ../${{env.NGINX_VERSION}} + run: make clean + - name: configure (build as a dynamic module) + working-directory: ../${{env.NGINX_VERSION}} + run: ./configure --add-dynamic-module=../nginx-http-flv-module + - name: make + working-directory: ../${{env.NGINX_VERSION}} + run: make + - name: remove + working-directory: ../ + run: rm -rf "${{env.NGINX_VERSION}}*" diff --git a/ngx_http_flv_module/AUTHORS b/ngx_http_flv_module/AUTHORS new file mode 100644 index 0000000..dda3fa0 --- /dev/null +++ b/ngx_http_flv_module/AUTHORS @@ -0,0 +1,63 @@ +Project author: + + Roman Arutyunyan + Moscow, Russia + Contacts: + arut@qip.ru + arutyunyan.roman@gmail.com + + Winshining + Beijing, China + Contacts: + winshining@163.com + + Gnolizuh + Beijing, China + Contacts: + huzilong_007@163.com + huzilong@kingsoft.com + + han4235 + Suzhou, China + Contacts: + https://github.com/han4235 + + plainheart + Zhengzhou, China + Contacts: + https://github.com/plainheart + + HeyJupiter: + Seattle, US + Contacts: + https://github.com/HeyJupiter + + Vladimir Vainer + - + Contacts: + https://github.com/ferreus + + ever4Keny + China + Contacts: + https://github.com/ever4Keny + + spacewander + Guangzhou, China + Contacts: + spacewanderlzx@gmail.com + + ham3r + - + Contacts: + https://github.com/ham3r + + deamos + Scranton, PA + Contacts: + https://github.com/deamos + + vacing + Shenzhen, China + Contacts: + https://github.com/vacing diff --git a/ngx_http_flv_module/LICENSE b/ngx_http_flv_module/LICENSE new file mode 100644 index 0000000..795779f --- /dev/null +++ b/ngx_http_flv_module/LICENSE @@ -0,0 +1,32 @@ +BSD 2-Clause License + +Copyright (c) 2012-2017, Roman Arutyunyan +Copyright (c) 2017-2025, Winshining +Copyright (c) 2018, han4235, Vladimir Vainer +Copyright (c) 2018-2019, plainheart, HeyJupiter +Copyright (c) 2019, ever4Keny +Copyright (c) 2020, spacewander, ham3r +Copyright (c) 2022, deamons +Copyright (c) 2024, vacing +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ngx_http_flv_module/README.CN.md b/ngx_http_flv_module/README.CN.md new file mode 100644 index 0000000..5e292e5 --- /dev/null +++ b/ngx_http_flv_module/README.CN.md @@ -0,0 +1,417 @@ +# nginx-http-flv-module + +![nginx-http-flv-module workflow](https://github.com/winshining/nginx-http-flv-module/actions/workflows/nginx-http-flv-module.yml/badge.svg?branch=master) + +一款基于 [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) 的流媒体服务器。 + +[English README](https://github.com/winshining/nginx-http-flv-module/blob/master/README.md)。 + +如果您喜欢这个模块,可以通过赞赏来支持我的工作,非常感谢! + +![reward_qrcode_winshining](https://gitee.com/winshining/nginx-http-flv-module/raw/master/qrcode/reward_qrcode_winshining.png) + +### 感谢 + +* Igor Sysoev,[NGINX](http://nginx.org) 的作者。 + +* Roman Arutyunyan,[nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) 的作者。 + +* 贡献者,详情见 [AUTHORS](https://github.com/winshining/nginx-http-flv-module/blob/master/AUTHORS)。 + +## 功能 + +* [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) 提供的所有功能。 + +* nginx-http-flv-module 的其他功能与 [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) 的对比: + +| 功能 | nginx-http-flv-module | nginx-rtmp-module | 备注 | +| :----------------------: | :-------------------: | :---------------: | :--------------------------------------: | +| HTTP-FLV (播放) | √ | x | 支持 HTTPS-FLV 和 chunked 回复 | +| GOP 缓存 | √ | x | | +| 虚拟主机 | √ | x | | +| 省略 `listen` 配置项 | √ | 见备注 | 配置中必须有一个 `listen` | +| RTMP/HTTP-FLV 纯音频支持 | √ | 见备注 |`wait_video` 或 `wait_key` 开启后无法工作 | +| HLS 单轨支持 | √ | x | | +| `reuseport` 支持 | √ | x | | +| 定时打印访问记录 | √ | x | | +| JSON 风格的数据信息 | √ | x | | +| 录制的数据信息 | √ | x | | +| 大小端无关 | √ | 见备注 | `big-endian` 分支部分支持 | + +## 兼容性 + +[NGINX](http://nginx.org) 的版本**应该**大于或者等于 1.2.6,与其他版本的兼容性未知。 + +## 支持的系统 + +* Linux(推荐)/ FreeBSD / MacOS / Windows(受限)。 + +## 支持的播放器 + +* [VLC](http://www.videolan.org) (RTMP & HTTP-FLV) / [OBS](https://obsproject.com) (RTMP & HTTP-FLV) / [JW Player](https://www.jwplayer.com) (RTMP) / [flv.js](https://github.com/Bilibili/flv.js) (HTTP-FLV). + +### 注意 + +* Adobe 将在 2020 年 12 月 31 日之后停止对 [flash 播放器](https://www.adobe.com/products/flashplayer.html) 的官方支持,详情见 [Adobe Flash Player EOL General Information Page](https://www.adobe.com/products/flashplayer/end-of-life.html)。主流浏览器随后将移除 flash 播放器,使用 flash 播放器的插件将不再可用。 + +* [flv.js](https://github.com/Bilibili/flv.js) 只能运行在支持 [Media Source Extensions](https://www.w3.org/TR/media-source) 的浏览器上。 + +## 依赖 + +* 在类 Unix 系统上,需要 GNU make,用于调用编译器来编译软件。 + +* 在类 Unix 系统上,需要 GCC。或者在 Windows 上,需要 MSVC,用于编译软件。 + +* 在类 Unix 系统上,需要 GDB,用于调试软件(可选)。 + +* [FFmpeg](http://ffmpeg.org) 或者 [OBS](https://obsproject.com),用于发布媒体流。 + +* [VLC](http://www.videolan.org)(推荐)或者 [flv.js](https://github.com/Bilibili/flv.js)(推荐),用于播放媒体流。 + +* 如果 NGINX 要支持正则表达式,需要 [PCRE库](http://www.pcre.org)。 + +* 如果 NGINX 要支持加密访问,需要 [OpenSSL库](https://www.openssl.org)。 + +* 如果 NGINX 要支持压缩,需要 [zlib库](http://www.zlib.net)。 + +## 创建 + +### 注意 + +nginx-http-flv-module 包含了 [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) 所有的功能,所以**不要**将 nginx-http-flv-module 和 [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) 一起编译。 + +### 在 Windows 上 + +编译步骤请参考 [Building nginx on the Win32 platform with Visual C](http://nginx.org/en/docs/howto_build_on_win32.html),不要忘了在 `Run configure script` 步骤中添加 `--add-module=/path/to/nginx-http-flv-module`。 + +#### 注意 + +如果使用没有完整支持 x64 的编译器来编译此模块,例如 VS2010,请务必使用默认设置(目标机器类型 x86)。 + +### 在类 Unix 系统上 + +下载 [NGINX](http://nginx.org) 和 nginx-http-flv-module。 + +将它们解压到某一路径。 + +打开 NGINX 的源代码路径并执行: + +#### 将模块编译进 [NGINX](http://nginx.org) + + ./configure --add-module=/path/to/nginx-http-flv-module + make + make install + +或者 + +#### 将模块编译为动态模块 + + ./configure --add-dynamic-module=/path/to/nginx-http-flv-module + make + make install + +#### 注意 + +如果将模块编译为动态模块,那么 [NGINX](http://nginx.org) 的版本号**必须**大于或者等于 1.9.11。 + +## 使用方法 + +关于 [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) 用法的详情,请参考 [README.md](https://github.com/arut/nginx-rtmp-module/blob/master/README.md)。 + +### 发布 + +为了简单起见,不用转码: + + ffmpeg -re -i MEDIA_FILE_NAME -c copy -f flv rtmp://example.com[:port]/appname/streamname + +#### 注意 + +一些旧版本的 [FFmpeg](http://ffmpeg.org) 不支持选项 `-c copy`,可以使用选项 `-vcodec copy -acodec copy` 替代。 + +`appname` 用于匹配 rtmp 配置块中的 application 块(更多详情见下文)。 + +`streamname` 可以随意指定,但是**不能**省略。 + +**RTMP 默认端口**为 **1935**,如果要使用其他端口,必须指定 `:port`。 + +### 播放 + +#### HTTP-FLV 方式 + + http://example.com[:port]/dir?[port=xxx&]app=appname&stream=streamname + +#### 注意 + +* 如果使用 [ffplay](http://www.ffmpeg.org/ffplay.html) 命令行方式播放流,那么**必须**为上述的 url 加上引号,否则 url 中的参数会被丢弃(有些不太智能的 shell 会把 "&" 解释为"后台运行")。 + +* 如果使用 [flv.js](https://github.com/Bilibili/flv.js) 播放流,那么请保证发布的流被正确编码,因为 [flv.js](https://github.com/Bilibili/flv.js) **只支持 H.264 编码的视频和 AAC/MP3 编码的音频**。 + +参数 `dir` 用于匹配 http 配置块中的 location 块(更多详情见下文)。 + +**HTTP 默认端口**为 **80**, 如果使用了其他端口,必须指定 `:port`。 + +**RTMP 默认端口**为 **1935**,如果使用了其他端口,必须指定 `port=xxx`。 + +参数 `app` 的值(appname)用来匹配 application 块,但是如果请求的 `app` 出现在多个 server 块中,并且这些 server 块有相同的地址和端口配置,那么还需要用匹配主机名的 `server_name` 配置项来区分请求的是哪个 application 块,否则,将匹配第一个 application 块。 + +参数 `stream` 的值(streamname)用来匹配发布的流的名称。 + +#### 例子 + +假设在 `http` 配置块中的 `listen` 配置项是: + + http { + ... + server { + listen 8080; #不是默认的 80 端口 + ... + + location /live { + flv_live on; + } + } + } + +在 `rtmp` 配置块中的 `listen` 配置项是: + + rtmp { + ... + server { + listen 1985; #不是默认的 1935 端口 + ... + + application myapp { + live on; + } + } + } + +并且发布的流的名称是 `mystream`,那么基于 HTTP 的播放 url 是: + + http://example.com:8080/live?port=1985&app=myapp&stream=mystream + +#### 注意 + +由于一些播放器不支持 HTTP 块传输, 这种情况下最好在指定了 `flv_live on;` 的 location 中指定 `chunked_transfer_encoding off`,否则播放会失败。 + +#### RTMP 方式 + + rtmp://example.com[:port]/appname/streamname + +#### HLS 方式 + + http://example.com[:port]/dir/streamname.m3u8 + +#### DASH 方式 + + http://example.com[:port]/dir/streamname.mpd + +## 示例图片 + +### RTMP ([JW Player](https://www.jwplayer.com)) & HTTP-FLV ([VLC](http://www.videolan.org)) + +![RTMP & HTTP-FLV](samples/jwplayer_vlc.png) + +### HTTP-FLV ([flv.js](https://github.com/Bilibili/flv.js)) + +![HTTP-FLV](samples/flv.js.png) + +## nginx.conf 实例 + +### 注意 + +配置项 `rtmp_auto_push`,`rtmp_auto_push_reconnect` 和 `rtmp_socket_dir` 在 Windows 上不起作用,除了 Windows 10 17063 以及后续版本之外,因为多进程模式的 `relay` 需要 Unix domain socket 的支持,详情请参考 [Unix domain socket on Windows 10](https://blogs.msdn.microsoft.com/commandline/2017/12/19/af_unix-comes-to-windows)。 + +最好将配置项 `worker_processes` 设置为 1,因为在多进程模式下,`ngx_rtmp_stat_module` 可能不会从指定的 worker 进程获取统计数据,因为 HTTP 请求是被随机分配给 worker 进程的。`ngx_rtmp_control_module` 也有同样的问题。这个问题可以通过这个补丁 [per-worker-listener](https://github.com/arut/nginx-patches/blob/master/per-worker-listener) 优化。 + +另外,`vhost` 功能在单进程模式下没有问题,但是在多进程模式下还不能完全正确运行,等待修复。例如,下面的配置在多进程模式下是没有问题的: + + rtmp { + ... + server { + listen 1935; + server_name domain_name; + + application myapp { + ... + } + } + } + +而使用下面的配置,当 publisher 在第二个 `server` 上发布媒体流,播放请求以该配置(不管端口是不是 1935)访问非 publisher 的 worker 进程时是有问题的: + + rtmp { + ... + server { + listen 1935; + server_name 1st_domain_name; + + application myapp { + ... + } + } + + server { + listen 1945; + server_name 2nd_domain_name; + + application myapp { + ... + } + } + } + +如果 [NGINX](http://nginx.org) 是以多进程模式运行并且平台支持 socket 选项 `SO_REUSEPORT`,那么在配置项 `listen` 后添加选项 `reuseport` 可以解决惊群问题。 + + rtmp { + ... + + server { + listen 1935 reuseport; + ... + } + } + +### 配置实例 + + worker_processes 1; #运行在 Windows 上时,设置为 1,因为 Windows 不支持 Unix domain socket + #worker_processes auto; #1.3.8 和 1.2.5 以及之后的版本 + + #worker_cpu_affinity 0001 0010 0100 1000; #只能用于 FreeBSD 和 Linux + #worker_cpu_affinity auto; #1.9.10 以及之后的版本 + + error_log logs/error.log error; + + #如果此模块被编译为动态模块并且要使用与 RTMP 相关的功 + #能时,必须指定下面的配置项并且它必须位于 events 配置 + #项之前,否则 NGINX 启动时不会加载此模块或者加载失败 + + #load_module modules/ngx_http_flv_live_module.so; + + events { + worker_connections 4096; + } + + http { + include mime.types; + default_type application/octet-stream; + + keepalive_timeout 65; + + server { + listen 80; + + location / { + root /var/www; + index index.html index.htm; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + + location /live { + flv_live on; #打开 HTTP 播放 FLV 直播流功能 + chunked_transfer_encoding on; #支持 'Transfer-Encoding: chunked' 方式回复 + + add_header 'Access-Control-Allow-Origin' '*'; #添加额外的 HTTP 头 + add_header 'Access-Control-Allow-Credentials' 'true'; #添加额外的 HTTP 头 + } + + location /hls { + types { + application/vnd.apple.mpegurl m3u8; + video/mp2t ts; + } + + root /tmp; + add_header 'Cache-Control' 'no-cache'; + } + + location /dash { + root /tmp; + add_header 'Cache-Control' 'no-cache'; + } + + location /stat { + #推流播放和录制统计数据的配置 + + rtmp_stat all; + rtmp_stat_stylesheet stat.xsl; + } + + location /stat.xsl { + root /var/www/rtmp; #指定 stat.xsl 的位置 + } + + #如果需要 JSON 风格的 stat, 不用指定 stat.xsl + #但是需要指定一个新的配置项 rtmp_stat_format + + #location /stat { + # rtmp_stat all; + # rtmp_stat_format json; + #} + + location /control { + rtmp_control all; #rtmp 控制模块的配置 + } + } + } + + rtmp_auto_push on; + rtmp_auto_push_reconnect 1s; + rtmp_socket_dir /tmp; + + rtmp { + out_queue 4096; + out_cork 8; + max_streams 128; + timeout 15s; + drop_idle_publisher 15s; + + log_interval 5s; #log 模块在 access.log 中记录日志的间隔时间,对调试非常有用 + log_size 1m; #log 模块用来记录日志的缓冲区大小 + + server { + listen 1935; + server_name www.test.*; #用于虚拟主机名后缀通配 + + application myapp { + live on; + gop_cache on; #打开 GOP 缓存,减少首屏等待时间 + } + + application hls { + live on; + hls on; + hls_path /tmp/hls; + } + + application dash { + live on; + dash on; + dash_path /tmp/dash; + } + } + + server { + listen 1935; + server_name *.test.com; #用于虚拟主机名前缀通配 + + application myapp { + live on; + gop_cache on; #打开 GOP 缓存,减少首屏等待时间 + } + } + + server { + listen 1935; + server_name www.test.com; #用于虚拟主机名完全匹配 + + application myapp { + live on; + gop_cache on; #打开 GOP 缓存,减少首屏等待时间 + } + } + } diff --git a/ngx_http_flv_module/README.md b/ngx_http_flv_module/README.md new file mode 100644 index 0000000..b2af3dc --- /dev/null +++ b/ngx_http_flv_module/README.md @@ -0,0 +1,418 @@ +# nginx-http-flv-module + +![nginx-http-flv-module workflow](https://github.com/winshining/nginx-http-flv-module/actions/workflows/nginx-http-flv-module.yml/badge.svg?branch=master) + +A media streaming server based on [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module). + +[中文说明](https://github.com/winshining/nginx-http-flv-module/blob/master/README.CN.md). + +Donate if you like this module. Many thanks to you! + +PayPal + +### Credits + +* Igor Sysoev, the creator of [NGINX](http://nginx.org). + +* Roman Arutyunyan, who created [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module). + +* Contributors, refer to [AUTHORS](https://github.com/winshining/nginx-http-flv-module/blob/master/AUTHORS) for details. + +## Features + +* All features [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) provides. + +* Other features provided by nginx-http-flv-module vs [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module): + +| Features | nginx-http-flv-module | nginx-rtmp-module | Remarks | +| :--------------------------------: | :-------------------: | :---------------: | :--------------------------------------------: | +| HTTP-FLV (for play) | √ | x | HTTPS-FLV and chunked response supported | +| GOP cache | √ | x | | +| Virtual Host | √ | x | | +| Omit `listen` directive | √ | See remarks | There MUST be at least one `listen` directive | +|Audio-only support for RTMP/HTTP-FLV| √ | See remarks | Won't work if `wait_video` or `wait_key` is on | +| Single-track support for HLS | √ | x | | +| `reuseport` support | √ | x | | +| Timer for access log | √ | x | | +| JSON style statistics | √ | x | | +| Statistics for recordings | √ | x | | +| Independent of endianness | √ | See remarks | Partially supported in branch `big-endian` | + +## Compatibility + +The [NGINX](http://nginx.org) version **SHOULD** be equal to or greater than 1.2.6, the compatibility with other versions is unknown. + +## Systems supported + +* Linux (recommended) / FreeBSD / MacOS / Windows (limited). + +## Players supported + +* [VLC](http://www.videolan.org) (RTMP & HTTP-FLV) / [OBS](https://obsproject.com) (RTMP & HTTP-FLV) / [JW Player](https://www.jwplayer.com) (RTMP) / [flv.js](https://github.com/Bilibili/flv.js) (HTTP-FLV). + +### Note + +* [Flash player](https://www.adobe.com/products/flashplayer.html) will be no longer supported officially by Adobe after December 31, 2020, refer to [Adobe Flash Player EOL General Information Page](https://www.adobe.com/products/flashplayer/end-of-life.html) for details. Plugins that use flash player won't work after the major browsers subsequently remove flash player. + +* [flv.js](https://github.com/Bilibili/flv.js) can only run with browsers that support [Media Source Extensions](https://www.w3.org/TR/media-source). + +## Prerequisites + +* GNU make for activating compiler on Unix-like systems to compile software. + +* GCC for compilation on Unix-like systems or MSVC for compilation on Windows. + +* GDB for debug on Unix-like systems. + +* [FFmpeg](http://ffmpeg.org) or [OBS](https://obsproject.com) for publishing media streams. + +* [VLC](http://www.videolan.org) (recommended) or [flv.js](https://github.com/Bilibili/flv.js) (recommended) for playing media streams. + +* [PCRE](http://www.pcre.org) for NGINX if regular expressions needed. + +* [OpenSSL](https://www.openssl.org) for NGINX if encrypted access needed. + +* [zlib](http://www.zlib.net) for NGINX if compression needed. + +## Build + +### Note + +nginx-http-flv-module has all features that [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module) provides, so **DON'T** compile nginx-http-flv-module along with [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module). + +### On Windows + +For details about build steps, please refer to [Building nginx on the Win32 platform with Visual C](http://nginx.org/en/docs/howto_build_on_win32.html), and don't forget to add `--add-module=/path/to/nginx-http-flv-module` in `Run configure script` step. + +#### Note + +If some compilers which do not support x64 perfectly, VS2010 for example, are used to compile the module, please make sure that the default settings are used (target machine type x86). + +### On Unix-like systems + +Download [NGINX](http://nginx.org) and nginx-http-flv-module. + +Uncompress them. + +cd to NGINX source directory & run this: + +#### Compile the module into [NGINX](http://nginx.org) + + ./configure --add-module=/path/to/nginx-http-flv-module + make + make install + +or + +#### Compile the module as a dynamic module + + ./configure --add-dynamic-module=/path/to/nginx-http-flv-module + make + make install + +#### Note + +If the module is compiled as a dynamic module, the [NGINX](http://nginx.org) version **MUST** be equal to or greater than 1.9.11. + +## Usage + +For details of usages of [nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module), please refer to [README.md](https://github.com/arut/nginx-rtmp-module/blob/master/README.md). + +### Publish + +For simplicity, transcoding is not used (so **-c copy** is used): + + ffmpeg -re -i MEDIA_FILE_NAME -c copy -f flv rtmp://example.com[:port]/appname/streamname + +#### Note + +Some legacy versions of [FFmpeg](http://ffmpeg.org) don't support the option `-c copy`, the options `-vcodec copy -acodec copy` can be used instead. + +The `appname` is used to match an application block in rtmp block (see below for details). + +The `streamname` can be specified at will but can **NOT** be omitted. + +The **default port for RTMP** is **1935**, if some other ports were used, `:port` must be specified. + +### Play + +#### via HTTP-FLV + + http://example.com[:port]/dir?[port=xxx&]app=appname&stream=streamname + +#### Note + +* If [ffplay](http://www.ffmpeg.org/ffplay.html) is used in command line to play the stream, the url above **MUST** be enclosed by quotation marks, or arguments in url will be discarded (some shells not so smart will interpret "&" as "run in background"). + +* If [flv.js](https://github.com/Bilibili/flv.js) is used to play the stream, make sure that the published stream is encoded properly, for [flv.js](https://github.com/Bilibili/flv.js) supports **ONLY H.264 encoded video and AAC/MP3 encoded audio**. + +The `dir` is used to match location blocks in http block (see below for details). + +The **default port for HTTP** is **80**, if some other ports were used, `:port` must be specified. + +The **default port for RTMP** is **1935**, if some other ports were used, `port=xxx` must be specified. + +The value of `app` (appname) is used to match an application block, but if the requested `app` appears in several server blocks and those blocks have the same address and port configuration, host name matches `server_name` directive will be additionally used to identify the requested application block, otherwise the first one is matched. + +The value of `stream` (streamname) is used to match the name of published stream. + +#### Example + +Assume that `listen` directive specified in `http` block is: + + http { + ... + server { + listen 8080; #not default port 80 + ... + + location /live { + flv_live on; + } + } + } + +And `listen` directive specified in `rtmp` block is: + + rtmp { + ... + server { + listen 1985; #not default port 1935 + ... + + application myapp { + live on; + } + } + } + +And the name of published stream is `mystream`, then the url of playback based on HTTP is: + + http://example.com:8080/live?port=1985&app=myapp&stream=mystream + +#### Note + +Since some players don't support HTTP chunked transmission, it's better to specify `chunked_transfer_encoding off;` in location where `flv_live on;` is specified in this case, or play will fail. + +#### via RTMP + + rtmp://example.com[:port]/appname/streamname + +#### via HLS + + http://example.com[:port]/dir/streamname.m3u8 + +#### via DASH + + http://example.com[:port]/dir/streamname.mpd + +## Sample Pictures + +### RTMP ([JW Player](https://www.jwplayer.com)) & HTTP-FLV ([VLC](http://www.videolan.org)) + +![RTMP & HTTP-FLV](samples/jwplayer_vlc.png) + +### HTTP-FLV ([flv.js](https://github.com/Bilibili/flv.js)) + +![HTTP-FLV](samples/flv.js.png) + +## Example nginx.conf + +### Note + +The directives `rtmp_auto_push`, `rtmp_auto_push_reconnect` and `rtmp_socket_dir` will not function on Windows except on Windows 10 17063 and later versions, because `relay` in multiple processes mode needs help of Unix domain socket, please refer to [Unix domain socket on Windows 10](https://blogs.msdn.microsoft.com/commandline/2017/12/19/af_unix-comes-to-windows) for details. + +It's better to specify the directive `worker_processes` as 1, because `ngx_rtmp_stat_module` may not get statistics from a specified worker process in multi-processes mode, for HTTP requests are randomly distributed to worker processes. `ngx_rtmp_control_module` has the same problem. The problem can be optimized by this patch [per-worker-listener](https://github.com/arut/nginx-patches/blob/master/per-worker-listener). + +In addtion, `vhost` feature is OK in single process mode but not perfect in multi-processes mode yet, waiting to be fixed. For example, the following configuration is OK in multi-processes mode: + + rtmp { + ... + server { + listen 1935; + server_name domain_name; + + application myapp { + ... + } + } + } + +While the following configuration doesn't work properly for play requests distinated to the second `server` (whether port is 1935 or not) of non-publisher worker processes: + + rtmp { + ... + server { + listen 1935; + server_name 1st_domain_name; + + application myapp { + ... + } + } + + server { + listen 1945; + server_name 2nd_domain_name; + + application myapp { + ... + } + } + } + +If [NGINX](http://nginx.org) is running in muti-processes mode and socket option `SO_REUSEPORT` is supported by platform, adding option `reuseport` for the directive `listen` will resolve the thundering herd problem. + + rtmp { + ... + + server { + listen 1935 reuseport; + ... + } + } + +### Example configuration + + worker_processes 1; #should be 1 for Windows, for it doesn't support Unix domain socket + #worker_processes auto; #from versions 1.3.8 and 1.2.5 + + #worker_cpu_affinity 0001 0010 0100 1000; #only available on FreeBSD and Linux + #worker_cpu_affinity auto; #from version 1.9.10 + + error_log logs/error.log error; + + #if the module is compiled as a dynamic module and features relevant + #to RTMP are needed, the command below MUST be specified and MUST be + #located before events directive, otherwise the module won't be loaded + #or will be loaded unsuccessfully when NGINX is started + + #load_module modules/ngx_http_flv_live_module.so; + + events { + worker_connections 4096; + } + + http { + include mime.types; + default_type application/octet-stream; + + keepalive_timeout 65; + + server { + listen 80; + + location / { + root /var/www; + index index.html index.htm; + } + + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root html; + } + + location /live { + flv_live on; #open flv live streaming (subscribe) + chunked_transfer_encoding on; #open 'Transfer-Encoding: chunked' response + + add_header 'Access-Control-Allow-Origin' '*'; #add additional HTTP header + add_header 'Access-Control-Allow-Credentials' 'true'; #add additional HTTP header + } + + location /hls { + types { + application/vnd.apple.mpegurl m3u8; + video/mp2t ts; + } + + root /tmp; + add_header 'Cache-Control' 'no-cache'; + } + + location /dash { + root /tmp; + add_header 'Cache-Control' 'no-cache'; + } + + location /stat { + #configuration of streaming & recording statistics + + rtmp_stat all; + rtmp_stat_stylesheet stat.xsl; + } + + location /stat.xsl { + root /var/www/rtmp; #specify in where stat.xsl located + } + + #if JSON style stat needed, no need to specify + #stat.xsl but a new directive rtmp_stat_format + + #location /stat { + # rtmp_stat all; + # rtmp_stat_format json; + #} + + location /control { + rtmp_control all; #configuration of control module of rtmp + } + } + } + + rtmp_auto_push on; + rtmp_auto_push_reconnect 1s; + rtmp_socket_dir /tmp; + + rtmp { + out_queue 4096; + out_cork 8; + max_streams 128; + timeout 15s; + drop_idle_publisher 15s; + + log_interval 5s; #interval used by log module to log in access.log, it is very useful for debug + log_size 1m; #buffer size used by log module to log in access.log + + server { + listen 1935; + server_name www.test.*; #for suffix wildcard matching of virtual host name + + application myapp { + live on; + gop_cache on; #open GOP cache for reducing the wating time for the first picture of video + } + + application hls { + live on; + hls on; + hls_path /tmp/hls; + } + + application dash { + live on; + dash on; + dash_path /tmp/dash; + } + } + + server { + listen 1935; + server_name *.test.com; #for prefix wildcard matching of virtual host name + + application myapp { + live on; + gop_cache on; #open GOP cache for reducing the wating time for the first picture of video + } + } + + server { + listen 1935; + server_name www.test.com; #for completely matching of virtual host name + + application myapp { + live on; + gop_cache on; #open GOP cache for reducing the wating time for the first picture of video + } + } + } diff --git a/ngx_http_flv_module/config b/ngx_http_flv_module/config new file mode 100644 index 0000000..753acf3 --- /dev/null +++ b/ngx_http_flv_module/config @@ -0,0 +1,161 @@ +ngx_addon_name="ngx_http_flv_live_module" + +RTMP_CORE_MODULES=" \ + ngx_rtmp_module \ + ngx_rtmp_core_module \ + ngx_rtmp_cmd_module \ + ngx_rtmp_gop_cache_module \ + ngx_rtmp_codec_module \ + ngx_rtmp_access_module \ + ngx_rtmp_record_module \ + ngx_rtmp_live_module \ + ngx_rtmp_flv_live_index_module \ + ngx_rtmp_play_module \ + ngx_rtmp_flv_module \ + ngx_rtmp_mp4_module \ + ngx_rtmp_netcall_module \ + ngx_rtmp_relay_module \ + ngx_rtmp_exec_module \ + ngx_rtmp_auto_push_module \ + ngx_rtmp_auto_push_index_module \ + ngx_rtmp_log_module \ + ngx_rtmp_limit_module \ + ngx_rtmp_hls_module \ + ngx_rtmp_dash_module \ + ngx_rtmp_notify_module \ + " + + +RTMP_HTTP_MODULES=" \ + ngx_rtmp_stat_module \ + ngx_rtmp_control_module \ + ngx_http_flv_live_module \ + " + + +RTMP_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_gop_cache_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_version.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_dir/ngx_rtmp_bitop.h \ + $ngx_addon_dir/ngx_rtmp_proxy_protocol.h \ + $ngx_addon_dir/ngx_rtmp_variables.h \ + $ngx_addon_dir/hls/ngx_rtmp_hls_module.h \ + $ngx_addon_dir/hls/ngx_rtmp_mpegts.h \ + $ngx_addon_dir/hls/ngx_rtmp_mpegts_crc.h \ + $ngx_addon_dir/dash/ngx_rtmp_mp4.h \ + " + + +RTMP_CORE_SRCS=" \ + $ngx_addon_dir/ngx_rtmp.c \ + $ngx_addon_dir/ngx_rtmp_init.c \ + $ngx_addon_dir/ngx_rtmp_handshake.c \ + $ngx_addon_dir/ngx_rtmp_handler.c \ + $ngx_addon_dir/ngx_rtmp_amf.c \ + $ngx_addon_dir/ngx_rtmp_send.c \ + $ngx_addon_dir/ngx_rtmp_shared.c \ + $ngx_addon_dir/ngx_rtmp_eval.c \ + $ngx_addon_dir/ngx_rtmp_receive.c \ + $ngx_addon_dir/ngx_rtmp_core_module.c \ + $ngx_addon_dir/ngx_rtmp_cmd_module.c \ + $ngx_addon_dir/ngx_rtmp_gop_cache_module.c \ + $ngx_addon_dir/ngx_rtmp_codec_module.c \ + $ngx_addon_dir/ngx_rtmp_access_module.c \ + $ngx_addon_dir/ngx_rtmp_record_module.c \ + $ngx_addon_dir/ngx_rtmp_live_module.c \ + $ngx_addon_dir/ngx_rtmp_flv_live_index_module.c \ + $ngx_addon_dir/ngx_rtmp_play_module.c \ + $ngx_addon_dir/ngx_rtmp_flv_module.c \ + $ngx_addon_dir/ngx_rtmp_mp4_module.c \ + $ngx_addon_dir/ngx_rtmp_netcall_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 \ + $ngx_addon_dir/ngx_rtmp_auto_push_module.c \ + $ngx_addon_dir/ngx_rtmp_notify_module.c \ + $ngx_addon_dir/ngx_rtmp_log_module.c \ + $ngx_addon_dir/ngx_rtmp_limit_module.c \ + $ngx_addon_dir/ngx_rtmp_bitop.c \ + $ngx_addon_dir/ngx_rtmp_proxy_protocol.c \ + $ngx_addon_dir/ngx_rtmp_variables.c \ + $ngx_addon_dir/ngx_rtmp_parse.c \ + $ngx_addon_dir/hls/ngx_rtmp_hls_module.c \ + $ngx_addon_dir/dash/ngx_rtmp_dash_module.c \ + $ngx_addon_dir/hls/ngx_rtmp_mpegts.c \ + $ngx_addon_dir/hls/ngx_rtmp_mpegts_crc.c \ + $ngx_addon_dir/dash/ngx_rtmp_mp4.c \ + " + + +RTMP_HTTP_DEPS=" \ + $ngx_addon_dir/ngx_http_flv_live_module.h \ + " + + +RTMP_HTTP_SRCS=" \ + $ngx_addon_dir/ngx_rtmp_stat_module.c \ + $ngx_addon_dir/ngx_rtmp_control_module.c \ + $ngx_addon_dir/ngx_http_flv_live_module.c \ + " + +if [ -f auto/module ] ; then + ngx_module_incs=$ngx_addon_dir + ngx_module_deps="$RTMP_DEPS $RTMP_HTTP_DEPS" + + if [ $ngx_module_link = DYNAMIC ] ; then + ngx_module_name="$ngx_addon_name $RTMP_CORE_MODULES $RTMP_HTTP_MODULES" + ngx_module_srcs="$RTMP_CORE_SRCS $RTMP_HTTP_SRCS" + + . auto/module + + dynamic_modules=`eval echo '$'"${ngx_module}_MODULES" | sed -e "s/ \{0,\}$ngx_addon_name//"` + eval ${ngx_module}_MODULES=\"$dynamic_modules\" + unset dynamic_modules + + order_modules=`eval echo '$'"${ngx_module}_ORDER"` + if [ -n "$order_modules" ] + then + eval ${ngx_module}_ORDER=\"`echo "$order_modules" | sed -e "s/ \{0,\}$ngx_addon_name//"`\" + unset order_modules + fi + else + ngx_module_type=CORE + ngx_module_name=$RTMP_CORE_MODULES + ngx_module_deps=$RTMP_DEPS + ngx_module_srcs=$RTMP_CORE_SRCS + + . auto/module + + + ngx_module_type=HTTP + ngx_module_name=$RTMP_HTTP_MODULES + ngx_module_deps=$RTMP_HTTP_DEPS + ngx_module_srcs=$RTMP_HTTP_SRCS + + . auto/module + fi + +else + CORE_MODULES="$CORE_MODULES $RTMP_CORE_MODULES" + HTTP_MODULES="$HTTP_MODULES $RTMP_HTTP_MODULES" + + NGX_ADDON_DEPS="$NGX_ADDON_DEPS $RTMP_DEPS $RTMP_HTTP_DEPS" + NGX_ADDON_SRCS="$NGX_ADDON_SRCS $RTMP_CORE_SRCS $RTMP_HTTP_SRCS" + + CFLAGS="$CFLAGS -I$ngx_addon_dir" +fi + +USE_OPENSSL=YES + diff --git a/ngx_http_flv_module/dash/ngx_rtmp_dash_module.c b/ngx_http_flv_module/dash/ngx_rtmp_dash_module.c new file mode 100644 index 0000000..6e65ce4 --- /dev/null +++ b/ngx_http_flv_module/dash/ngx_rtmp_dash_module.c @@ -0,0 +1,1539 @@ + + +#include +#include +#include +#include +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_mp4.h" + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_close_stream_pt next_close_stream; +static ngx_rtmp_stream_begin_pt next_stream_begin; +static ngx_rtmp_stream_eof_pt next_stream_eof; + + +static ngx_int_t ngx_rtmp_dash_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_dash_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_dash_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +static ngx_int_t ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s); + + +#define NGX_RTMP_DASH_BUFSIZE (1024*1024) +#define NGX_RTMP_DASH_MAX_VIDEO_MDAT (10*1024*1024) +#define NGX_RTMP_DASH_MAX_AUDIO_MDAT (1024*1024) +#define NGX_RTMP_DASH_MAX_SAMPLES 1024 +#define NGX_RTMP_DASH_DIR_ACCESS 0744 + + +typedef struct { + uint32_t timestamp; + uint32_t duration; +} ngx_rtmp_dash_frag_t; + + +typedef struct { + ngx_uint_t id; + ngx_uint_t opened; + ngx_uint_t mdat_size; + ngx_uint_t sample_count; + ngx_uint_t sample_mask; + ngx_fd_t fd; + char type; + uint32_t earliest_pres_time; + uint32_t latest_pres_time; + ngx_rtmp_mp4_sample_t samples[NGX_RTMP_DASH_MAX_SAMPLES]; +} ngx_rtmp_dash_track_t; + + +typedef struct { + ngx_str_t playlist; + ngx_str_t playlist_bak; + ngx_str_t name; + ngx_str_t stream; + time_t start_time; + + ngx_uint_t nfrags; + ngx_uint_t frag; + ngx_rtmp_dash_frag_t *frags; /* circular 2 * winfrags + 1 */ + + unsigned opened:1; + unsigned has_video:1; + unsigned has_audio:1; + + ngx_file_t video_file; + ngx_file_t audio_file; + + ngx_uint_t id; + + ngx_rtmp_dash_track_t audio; + ngx_rtmp_dash_track_t video; +} ngx_rtmp_dash_ctx_t; + + +typedef struct { + ngx_str_t path; + ngx_msec_t playlen; +} ngx_rtmp_dash_cleanup_t; + + +typedef struct { + ngx_flag_t dash; + ngx_msec_t fraglen; + ngx_msec_t playlen; + ngx_flag_t nested; + ngx_str_t path; + ngx_uint_t winfrags; + ngx_flag_t cleanup; + ngx_path_t *slot; +} ngx_rtmp_dash_app_conf_t; + + +static ngx_command_t ngx_rtmp_dash_commands[] = { + + { ngx_string("dash"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, dash), + NULL }, + + { ngx_string("dash_fragment"), + 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_dash_app_conf_t, fraglen), + NULL }, + + { ngx_string("dash_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_dash_app_conf_t, path), + NULL }, + + { ngx_string("dash_playlist_length"), + 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_dash_app_conf_t, playlen), + NULL }, + + { ngx_string("dash_cleanup"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, cleanup), + NULL }, + + { ngx_string("dash_nested"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_dash_app_conf_t, nested), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_dash_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_dash_postconfiguration, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_rtmp_dash_create_app_conf, /* create location configuration */ + ngx_rtmp_dash_merge_app_conf, /* merge location configuration */ +}; + + +ngx_module_t ngx_rtmp_dash_module = { + NGX_MODULE_V1, + &ngx_rtmp_dash_module_ctx, /* module context */ + ngx_rtmp_dash_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static ngx_rtmp_dash_frag_t * +ngx_rtmp_dash_get_frag(ngx_rtmp_session_t *s, ngx_int_t n) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + return &ctx->frags[(ctx->frag + n) % (dacf->winfrags * 2 + 1)]; +} + + +static void +ngx_rtmp_dash_next_frag(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (ctx->nfrags == dacf->winfrags) { + ctx->frag++; + } else { + ctx->nfrags++; + } +} + + +static ngx_int_t +ngx_rtmp_dash_rename_file(u_char *src, u_char *dst) +{ + /* rename file with overwrite */ + +#if (NGX_WIN32) + return MoveFileEx((LPCTSTR) src, (LPCTSTR) dst, MOVEFILE_REPLACE_EXISTING); +#else + return ngx_rename_file(src, dst); +#endif +} + + +static ngx_int_t +ngx_rtmp_dash_write_playlist(ngx_rtmp_session_t *s) +{ + char *sep; + u_char *p, *last; + ssize_t n; + ngx_fd_t fd; + struct tm tm; + ngx_str_t noname, *name; + ngx_uint_t i; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_dash_frag_t *f; + ngx_rtmp_dash_app_conf_t *dacf; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + static u_char start_time[sizeof("1970-09-28T12:00:00Z")]; + static u_char pub_time[sizeof("1970-09-28T12:00:00Z")]; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (dacf == NULL || ctx == NULL || codec_ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->id == 0) { + ngx_rtmp_dash_write_init_segments(s); + } + + fd = ngx_open_file(ctx->playlist_bak.data, NGX_FILE_WRONLY, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: open failed: '%V'", &ctx->playlist_bak); + return NGX_ERROR; + } + + +#define NGX_RTMP_DASH_MANIFEST_HEADER \ + "\n" \ + "\n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_VIDEO \ + " \n" \ + " \n" \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_VIDEO_FOOTER \ + " \n" \ + " \n" \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_TIME \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_AUDIO \ + " \n" \ + " \n" \ + " \n" \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_AUDIO_FOOTER \ + " \n" \ + " \n" \ + " \n" \ + " \n" + + +#define NGX_RTMP_DASH_MANIFEST_FOOTER \ + " \n" \ + "\n" + + ngx_libc_gmtime(ctx->start_time, &tm); + + ngx_sprintf(start_time, "%4d-%02d-%02dT%02d:%02d:%02dZ%Z", + tm.tm_year + 1900, tm.tm_mon + 1, + tm.tm_mday, tm.tm_hour, + tm.tm_min, tm.tm_sec); + + ngx_libc_gmtime(ngx_time(), &tm); + + ngx_sprintf(pub_time, "%4d-%02d-%02dT%02d:%02d:%02dZ%Z", + tm.tm_year + 1900, tm.tm_mon + 1, + tm.tm_mday, tm.tm_hour, + tm.tm_min, tm.tm_sec); + + last = buffer + sizeof(buffer); + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_HEADER, + start_time, + pub_time, + (ngx_uint_t) (dacf->fraglen / 1000), + (ngx_uint_t) (dacf->fraglen / 500), + (ngx_uint_t) (dacf->playlen / 1000)); + + n = ngx_write_fd(fd, buffer, p - buffer); + + ngx_str_null(&noname); + + name = (dacf->nested ? &noname : &ctx->name); + sep = (dacf->nested ? "" : "-"); + + if (ctx->has_video) { + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_VIDEO, + codec_ctx->width, + codec_ctx->height, + codec_ctx->frame_rate, + &ctx->name, + codec_ctx->avc_profile, + codec_ctx->avc_compat, + codec_ctx->avc_level, + codec_ctx->width, + codec_ctx->height, + codec_ctx->frame_rate, + (ngx_uint_t) (codec_ctx->video_data_rate * 1000), + name, sep, + name, sep); + + for (i = 0; i < ctx->nfrags; i++) { + f = ngx_rtmp_dash_get_frag(s, i); + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_TIME, + f->timestamp, f->duration); + } + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_VIDEO_FOOTER); + + n = ngx_write_fd(fd, buffer, p - buffer); + } + + if (ctx->has_audio) { + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_AUDIO, + &ctx->name, + codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC ? + (codec_ctx->aac_sbr ? "40.5" : "40.2") : "6b", + codec_ctx->sample_rate, + (ngx_uint_t) (codec_ctx->audio_data_rate * 1000), + name, sep, + name, sep); + + for (i = 0; i < ctx->nfrags; i++) { + f = ngx_rtmp_dash_get_frag(s, i); + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_TIME, + f->timestamp, f->duration); + } + + p = ngx_slprintf(p, last, NGX_RTMP_DASH_MANIFEST_AUDIO_FOOTER); + + n = ngx_write_fd(fd, buffer, p - buffer); + } + + p = ngx_slprintf(buffer, last, NGX_RTMP_DASH_MANIFEST_FOOTER); + n = ngx_write_fd(fd, buffer, p - buffer); + + if (n < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: write failed: '%V'", &ctx->playlist_bak); + ngx_close_file(fd); + return NGX_ERROR; + } + + ngx_close_file(fd); + + if (ngx_rtmp_dash_rename_file(ctx->playlist_bak.data, ctx->playlist.data) + == NGX_FILE_ERROR) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: rename failed: '%V'->'%V'", + &ctx->playlist_bak, &ctx->playlist); + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_write_init_segments(ngx_rtmp_session_t *s) +{ + ngx_fd_t fd; + ngx_int_t rc; + ngx_buf_t b; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (ctx == NULL || codec_ctx == NULL) { + return NGX_ERROR; + } + + /* init video */ + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "init.m4v") = 0; + + fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, NGX_FILE_TRUNCATE, + NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating video init file"); + return NGX_ERROR; + } + + b.start = buffer; + b.end = b.start + sizeof(buffer); + b.pos = b.last = b.start; + + ngx_rtmp_mp4_write_ftyp(&b); + ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_VIDEO_TRACK); + + rc = ngx_write_fd(fd, b.start, (size_t) (b.last - b.start)); + if (rc == NGX_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: writing video init failed"); + } + + ngx_close_file(fd); + + /* init audio */ + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "init.m4a") = 0; + + fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, NGX_FILE_TRUNCATE, + NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating dash audio init file"); + return NGX_ERROR; + } + + b.pos = b.last = b.start; + + ngx_rtmp_mp4_write_ftyp(&b); + ngx_rtmp_mp4_write_moov(s, &b, NGX_RTMP_MP4_AUDIO_TRACK); + + rc = ngx_write_fd(fd, b.start, (size_t) (b.last - b.start)); + if (rc == NGX_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: writing audio init failed"); + } + + ngx_close_file(fd); + + return NGX_OK; +} + + +static void +ngx_rtmp_dash_close_fragment(ngx_rtmp_session_t *s, ngx_rtmp_dash_track_t *t) +{ + u_char *pos, *pos1; + size_t left; + ssize_t n; + ngx_fd_t fd; + ngx_buf_t b; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_frag_t *f; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + + if (!t->opened) { + return; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: close fragment id=%ui, type=%c, pts=%uD", + t->id, t->type, t->earliest_pres_time); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + b.start = buffer; + b.end = buffer + sizeof(buffer); + b.pos = b.last = b.start; + + ngx_rtmp_mp4_write_styp(&b); + + pos = b.last; + b.last += 44; /* leave room for sidx */ + + ngx_rtmp_mp4_write_moof(&b, t->earliest_pres_time, t->sample_count, + t->samples, t->sample_mask, t->id); + pos1 = b.last; + b.last = pos; + + ngx_rtmp_mp4_write_sidx(&b, t->mdat_size + 8 + (pos1 - (pos + 44)), + t->earliest_pres_time, t->latest_pres_time); + b.last = pos1; + ngx_rtmp_mp4_write_mdat(&b, t->mdat_size + 8); + + /* move the data down to make room for the headers */ + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "%uD.m4%c", + f->timestamp, t->type) = 0; + + fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating dash temp video file"); + goto done; + } + + if (ngx_write_fd(fd, b.pos, (size_t) (b.last - b.pos)) == NGX_ERROR) { + goto done; + } + + left = (size_t) t->mdat_size; + +#if (NGX_WIN32) + if (SetFilePointer(t->fd, 0, 0, FILE_BEGIN) == INVALID_SET_FILE_POINTER) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: SetFilePointer error"); + goto done; + } +#else + if (lseek(t->fd, 0, SEEK_SET) == -1) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: lseek error"); + goto done; + } +#endif + + while (left > 0) { + + n = ngx_read_fd(t->fd, buffer, ngx_min(sizeof(buffer), left)); + if (n == 0 || n == NGX_ERROR) { + break; + } + + n = ngx_write_fd(fd, buffer, (size_t) n); + if (n == 0 || n == NGX_ERROR) { + break; + } + + left -= n; + } + +done: + + if (fd != NGX_INVALID_FILE) { + ngx_close_file(fd); + } + + ngx_close_file(t->fd); + + t->fd = NGX_INVALID_FILE; + t->opened = 0; +} + + +static ngx_int_t +ngx_rtmp_dash_close_fragments(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + if (ctx == NULL || !ctx->opened) { + return NGX_OK; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: close fragments"); + + ngx_rtmp_dash_close_fragment(s, &ctx->video); + ngx_rtmp_dash_close_fragment(s, &ctx->audio); + + ngx_rtmp_dash_next_frag(s); + + ngx_rtmp_dash_write_playlist(s); + + ctx->id++; + ctx->opened = 0; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_open_fragment(ngx_rtmp_session_t *s, ngx_rtmp_dash_track_t *t, + ngx_uint_t id, char type) +{ + ngx_rtmp_dash_ctx_t *ctx; + + if (t->opened) { + return NGX_OK; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: open fragment id=%ui, type='%c'", id, type); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + *ngx_sprintf(ctx->stream.data + ctx->stream.len, "raw.m4%c", type) = 0; + + t->fd = ngx_open_file(ctx->stream.data, NGX_FILE_RDWR, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (t->fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: error creating fragment file"); + return NGX_ERROR; + } + + t->id = id; + t->type = type; + t->sample_count = 0; + t->earliest_pres_time = 0; + t->latest_pres_time = 0; + t->mdat_size = 0; + t->opened = 1; + + if (type == 'v') { + t->sample_mask = NGX_RTMP_MP4_SAMPLE_SIZE| + NGX_RTMP_MP4_SAMPLE_DURATION| + NGX_RTMP_MP4_SAMPLE_DELAY| + NGX_RTMP_MP4_SAMPLE_KEY; + } else { + t->sample_mask = NGX_RTMP_MP4_SAMPLE_SIZE| + NGX_RTMP_MP4_SAMPLE_DURATION; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_open_fragments(ngx_rtmp_session_t *s) +{ + ngx_rtmp_dash_ctx_t *ctx; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: open fragments"); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (ctx->opened) { + return NGX_OK; + } + + ngx_rtmp_dash_open_fragment(s, &ctx->video, ctx->id, 'v'); + + ngx_rtmp_dash_open_fragment(s, &ctx->audio, ctx->id, 'a'); + + ctx->opened = 1; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_ensure_directory(ngx_rtmp_session_t *s) +{ + size_t len; + ngx_file_info_t fi; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + static u_char path[NGX_MAX_PATH + 1]; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + + *ngx_snprintf(path, sizeof(path) - 1, "%V", &dacf->path) = 0; + + if (ngx_file_info(path, &fi) == NGX_FILE_ERROR) { + + if (ngx_errno != NGX_ENOENT) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_file_info_n " failed on '%V'", + &dacf->path); + return NGX_ERROR; + } + + /* ENOENT */ + + if (ngx_create_dir(path, NGX_RTMP_DASH_DIR_ACCESS) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_create_dir_n " failed on '%V'", + &dacf->path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: directory '%V' created", &dacf->path); + + } else { + + if (!ngx_is_dir(&fi)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: '%V' exists and is not a directory", + &dacf->path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: directory '%V' exists", &dacf->path); + } + + if (!dacf->nested) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + len = dacf->path.len; + if (dacf->path.data[len - 1] == '/') { + len--; + } + + *ngx_snprintf(path, sizeof(path) - 1, "%*s/%V", len, dacf->path.data, + &ctx->name) = 0; + + if (ngx_file_info(path, &fi) != NGX_FILE_ERROR) { + + if (ngx_is_dir(&fi)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: directory '%s' exists", path); + return NGX_OK; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: '%s' exists and is not a directory", path); + + return NGX_ERROR; + } + + if (ngx_errno != NGX_ENOENT) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_file_info_n " failed on '%s'", path); + return NGX_ERROR; + } + + /* NGX_ENOENT */ + + if (ngx_create_dir(path, NGX_RTMP_DASH_DIR_ACCESS) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_create_dir_n " failed on '%s'", path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: directory '%s' created", path); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + u_char *p; + size_t len; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_frag_t *f; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + if (dacf == NULL || !dacf->dash || dacf->path.len == 0) { + goto next; + } + + if (s->auto_pushed) { + goto next; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: publish: name='%s' type='%s'", v->name, v->type); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_dash_ctx_t)); + if (ctx == NULL) { + goto next; + } + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_dash_module); + + } else { + if (ctx->opened) { + goto next; + } + + f = ctx->frags; + ngx_memzero(ctx, sizeof(ngx_rtmp_dash_ctx_t)); + ctx->frags = f; + } + + if (ctx->frags == NULL) { + ctx->frags = ngx_pcalloc(s->connection->pool, + sizeof(ngx_rtmp_dash_frag_t) * + (dacf->winfrags * 2 + 1)); + if (ctx->frags == NULL) { + return NGX_ERROR; + } + } + + ctx->id = 0; + + if (ngx_strstr(v->name, "..")) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "dash: bad stream name: '%s'", v->name); + return NGX_ERROR; + } + + ctx->name.len = ngx_strlen(v->name); + ctx->name.data = ngx_palloc(s->connection->pool, ctx->name.len + 1); + + if (ctx->name.data == NULL) { + return NGX_ERROR; + } + + *ngx_cpymem(ctx->name.data, v->name, ctx->name.len) = 0; + + len = dacf->path.len + 1 + ctx->name.len + sizeof(".mpd"); + if (dacf->nested) { + len += sizeof("/index") - 1; + } + + ctx->playlist.data = ngx_palloc(s->connection->pool, len); + if (ctx->playlist.data == NULL) { + return NGX_ERROR; + } + + p = ngx_cpymem(ctx->playlist.data, dacf->path.data, dacf->path.len); + + if (p[-1] != '/') { + *p++ = '/'; + } + + p = ngx_cpymem(p, ctx->name.data, ctx->name.len); + + /* + * ctx->stream holds initial part of stream file path + * however the space for the whole stream path + * is allocated + */ + + ctx->stream.len = p - ctx->playlist.data + 1; + ctx->stream.data = ngx_palloc(s->connection->pool, + ctx->stream.len + NGX_INT32_LEN + + sizeof(".m4x")); + if (ctx->stream.data == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(ctx->stream.data, ctx->playlist.data, ctx->stream.len - 1); + ctx->stream.data[ctx->stream.len - 1] = (dacf->nested ? '/' : '-'); + + if (dacf->nested) { + p = ngx_cpymem(p, "/index.mpd", sizeof("/index.mpd") - 1); + } else { + p = ngx_cpymem(p, ".mpd", sizeof(".mpd") - 1); + } + + ctx->playlist.len = p - ctx->playlist.data; + + *p = 0; + + /* playlist bak (new playlist) path */ + + ctx->playlist_bak.data = ngx_palloc(s->connection->pool, + ctx->playlist.len + sizeof(".bak")); + if (ctx->playlist_bak.data == NULL) { + return NGX_ERROR; + } + + p = ngx_cpymem(ctx->playlist_bak.data, ctx->playlist.data, + ctx->playlist.len); + p = ngx_cpymem(p, ".bak", sizeof(".bak") - 1); + + ctx->playlist_bak.len = p - ctx->playlist_bak.data; + + *p = 0; + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: playlist='%V' playlist_bak='%V' stream_pattern='%V'", + &ctx->playlist, &ctx->playlist_bak, &ctx->stream); + + ctx->start_time = ngx_time(); + + if (ngx_rtmp_dash_ensure_directory(s) != NGX_OK) { + return NGX_ERROR; + } + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_dash_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + + if (dacf == NULL || !dacf->dash || ctx == NULL) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "dash: delete stream"); + + ngx_rtmp_dash_close_fragments(s); + +next: + return next_close_stream(s, v); +} + + +static void +ngx_rtmp_dash_update_fragments(ngx_rtmp_session_t *s, ngx_int_t boundary, + uint32_t timestamp) +{ + int32_t d; + ngx_int_t hit; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_dash_frag_t *f; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + + d = (int32_t) (timestamp - f->timestamp); + + if (d >= 0) { + + f->duration = timestamp - f->timestamp; + hit = (f->duration >= dacf->fraglen); + + } else { + + /* sometimes clients generate slightly unordered frames */ + + hit = (-d > 1000); + } + + if (ctx->has_video && !hit) { + boundary = 0; + } + + if (!ctx->has_video && ctx->has_audio) { + boundary = hit; + } + + /* generally, the size of a audio fragment is mush smaller than + * the size of a video fragment, so it will take a long time to + * produce a audio fragment if a stream contains only audio. + */ + + if (ctx->audio.mdat_size >= NGX_RTMP_DASH_MAX_AUDIO_MDAT) { + boundary = 1; + } + + if (ctx->video.mdat_size >= NGX_RTMP_DASH_MAX_VIDEO_MDAT) { + boundary = 1; + } + + if (!ctx->opened) { + boundary = 1; + } + + if (boundary) { + ngx_rtmp_dash_close_fragments(s); + ngx_rtmp_dash_open_fragments(s); + + f = ngx_rtmp_dash_get_frag(s, ctx->nfrags); + f->timestamp = timestamp; + } +} + + +static ngx_int_t +ngx_rtmp_dash_append(ngx_rtmp_session_t *s, ngx_chain_t *in, + ngx_rtmp_dash_track_t *t, ngx_int_t key, uint32_t timestamp, uint32_t delay) +{ + u_char *p; + size_t size, bsize; + ngx_rtmp_mp4_sample_t *smpl; + + static u_char buffer[NGX_RTMP_DASH_BUFSIZE]; + + p = buffer; + size = 0; + + for (; in && size < sizeof(buffer); in = in->next) { + + bsize = (size_t) (in->buf->last - in->buf->pos); + if (size + bsize > sizeof(buffer)) { + bsize = (size_t) (sizeof(buffer) - size); + } + + p = ngx_cpymem(p, in->buf->pos, bsize); + size += bsize; + } + + ngx_rtmp_dash_update_fragments(s, key, timestamp); + + if (t->sample_count == 0) { + t->earliest_pres_time = timestamp; + } + + t->latest_pres_time = timestamp; + + if (t->sample_count < NGX_RTMP_DASH_MAX_SAMPLES) { + + if (ngx_write_fd(t->fd, buffer, size) == NGX_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: " ngx_write_fd_n " failed"); + return NGX_ERROR; + } + + smpl = &t->samples[t->sample_count]; + + smpl->delay = delay; + smpl->size = (uint32_t) size; + smpl->duration = 0; + smpl->timestamp = timestamp; + smpl->key = (key ? 1 : 0); + + if (t->sample_count > 0) { + smpl = &t->samples[t->sample_count - 1]; + smpl->duration = timestamp - smpl->timestamp; + } + + t->sample_count++; + t->mdat_size += (ngx_uint_t) size; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + u_char htype; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (dacf == NULL || !dacf->dash || ctx == NULL || + codec_ctx == NULL || h->mlen < 2) + { + return NGX_OK; + } + + /* Only AAC is supported */ + + if (codec_ctx->audio_codec_id != NGX_RTMP_AUDIO_AAC || + codec_ctx->aac_header == NULL) + { + return NGX_OK; + } + + if (in->buf->last - in->buf->pos < 2) { + return NGX_ERROR; + } + + /* skip AAC config */ + + htype = in->buf->pos[1]; + if (htype != 1) { + return NGX_OK; + } + + ctx->has_audio = 1; + + /* skip RTMP & AAC headers */ + + in->buf->pos += 2; + + return ngx_rtmp_dash_append(s, in, &ctx->audio, 0, h->timestamp, 0); +} + + +static ngx_int_t +ngx_rtmp_dash_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + u_char *p; + uint8_t ftype, htype; + uint32_t delay; + ngx_rtmp_dash_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_dash_app_conf_t *dacf; + + dacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_dash_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_dash_module); + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (dacf == NULL || !dacf->dash || ctx == NULL || codec_ctx == NULL || + codec_ctx->avc_header == NULL || h->mlen < 5) + { + return NGX_OK; + } + + /* Only H264 is supported */ + + if (codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264) { + return NGX_OK; + } + + if (in->buf->last - in->buf->pos < 5) { + return NGX_ERROR; + } + + ftype = (in->buf->pos[0] & 0xf0) >> 4; + + /* skip AVC config */ + + htype = in->buf->pos[1]; + if (htype != 1) { + return NGX_OK; + } + + p = (u_char *) &delay; + + p[0] = in->buf->pos[4]; + p[1] = in->buf->pos[3]; + p[2] = in->buf->pos[2]; + p[3] = 0; + + ctx->has_video = 1; + + /* skip RTMP & H264 headers */ + + in->buf->pos += 5; + + return ngx_rtmp_dash_append(s, in, &ctx->video, ftype == 1, h->timestamp, + delay); +} + + +static ngx_int_t +ngx_rtmp_dash_stream_begin(ngx_rtmp_session_t *s, ngx_rtmp_stream_begin_t *v) +{ + return next_stream_begin(s, v); +} + + +static ngx_int_t +ngx_rtmp_dash_stream_eof(ngx_rtmp_session_t *s, ngx_rtmp_stream_eof_t *v) +{ + ngx_rtmp_dash_close_fragments(s); + + return next_stream_eof(s, v); +} + + +static ngx_int_t +ngx_rtmp_dash_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) +{ + time_t mtime, max_age; + u_char *p; + u_char path[NGX_MAX_PATH + 1], mpd_path[NGX_MAX_PATH + 1]; + ngx_dir_t dir; + ngx_err_t err; + ngx_str_t name, spath, mpd; + ngx_int_t nentries, nerased; + ngx_file_info_t fi; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup path='%V' playlen=%M", ppath, playlen); + + if (ngx_open_dir(ppath, &dir) != NGX_OK) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, ngx_errno, + "dash: cleanup open dir failed '%V'", ppath); + return NGX_ERROR; + } + + nentries = 0; + nerased = 0; + + for ( ;; ) { + ngx_set_errno(0); + + if (ngx_read_dir(&dir) == NGX_ERROR) { + err = ngx_errno; + + if (ngx_close_dir(&dir) == NGX_ERROR) { + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, ngx_errno, + "dash: cleanup " ngx_close_dir_n " \"%V\" failed", + ppath); + } + + if (err == NGX_ENOMOREFILES) { + return nentries - nerased; + } + + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, err, + "dash: cleanup " ngx_read_dir_n + " '%V' failed", ppath); + return NGX_ERROR; + } + + name.data = ngx_de_name(&dir); + if (name.data[0] == '.') { + continue; + } + + name.len = ngx_de_namelen(&dir); + + p = ngx_snprintf(path, sizeof(path) - 1, "%V/%V", ppath, &name); + *p = 0; + + spath.data = path; + spath.len = p - path; + + nentries++; + + if (!dir.valid_info && ngx_de_info(path, &dir) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, ngx_errno, + "dash: cleanup " ngx_de_info_n " \"%V\" failed", + &spath); + + continue; + } + + if (ngx_de_is_dir(&dir)) { + + if (ngx_rtmp_dash_cleanup_dir(&spath, playlen) == 0) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup dir '%V'", &name); + + /* + * null-termination gets spoiled in win32 + * version of ngx_open_dir + */ + + *p = 0; + + if (ngx_delete_dir(path) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, + "dash: cleanup " ngx_delete_dir_n + " failed on '%V'", &spath); + } else { + nerased++; + } + } + + continue; + } + + if (!ngx_de_is_file(&dir)) { + continue; + } + + if (name.len >= 8 && name.data[name.len - 8] == 'i' && + name.data[name.len - 7] == 'n' && + name.data[name.len - 6] == 'i' && + name.data[name.len - 5] == 't' && + name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'm' && + name.data[name.len - 2] == '4') + { + if (name.len == 8) { + ngx_str_set(&mpd, "index"); + } else { + mpd.data = name.data; + mpd.len = name.len - 9; + } + + p = ngx_snprintf(mpd_path, sizeof(mpd_path) - 1, "%V/%V.mpd", + ppath, &mpd); + *p = 0; + + if (ngx_file_info(mpd_path, &fi) != NGX_FILE_ERROR) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup '%V' delayed, mpd exists '%s'", + &name, mpd_path); + continue; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup '%V' allowed, mpd missing '%s'", + &name, mpd_path); + + max_age = 0; + + } else if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'm' && + name.data[name.len - 2] == '4' && + name.data[name.len - 1] == 'v') + { + max_age = playlen / 500; + + } else if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'm' && + name.data[name.len - 2] == '4' && + name.data[name.len - 1] == 'a') + { + max_age = playlen / 500; + + } else if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'm' && + name.data[name.len - 2] == 'p' && + name.data[name.len - 1] == 'd') + { + max_age = playlen / 500; + + } else if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'r' && + name.data[name.len - 2] == 'a' && + name.data[name.len - 1] == 'w') + { + max_age = playlen / 1000; + + } else { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup skip unknown file type '%V'", &name); + continue; + } + + mtime = ngx_de_mtime(&dir); + if (mtime + max_age > ngx_cached_time->sec) { + continue; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "dash: cleanup '%V' mtime=%T age=%T", + &name, mtime, ngx_cached_time->sec - mtime); + + if (ngx_delete_file(path) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, + "dash: cleanup " ngx_delete_file_n " failed on '%V'", + &spath); + continue; + } + + nerased++; + } +} + + +#if (nginx_version >= 1011005) +static ngx_msec_t +#else +static time_t +#endif +ngx_rtmp_dash_cleanup(void *data) +{ + ngx_rtmp_dash_cleanup_t *cleanup = data; + + ngx_rtmp_dash_cleanup_dir(&cleanup->path, cleanup->playlen); + +#if (nginx_version >= 1011005) + return cleanup->playlen * 2; +#else + return cleanup->playlen / 500; +#endif +} + + +static void * +ngx_rtmp_dash_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_dash_app_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_dash_app_conf_t)); + if (conf == NULL) { + return NULL; + } + + conf->dash = NGX_CONF_UNSET; + conf->fraglen = NGX_CONF_UNSET_MSEC; + conf->playlen = NGX_CONF_UNSET_MSEC; + conf->cleanup = NGX_CONF_UNSET; + conf->nested = NGX_CONF_UNSET; + + return conf; +} + + +static char * +ngx_rtmp_dash_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_dash_app_conf_t *prev = parent; + ngx_rtmp_dash_app_conf_t *conf = child; + ngx_rtmp_dash_cleanup_t *cleanup; + + ngx_conf_merge_value(conf->dash, prev->dash, 0); + ngx_conf_merge_msec_value(conf->fraglen, prev->fraglen, 5000); + ngx_conf_merge_msec_value(conf->playlen, prev->playlen, 30000); + ngx_conf_merge_value(conf->cleanup, prev->cleanup, 1); + ngx_conf_merge_value(conf->nested, prev->nested, 0); + + if (conf->fraglen) { + conf->winfrags = conf->playlen / conf->fraglen; + } + + /* schedule cleanup */ + + if (conf->dash && conf->path.len && conf->cleanup) { + if (conf->path.data[conf->path.len - 1] == '/') { + conf->path.len--; + } + + cleanup = ngx_pcalloc(cf->pool, sizeof(*cleanup)); + if (cleanup == NULL) { + return NGX_CONF_ERROR; + } + + cleanup->path = conf->path; + cleanup->playlen = conf->playlen; + + conf->slot = ngx_pcalloc(cf->pool, sizeof(*conf->slot)); + if (conf->slot == NULL) { + return NGX_CONF_ERROR; + } + + conf->slot->manager = ngx_rtmp_dash_cleanup; + conf->slot->name = conf->path; + conf->slot->data = cleanup; + conf->slot->conf_file = cf->conf_file->file.name.data; + conf->slot->line = cf->conf_file->line; + + if (ngx_add_path(cf, &conf->slot) != NGX_OK) { + return NGX_CONF_ERROR; + } + } + + ngx_conf_merge_str_value(conf->path, prev->path, ""); + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_dash_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_handler_pt *h; + ngx_rtmp_core_main_conf_t *cmcf; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); + *h = ngx_rtmp_dash_video; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); + *h = ngx_rtmp_dash_audio; + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_dash_publish; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_dash_close_stream; + + next_stream_begin = ngx_rtmp_stream_begin; + ngx_rtmp_stream_begin = ngx_rtmp_dash_stream_begin; + + next_stream_eof = ngx_rtmp_stream_eof; + ngx_rtmp_stream_eof = ngx_rtmp_dash_stream_eof; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/dash/ngx_rtmp_mp4.c b/ngx_http_flv_module/dash/ngx_rtmp_mp4.c new file mode 100644 index 0000000..dd680c9 --- /dev/null +++ b/ngx_http_flv_module/dash/ngx_rtmp_mp4.c @@ -0,0 +1,1167 @@ + + +#include +#include +#include "ngx_rtmp_mp4.h" +#include + + +static ngx_int_t +ngx_rtmp_mp4_field_32(ngx_buf_t *b, uint32_t n) +{ + u_char bytes[4]; + + bytes[0] = ((uint32_t) n >> 24) & 0xFF; + bytes[1] = ((uint32_t) n >> 16) & 0xFF; + bytes[2] = ((uint32_t) n >> 8) & 0xFF; + bytes[3] = (uint32_t) n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_field_24(ngx_buf_t *b, uint32_t n) +{ + u_char bytes[3]; + + bytes[0] = ((uint32_t) n >> 16) & 0xFF; + bytes[1] = ((uint32_t) n >> 8) & 0xFF; + bytes[2] = (uint32_t) n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_field_16(ngx_buf_t *b, uint16_t n) +{ + u_char bytes[2]; + + bytes[0] = ((uint32_t) n >> 8) & 0xFF; + bytes[1] = (uint32_t) n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_field_8(ngx_buf_t *b, uint8_t n) +{ + u_char bytes[1]; + + bytes[0] = n & 0xFF; + + if (b->last + sizeof(bytes) > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, bytes, sizeof(bytes)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_put_descr(ngx_buf_t *b, int tag, size_t size) +{ + ngx_rtmp_mp4_field_8(b, (uint8_t) tag); + ngx_rtmp_mp4_field_8(b, size & 0x7F); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_data(ngx_buf_t *b, void *data, size_t n) +{ + if (b->last + n > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, (u_char *) data, n); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_box(ngx_buf_t *b, const char box[4]) +{ + if (b->last + 4 > b->end) { + return NGX_ERROR; + } + + b->last = ngx_cpymem(b->last, (u_char *) box, 4); + + return NGX_OK; +} + + +static u_char * +ngx_rtmp_mp4_start_box(ngx_buf_t *b, const char box[4]) +{ + u_char *p; + + p = b->last; + + if (ngx_rtmp_mp4_field_32(b, 0) != NGX_OK) { + return NULL; + } + + if (ngx_rtmp_mp4_box(b, box) != NGX_OK) { + return NULL; + } + + return p; +} + + +static ngx_int_t +ngx_rtmp_mp4_update_box_size(ngx_buf_t *b, u_char *p) +{ + u_char *curpos; + + if (p == NULL) { + return NGX_ERROR; + } + + curpos = b->last; + + b->last = p; + + ngx_rtmp_mp4_field_32(b, (uint32_t) (curpos - p)); + + b->last = curpos; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_matrix(ngx_buf_t *buf, uint32_t a, uint32_t b, uint32_t c, + uint32_t d, uint32_t tx, uint32_t ty) +{ + +/* + * transformation matrix + * |a b u| + * |c d v| + * |tx ty w| + */ + + ngx_rtmp_mp4_field_32(buf, a << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, b << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, 0); /* u in 2.30 format */ + ngx_rtmp_mp4_field_32(buf, c << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, d << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, 0); /* v in 2.30 format */ + ngx_rtmp_mp4_field_32(buf, tx << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, ty << 16); /* 16.16 format */ + ngx_rtmp_mp4_field_32(buf, 1 << 30); /* w in 2.30 format */ + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_mp4_write_ftyp(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "ftyp"); + + /* major brand */ + ngx_rtmp_mp4_box(b, "iso6"); + + /* minor version */ + ngx_rtmp_mp4_field_32(b, 1); + + /* compatible brands */ + ngx_rtmp_mp4_box(b, "isom"); + ngx_rtmp_mp4_box(b, "iso6"); + ngx_rtmp_mp4_box(b, "dash"); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_mp4_write_styp(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "styp"); + + /* major brand */ + ngx_rtmp_mp4_box(b, "iso6"); + + /* minor version */ + ngx_rtmp_mp4_field_32(b, 1); + + /* compatible brands */ + ngx_rtmp_mp4_box(b, "isom"); + ngx_rtmp_mp4_box(b, "iso6"); + ngx_rtmp_mp4_box(b, "dash"); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mvhd(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mvhd"); + + /* version */ + ngx_rtmp_mp4_field_32(b, 0); + + /* creation time */ + ngx_rtmp_mp4_field_32(b, 0); + + /* modification time */ + ngx_rtmp_mp4_field_32(b, 0); + + /* timescale */ + ngx_rtmp_mp4_field_32(b, 1000); + + /* duration */ + ngx_rtmp_mp4_field_32(b, 0); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0x00010000); + ngx_rtmp_mp4_field_16(b, 0x0100); + ngx_rtmp_mp4_field_16(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + ngx_rtmp_mp4_write_matrix(b, 1, 0, 0, 1, 0, 0); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + /* next track id */ + ngx_rtmp_mp4_field_32(b, 1); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_tkhd(ngx_rtmp_session_t *s, ngx_buf_t *b, + ngx_rtmp_mp4_track_type_t ttype) +{ + u_char *pos; + ngx_rtmp_codec_ctx_t *codec_ctx; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + pos = ngx_rtmp_mp4_start_box(b, "tkhd"); + + /* version */ + ngx_rtmp_mp4_field_8(b, 0); + + /* flags: TrackEnabled */ + ngx_rtmp_mp4_field_24(b, 0x0000000f); + + /* creation time */ + ngx_rtmp_mp4_field_32(b, 0); + + /* modification time */ + ngx_rtmp_mp4_field_32(b, 0); + + /* track id */ + ngx_rtmp_mp4_field_32(b, 1); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + + /* duration */ + ngx_rtmp_mp4_field_32(b, 0); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + /* reserved */ + ngx_rtmp_mp4_field_16(b, ttype == NGX_RTMP_MP4_VIDEO_TRACK ? 0 : 0x0100); + + /* reserved */ + ngx_rtmp_mp4_field_16(b, 0); + + ngx_rtmp_mp4_write_matrix(b, 1, 0, 0, 1, 0, 0); + + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + ngx_rtmp_mp4_field_32(b, (uint32_t) codec_ctx->width << 16); + ngx_rtmp_mp4_field_32(b, (uint32_t) codec_ctx->height << 16); + } else { + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mdhd(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mdhd"); + + /* version */ + ngx_rtmp_mp4_field_32(b, 0); + + /* creation time */ + ngx_rtmp_mp4_field_32(b, 0); + + /* modification time */ + ngx_rtmp_mp4_field_32(b, 0); + + /* time scale*/ + ngx_rtmp_mp4_field_32(b, 1000); + + /* duration */ + ngx_rtmp_mp4_field_32(b, 0); + + /* lanuguage */ + ngx_rtmp_mp4_field_16(b, 0x15C7); + + /* reserved */ + ngx_rtmp_mp4_field_16(b, 0); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_hdlr(ngx_buf_t *b, ngx_rtmp_mp4_track_type_t ttype) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "hdlr"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* pre defined */ + ngx_rtmp_mp4_field_32(b, 0); + + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + ngx_rtmp_mp4_box(b, "vide"); + } else { + ngx_rtmp_mp4_box(b, "soun"); + } + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + /* video handler string, NULL-terminated */ + ngx_rtmp_mp4_data(b, "VideoHandler", sizeof("VideoHandler")); + } else { + /* sound handler string, NULL-terminated */ + ngx_rtmp_mp4_data(b, "SoundHandler", sizeof("SoundHandler")); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_vmhd(ngx_buf_t *b) +{ + /* size is always 20, apparently */ + ngx_rtmp_mp4_field_32(b, 20); + + ngx_rtmp_mp4_box(b, "vmhd"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0x01); + + /* reserved (graphics mode=copy) */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_smhd(ngx_buf_t *b) +{ + /* size is always 16, apparently */ + ngx_rtmp_mp4_field_32(b, 16); + + ngx_rtmp_mp4_box(b, "smhd"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* reserved (balance normally=0) */ + ngx_rtmp_mp4_field_16(b, 0); + ngx_rtmp_mp4_field_16(b, 0); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_dref(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "dref"); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* entry count */ + ngx_rtmp_mp4_field_32(b, 1); + + /* url size */ + ngx_rtmp_mp4_field_32(b, 0xc); + + ngx_rtmp_mp4_box(b, "url "); + + /* version and flags */ + ngx_rtmp_mp4_field_32(b, 0x00000001); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_dinf(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "dinf"); + + ngx_rtmp_mp4_write_dref(b); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_avcc(ngx_rtmp_session_t *s, ngx_buf_t *b) +{ + u_char *pos, *p; + ngx_chain_t *in; + ngx_rtmp_codec_ctx_t *codec_ctx; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (codec_ctx == NULL) { + return NGX_ERROR; + } + + in = codec_ctx->avc_header; + if (in == NULL) { + return NGX_ERROR; + } + + pos = ngx_rtmp_mp4_start_box(b, "avcC"); + + /* assume config fits one chunk (highly probable) */ + + /* + * Skip: + * - flv fmt + * - H264 CONF/PICT (0x00) + * - 0 + * - 0 + * - 0 + */ + + p = in->buf->pos + 5; + + if (p < in->buf->last) { + ngx_rtmp_mp4_data(b, p, (size_t) (in->buf->last - p)); + } else { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "dash: invalid avcc received"); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_video(ngx_rtmp_session_t *s, ngx_buf_t *b) +{ + u_char *pos; + ngx_rtmp_codec_ctx_t *codec_ctx; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + pos = ngx_rtmp_mp4_start_box(b, "avc1"); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_16(b, 0); + + /* data reference index */ + ngx_rtmp_mp4_field_16(b, 1); + + /* codec stream version & revision */ + ngx_rtmp_mp4_field_16(b, 0); + ngx_rtmp_mp4_field_16(b, 0); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + /* width & height */ + ngx_rtmp_mp4_field_16(b, (uint16_t) codec_ctx->width); + ngx_rtmp_mp4_field_16(b, (uint16_t) codec_ctx->height); + + /* horizontal & vertical resolutions 72 dpi */ + ngx_rtmp_mp4_field_32(b, 0x00480000); + ngx_rtmp_mp4_field_32(b, 0x00480000); + + /* data size */ + ngx_rtmp_mp4_field_32(b, 0); + + /* frame count */ + ngx_rtmp_mp4_field_16(b, 1); + + /* compressor name */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_16(b, 0x18); + ngx_rtmp_mp4_field_16(b, 0xffff); + + ngx_rtmp_mp4_write_avcc(s, b); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_esds(ngx_rtmp_session_t *s, ngx_buf_t *b) +{ + size_t dsi_len; + u_char *pos, *dsi; + ngx_buf_t *db; + ngx_rtmp_codec_ctx_t *codec_ctx; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (codec_ctx == NULL || codec_ctx->aac_header == NULL) { + return NGX_ERROR; + } + + db = codec_ctx->aac_header->buf; + if (db == NULL) { + return NGX_ERROR; + } + + dsi = db->pos + 2; + if (dsi > db->last) { + return NGX_ERROR; + } + + dsi_len = db->last - dsi; + + pos = ngx_rtmp_mp4_start_box(b, "esds"); + + /* version */ + ngx_rtmp_mp4_field_32(b, 0); + + + /* ES Descriptor */ + + ngx_rtmp_mp4_put_descr(b, 0x03, 23 + dsi_len); + + /* ES_ID */ + ngx_rtmp_mp4_field_16(b, 1); + + /* flags */ + ngx_rtmp_mp4_field_8(b, 0); + + + /* DecoderConfig Descriptor */ + + ngx_rtmp_mp4_put_descr(b, 0x04, 15 + dsi_len); + + /* objectTypeIndication: Audio ISO/IEC 14496-3 (AAC) */ + ngx_rtmp_mp4_field_8(b, 0x40); + + /* streamType: AudioStream */ + ngx_rtmp_mp4_field_8(b, 0x15); + + /* bufferSizeDB */ + ngx_rtmp_mp4_field_24(b, 0); + + /* maxBitrate */ + ngx_rtmp_mp4_field_32(b, 0x0001F151); + + /* avgBitrate */ + ngx_rtmp_mp4_field_32(b, 0x0001F14D); + + + /* DecoderSpecificInfo Descriptor */ + + ngx_rtmp_mp4_put_descr(b, 0x05, dsi_len); + ngx_rtmp_mp4_data(b, dsi, dsi_len); + + + /* SL Descriptor */ + + ngx_rtmp_mp4_put_descr(b, 0x06, 1); + ngx_rtmp_mp4_field_8(b, 0x02); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_audio(ngx_rtmp_session_t *s, ngx_buf_t *b) +{ + u_char *pos; + ngx_rtmp_codec_ctx_t *codec_ctx; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + pos = ngx_rtmp_mp4_start_box(b, "mp4a"); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_16(b, 0); + + /* data reference index */ + ngx_rtmp_mp4_field_16(b, 1); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + ngx_rtmp_mp4_field_32(b, 0); + + /* channel count */ + ngx_rtmp_mp4_field_16(b, (uint16_t) codec_ctx->audio_channels); + + /* sample size */ + ngx_rtmp_mp4_field_16(b, (uint16_t) (codec_ctx->sample_size * 8)); + + /* reserved */ + ngx_rtmp_mp4_field_32(b, 0); + + /* time scale */ + ngx_rtmp_mp4_field_16(b, 1000); + + /* sample rate */ + ngx_rtmp_mp4_field_16(b, (uint16_t) codec_ctx->sample_rate); + + ngx_rtmp_mp4_write_esds(s, b); +#if 0 + /* tag size*/ + ngx_rtmp_mp4_field_32(b, 8); + + /* null tag */ + ngx_rtmp_mp4_field_32(b, 0); +#endif + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stsd(ngx_rtmp_session_t *s, ngx_buf_t *b, + ngx_rtmp_mp4_track_type_t ttype) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stsd"); + + /* version & flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* entry count */ + ngx_rtmp_mp4_field_32(b, 1); + + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + ngx_rtmp_mp4_write_video(s, b); + } else { + ngx_rtmp_mp4_write_audio(s, b); + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stts(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stts"); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* entry count */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stsc(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stsc"); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* entry count */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stsz(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stsz"); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* entry count */ + ngx_rtmp_mp4_field_32(b, 0); /* moar zeros */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stco(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stco"); + + ngx_rtmp_mp4_field_32(b, 0); /* version */ + ngx_rtmp_mp4_field_32(b, 0); /* entry count */ + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_stbl(ngx_rtmp_session_t *s, ngx_buf_t *b, + ngx_rtmp_mp4_track_type_t ttype) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "stbl"); + + ngx_rtmp_mp4_write_stsd(s, b, ttype); + ngx_rtmp_mp4_write_stts(b); + ngx_rtmp_mp4_write_stsc(b); + ngx_rtmp_mp4_write_stsz(b); + ngx_rtmp_mp4_write_stco(b); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_minf(ngx_rtmp_session_t *s, ngx_buf_t *b, + ngx_rtmp_mp4_track_type_t ttype) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "minf"); + + if (ttype == NGX_RTMP_MP4_VIDEO_TRACK) { + ngx_rtmp_mp4_write_vmhd(b); + } else { + ngx_rtmp_mp4_write_smhd(b); + } + + ngx_rtmp_mp4_write_dinf(b); + ngx_rtmp_mp4_write_stbl(s, b, ttype); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mdia(ngx_rtmp_session_t *s, ngx_buf_t *b, + ngx_rtmp_mp4_track_type_t ttype) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mdia"); + + ngx_rtmp_mp4_write_mdhd(b); + ngx_rtmp_mp4_write_hdlr(b, ttype); + ngx_rtmp_mp4_write_minf(s, b, ttype); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + +static ngx_int_t +ngx_rtmp_mp4_write_trak(ngx_rtmp_session_t *s, ngx_buf_t *b, + ngx_rtmp_mp4_track_type_t ttype) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "trak"); + + ngx_rtmp_mp4_write_tkhd(s, b, ttype); + ngx_rtmp_mp4_write_mdia(s, b, ttype); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mvex(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mvex"); + + ngx_rtmp_mp4_field_32(b, 0x20); + + ngx_rtmp_mp4_box(b, "trex"); + + /* version & flags */ + ngx_rtmp_mp4_field_32(b, 0); + + /* track id */ + ngx_rtmp_mp4_field_32(b, 1); + + /* default sample description index */ + ngx_rtmp_mp4_field_32(b, 1); + + /* default sample duration */ + ngx_rtmp_mp4_field_32(b, 0); + + /* default sample size, 1024 for AAC */ + ngx_rtmp_mp4_field_32(b, 0); + + /* default sample flags, key on */ + ngx_rtmp_mp4_field_32(b, 0); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_mp4_write_moov(ngx_rtmp_session_t *s, ngx_buf_t *b, + ngx_rtmp_mp4_track_type_t ttype) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "moov"); + + ngx_rtmp_mp4_write_mvhd(b); + ngx_rtmp_mp4_write_mvex(b); + ngx_rtmp_mp4_write_trak(s, b, ttype); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_tfhd(ngx_buf_t *b) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "tfhd"); + + /* version & flags */ + ngx_rtmp_mp4_field_32(b, 0x00020000); + + /* track id */ + ngx_rtmp_mp4_field_32(b, 1); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_tfdt(ngx_buf_t *b, uint32_t earliest_pres_time) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "tfdt"); + + /* version == 1 aka 64 bit integer */ + ngx_rtmp_mp4_field_32(b, 0x00000000); + ngx_rtmp_mp4_field_32(b, earliest_pres_time); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_trun(ngx_buf_t *b, uint32_t sample_count, + ngx_rtmp_mp4_sample_t *samples, ngx_uint_t sample_mask, u_char *moof_pos) +{ + u_char *pos; + uint32_t i, offset, nitems, flags; + + pos = ngx_rtmp_mp4_start_box(b, "trun"); + + nitems = 0; + + /* data offset present */ + flags = 0x01; + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_DURATION) { + nitems++; + flags |= 0x000100; + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_SIZE) { + nitems++; + flags |= 0x000200; + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_KEY) { + nitems++; + flags |= 0x000400; + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_DELAY) { + nitems++; + flags |= 0x000800; + } + + offset = (pos - moof_pos) + 20 + (sample_count * nitems * 4) + 8; + + ngx_rtmp_mp4_field_32(b, flags); + ngx_rtmp_mp4_field_32(b, sample_count); + ngx_rtmp_mp4_field_32(b, offset); + + for (i = 0; i < sample_count; i++, samples++) { + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_DURATION) { + ngx_rtmp_mp4_field_32(b, samples->duration); + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_SIZE) { + ngx_rtmp_mp4_field_32(b, samples->size); + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_KEY) { + ngx_rtmp_mp4_field_32(b, samples->key ? 0x00000000 : 0x00010000); + } + + if (sample_mask & NGX_RTMP_MP4_SAMPLE_DELAY) { + ngx_rtmp_mp4_field_32(b, samples->delay); + } + } + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_traf(ngx_buf_t *b, uint32_t earliest_pres_time, + uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, + ngx_uint_t sample_mask, u_char *moof_pos) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "traf"); + + ngx_rtmp_mp4_write_tfhd(b); + ngx_rtmp_mp4_write_tfdt(b, earliest_pres_time); + ngx_rtmp_mp4_write_trun(b, sample_count, samples, sample_mask, moof_pos); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_write_mfhd(ngx_buf_t *b, uint32_t index) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "mfhd"); + + /* don't know what this is */ + ngx_rtmp_mp4_field_32(b, 0); + + /* fragment index. */ + ngx_rtmp_mp4_field_32(b, index); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_mp4_write_sidx(ngx_buf_t *b, ngx_uint_t reference_size, + uint32_t earliest_pres_time, uint32_t latest_pres_time) +{ + u_char *pos; + uint32_t duration; + + duration = latest_pres_time - earliest_pres_time; + + pos = ngx_rtmp_mp4_start_box(b, "sidx"); + + /* version */ + ngx_rtmp_mp4_field_32(b, 0); + + /* reference id */ + ngx_rtmp_mp4_field_32(b, 1); + + /* timescale */ + ngx_rtmp_mp4_field_32(b, 1000); + + /* earliest presentation time */ + ngx_rtmp_mp4_field_32(b, earliest_pres_time); + + /* first offset */ + ngx_rtmp_mp4_field_32(b, duration); /*TODO*/ + + /* reserved */ + ngx_rtmp_mp4_field_16(b, 0); + + /* reference count = 1 */ + ngx_rtmp_mp4_field_16(b, 1); + + /* 1st bit is reference type, the rest is reference size */ + ngx_rtmp_mp4_field_32(b, reference_size); + + /* subsegment duration */ + ngx_rtmp_mp4_field_32(b, duration); + + /* first bit is startsWithSAP (=1), next 3 bits are SAP type (=001) */ + ngx_rtmp_mp4_field_8(b, 0x90); + + /* SAP delta time */ + ngx_rtmp_mp4_field_24(b, 0); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_mp4_write_moof(ngx_buf_t *b, uint32_t earliest_pres_time, + uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, + ngx_uint_t sample_mask, uint32_t index) +{ + u_char *pos; + + pos = ngx_rtmp_mp4_start_box(b, "moof"); + + ngx_rtmp_mp4_write_mfhd(b, index); + ngx_rtmp_mp4_write_traf(b, earliest_pres_time, sample_count, samples, + sample_mask, pos); + + ngx_rtmp_mp4_update_box_size(b, pos); + + return NGX_OK; +} + + +ngx_uint_t +ngx_rtmp_mp4_write_mdat(ngx_buf_t *b, ngx_uint_t size) +{ + ngx_rtmp_mp4_field_32(b, size); + + ngx_rtmp_mp4_box(b, "mdat"); + + return NGX_OK; +} diff --git a/ngx_http_flv_module/dash/ngx_rtmp_mp4.h b/ngx_http_flv_module/dash/ngx_rtmp_mp4.h new file mode 100644 index 0000000..697b6c8 --- /dev/null +++ b/ngx_http_flv_module/dash/ngx_rtmp_mp4.h @@ -0,0 +1,52 @@ + + +#ifndef _NGX_RTMP_MP4_H_INCLUDED_ +#define _NGX_RTMP_MP4_H_INCLUDED_ + + +#include +#include +#include + + +#define NGX_RTMP_MP4_SAMPLE_SIZE 0x01 +#define NGX_RTMP_MP4_SAMPLE_DURATION 0x02 +#define NGX_RTMP_MP4_SAMPLE_DELAY 0x04 +#define NGX_RTMP_MP4_SAMPLE_KEY 0x08 + + +typedef struct { + uint32_t size; + uint32_t duration; + uint32_t delay; + uint32_t timestamp; + unsigned key:1; +} ngx_rtmp_mp4_sample_t; + + +typedef enum { + NGX_RTMP_MP4_FILETYPE_INIT, + NGX_RTMP_MP4_FILETYPE_SEG +} ngx_rtmp_mp4_file_type_t; + + +typedef enum { + NGX_RTMP_MP4_VIDEO_TRACK, + NGX_RTMP_MP4_AUDIO_TRACK +} ngx_rtmp_mp4_track_type_t; + + +ngx_int_t ngx_rtmp_mp4_write_ftyp(ngx_buf_t *b); +ngx_int_t ngx_rtmp_mp4_write_styp(ngx_buf_t *b); +ngx_int_t ngx_rtmp_mp4_write_moov(ngx_rtmp_session_t *s, ngx_buf_t *b, + ngx_rtmp_mp4_track_type_t ttype); +ngx_int_t ngx_rtmp_mp4_write_moof(ngx_buf_t *b, uint32_t earliest_pres_time, + uint32_t sample_count, ngx_rtmp_mp4_sample_t *samples, + ngx_uint_t sample_mask, uint32_t index); +ngx_int_t ngx_rtmp_mp4_write_sidx(ngx_buf_t *b, + ngx_uint_t reference_size, uint32_t earliest_pres_time, + uint32_t latest_pres_time); +ngx_uint_t ngx_rtmp_mp4_write_mdat(ngx_buf_t *b, ngx_uint_t size); + + +#endif /* _NGX_RTMP_MP4_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/doc/README.md b/ngx_http_flv_module/doc/README.md new file mode 100644 index 0000000..407efa7 --- /dev/null +++ b/ngx_http_flv_module/doc/README.md @@ -0,0 +1,2 @@ +Documentation is available here: +https://github.com/arut/nginx-rtmp-module/wiki diff --git a/ngx_http_flv_module/hls/ngx_rtmp_hls_module.c b/ngx_http_flv_module/hls/ngx_rtmp_hls_module.c new file mode 100644 index 0000000..84cded2 --- /dev/null +++ b/ngx_http_flv_module/hls/ngx_rtmp_hls_module.c @@ -0,0 +1,2591 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include +#include +#include +#include "ngx_rtmp_mpegts.h" + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_close_stream_pt next_close_stream; +static ngx_rtmp_stream_begin_pt next_stream_begin; +static ngx_rtmp_stream_eof_pt next_stream_eof; + + +static char *ngx_rtmp_hls_variant(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static ngx_int_t ngx_rtmp_hls_postconfiguration(ngx_conf_t *cf); +static void *ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf); +static char *ngx_rtmp_hls_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +static ngx_int_t ngx_rtmp_hls_flush_audio(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_hls_ensure_directory(ngx_rtmp_session_t *s, + ngx_str_t *path); +static char *ngx_rtmp_hls_set_permissions(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); + + +#define NGX_RTMP_HLS_BUFSIZE (1024*1024) +#define NGX_RTMP_HLS_DEFAULT_DIR_ACCESS 0744 + + +typedef struct { + uint64_t id; + uint64_t key_id; + double duration; + unsigned active:1; + unsigned discont:1; /* before */ +} ngx_rtmp_hls_frag_t; + + +typedef struct { + ngx_str_t suffix; + ngx_array_t args; +} ngx_rtmp_hls_variant_t; + + +typedef struct { + unsigned opened:1; + + ngx_rtmp_mpegts_file_t file; + + ngx_str_t playlist; + ngx_str_t playlist_bak; + ngx_str_t var_playlist; + ngx_str_t var_playlist_bak; + ngx_str_t stream; + ngx_str_t keyfile; + ngx_str_t name; + u_char key[16]; + + uint64_t frag; + uint64_t frag_ts; + uint64_t key_id; + ngx_uint_t nfrags; + ngx_rtmp_hls_frag_t *frags; /* circular 2 * winfrags + 1 */ + + ngx_uint_t audio_cc; + ngx_uint_t video_cc; + ngx_uint_t key_frags; + + uint64_t aframe_base; + uint64_t aframe_num; + + ngx_buf_t *aframe; + uint64_t aframe_pts; + + ngx_rtmp_hls_variant_t *var; +} ngx_rtmp_hls_ctx_t; + + +typedef struct { + ngx_str_t path; + ngx_msec_t playlen; + ngx_uint_t frags_per_key; +} ngx_rtmp_hls_cleanup_t; + + +typedef struct { + ngx_flag_t hls; + ngx_msec_t fraglen; + ngx_msec_t max_fraglen; + ngx_msec_t muxdelay; + ngx_msec_t sync; + ngx_msec_t playlen; + ngx_uint_t winfrags; + ngx_flag_t continuous; + ngx_flag_t nested; + ngx_str_t path; + ngx_uint_t naming; + ngx_uint_t slicing; + ngx_uint_t type; + ngx_path_t *slot; + ngx_msec_t max_audio_delay; + size_t audio_buffer_size; + ngx_flag_t cleanup; + ngx_array_t *variant; + ngx_str_t base_url; + ngx_int_t granularity; + ngx_flag_t keys; + ngx_str_t key_path; + ngx_str_t key_url; + ngx_uint_t frags_per_key; + ngx_uint_t dir_access; + ngx_str_t nested_index_filename; +} ngx_rtmp_hls_app_conf_t; + + +#define NGX_RTMP_HLS_NAMING_SEQUENTIAL 1 +#define NGX_RTMP_HLS_NAMING_TIMESTAMP 2 +#define NGX_RTMP_HLS_NAMING_SYSTEM 3 + + +#define NGX_RTMP_HLS_SLICING_PLAIN 1 +#define NGX_RTMP_HLS_SLICING_ALIGNED 2 + + +#define NGX_RTMP_HLS_TYPE_LIVE 1 +#define NGX_RTMP_HLS_TYPE_EVENT 2 + + +static ngx_conf_enum_t ngx_rtmp_hls_naming_slots[] = { + { ngx_string("sequential"), NGX_RTMP_HLS_NAMING_SEQUENTIAL }, + { ngx_string("timestamp"), NGX_RTMP_HLS_NAMING_TIMESTAMP }, + { ngx_string("system"), NGX_RTMP_HLS_NAMING_SYSTEM }, + { ngx_null_string, 0 } +}; + + +static ngx_conf_enum_t ngx_rtmp_hls_slicing_slots[] = { + { ngx_string("plain"), NGX_RTMP_HLS_SLICING_PLAIN }, + { ngx_string("aligned"), NGX_RTMP_HLS_SLICING_ALIGNED }, + { ngx_null_string, 0 } +}; + + +static ngx_conf_enum_t ngx_rtmp_hls_type_slots[] = { + { ngx_string("live"), NGX_RTMP_HLS_TYPE_LIVE }, + { ngx_string("event"), NGX_RTMP_HLS_TYPE_EVENT }, + { ngx_null_string, 0 } +}; + + +static ngx_command_t ngx_rtmp_hls_commands[] = { + + { ngx_string("hls"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, hls), + NULL }, + + { ngx_string("hls_fragment"), + 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, fraglen), + NULL }, + + { ngx_string("hls_max_fragment"), + 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, max_fraglen), + NULL }, + + { ngx_string("hls_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_hls_app_conf_t, path), + NULL }, + + { ngx_string("hls_playlist_length"), + 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, playlen), + NULL }, + + { ngx_string("hls_muxdelay"), + 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, 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_string("hls_continuous"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, continuous), + NULL }, + + { ngx_string("hls_nested"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, nested), + NULL }, + + { ngx_string("hls_fragment_naming"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, naming), + &ngx_rtmp_hls_naming_slots }, + + { ngx_string("hls_fragment_slicing"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, slicing), + &ngx_rtmp_hls_slicing_slots }, + + { ngx_string("hls_type"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, type), + &ngx_rtmp_hls_type_slots }, + + { ngx_string("hls_max_audio_delay"), + 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, max_audio_delay), + NULL }, + + { ngx_string("hls_audio_buffer_size"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, audio_buffer_size), + NULL }, + + { ngx_string("hls_cleanup"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, cleanup), + NULL }, + + { ngx_string("hls_variant"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_hls_variant, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("hls_base_url"), + 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_hls_app_conf_t, base_url), + NULL }, + + { ngx_string("hls_fragment_naming_granularity"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, granularity), + NULL }, + + { ngx_string("hls_keys"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, keys), + NULL }, + + { ngx_string("hls_key_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_hls_app_conf_t, key_path), + NULL }, + + { ngx_string("hls_key_url"), + 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_hls_app_conf_t, key_url), + NULL }, + + { ngx_string("hls_fragments_per_key"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, frags_per_key), + NULL }, + + { ngx_string("hls_dir_access"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_hls_set_permissions, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_hls_app_conf_t, dir_access), + NULL }, + + { ngx_string("hls_nested_index_filename"), + 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_hls_app_conf_t, nested_index_filename), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_hls_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_hls_postconfiguration, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_rtmp_hls_create_app_conf, /* create location configuration */ + ngx_rtmp_hls_merge_app_conf, /* merge location configuration */ +}; + + +ngx_module_t ngx_rtmp_hls_module = { + NGX_MODULE_V1, + &ngx_rtmp_hls_module_ctx, /* module context */ + ngx_rtmp_hls_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static ngx_rtmp_hls_frag_t * +ngx_rtmp_hls_get_frag(ngx_rtmp_session_t *s, ngx_int_t n) +{ + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_hls_app_conf_t *hacf; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + return &ctx->frags[(ctx->frag + n) % (hacf->winfrags * 2 + 1)]; +} + + +static void +ngx_rtmp_hls_next_frag(ngx_rtmp_session_t *s) +{ + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_hls_app_conf_t *hacf; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + if (ctx->nfrags == hacf->winfrags) { + ctx->frag++; + } else { + ctx->nfrags++; + } +} + + +static ngx_int_t +ngx_rtmp_hls_rename_file(u_char *src, u_char *dst) +{ + /* rename file with overwrite */ + +#if (NGX_WIN32) + return MoveFileEx((LPCTSTR) src, (LPCTSTR) dst, MOVEFILE_REPLACE_EXISTING); +#else + return ngx_rename_file(src, dst); +#endif +} + + +static ngx_int_t +ngx_rtmp_hls_write_variant_playlist(ngx_rtmp_session_t *s) +{ + static u_char buffer[1024]; + + u_char *p, *last; + ssize_t rc; + ngx_fd_t fd; + ngx_str_t *arg; + ngx_uint_t n, k; + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_hls_variant_t *var; + ngx_rtmp_hls_app_conf_t *hacf; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + fd = ngx_open_file(ctx->var_playlist_bak.data, NGX_FILE_WRONLY, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_open_file_n " failed: '%V'", + &ctx->var_playlist_bak); + + return NGX_ERROR; + } + +#define NGX_RTMP_HLS_VAR_HEADER "#EXTM3U\n#EXT-X-VERSION:3\n" + + rc = ngx_write_fd(fd, NGX_RTMP_HLS_VAR_HEADER, + sizeof(NGX_RTMP_HLS_VAR_HEADER) - 1); + if (rc < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_write_fd_n " failed: '%V'", + &ctx->var_playlist_bak); + ngx_close_file(fd); + return NGX_ERROR; + } + + var = hacf->variant->elts; + for (n = 0; n < hacf->variant->nelts; n++, var++) + { + p = buffer; + last = buffer + sizeof(buffer); + + /* TODO: PROGRAM-ID was removed in protocol version 6 */ + p = ngx_slprintf(p, last, "#EXT-X-STREAM-INF:PROGRAM-ID=1"); + + arg = var->args.elts; + for (k = 0; k < var->args.nelts; k++, arg++) { + p = ngx_slprintf(p, last, ",%V", arg); + } + + if (p < last) { + *p++ = '\n'; + } + + p = ngx_slprintf(p, last, "%V%*s%V", + &hacf->base_url, + ctx->name.len - ctx->var->suffix.len, ctx->name.data, + &var->suffix); + if (hacf->nested) { + p = ngx_slprintf(p, last, "/%V", &hacf->nested_index_filename); + } + + p = ngx_slprintf(p, last, "%s", ".m3u8\n"); + + rc = ngx_write_fd(fd, buffer, p - buffer); + if (rc < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_write_fd_n " failed '%V'", + &ctx->var_playlist_bak); + ngx_close_file(fd); + return NGX_ERROR; + } + } + + ngx_close_file(fd); + + if (ngx_rtmp_hls_rename_file(ctx->var_playlist_bak.data, + ctx->var_playlist.data) + == NGX_FILE_ERROR) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: rename failed: '%V'->'%V'", + &ctx->var_playlist_bak, &ctx->var_playlist); + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_hls_write_playlist(ngx_rtmp_session_t *s) +{ + static u_char buffer[1024]; + ngx_fd_t fd; + u_char *p, *end; + ngx_rtmp_hls_ctx_t *ctx; + ssize_t n; + ngx_rtmp_hls_app_conf_t *hacf; + ngx_rtmp_hls_frag_t *f; + ngx_uint_t i, max_frag; + ngx_str_t name_part, key_name_part; + uint64_t prev_key_id; + const char *sep, *key_sep; + + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + fd = ngx_open_file(ctx->playlist_bak.data, NGX_FILE_WRONLY, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_open_file_n " failed: '%V'", + &ctx->playlist_bak); + return NGX_ERROR; + } + + max_frag = hacf->fraglen / 1000; + + for (i = 0; i < ctx->nfrags; i++) { + f = ngx_rtmp_hls_get_frag(s, i); + if (f->duration > max_frag) { + max_frag = (ngx_uint_t) (f->duration + .5); + } + } + + p = buffer; + end = p + sizeof(buffer); + + p = ngx_slprintf(p, end, + "#EXTM3U\n" + "#EXT-X-VERSION:3\n" + "#EXT-X-MEDIA-SEQUENCE:%uL\n" + "#EXT-X-TARGETDURATION:%ui\n", + ctx->frag, max_frag); + + if (hacf->type == NGX_RTMP_HLS_TYPE_EVENT) { + p = ngx_slprintf(p, end, "#EXT-X-PLAYLIST-TYPE: EVENT\n"); + } + + n = ngx_write_fd(fd, buffer, p - buffer); + if (n < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_write_fd_n " failed: '%V'", + &ctx->playlist_bak); + ngx_close_file(fd); + return NGX_ERROR; + } + + sep = hacf->nested ? (hacf->base_url.len ? "/" : "") : "-"; + key_sep = hacf->nested ? (hacf->key_url.len ? "/" : "") : "-"; + + name_part.len = 0; + if (!hacf->nested || hacf->base_url.len) { + name_part = ctx->name; + } + + key_name_part.len = 0; + if (!hacf->nested || hacf->key_url.len) { + key_name_part = ctx->name; + } + + prev_key_id = 0; + + for (i = 0; i < ctx->nfrags; i++) { + f = ngx_rtmp_hls_get_frag(s, i); + + p = buffer; + end = p + sizeof(buffer); + + if (f->discont) { + p = ngx_slprintf(p, end, "#EXT-X-DISCONTINUITY\n"); + } + + if (hacf->keys && (i == 0 || f->key_id != prev_key_id)) { + p = ngx_slprintf(p, end, "#EXT-X-KEY:METHOD=AES-128," + "URI=\"%V%V%s%uL.key\",IV=0x%032XL\n", + &hacf->key_url, &key_name_part, + key_sep, f->key_id, f->key_id); + } + + prev_key_id = f->key_id; + + p = ngx_slprintf(p, end, + "#EXTINF:%.3f,\n" + "%V%V%s%uL.ts\n", + f->duration, &hacf->base_url, &name_part, sep, f->id); + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: fragment frag=%uL, n=%ui/%ui, duration=%.3f, " + "discont=%i", + ctx->frag, i + 1, ctx->nfrags, f->duration, f->discont); + + n = ngx_write_fd(fd, buffer, p - buffer); + if (n < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_write_fd_n " failed '%V'", + &ctx->playlist_bak); + ngx_close_file(fd); + return NGX_ERROR; + } + } + + ngx_close_file(fd); + + if (ngx_rtmp_hls_rename_file(ctx->playlist_bak.data, ctx->playlist.data) + == NGX_FILE_ERROR) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: rename failed: '%V'->'%V'", + &ctx->playlist_bak, &ctx->playlist); + return NGX_ERROR; + } + + if (ctx->var) { + return ngx_rtmp_hls_write_variant_playlist(s); + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_hls_copy(ngx_rtmp_session_t *s, void *dst, u_char **src, size_t n, + ngx_chain_t **in) +{ + u_char *last; + size_t pn; + + if (*in == NULL) { + return NGX_ERROR; + } + + for ( ;; ) { + last = (*in)->buf->last; + + if ((size_t)(last - *src) >= n) { + if (dst) { + ngx_memcpy(dst, *src, n); + } + + *src += n; + + while (*in && *src == (*in)->buf->last) { + *in = (*in)->next; + if (*in) { + *src = (*in)->buf->pos; + } + } + + return NGX_OK; + } + + pn = last - *src; + + if (dst) { + ngx_memcpy(dst, *src, pn); + dst = (u_char *)dst + pn; + } + + n -= pn; + *in = (*in)->next; + + if (*in == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: failed to read %uz byte(s)", n); + return NGX_ERROR; + } + + *src = (*in)->buf->pos; + } +} + + +static ngx_int_t +ngx_rtmp_hls_append_aud(ngx_rtmp_session_t *s, ngx_buf_t *out) +{ + static u_char aud_nal[] = { 0x00, 0x00, 0x00, 0x01, 0x09, 0xf0 }; + + if (out->last + sizeof(aud_nal) > out->end) { + return NGX_ERROR; + } + + out->last = ngx_cpymem(out->last, aud_nal, sizeof(aud_nal)); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_hls_append_sps_pps(ngx_rtmp_session_t *s, ngx_buf_t *out) +{ + ngx_rtmp_codec_ctx_t *codec_ctx; + u_char *p; + ngx_chain_t *in; + ngx_rtmp_hls_ctx_t *ctx; + int8_t nnals; + uint16_t len, rlen; + ngx_int_t n; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (ctx == NULL || codec_ctx == NULL) { + return NGX_ERROR; + } + + in = codec_ctx->avc_header; + if (in == NULL) { + return NGX_ERROR; + } + + p = in->buf->pos; + + /* + * Skip bytes: + * - flv fmt + * - H264 CONF/PICT (0x00) + * - 0 + * - 0 + * - 0 + * - version + * - profile + * - compatibility + * - level + * - nal bytes + */ + + if (ngx_rtmp_hls_copy(s, NULL, &p, 10, &in) != NGX_OK) { + return NGX_ERROR; + } + + /* number of SPS NALs */ + if (ngx_rtmp_hls_copy(s, &nnals, &p, 1, &in) != NGX_OK) { + return NGX_ERROR; + } + + nnals &= 0x1f; /* 5lsb */ + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: SPS number: %uz", nnals); + + /* SPS */ + for (n = 0; ; ++n) { + for (; nnals; --nnals) { + + /* NAL length */ + if (ngx_rtmp_hls_copy(s, &rlen, &p, 2, &in) != NGX_OK) { + return NGX_ERROR; + } + + len = ntohs(rlen); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: header NAL length: %uz", (size_t) len); + + /* AnnexB prefix */ + if (out->end - out->last < 4) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: too small buffer for header NAL size"); + return NGX_ERROR; + } + + *out->last++ = 0; + *out->last++ = 0; + *out->last++ = 0; + *out->last++ = 1; + + /* NAL body */ + if (out->end - out->last < len) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: too small buffer for header NAL"); + return NGX_ERROR; + } + + if (ngx_rtmp_hls_copy(s, out->last, &p, len, &in) != NGX_OK) { + return NGX_ERROR; + } + + out->last += len; + } + + if (n == 1) { + break; + } + + /* number of PPS NALs */ + if (ngx_rtmp_hls_copy(s, &nnals, &p, 1, &in) != NGX_OK) { + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: PPS number: %uz", nnals); + } + + return NGX_OK; +} + + +static uint64_t +ngx_rtmp_hls_get_fragment_id(ngx_rtmp_session_t *s, uint64_t ts) +{ + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_hls_app_conf_t *hacf; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + + switch (hacf->naming) { + + case NGX_RTMP_HLS_NAMING_TIMESTAMP: + return ts; + + case NGX_RTMP_HLS_NAMING_SYSTEM: + return (uint64_t) ngx_cached_time->sec * 1000 + ngx_cached_time->msec; + + default: /* NGX_RTMP_HLS_NAMING_SEQUENTIAL */ + return ctx->frag + ctx->nfrags; + } +} + + +static ngx_int_t +ngx_rtmp_hls_close_fragment(ngx_rtmp_session_t *s) +{ + ngx_rtmp_hls_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + if (ctx == NULL || !ctx->opened) { + return NGX_OK; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: close fragment n=%uL", ctx->frag); + + ngx_rtmp_mpegts_close_file(&ctx->file); + + ctx->opened = 0; + + ngx_rtmp_hls_next_frag(s); + + ngx_rtmp_hls_write_playlist(s); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_hls_open_fragment(ngx_rtmp_session_t *s, uint64_t ts, + ngx_int_t discont) +{ + uint64_t id; + ngx_fd_t fd; + ngx_uint_t g; + ngx_uint_t counter; + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_hls_frag_t *f; + ngx_rtmp_hls_app_conf_t *hacf; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + if (ctx->opened) { + return NGX_OK; + } + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + + if (ngx_rtmp_hls_ensure_directory(s, &hacf->path) != NGX_OK) { + return NGX_ERROR; + } + + if (hacf->keys && + ngx_rtmp_hls_ensure_directory(s, &hacf->key_path) != NGX_OK) + { + return NGX_ERROR; + } + + id = ngx_rtmp_hls_get_fragment_id(s, ts); + + if (hacf->granularity) { + g = (ngx_uint_t) hacf->granularity; + id = (uint64_t) (id / g) * g; + } + + ngx_sprintf(ctx->stream.data + ctx->stream.len, "%uL.ts%Z", id); + + if (hacf->keys) { + if (ctx->key_frags == 0) { + + ctx->key_frags = hacf->frags_per_key - 1; + ctx->key_id = id; + + if (RAND_bytes(ctx->key, 16) < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: failed to create key"); + return NGX_ERROR; + } + + ngx_sprintf(ctx->keyfile.data + ctx->keyfile.len, "%uL.key%Z", id); + + fd = ngx_open_file(ctx->keyfile.data, NGX_FILE_WRONLY, + NGX_FILE_TRUNCATE, NGX_FILE_DEFAULT_ACCESS); + + if (fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: failed to open key file '%s'", + ctx->keyfile.data); + return NGX_ERROR; + } + + if (ngx_write_fd(fd, ctx->key, 16) != 16) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: failed to write key file '%s'", + ctx->keyfile.data); + ngx_close_file(fd); + return NGX_ERROR; + } + + ngx_close_file(fd); + + } else { + if (hacf->frags_per_key) { + ctx->key_frags--; + } + + if (ngx_set_file_time(ctx->keyfile.data, 0, ngx_cached_time->sec) + != NGX_OK) + { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, ngx_errno, + ngx_set_file_time_n " '%s' failed", + ctx->keyfile.data); + } + } + } + + counter = (ngx_uint_t) (ctx->nfrags + ctx->frag); + + ngx_log_debug7(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: open fragment file='%s', keyfile='%s', " + "frag=%uL, n=%ui, time=%uL, discont=%i, cc=%u", + ctx->stream.data, + ctx->keyfile.data ? ctx->keyfile.data : (u_char *) "", + ctx->frag, ctx->nfrags, ts, discont, counter); + + if (hacf->keys && + ngx_rtmp_mpegts_init_encryption(&ctx->file, ctx->key, 16, ctx->key_id) + != NGX_OK) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: failed to initialize hls encryption"); + return NGX_ERROR; + } + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (ngx_rtmp_mpegts_open_file(&ctx->file, ctx->stream.data, codec_ctx, + counter, s->connection->log) + != NGX_OK) + { + return NGX_ERROR; + } + + ctx->opened = 1; + + f = ngx_rtmp_hls_get_frag(s, ctx->nfrags); + + ngx_memzero(f, sizeof(*f)); + + f->active = 1; + f->discont = discont; + f->id = id; + f->key_id = ctx->key_id; + + ctx->frag_ts = ts; + + /* start fragment with audio to make iPhone happy */ + + ngx_rtmp_hls_flush_audio(s); + + return NGX_OK; +} + + +static void +ngx_rtmp_hls_restore_stream(ngx_rtmp_session_t *s) +{ + ngx_rtmp_hls_ctx_t *ctx; + ngx_file_t file; + ssize_t ret; + off_t offset; + u_char *p, *last, *end, *next, *pa, *pp, c; + ngx_rtmp_hls_frag_t *f; + double duration; + ngx_int_t discont; + uint64_t mag, key_id, base; + static u_char buffer[4096]; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + ngx_memzero(&file, sizeof(file)); + + file.log = s->connection->log; + + ngx_str_set(&file.name, "m3u8"); + + file.fd = ngx_open_file(ctx->playlist.data, NGX_FILE_RDONLY, NGX_FILE_OPEN, + 0); + if (file.fd == NGX_INVALID_FILE) { + return; + } + + offset = 0; + ctx->nfrags = 0; + f = NULL; + duration = 0; + discont = 0; + key_id = 0; + + for ( ;; ) { + + ret = ngx_read_file(&file, buffer, sizeof(buffer), offset); + if (ret <= 0) { + goto done; + } + + p = buffer; + end = buffer + ret; + + for ( ;; ) { + last = ngx_strlchr(p, end, '\n'); + + if (last == NULL) { + if (p == buffer) { + goto done; + } + break; + } + + next = last + 1; + offset += (next - p); + + if (p != last && last[-1] == '\r') { + last--; + } + + +#define NGX_RTMP_MSEQ "#EXT-X-MEDIA-SEQUENCE:" +#define NGX_RTMP_MSEQ_LEN (sizeof(NGX_RTMP_MSEQ) - 1) + + + if (ngx_memcmp(p, NGX_RTMP_MSEQ, NGX_RTMP_MSEQ_LEN) == 0) { + + ctx->frag = (uint64_t) strtod((const char *) + &p[NGX_RTMP_MSEQ_LEN], NULL); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: restore sequence frag=%uL", ctx->frag); + } + + +#define NGX_RTMP_XKEY "#EXT-X-KEY:" +#define NGX_RTMP_XKEY_LEN (sizeof(NGX_RTMP_XKEY) - 1) + + if (ngx_memcmp(p, NGX_RTMP_XKEY, NGX_RTMP_XKEY_LEN) == 0) { + + /* recover key id from initialization vector */ + + key_id = 0; + base = 1; + pp = last - 1; + + for ( ;; ) { + if (pp < p) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: failed to read key id"); + break; + } + + c = *pp; + if (c == 'x') { + break; + } + + if (c >= '0' && c <= '9') { + c -= '0'; + goto next; + } + + c |= 0x20; + + if (c >= 'a' && c <= 'f') { + c -= 'a' - 10; + goto next; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: bad character in key id"); + break; + + next: + + key_id += base * c; + base *= 0x10; + pp--; + } + } + + +#define NGX_RTMP_EXTINF "#EXTINF:" +#define NGX_RTMP_EXTINF_LEN (sizeof(NGX_RTMP_EXTINF) - 1) + + + if (ngx_memcmp(p, NGX_RTMP_EXTINF, NGX_RTMP_EXTINF_LEN) == 0) { + + duration = strtod((const char *) &p[NGX_RTMP_EXTINF_LEN], NULL); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: restore durarion=%.3f", duration); + } + + +#define NGX_RTMP_DISCONT "#EXT-X-DISCONTINUITY" +#define NGX_RTMP_DISCONT_LEN (sizeof(NGX_RTMP_DISCONT) - 1) + + + if (ngx_memcmp(p, NGX_RTMP_DISCONT, NGX_RTMP_DISCONT_LEN) == 0) { + + discont = 1; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: discontinuity"); + } + + /* find '.ts\r' */ + + if (p + 4 <= last && + last[-3] == '.' && last[-2] == 't' && last[-1] == 's') + { + f = ngx_rtmp_hls_get_frag(s, ctx->nfrags); + + ngx_memzero(f, sizeof(*f)); + + f->duration = duration; + f->discont = discont; + f->active = 1; + f->id = 0; + + discont = 0; + + mag = 1; + for (pa = last - 4; pa >= p; pa--) { + if (*pa < '0' || *pa > '9') { + break; + } + f->id += (*pa - '0') * mag; + mag *= 10; + } + + f->key_id = key_id; + + ngx_rtmp_hls_next_frag(s); + + ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: restore fragment '%*s' id=%uL, " + "duration=%.3f, frag=%uL, nfrags=%ui", + (size_t) (last - p), p, f->id, f->duration, + ctx->frag, ctx->nfrags); + } + + p = next; + } + } + +done: + ngx_close_file(file.fd); +} + + +static ngx_int_t +ngx_rtmp_hls_ensure_directory(ngx_rtmp_session_t *s, ngx_str_t *path) +{ + size_t len; + ngx_file_info_t fi; + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_hls_app_conf_t *hacf; + + static u_char zpath[NGX_MAX_PATH + 1]; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + + if (path->len + 1 > sizeof(zpath)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: too long path"); + return NGX_ERROR; + } + + ngx_snprintf(zpath, sizeof(zpath), "%V%Z", path); + + if (ngx_file_info(zpath, &fi) == NGX_FILE_ERROR) { + + if (ngx_errno != NGX_ENOENT) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_file_info_n " failed on '%V'", path); + return NGX_ERROR; + } + + /* ENOENT */ + + if (ngx_create_dir(zpath, hacf->dir_access) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_create_dir_n " failed on '%V'", path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: directory '%V' created", path); + + } else { + + if (!ngx_is_dir(&fi)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: '%V' exists and is not a directory", path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: directory '%V' exists", path); + } + + if (!hacf->nested) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + len = path->len; + if (path->data[len - 1] == '/') { + len--; + } + + if (len + 1 + ctx->name.len + 1 > sizeof(zpath)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: too long path"); + return NGX_ERROR; + } + + ngx_snprintf(zpath, sizeof(zpath) - 1, "%*s/%V%Z", len, path->data, + &ctx->name); + + if (ngx_file_info(zpath, &fi) != NGX_FILE_ERROR) { + + if (ngx_is_dir(&fi)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: directory '%s' exists", zpath); + return NGX_OK; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: '%s' exists and is not a directory", zpath); + + return NGX_ERROR; + } + + if (ngx_errno != NGX_ENOENT) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_file_info_n " failed on '%s'", zpath); + return NGX_ERROR; + } + + /* NGX_ENOENT */ + + if (ngx_create_dir(zpath, hacf->dir_access) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "hls: " ngx_create_dir_n " failed on '%s'", zpath); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: directory '%s' created", zpath); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_hls_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_hls_app_conf_t *hacf; + ngx_rtmp_hls_ctx_t *ctx; + u_char *p, *pp; + ngx_rtmp_hls_frag_t *f; + ngx_buf_t *b; + size_t len; + ngx_rtmp_hls_variant_t *var; + ngx_uint_t n; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + if (hacf == NULL || !hacf->hls || hacf->path.len == 0) { + goto next; + } + + if (s->auto_pushed) { + goto next; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: publish: name='%s' type='%s'", + v->name, v->type); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_hls_ctx_t)); + if (ctx == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: failed to allocate for publish ctx"); + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_hls_module); + } else { + + f = ctx->frags; + b = ctx->aframe; + + ngx_memzero(ctx, sizeof(ngx_rtmp_hls_ctx_t)); + + ctx->frags = f; + ctx->aframe = b; + + if (b) { + b->pos = b->last = b->start; + } + } + + if (ctx->frags == NULL) { + ctx->frags = ngx_pcalloc(s->connection->pool, + sizeof(ngx_rtmp_hls_frag_t) * + (hacf->winfrags * 2 + 1)); + if (ctx->frags == NULL) { + return NGX_ERROR; + } + } + + if (ngx_strstr(v->name, "..")) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: bad stream name: '%s'", v->name); + return NGX_ERROR; + } + + ctx->name.len = ngx_strlen(v->name); + ctx->name.data = ngx_palloc(s->connection->pool, ctx->name.len + 1); + + if (ctx->name.data == NULL) { + return NGX_ERROR; + } + + *ngx_cpymem(ctx->name.data, v->name, ctx->name.len) = 0; + + len = hacf->path.len + 1 + ctx->name.len + sizeof(".m3u8"); + if (hacf->nested) { + len += hacf->nested_index_filename.len + 1; + } + + ctx->playlist.data = ngx_palloc(s->connection->pool, len); + if (ctx->playlist.data == NULL) { + return NGX_ERROR; + } + + p = ngx_cpymem(ctx->playlist.data, hacf->path.data, hacf->path.len); + + if (p[-1] != '/') { + *p++ = '/'; + } + + p = ngx_cpymem(p, ctx->name.data, ctx->name.len); + + /* + * ctx->stream holds initial part of stream file path + * however the space for the whole stream path + * is allocated + */ + + ctx->stream.len = p - ctx->playlist.data + 1; + ctx->stream.data = ngx_palloc(s->connection->pool, + ctx->stream.len + NGX_INT64_LEN + + sizeof(".ts")); + if (ctx->stream.data == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(ctx->stream.data, ctx->playlist.data, ctx->stream.len - 1); + ctx->stream.data[ctx->stream.len - 1] = (hacf->nested ? '/' : '-'); + + /* variant playlist path */ + + if (hacf->variant) { + var = hacf->variant->elts; + for (n = 0; n < hacf->variant->nelts; n++, var++) { + if (ctx->name.len > var->suffix.len && + ngx_memcmp(var->suffix.data, + ctx->name.data + ctx->name.len - var->suffix.len, + var->suffix.len) + == 0) + { + ctx->var = var; + + len = (size_t) (p - ctx->playlist.data); + + ctx->var_playlist.len = len - var->suffix.len + sizeof(".m3u8") + - 1; + ctx->var_playlist.data = ngx_palloc(s->connection->pool, + ctx->var_playlist.len + 1); + if (ctx->var_playlist.data == NULL) { + return NGX_ERROR; + } + + pp = ngx_cpymem(ctx->var_playlist.data, ctx->playlist.data, + len - var->suffix.len); + pp = ngx_cpymem(pp, ".m3u8", sizeof(".m3u8") - 1); + *pp = 0; + + ctx->var_playlist_bak.len = ctx->var_playlist.len + + sizeof(".bak") - 1; + ctx->var_playlist_bak.data = ngx_palloc(s->connection->pool, + ctx->var_playlist_bak.len + 1); + if (ctx->var_playlist_bak.data == NULL) { + return NGX_ERROR; + } + + pp = ngx_cpymem(ctx->var_playlist_bak.data, + ctx->var_playlist.data, + ctx->var_playlist.len); + pp = ngx_cpymem(pp, ".bak", sizeof(".bak") - 1); + *pp = 0; + + break; + } + } + } + + + /* playlist path */ + + if (hacf->nested) { + p = ngx_cpymem(p, "/", sizeof("/") - 1); + p = ngx_cpymem(p, hacf->nested_index_filename.data, + hacf->nested_index_filename.len); + } + + p = ngx_cpymem(p, ".m3u8", sizeof(".m3u8") - 1); + + ctx->playlist.len = p - ctx->playlist.data; + + *p = 0; + + /* playlist bak (new playlist) path */ + + ctx->playlist_bak.data = ngx_palloc(s->connection->pool, + ctx->playlist.len + sizeof(".bak")); + if (ctx->playlist_bak.data == NULL) { + return NGX_ERROR; + } + + p = ngx_cpymem(ctx->playlist_bak.data, ctx->playlist.data, + ctx->playlist.len); + p = ngx_cpymem(p, ".bak", sizeof(".bak") - 1); + + ctx->playlist_bak.len = p - ctx->playlist_bak.data; + + *p = 0; + + /* key path */ + + if (hacf->keys) { + len = hacf->key_path.len + 1 + ctx->name.len + 1 + NGX_INT64_LEN + + sizeof(".key"); + + ctx->keyfile.data = ngx_palloc(s->connection->pool, len); + if (ctx->keyfile.data == NULL) { + return NGX_ERROR; + } + + p = ngx_cpymem(ctx->keyfile.data, hacf->key_path.data, + hacf->key_path.len); + + if (p[-1] != '/') { + *p++ = '/'; + } + + p = ngx_cpymem(p, ctx->name.data, ctx->name.len); + *p++ = (hacf->nested ? '/' : '-'); + + ctx->keyfile.len = p - ctx->keyfile.data; + } + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: playlist='%V' playlist_bak='%V' " + "stream_pattern='%V' keyfile_pattern='%V'", + &ctx->playlist, &ctx->playlist_bak, + &ctx->stream, &ctx->keyfile); + + if (hacf->continuous) { + ngx_rtmp_hls_restore_stream(s); + } + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_hls_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_hls_app_conf_t *hacf; + ngx_rtmp_hls_ctx_t *ctx; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + if (hacf == NULL || !hacf->hls || ctx == NULL) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: close stream"); + + ngx_rtmp_hls_close_fragment(s); + +next: + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_hls_parse_aac_header(ngx_rtmp_session_t *s, ngx_uint_t *objtype, + ngx_uint_t *srindex, ngx_uint_t *chconf) +{ + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_chain_t *cl; + u_char *p, b0, b1; + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + cl = codec_ctx->aac_header; + + p = cl->buf->pos; + + if (ngx_rtmp_hls_copy(s, NULL, &p, 2, &cl) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_hls_copy(s, &b0, &p, 1, &cl) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_hls_copy(s, &b1, &p, 1, &cl) != NGX_OK) { + return NGX_ERROR; + } + + *objtype = b0 >> 3; + if (*objtype == 0 || *objtype == 0x1f) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: unsupported adts object type:%ui", *objtype); + return NGX_ERROR; + } + + if (*objtype > 4) { + + /* + * Mark all extended profiles as LC + * to make Android as happy as possible. + */ + + *objtype = 2; + } + + *srindex = ((b0 << 1) & 0x0f) | ((b1 & 0x80) >> 7); + if (*srindex == 0x0f) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: unsupported adts sample rate:%ui", *srindex); + return NGX_ERROR; + } + + *chconf = (b1 >> 3) & 0x0f; + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: aac object_type:%ui, sample_rate_index:%ui, " + "channel_config:%ui", *objtype, *srindex, *chconf); + + return NGX_OK; +} + + +static void +ngx_rtmp_hls_update_fragment(ngx_rtmp_session_t *s, uint64_t ts, + ngx_int_t boundary, ngx_uint_t flush_rate) +{ + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_hls_app_conf_t *hacf; + ngx_rtmp_hls_frag_t *f; + ngx_msec_t ts_frag_len; + ngx_int_t same_frag, force,discont; + ngx_buf_t *b; + int64_t d; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + f = NULL; + force = 0; + discont = 1; + + if (ctx->opened) { + f = ngx_rtmp_hls_get_frag(s, ctx->nfrags); + d = (int64_t) (ts - ctx->frag_ts); + + if (d > (int64_t) hacf->max_fraglen * 90 || d < -90000) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: force fragment split: %.3f sec, ", d / 90000.); + force = 1; + + } else { + f->duration = (ts - ctx->frag_ts) / 90000.; + discont = 0; + } + } + + switch (hacf->slicing) { + case NGX_RTMP_HLS_SLICING_PLAIN: + if (f && f->duration < hacf->fraglen / 1000.) { + boundary = 0; + } + break; + + case NGX_RTMP_HLS_SLICING_ALIGNED: + + ts_frag_len = hacf->fraglen * 90; + same_frag = ctx->frag_ts / ts_frag_len == ts / ts_frag_len; + + if (f && same_frag) { + boundary = 0; + } + + if (f == NULL && (ctx->frag_ts == 0 || same_frag)) { + ctx->frag_ts = ts; + boundary = 0; + } + + break; + } + + if (boundary || force) { + ngx_rtmp_hls_close_fragment(s); + ngx_rtmp_hls_open_fragment(s, ts, discont); + } + + b = ctx->aframe; + if (ctx->opened && b && b->last > b->pos && + ctx->aframe_pts + (uint64_t) hacf->max_audio_delay * 90 / flush_rate + < ts) + { + ngx_rtmp_hls_flush_audio(s); + } +} + + +static ngx_int_t +ngx_rtmp_hls_flush_audio(ngx_rtmp_session_t *s) +{ + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_mpegts_frame_t frame; + ngx_int_t rc; + ngx_buf_t *b; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + if (ctx == NULL || !ctx->opened) { + return NGX_OK; + } + + b = ctx->aframe; + + if (b == NULL || b->pos == b->last) { + return NGX_OK; + } + + ngx_memzero(&frame, sizeof(frame)); + + frame.dts = ctx->aframe_pts; + frame.pts = frame.dts; + frame.cc = ctx->audio_cc; + frame.pid = 0x101; + frame.sid = 0xc0; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: flush audio pts=%uL", frame.pts); + + rc = ngx_rtmp_mpegts_write_frame(&ctx->file, &frame, b); + + if (rc != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: audio flush failed"); + } + + ctx->audio_cc = frame.cc; + b->pos = b->last = b->start; + + return rc; +} + + +static ngx_int_t +ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_hls_app_conf_t *hacf; + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + uint64_t pts, est_pts; + int64_t dpts; + size_t bsize; + ngx_buf_t *b; + u_char *p; + ngx_uint_t objtype, srindex, chconf, size; + ngx_uint_t aud_codec_id, samples; + ngx_chain_t *cl; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (hacf == NULL || !hacf->hls || ctx == NULL || + codec_ctx == NULL || h->mlen < 2) + { + return NGX_OK; + } + + aud_codec_id = codec_ctx->audio_codec_id; + + if (aud_codec_id != NGX_RTMP_AUDIO_AAC && + aud_codec_id != NGX_RTMP_AUDIO_MP3) + { + return NGX_OK; + } + + if ((aud_codec_id == NGX_RTMP_AUDIO_AAC && + codec_ctx->aac_header == NULL) || ngx_rtmp_is_codec_header(in)) + { + return NGX_OK; + } + + b = ctx->aframe; + + if (b == NULL) { + + b = ngx_pcalloc(s->connection->pool, sizeof(ngx_buf_t)); + if (b == NULL) { + return NGX_ERROR; + } + + ctx->aframe = b; + + b->start = ngx_palloc(s->connection->pool, hacf->audio_buffer_size); + if (b->start == NULL) { + return NGX_ERROR; + } + + b->end = b->start + hacf->audio_buffer_size; + b->pos = b->last = b->start; + } + + size = aud_codec_id == NGX_RTMP_AUDIO_AAC ? h->mlen - 2 + 7 : h->mlen - 1; + pts = (uint64_t) h->timestamp * 90; + + if (b->start + size > b->end) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: too big audio frame"); + return NGX_OK; + } + + /* + * start new fragment here if + * there's no video at all, otherwise + * do it in video handler + */ + + ngx_rtmp_hls_update_fragment(s, pts, codec_ctx->avc_header == NULL, 2); + + if (b->last + size > b->end) { + ngx_rtmp_hls_flush_audio(s); + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: audio pts=%uL", pts); + + cl = in; + p = b->last; + + if (aud_codec_id == NGX_RTMP_AUDIO_AAC) { + if (b->last + 7 > b->end) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: not enough buffer for audio header"); + return NGX_OK; + } + + b->last += 5; + } else { + in->buf->pos += 1; + } + + /* copy payload */ + + for (; in && b->last < b->end; in = in->next) { + + bsize = in->buf->last - in->buf->pos; + if (b->last + bsize > b->end) { + bsize = b->end - b->last; + } + + b->last = ngx_cpymem(b->last, in->buf->pos, bsize); + } + + if (aud_codec_id == NGX_RTMP_AUDIO_MP3) { + cl->buf->pos -= 1; + } + + /* make up ADTS header */ + + if (aud_codec_id == NGX_RTMP_AUDIO_AAC) { + if (ngx_rtmp_hls_parse_aac_header(s, &objtype, &srindex, &chconf) + != NGX_OK) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: aac header error"); + return NGX_OK; + } + + /* we have 5 free bytes + 2 bytes of RTMP frame header */ + + p[0] = 0xff; + p[1] = 0xf1; + p[2] = (u_char) (((objtype - 1) << 6) | (srindex << 2) | + ((chconf & 0x04) >> 2)); + p[3] = (u_char) (((chconf & 0x03) << 6) | ((size >> 11) & 0x03)); + p[4] = (u_char) (size >> 3); + p[5] = (u_char) ((size << 5) | 0x1f); + p[6] = 0xfc; + + if (p != b->start) { + ctx->aframe_num++; + return NGX_OK; + } + } + + ctx->aframe_pts = pts; + + if (!hacf->sync || codec_ctx->sample_rate == 0) { + return NGX_OK; + } + + /* align audio frames */ + + /* TODO: We assume here AAC frame size is 1024 + * Need to handle AAC frames with frame size of 960 */ + + samples = aud_codec_id == NGX_RTMP_AUDIO_AAC ? 1024 : 1152; + est_pts = ctx->aframe_base + ctx->aframe_num * 90000 * samples / + codec_ctx->sample_rate; + dpts = (int64_t) (est_pts - pts); + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: audio sync dpts=%L (%.5fs)", + dpts, dpts / 90000.); + + if (dpts <= (int64_t) hacf->sync * 90 && + dpts >= (int64_t) hacf->sync * -90) + { + ctx->aframe_num++; + ctx->aframe_pts = est_pts; + return NGX_OK; + } + + ctx->aframe_base = pts; + ctx->aframe_num = 1; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: audio sync gap dpts=%L (%.5fs)", + dpts, dpts / 90000.); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_hls_app_conf_t *hacf; + ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + u_char *p; + uint8_t fmt, ftype, htype, nal_type, src_nal_type; + uint32_t len, rlen; + ngx_buf_t out, *b; + uint32_t cts; + ngx_rtmp_mpegts_frame_t frame; + ngx_uint_t nal_bytes; + ngx_int_t aud_sent, sps_pps_sent, boundary; + static u_char buffer[NGX_RTMP_HLS_BUFSIZE]; + + hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (hacf == NULL || !hacf->hls || ctx == NULL || codec_ctx == NULL || + codec_ctx->avc_header == NULL || h->mlen < 1) + { + return NGX_OK; + } + + /* Only H264 is supported */ + if (codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264) { + return NGX_OK; + } + + p = in->buf->pos; + if (ngx_rtmp_hls_copy(s, &fmt, &p, 1, &in) != NGX_OK) { + return NGX_ERROR; + } + + /* 1: keyframe (IDR) + * 2: inter frame + * 3: disposable inter frame */ + + ftype = (fmt & 0xf0) >> 4; + + /* H264 HDR/PICT */ + + if (ngx_rtmp_hls_copy(s, &htype, &p, 1, &in) != NGX_OK) { + return NGX_ERROR; + } + + /* proceed only with PICT */ + + if (htype != 1) { + return NGX_OK; + } + + /* 3 bytes: decoder delay */ + + if (ngx_rtmp_hls_copy(s, &cts, &p, 3, &in) != NGX_OK) { + return NGX_ERROR; + } + + cts = ((cts & 0x00FF0000) >> 16) | ((cts & 0x000000FF) << 16) | + (cts & 0x0000FF00); + + ngx_memzero(&out, sizeof(out)); + + out.start = buffer; + out.end = buffer + sizeof(buffer); + out.pos = out.start; + out.last = out.pos; + + nal_bytes = codec_ctx->avc_nal_bytes; + aud_sent = 0; + sps_pps_sent = 0; + + while (in) { + if (ngx_rtmp_hls_copy(s, &rlen, &p, nal_bytes, &in) != NGX_OK) { + return NGX_OK; + } + + if (nal_bytes != 3 && nal_bytes != 4) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: incorrect NAL start code length"); + return NGX_ERROR; + } + + if (nal_bytes == 3) { + len = ngx_rtmp_n3_to_h4((u_char *) &rlen); + } else { + len = ntohl(rlen); + } + + if (len == 0) { + continue; + } + + if (ngx_rtmp_hls_copy(s, &src_nal_type, &p, 1, &in) != NGX_OK) { + return NGX_OK; + } + + nal_type = src_nal_type & 0x1f; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: h264 NAL type=%ui, len=%uD", + (ngx_uint_t) nal_type, len); + + if (nal_type >= NGX_RTMP_NALU_SPS && nal_type <= NGX_RTMP_NALU_AUD) { + if (ngx_rtmp_hls_copy(s, NULL, &p, len - 1, &in) != NGX_OK) { + return NGX_ERROR; + } + continue; + } + + if (!aud_sent) { + switch (nal_type) { + case NGX_RTMP_NALU_SLICE: + case NGX_RTMP_NALU_IDR: + case NGX_RTMP_NALU_SEI: + if (ngx_rtmp_hls_append_aud(s, &out) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: error appending AUD NAL"); + } + + /* fall through */ + + case NGX_RTMP_NALU_AUD: + aud_sent = 1; + break; + } + } + + switch (nal_type) { + case NGX_RTMP_NALU_SLICE: + sps_pps_sent = 0; + break; + case NGX_RTMP_NALU_IDR: + if (sps_pps_sent) { + break; + } + if (ngx_rtmp_hls_append_sps_pps(s, &out) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: error appenging SPS/PPS NALs"); + } + sps_pps_sent = 1; + break; + } + + /* AnnexB prefix */ + + if (out.end - out.last < 5) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: not enough buffer for AnnexB prefix"); + return NGX_OK; + } + + /* first AnnexB prefix is long (4 bytes) */ + + if (out.last == out.pos) { + *out.last++ = 0; + } + + *out.last++ = 0; + *out.last++ = 0; + *out.last++ = 1; + *out.last++ = src_nal_type; + + /* NAL body */ + + if (out.end - out.last < (ngx_int_t) len) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: not enough buffer for NAL"); + return NGX_OK; + } + + if (ngx_rtmp_hls_copy(s, out.last, &p, len - 1, &in) != NGX_OK) { + return NGX_ERROR; + } + + out.last += (len - 1); + } + + ngx_memzero(&frame, sizeof(frame)); + + frame.cc = ctx->video_cc; + frame.dts = (uint64_t) h->timestamp * 90; + frame.pts = frame.dts + cts * 90; + frame.pid = 0x100; + frame.sid = 0xe0; + frame.key = (ftype == NGX_RTMP_FRAME_IDR); + + /* + * start new fragment if + * - we have video key frame AND + * - we have audio buffered or have no audio at all or stream is closed + */ + + b = ctx->aframe; + boundary = frame.key && (codec_ctx->aac_header == NULL || !ctx->opened || + (b && b->last > b->pos)); + + ngx_rtmp_hls_update_fragment(s, frame.dts, boundary, 1); + + if (!ctx->opened) { + return NGX_OK; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: video pts=%uL, dts=%uL", frame.pts, frame.dts); + + if (ngx_rtmp_mpegts_write_frame(&ctx->file, &frame, &out) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: video frame failed"); + } + + ctx->video_cc = frame.cc; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_hls_stream_begin(ngx_rtmp_session_t *s, ngx_rtmp_stream_begin_t *v) +{ + return next_stream_begin(s, v); +} + + +static ngx_int_t +ngx_rtmp_hls_stream_eof(ngx_rtmp_session_t *s, ngx_rtmp_stream_eof_t *v) +{ + ngx_rtmp_hls_flush_audio(s); + + ngx_rtmp_hls_close_fragment(s); + + return next_stream_eof(s, v); +} + + +static ngx_int_t +ngx_rtmp_hls_cleanup_dir(ngx_str_t *ppath, ngx_msec_t playlen) +{ + ngx_dir_t dir; + time_t mtime, max_age; + ngx_err_t err; + ngx_str_t name, spath; + u_char *p; + ngx_int_t nentries, nerased; + u_char path[NGX_MAX_PATH + 1]; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "hls: cleanup path='%V' playlen=%M", + ppath, playlen); + + if (ngx_open_dir(ppath, &dir) != NGX_OK) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, ngx_errno, + "hls: cleanup open dir failed '%V'", ppath); + return NGX_ERROR; + } + + nentries = 0; + nerased = 0; + + for ( ;; ) { + ngx_set_errno(0); + + if (ngx_read_dir(&dir) == NGX_ERROR) { + err = ngx_errno; + + if (ngx_close_dir(&dir) == NGX_ERROR) { + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, ngx_errno, + "hls: cleanup " ngx_close_dir_n " \"%V\" failed", + ppath); + } + + if (err == NGX_ENOMOREFILES) { + return nentries - nerased; + } + + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, err, + "hls: cleanup " ngx_read_dir_n + " '%V' failed", ppath); + return NGX_ERROR; + } + + name.data = ngx_de_name(&dir); + if (name.data[0] == '.') { + continue; + } + + name.len = ngx_de_namelen(&dir); + + p = ngx_snprintf(path, sizeof(path) - 1, "%V/%V", ppath, &name); + *p = 0; + + spath.data = path; + spath.len = p - path; + + nentries++; + + if (!dir.valid_info && ngx_de_info(path, &dir) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_CRIT, ngx_cycle->log, ngx_errno, + "hls: cleanup " ngx_de_info_n " \"%V\" failed", + &spath); + + continue; + } + + if (ngx_de_is_dir(&dir)) { + + if (ngx_rtmp_hls_cleanup_dir(&spath, playlen) == 0) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "hls: cleanup dir '%V'", &name); + + /* + * null-termination gets spoiled in win32 + * version of ngx_open_dir + */ + + *p = 0; + + if (ngx_delete_dir(path) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, + "hls: cleanup " ngx_delete_dir_n + " failed on '%V'", &spath); + } else { + nerased++; + } + } + + continue; + } + + if (!ngx_de_is_file(&dir)) { + continue; + } + + if (name.len >= 3 && name.data[name.len - 3] == '.' && + name.data[name.len - 2] == 't' && + name.data[name.len - 1] == 's') + { + max_age = playlen / 500; + + } else if (name.len >= 5 && name.data[name.len - 5] == '.' && + name.data[name.len - 4] == 'm' && + name.data[name.len - 3] == '3' && + name.data[name.len - 2] == 'u' && + name.data[name.len - 1] == '8') + { + max_age = playlen / 1000; + + } else if (name.len >= 4 && name.data[name.len - 4] == '.' && + name.data[name.len - 3] == 'k' && + name.data[name.len - 2] == 'e' && + name.data[name.len - 1] == 'y') + { + max_age = playlen / 500; + + } else { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "hls: cleanup skip unknown file type '%V'", &name); + continue; + } + + mtime = ngx_de_mtime(&dir); + if (mtime + max_age > ngx_cached_time->sec) { + continue; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, ngx_cycle->log, 0, + "hls: cleanup '%V' mtime=%T age=%T", + &name, mtime, ngx_cached_time->sec - mtime); + + if (ngx_delete_file(path) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, ngx_cycle->log, ngx_errno, + "hls: cleanup " ngx_delete_file_n " failed on '%V'", + &spath); + continue; + } + + nerased++; + } +} + + +#if (nginx_version >= 1011005) +static ngx_msec_t +#else +static time_t +#endif +ngx_rtmp_hls_cleanup(void *data) +{ + ngx_rtmp_hls_cleanup_t *cleanup = data; + + ngx_rtmp_hls_cleanup_dir(&cleanup->path, cleanup->playlen); + +#if (nginx_version >= 1011005) + return cleanup->playlen * 2; +#else + return cleanup->playlen / 500; +#endif +} + + +static char * +ngx_rtmp_hls_variant(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_hls_app_conf_t *hacf = conf; + + ngx_str_t *value, *arg; + ngx_uint_t n; + ngx_rtmp_hls_variant_t *var; + + value = cf->args->elts; + + if (hacf->variant == NULL) { + hacf->variant = ngx_array_create(cf->pool, 1, + sizeof(ngx_rtmp_hls_variant_t)); + if (hacf->variant == NULL) { + return NGX_CONF_ERROR; + } + } + + var = ngx_array_push(hacf->variant); + if (var == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memzero(var, sizeof(ngx_rtmp_hls_variant_t)); + + var->suffix = value[1]; + + if (cf->args->nelts == 2) { + return NGX_CONF_OK; + } + + if (ngx_array_init(&var->args, cf->pool, cf->args->nelts - 2, + sizeof(ngx_str_t)) + != NGX_OK) + { + return NGX_CONF_ERROR; + } + + arg = ngx_array_push_n(&var->args, cf->args->nelts - 2); + if (arg == NULL) { + return NGX_CONF_ERROR; + } + + for (n = 2; n < cf->args->nelts; n++) { + *arg++ = value[n]; + } + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_hls_set_permissions(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_hls_app_conf_t *hacf = conf; + + size_t i; + ngx_uint_t f, base, step; + ngx_str_t *value; + + if (hacf->dir_access != NGX_CONF_UNSET_UINT) { + return "is duplicate"; + } + + value = cf->args->elts; + if (value[1].data[0] != '0') { + return "invalid octal: should start with 0"; + } + + if (value[1].len != 4) { + return "invalid permission mask: should be exactly 4 characters long"; + } + + hacf->dir_access = 0; + f = 1; + + for (base = 8, step = 0; base != 1; base >>= 1) { + step++; + } + + for (i = value[1].len - 2; i >= 1; i--) { + f <<= step; + } + + for (i = 1; i < value[1].len; i++) { + if (value[1].data[i] < '0' || value[1].data[i] > '7') { + return "invalid octal number"; + } + + hacf->dir_access += (value[1].data[i] - '0') * f; + f >>= step; + } + + return NGX_CONF_OK; +} + + +static void * +ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_hls_app_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_hls_app_conf_t)); + if (conf == NULL) { + return NULL; + } + + conf->hls = NGX_CONF_UNSET; + conf->fraglen = NGX_CONF_UNSET_MSEC; + conf->max_fraglen = NGX_CONF_UNSET_MSEC; + conf->muxdelay = NGX_CONF_UNSET_MSEC; + conf->sync = NGX_CONF_UNSET_MSEC; + conf->playlen = NGX_CONF_UNSET_MSEC; + conf->continuous = NGX_CONF_UNSET; + conf->nested = NGX_CONF_UNSET; + conf->naming = NGX_CONF_UNSET_UINT; + conf->slicing = NGX_CONF_UNSET_UINT; + conf->type = NGX_CONF_UNSET_UINT; + conf->max_audio_delay = NGX_CONF_UNSET_MSEC; + conf->audio_buffer_size = NGX_CONF_UNSET_SIZE; + conf->cleanup = NGX_CONF_UNSET; + conf->granularity = NGX_CONF_UNSET; + conf->keys = NGX_CONF_UNSET; + conf->frags_per_key = NGX_CONF_UNSET_UINT; + conf->dir_access = NGX_CONF_UNSET_UINT; + + return conf; +} + + +static char * +ngx_rtmp_hls_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_hls_app_conf_t *prev = parent; + ngx_rtmp_hls_app_conf_t *conf = child; + ngx_rtmp_hls_cleanup_t *cleanup; + + 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->max_fraglen, prev->max_fraglen, + conf->fraglen * 10); + ngx_conf_merge_msec_value(conf->muxdelay, prev->muxdelay, 700); + ngx_conf_merge_msec_value(conf->sync, prev->sync, 2); + ngx_conf_merge_msec_value(conf->playlen, prev->playlen, 30000); + ngx_conf_merge_value(conf->continuous, prev->continuous, 1); + ngx_conf_merge_value(conf->nested, prev->nested, 0); + ngx_conf_merge_uint_value(conf->naming, prev->naming, + NGX_RTMP_HLS_NAMING_SEQUENTIAL); + ngx_conf_merge_uint_value(conf->slicing, prev->slicing, + NGX_RTMP_HLS_SLICING_PLAIN); + ngx_conf_merge_uint_value(conf->type, prev->type, + NGX_RTMP_HLS_TYPE_LIVE); + ngx_conf_merge_msec_value(conf->max_audio_delay, prev->max_audio_delay, + 300); + ngx_conf_merge_size_value(conf->audio_buffer_size, prev->audio_buffer_size, + NGX_RTMP_HLS_BUFSIZE); + ngx_conf_merge_value(conf->cleanup, prev->cleanup, 1); + ngx_conf_merge_str_value(conf->base_url, prev->base_url, ""); + ngx_conf_merge_value(conf->granularity, prev->granularity, 0); + ngx_conf_merge_value(conf->keys, prev->keys, 0); + ngx_conf_merge_str_value(conf->key_url, prev->key_url, ""); + ngx_conf_merge_uint_value(conf->frags_per_key, prev->frags_per_key, 0); + ngx_conf_merge_uint_value(conf->dir_access, prev->dir_access, + NGX_RTMP_HLS_DEFAULT_DIR_ACCESS); + ngx_conf_merge_str_value(conf->nested_index_filename, + prev->nested_index_filename, "index"); + + if (conf->fraglen) { + conf->winfrags = conf->playlen / conf->fraglen; + } + + /* schedule cleanup */ + + if (conf->hls && conf->path.len && conf->cleanup && + conf->type != NGX_RTMP_HLS_TYPE_EVENT) + { + if (conf->path.data[conf->path.len - 1] == '/') { + conf->path.len--; + } + + cleanup = ngx_pcalloc(cf->pool, sizeof(*cleanup)); + if (cleanup == NULL) { + return NGX_CONF_ERROR; + } + + cleanup->path = conf->path; + cleanup->playlen = conf->playlen; + + conf->slot = ngx_pcalloc(cf->pool, sizeof(*conf->slot)); + if (conf->slot == NULL) { + return NGX_CONF_ERROR; + } + + conf->slot->manager = ngx_rtmp_hls_cleanup; + conf->slot->name = conf->path; + conf->slot->data = cleanup; + conf->slot->conf_file = cf->conf_file->file.name.data; + conf->slot->line = cf->conf_file->line; + + if (ngx_add_path(cf, &conf->slot) != NGX_OK) { + return NGX_CONF_ERROR; + } + } + + ngx_conf_merge_str_value(conf->path, prev->path, ""); + + if (conf->keys && conf->cleanup && conf->key_path.len && + ngx_strcmp(conf->key_path.data, conf->path.data) != 0 && + conf->type != NGX_RTMP_HLS_TYPE_EVENT) + { + if (conf->key_path.data[conf->key_path.len - 1] == '/') { + conf->key_path.len--; + } + + cleanup = ngx_pcalloc(cf->pool, sizeof(*cleanup)); + if (cleanup == NULL) { + return NGX_CONF_ERROR; + } + + cleanup->path = conf->key_path; + cleanup->playlen = conf->playlen; + + conf->slot = ngx_pcalloc(cf->pool, sizeof(*conf->slot)); + if (conf->slot == NULL) { + return NGX_CONF_ERROR; + } + + conf->slot->manager = ngx_rtmp_hls_cleanup; + conf->slot->name = conf->key_path; + conf->slot->data = cleanup; + conf->slot->conf_file = cf->conf_file->file.name.data; + conf->slot->line = cf->conf_file->line; + + if (ngx_add_path(cf, &conf->slot) != NGX_OK) { + return NGX_CONF_ERROR; + } + } + + ngx_conf_merge_str_value(conf->key_path, prev->key_path, ""); + + if (conf->key_path.len == 0) { + conf->key_path = conf->path; + } + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_hls_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); + *h = ngx_rtmp_hls_video; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); + *h = ngx_rtmp_hls_audio; + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_hls_publish; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_hls_close_stream; + + next_stream_begin = ngx_rtmp_stream_begin; + ngx_rtmp_stream_begin = ngx_rtmp_hls_stream_begin; + + next_stream_eof = ngx_rtmp_stream_eof; + ngx_rtmp_stream_eof = ngx_rtmp_hls_stream_eof; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/hls/ngx_rtmp_hls_module.h b/ngx_http_flv_module/hls/ngx_rtmp_hls_module.h new file mode 100644 index 0000000..b2e01d6 --- /dev/null +++ b/ngx_http_flv_module/hls/ngx_rtmp_hls_module.h @@ -0,0 +1,15 @@ + +/* + * Copyright (C) Winshining + */ + + +#ifndef _NGX_RTMP_HLS_MODULE_H_ +#define _NGX_RTMP_HLS_MODULE_H_ + + +ngx_int_t ngx_rtmp_hls_copy(ngx_rtmp_session_t *s, void *dst, u_char **src, + size_t n, ngx_chain_t **in); + + +#endif /* _NGX_RTMP_HLS_MODULE_H_ */ diff --git a/ngx_http_flv_module/hls/ngx_rtmp_mpegts.c b/ngx_http_flv_module/hls/ngx_rtmp_mpegts.c new file mode 100644 index 0000000..873bbb1 --- /dev/null +++ b/ngx_http_flv_module/hls/ngx_rtmp_mpegts.c @@ -0,0 +1,474 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_mpegts.h" +#include "ngx_rtmp_mpegts_crc.h" + + +static u_char ngx_rtmp_mpegts_header[] = { + + /* TS */ + 0x47, 0x40, 0x00, 0x10, 0x00, + /* PSI */ + 0x00, 0xb0, 0x0d, 0x00, 0x01, 0xc1, 0x00, 0x00, + /* PAT */ + 0x00, 0x01, 0xf0, 0x01, + /* CRC */ + 0x2e, 0x70, 0x19, 0x05, + /* stuffing 167 bytes */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + + /* TS */ + 0x47, 0x50, 0x01, 0x10, 0x00, + /* PSI */ + 0x02, 0xb0, 0x17, 0x00, 0x01, 0xc1, 0x00, 0x00, + /* PMT */ + 0xe1, 0x00, + 0xf0, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, /* video, filled dynamically */ + 0xff, 0xff, 0xff, 0xff, 0xff, /* audio, filled dynamically */ + /* CRC */ + 0xff, 0xff, 0xff, 0xff, /* calculated dynamically */ + /* stuffing 157 bytes */ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff +}; + + +static u_char ngx_rtmp_mpegts_h264_header[] = { + 0x1b, 0xe1, 0x00, 0xf0, 0x00 +}; + + +static u_char ngx_rtmp_mpegts_aac_header[] = { + 0x0f, 0xe1, 0x01, 0xf0, 0x00 +}; + + +static u_char ngx_rtmp_mpegts_mp3_header[] = { + 0x03, 0xe1, 0x01, 0xf0, 0x00 +}; + + +#define NGX_RTMP_MPEGTS_PMT_CRC_START_OFFSET 193 +#define NGX_RTMP_MPEGTS_PMT_SECTION_LENGTH_OFFSET 195 +#define NGX_RTMP_MPEGTS_PMT_LOOP_OFFSET 205 +#define NGX_RTMP_MPEGTS_PMT_CRC_MIN_LENGTH 12 +#define NGX_RTMP_MPEGTS_STREAM_BYTES 5 + + +/* 700 ms PCR delay */ +#define NGX_RTMP_HLS_DELAY 63000 + + +static ngx_int_t +ngx_rtmp_mpegts_write_file(ngx_rtmp_mpegts_file_t *file, u_char *in, + size_t in_size) +{ + u_char *out; + size_t out_size, n; + ssize_t rc; + + static u_char buf[1024]; + + if (!file->encrypt) { + ngx_log_debug1(NGX_LOG_DEBUG_CORE, file->log, 0, + "mpegts: write %uz bytes", in_size); + + rc = ngx_write_fd(file->fd, in, in_size); + if (rc < 0) { + return NGX_ERROR; + } + + return NGX_OK; + } + + /* encrypt */ + + ngx_log_debug1(NGX_LOG_DEBUG_CORE, file->log, 0, + "mpegts: write %uz encrypted bytes", in_size); + + out = buf; + out_size = sizeof(buf); + + if (file->size > 0 && file->size + in_size >= 16) { + ngx_memcpy(file->buf + file->size, in, 16 - file->size); + + in += 16 - file->size; + in_size -= 16 - file->size; + + AES_cbc_encrypt(file->buf, out, 16, &file->key, file->iv, AES_ENCRYPT); + + out += 16; + out_size -= 16; + + file->size = 0; + } + + for ( ;; ) { + n = in_size & ~0x0f; + + if (n > 0) { + if (n > out_size) { + n = out_size; + } + + AES_cbc_encrypt(in, out, n, &file->key, file->iv, AES_ENCRYPT); + + in += n; + in_size -= n; + + } else if (out == buf) { + break; + } + + rc = ngx_write_fd(file->fd, buf, out - buf + n); + if (rc < 0) { + return NGX_ERROR; + } + + out = buf; + out_size = sizeof(buf); + } + + if (in_size) { + ngx_memcpy(file->buf + file->size, in, in_size); + file->size += in_size; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mpegts_write_header(ngx_rtmp_mpegts_file_t *file, + ngx_rtmp_codec_ctx_t *codec_ctx, ngx_uint_t counter) +{ + ngx_int_t stream_bytes; + ngx_rtmp_mpegts_crc_t crc; + u_char buf[sizeof(ngx_rtmp_mpegts_header)]; + + if (codec_ctx->video_codec_id == 0 && codec_ctx->audio_codec_id == 0) { + return NGX_ERROR; + } + + stream_bytes = 0; + ngx_memcpy(buf, ngx_rtmp_mpegts_header, sizeof(ngx_rtmp_mpegts_header)); + + /* 4 bits, periodical */ + counter %= 0x10; + /* fill headers */ + buf[3] = (buf[3] & 0xf0) + (u_char) counter; + buf[191] = (buf[191] & 0xf0) + (u_char) counter; + + if (codec_ctx->video_codec_id) { + /* video info */ + ngx_memcpy(buf + NGX_RTMP_MPEGTS_PMT_LOOP_OFFSET + stream_bytes, + ngx_rtmp_mpegts_h264_header, NGX_RTMP_MPEGTS_STREAM_BYTES); + + stream_bytes += NGX_RTMP_MPEGTS_STREAM_BYTES; + } + + if (codec_ctx->audio_codec_id) { + /* audio info */ + if (codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC) { + ngx_memcpy(buf + NGX_RTMP_MPEGTS_PMT_LOOP_OFFSET + stream_bytes, + ngx_rtmp_mpegts_aac_header, NGX_RTMP_MPEGTS_STREAM_BYTES); + } else { + ngx_memcpy(buf + NGX_RTMP_MPEGTS_PMT_LOOP_OFFSET + stream_bytes, + ngx_rtmp_mpegts_mp3_header, NGX_RTMP_MPEGTS_STREAM_BYTES); + } + + stream_bytes += NGX_RTMP_MPEGTS_STREAM_BYTES; + } + + /* calculate section length */ + buf[NGX_RTMP_MPEGTS_PMT_SECTION_LENGTH_OFFSET] = 13 + stream_bytes; + + /* calculate CRC */ + crc = ngx_rtmp_mpegts_crc_init(); + crc = ngx_rtmp_mpegts_crc_update(crc, + buf + NGX_RTMP_MPEGTS_PMT_CRC_START_OFFSET, + NGX_RTMP_MPEGTS_PMT_CRC_MIN_LENGTH + stream_bytes); + crc = ngx_rtmp_mpegts_crc_finalize(crc); + + buf[NGX_RTMP_MPEGTS_PMT_LOOP_OFFSET + stream_bytes] = (crc >> 24) & 0xff; + buf[NGX_RTMP_MPEGTS_PMT_LOOP_OFFSET + stream_bytes + 1] = (crc >> 16) & 0xff; + buf[NGX_RTMP_MPEGTS_PMT_LOOP_OFFSET + stream_bytes + 2] = (crc >> 8) & 0xff; + buf[NGX_RTMP_MPEGTS_PMT_LOOP_OFFSET + stream_bytes + 3] = crc & 0xff; + + return ngx_rtmp_mpegts_write_file(file, buf, sizeof(buf)); +} + + +static u_char * +ngx_rtmp_mpegts_write_pcr(u_char *p, uint64_t pcr) +{ + *p++ = (u_char) (pcr >> 25); + *p++ = (u_char) (pcr >> 17); + *p++ = (u_char) (pcr >> 9); + *p++ = (u_char) (pcr >> 1); + *p++ = (u_char) (pcr << 7 | 0x7e); + *p++ = 0; + + return p; +} + + +static u_char * +ngx_rtmp_mpegts_write_pts(u_char *p, ngx_uint_t fb, uint64_t pts) +{ + ngx_uint_t val; + + val = fb << 4 | (((pts >> 30) & 0x07) << 1) | 1; + *p++ = (u_char) val; + + val = (((pts >> 15) & 0x7fff) << 1) | 1; + *p++ = (u_char) (val >> 8); + *p++ = (u_char) val; + + val = (((pts) & 0x7fff) << 1) | 1; + *p++ = (u_char) (val >> 8); + *p++ = (u_char) val; + + return p; +} + + +ngx_int_t +ngx_rtmp_mpegts_write_frame(ngx_rtmp_mpegts_file_t *file, + ngx_rtmp_mpegts_frame_t *f, ngx_buf_t *b) +{ + ngx_uint_t pes_size, header_size, body_size, in_size, stuff_size, flags; + u_char packet[188], *p, *base; + ngx_int_t first, rc; + + ngx_log_debug6(NGX_LOG_DEBUG_CORE, file->log, 0, + "mpegts: pid=%ui, sid=%ui, pts=%uL, " + "dts=%uL, key=%ui, size=%ui", + f->pid, f->sid, f->pts, f->dts, + (ngx_uint_t) f->key, (size_t) (b->last - b->pos)); + + first = 1; + + while (b->pos < b->last) { + p = packet; + + f->cc++; + + *p++ = 0x47; + *p++ = (u_char) (f->pid >> 8); + + if (first) { + p[-1] |= 0x40; + } + + *p++ = (u_char) f->pid; + *p++ = 0x10 | (f->cc & 0x0f); /* payload */ + + if (first) { + + if (f->key) { + packet[3] |= 0x20; /* adaptation */ + + *p++ = 7; /* size */ + *p++ = 0x50; /* random access + PCR */ + + p = ngx_rtmp_mpegts_write_pcr(p, f->dts - NGX_RTMP_HLS_DELAY); + } + + /* PES header */ + + *p++ = 0x00; + *p++ = 0x00; + *p++ = 0x01; + *p++ = (u_char) f->sid; + + header_size = 5; + flags = 0x80; /* PTS */ + + if (f->dts != f->pts) { + header_size += 5; + flags |= 0x40; /* DTS */ + } + + pes_size = (b->last - b->pos) + header_size + 3; + if (pes_size > 0xffff) { + pes_size = 0; + } + + *p++ = (u_char) (pes_size >> 8); + *p++ = (u_char) pes_size; + *p++ = 0x80; /* H222 */ + *p++ = (u_char) flags; + *p++ = (u_char) header_size; + + p = ngx_rtmp_mpegts_write_pts(p, flags >> 6, f->pts + + NGX_RTMP_HLS_DELAY); + + if (f->dts != f->pts) { + p = ngx_rtmp_mpegts_write_pts(p, 1, f->dts + + NGX_RTMP_HLS_DELAY); + } + + first = 0; + } + + body_size = (ngx_uint_t) (packet + sizeof(packet) - p); + in_size = (ngx_uint_t) (b->last - b->pos); + + if (body_size <= in_size) { + ngx_memcpy(p, b->pos, body_size); + b->pos += body_size; + + } else { + stuff_size = (body_size - in_size); + + if (packet[3] & 0x20) { + + /* has adaptation */ + + base = &packet[5] + packet[4]; + p = ngx_movemem(base + stuff_size, base, p - base); + ngx_memset(base, 0xff, stuff_size); + packet[4] += (u_char) stuff_size; + + } else { + + /* no adaptation */ + + packet[3] |= 0x20; + p = ngx_movemem(&packet[4] + stuff_size, &packet[4], + p - &packet[4]); + + packet[4] = (u_char) (stuff_size - 1); + if (stuff_size >= 2) { + packet[5] = 0; + ngx_memset(&packet[6], 0xff, stuff_size - 2); + } + } + + ngx_memcpy(p, b->pos, in_size); + b->pos = b->last; + } + + rc = ngx_rtmp_mpegts_write_file(file, packet, sizeof(packet)); + if (rc != NGX_OK) { + return rc; + } + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_mpegts_init_encryption(ngx_rtmp_mpegts_file_t *file, + u_char *key, size_t key_len, uint64_t iv) +{ + if (AES_set_encrypt_key(key, key_len * 8, &file->key)) { + return NGX_ERROR; + } + + ngx_memzero(file->iv, 8); + + file->iv[8] = (u_char) (iv >> 56); + file->iv[9] = (u_char) (iv >> 48); + file->iv[10] = (u_char) (iv >> 40); + file->iv[11] = (u_char) (iv >> 32); + file->iv[12] = (u_char) (iv >> 24); + file->iv[13] = (u_char) (iv >> 16); + file->iv[14] = (u_char) (iv >> 8); + file->iv[15] = (u_char) (iv); + + file->encrypt = 1; + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_mpegts_open_file(ngx_rtmp_mpegts_file_t *file, u_char *path, + ngx_rtmp_codec_ctx_t *codec_ctx, ngx_uint_t counter, ngx_log_t *log) +{ + file->log = log; + + file->fd = ngx_open_file(path, NGX_FILE_WRONLY, NGX_FILE_TRUNCATE, + NGX_FILE_DEFAULT_ACCESS); + + if (file->fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, log, ngx_errno, + "hls: error creating fragment file"); + return NGX_ERROR; + } + + file->size = 0; + + if (ngx_rtmp_mpegts_write_header(file, codec_ctx, counter) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, log, ngx_errno, + "hls: error writing fragment header"); + ngx_close_file(file->fd); + return NGX_ERROR; + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_mpegts_close_file(ngx_rtmp_mpegts_file_t *file) +{ + u_char buf[16]; + ssize_t rc; + + if (file->encrypt) { + ngx_memset(file->buf + file->size, 16 - file->size, 16 - file->size); + + AES_cbc_encrypt(file->buf, buf, 16, &file->key, file->iv, AES_ENCRYPT); + + rc = ngx_write_fd(file->fd, buf, 16); + if (rc < 0) { + return NGX_ERROR; + } + } + + ngx_close_file(file->fd); + + return NGX_OK; +} diff --git a/ngx_http_flv_module/hls/ngx_rtmp_mpegts.h b/ngx_http_flv_module/hls/ngx_rtmp_mpegts.h new file mode 100644 index 0000000..14b2e54 --- /dev/null +++ b/ngx_http_flv_module/hls/ngx_rtmp_mpegts.h @@ -0,0 +1,48 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#ifndef _NGX_RTMP_MPEGTS_H_INCLUDED_ +#define _NGX_RTMP_MPEGTS_H_INCLUDED_ + + +#include +#include +#include +#include "ngx_rtmp_codec_module.h" + + +typedef struct { + ngx_fd_t fd; + ngx_log_t *log; + unsigned encrypt:1; + unsigned size:4; + u_char buf[16]; + u_char iv[16]; + AES_KEY key; +} ngx_rtmp_mpegts_file_t; + + +typedef struct { + uint64_t pts; + uint64_t dts; + ngx_uint_t pid; + ngx_uint_t sid; + ngx_uint_t cc; + unsigned key:1; +} ngx_rtmp_mpegts_frame_t; + + +ngx_int_t ngx_rtmp_mpegts_init_encryption(ngx_rtmp_mpegts_file_t *file, + u_char *key, size_t key_len, uint64_t iv); +ngx_int_t ngx_rtmp_mpegts_open_file(ngx_rtmp_mpegts_file_t *file, u_char *path, + ngx_rtmp_codec_ctx_t *codec_ctx, ngx_uint_t counter, ngx_log_t *log); +ngx_int_t ngx_rtmp_mpegts_close_file(ngx_rtmp_mpegts_file_t *file); +ngx_int_t ngx_rtmp_mpegts_write_frame(ngx_rtmp_mpegts_file_t *file, + ngx_rtmp_mpegts_frame_t *f, ngx_buf_t *b); + + +#endif /* _NGX_RTMP_MPEGTS_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/hls/ngx_rtmp_mpegts_crc.c b/ngx_http_flv_module/hls/ngx_rtmp_mpegts_crc.c new file mode 100644 index 0000000..1eff166 --- /dev/null +++ b/ngx_http_flv_module/hls/ngx_rtmp_mpegts_crc.c @@ -0,0 +1,81 @@ +/** + * \file crc.c + * Functions and types for CRC checks. + * + * Generated on Thu May 5 15:32:31 2016, + * by pycrc v0.9, https://pycrc.org + * using the configuration: + * Width = 32 + * Poly = 0x04c11db7 + * Xor_In = 0xffffffff + * ReflectIn = False + * Xor_Out = 0x00000000 + * ReflectOut = False + * Algorithm = table-driven + *****************************************************************************/ +#include +#include "ngx_rtmp_mpegts_crc.h" /* include the header file generated with pycrc */ + + +/** + * Static table used for the table_driven implementation. + *****************************************************************************/ +static const ngx_rtmp_mpegts_crc_t ngx_rtmp_mpegts_crc_table[256] = { + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, 0x1a864db2, 0x1e475005, + 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, + 0x4c11db70, 0x48d0c6c7, 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, 0x709f7b7a, 0x745e66cd, + 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, + 0xbe2b5b58, 0xbaea46ef, 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, 0xceb42022, 0xca753d95, + 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, + 0x34867077, 0x30476dc0, 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, 0x0808d07d, 0x0cc9cdca, + 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, + 0x5e9f46bf, 0x5a5e5b08, 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, 0xb6238b25, 0xb2e29692, + 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, + 0xe0b41de7, 0xe4750050, 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, 0xdc3abded, 0xd8fba05a, + 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, + 0x4f040d56, 0x4bc510e1, 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, 0x3f9b762c, 0x3b5a6b9b, + 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, + 0xf12f560e, 0xf5ee4bb9, 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, 0xcda1f604, 0xc960ebb3, + 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, + 0x9b3660c6, 0x9ff77d71, 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, 0x470cdd2b, 0x43cdc09c, + 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, + 0x119b4be9, 0x155a565e, 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, 0x2d15ebe3, 0x29d4f654, + 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, + 0xe3a1cbc1, 0xe760d676, 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, 0x933eb0bb, 0x97ffad0c, + 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4 +}; + + +/** + * Update the crc value with new data. + * + * \param crc The current crc value. + * \param data Pointer to a buffer of \a data_len bytes. + * \param data_len Number of bytes in the \a data buffer. + * \return The updated crc value. + *****************************************************************************/ +ngx_rtmp_mpegts_crc_t +ngx_rtmp_mpegts_crc_update(ngx_rtmp_mpegts_crc_t crc, const void *data, size_t data_len) +{ + uint32_t tbl_idx; + const u_char *d = (const unsigned char *) data; + + while (data_len--) { + tbl_idx = ((crc >> 24) ^ *d) & 0xff; + crc = (ngx_rtmp_mpegts_crc_table[tbl_idx] ^ (crc << 8)) & 0xffffffff; + + d++; + } + + return crc & 0xffffffff; +} diff --git a/ngx_http_flv_module/hls/ngx_rtmp_mpegts_crc.h b/ngx_http_flv_module/hls/ngx_rtmp_mpegts_crc.h new file mode 100644 index 0000000..8f89e9a --- /dev/null +++ b/ngx_http_flv_module/hls/ngx_rtmp_mpegts_crc.h @@ -0,0 +1,85 @@ +/** + * \file crc.h + * Functions and types for CRC checks. + * + * Generated on Thu May 5 15:32:22 2016, + * by pycrc v0.9, https://pycrc.org + * using the configuration: + * Width = 32 + * Poly = 0x04c11db7 + * Xor_In = 0xffffffff + * ReflectIn = False + * Xor_Out = 0x00000000 + * ReflectOut = False + * Algorithm = table-driven + *****************************************************************************/ +#ifndef _NGX_RTMP_MPEGTS_CRC_H_INCLUDED_ +#define _NGX_RTMP_MPEGTS_CRC_H_INCLUDED_ + + +#include + + +#ifdef __cplusplus +extern "C" { +#endif + + +/** + * The definition of the used algorithm. + * + * This is not used anywhere in the generated code, but it may be used by the + * application code to call algoritm-specific code, is desired. + *****************************************************************************/ +#define CRC_ALGO_TABLE_DRIVEN 1 + + +/** + * The type of the CRC values. + * + * This type must be big enough to contain at least 32 bits. + *****************************************************************************/ +typedef uint_fast32_t ngx_rtmp_mpegts_crc_t; + + +/** + * Calculate the initial crc value. + * + * \return The initial crc value. + *****************************************************************************/ +static ngx_inline ngx_rtmp_mpegts_crc_t ngx_rtmp_mpegts_crc_init(void) +{ + return 0xffffffff; +} + + +/** + * Update the crc value with new data. + * + * \param crc The current crc value. + * \param data Pointer to a buffer of \a data_len bytes. + * \param data_len Number of bytes in the \a data buffer. + * \return The updated crc value. + *****************************************************************************/ +ngx_rtmp_mpegts_crc_t ngx_rtmp_mpegts_crc_update(ngx_rtmp_mpegts_crc_t crc, + const void *data, size_t data_len); + + +/** + * Calculate the final crc value. + * + * \param crc The current crc value. + * \return The final crc value. + *****************************************************************************/ +static ngx_inline ngx_rtmp_mpegts_crc_t +ngx_rtmp_mpegts_crc_finalize(ngx_rtmp_mpegts_crc_t crc) +{ + return crc ^ 0x00000000; +} + + +#ifdef __cplusplus +} /* closing brace for extern "C" */ +#endif + +#endif /* _NGX_RTMP_MPEGTS_CRC_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_http_flv_live_module.c b/ngx_http_flv_module/ngx_http_flv_live_module.c new file mode 100644 index 0000000..d68e150 --- /dev/null +++ b/ngx_http_flv_module/ngx_http_flv_live_module.c @@ -0,0 +1,2552 @@ + +/* + * Copyright (C) Winshining + */ + +#include +#include +#include +#include "ngx_http_flv_live_module.h" +#include "ngx_rtmp_bandwidth.h" + + +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_close_stream_pt next_close_stream; + + +ngx_rtmp_play_pt http_flv_live_next_play; +ngx_rtmp_close_stream_pt http_flv_live_next_close_stream; + + +static ngx_int_t ngx_http_flv_live_init(ngx_conf_t *cf); +static void *ngx_http_flv_live_create_loc_conf(ngx_conf_t *cf); +static char *ngx_http_flv_live_merge_loc_conf(ngx_conf_t *cf, + void *parent, void *child); + + +static ngx_int_t ngx_http_flv_live_handler(ngx_http_request_t *r); +static void ngx_http_flv_live_cleanup(void *data); +static ngx_int_t ngx_http_flv_live_init_process(ngx_cycle_t *cycle); + +static void ngx_http_flv_live_send_tail(ngx_rtmp_session_t *s); +static ngx_int_t ngx_http_flv_live_send_message(ngx_rtmp_session_t *s, + ngx_chain_t *out, ngx_uint_t priority); +static ngx_chain_t *ngx_http_flv_live_meta_message(ngx_rtmp_session_t *, + ngx_chain_t *in); +static ngx_chain_t *ngx_http_flv_live_append_message(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_rtmp_header_t *lh, ngx_chain_t *in); +static void ngx_http_flv_live_free_message(ngx_rtmp_session_t *s, + ngx_chain_t *in); +static ngx_int_t ngx_http_flv_live_join(ngx_rtmp_session_t *s, u_char *name, + unsigned int publisher); +static ngx_chain_t *ngx_http_flv_live_append_shared_bufs( + ngx_rtmp_core_srv_conf_t *cscf, ngx_rtmp_header_t *h, ngx_chain_t *in, + ngx_flag_t chunked); + +static void ngx_http_flv_live_close_http_request(ngx_rtmp_session_t *s); +static ngx_int_t ngx_http_flv_live_headers_filter(ngx_rtmp_session_t *s); +static ngx_int_t ngx_http_flv_live_header_filter(ngx_rtmp_session_t *s); + +#if (nginx_version <= 1003014) +static void ngx_http_do_free_request(ngx_http_request_t *r, ngx_int_t rc); +static void ngx_http_do_log_request(ngx_http_request_t *r); +#endif + + +typedef struct ngx_http_header_val_s ngx_http_header_val_t; + +typedef ngx_int_t (*ngx_http_set_header_pt)(ngx_http_request_t *r, + ngx_http_header_val_t *hv, ngx_str_t *value); + + +typedef struct { + ngx_str_t name; + ngx_uint_t offset; + ngx_http_set_header_pt handler; +} ngx_http_set_header_t; + + +struct ngx_http_header_val_s { + ngx_http_complex_value_t value; + ngx_str_t key; + ngx_http_set_header_pt handler; + ngx_uint_t offset; +#if (nginx_version >= 1007005) + ngx_uint_t always; /* unsigned always:1 */ +#endif +}; + + +typedef enum { + NGX_HTTP_EXPIRES_OFF, +} ngx_http_expires_t; + + +typedef struct { + ngx_http_expires_t expires; + time_t expires_time; +#if (nginx_version >= 1007009) + ngx_http_complex_value_t *expires_value; +#endif + ngx_array_t *headers; +} ngx_http_headers_conf_t; + + +extern ngx_module_t ngx_http_headers_filter_module; + + +static u_char ngx_http_server_string[] = "Server: nginx" CRLF; +static u_char ngx_http_server_full_string[] = "Server: " NGINX_VER CRLF; +#if (nginx_version >= 1011010) +static u_char ngx_http_server_build_string[] = "Server: " NGINX_VER_BUILD CRLF; +#endif + + +static ngx_str_t ngx_http_status_lines[] = { + + ngx_string("200 OK"), + ngx_null_string, /* "201 Created" */ + ngx_null_string, /* "202 Accepted" */ + ngx_null_string, /* "203 Non-Authoritative Information" */ + ngx_null_string, /* "204 No Content" */ + ngx_null_string, /* "205 Reset Content" */ + ngx_null_string, /* "206 Partial Content" */ + + /* ngx_null_string, */ /* "207 Multi-Status" */ + +#define NGX_HTTP_LAST_2XX 207 +#define NGX_HTTP_OFF_3XX (NGX_HTTP_LAST_2XX - 200) + + /* ngx_null_string, */ /* "300 Multiple Choices" */ + + ngx_string("301 Moved Permanently"), + ngx_string("302 Moved Temporarily"), + ngx_null_string, /* "303 See Other" */ + ngx_null_string, /* "304 Not Modified" */ + ngx_null_string, /* "305 Use Proxy" */ + ngx_null_string, /* "306 unused" */ + ngx_string("307 Temporary Redirect"), + +#define NGX_HTTP_LAST_3XX 308 +#define NGX_HTTP_OFF_4XX (NGX_HTTP_LAST_3XX - 301 + NGX_HTTP_OFF_3XX) + + ngx_string("400 Bad Request"), + ngx_null_string, /* "401 Unauthorized" */ + ngx_null_string, /* "402 Payment Required" */ + ngx_string("403 Forbidden"), + ngx_string("404 Not Found"), + ngx_string("405 Not Allowed"), + ngx_null_string, /* "406 Not Acceptable" */ + ngx_null_string, /* "407 Proxy Authentication Required" */ + ngx_null_string, /* "408 Request Time-out" */ + ngx_null_string, /* "409 Conflict" */ + ngx_null_string, /* "410 Gone" */ + ngx_null_string, /* "411 Length Required" */ + ngx_null_string, /* "412 Precondition Failed" */ + ngx_null_string, /* "413 Request Entity Too Large" */ + ngx_null_string, /* "414 Request-URI Too Large" */ + ngx_null_string, /* "415 Unsupported Media Type" */ + ngx_null_string, /* "416 Requested Range Not Satisfiable" */ + ngx_null_string, /* "417 Expectation Failed" */ + ngx_null_string, /* "418 unused" */ + ngx_null_string, /* "419 unused" */ + ngx_null_string, /* "420 unused" */ + ngx_null_string, /* "421 Misdirected Request" */ + + /* ngx_null_string, */ /* "422 Unprocessable Entity" */ + /* ngx_null_string, */ /* "423 Locked" */ + /* ngx_null_string, */ /* "424 Failed Dependency" */ + +#define NGX_HTTP_LAST_4XX 422 +#define NGX_HTTP_OFF_5XX (NGX_HTTP_LAST_4XX - 400 + NGX_HTTP_OFF_4XX) + + ngx_string("500 Internal Server Error"), + ngx_null_string, /* "501 Not Implemented" */ + ngx_null_string, /* "502 Bad Gateway" */ + ngx_string("503 Service Temporarily Unavailable"), + ngx_null_string, /* "504 Gateway Time-out" */ + ngx_null_string, /* "505 HTTP Version Not Supported" */ + ngx_null_string, /* "506 Variant Also Negotiates" */ + ngx_null_string, /* "507 Insufficient Storage" */ + + /* ngx_null_string, */ /* "508 unused" */ + /* ngx_null_string, */ /* "509 unused" */ + /* ngx_null_string, */ /* "510 Not Extended" */ + +#define NGX_HTTP_LAST_5XX 508 + +}; + + +extern ngx_rtmp_live_proc_handler_t ngx_rtmp_live_proc_handler; +static ngx_rtmp_live_proc_handler_t ngx_http_flv_live_proc_handler = { + NULL, + NULL, + NULL, + NULL, + ngx_http_flv_live_send_message, + ngx_http_flv_live_meta_message, + ngx_http_flv_live_append_message, + ngx_http_flv_live_free_message +}; + +ngx_rtmp_live_proc_handler_t *ngx_rtmp_live_proc_handlers[] = { + &ngx_rtmp_live_proc_handler, + &ngx_http_flv_live_proc_handler +}; + + +static ngx_int_t ngx_http_flv_live_init_handlers(ngx_cycle_t *cycle); + + +static ngx_int_t ngx_http_flv_live_request(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); + +static void ngx_http_flv_live_free_request(ngx_rtmp_session_t *s); + + +static void ngx_http_flv_live_read_handler(ngx_event_t *rev); +static void ngx_http_flv_live_write_handler(ngx_event_t *wev); + +static ngx_int_t ngx_http_flv_live_send(ngx_rtmp_session_t *s); +static void ngx_http_flv_live_correct_timestamp(ngx_rtmp_session_t *s, + ngx_flag_t correct); + +static ngx_int_t ngx_http_flv_live_preprocess(ngx_http_request_t *r, + ngx_rtmp_connection_t *rconn); + +static ngx_rtmp_session_t *ngx_http_flv_live_init_connection( + ngx_http_request_t *r, ngx_rtmp_connection_t *rconn); +static ngx_rtmp_session_t *ngx_http_flv_live_init_session( + ngx_http_request_t *r, ngx_rtmp_addr_conf_t *add_conf); +static ngx_int_t ngx_http_flv_live_connect_init(ngx_rtmp_session_t *s, + ngx_str_t *app, ngx_str_t *stream); + + +static ngx_http_module_t ngx_http_flv_live_module_ctx = { + NULL, + ngx_http_flv_live_init, /* postconfiguration */ + NULL, + NULL, + NULL, + NULL, + ngx_http_flv_live_create_loc_conf, /* create location configuration */ + ngx_http_flv_live_merge_loc_conf /* merge location configuration */ +}; + + +static ngx_command_t ngx_http_flv_live_commands[] = { + { ngx_string("flv_live"), + NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_http_flv_live_conf_t, flv_live), + NULL }, + + ngx_null_command +}; + + +ngx_module_t ngx_http_flv_live_module = { + NGX_MODULE_V1, + &ngx_http_flv_live_module_ctx, + ngx_http_flv_live_commands, + NGX_HTTP_MODULE, + NULL, + NULL, + ngx_http_flv_live_init_process, + NULL, + NULL, + NULL, + NULL, + NGX_MODULE_V1_PADDING +}; + + +static ngx_int_t +ngx_http_flv_live_init(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); + + /* insert in the NGX_HTTP_CONTENT_PHASE */ + h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_flv_live_handler; + + return NGX_OK; +} + + +static void * +ngx_http_flv_live_create_loc_conf(ngx_conf_t *cf) +{ + ngx_http_flv_live_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_flv_live_conf_t)); + if (conf == NULL) { + return NULL; + } + + conf->flv_live = NGX_CONF_UNSET; + + return (void *) conf; +} + + +static char * +ngx_http_flv_live_merge_loc_conf(ngx_conf_t *cf, + void *parent, void *child) +{ + ngx_http_flv_live_conf_t *prev = parent; + ngx_http_flv_live_conf_t *conf = child; + + ngx_conf_merge_value(conf->flv_live, prev->flv_live, 0); + + return NGX_CONF_OK; +} + + +ngx_int_t +ngx_http_flv_live_init_handlers(ngx_cycle_t *cycle) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + + cmcf = ngx_rtmp_cycle_get_module_main_conf(cycle, ngx_rtmp_core_module); + if (cmcf == NULL) { + return NGX_OK; + } + + /* rtmp live conf aready exsits, so add additional event handlers */ + h = ngx_array_push(&cmcf->events[NGX_HTTP_FLV_LIVE_REQUEST]); + *h = ngx_http_flv_live_request; + + next_play = http_flv_live_next_play; + next_close_stream = http_flv_live_next_close_stream; + + http_flv_live_next_play = NULL; + http_flv_live_next_close_stream = NULL; + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_flv_live_init_process(ngx_cycle_t *cycle) +{ + return ngx_http_flv_live_init_handlers(cycle); +} + + +/* + * chunk format: + * hex1\r\n + * content1(hex1)\r\n + * hex2\r\n + * content2(hex2)\r\n + * ... + * 0\r\n\r\n + */ +ngx_int_t +ngx_http_flv_live_send_header(ngx_rtmp_session_t *s) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_http_core_loc_conf_t *clcf; + ngx_http_request_t *r; + ngx_rtmp_live_ctx_t *live_ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_list_part_t *part; + ngx_table_elt_t *e, *header; + u_char *p; + ngx_chain_t cl_flv_hdr, *pkt; + ngx_buf_t buf_flv_hdr; + ngx_uint_t i; + ngx_str_t chunked_flv_header; + ngx_str_t consec_flv_header; + u_char chunked_flv_header_data[18]; + ngx_flag_t connection_header; + + /** + * |F|L|V|ver|00000101|header_size|0|0|0|0|, ngx_http_flv_module.c + * for more details, please refer to http://www.adobe.com/devnet/f4v.html + **/ + u_char flv_header[] = "FLV\x1\0\0\0\0\x9\0\0\0\0"; + + r = s->data; + + r->headers_out.status = NGX_HTTP_OK; + + ngx_str_set(&r->headers_out.content_type, "video/x-flv"); + + /* fill HTTP header 'Connection' according to headers_in */ + r->keepalive = 0; + + connection_header = 0; + part = &r->headers_in.headers.part; + header = part->elts; + + for (i = 0; /* void */; i++) { + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + header = part->elts; + i = 0; + } + + if (header[i].hash == 0) { + continue; + } + + if (ngx_strcasecmp(header[i].key.data, (u_char *) "connection") == 0) { + connection_header = 1; + if (ngx_strcasecmp(header[i].value.data, (u_char *) "keep-alive") + == 0) + { + r->keepalive = 1; + } + + break; + } + } + + if (!connection_header && r->http_version == NGX_HTTP_VERSION_11) { + r->keepalive = 1; + } + + live_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (live_ctx && !live_ctx->active) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: try to send header when session not active"); + + return NGX_ERROR; + } + + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + codec_ctx = ngx_rtmp_get_module_ctx(s->publisher, ngx_rtmp_codec_module); + if (codec_ctx->video_codec_id != 0) { + flv_header[4] |= 0x1; + } + + if (codec_ctx->audio_codec_id != 0 + && codec_ctx->audio_codec_id != NGX_RTMP_AUDIO_UNCOMPRESSED) + { + flv_header[4] |= (0x1 << 2); + } + + if (clcf->chunked_transfer_encoding && + r->http_version == NGX_HTTP_VERSION_11) + { + r->chunked = 1; + + p = chunked_flv_header_data; + *p++ = 'd'; + *p++ = CR; + *p++ = LF; + ngx_memmove(p, flv_header, 13); + p += 13; + *p++ = CR; + *p++ = LF; + chunked_flv_header.data = chunked_flv_header_data; + chunked_flv_header.len = 18; + + buf_flv_hdr.pos = chunked_flv_header.data; + buf_flv_hdr.last = chunked_flv_header.data + chunked_flv_header.len; + } else { + consec_flv_header.data = flv_header; + consec_flv_header.len = 13; + + buf_flv_hdr.pos = consec_flv_header.data; + buf_flv_hdr.last = consec_flv_header.data + consec_flv_header.len; + } + + e = r->headers_out.expires; + if (e == NULL) { + + e = ngx_list_push(&r->headers_out.headers); + if (e == NULL) { + return NGX_ERROR; + } + + r->headers_out.expires = e; + + e->hash = 1; + ngx_str_set(&e->key, "Expires"); + } + + e->value.data = (u_char *) "-1"; + e->value.len = ngx_strlen("-1"); + + if (ngx_http_flv_live_headers_filter(s) == NGX_ERROR) { + return NGX_ERROR; + } + + buf_flv_hdr.start = buf_flv_hdr.pos; + buf_flv_hdr.end = buf_flv_hdr.last; + + cl_flv_hdr.buf = &buf_flv_hdr; + cl_flv_hdr.next = NULL; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + pkt = ngx_rtmp_append_shared_bufs(cscf, NULL, &cl_flv_hdr); + if (pkt == NULL) { + return NGX_ERROR; + } + + ngx_http_flv_live_send_message(s, pkt, 0); + ngx_rtmp_free_shared_chain(cscf, pkt); + + return NGX_OK; +} + + +/** + * for adding non-standard HTTP headers + **/ +static ngx_int_t +ngx_http_flv_live_headers_filter(ngx_rtmp_session_t *s) +{ + ngx_str_t value; + ngx_uint_t i; +#if (nginx_version >= 1007005) + ngx_uint_t safe_status; +#endif + ngx_http_header_val_t *h; + ngx_http_headers_conf_t *conf; + ngx_http_request_t *r; + + r = s->data; + + conf = ngx_http_get_module_loc_conf(r, ngx_http_headers_filter_module); + + /* force */ + conf->expires = NGX_HTTP_EXPIRES_OFF; + + if (conf->headers == NULL) { + return ngx_http_flv_live_header_filter(s); + } + +#if (nginx_version >= 1007005) + switch (r->headers_out.status) { + + case NGX_HTTP_OK: + case NGX_HTTP_CREATED: + case NGX_HTTP_NO_CONTENT: + case NGX_HTTP_PARTIAL_CONTENT: + case NGX_HTTP_MOVED_PERMANENTLY: + case NGX_HTTP_MOVED_TEMPORARILY: + case NGX_HTTP_SEE_OTHER: + case NGX_HTTP_NOT_MODIFIED: + case NGX_HTTP_TEMPORARY_REDIRECT: + safe_status = 1; + break; + + default: + safe_status = 0; + } +#endif + + if (conf->headers) { + h = conf->headers->elts; + for (i = 0; i < conf->headers->nelts; i++) { + +#if (nginx_version >= 1007005) + if (!safe_status && !h[i].always) { + continue; + } +#endif + + if (ngx_http_complex_value(r, &h[i].value, &value) != NGX_OK) { + return NGX_ERROR; + } + + if (h[i].handler(r, &h[i], &value) != NGX_OK) { + return NGX_ERROR; + } + } + } + + return ngx_http_flv_live_header_filter(s); +} + + +static ngx_int_t +ngx_http_flv_live_header_filter(ngx_rtmp_session_t *s) +{ + u_char *p; + size_t len; + ngx_str_t *status_line; + ngx_buf_t *b; + ngx_uint_t status, i; + ngx_chain_t out, *pkt; + ngx_list_part_t *part; + ngx_table_elt_t *header; + ngx_http_core_loc_conf_t *clcf; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_http_request_t *r; + + r = s->data; + + if (r->header_sent) { + return NGX_OK; + } + + r->header_sent = 1; + + if (r->chunked && r->http_version < NGX_HTTP_VERSION_11) { + ngx_log_error(NGX_LOG_WARN, s->connection->log, 0, + "flv live: chunked only supported by HTTP/1.1"); + + r->chunked = 0; + } + + len = sizeof("HTTP/1.x ") - 1 + sizeof(CRLF) - 1 + /* the end of the header */ + + sizeof(CRLF) - 1; + + /* status line */ + + if (r->headers_out.status_line.len) { + len += r->headers_out.status_line.len; + status_line = &r->headers_out.status_line; +#if (NGX_SUPPRESS_WARN) + status = 0; +#endif + + } else { + + status = r->headers_out.status; + + if (status >= NGX_HTTP_OK + && status < NGX_HTTP_LAST_2XX) + { + /* 2XX */ + + status -= NGX_HTTP_OK; + status_line = &ngx_http_status_lines[status]; + len += ngx_http_status_lines[status].len; + + } else if (status >= NGX_HTTP_MOVED_PERMANENTLY + && status < NGX_HTTP_LAST_3XX) + { + /* 3XX */ + + status = status - NGX_HTTP_MOVED_PERMANENTLY + NGX_HTTP_OFF_3XX; + status_line = &ngx_http_status_lines[status]; + len += ngx_http_status_lines[status].len; + + } else if (status >= NGX_HTTP_BAD_REQUEST + && status < NGX_HTTP_LAST_4XX) + { + /* 4XX */ + status = status - NGX_HTTP_BAD_REQUEST + + NGX_HTTP_OFF_4XX; + + status_line = &ngx_http_status_lines[status]; + len += ngx_http_status_lines[status].len; + + } else if (status >= NGX_HTTP_INTERNAL_SERVER_ERROR + && status < NGX_HTTP_LAST_5XX) + { + /* 5XX */ + status = status - NGX_HTTP_INTERNAL_SERVER_ERROR + + NGX_HTTP_OFF_5XX; + + status_line = &ngx_http_status_lines[status]; + len += ngx_http_status_lines[status].len; + + } else { + len += NGX_INT_T_LEN + 1 /* SP */; + status_line = NULL; + } + + if (status_line && status_line->len == 0) { + status = r->headers_out.status; + len += NGX_INT_T_LEN + 1 /* SP */; + status_line = NULL; + } + } + + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + if (r->headers_out.server == NULL) { +#if (nginx_version >= 1011010) + if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) { + len += sizeof(ngx_http_server_full_string) - 1; + + } else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) { + len += sizeof(ngx_http_server_build_string) - 1; + + } else { + len += sizeof(ngx_http_server_string) - 1; + } +#else + len += clcf->server_tokens ? sizeof(ngx_http_server_full_string) - 1 : + sizeof(ngx_http_server_string) - 1; +#endif + } + + if (r->headers_out.date == NULL) { + len += sizeof("Date: Mon, 28 Sep 1970 06:00:00 GMT" CRLF) - 1; + } + + if (r->headers_out.content_type.len) { + len += sizeof("Content-Type: ") - 1 + + r->headers_out.content_type.len + 2; + + if (r->headers_out.content_type_len == r->headers_out.content_type.len + && r->headers_out.charset.len) + { + len += sizeof("; charset=") - 1 + r->headers_out.charset.len; + } + } + + if (r->headers_out.content_length == NULL + && r->headers_out.content_length_n >= 0) + { + len += sizeof("Content-Length: ") - 1 + NGX_OFF_T_LEN + 2; + } + + if (r->headers_out.last_modified == NULL + && r->headers_out.last_modified_time != -1) + { + len += sizeof("Last-Modified: Mon, 28 Sep 1970 06:00:00 GMT" CRLF) - 1; + } + + if (r->chunked) { + len += sizeof("Transfer-Encoding: chunked" CRLF) - 1; + } + + if (r->keepalive) { + len += sizeof("Connection: keep-alive" CRLF) - 1; + + /* + * MSIE and Opera ignore the "Keep-Alive: timeout=" header. + * MSIE keeps the connection alive for about 60-65 seconds. + * Opera keeps the connection alive very long. + * Mozilla keeps the connection alive for N plus about 1-10 seconds. + * Konqueror keeps the connection alive for about N seconds. + */ + + if (clcf->keepalive_header) { + len += sizeof("Keep-Alive: timeout=") - 1 + NGX_TIME_T_LEN + 2; + } + + } else { + len += sizeof("Connection: close" CRLF) - 1; + } + + part = &r->headers_out.headers.part; + header = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + header = part->elts; + i = 0; + } + + if (header[i].hash == 0) { + continue; + } + + len += header[i].key.len + sizeof(": ") - 1 + header[i].value.len + + sizeof(CRLF) - 1; + } + + b = ngx_create_temp_buf(r->pool, len); + if (b == NULL) { + return NGX_ERROR; + } + + /* "HTTP/1.x " */ + if (r->http_version == NGX_HTTP_VERSION_10) { + b->last = ngx_cpymem(b->last, "HTTP/1.0 ", sizeof("HTTP/1.x ") - 1); + } else { + b->last = ngx_cpymem(b->last, "HTTP/1.1 ", sizeof("HTTP/1.x ") - 1); + } + + /* status line */ + if (status_line) { + b->last = ngx_copy(b->last, status_line->data, status_line->len); + + } else { + b->last = ngx_sprintf(b->last, "%03ui ", status); + } + *b->last++ = CR; *b->last++ = LF; + + if (r->headers_out.server == NULL) { +#if (nginx_version >= 1011010) + if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) { + p = ngx_http_server_full_string; + len = sizeof(ngx_http_server_full_string) - 1; + + } else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) { + p = ngx_http_server_build_string; + len = sizeof(ngx_http_server_build_string) - 1; + + } else { +#else + if (clcf->server_tokens) { + p = (u_char *) ngx_http_server_full_string; + len = sizeof(ngx_http_server_full_string) - 1; + + } else { +#endif + p = ngx_http_server_string; + len = sizeof(ngx_http_server_string) - 1; + } + + b->last = ngx_cpymem(b->last, p, len); + } + + if (r->headers_out.date == NULL) { + b->last = ngx_cpymem(b->last, "Date: ", sizeof("Date: ") - 1); + b->last = ngx_cpymem(b->last, ngx_cached_http_time.data, + ngx_cached_http_time.len); + + *b->last++ = CR; *b->last++ = LF; + } + + if (r->headers_out.content_type.len) { + b->last = ngx_cpymem(b->last, "Content-Type: ", + sizeof("Content-Type: ") - 1); + p = b->last; + b->last = ngx_copy(b->last, r->headers_out.content_type.data, + r->headers_out.content_type.len); + + if (r->headers_out.content_type_len == r->headers_out.content_type.len + && r->headers_out.charset.len) + { + b->last = ngx_cpymem(b->last, "; charset=", + sizeof("; charset=") - 1); + b->last = ngx_copy(b->last, r->headers_out.charset.data, + r->headers_out.charset.len); + + /* update r->headers_out.content_type for possible logging */ + + r->headers_out.content_type.len = b->last - p; + r->headers_out.content_type.data = p; + } + + *b->last++ = CR; *b->last++ = LF; + } + + if (r->chunked) { + b->last = ngx_cpymem(b->last, "Transfer-Encoding: chunked" CRLF, + sizeof("Transfer-Encoding: chunked" CRLF) - 1); + } + + if (r->keepalive) { + b->last = ngx_cpymem(b->last, "Connection: keep-alive" CRLF, + sizeof("Connection: keep-alive" CRLF) - 1); + + if (clcf->keepalive_header) { + b->last = ngx_sprintf(b->last, "Keep-Alive: timeout=%T" CRLF, + clcf->keepalive_header); + } + + } else { + b->last = ngx_cpymem(b->last, "Connection: close" CRLF, + sizeof("Connection: close" CRLF) - 1); + } + + part = &r->headers_out.headers.part; + header = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + + part = part->next; + header = part->elts; + i = 0; + } + + if (header[i].hash == 0) { + continue; + } + + b->last = ngx_copy(b->last, header[i].key.data, header[i].key.len); + *b->last++ = ':'; *b->last++ = ' '; + + b->last = ngx_copy(b->last, header[i].value.data, header[i].value.len); + *b->last++ = CR; *b->last++ = LF; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "%*s", (size_t) (b->last - b->pos), b->pos); + + /* the end of HTTP header */ + *b->last++ = CR; + *b->last++ = LF; + + r->header_size = b->last - b->pos; + + out.buf = b; + out.next = NULL; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + pkt = ngx_rtmp_append_shared_bufs(cscf, NULL, &out); + if (pkt == NULL) { + return NGX_ERROR; + } + + ngx_http_flv_live_send_message(s, pkt, 0); + ngx_rtmp_free_shared_chain(cscf, pkt); + + return NGX_OK; +} + + +static void +ngx_http_flv_live_send_tail(ngx_rtmp_session_t *s) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_chain_t cl_resp_hdr, *pkt; + ngx_buf_t buf_resp_hdr; + + const ngx_str_t response_tail = ngx_string("0" CRLF CRLF); + + buf_resp_hdr.pos = response_tail.data; + buf_resp_hdr.last = response_tail.data + response_tail.len; + buf_resp_hdr.start = buf_resp_hdr.pos; + buf_resp_hdr.end = buf_resp_hdr.last; + + cl_resp_hdr.buf = &buf_resp_hdr; + cl_resp_hdr.next = NULL; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + pkt = ngx_rtmp_append_shared_bufs(cscf, NULL, &cl_resp_hdr); + ngx_http_flv_live_send_message(s, pkt, 0); + ngx_rtmp_free_shared_chain(cscf, pkt); +} + + +static ngx_int_t +ngx_http_flv_live_send_message(ngx_rtmp_session_t *s, + ngx_chain_t *out, ngx_uint_t priority) +{ + ngx_uint_t nmsg; + + nmsg = (s->out_last + s->out_queue - s->out_pos) % s->out_queue + 1; + + if (priority > 3) { + priority = 3; + } + + /* drop packet? + * Note we always leave 1 slot free */ + if (nmsg + priority * s->out_queue / 4 >= s->out_queue) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv live: HTTP drop message bufs=%ui, priority=%ui", + nmsg, priority); + + return NGX_AGAIN; + } + + s->out[s->out_last++] = out; + s->out_last %= s->out_queue; + + ngx_rtmp_acquire_shared_chain(out); + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv live: HTTP send nmsg=%ui, priority=%ui #%ui", + nmsg, priority, s->out_last); + + if (priority && s->out_buffer && nmsg < s->out_cork) { + return NGX_OK; + } + + if (!s->connection->write->active) { + ngx_http_flv_live_write_handler(s->connection->write); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_flv_live_request(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_play_t v; + + ngx_int_t rc; + ngx_http_request_t *r; + ngx_http_flv_live_ctx_t *ctx; + + r = s->data; + ctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module); + + rc = ngx_http_flv_live_connect_init(s, &ctx->app, &ctx->stream); + if (rc != NGX_OK) { + return NGX_ERROR; + } + + if (s->notify_connect) { + return NGX_OK; + } + + ngx_memzero(&v, sizeof(ngx_rtmp_play_t)); + + ngx_memcpy(v.name, ctx->stream.data, ngx_min(ctx->stream.len, + sizeof(v.name) - 1)); + ngx_memcpy(v.args, s->args.data, ngx_min(s->args.len, + sizeof(v.args) - 1)); + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "flv live: name='%s' args='%s' start=%i duration=%i " + "reset=%i silent=%i", + v.name, v.args, (ngx_int_t) v.start, + (ngx_int_t) v.duration, (ngx_int_t) v.reset, + (ngx_int_t) v.silent); + + return ngx_rtmp_play(s, &v); +} + + +/* +--------------+ +-------------+ + * | Client | | | Server | + * +------+-------+ | +------+------+ + * | Handshaking done | + * | | | + * | | | + * |----------- Command Message(connect) ------->| + * | | + * |<------- Window Acknowledgement Size --------| + * | | + * |<----------- Set Peer Bandwidth -------------| + * | | + * |-------- Window Acknowledgement Size ------->| + * | | + * |<------ User Control Message(StreamBegin) ---| + * | | + * |<------------ Command Message ---------------| + * | (_result- connect response) | + * + * omit the user control message feedback + */ +void +ngx_http_flv_live_set_status(ngx_rtmp_session_t *s, unsigned active) +{ + ngx_rtmp_live_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + + ctx->active = active; + + ctx->cs[0].active = 0; + ctx->cs[0].dropped = 0; + + ctx->cs[1].active = 0; + ctx->cs[1].dropped = 0; +} + + +static ngx_int_t +ngx_http_flv_live_join(ngx_rtmp_session_t *s, u_char *name, + unsigned int publisher) +{ + ngx_rtmp_live_ctx_t *ctx; + ngx_rtmp_live_stream_t **stream; + ngx_rtmp_live_app_conf_t *lacf; + + /* only for subscribers */ + if (publisher) { + return NGX_DECLINED; + } + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + return NGX_DECLINED; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx && ctx->stream) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv live: already joined"); + + return NGX_DECLINED; + } + + if (ctx == NULL) { + ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_live_ctx_t)); + if (ctx == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: failed to allocate for ctx"); + + return NGX_ERROR; + } + + 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, + "flv live: join '%s'", name); + + stream = ngx_rtmp_live_get_stream(s, name, lacf->idle_streams); + + if (stream == NULL || + !(publisher || (*stream)->publishing || lacf->idle_streams)) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: stream not found"); + + return NGX_ERROR; + } + + ctx->stream = *stream; + ctx->publishing = publisher; + ctx->next = (*stream)->ctx; + ctx->protocol = NGX_RTMP_PROTOCOL_HTTP; + + (*stream)->ctx = ctx; + + if (ctx->stream->pub_ctx) { + s->publisher = ctx->stream->pub_ctx->session; + } + + if (lacf->buflen) { + s->out_buffer = 1; + } + + ctx->cs[0].csid = NGX_RTMP_CSID_VIDEO; + ctx->cs[1].csid = NGX_RTMP_CSID_AUDIO; + + if (!ctx->publishing && ctx->stream->active) { + ngx_http_flv_live_set_status(s, 1); + } + + return NGX_OK; +} + + +ngx_int_t +ngx_http_flv_live_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_live_app_conf_t *lacf; + ngx_http_request_t *r; + + r = s->data; + if (r == NULL) { + goto next; + } + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL || !lacf->live) { + goto next; + } + +#if (nginx_version >= 1013001) + /** + * when playing from pull, the downstream requests on the most + * of time return before the upstream requests, flv.js always + * sends HTTP header 'Connection: keep-alive', but Nginx has + * deleted r->blocked in ngx_http_finalize_request, that causes + * ngx_http_set_keepalive to run the cleanup handlers to close + * the connection between downstream and server, so play fails + **/ + r->keepalive = 0; +#endif + + /* join stream as a subscriber */ + + if (ngx_http_flv_live_join(s, v->name, 0) == NGX_ERROR) { + return NGX_ERROR; + } + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv live play: name='%s' start=%uD duration=%uD reset=%d", + v->name, (uint32_t) v->start, + (uint32_t) v->duration, (uint32_t) v->reset); + +next: + return next_play(s, v); +} + + +static void +ngx_http_flv_live_close_http_request(ngx_rtmp_session_t *s) +{ + ngx_http_request_t *r; + + r = s->data; + if (r && r->connection && !r->connection->destroyed) { + if (r->chunked) { + ngx_http_flv_live_send_tail(s); + } + } +} + + +ngx_int_t +ngx_http_flv_live_close_stream(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_live_ctx_t *ctx, **cctx, *unlink; + ngx_http_request_t *r; + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_live_stream_t **stream; + ngx_flag_t passive; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + goto next; + } + + passive = 0; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL) { + goto next; + } + + if (ctx->stream == NULL) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv live: not joined"); + + goto next; + } + + if (ctx->protocol == NGX_RTMP_PROTOCOL_RTMP) { + /* close RTMP live play */ + if (!ctx->publishing) { + goto next; + } + + /* close all http flv live streams */ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv live: push closed '%s', close live streams subscribed", + ctx->stream->name); + + passive = 1; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv live: leave '%s'", ctx->stream->name); + + if (passive) { + for (cctx = &ctx->stream->ctx; *cctx; /* void */) { + if ((*cctx)->protocol == NGX_RTMP_PROTOCOL_HTTP && + !lacf->idle_streams) + { + ngx_http_flv_live_close_http_request((*cctx)->session); + + if (!(*cctx)->publishing && (*cctx)->stream->active) { + ngx_http_flv_live_set_status((*cctx)->session, 0); + } + + ngx_http_flv_live_free_request((*cctx)->session); + ngx_rtmp_finalize_session((*cctx)->session); + + unlink = *cctx; + + *cctx = (*cctx)->next; + + unlink->next = NULL; + } else { + cctx = &(*cctx)->next; + } + } + } else { + for (cctx = &ctx->stream->ctx; *cctx; /* void */) { + if (*cctx == ctx) { + if (!ctx->publishing && ctx->stream->active) { + ngx_http_flv_live_set_status(s, 0); + } + + *cctx = ctx->next; + + if (ctx->stream->pub_ctx == NULL && + ctx->stream->ctx == NULL) + { + stream = ngx_rtmp_live_get_stream(s, ctx->stream->name, 0); + if (stream) { + *stream = (*stream)->next; + + ctx->stream->next = lacf->free_streams; + lacf->free_streams = ctx->stream; + } + } + + ctx->next = NULL; + ctx->stream = NULL; + + ngx_http_flv_live_free_request(s); + s->connection->destroyed = 1; + + break; + } else { + cctx = &(*cctx)->next; + } + } + } + + /** + * close only http requests here, the other + * requests were left for next_close_stream + **/ + +next: + if (s->notify_connect || s->notify_play) { + r = s->data; + if (r) { + ngx_http_flv_live_free_request(s); + s->connection->destroyed = 1; + } + } + + return next_close_stream(s, v); +} + + +static void +ngx_http_flv_live_free_request(ngx_rtmp_session_t *s) +{ + ngx_connection_t *c; + ngx_http_request_t *r; + ngx_http_cleanup_t **cln; + + r = s->data; + if (r) { + for (cln = &r->cleanup; *cln; /* void */) { + if ((*cln)->handler == ngx_http_flv_live_cleanup) { + *cln = (*cln)->next; + break; + } + + cln = &(*cln)->next; + } + + c = r->connection; + +#if (nginx_version <= 1003014) + ngx_http_do_free_request(r, 0); +#else + ngx_http_free_request(r, 0); +#endif + +#if (NGX_HTTP_SSL) + if (c->ssl) { + ngx_ssl_shutdown(c); + } +#endif + + /* for later processing */ + c->destroyed = 0; + } +} + + +#if (nginx_version <= 1003014) +static void +ngx_http_do_free_request(ngx_http_request_t *r, ngx_int_t rc) +{ + ngx_log_t *log; + ngx_pool_t *pool; + struct linger linger; + ngx_http_cleanup_t *cln; + ngx_http_log_ctx_t *ctx; + ngx_http_core_loc_conf_t *clcf; + + log = r->connection->log; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, log, 0, "http close request"); + + if (r->pool == NULL) { + ngx_log_error(NGX_LOG_ALERT, log, 0, "http request already closed"); + return; + } + + cln = r->cleanup; + r->cleanup = NULL; + + while (cln) { + if (cln->handler) { + cln->handler(cln->data); + } + + cln = cln->next; + } + +#if (NGX_STAT_STUB) + + if (r->stat_reading) { + (void) ngx_atomic_fetch_add(ngx_stat_reading, -1); + } + + if (r->stat_writing) { + (void) ngx_atomic_fetch_add(ngx_stat_writing, -1); + } + +#endif + + if (rc > 0 && (r->headers_out.status == 0 || r->connection->sent == 0)) { + r->headers_out.status = rc; + } + + log->action = "logging request"; + + ngx_http_do_log_request(r); + + log->action = "closing request"; + + if (r->connection->timedout) { + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + if (clcf->reset_timedout_connection) { + linger.l_onoff = 1; + linger.l_linger = 0; + + if (setsockopt(r->connection->fd, SOL_SOCKET, SO_LINGER, + (const void *) &linger, sizeof(struct linger)) == -1) + { + ngx_log_error(NGX_LOG_ALERT, log, ngx_socket_errno, + "setsockopt(SO_LINGER) failed"); + } + } + } + + /* the various request strings were allocated from r->pool */ + ctx = log->data; + ctx->request = NULL; + + r->request_line.len = 0; + + r->connection->destroyed = 1; + + /* + * Setting r->pool to NULL will increase probability to catch double close + * of request since the request object is allocated from its own pool. + */ + + pool = r->pool; + r->pool = NULL; + + ngx_destroy_pool(pool); +} + + +static void +ngx_http_do_log_request(ngx_http_request_t *r) +{ + ngx_uint_t i, n; + ngx_http_handler_pt *log_handler; + ngx_http_core_main_conf_t *cmcf; + + cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module); + + log_handler = cmcf->phases[NGX_HTTP_LOG_PHASE].handlers.elts; + n = cmcf->phases[NGX_HTTP_LOG_PHASE].handlers.nelts; + + for (i = 0; i < n; i++) { + log_handler[i](r); + } +} +#endif + + +static void +ngx_http_flv_live_read_handler(ngx_event_t *rev) +{ + ngx_connection_t *c; + ngx_http_request_t *r; + ngx_rtmp_session_t *s; + ngx_int_t n; + ngx_http_flv_live_ctx_t *ctx; + u_char buf[NGX_BUFF_MAX_SIZE]; + + c = rev->data; + if (c->destroyed) { + return; + } + + r = c->data; + ctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module); + s = ctx->s; + + for ( ;; ) { + n = c->recv(c, buf, sizeof(buf)); + + if (n == NGX_AGAIN) { + ngx_add_timer(c->read, s->timeout); + + if (ngx_handle_read_event(c->read, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + + break; + } else if (n == 0 || n == -1) { + ngx_rtmp_finalize_session(s); + break; + } + } +} + + +static void +ngx_http_flv_live_write_handler(ngx_event_t *wev) +{ + ngx_connection_t *c; + ngx_http_request_t *r; + ngx_rtmp_session_t *s; + ngx_int_t n; + ngx_rtmp_live_ctx_t *lctx; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_http_flv_live_ctx_t *ctx; + + c = wev->data; + if (c->destroyed) { + return; + } + + r = c->data; + ctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module); + s = ctx->s; + + if (wev->timedout) { + ngx_log_error(NGX_LOG_ERR, c->log, NGX_ETIMEDOUT, + "flv live: client timed out"); + c->timedout = 1; + ngx_rtmp_finalize_session(s); + return; + } + + if (wev->timer_set) { + ngx_del_timer(wev); + } + + if (s->out_chain == NULL && s->out_pos != s->out_last) { + s->out_chain = s->out[s->out_pos]; + s->out_bpos = s->out_chain->buf->pos; + } + + while (s->out_chain) { + n = ngx_http_flv_live_send(s); + + if (n == NGX_AGAIN || n == 0) { + ngx_add_timer(c->write, s->timeout); + if (ngx_handle_write_event(c->write, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + return; + } + + if (n < 0) { + ngx_rtmp_finalize_session(s); + return; + } + + s->out_bytes += n; + s->ping_reset = 1; + ngx_rtmp_update_bandwidth(&ngx_rtmp_bw_out, n); + s->out_bpos += n; + + if (s->out_bpos == s->out_chain->buf->last) { + s->out_chain = s->out_chain->next; + if (s->out_chain == NULL) { + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + ngx_rtmp_free_shared_chain(cscf, s->out[s->out_pos]); + s->out[s->out_pos] = NULL; + ++s->out_pos; + s->out_pos %= s->out_queue; + if (s->out_pos == s->out_last) { + break; + } + s->out_chain = s->out[s->out_pos]; + } + s->out_bpos = s->out_chain->buf->pos; + } + } + + lctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (lctx && !lctx->publishing && !wev->timer_set) { + ngx_add_timer(wev, s->timeout); + } + + if (wev->active) { + ngx_del_event(wev, NGX_WRITE_EVENT, 0); + } + + ngx_event_process_posted((ngx_cycle_t *) ngx_cycle, &s->posted_dry_events); +} + + +static ngx_int_t +ngx_http_flv_live_send(ngx_rtmp_session_t *s) +{ + ngx_int_t n; + ngx_connection_t *c; + ngx_http_request_t *r; + + c = s->connection; + r = s->data; + + if (r->chunked) { + if (s->out_chain == s->out[s->out_pos]) { + n = c->send(c, s->out_bpos, s->out_chain->buf->last - s->out_bpos); + if (n == NGX_AGAIN || n == 0 || n < 0) { + return n; + } + + if (n != s->out_chain->buf->last - s->out_bpos || + s->out_chain->next == NULL) + { + return n; + } + + s->out_chain = s->out_chain->next; + + s->out_bytes += n; + s->out_bpos = s->out_chain->buf->pos; + s->ping_reset = 1; + ngx_rtmp_update_bandwidth(&ngx_rtmp_bw_out, n); + } + } + + ngx_http_flv_live_correct_timestamp(s, 1); + n = c->send(c, s->out_bpos, s->out_chain->buf->last - s->out_bpos); + ngx_http_flv_live_correct_timestamp(s, 0); + + return n; +} + + +static void +ngx_http_flv_live_correct_timestamp(ngx_rtmp_session_t *s, ngx_flag_t correct) +{ + uint8_t type; + uint32_t timestamp; + u_char *p; + ngx_chain_t *cl; + ngx_buf_t *b; + + cl = s->out_chain; + if (cl == NULL) { + return; + } + + b = cl->buf; + if (b->start + NGX_RTMP_MAX_CHUNK_HEADER != b->pos) { + type = b->pos[0] & 0x1f; + + ngx_log_debug3(NGX_LOG_DEBUG_HTTP, s->connection->log, 0, + "flv live: type=%uD, correct=%uD, offset_timestamp=%uD", + type, correct, s->offset_timestamp); + + if (type != NGX_RTMP_MSG_VIDEO && type != NGX_RTMP_MSG_AUDIO) { + return; + } + + p = b->pos + 4; + timestamp = ngx_rtmp_n3_to_h4(p); + timestamp |= ((uint32_t) p[3] << 24); + + if (correct) { + timestamp -= s->offset_timestamp; + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, s->connection->log, 0, + "flv live: correct timestamp=%uD", timestamp); + } else { + timestamp += s->offset_timestamp; + ngx_log_debug1(NGX_LOG_DEBUG_HTTP, s->connection->log, 0, + "flv live: recover timestamp=%uD", timestamp); + } + + p = b->pos + 4; + ngx_rtmp_h4_to_n3(p, timestamp); + p += 3; + *p++ = (u_char) (timestamp >> 24); + } +} + + +static ngx_int_t +ngx_http_flv_live_preprocess(ngx_http_request_t *r, + ngx_rtmp_connection_t *rconn) +{ + ngx_http_flv_live_ctx_t *ctx; + ngx_listening_t *ls; + struct sockaddr *local_sockaddr; + + struct sockaddr_in *ls_sin, *sin; +#if (NGX_HAVE_INET6) + struct sockaddr_in6 *ls_sin6, *sin6; +#endif + + ngx_rtmp_in_addr_t *addr; +#if (NGX_HAVE_INET6) + ngx_rtmp_in6_addr_t *addr6; +#endif + + ngx_rtmp_port_t *rport; + + ngx_str_t arg_app = ngx_string("app"); + ngx_str_t arg_stream = ngx_string("stream"); + ngx_str_t arg_port = ngx_string("port"); + ngx_int_t in_port; + ngx_uint_t i, n; + ngx_flag_t port_match, addr_match; + unsigned short sa_family; + + ctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module); + + /** + * if requested args are escaped, for example, urls in the + * history list of vlc for Android (or all mobile platforms) + **/ + if (r->args.len == 0 && r->uri.len) { + ngx_http_split_args(r, &r->uri, &r->args); + } + + if (ngx_http_arg(r, arg_port.data, arg_port.len, &ctx->port) != NGX_OK) { + /* no port in args */ + ctx->port.len = ngx_strlen("1935"); + ctx->port.data = ngx_pcalloc(r->pool, ctx->port.len + 1); + if (ctx->port.data == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(ctx->port.data, (const void *) "1935", ctx->port.len); + in_port = 1935; + } else { + in_port = ngx_atoi(ctx->port.data, ctx->port.len); + if (in_port == NGX_ERROR || (in_port < 0 || in_port > 65535)) { + return NGX_ERROR; + } + } + + in_port = htons(in_port); + + port_match = 1; + addr_match = 1; + + ls = ngx_cycle->listening.elts; + for (n = 0; n < ngx_cycle->listening.nelts; ++n, ++ls) { + if (ls->handler == ngx_rtmp_init_connection) { + local_sockaddr = r->connection->local_sockaddr; + sa_family = local_sockaddr->sa_family; + + if (sa_family != ls->sockaddr->sa_family) { +#if (NGX_HAVE_INET6) + if (ls->sockaddr->sa_family == AF_INET6) { + if (ls->ipv6only) { +#endif + continue; +#if (NGX_HAVE_INET6) + } else { + if (local_sockaddr->sa_family != AF_INET) { + continue; + } + + sa_family = AF_INET6; + } + } +#endif + } + + switch (sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + ls_sin6 = (struct sockaddr_in6 *) ls->sockaddr; + if (in_port != ls_sin6->sin6_port) { + port_match = 0; + } + + break; +#endif + + default: + ls_sin = (struct sockaddr_in *) ls->sockaddr; + if (in_port != ls_sin->sin_port) { + port_match = 0; + } + } + + if (!port_match) { + port_match = 1; + continue; + } + + rport = ls->servers; + + if (rport->naddrs > 1) { + /** + * listen xxx.xxx.xxx.xxx:port + * listen port + **/ + switch (sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + sin6 = (struct sockaddr_in6 *) ls->sockaddr; + + addr6 = rport->addrs; + + /* the last address is "*" */ + + for (i = 0; i < rport->naddrs - 1; i++) { + if (ngx_memcmp(&addr6[i].addr6, &sin6->sin6_addr, 16) + == 0) + { + break; + } + } + + rconn->addr_conf = &addr6[i].conf; + + break; +#endif + + default: + sin = (struct sockaddr_in *) ls->sockaddr; + + addr = rport->addrs; + + /* the last address is "*" */ + + for (i = 0; i < rport->naddrs - 1; i++) { + if (addr[i].addr == sin->sin_addr.s_addr) { + break; + } + } + + rconn->addr_conf = &addr[i].conf; + } + } else { + switch (sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + sin6 = (struct sockaddr_in6 *) ls->sockaddr; + + addr6 = rport->addrs; + if (ngx_memcmp(&addr6[0].addr6, &sin6->sin6_addr, 16)) { + addr_match = 0; + } else { + rconn->addr_conf = &addr6[0].conf; + } + + break; +#endif + + default: + sin = (struct sockaddr_in *) ls->sockaddr; + + addr = rport->addrs; + if (addr[0].addr != sin->sin_addr.s_addr) { + addr_match = 0; + } else { + rconn->addr_conf = &addr[0].conf; + } + } + } + + if (!addr_match) { + addr_match = 1; + continue; + } + + break; + } + } + + if (n == ngx_cycle->listening.nelts) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "flv live: failed to find configured port: '%V'", &ctx->port); + + return NGX_ERROR; + } + + if (ngx_http_arg(r, arg_app.data, arg_app.len, &ctx->app) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "flv live: app args MUST be specified"); + + return NGX_ERROR; + } + + if (ngx_http_arg(r, arg_stream.data, arg_stream.len, + &ctx->stream) != NGX_OK) + { + ctx->stream.data = (u_char *) ""; + ctx->stream.len = 0; + } + + return NGX_OK; +} + + +ngx_rtmp_session_t * +ngx_http_flv_live_init_connection(ngx_http_request_t *r, + ngx_rtmp_connection_t *rconn) +{ + ngx_rtmp_session_t *s; + ngx_connection_t *c; + void *data; + + c = r->connection; + + /* the default server configuration for the address:port */ + rconn->conf_ctx = rconn->addr_conf->default_server->ctx; + + ++ngx_rtmp_naccepted; + + data = c->data; + c->data = rconn; + + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "flv live: *%ui client connected '%V'", c->number, &c->addr_text); + + s = ngx_http_flv_live_init_session(r, rconn->addr_conf); + c->data = data; + + if (s == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "flv live: failed to init connection for session"); + + return NULL; + } + + /* only auto-pushed connections are + * done through unix socket */ + + s->auto_pushed = 0; + + c->write->handler = ngx_http_flv_live_write_handler; + c->read->handler = ngx_http_flv_live_read_handler; + + if (c->write->active) { + if (ngx_del_event(c->write, NGX_WRITE_EVENT, 0) != NGX_OK) { + return NULL; + } + } + + return s; +} + + +static ngx_rtmp_session_t * +ngx_http_flv_live_init_session(ngx_http_request_t *r, + ngx_rtmp_addr_conf_t *addr_conf) +{ + ngx_rtmp_session_t *s; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_error_log_ctx_t *ctx; + ngx_connection_t *c; + + c = r->connection; + + s = ngx_pcalloc(c->pool, sizeof(ngx_rtmp_session_t)); + if (s == NULL) { + /* let other handlers process */ + goto failed; + } + + s->rtmp_connection = c->data; + + s->main_conf = addr_conf->default_server->ctx->main_conf; + s->srv_conf = addr_conf->default_server->ctx->srv_conf; + + s->addr_text = &addr_conf->addr_text; + + s->connection = c; + + ctx = ngx_palloc(c->pool, sizeof(ngx_rtmp_error_log_ctx_t)); + if (ctx == NULL) { + goto failed; + } + + ctx->client = &c->addr_text; + ctx->session = s; + + c->log->connection = c->number; + c->log->handler = ngx_rtmp_log_error; + c->log->data = ctx; + c->log->action = NULL; + + c->log_error = NGX_ERROR_INFO; + + s->ctx = ngx_pcalloc(c->pool, sizeof(void *) * ngx_rtmp_max_module); + if (s->ctx == NULL) { + goto failed; + } + + s->out_pool = ngx_create_pool(4096, c->log); + if (s->out_pool == NULL) { + goto failed; + } + + s->out = ngx_pcalloc(s->out_pool, sizeof(ngx_chain_t *) + * ((ngx_rtmp_core_srv_conf_t *) + addr_conf->default_server->ctx->srv_conf + [ngx_rtmp_core_module.ctx_index])->out_queue); + if (s->out == NULL) { + goto failed; + } + + s->in_streams_pool = ngx_create_pool(4096, c->log); + if (s->in_streams_pool == NULL) { + goto failed; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + s->out_queue = cscf->out_queue; + s->out_cork = cscf->out_cork; + s->in_streams = ngx_pcalloc(s->in_streams_pool, sizeof(ngx_rtmp_stream_t) + * cscf->max_streams); + if (s->in_streams == NULL) { + goto failed; + } + +#if (nginx_version >= 1007005) + ngx_queue_init(&s->posted_dry_events); +#endif + + s->epoch = ngx_current_msec; + s->timeout = cscf->timeout; + s->buflen = cscf->buflen; + ngx_rtmp_set_chunk_size(s, NGX_RTMP_DEFAULT_CHUNK_SIZE); + + if (ngx_rtmp_fire_event(s, NGX_RTMP_CONNECT, NULL, NULL) != NGX_OK) { + goto failed; + } + + s->data = (void *) r; + + return s; + +failed: + if (s && s->out_pool) { + ngx_destroy_pool(s->out_pool); + s->out_pool = NULL; + } + + if (s && s->in_streams_pool) { + ngx_destroy_pool(s->in_streams_pool); + s->in_streams_pool = NULL; + } + + return NULL; +} + + +static ngx_int_t +ngx_http_flv_live_connect_init(ngx_rtmp_session_t *s, ngx_str_t *app, + ngx_str_t *stream) +{ + ngx_rtmp_connect_t v; + ngx_connection_t *c; + ngx_http_request_t *r; + u_char name[NGX_RTMP_MAX_NAME]; + + r = s->data; + c = s->connection; + + ngx_memzero(&v, sizeof(ngx_rtmp_connect_t)); + + ngx_memcpy(v.app, app->data, ngx_min(app->len, sizeof(v.app) - 1)); + ngx_memcpy(v.args, r->args.data, ngx_min(r->args.len, sizeof(v.args) - 1)); + ngx_memcpy(v.flashver, "flv_live 1.1", ngx_strlen("flv_live 1.1")); + + *ngx_snprintf(v.tc_url, NGX_RTMP_MAX_URL, "http://%V/%V", + &r->headers_in.host->value, app) = 0; + +#define NGX_RTMP_SET_STRPAR(name) \ + s->name.len = ngx_strlen(v.name); \ + s->name.data = ngx_palloc(c->pool, s->name.len); \ + if (s->name.data == NULL) { \ + return NGX_ERROR; \ + } \ + ngx_memcpy(s->name.data, v.name, s->name.len) + + NGX_RTMP_SET_STRPAR(app); + NGX_RTMP_SET_STRPAR(args); + NGX_RTMP_SET_STRPAR(flashver); + NGX_RTMP_SET_STRPAR(tc_url); + +#undef NGX_RTMP_SET_STRPAR + + ngx_memzero(name, NGX_RTMP_MAX_NAME); + ngx_memcpy(name, stream->data, + ngx_min(stream->len, NGX_RTMP_MAX_NAME - 1)); + + if (ngx_rtmp_process_request_line(s, name, v.args, + (const u_char *) "flv live connect") != NGX_OK) + { + return NGX_ERROR; + } + + if (ngx_rtmp_process_virtual_host(s) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: failed to process virtual host"); + + return NGX_ERROR; + } + + s->stream.len = stream->len; + s->stream.data = ngx_pstrdup(c->pool, stream); + + return ngx_rtmp_connect(s, &v); +} + + +static ngx_chain_t * +ngx_http_flv_live_meta_message(ngx_rtmp_session_t *s, ngx_chain_t *in) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_http_request_t *r; + ngx_chain_t *meta, *iter, *out; + u_char *p, *save; + uint8_t fmt; + uint32_t csid; + ngx_int_t thsize; + ngx_rtmp_header_t ch; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return NULL; + } + + r = s->data; + if (r == NULL || (r->connection && r->connection->destroyed)) { + return NULL; + } + + /* remove RTMP header in meta */ + meta = in; + p = meta->buf->pos; + save = meta->buf->pos; + if (meta->buf->last == p) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: no meta"); + + return NULL; + } + + fmt = (*p >> 6) & 0x03; + if (fmt) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: non-type 0 format chunk message header"); + + return NULL; + } + + csid = *p++ & 0x3f; + if (csid == 0) { + if (meta->buf->last - p < 1) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: incorrect basic header 2"); + + return NULL; + } + + p += 1; + } else if (csid == 1) { + if (meta->buf->last - p < 2) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: incorrect basic header 3"); + + return NULL; + } + + p += 2; + } + + thsize = p - meta->buf->pos; + + /* + * Chunk Message Header - Type 0 + * |timestamp(3B)|msg len(3B)|msg type id(1B)|msg stream id(4B)| + */ + if (meta->buf->last - p <= 11) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv live: incorrect chunk message header"); + + return NULL; + } + + p += 11; + meta->buf->pos = p; + + for (iter = meta->next; iter; iter = iter->next) { + iter->buf->pos += thsize; + } + + ch.timestamp = 0; + ch.type = NGX_RTMP_MSG_AMF_META; + + out = ngx_http_flv_live_append_message(s, &ch, NULL, meta); + + in->buf->pos = save; + for (iter = meta->next; iter; iter = iter->next) { + iter->buf->pos -= thsize; + } + + return out; +} + + +static ngx_chain_t * +ngx_http_flv_live_append_message(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_rtmp_header_t *lh, ngx_chain_t *in) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_http_request_t *r; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return NULL; + } + + r = s->data; + if (r == NULL || r->connection == NULL || r->connection->destroyed) { + return NULL; + } + + if (h->type == NGX_RTMP_MSG_VIDEO || h->type == NGX_RTMP_MSG_AUDIO) { + if (!s->offset_timestamp_set) { + s->offset_timestamp_set = 1; + s->offset_timestamp = h->timestamp; + } else if (h->timestamp == 0) { + s->offset_timestamp = 0; + } + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, s->connection->log, 0, + "flv live: timestamp=%uD, offset_timestamp=%uD", + h->timestamp, s->offset_timestamp); + } + + return ngx_http_flv_live_append_shared_bufs(cscf, h, in, r->chunked); +} + + +/* + * Brief format: + * |Tag|PreviousTagSize| + * Detailed format: + * |Reserved(2b)+Filter(1b)+TagType(5b)|DataLength(3B)|TimeStamp(3B)| + * TimeStampExt(1B)|StreamID(3B)|Data(DataLengthB)|PreviousTagSize| + */ +static ngx_chain_t * +ngx_http_flv_live_append_shared_bufs(ngx_rtmp_core_srv_conf_t *cscf, + ngx_rtmp_header_t *h, ngx_chain_t *in, ngx_flag_t chunked) +{ + ngx_chain_t *tag, *chunk_head, *chunk_tail, chunk, + *iter, *last_in, **tail, prev_tag_size; + u_char *pos, +#if !(NGX_WIN32) + chunk_item[ngx_strlen("0000000000000000" CRLF) + 1]; +#else + chunk_item[19]; +#endif + uint32_t data_size, size; + off_t tag_size; + ngx_buf_t prev_tag_size_buf, chunk_buf; + + for (data_size = 0, iter = in, last_in = iter; iter; iter = iter->next) { + last_in = iter; + data_size += (iter->buf->last - iter->buf->pos); + } + + tail = &last_in->next; + *tail = &prev_tag_size; + + tag_size = data_size + NGX_FLV_TAG_HEADER_SIZE; + + prev_tag_size.buf = &prev_tag_size_buf; + prev_tag_size.next = NULL; + + prev_tag_size_buf.start = (u_char *) &size; + prev_tag_size_buf.end = prev_tag_size_buf.start + sizeof(uint32_t); + prev_tag_size_buf.pos = prev_tag_size_buf.start; + prev_tag_size_buf.last = prev_tag_size_buf.end; + + pos = prev_tag_size_buf.pos; + *(uint32_t *) pos = htonl(tag_size); + pos += 4; + + /* ngx_rtmp_alloc_shared_buf returns the memory: + * |4B|sizeof(ngx_chain_t)|sizeof(ngx_buf_t)|NGX_RTMP_MAX_CHUNK_HEADER| + * chunk_size| + * the tag->buf->pos points to the addr of last part of memory + */ + tag = ngx_rtmp_append_shared_bufs(cscf, NULL, in); + if (tag == NULL) { + return NULL; + } + + /* it links to the local variable, unlink it */ + *tail = NULL; + + tag->buf->pos -= NGX_FLV_TAG_HEADER_SIZE; + pos = tag->buf->pos; + + /* type, 5bits */ + *pos++ = (u_char) (h->type & 0x1f); + + /* data length, 3B */ + ngx_rtmp_h4_to_n3(pos, data_size); + pos += 3; + + /* timestamp, 3B + ext, 1B */ + ngx_rtmp_h4_to_n3(pos, h->timestamp); + pos += 3; + *pos++ = (u_char) (h->timestamp >> 24); + + /* streamId, 3B, always be 0 */ + *pos++ = 0; + *pos++ = 0; + *pos++ = 0; + + /* add chunk header and tail */ + if (chunked) { + /* 4 is the size of previous tag size itself */ + *ngx_sprintf(chunk_item, "%xO" CRLF, tag_size + 4) = 0; + + chunk_buf.start = chunk_item; + chunk_buf.pos = chunk_buf.start; + chunk_buf.end = chunk_buf.start + ngx_strlen(chunk_item); + chunk_buf.last = chunk_buf.end; + + chunk.buf = &chunk_buf; + chunk.next = NULL; + + chunk_head = ngx_rtmp_append_shared_bufs(cscf, NULL, &chunk); + if (chunk_head == NULL) { + return NULL; + } + + for (iter = tag, last_in = iter; iter; iter = iter->next) { + last_in = iter; + } + + /* save the memory, very likely */ +#if !(NGX_WIN32) + if (__builtin_expect(last_in->buf->last + 2 <= last_in->buf->end, 1)) { +#else + if (last_in->buf->last + 2 <= last_in->buf->end) { +#endif + *last_in->buf->last++ = CR; + *last_in->buf->last++ = LF; + } else { + *ngx_sprintf(chunk_item, CRLF) = 0; + chunk_buf.start = chunk_item; + chunk_buf.pos = chunk_buf.start; + chunk_buf.end = chunk_buf.start + ngx_strlen(chunk_item); + chunk_buf.last = chunk_buf.end; + + chunk.buf = &chunk_buf; + chunk.next = NULL; + + chunk_tail = ngx_rtmp_append_shared_bufs(cscf, NULL, &chunk); + if (chunk_tail == NULL) { + return NULL; + } + + tail = &last_in->next; + *tail = chunk_tail; + } + + chunk_head->next = tag; + + return chunk_head; + } + + return tag; +} + + +static void +ngx_http_flv_live_free_message(ngx_rtmp_session_t *s, ngx_chain_t *in) +{ + ngx_rtmp_core_srv_conf_t *cscf; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return; + } + + ngx_rtmp_free_shared_chain(cscf, in); +} + + +static void +ngx_http_flv_live_close_session_handler(ngx_rtmp_session_t *s) +{ + ngx_connection_t *c; + ngx_rtmp_core_srv_conf_t *cscf; + + c = s->connection; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + ngx_log_error(NGX_LOG_INFO, c->log, 0, "flv live: close session"); + + ngx_rtmp_fire_event(s, NGX_RTMP_DISCONNECT, NULL, NULL); + + if (s->ping_evt.timer_set) { + ngx_del_timer(&s->ping_evt); + } + + if (s->in_old_pool) { + ngx_destroy_pool(s->in_old_pool); + } + + if (s->in_pool) { + ngx_destroy_pool(s->in_pool); + } + + while (s->out_pos != s->out_last) { + ngx_rtmp_free_shared_chain(cscf, s->out[s->out_pos]); + + s->out_pos++; + s->out_pos %= s->out_queue; + } + + if (s->in_streams_pool) { + ngx_destroy_pool(s->in_streams_pool); + } + + if (s->out_pool) { + ngx_destroy_pool(s->out_pool); + } +} + + +static void +ngx_http_flv_live_cleanup(void *data) +{ + ngx_rtmp_session_t *s; + + s = data; + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "flv live: close connection"); + + ngx_http_flv_live_close_session_handler(s); +} + + +static ngx_int_t +ngx_http_flv_live_handler(ngx_http_request_t *r) +{ + ngx_int_t rc; + ngx_http_flv_live_conf_t *hfcf; + ngx_http_cleanup_t *cln; + ngx_http_flv_live_ctx_t *ctx; + ngx_rtmp_session_t *s; + ngx_rtmp_connection_t *rconn; + + if (ngx_exiting || ngx_terminate) { + return NGX_HTTP_CLOSE; + } + + hfcf = ngx_http_get_module_loc_conf(r, ngx_http_flv_live_module); + if (!hfcf->flv_live) { + return NGX_DECLINED; + } + + if (!(r->method & (NGX_HTTP_GET))) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "flv live: HTTP method was not \"GET\""); + + return NGX_HTTP_NOT_ALLOWED; + } + + if (r->http_version == NGX_HTTP_VERSION_9 +#if (NGX_HTTP_V2) + || r->http_version == NGX_HTTP_VERSION_20 +#endif + ) + { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "flv live: HTTP version 0.9 or 2.0 not supported"); + + return NGX_HTTP_NOT_ALLOWED; + } + + if (r->uri.data[r->uri.len - 1] == '/') { + return NGX_DECLINED; + } + + rc = ngx_http_discard_request_body(r); + + if (rc != NGX_OK) { + return rc; + } + + ctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module); + if (ctx == NULL) { + ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_flv_live_ctx_t)); + + if (ctx == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ngx_http_set_ctx(r, ctx, ngx_http_flv_live_module); + } + + rconn = ngx_pcalloc(r->pool, sizeof(ngx_rtmp_connection_t)); + if (rconn == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (ngx_http_flv_live_preprocess(r, rconn) != NGX_OK) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + s = ngx_http_flv_live_init_connection(r, rconn); + if (s == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ctx->s = s; + + /* live, ranges not allowed */ + r->allow_ranges = 0; + r->read_event_handler = ngx_http_test_reading; + + cln = ngx_http_cleanup_add(r, 0); + if (cln == NULL) { + return NGX_DECLINED; + } + + cln->handler = ngx_http_flv_live_cleanup; + cln->data = s; + + if (ngx_rtmp_fire_event(s, NGX_HTTP_FLV_LIVE_REQUEST, NULL, NULL) + != NGX_OK) + { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + r->main->count++; + + return NGX_DONE; +} diff --git a/ngx_http_flv_module/ngx_http_flv_live_module.h b/ngx_http_flv_module/ngx_http_flv_live_module.h new file mode 100644 index 0000000..cadb312 --- /dev/null +++ b/ngx_http_flv_module/ngx_http_flv_live_module.h @@ -0,0 +1,78 @@ + +/* + * Copyright (C) Winshining + */ + +#ifndef _NGX_HTTP_FLV_LIVE_H_INCLUDED_ +#define _NGX_HTTP_FLV_LIVE_H_INCLUDED_ + + +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_codec_module.h" + + +#define NGX_HASH_MAX_SIZE 0x80 +#define NGX_HASH_MAX_BUKET_SIZE 0x40 +#define NGX_BUFF_MAX_SIZE 0x80 +#define NGX_FLV_TAG_HEADER_SIZE 11 + + +extern ngx_module_t ngx_rtmp_module; + + +extern ngx_rtmp_play_pt http_flv_live_next_play; +extern ngx_rtmp_close_stream_pt http_flv_live_next_close_stream; + + +#define ngx_rtmp_cycle_get_module_main_conf(cycle, module) \ + (cycle->conf_ctx[ngx_rtmp_module.index] ? \ + ((ngx_rtmp_conf_ctx_t *) cycle->conf_ctx[ngx_rtmp_module.index]) \ + ->main_conf[module.ctx_index]: \ + NULL) + + +typedef struct ngx_http_flv_live_ctx_s { + ngx_rtmp_session_t *s; + ngx_flag_t header_sent; + + ngx_str_t app; + ngx_str_t port; + ngx_str_t stream; +} ngx_http_flv_live_ctx_t; + + +typedef struct ngx_http_flv_live_conf_s { + ngx_flag_t flv_live; +} ngx_http_flv_live_conf_t; + + +typedef struct { + ngx_chain_t *meta; + ngx_chain_t *apkt; + ngx_chain_t *acopkt; + ngx_chain_t *rpkt; + + ngx_int_t (*send_message_pt)(ngx_rtmp_session_t *s, + ngx_chain_t *out, ngx_uint_t priority); + ngx_chain_t *(*meta_message_pt)(ngx_rtmp_session_t *s, + ngx_chain_t *in); + ngx_chain_t *(*append_message_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_rtmp_header_t *lh, + ngx_chain_t *in); + void (*free_message_pt)(ngx_rtmp_session_t *s, + ngx_chain_t *in); +} ngx_rtmp_live_proc_handler_t; + + +ngx_int_t ngx_http_flv_live_play(ngx_rtmp_session_t *s, + ngx_rtmp_play_t *v); +ngx_int_t ngx_http_flv_live_close_stream(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v); + +ngx_int_t ngx_http_flv_live_send_header(ngx_rtmp_session_t *s); +void ngx_http_flv_live_set_status(ngx_rtmp_session_t *s, unsigned active); + + +#endif + diff --git a/ngx_http_flv_module/ngx_rtmp.c b/ngx_http_flv_module/ngx_rtmp.c new file mode 100644 index 0000000..77260ed --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp.c @@ -0,0 +1,1509 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include +#include +#include "ngx_rtmp.h" + + +static char *ngx_rtmp_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +static ngx_int_t ngx_rtmp_optimize_servers(ngx_conf_t *cf, + ngx_rtmp_core_main_conf_t *cmcf, ngx_array_t *ports); +static ngx_int_t ngx_rtmp_server_names(ngx_conf_t *cf, + ngx_rtmp_core_main_conf_t *cmcf, ngx_rtmp_conf_addr_t *addr); +static int ngx_libc_cdecl ngx_rtmp_cmp_dns_wildcards(const void *one, + const void *two); + +static ngx_int_t ngx_rtmp_init_listening(ngx_conf_t *cf, + ngx_rtmp_conf_port_t *port); +static ngx_listening_t *ngx_rtmp_add_listening(ngx_conf_t *cf, + ngx_rtmp_conf_addr_t *addr); + +static ngx_int_t ngx_rtmp_add_addrs(ngx_conf_t *cf, ngx_rtmp_port_t *mport, + ngx_rtmp_conf_addr_t *addr); +#if (NGX_HAVE_INET6) +static ngx_int_t ngx_rtmp_add_addrs6(ngx_conf_t *cf, ngx_rtmp_port_t *mport, + ngx_rtmp_conf_addr_t *addr); +#endif +static ngx_int_t ngx_rtmp_cmp_conf_addrs(const void *one, const void *two); +static ngx_int_t ngx_rtmp_find_virtual_server(ngx_connection_t *c, + ngx_rtmp_virtual_names_t *virtual_names, ngx_str_t *host, + ngx_rtmp_session_t *s, ngx_rtmp_core_srv_conf_t **cscfp); +static ngx_int_t ngx_rtmp_init_events(ngx_conf_t *cf, + ngx_rtmp_core_main_conf_t *cmcf); +static ngx_int_t ngx_rtmp_init_event_handlers(ngx_conf_t *cf, + ngx_rtmp_core_main_conf_t *cmcf); +static char * ngx_rtmp_merge_applications(ngx_conf_t *cf, + ngx_array_t *applications, void **app_conf, ngx_rtmp_module_t *module, + ngx_uint_t ctx_index); +static ngx_int_t ngx_rtmp_init_process(ngx_cycle_t *cycle); + + +#if (nginx_version >= 1007011) +ngx_queue_t ngx_rtmp_init_queue; +#elif (nginx_version >= 1007005) +ngx_thread_volatile ngx_queue_t ngx_rtmp_init_queue; +#else +ngx_thread_volatile ngx_event_t *ngx_rtmp_init_queue; +#endif + + +ngx_uint_t ngx_rtmp_max_module; + + +static ngx_command_t ngx_rtmp_commands[] = { + + { ngx_string("rtmp"), + NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, + ngx_rtmp_block, + 0, + 0, + NULL }, + + ngx_null_command +}; + + +static ngx_core_module_t ngx_rtmp_module_ctx = { + ngx_string("rtmp"), + NULL, + NULL +}; + + +ngx_module_t ngx_rtmp_module = { + NGX_MODULE_V1, + &ngx_rtmp_module_ctx, /* module context */ + ngx_rtmp_commands, /* module directives */ + NGX_CORE_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + ngx_rtmp_init_process, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static char * +ngx_rtmp_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + char *rv; + ngx_uint_t m, mi, s; + ngx_conf_t pcf; + ngx_module_t **modules; + ngx_rtmp_module_t *module; + ngx_rtmp_conf_ctx_t *ctx; + ngx_rtmp_core_srv_conf_t *cscf, **cscfp; + ngx_rtmp_core_main_conf_t *cmcf; + + ctx = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_conf_ctx_t)); + if (ctx == NULL) { + return NGX_CONF_ERROR; + } + + *(ngx_rtmp_conf_ctx_t **) conf = ctx; + + /* count the number of the rtmp modules and set up their indices */ + +#if (nginx_version >= 1009011) + + ngx_rtmp_max_module = ngx_count_modules(cf->cycle, NGX_RTMP_MODULE); + +#else + + ngx_rtmp_max_module = 0; + for (m = 0; ngx_modules[m]; m++) { + if (ngx_modules[m]->type != NGX_RTMP_MODULE) { + continue; + } + + ngx_modules[m]->ctx_index = ngx_rtmp_max_module++; + } + +#endif + + + /* the rtmp main_conf context, it is the same in the all rtmp contexts */ + + ctx->main_conf = ngx_pcalloc(cf->pool, + sizeof(void *) * ngx_rtmp_max_module); + if (ctx->main_conf == NULL) { + return NGX_CONF_ERROR; + } + + + /* + * the rtmp null srv_conf context, it is used to merge + * the server{}s' srv_conf's + */ + + ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_rtmp_max_module); + if (ctx->srv_conf == NULL) { + return NGX_CONF_ERROR; + } + + + /* + * the rtmp null app_conf context, it is used to merge + * the server{}s' app_conf's + */ + + ctx->app_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_rtmp_max_module); + if (ctx->app_conf == NULL) { + return NGX_CONF_ERROR; + } + + + /* + * create the main_conf's, the null srv_conf's, and the null app_conf's + * of the all rtmp modules + */ + +#if (nginx_version >= 1009011) + modules = cf->cycle->modules; +#else + modules = ngx_modules; +#endif + + for (m = 0; modules[m]; m++) { + if (modules[m]->type != NGX_RTMP_MODULE) { + continue; + } + + module = modules[m]->ctx; + mi = modules[m]->ctx_index; + + if (module->create_main_conf) { + ctx->main_conf[mi] = module->create_main_conf(cf); + if (ctx->main_conf[mi] == NULL) { + return NGX_CONF_ERROR; + } + } + + if (module->create_srv_conf) { + ctx->srv_conf[mi] = module->create_srv_conf(cf); + if (ctx->srv_conf[mi] == NULL) { + return NGX_CONF_ERROR; + } + } + + if (module->create_app_conf) { + ctx->app_conf[mi] = module->create_app_conf(cf); + if (ctx->app_conf[mi] == NULL) { + return NGX_CONF_ERROR; + } + } + } + + pcf = *cf; + cf->ctx = ctx; + + for (m = 0; modules[m]; m++) { + if (modules[m]->type != NGX_RTMP_MODULE) { + continue; + } + + module = modules[m]->ctx; + + if (module->preconfiguration) { + if (module->preconfiguration(cf) != NGX_OK) { + return NGX_CONF_ERROR; + } + } + } + + /* parse inside the rtmp{} block */ + + cf->module_type = NGX_RTMP_MODULE; + cf->cmd_type = NGX_RTMP_MAIN_CONF; + rv = ngx_conf_parse(cf, NULL); + + if (rv != NGX_CONF_OK) { + *cf = pcf; + return rv; + } + + + /* init rtmp{} main_conf's, merge the server{}s' srv_conf's */ + + cmcf = ctx->main_conf[ngx_rtmp_core_module.ctx_index]; + cscfp = cmcf->servers.elts; + + for (m = 0; modules[m]; m++) { + if (modules[m]->type != NGX_RTMP_MODULE) { + continue; + } + + module = modules[m]->ctx; + mi = modules[m]->ctx_index; + + /* init rtmp{} main_conf's */ + + cf->ctx = ctx; + + if (module->init_main_conf) { + rv = module->init_main_conf(cf, ctx->main_conf[mi]); + if (rv != NGX_CONF_OK) { + *cf = pcf; + return rv; + } + } + + for (s = 0; s < cmcf->servers.nelts; s++) { + + /* merge the server{}s' srv_conf's */ + + cf->ctx = cscfp[s]->ctx; + + if (module->merge_srv_conf) { + rv = module->merge_srv_conf(cf, + ctx->srv_conf[mi], + cscfp[s]->ctx->srv_conf[mi]); + if (rv != NGX_CONF_OK) { + *cf = pcf; + return rv; + } + } + + if (module->merge_app_conf) { + + /* merge the server{}'s app_conf */ + + /*ctx->app_conf = cscfp[s]->ctx->app_conf;*/ + + rv = module->merge_app_conf(cf, + ctx->app_conf[mi], + cscfp[s]->ctx->app_conf[mi]); + if (rv != NGX_CONF_OK) { + *cf = pcf; + return rv; + } + + /* merge the applications{}' app_conf's */ + + cscf = cscfp[s]->ctx->srv_conf[ngx_rtmp_core_module.ctx_index]; + + rv = ngx_rtmp_merge_applications(cf, &cscf->applications, + cscfp[s]->ctx->app_conf, + module, mi); + if (rv != NGX_CONF_OK) { + *cf = pcf; + return rv; + } + } + + } + } + + cf->ctx = ctx; + + if (ngx_rtmp_init_events(cf, cmcf) != NGX_OK) { + return NGX_CONF_ERROR; + } + + for (m = 0; modules[m]; m++) { + if (modules[m]->type != NGX_RTMP_MODULE) { + continue; + } + + module = modules[m]->ctx; + + if (module->postconfiguration) { + if (module->postconfiguration(cf) != NGX_OK) { + return NGX_CONF_ERROR; + } + } + } + + *cf = pcf; + + cscfp = cmcf->servers.elts; + for (s = 0; s < cmcf->servers.nelts; s++) { + cscfp[s]->index = s; + } + + if (ngx_rtmp_init_event_handlers(cf, cmcf) != NGX_OK) { + return NGX_CONF_ERROR; + } + + if (ngx_rtmp_optimize_servers(cf, cmcf, cmcf->ports) != NGX_OK) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_merge_applications(ngx_conf_t *cf, ngx_array_t *applications, + void **app_conf, ngx_rtmp_module_t *module, ngx_uint_t ctx_index) +{ + char *rv; + ngx_rtmp_conf_ctx_t *ctx, saved; + ngx_rtmp_core_app_conf_t **cacfp; + ngx_uint_t n; + ngx_rtmp_core_app_conf_t *cacf; + + if (applications == NULL) { + return NGX_CONF_OK; + } + + ctx = (ngx_rtmp_conf_ctx_t *) cf->ctx; + saved = *ctx; + + cacfp = applications->elts; + for (n = 0; n < applications->nelts; ++n, ++cacfp) { + + ctx->app_conf = (*cacfp)->app_conf; + + rv = module->merge_app_conf(cf, app_conf[ctx_index], + (*cacfp)->app_conf[ctx_index]); + if (rv != NGX_CONF_OK) { + return rv; + } + + cacf = (*cacfp)->app_conf[ngx_rtmp_core_module.ctx_index]; + rv = ngx_rtmp_merge_applications(cf, &cacf->applications, + (*cacfp)->app_conf, + module, ctx_index); + if (rv != NGX_CONF_OK) { + return rv; + } + } + + *ctx = saved; + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_init_events(ngx_conf_t *cf, ngx_rtmp_core_main_conf_t *cmcf) +{ + size_t n; + + for(n = 0; n < NGX_RTMP_MAX_EVENT; ++n) { + if (ngx_array_init(&cmcf->events[n], cf->pool, 1, + sizeof(ngx_rtmp_handler_pt)) != NGX_OK) + { + return NGX_ERROR; + } + } + + if (ngx_array_init(&cmcf->amf, cf->pool, 1, + sizeof(ngx_rtmp_amf_handler_t)) != NGX_OK) + { + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_init_event_handlers(ngx_conf_t *cf, ngx_rtmp_core_main_conf_t *cmcf) +{ + ngx_hash_init_t calls_hash; + ngx_rtmp_handler_pt *eh; + ngx_rtmp_amf_handler_t *h; + ngx_hash_key_t *ha; + size_t n, m; + + static size_t pm_events[] = { + NGX_RTMP_MSG_CHUNK_SIZE, + NGX_RTMP_MSG_ABORT, + NGX_RTMP_MSG_ACK, + NGX_RTMP_MSG_ACK_SIZE, + NGX_RTMP_MSG_BANDWIDTH + }; + + static size_t amf_events[] = { + NGX_RTMP_MSG_AMF_CMD, + NGX_RTMP_MSG_AMF_META, + NGX_RTMP_MSG_AMF_SHARED, + NGX_RTMP_MSG_AMF3_CMD, + NGX_RTMP_MSG_AMF3_META, + NGX_RTMP_MSG_AMF3_SHARED + }; + + /* init standard protocol events */ + for(n = 0; n < sizeof(pm_events) / sizeof(pm_events[0]); ++n) { + eh = ngx_array_push(&cmcf->events[pm_events[n]]); + *eh = ngx_rtmp_protocol_message_handler; + } + + /* init amf events */ + for(n = 0; n < sizeof(amf_events) / sizeof(amf_events[0]); ++n) { + eh = ngx_array_push(&cmcf->events[amf_events[n]]); + *eh = ngx_rtmp_amf_message_handler; + } + + /* init user protocol events */ + eh = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_USER]); + *eh = ngx_rtmp_user_message_handler; + + /* aggregate to audio/video map */ + eh = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AGGREGATE]); + *eh = ngx_rtmp_aggregate_message_handler; + + /* init amf callbacks */ + if (ngx_array_init(&cmcf->amf_arrays, cf->pool, + 1, sizeof(ngx_hash_key_t)) != NGX_OK) + { + return NGX_ERROR; + } + + h = cmcf->amf.elts; + for(n = 0; n < cmcf->amf.nelts; ++n, ++h) { + ha = cmcf->amf_arrays.elts; + for(m = 0; m < cmcf->amf_arrays.nelts; ++m, ++ha) { + if (h->name.len == ha->key.len + && !ngx_strncmp(h->name.data, ha->key.data, ha->key.len)) + { + break; + } + } + if (m == cmcf->amf_arrays.nelts) { + ha = ngx_array_push(&cmcf->amf_arrays); + ha->key = h->name; + ha->key_hash = ngx_hash_key_lc(ha->key.data, ha->key.len); + ha->value = ngx_array_create(cf->pool, 1, + sizeof(ngx_rtmp_handler_pt)); + if (ha->value == NULL) { + return NGX_ERROR; + } + } + + eh = ngx_array_push((ngx_array_t*)ha->value); + *eh = h->handler; + } + + calls_hash.hash = &cmcf->amf_hash; + calls_hash.key = ngx_hash_key_lc; + calls_hash.max_size = 512; + calls_hash.bucket_size = ngx_cacheline_size; + calls_hash.name = "amf_hash"; + calls_hash.pool = cf->pool; + calls_hash.temp_pool = NULL; + + if (ngx_hash_init(&calls_hash, cmcf->amf_arrays.elts, cmcf->amf_arrays.nelts) + != NGX_OK) + { + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_optimize_servers(ngx_conf_t *cf, ngx_rtmp_core_main_conf_t *cmcf, + ngx_array_t *ports) +{ + ngx_uint_t p, a; + ngx_rtmp_conf_port_t *port; + ngx_rtmp_conf_addr_t *addr; + + if (ports == NULL) { + return NGX_OK; + } + + port = ports->elts; + for (p = 0; p < ports->nelts; p++) { + + ngx_sort(port[p].addrs.elts, (size_t) port[p].addrs.nelts, + sizeof(ngx_rtmp_conf_addr_t), ngx_rtmp_cmp_conf_addrs); + + /* + * check whether all name-based servers have the same + * configuration as a default server for given address:port + */ + + addr = port[p].addrs.elts; + for (a = 0; a < port[p].addrs.nelts; a++) { + + if (addr[a].servers.nelts > 1 +#if (NGX_PCRE) + || addr[a].default_server->captures +#endif + ) + { + if (ngx_rtmp_server_names(cf, cmcf, &addr[a]) != NGX_OK) { + return NGX_ERROR; + } + } + } + + if (ngx_rtmp_init_listening(cf, &port[p]) != NGX_OK) { + return NGX_ERROR; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_server_names(ngx_conf_t *cf, ngx_rtmp_core_main_conf_t *cmcf, + ngx_rtmp_conf_addr_t *addr) +{ + ngx_int_t rc; + ngx_uint_t n, s; + ngx_hash_init_t hash; + ngx_hash_keys_arrays_t ha; + ngx_rtmp_server_name_t *name; + ngx_rtmp_core_srv_conf_t **cscfp; +#if (NGX_PCRE) + ngx_uint_t regex, i; + + regex = 0; +#endif + + ngx_memzero(&ha, sizeof(ngx_hash_keys_arrays_t)); + + ha.temp_pool = ngx_create_pool(NGX_DEFAULT_POOL_SIZE, cf->log); + if (ha.temp_pool == NULL) { + return NGX_ERROR; + } + + ha.pool = cf->pool; + + if (ngx_hash_keys_array_init(&ha, NGX_HASH_LARGE) != NGX_OK) { + goto failed; + } + + cscfp = addr->servers.elts; + + for (s = 0; s < addr->servers.nelts; s++) { + + name = cscfp[s]->server_names.elts; + + for (n = 0; n < cscfp[s]->server_names.nelts; n++) { + +#if (NGX_PCRE) + if (name[n].regex) { + regex++; + continue; + } +#endif + + rc = ngx_hash_add_key(&ha, &name[n].name, name[n].server, + NGX_HASH_WILDCARD_KEY); + + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + if (rc == NGX_DECLINED) { + ngx_log_error(NGX_LOG_EMERG, cf->log, 0, + "invalid server name or wildcard \"%V\" on %s", + &name[n].name, addr->opt.addr); + return NGX_ERROR; + } + + if (rc == NGX_BUSY) { + ngx_log_error(NGX_LOG_WARN, cf->log, 0, + "conflicting server name \"%V\" on %s, ignored", + &name[n].name, addr->opt.addr); + } + } + } + + hash.key = ngx_hash_key_lc; + hash.max_size = cmcf->server_names_hash_max_size; + hash.bucket_size = cmcf->server_names_hash_bucket_size; + hash.name = "server_names_hash"; + hash.pool = cf->pool; + + if (ha.keys.nelts) { + hash.hash = &addr->hash; + hash.temp_pool = NULL; + + if (ngx_hash_init(&hash, ha.keys.elts, ha.keys.nelts) != NGX_OK) { + goto failed; + } + } + + if (ha.dns_wc_head.nelts) { + + ngx_qsort(ha.dns_wc_head.elts, (size_t) ha.dns_wc_head.nelts, + sizeof(ngx_hash_key_t), ngx_rtmp_cmp_dns_wildcards); + + hash.hash = NULL; + hash.temp_pool = ha.temp_pool; + + if (ngx_hash_wildcard_init(&hash, ha.dns_wc_head.elts, + ha.dns_wc_head.nelts) + != NGX_OK) + { + goto failed; + } + + addr->wc_head = (ngx_hash_wildcard_t *) hash.hash; + } + + if (ha.dns_wc_tail.nelts) { + + ngx_qsort(ha.dns_wc_tail.elts, (size_t) ha.dns_wc_tail.nelts, + sizeof(ngx_hash_key_t), ngx_rtmp_cmp_dns_wildcards); + + hash.hash = NULL; + hash.temp_pool = ha.temp_pool; + + if (ngx_hash_wildcard_init(&hash, ha.dns_wc_tail.elts, + ha.dns_wc_tail.nelts) + != NGX_OK) + { + goto failed; + } + + addr->wc_tail = (ngx_hash_wildcard_t *) hash.hash; + } + + ngx_destroy_pool(ha.temp_pool); + +#if (NGX_PCRE) + + if (regex == 0) { + return NGX_OK; + } + + addr->nregex = regex; + addr->regex = ngx_palloc(cf->pool, regex * sizeof(ngx_rtmp_server_name_t)); + if (addr->regex == NULL) { + return NGX_ERROR; + } + + i = 0; + + for (s = 0; s < addr->servers.nelts; s++) { + + name = cscfp[s]->server_names.elts; + + for (n = 0; n < cscfp[s]->server_names.nelts; n++) { + if (name[n].regex) { + addr->regex[i++] = name[n]; + } + } + } + +#endif + + return NGX_OK; + +failed: + + ngx_destroy_pool(ha.temp_pool); + + return NGX_ERROR; +} + + +static int ngx_libc_cdecl +ngx_rtmp_cmp_dns_wildcards(const void *one, const void *two) +{ + ngx_hash_key_t *first, *second; + + first = (ngx_hash_key_t *) one; + second = (ngx_hash_key_t *) two; + + return ngx_dns_strcmp(first->key.data, second->key.data); +} + + +static ngx_int_t +ngx_rtmp_init_listening(ngx_conf_t *cf, ngx_rtmp_conf_port_t *port) +{ + ngx_uint_t i, last, bind_wildcard; + ngx_listening_t *ls; + ngx_rtmp_port_t *rport; + ngx_rtmp_conf_addr_t *addr; + + addr = port->addrs.elts; + last = port->addrs.nelts; + + /* + * If there is a binding to an "*:port" then we need to bind() to + * the "*:port" only and ignore other implicit bindings. The bindings + * have been already sorted: explicit bindings are on the start, then + * implicit bindings go, and wildcard binding is in the end. + */ + + if (addr[last - 1].opt.wildcard) { + addr[last - 1].opt.bind = 1; + bind_wildcard = 1; + + } else { + bind_wildcard = 0; + } + + i = 0; + + while (i < last) { + + if (bind_wildcard && !addr[i].opt.bind) { + i++; + continue; + } + + ls = ngx_rtmp_add_listening(cf, &addr[i]); + if (ls == NULL) { + return NGX_ERROR; + } + + rport = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_port_t)); + if (rport == NULL) { + return NGX_ERROR; + } + + /* used in ngx_rtmp_init_connection */ + ls->servers = rport; + + rport->naddrs = i + 1; + + switch (ls->sockaddr->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + if (ngx_rtmp_add_addrs6(cf, rport, addr) != NGX_OK) { + return NGX_ERROR; + } + break; +#endif + default: /* AF_INET */ + if (ngx_rtmp_add_addrs(cf, rport, addr) != NGX_OK) { + return NGX_ERROR; + } + break; + } + +#if (nginx_version >= 1009001 && nginx_version <= 1015001) + if (ngx_clone_listening(cf, ls) != NGX_OK) { + return NGX_ERROR; + } +#endif + + addr++; + last--; + } + + return NGX_OK; +} + + +static ngx_listening_t * +ngx_rtmp_add_listening(ngx_conf_t *cf, ngx_rtmp_conf_addr_t *addr) +{ + ngx_listening_t *ls; + ngx_rtmp_core_srv_conf_t *cscf; + + ls = ngx_create_listening(cf, &addr->opt.sockaddr.sockaddr, + addr->opt.socklen); + if (ls == NULL) { + return NULL; + } + + ls->addr_ntop = 1; + + ls->handler = ngx_rtmp_init_connection; + + cscf = addr->default_server; + ls->pool_size = cscf->connection_pool_size; + + ls->logp = &cf->cycle->new_log; + ls->log.data = &ls->addr_text; + ls->log.handler = ngx_accept_log_error; + +#if (NGX_WIN32) + { + ngx_iocp_conf_t *iocpcf = NULL; + + if (ngx_get_conf(cf->cycle->conf_ctx, ngx_events_module)) { + iocpcf = ngx_event_get_conf(cf->cycle->conf_ctx, ngx_iocp_module); + } + if (iocpcf && iocpcf->acceptex_read) { + ls->post_accept_buffer_size = 1024; + } + } +#endif + + ls->backlog = addr->opt.backlog; + ls->rcvbuf = addr->opt.rcvbuf; + ls->sndbuf = addr->opt.sndbuf; + + ls->keepalive = addr->opt.so_keepalive; +#if (NGX_HAVE_KEEPALIVE_TUNABLE) + ls->keepidle = addr->opt.tcp_keepidle; + ls->keepintvl = addr->opt.tcp_keepintvl; + ls->keepcnt = addr->opt.tcp_keepcnt; +#endif + +#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER) + ls->accept_filter = addr->opt.accept_filter; +#endif + +#if (NGX_HAVE_DEFERRED_ACCEPT && defined TCP_DEFER_ACCEPT) + ls->deferred_accept = addr->opt.deferred_accept; +#endif + +#if (NGX_HAVE_INET6) + ls->ipv6only = addr->opt.ipv6only; +#endif + +#if (NGX_HAVE_SETFIB) + ls->setfib = addr->opt.setfib; +#endif + +#if (NGX_HAVE_TCP_FASTOPEN) + ls->fastopen = addr->opt.fastopen; +#endif + +#if (NGX_HAVE_REUSEPORT) + ls->reuseport = addr->opt.reuseport; +#endif + + return ls; +} + + +static ngx_int_t +ngx_rtmp_add_addrs(ngx_conf_t *cf, ngx_rtmp_port_t *mport, + ngx_rtmp_conf_addr_t *addr) +{ + ngx_uint_t i; + ngx_rtmp_in_addr_t *addrs; + struct sockaddr_in *sin; + ngx_rtmp_virtual_names_t *vn; + + u_char *p, buf[NGX_SOCKADDR_STRLEN]; + size_t len; + + mport->addrs = ngx_pcalloc(cf->pool, + mport->naddrs * sizeof(ngx_rtmp_in_addr_t)); + if (mport->addrs == NULL) { + return NGX_ERROR; + } + + addrs = mport->addrs; + + for (i = 0; i < mport->naddrs; i++) { + + sin = &addr[i].opt.sockaddr.sockaddr_in; + addrs[i].addr = sin->sin_addr.s_addr; + addrs[i].conf.default_server = addr[i].default_server; + addrs[i].conf.proxy_protocol = addr[i].opt.proxy_protocol; + + len = ngx_sock_ntop(&addr[i].opt.sockaddr.sockaddr, +#if (nginx_version >= 1005003) + addr[i].opt.socklen, +#endif + buf, NGX_SOCKADDR_STRLEN, 1); + + p = ngx_pcalloc(cf->pool, len); + if (p == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(p, buf, len); + addrs[i].conf.addr_text.len = len; + addrs[i].conf.addr_text.data = p; + + if (addr[i].hash.buckets == NULL + && (addr[i].wc_head == NULL + || addr[i].wc_head->hash.buckets == NULL) + && (addr[i].wc_tail == NULL + || addr[i].wc_tail->hash.buckets == NULL) +#if (NGX_PCRE) + && addr[i].nregex == 0 +#endif + ) + { + continue; + } + + vn = ngx_palloc(cf->pool, sizeof(ngx_rtmp_virtual_names_t)); + if (vn == NULL) { + return NGX_ERROR; + } + + addrs[i].conf.virtual_names = vn; + + vn->names.hash = addr[i].hash; + vn->names.wc_head = addr[i].wc_head; + vn->names.wc_tail = addr[i].wc_tail; +#if (NGX_PCRE) + vn->nregex = addr[i].nregex; + vn->regex = addr[i].regex; +#endif + } + + return NGX_OK; +} + + +#if (NGX_HAVE_INET6) + +static ngx_int_t +ngx_rtmp_add_addrs6(ngx_conf_t *cf, ngx_rtmp_port_t *mport, + ngx_rtmp_conf_addr_t *addr) +{ + ngx_uint_t i; + ngx_rtmp_in6_addr_t *addrs6; + struct sockaddr_in6 *sin6; + ngx_rtmp_virtual_names_t *vn; + + u_char *p, buf[NGX_SOCKADDR_STRLEN]; + size_t len; + + mport->addrs = ngx_pcalloc(cf->pool, + mport->naddrs * sizeof(ngx_rtmp_in6_addr_t)); + if (mport->addrs == NULL) { + return NGX_ERROR; + } + + addrs6 = mport->addrs; + + for (i = 0; i < mport->naddrs; i++) { + + sin6 = &addr[i].opt.sockaddr.sockaddr_in6; + addrs6[i].addr6 = sin6->sin6_addr; + addrs6[i].conf.default_server = addr[i].default_server; + addrs6[i].conf.proxy_protocol = addr[i].opt.proxy_protocol; + + len = ngx_sock_ntop(&addr[i].opt.sockaddr.sockaddr, +#if (nginx_version >= 1005003) + addr[i].opt.socklen, +#endif + buf, NGX_SOCKADDR_STRLEN, 1); + + p = ngx_pcalloc(cf->pool, len); + if (p == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(p, buf, len); + addrs6[i].conf.addr_text.len = len; + addrs6[i].conf.addr_text.data = p; + + if (addr[i].hash.buckets == NULL + && (addr[i].wc_head == NULL + || addr[i].wc_head->hash.buckets == NULL) + && (addr[i].wc_tail == NULL + || addr[i].wc_tail->hash.buckets == NULL) +#if (NGX_PCRE) + && addr[i].nregex == 0 +#endif + ) + { + continue; + } + + vn = ngx_palloc(cf->pool, sizeof(ngx_rtmp_virtual_names_t)); + if (vn == NULL) { + return NGX_ERROR; + } + + addrs6[i].conf.virtual_names = vn; + + vn->names.hash = addr[i].hash; + vn->names.wc_head = addr[i].wc_head; + vn->names.wc_tail = addr[i].wc_tail; +#if (NGX_PCRE) + vn->nregex = addr[i].nregex; + vn->regex = addr[i].regex; +#endif + } + + return NGX_OK; +} + +#endif + + +static ngx_int_t +ngx_rtmp_cmp_conf_addrs(const void *one, const void *two) +{ + ngx_rtmp_conf_addr_t *first, *second; + + first = (ngx_rtmp_conf_addr_t *) one; + second = (ngx_rtmp_conf_addr_t *) two; + + if (first->opt.wildcard) { + /* a wildcard address must be the last resort, shift it to the end */ + return 1; + } + + if (second->opt.wildcard) { + /* a wildcard address must be the last resort, shift it to the end */ + return -1; + } + + if (first->opt.bind && !second->opt.bind) { + /* shift explicit bind()ed addresses to the start */ + return -1; + } + + if (!first->opt.bind && second->opt.bind) { + /* shift explicit bind()ed addresses to the start */ + return 1; + } + + /* do not sort by default */ + + return 0; +} + + +ngx_int_t +ngx_rtmp_fire_event(ngx_rtmp_session_t *s, ngx_uint_t evt, + ngx_rtmp_header_t *h, ngx_chain_t *in) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_array_t *ch; + ngx_rtmp_handler_pt *hh; + size_t n; + + cmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_core_module); + + ch = &cmcf->events[evt]; + hh = ch->elts; + for(n = 0; n < ch->nelts; ++n, ++hh) { + if (*hh && (*hh)(s, h, in) != NGX_OK) { + return NGX_ERROR; + } + } + return NGX_OK; +} + + +void * +ngx_rtmp_rmemcpy(void *dst, const void* src, size_t n) +{ + u_char *d, *s; + + d = dst; + s = (u_char*)src + n - 1; + + while(s >= (u_char*)src) { + *d++ = *s--; + } + + return dst; +} + + +u_char * +ngx_rtmp_h4_to_n3(u_char *dst, uint32_t h) +{ + dst[0] = (u_char) (h >> 16); + dst[1] = (u_char) (h >> 8); + dst[2] = (u_char) h; + + return dst; +} + + +uint32_t +ngx_rtmp_n3_to_h4(u_char *n) +{ + return ((uint32_t) n[0] << 16) | ((uint32_t) n[1] << 8) | (uint32_t) n[2]; +} + + +static ngx_int_t +ngx_rtmp_init_process(ngx_cycle_t *cycle) +{ +#if (nginx_version >= 1007005) + ngx_queue_init(&ngx_rtmp_init_queue); +#endif + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_process_virtual_host(ngx_rtmp_session_t *s) +{ + u_char *p; + ngx_int_t rc; + ngx_str_t host; + ngx_str_t hschema, rschema, *schema; + + if (s->auto_pushed) { + goto next; + } + + hschema.data = (u_char *)"http://"; + hschema.len = ngx_strlen(hschema.data); + + rschema.data = (u_char *) "rtmp://"; + rschema.len = ngx_strlen(rschema.data); + + do { + schema = &hschema; + + if (s->tc_url.len > schema->len + && ngx_strncasecmp(s->tc_url.data, schema->data, schema->len) == 0) + { + break; + } + + schema = &rschema; + + if (s->tc_url.len > schema->len + && ngx_strncasecmp(s->tc_url.data, schema->data, schema->len) == 0) + { + break; + } + + return NGX_ERROR; + } while (0); + + s->host_start = s->tc_url.data + schema->len; + + p = ngx_strlchr(s->host_start, s->tc_url.data + s->tc_url.len, ':'); + if (p) { + s->host_end = p; + } else { + p = ngx_strlchr(s->host_start, s->tc_url.data + s->tc_url.len, '/'); + s->host_end = p ? p : (s->host_start + s->tc_url.len - schema->len); + } + +next: + host.len = s->host_end - s->host_start; + host.data = s->host_start; + + rc = ngx_rtmp_validate_host(&host, s->connection->pool, 0); + + if (rc == NGX_DECLINED) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "client send invalid host in request line"); + return NGX_ERROR; + } + +#if 0 + /* TODO: send error details to client */ + if (rc == NGX_ERROR) { + return NGX_ERROR; + } +#endif + + if (ngx_rtmp_set_virtual_server(s, &host) == NGX_ERROR) { + return NGX_ERROR; + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_validate_host(ngx_str_t *host, ngx_pool_t *pool, ngx_uint_t alloc) +{ + u_char *h, ch; + size_t i, dot_pos, host_len; + + enum { + sw_usual = 0, + sw_literal, + sw_rest + } state; + + dot_pos = host->len; + host_len = host->len; + + h = host->data; + + state = sw_usual; + + for (i = 0; i < host->len; i++) { + ch = h[i]; + + switch (ch) { + + case '.': + if (dot_pos == i - 1) { + return NGX_DECLINED; + } + dot_pos = i; + break; + + case ':': + if (state == sw_usual) { + host_len = i; + state = sw_rest; + } + break; + + case '[': + if (i == 0) { + state = sw_literal; + } + break; + + case ']': + if (state == sw_literal) { + host_len = i + 1; + state = sw_rest; + } + break; + + case '\0': + return NGX_DECLINED; + + default: + + if (ngx_path_separator(ch)) { + return NGX_DECLINED; + } + + if (ch >= 'A' && ch <= 'Z') { + alloc = 1; + } + + break; + } + } + + if (dot_pos == host_len - 1) { + host_len--; + } + + if (host_len == 0) { + return NGX_DECLINED; + } + + if (alloc) { + host->data = ngx_pnalloc(pool, host_len); + if (host->data == NULL) { + return NGX_ERROR; + } + + ngx_strlow(host->data, h, host_len); + } + + host->len = host_len; + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_set_virtual_server(ngx_rtmp_session_t *s, ngx_str_t *host) +{ + ngx_int_t rc; + ngx_int_t i; + ngx_rtmp_connection_t *rconn; + ngx_rtmp_core_srv_conf_t *cscf, *dcscf; + ngx_rtmp_stream_t *in_streams; + +#if (NGX_SUPPRESS_WARN) + cscf = NULL; +#endif + + rconn = s->rtmp_connection; + + rc = ngx_rtmp_find_virtual_server(s->connection, + rconn->addr_conf->virtual_names, + host, s, &cscf); + + if (rc == NGX_ERROR) { + ngx_rtmp_finalize_session(s); + return NGX_ERROR; + } + + if (rc == NGX_DECLINED) { + return NGX_OK; + } + + dcscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + /* reinitialization */ + s->server_changed = 1; + s->srv_conf = cscf->ctx->srv_conf; + + if (dcscf->out_queue != cscf->out_queue) { + /* use new pool */ + s->out_temp_pool = ngx_create_pool(4096, s->connection->log); + if (s->out_temp_pool == NULL) { + ngx_rtmp_finalize_session(s); + return NGX_ERROR; + } + + /* save memory */ + ngx_destroy_pool(s->out_pool); + s->out_pool = s->out_temp_pool; + + /* send not used yet, need not copy data */ + s->out = ngx_pcalloc(s->out_pool, sizeof(ngx_chain_t *) + * ((ngx_rtmp_core_srv_conf_t *) + cscf->ctx->srv_conf[ngx_rtmp_core_module + .ctx_index])->out_queue); + if (s->out == NULL) { + ngx_rtmp_finalize_session(s); + return NGX_ERROR; + } + + s->out_queue = cscf->out_queue; + } + + if (dcscf->max_streams != cscf->max_streams) { + /* use new pool */ + s->in_streams_temp_pool = ngx_create_pool(4096, s->connection->log); + if (s->in_streams_temp_pool == NULL) { + ngx_rtmp_finalize_session(s); + return NGX_ERROR; + } + + in_streams = ngx_pcalloc(s->in_streams_temp_pool, + sizeof(ngx_rtmp_stream_t) * cscf->max_streams); + if (in_streams == NULL) { + ngx_rtmp_finalize_session(s); + return NGX_ERROR; + } + + /* copy data from s->in_streams to in_streams */ + ngx_memmove(in_streams, s->in_streams, sizeof(ngx_rtmp_stream_t) + * ngx_min(dcscf->max_streams, cscf->max_streams)); + + if (dcscf->max_streams > cscf->max_streams) { + for (i = cscf->max_streams; i < dcscf->max_streams; i++) { + if (s->in_streams[i].hdr.csid) { + ngx_rtmp_finalize_session(s); + return NGX_ERROR; + } + } + } + + s->in_streams = in_streams; + + /* save memory */ + ngx_destroy_pool(s->in_streams_pool); + s->in_streams_pool = s->in_streams_temp_pool; + } + + s->out_cork = cscf->out_cork; + s->timeout = cscf->timeout; + s->buflen = cscf->buflen; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_find_virtual_server(ngx_connection_t *c, + ngx_rtmp_virtual_names_t *virtual_names, ngx_str_t *host, + ngx_rtmp_session_t *s, ngx_rtmp_core_srv_conf_t **cscfp) +{ + ngx_rtmp_core_srv_conf_t *cscf; + + if (virtual_names == NULL) { + return NGX_DECLINED; + } + + cscf = ngx_hash_find_combined(&virtual_names->names, + ngx_hash_key(host->data, host->len), + host->data, host->len); + + if (cscf) { + *cscfp = cscf; + return NGX_OK; + } + +#if (NGX_PCRE) + + if (host->len && virtual_names->nregex) { + ngx_int_t n; + ngx_uint_t i; + ngx_rtmp_server_name_t *sn; + + sn = virtual_names->regex; + + for (i = 0; i < virtual_names->nregex; i++) { + + n = ngx_rtmp_regex_exec(s, sn[i].regex, host); + + if (n == NGX_DECLINED) { + continue; + } + + if (n == NGX_OK) { + *cscfp = sn[i].server; + return NGX_OK; + } + + return NGX_ERROR; + } + } + +#endif /* NGX_PCRE */ + + return NGX_DECLINED; +} + + +#if (nginx_version <= 1011001) +in_port_t +ngx_inet_get_port(struct sockaddr *sa) +{ + struct sockaddr_in *sin; +#if (NGX_HAVE_INET6) + struct sockaddr_in6 *sin6; +#endif + + switch (sa->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + sin6 = (struct sockaddr_in6 *) sa; + return ntohs(sin6->sin6_port); +#endif + +#if (NGX_HAVE_UNIX_DOMAIN) + case AF_UNIX: + return 0; +#endif + + default: /* AF_INET */ + sin = (struct sockaddr_in *) sa; + return ntohs(sin->sin_port); + } +} + + +void +ngx_inet_set_port(struct sockaddr *sa, in_port_t port) +{ + struct sockaddr_in *sin; +#if (NGX_HAVE_INET6) + struct sockaddr_in6 *sin6; +#endif + + switch (sa->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + sin6 = (struct sockaddr_in6 *) sa; + sin6->sin6_port = htons(port); + break; +#endif + +#if (NGX_HAVE_UNIX_DOMAIN) + case AF_UNIX: + break; +#endif + + default: /* AF_INET */ + sin = (struct sockaddr_in *) sa; + sin->sin_port = htons(port); + break; + } +} +#endif diff --git a/ngx_http_flv_module/ngx_rtmp.h b/ngx_http_flv_module/ngx_rtmp.h new file mode 100644 index 0000000..d849a6e --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp.h @@ -0,0 +1,850 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#ifndef _NGX_RTMP_H_INCLUDED_ +#define _NGX_RTMP_H_INCLUDED_ + + +#include +#include +#include +#include +#include + +#include "ngx_rtmp_amf.h" +#include "ngx_rtmp_bandwidth.h" + + +typedef struct ngx_rtmp_core_srv_conf_s ngx_rtmp_core_srv_conf_t; +typedef struct ngx_rtmp_session_s ngx_rtmp_session_t; +typedef struct ngx_rtmp_virtual_names_s ngx_rtmp_virtual_names_t; + + +#include "ngx_rtmp_variables.h" + + +#if (NGX_WIN32) +typedef __int8 int8_t; +typedef unsigned __int8 uint8_t; +#endif + + +typedef struct { + void **main_conf; + void **srv_conf; + void **app_conf; +} ngx_rtmp_conf_ctx_t; + + +typedef struct { + ngx_str_t addr_text; + + /* the default server configuration for this address:port */ + ngx_rtmp_core_srv_conf_t *default_server; + + ngx_rtmp_virtual_names_t *virtual_names; + + unsigned proxy_protocol:1; +} ngx_rtmp_addr_conf_t; + +typedef struct { + in_addr_t addr; + ngx_rtmp_addr_conf_t conf; +} ngx_rtmp_in_addr_t; + + +#if (NGX_HAVE_INET6) + +typedef struct { + struct in6_addr addr6; + ngx_rtmp_addr_conf_t conf; +} ngx_rtmp_in6_addr_t; + +#endif + + +typedef struct { + /* ngx_rtmp_in_addr_t or ngx_rtmp_in_addr6_t */ + void *addrs; + ngx_uint_t naddrs; +} ngx_rtmp_port_t; + + +typedef struct { + int family; + in_port_t port; + ngx_array_t addrs; /* array of ngx_rtmp_conf_addr_t */ +} ngx_rtmp_conf_port_t; + + +#if (nginx_version <= 1010003) +typedef union { + struct sockaddr sockaddr; + struct sockaddr_in sockaddr_in; +#if (NGX_HAVE_INET6) + struct sockaddr_in6 sockaddr_in6; +#endif +#if (NGX_HAVE_UNIX_DOMAIN) + struct sockaddr_un sockaddr_un; +#endif +} ngx_sockaddr_t; +#endif + + +typedef struct { + ngx_sockaddr_t sockaddr; + socklen_t socklen; + + unsigned set:1; + unsigned default_server:1; + unsigned bind:1; + unsigned wildcard:1; +#if (NGX_HAVE_INET6) + unsigned ipv6only:1; +#endif + unsigned deferred_accept:1; + unsigned reuseport:1; + unsigned so_keepalive:2; + unsigned proxy_protocol:1; + + int backlog; + int rcvbuf; + int sndbuf; +#if (NGX_HAVE_SETFIB) + int setfib; +#endif +#if (NGX_HAVE_TCP_FASTOPEN) + int fastopen; +#endif +#if (NGX_HAVE_KEEPALIVE_TUNABLE) + int tcp_keepidle; + int tcp_keepintvl; + int tcp_keepcnt; +#endif + +#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER) + char *accept_filter; +#endif + + u_char addr[NGX_SOCKADDR_STRLEN + 1]; +} ngx_rtmp_listen_opt_t; + + +typedef struct { +#if (NGX_PCRE) + ngx_rtmp_regex_t *regex; +#endif + ngx_rtmp_core_srv_conf_t *server; /* virtual name server conf */ + ngx_str_t name; +} ngx_rtmp_server_name_t; + + +typedef struct { + ngx_rtmp_listen_opt_t opt; + + ngx_hash_t hash; + ngx_hash_wildcard_t *wc_head; + ngx_hash_wildcard_t *wc_tail; + +#if (NGX_PCRE) + ngx_uint_t nregex; + ngx_rtmp_server_name_t *regex; +#endif + + /* the default server configuration for this address:port */ + ngx_rtmp_core_srv_conf_t *default_server; + ngx_array_t servers; /* array of ngx_rtmp_core_srv_conf_t */ +} ngx_rtmp_conf_addr_t; + + +typedef struct { + ngx_rtmp_addr_conf_t *addr_conf; + ngx_rtmp_conf_ctx_t *conf_ctx; + + ngx_buf_t **busy; + ngx_int_t nbusy; + + ngx_buf_t **free; + ngx_int_t nfree; +} ngx_rtmp_connection_t; + + +#define NGX_RTMP_VERSION 3 + +#define NGX_LOG_DEBUG_RTMP NGX_LOG_DEBUG_CORE + +#define NGX_RTMP_DEFAULT_CHUNK_SIZE 128 + + +/* RTMP message types */ +#define NGX_RTMP_MSG_CHUNK_SIZE 1 +#define NGX_RTMP_MSG_ABORT 2 +#define NGX_RTMP_MSG_ACK 3 +#define NGX_RTMP_MSG_USER 4 +#define NGX_RTMP_MSG_ACK_SIZE 5 +#define NGX_RTMP_MSG_BANDWIDTH 6 +#define NGX_RTMP_MSG_EDGE 7 +#define NGX_RTMP_MSG_AUDIO 8 +#define NGX_RTMP_MSG_VIDEO 9 +#define NGX_RTMP_MSG_AMF3_META 15 +#define NGX_RTMP_MSG_AMF3_SHARED 16 +#define NGX_RTMP_MSG_AMF3_CMD 17 +#define NGX_RTMP_MSG_AMF_META 18 +#define NGX_RTMP_MSG_AMF_SHARED 19 +#define NGX_RTMP_MSG_AMF_CMD 20 +#define NGX_RTMP_MSG_AGGREGATE 22 +#define NGX_RTMP_MSG_MAX 22 + +#define NGX_RTMP_MAX_CHUNK_SIZE 10485760 + +#define NGX_RTMP_CONNECT NGX_RTMP_MSG_MAX + 1 +#define NGX_RTMP_DISCONNECT NGX_RTMP_MSG_MAX + 2 +#define NGX_RTMP_HANDSHAKE_DONE NGX_RTMP_MSG_MAX + 3 +#define NGX_HTTP_FLV_LIVE_REQUEST NGX_RTMP_MSG_MAX + 4 +#define NGX_RTMP_MAX_EVENT NGX_RTMP_MSG_MAX + 5 + + +/* RMTP control message types */ +#define NGX_RTMP_USER_STREAM_BEGIN 0 +#define NGX_RTMP_USER_STREAM_EOF 1 +#define NGX_RTMP_USER_STREAM_DRY 2 +#define NGX_RTMP_USER_SET_BUFLEN 3 +#define NGX_RTMP_USER_RECORDED 4 +#define NGX_RTMP_USER_PING_REQUEST 6 +#define NGX_RTMP_USER_PING_RESPONSE 7 +#define NGX_RTMP_USER_UNKNOWN 8 +#define NGX_RTMP_USER_BUFFER_END 31 + + +/* Chunk header: + * max 3 basic header + * + max 11 message header + * + max 4 extended header (timestamp) */ +#define NGX_RTMP_MAX_CHUNK_HEADER 18 + + +enum { + NGX_RTMP_PROTOCOL_RTMP = 0, + NGX_RTMP_PROTOCOL_HTTP +}; + + +#define NGX_RTMP_INTERNAL_SERVER_ERROR 500 + + +typedef struct { + uint32_t csid; /* chunk stream id */ + uint32_t timestamp; /* timestamp (delta) */ + uint32_t mlen; /* message length */ + uint8_t type; /* message type id */ + uint32_t msid; /* message stream id */ +} ngx_rtmp_header_t; + + +typedef struct { + ngx_rtmp_header_t hdr; + uint32_t dtime; + uint32_t len; /* current fragment length */ + uint8_t ext; + ngx_chain_t *in; +} ngx_rtmp_stream_t; + + +/* disable zero-sized array warning by msvc */ + +#if (NGX_WIN32) +#pragma warning(push) +#pragma warning(disable:4200) +#endif + + +struct ngx_rtmp_session_s { + uint32_t signature; /* "RTMP" */ /* <-- FIXME wtf */ + + ngx_int_t port; + ngx_buf_t *request_line; + ngx_str_t uri; + ngx_str_t unparsed_uri; + + time_t start_sec; + ngx_msec_t start_msec; + + ngx_event_t close; + + void **ctx; + void **main_conf; + void **srv_conf; + void **app_conf; + + void *data; + ngx_event_t push_evt; + + ngx_str_t *addr_text; + ngx_flag_t connected; + +#if (nginx_version >= 1007005) + ngx_queue_t posted_dry_events; +#else + ngx_event_t *posted_dry_events; +#endif + + ngx_rtmp_variable_value_t *variables; + + /* client buffer time in msec */ + uint32_t buflen; + uint32_t ack_size; + + /* connection parameters */ + ngx_str_t app; + ngx_str_t stream; + ngx_str_t args; + ngx_str_t flashver; + ngx_str_t swf_url; + ngx_str_t tc_url; + uint32_t acodecs; + uint32_t vcodecs; + ngx_str_t page_url; + + /* handshake data */ + ngx_buf_t *hs_buf; + u_char *hs_digest; + unsigned hs_old:1; + ngx_uint_t hs_stage; + + /* connection timestamps */ + ngx_msec_t epoch; + ngx_msec_t peer_epoch; + ngx_msec_t base_time; + uint32_t current_time; + + /* ping */ + ngx_event_t ping_evt; + unsigned ping_active:1; + unsigned ping_reset:1; + + /* auto-pushed? */ + unsigned auto_pushed:1; + unsigned relay:1; + unsigned static_relay:1; + + /* URI with "/." and on Win32 with "//" */ + unsigned complex_uri:1; + /* URI with "%" */ + unsigned quoted_uri:1; + /* URI with "+" */ + unsigned plus_in_uri:1; + /* URI with " " */ + unsigned space_in_uri:1; + + unsigned offset_timestamp_set:1; + + uint32_t offset_timestamp; + + u_char *uri_start; + u_char *uri_end; + u_char *args_start; + u_char *schema_start; + u_char *schema_end; + u_char *host_start; + u_char *host_end; + u_char *port_start; + u_char *port_end; + + unsigned keepalive:1; + + unsigned valid_unparsed_uri:1; + +#if (NGX_PCRE) + ngx_uint_t ncaptures; + int *captures; + u_char *captures_data; +#endif + + ngx_rtmp_connection_t *rtmp_connection; + + ngx_rtmp_session_t *publisher; + + ngx_pool_t *in_streams_pool; + ngx_pool_t *in_streams_temp_pool; + + ngx_pool_t *out_pool; + ngx_pool_t *out_temp_pool; + + unsigned server_changed:1; + unsigned notify_connect:1; + unsigned notify_play:1; + + /* input stream 0 (reserved by RTMP spec) + * is used as free chain link */ + + ngx_rtmp_stream_t *in_streams; + uint32_t in_csid; + ngx_uint_t in_chunk_size; + ngx_pool_t *in_pool; + uint32_t in_bytes; + uint32_t in_last_ack; + + ngx_pool_t *in_old_pool; + ngx_int_t in_chunk_size_changing; + + ngx_connection_t *connection; + + /* circular buffer of RTMP message pointers */ + ngx_msec_t timeout; + uint32_t out_bytes; + size_t out_pos, out_last; + ngx_chain_t *out_chain; + u_char *out_bpos; + unsigned out_buffer:1; + size_t out_queue; + size_t out_cork; + ngx_chain_t **out; +}; + + +#if (NGX_WIN32) +#pragma warning(pop) +#endif + + +/* handler result code: + * NGX_ERROR - error + * NGX_OK - success, may continue + * NGX_DONE - success, input parsed, reply sent; need no + * more calls on this event */ +typedef ngx_int_t (*ngx_rtmp_handler_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); + + +typedef struct { + ngx_str_t name; + ngx_rtmp_handler_pt handler; +} ngx_rtmp_amf_handler_t; + + +typedef struct { + ngx_array_t servers; /* ngx_rtmp_core_srv_conf_t */ + + ngx_array_t events[NGX_RTMP_MAX_EVENT]; + + ngx_hash_t amf_hash; + ngx_array_t amf_arrays; + ngx_array_t amf; + + ngx_hash_t variables_hash; + + ngx_array_t variables; /* ngx_http_variable_t */ + ngx_array_t prefix_variables; /* ngx_http_variable_t */ + ngx_uint_t ncaptures; + + ngx_uint_t server_names_hash_max_size; + ngx_uint_t server_names_hash_bucket_size; + + ngx_uint_t variables_hash_max_size; + ngx_uint_t variables_hash_bucket_size; + + ngx_hash_keys_arrays_t *variables_keys; + ngx_array_t *ports; /* ngx_rtmp_conf_port_t */ +} ngx_rtmp_core_main_conf_t; + + +/* global main conf for stats */ +extern ngx_rtmp_core_main_conf_t *ngx_rtmp_core_main_conf; + + +struct ngx_rtmp_core_srv_conf_s { + /* array of the ngx_rtmp_server_name_t, "server_name" directive */ + ngx_array_t server_names; + + ngx_array_t applications; /* ngx_rtmp_core_app_conf_t */ + + ngx_uint_t index; /* index in server array */ + + ngx_msec_t timeout; + ngx_msec_t ping; + ngx_msec_t ping_timeout; + ngx_flag_t so_keepalive; + ngx_int_t max_streams; + + ngx_uint_t ack_window; + + ngx_int_t chunk_size; + ngx_pool_t *pool; + ngx_chain_t *free; + ngx_chain_t *free_hs; + 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; + ngx_msec_t buflen; + + ngx_rtmp_conf_ctx_t *ctx; + + ngx_str_t server_name; + + size_t connection_pool_size; + + ngx_flag_t merge_slashes; + + unsigned listen:1; +#if (NGX_PCRE) + unsigned captures:1; +#endif + + in_port_t port; +}; + + +struct ngx_rtmp_virtual_names_s { + ngx_hash_combined_t names; + + ngx_uint_t nregex; + ngx_rtmp_server_name_t *regex; +}; + + +typedef struct { + ngx_array_t applications; /* ngx_rtmp_core_app_conf_t */ + ngx_str_t name; + void **app_conf; + +#if (NGX_PCRE) + ngx_rtmp_regex_t *regex; +#endif + + size_t send_lowat; + + ngx_msec_t send_timeout; + ngx_msec_t resolver_timeout; + + ngx_resolver_t *resolver; + + ngx_flag_t tcp_nopush; + ngx_flag_t tcp_nodelay; +} ngx_rtmp_core_app_conf_t; + + +typedef struct { + ngx_str_t *client; + ngx_rtmp_session_t *session; +} ngx_rtmp_error_log_ctx_t; + + +typedef struct { + ngx_int_t (*preconfiguration)(ngx_conf_t *cf); + ngx_int_t (*postconfiguration)(ngx_conf_t *cf); + + void *(*create_main_conf)(ngx_conf_t *cf); + char *(*init_main_conf)(ngx_conf_t *cf, void *conf); + + void *(*create_srv_conf)(ngx_conf_t *cf); + char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, + void *conf); + + void *(*create_app_conf)(ngx_conf_t *cf); + char *(*merge_app_conf)(ngx_conf_t *cf, void *prev, + void *conf); +} ngx_rtmp_module_t; + +#define NGX_RTMP_MODULE 0x504D5452 /* "RTMP" */ + +#define NGX_RTMP_MAIN_CONF 0x02000000 +#define NGX_RTMP_SRV_CONF 0x04000000 +#define NGX_RTMP_APP_CONF 0x08000000 +#define NGX_RTMP_REC_CONF 0x10000000 +#define NGX_RTMP_UPS_CONF 0x20000000 + +#define NGX_RTMP_MAIN_CONF_OFFSET offsetof(ngx_rtmp_conf_ctx_t, main_conf) +#define NGX_RTMP_SRV_CONF_OFFSET offsetof(ngx_rtmp_conf_ctx_t, srv_conf) +#define NGX_RTMP_APP_CONF_OFFSET offsetof(ngx_rtmp_conf_ctx_t, app_conf) + + +#define ngx_rtmp_get_module_ctx(s, module) (s)->ctx[module.ctx_index] +#define ngx_rtmp_set_ctx(s, c, module) s->ctx[module.ctx_index] = c; +#define ngx_rtmp_delete_ctx(s, module) s->ctx[module.ctx_index] = NULL; + + +#define ngx_rtmp_get_module_main_conf(s, module) \ + (s)->main_conf[module.ctx_index] +#define ngx_rtmp_get_module_srv_conf(s, module) (s)->srv_conf[module.ctx_index] +#define ngx_rtmp_get_module_app_conf(s, module) ((s)->app_conf ? \ + (s)->app_conf[module.ctx_index] : NULL) + +#define ngx_rtmp_conf_get_module_main_conf(cf, module) \ + ((ngx_rtmp_conf_ctx_t *) cf->ctx)->main_conf[module.ctx_index] +#define ngx_rtmp_conf_get_module_srv_conf(cf, module) \ + ((ngx_rtmp_conf_ctx_t *) cf->ctx)->srv_conf[module.ctx_index] +#define ngx_rtmp_conf_get_module_app_conf(cf, module) \ + ((ngx_rtmp_conf_ctx_t *) cf->ctx)->app_conf[module.ctx_index] + + +#ifdef NGX_DEBUG +char *ngx_rtmp_message_type(uint8_t type); +char *ngx_rtmp_user_message_type(uint16_t evt); +#endif + +void ngx_rtmp_init_connection(ngx_connection_t *c); +ngx_rtmp_session_t *ngx_rtmp_init_session(ngx_connection_t *c, + ngx_rtmp_addr_conf_t *addr_conf); +void ngx_rtmp_finalize_session(ngx_rtmp_session_t *s); +void ngx_rtmp_handshake(ngx_rtmp_session_t *s); +void ngx_rtmp_client_handshake(ngx_rtmp_session_t *s, unsigned async); +void ngx_rtmp_free_handshake_buffers(ngx_rtmp_session_t *s); +void ngx_rtmp_cycle(ngx_rtmp_session_t *s); +void ngx_rtmp_reset_ping(ngx_rtmp_session_t *s); + +ngx_chain_t *ngx_rtmp_alloc_in_buf(ngx_rtmp_session_t *s); +ngx_int_t ngx_rtmp_finalize_set_chunk_size(ngx_rtmp_session_t *s); + +ngx_int_t ngx_rtmp_fire_event(ngx_rtmp_session_t *s, ngx_uint_t evt, + ngx_rtmp_header_t *h, ngx_chain_t *in); + + +ngx_int_t ngx_rtmp_set_chunk_size(ngx_rtmp_session_t *s, ngx_uint_t size); + + +/* Bit reverse: we need big-endians in many places */ +void *ngx_rtmp_rmemcpy(void *dst, const void *src, size_t n); +u_char *ngx_rtmp_h4_to_n3(u_char *dst, uint32_t h); +uint32_t ngx_rtmp_n3_to_h4(u_char *n); + + +static ngx_inline uint16_t +ngx_rtmp_r16(uint16_t n) +{ +#if (NGX_HAVE_LITTLE_ENDIAN) + return (n << 8) | (n >> 8); +#else + return n; +#endif +} + + +static ngx_inline uint32_t +ngx_rtmp_r32(uint32_t n) +{ +#if (NGX_HAVE_LITTLE_ENDIAN) + return (n << 24) | ((n << 8) & 0xff0000) | ((n >> 8) & 0xff00) | (n >> 24); +#else + return n; +#endif +} + + +static ngx_inline uint64_t +ngx_rtmp_r64(uint64_t n) +{ +#if (NGX_HAVE_LITTLE_ENDIAN) + return (uint64_t) ngx_rtmp_r32((uint32_t) n) << 32 | + ngx_rtmp_r32((uint32_t) (n >> 32)); +#else + return n; +#endif +} + + +/* Receiving messages */ +ngx_int_t ngx_rtmp_receive_message(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); +ngx_int_t ngx_rtmp_protocol_message_handler(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); +ngx_int_t ngx_rtmp_user_message_handler(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); +ngx_int_t ngx_rtmp_aggregate_message_handler(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); +ngx_int_t ngx_rtmp_amf_message_handler(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); +ngx_int_t ngx_rtmp_amf_shared_object_handler(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); + + +/* Shared output buffers */ + +/* Store refcount in negative bytes of shared buffer */ + +#define NGX_RTMP_REFCOUNT_TYPE uint32_t +#define NGX_RTMP_REFCOUNT_BYTES sizeof(NGX_RTMP_REFCOUNT_TYPE) + +#define ngx_rtmp_ref(b) \ + *((NGX_RTMP_REFCOUNT_TYPE*)(b) - 1) + +#define ngx_rtmp_ref_set(b, v) \ + ngx_rtmp_ref(b) = v + +#define ngx_rtmp_ref_get(b) \ + ++ngx_rtmp_ref(b) + +#define ngx_rtmp_ref_put(b) \ + --ngx_rtmp_ref(b) + +ngx_chain_t *ngx_rtmp_alloc_shared_buf(ngx_rtmp_core_srv_conf_t *cscf); +void ngx_rtmp_free_shared_chain(ngx_rtmp_core_srv_conf_t *cscf, + ngx_chain_t *in); +ngx_chain_t *ngx_rtmp_append_shared_bufs(ngx_rtmp_core_srv_conf_t *cscf, + ngx_chain_t *head, ngx_chain_t *in); + +#define ngx_rtmp_acquire_shared_chain(in) \ + ngx_rtmp_ref_get(in); \ + + +/* Sending messages */ +void ngx_rtmp_prepare_message(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_rtmp_header_t *lh, ngx_chain_t *out); +ngx_int_t ngx_rtmp_send_message(ngx_rtmp_session_t *s, ngx_chain_t *out, + ngx_uint_t priority); + +/* Note on priorities: + * the bigger value the lower the priority. + * priority=0 is the highest */ + + +#define NGX_RTMP_LIMIT_SOFT 0 +#define NGX_RTMP_LIMIT_HARD 1 +#define NGX_RTMP_LIMIT_DYNAMIC 2 + +/* Protocol control messages */ +ngx_chain_t *ngx_rtmp_create_chunk_size(ngx_rtmp_session_t *s, + uint32_t chunk_size); +ngx_chain_t *ngx_rtmp_create_abort(ngx_rtmp_session_t *s, + uint32_t csid); +ngx_chain_t *ngx_rtmp_create_ack(ngx_rtmp_session_t *s, + uint32_t seq); +ngx_chain_t *ngx_rtmp_create_ack_size(ngx_rtmp_session_t *s, + uint32_t ack_size); +ngx_chain_t *ngx_rtmp_create_bandwidth(ngx_rtmp_session_t *s, + uint32_t ack_size, uint8_t limit_type); + +ngx_int_t ngx_rtmp_send_chunk_size(ngx_rtmp_session_t *s, + uint32_t chunk_size); +ngx_int_t ngx_rtmp_send_abort(ngx_rtmp_session_t *s, + uint32_t csid); +ngx_int_t ngx_rtmp_send_ack(ngx_rtmp_session_t *s, + uint32_t seq); +ngx_int_t ngx_rtmp_send_ack_size(ngx_rtmp_session_t *s, + uint32_t ack_size); +ngx_int_t ngx_rtmp_send_bandwidth(ngx_rtmp_session_t *s, + uint32_t ack_size, uint8_t limit_type); + +/* User control messages */ +ngx_chain_t *ngx_rtmp_create_stream_begin(ngx_rtmp_session_t *s, + uint32_t msid); +ngx_chain_t *ngx_rtmp_create_stream_eof(ngx_rtmp_session_t *s, + uint32_t msid); +ngx_chain_t *ngx_rtmp_create_stream_dry(ngx_rtmp_session_t *s, + uint32_t msid); +ngx_chain_t *ngx_rtmp_create_set_buflen(ngx_rtmp_session_t *s, + uint32_t msid, uint32_t buflen_msec); +ngx_chain_t *ngx_rtmp_create_recorded(ngx_rtmp_session_t *s, + uint32_t msid); +ngx_chain_t *ngx_rtmp_create_ping_request(ngx_rtmp_session_t *s, + uint32_t timestamp); +ngx_chain_t *ngx_rtmp_create_ping_response(ngx_rtmp_session_t *s, + uint32_t timestamp); + +ngx_int_t ngx_rtmp_send_stream_begin(ngx_rtmp_session_t *s, + uint32_t msid); +ngx_int_t ngx_rtmp_send_stream_eof(ngx_rtmp_session_t *s, + uint32_t msid); +ngx_int_t ngx_rtmp_send_stream_dry(ngx_rtmp_session_t *s, + uint32_t msid); +ngx_int_t ngx_rtmp_send_set_buflen(ngx_rtmp_session_t *s, + uint32_t msid, uint32_t buflen_msec); +ngx_int_t ngx_rtmp_send_recorded(ngx_rtmp_session_t *s, + uint32_t msid); +ngx_int_t ngx_rtmp_send_ping_request(ngx_rtmp_session_t *s, + uint32_t timestamp); +ngx_int_t ngx_rtmp_send_ping_response(ngx_rtmp_session_t *s, + uint32_t timestamp); + +/* AMF sender/receiver */ +ngx_int_t ngx_rtmp_append_amf(ngx_rtmp_session_t *s, + ngx_chain_t **first, ngx_chain_t **last, + ngx_rtmp_amf_elt_t *elts, size_t nelts); +ngx_int_t ngx_rtmp_receive_amf(ngx_rtmp_session_t *s, ngx_chain_t *in, + ngx_rtmp_amf_elt_t *elts, size_t nelts); + +ngx_chain_t *ngx_rtmp_create_amf(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_rtmp_amf_elt_t *elts, size_t nelts); +ngx_int_t ngx_rtmp_send_amf(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_rtmp_amf_elt_t *elts, size_t nelts); + +/* AMF status sender */ +ngx_chain_t *ngx_rtmp_create_status(ngx_rtmp_session_t *s, char *code, + char *level, char *desc); +ngx_chain_t *ngx_rtmp_create_play_status(ngx_rtmp_session_t *s, char *code, + char *level, ngx_uint_t duration, ngx_uint_t bytes); +ngx_chain_t *ngx_rtmp_create_sample_access(ngx_rtmp_session_t *s); + +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); +ngx_int_t ngx_rtmp_send_sample_access(ngx_rtmp_session_t *s); + + +/* Frame types */ +#define NGX_RTMP_VIDEO_KEY_FRAME 1 +#define NGX_RTMP_VIDEO_INTER_FRAME 2 +#define NGX_RTMP_VIDEO_DISPOSABLE_FRAME 3 + + +static ngx_inline ngx_int_t +ngx_rtmp_get_video_frame_type(ngx_chain_t *in) +{ + return (in->buf->pos[0] & 0xf0) >> 4; +} + + +static ngx_inline ngx_int_t +ngx_rtmp_is_codec_header(ngx_chain_t *in) +{ + return in->buf->pos + 1 < in->buf->last && in->buf->pos[1] == 0; +} + + +extern ngx_rtmp_bandwidth_t ngx_rtmp_bw_out; +extern ngx_rtmp_bandwidth_t ngx_rtmp_bw_in; + + +extern ngx_uint_t ngx_rtmp_naccepted; +#if (nginx_version >= 1007011) +extern ngx_queue_t ngx_rtmp_init_queue; +#elif (nginx_version >= 1007005) +extern ngx_thread_volatile ngx_queue_t ngx_rtmp_init_queue; +#else +extern ngx_thread_volatile ngx_event_t *ngx_rtmp_init_queue; +#endif + +extern ngx_uint_t ngx_rtmp_max_module; +extern ngx_module_t ngx_rtmp_core_module; + + +u_char *ngx_rtmp_log_error(ngx_log_t *log, u_char *buf, size_t len); + + +ngx_int_t ngx_rtmp_parse_request_line(ngx_rtmp_session_t *s, ngx_buf_t *b); +ngx_int_t ngx_rtmp_process_request_uri(ngx_rtmp_session_t *s); +ngx_int_t ngx_rtmp_parse_complex_uri(ngx_rtmp_session_t *s, + ngx_uint_t merge_slashes); + +ngx_int_t ngx_rtmp_process_virtual_host(ngx_rtmp_session_t *s); +ngx_int_t ngx_rtmp_validate_host(ngx_str_t *host, ngx_pool_t *pool, + ngx_uint_t alloc); +ngx_int_t ngx_rtmp_set_virtual_server(ngx_rtmp_session_t *s, ngx_str_t *host); +ngx_int_t ngx_rtmp_process_request_line(ngx_rtmp_session_t *s, + const u_char *name, const u_char *args, const u_char *cmd); +#if (nginx_version <= 1011001) +in_port_t ngx_inet_get_port(struct sockaddr *sa); +void ngx_inet_set_port(struct sockaddr *sa, in_port_t port); +#endif + +ngx_int_t ngx_rtmp_send_fcpublish(ngx_rtmp_session_t *s, u_char *desc); +ngx_int_t ngx_rtmp_send_fcunpublish(ngx_rtmp_session_t *s, u_char *desc); + +#endif /* _NGX_RTMP_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_access_module.c b/ngx_http_flv_module/ngx_rtmp_access_module.c new file mode 100644 index 0000000..34f96e7 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_access_module.c @@ -0,0 +1,472 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_cmd_module.h" + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_play_pt next_play; + + +#define NGX_RTMP_ACCESS_PUBLISH 0x01 +#define NGX_RTMP_ACCESS_PLAY 0x02 + + +static char * ngx_rtmp_access_rule(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static ngx_int_t ngx_rtmp_access_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_access_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_access_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); + + +typedef struct { + in_addr_t mask; + in_addr_t addr; + ngx_uint_t deny; + ngx_uint_t flags; +} ngx_rtmp_access_rule_t; + + +#if (NGX_HAVE_INET6) + +typedef struct { + struct in6_addr addr; + struct in6_addr mask; + ngx_uint_t deny; + ngx_uint_t flags; +} ngx_rtmp_access_rule6_t; + +#endif + + +typedef struct { + 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 */ +#endif +} ngx_rtmp_access_app_conf_t; + + +static ngx_command_t ngx_rtmp_access_commands[] = { + + { ngx_string("allow"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE12, + ngx_rtmp_access_rule, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("deny"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE12, + ngx_rtmp_access_rule, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_access_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_access_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + ngx_rtmp_access_create_app_conf, /* create app configuration */ + ngx_rtmp_access_merge_app_conf, /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_access_module = { + NGX_MODULE_V1, + &ngx_rtmp_access_module_ctx, /* module context */ + ngx_rtmp_access_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static void * +ngx_rtmp_access_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_access_app_conf_t *aacf; + + aacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_access_app_conf_t)); + if (aacf == NULL) { + 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; +} + + +static ngx_int_t +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"); + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +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; + + 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)) { + return ngx_rtmp_access_found(s, rule[i].deny); + } + } + + return NGX_OK; +} + + +#if (NGX_HAVE_INET6) + +static ngx_int_t +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; + + 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) + { + size_t cl, ml, al; + u_char ct[NGX_INET6_ADDRSTRLEN]; + u_char mt[NGX_INET6_ADDRSTRLEN]; + u_char at[NGX_INET6_ADDRSTRLEN]; + + cl = ngx_inet6_ntop(p, ct, NGX_INET6_ADDRSTRLEN); + ml = ngx_inet6_ntop(rule6[i].mask.s6_addr, mt, NGX_INET6_ADDRSTRLEN); + al = ngx_inet6_ntop(rule6[i].addr.s6_addr, at, NGX_INET6_ADDRSTRLEN); + + ngx_log_debug6(NGX_LOG_DEBUG_HTTP, s->connection->log, 0, + "access: %*s %*s %*s", cl, ct, ml, mt, al, at); + } +#endif + + for (n = 0; n < 16; n++) { + if ((p[n] & rule6[i].mask.s6_addr[n]) != rule6[i].addr.s6_addr[n]) { + goto next; + } + } + + if (flag & rule6[i].flags) { + return ngx_rtmp_access_found(s, rule6[i].deny); + } + + next: + continue; + } + + return NGX_OK; +} + +#endif + + +static ngx_int_t +ngx_rtmp_access(ngx_rtmp_session_t *s, ngx_uint_t flag) +{ + struct sockaddr_in *sin; + ngx_rtmp_access_app_conf_t *ascf; +#if (NGX_HAVE_INET6) + u_char *p; + in_addr_t addr; + struct sockaddr_in6 *sin6; +#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"); + return NGX_ERROR; + } + + /* relay etc */ + if (s->connection->sockaddr == NULL) { + return NGX_OK; + } + + switch (s->connection->sockaddr->sa_family) { + + case AF_INET: + sin = (struct sockaddr_in *) s->connection->sockaddr; + return ngx_rtmp_access_inet(s, sin->sin_addr.s_addr, flag); + +#if (NGX_HAVE_INET6) + + case AF_INET6: + sin6 = (struct sockaddr_in6 *) s->connection->sockaddr; + p = sin6->sin6_addr.s6_addr; + + 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, htonl(addr), flag); + } + + return ngx_rtmp_access_inet6(s, p, flag); + +#endif + } + + return NGX_OK; +} + + +static char * +ngx_rtmp_access_rule(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_access_app_conf_t *ascf = conf; + + ngx_int_t rc; + ngx_uint_t all; + ngx_str_t *value; + ngx_cidr_t cidr; + ngx_rtmp_access_rule_t *rule; +#if (NGX_HAVE_INET6) + ngx_rtmp_access_rule6_t *rule6; +#endif + size_t n; + ngx_uint_t flags; + + ngx_memzero(&cidr, sizeof(ngx_cidr_t)); + + value = cf->args->elts; + + n = 1; + 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) + { + flags |= NGX_RTMP_ACCESS_PUBLISH; + continue; + + } + + if (value[n].len == sizeof("play") - 1 && + ngx_strcmp(value[1].data, "play") == 0) + { + flags |= NGX_RTMP_ACCESS_PLAY; + continue; + + } + + ngx_log_error(NGX_LOG_ERR, cf->log, 0, + "unexpected access specified: '%V'", &value[n]); + return NGX_CONF_ERROR; + } + } + + all = (value[n].len == 3 && ngx_strcmp(value[n].data, "all") == 0); + + if (!all) { + + rc = ngx_ptocidr(&value[n], &cidr); + + if (rc == NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "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]); + } + } + + switch (cidr.family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + case 0: /* all */ + + rule6 = ngx_array_push(&ascf->rules6); + if (rule6 == NULL) { + return NGX_CONF_ERROR; + } + + rule6->mask = cidr.u.in6.mask; + rule6->addr = cidr.u.in6.addr; + rule6->deny = (value[0].data[0] == 'd') ? 1 : 0; + rule6->flags = flags; + + if (!all) { + break; + } + + /* "all" passes through */ +#endif + /* fall through */ + + default: /* AF_INET */ + + rule = ngx_array_push(&ascf->rules); + if (rule == NULL) { + return NGX_CONF_ERROR; + } + + rule->mask = cidr.u.in.mask; + rule->addr = cidr.u.in.addr; + rule->deny = (value[0].data[0] == 'd') ? 1 : 0; + rule->flags = flags; + } + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_access_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + if (s->auto_pushed) { + goto next; + } + + if (ngx_rtmp_access(s, NGX_RTMP_ACCESS_PUBLISH) != NGX_OK) { + return NGX_ERROR; + } + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_access_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + if (ngx_rtmp_access(s, NGX_RTMP_ACCESS_PLAY) != NGX_OK) { + return NGX_ERROR; + } + + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_access_postconfiguration(ngx_conf_t *cf) +{ + /* chain handlers */ + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_access_publish; + + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_access_play; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_amf.c b/ngx_http_flv_module/ngx_rtmp_amf.c new file mode 100644 index 0000000..a1852b9 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_amf.c @@ -0,0 +1,653 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_amf.h" +#include "ngx_rtmp.h" +#include + + +static ngx_inline void* +ngx_rtmp_amf_reverse_copy(void *dst, void* src, size_t len) +{ + if (dst == NULL || src == NULL) { + return NULL; + } + +#if (NGX_HAVE_LITTLE_ENDIAN) + ngx_rtmp_rmemcpy(dst, src, len); +#else + ngx_memcpy(dst, src, len); +#endif + + return dst; +} + +#define NGX_RTMP_AMF_DEBUG_SIZE 16 + +#ifdef NGX_DEBUG +static void +ngx_rtmp_amf_debug(const char* op, ngx_log_t *log, u_char *p, size_t n) +{ + u_char hstr[3 * NGX_RTMP_AMF_DEBUG_SIZE + 1]; + u_char str[NGX_RTMP_AMF_DEBUG_SIZE + 1]; + u_char *hp, *sp; + static u_char hex[] = "0123456789ABCDEF"; + size_t i; + + hp = hstr; + sp = str; + + for(i = 0; i < n && i < NGX_RTMP_AMF_DEBUG_SIZE; ++i) { + *hp++ = ' '; + if (p) { + *hp++ = hex[(*p & 0xf0) >> 4]; + *hp++ = hex[*p & 0x0f]; + *sp++ = (*p >= 0x20 && *p <= 0x7e) ? + *p : (u_char)'?'; + ++p; + } else { + *hp++ = 'X'; + *hp++ = 'X'; + *sp++ = '?'; + } + } + *hp = *sp = '\0'; + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, log, 0, + "AMF %s (%d)%s '%s'", op, n, hstr, str); +} +#endif + +static ngx_int_t +ngx_rtmp_amf_get(ngx_rtmp_amf_ctx_t *ctx, void *p, size_t n) +{ + size_t size; + ngx_chain_t *l; + size_t offset; + u_char *pos, *last; +#ifdef NGX_DEBUG + void *op = p; + size_t on = n; +#endif + + if (!n) + return NGX_OK; + + for(l = ctx->link, offset = ctx->offset; l; l = l->next, offset = 0) { + + pos = l->buf->pos + offset; + last = l->buf->last; + + if (last >= pos + n) { + if (p) { + p = ngx_cpymem(p, pos, n); + } + ctx->offset = offset + n; + ctx->link = l; + +#ifdef NGX_DEBUG + ngx_rtmp_amf_debug("read", ctx->log, (u_char*)op, on); +#endif + + return NGX_OK; + } + + size = last - pos; + + if (p) { + p = ngx_cpymem(p, pos, size); + } + + n -= size; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, ctx->log, 0, + "AMF read eof (%d)", n); + + return NGX_DONE; +} + + +static ngx_int_t +ngx_rtmp_amf_put(ngx_rtmp_amf_ctx_t *ctx, void *p, size_t n) +{ + ngx_buf_t *b; + size_t size; + ngx_chain_t *l, *ln; + +#ifdef NGX_DEBUG + ngx_rtmp_amf_debug("write", ctx->log, (u_char*)p, n); +#endif + + l = ctx->link; + + if (ctx->link && ctx->first == NULL) { + ctx->first = ctx->link; + } + + while(n) { + b = l ? l->buf : NULL; + + if (b == NULL || b->last == b->end) { + + ln = ctx->alloc(ctx->arg); + if (ln == NULL) { + return NGX_ERROR; + } + + if (ctx->first == NULL) { + ctx->first = ln; + } + + if (l) { + l->next = ln; + } + + l = ln; + ctx->link = l; + b = l->buf; + } + + size = b->end - b->last; + + if (size >= n) { + b->last = ngx_cpymem(b->last, p, n); + return NGX_OK; + } + + b->last = ngx_cpymem(b->last, p, size); + p = (u_char*)p + size; + n -= size; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_amf_read_object(ngx_rtmp_amf_ctx_t *ctx, ngx_rtmp_amf_elt_t *elts, + size_t nelts) +{ + uint8_t type; + uint16_t len; + size_t n, namelen, maxlen; + ngx_int_t rc; + u_char buf[2]; + + maxlen = 0; + for(n = 0; n < nelts; ++n) { + namelen = elts[n].name.len; + if (namelen > maxlen) + maxlen = namelen; + } + + for( ;; ) { + +#if !(NGX_WIN32) + char name[maxlen]; +#else + char name[1024]; + if (maxlen > sizeof(name)) { + return NGX_ERROR; + } +#endif + /* read key */ + switch (ngx_rtmp_amf_get(ctx, buf, 2)) { + case NGX_DONE: + /* Envivio sends unfinalized arrays */ + return NGX_OK; + case NGX_OK: + break; + default: + return NGX_ERROR; + } + + ngx_rtmp_amf_reverse_copy(&len, buf, 2); + + if (!len) + break; + + if (len <= maxlen) { + rc = ngx_rtmp_amf_get(ctx, name, len); + + } else { + rc = ngx_rtmp_amf_get(ctx, name, maxlen); + if (rc != NGX_OK) + return NGX_ERROR; + rc = ngx_rtmp_amf_get(ctx, 0, len - maxlen); + } + + if (rc != NGX_OK) + return NGX_ERROR; + + /* TODO: if we require array to be sorted on name + * then we could be able to use binary search */ + for(n = 0; n < nelts + && (len != elts[n].name.len + || ngx_strncmp(name, elts[n].name.data, len)); + ++n); + + if (ngx_rtmp_amf_read(ctx, n < nelts ? &elts[n] : NULL, 1) != NGX_OK) + return NGX_ERROR; + } + + if (ngx_rtmp_amf_get(ctx, &type, 1) != NGX_OK + || type != NGX_RTMP_AMF_END) + { + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_amf_read_array(ngx_rtmp_amf_ctx_t *ctx, ngx_rtmp_amf_elt_t *elts, + size_t nelts) +{ + uint32_t len; + size_t n; + u_char buf[4]; + + /* read length */ + if (ngx_rtmp_amf_get(ctx, buf, 4) != NGX_OK) + return NGX_ERROR; + + ngx_rtmp_amf_reverse_copy(&len, buf, 4); + + for (n = 0; n < len; ++n) { + if (ngx_rtmp_amf_read(ctx, n < nelts ? &elts[n] : NULL, 1) != NGX_OK) + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_amf_read_variant(ngx_rtmp_amf_ctx_t *ctx, ngx_rtmp_amf_elt_t *elts, + size_t nelts) +{ + uint8_t type; + ngx_int_t rc; + size_t n; + ngx_rtmp_amf_elt_t elt; + + rc = ngx_rtmp_amf_get(ctx, &type, 1); + if (rc != NGX_OK) { + return rc; + } + + ngx_memzero(&elt, sizeof(elt)); + for (n = 0; n < nelts; ++n, ++elts) { + if (type == elts->type) { + elt.data = elts->data; + elt.len = elts->len; + } + } + + elt.type = type | NGX_RTMP_AMF_TYPELESS; + + return ngx_rtmp_amf_read(ctx, &elt, 1); +} + + +static ngx_int_t +ngx_rtmp_amf_is_compatible_type(uint8_t t1, uint8_t t2) +{ + return t1 == t2 + || (t1 == NGX_RTMP_AMF_OBJECT && t2 == NGX_RTMP_AMF_MIXED_ARRAY) + || (t2 == NGX_RTMP_AMF_OBJECT && t1 == NGX_RTMP_AMF_MIXED_ARRAY); +} + + +ngx_int_t +ngx_rtmp_amf_read(ngx_rtmp_amf_ctx_t *ctx, ngx_rtmp_amf_elt_t *elts, + size_t nelts) +{ + void *data; + ngx_int_t type; + uint8_t type8; + size_t n; + uint16_t len; + ngx_int_t rc; + u_char buf[8]; + uint32_t max_index; + + for(n = 0; n < nelts; ++n) { + + if (elts && elts->type & NGX_RTMP_AMF_TYPELESS) { + type = elts->type & ~NGX_RTMP_AMF_TYPELESS; + data = elts->data; + + } else { + switch (ngx_rtmp_amf_get(ctx, &type8, 1)) { + case NGX_DONE: + if (elts && elts->type & NGX_RTMP_AMF_OPTIONAL) { + return NGX_OK; + } + + /* fall through */ + + case NGX_ERROR: + return NGX_ERROR; + } + type = type8; + data = (elts && + ngx_rtmp_amf_is_compatible_type( + (uint8_t) (elts->type & 0xff), (uint8_t) type)) + ? elts->data + : NULL; + + if (elts && (elts->type & NGX_RTMP_AMF_CONTEXT)) { + if (data) { + *(ngx_rtmp_amf_ctx_t *) data = *ctx; + } + data = NULL; + } + } + + switch (type) { + case NGX_RTMP_AMF_NUMBER: + if (ngx_rtmp_amf_get(ctx, buf, 8) != NGX_OK) { + return NGX_ERROR; + } + ngx_rtmp_amf_reverse_copy(data, buf, 8); + break; + + case NGX_RTMP_AMF_BOOLEAN: + if (ngx_rtmp_amf_get(ctx, data, 1) != NGX_OK) { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_STRING: + if (ngx_rtmp_amf_get(ctx, buf, 2) != NGX_OK) { + return NGX_ERROR; + } + ngx_rtmp_amf_reverse_copy(&len, buf, 2); + + if (data == NULL) { + rc = ngx_rtmp_amf_get(ctx, data, len); + + } else if (elts && elts->len <= len) { + rc = ngx_rtmp_amf_get(ctx, data, elts->len - 1); + if (rc != NGX_OK) + return NGX_ERROR; + ((char*)data)[elts->len - 1] = 0; + rc = ngx_rtmp_amf_get(ctx, NULL, len - elts->len + 1); + + } else { + rc = ngx_rtmp_amf_get(ctx, data, len); + ((char*)data)[len] = 0; + } + + if (rc != NGX_OK) { + return NGX_ERROR; + } + + break; + + case NGX_RTMP_AMF_NULL: + case NGX_RTMP_AMF_ARRAY_NULL: + break; + + case NGX_RTMP_AMF_MIXED_ARRAY: + if (ngx_rtmp_amf_get(ctx, &max_index, 4) != NGX_OK) { + return NGX_ERROR; + } + + /* fall through */ + + case NGX_RTMP_AMF_OBJECT: + if (ngx_rtmp_amf_read_object(ctx, data, + data && elts ? elts->len / sizeof(ngx_rtmp_amf_elt_t) : 0 + ) != NGX_OK) + { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_ARRAY: + if (ngx_rtmp_amf_read_array(ctx, data, + data && elts ? elts->len / sizeof(ngx_rtmp_amf_elt_t) : 0 + ) != NGX_OK) + { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_VARIANT_: + if (ngx_rtmp_amf_read_variant(ctx, data, + data && elts ? elts->len / sizeof(ngx_rtmp_amf_elt_t) : 0 + ) != NGX_OK) + { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_INT8: + if (ngx_rtmp_amf_get(ctx, data, 1) != NGX_OK) { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_INT16: + if (ngx_rtmp_amf_get(ctx, buf, 2) != NGX_OK) { + return NGX_ERROR; + } + ngx_rtmp_amf_reverse_copy(data, buf, 2); + break; + + case NGX_RTMP_AMF_INT32: + if (ngx_rtmp_amf_get(ctx, buf, 4) != NGX_OK) { + return NGX_ERROR; + } + ngx_rtmp_amf_reverse_copy(data, buf, 4); + break; + + case NGX_RTMP_AMF_END: + return NGX_OK; + + default: + return NGX_ERROR; + } + + if (elts) { + ++elts; + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_amf_write_object(ngx_rtmp_amf_ctx_t *ctx, + ngx_rtmp_amf_elt_t *elts, size_t nelts) +{ + uint16_t len; + size_t n; + u_char buf[2]; + + for(n = 0; n < nelts; ++n) { + + len = (uint16_t) elts[n].name.len; + + if (ngx_rtmp_amf_put(ctx, + ngx_rtmp_amf_reverse_copy(buf, + &len, 2), 2) != NGX_OK) + { + return NGX_ERROR; + } + + if (ngx_rtmp_amf_put(ctx, elts[n].name.data, len) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_amf_write(ctx, &elts[n], 1) != NGX_OK) { + return NGX_ERROR; + } + } + + if (ngx_rtmp_amf_put(ctx, "\0\0", 2) != NGX_OK) { + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_amf_write_array(ngx_rtmp_amf_ctx_t *ctx, + ngx_rtmp_amf_elt_t *elts, size_t nelts) +{ + uint32_t len; + size_t n; + u_char buf[4]; + + len = nelts; + if (ngx_rtmp_amf_put(ctx, + ngx_rtmp_amf_reverse_copy(buf, + &len, 4), 4) != NGX_OK) + { + return NGX_ERROR; + } + + for(n = 0; n < nelts; ++n) { + if (ngx_rtmp_amf_write(ctx, &elts[n], 1) != NGX_OK) { + return NGX_ERROR; + } + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_amf_write(ngx_rtmp_amf_ctx_t *ctx, + ngx_rtmp_amf_elt_t *elts, size_t nelts) +{ + size_t n; + ngx_int_t type; + uint8_t type8; + void *data; + uint16_t len; + uint32_t max_index; + u_char buf[8]; + + for(n = 0; n < nelts; ++n) { + + type = elts[n].type; + data = elts[n].data; + len = (uint16_t) elts[n].len; + + if (type & NGX_RTMP_AMF_TYPELESS) { + type &= ~NGX_RTMP_AMF_TYPELESS; + } else { + type8 = (uint8_t)type; + if (ngx_rtmp_amf_put(ctx, &type8, 1) != NGX_OK) + return NGX_ERROR; + } + + switch(type) { + case NGX_RTMP_AMF_NUMBER: + if (ngx_rtmp_amf_put(ctx, + ngx_rtmp_amf_reverse_copy(buf, + data, 8), 8) != NGX_OK) + { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_BOOLEAN: + if (ngx_rtmp_amf_put(ctx, data, 1) != NGX_OK) { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_STRING: + if (len == 0 && data) { + len = (uint16_t) ngx_strlen((u_char*) data); + } + + if (ngx_rtmp_amf_put(ctx, + ngx_rtmp_amf_reverse_copy(buf, + &len, 2), 2) != NGX_OK) + { + return NGX_ERROR; + } + + if (ngx_rtmp_amf_put(ctx, data, len) != NGX_OK) { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_NULL: + case NGX_RTMP_AMF_ARRAY_NULL: + break; + + case NGX_RTMP_AMF_MIXED_ARRAY: + max_index = 0; + if (ngx_rtmp_amf_put(ctx, &max_index, 4) != NGX_OK) { + return NGX_ERROR; + } + + /* fall through */ + + case NGX_RTMP_AMF_OBJECT: + type8 = NGX_RTMP_AMF_END; + if (ngx_rtmp_amf_write_object(ctx, data, + elts[n].len / sizeof(ngx_rtmp_amf_elt_t)) != NGX_OK + || ngx_rtmp_amf_put(ctx, &type8, 1) != NGX_OK) + { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_ARRAY: + if (ngx_rtmp_amf_write_array(ctx, data, + elts[n].len / sizeof(ngx_rtmp_amf_elt_t)) != NGX_OK) + { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_INT8: + if (ngx_rtmp_amf_put(ctx, data, 1) != NGX_OK) { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_INT16: + if (ngx_rtmp_amf_put(ctx, + ngx_rtmp_amf_reverse_copy(buf, + data, 2), 2) != NGX_OK) + { + return NGX_ERROR; + } + break; + + case NGX_RTMP_AMF_INT32: + if (ngx_rtmp_amf_put(ctx, + ngx_rtmp_amf_reverse_copy(buf, + data, 4), 4) != NGX_OK) + { + return NGX_ERROR; + } + break; + + default: + return NGX_ERROR; + } + } + + return NGX_OK; +} + diff --git a/ngx_http_flv_module/ngx_rtmp_amf.h b/ngx_http_flv_module/ngx_rtmp_amf.h new file mode 100644 index 0000000..8f70a12 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_amf.h @@ -0,0 +1,71 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_AMF_H_INCLUDED_ +#define _NGX_RTMP_AMF_H_INCLUDED_ + + +#include +#include + + +/* basic types */ +#define NGX_RTMP_AMF_NUMBER 0x00 +#define NGX_RTMP_AMF_BOOLEAN 0x01 +#define NGX_RTMP_AMF_STRING 0x02 +#define NGX_RTMP_AMF_OBJECT 0x03 +#define NGX_RTMP_AMF_NULL 0x05 +#define NGX_RTMP_AMF_ARRAY_NULL 0x06 +#define NGX_RTMP_AMF_MIXED_ARRAY 0x08 +#define NGX_RTMP_AMF_END 0x09 +#define NGX_RTMP_AMF_ARRAY 0x0a + +/* extended types */ +#define NGX_RTMP_AMF_INT8 0x0100 +#define NGX_RTMP_AMF_INT16 0x0101 +#define NGX_RTMP_AMF_INT32 0x0102 +#define NGX_RTMP_AMF_VARIANT_ 0x0103 + +/* r/w flags */ +#define NGX_RTMP_AMF_OPTIONAL 0x1000 +#define NGX_RTMP_AMF_TYPELESS 0x2000 +#define NGX_RTMP_AMF_CONTEXT 0x4000 + +#define NGX_RTMP_AMF_VARIANT (NGX_RTMP_AMF_VARIANT_\ + |NGX_RTMP_AMF_TYPELESS) + + +typedef struct { + ngx_int_t type; + ngx_str_t name; + void *data; + size_t len; +} ngx_rtmp_amf_elt_t; + + +typedef ngx_chain_t * (*ngx_rtmp_amf_alloc_pt)(void *arg); + + +typedef struct { + ngx_chain_t *link, *first; + size_t offset; + ngx_rtmp_amf_alloc_pt alloc; + void *arg; + ngx_log_t *log; +} ngx_rtmp_amf_ctx_t; + + +/* reading AMF */ +ngx_int_t ngx_rtmp_amf_read(ngx_rtmp_amf_ctx_t *ctx, + ngx_rtmp_amf_elt_t *elts, size_t nelts); + +/* writing AMF */ +ngx_int_t ngx_rtmp_amf_write(ngx_rtmp_amf_ctx_t *ctx, + ngx_rtmp_amf_elt_t *elts, size_t nelts); + + +#endif /* _NGX_RTMP_AMF_H_INCLUDED_ */ + diff --git a/ngx_http_flv_module/ngx_rtmp_auto_push_module.c b/ngx_http_flv_module/ngx_rtmp_auto_push_module.c new file mode 100644 index 0000000..071bf9c --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_auto_push_module.c @@ -0,0 +1,706 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_relay_module.h" + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_delete_stream_pt next_delete_stream; + + +static ngx_int_t ngx_rtmp_auto_push_init_process(ngx_cycle_t *cycle); +static void ngx_rtmp_auto_push_exit_process(ngx_cycle_t *cycle); +static void * ngx_rtmp_auto_push_create_conf(ngx_cycle_t *cf); +static char * ngx_rtmp_auto_push_init_conf(ngx_cycle_t *cycle, void *conf); +#if (NGX_HAVE_UNIX_DOMAIN) +static ngx_int_t ngx_rtmp_auto_push_publish(ngx_rtmp_session_t *s, + ngx_rtmp_publish_t *v); +static ngx_int_t ngx_rtmp_auto_push_delete_stream(ngx_rtmp_session_t *s, + ngx_rtmp_delete_stream_t *v); +#endif + + +typedef struct ngx_rtmp_auto_push_ctx_s ngx_rtmp_auto_push_ctx_t; + +struct ngx_rtmp_auto_push_ctx_s { + ngx_int_t *slots; /* NGX_MAX_PROCESSES */ + u_char name[NGX_RTMP_MAX_NAME]; + u_char args[NGX_RTMP_MAX_ARGS]; + ngx_event_t push_evt; +}; + + +typedef struct { + ngx_flag_t auto_push; + ngx_str_t socket_dir; + ngx_msec_t push_reconnect; +} ngx_rtmp_auto_push_conf_t; + + +static ngx_command_t ngx_rtmp_auto_push_commands[] = { + + { ngx_string("rtmp_auto_push"), + NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + 0, + offsetof(ngx_rtmp_auto_push_conf_t, auto_push), + NULL }, + + { ngx_string("rtmp_auto_push_reconnect"), + NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + 0, + offsetof(ngx_rtmp_auto_push_conf_t, push_reconnect), + NULL }, + + { ngx_string("rtmp_socket_dir"), + NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + 0, + offsetof(ngx_rtmp_auto_push_conf_t, socket_dir), + NULL }, + + ngx_null_command +}; + + +static ngx_core_module_t ngx_rtmp_auto_push_module_ctx = { + ngx_string("rtmp_auto_push"), + ngx_rtmp_auto_push_create_conf, /* create conf */ + ngx_rtmp_auto_push_init_conf /* init conf */ +}; + + +ngx_module_t ngx_rtmp_auto_push_module = { + NGX_MODULE_V1, + &ngx_rtmp_auto_push_module_ctx, /* module context */ + ngx_rtmp_auto_push_commands, /* module directives */ + NGX_CORE_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + ngx_rtmp_auto_push_init_process, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + ngx_rtmp_auto_push_exit_process, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static ngx_rtmp_module_t ngx_rtmp_auto_push_index_module_ctx = { + NULL, /* preconfiguration */ + NULL, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + NULL, /* create app configuration */ + NULL /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_auto_push_index_module = { + NGX_MODULE_V1, + &ngx_rtmp_auto_push_index_module_ctx, /* module context */ + NULL, /* module directives */ + NGX_RTMP_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 +}; + + +#define NGX_RTMP_AUTO_PUSH_SOCKNAME "nginx-http-flv" + + +static ngx_int_t +ngx_rtmp_auto_push_init_process(ngx_cycle_t *cycle) +{ +#if (NGX_HAVE_UNIX_DOMAIN) + ngx_rtmp_auto_push_conf_t *apcf; + ngx_listening_t *ls, *lss; + struct sockaddr_un *saun; +#if (nginx_version >= 1009011) + ngx_event_t *rev; + ngx_connection_t *c; + ngx_module_t **modules; + ngx_int_t i, auto_push_index, event_core_index; +#endif + int reuseaddr; + ngx_socket_t s; + size_t n; + ngx_file_info_t fi; + ngx_pid_t pid; + + if (ngx_process != NGX_PROCESS_WORKER) { + return NGX_OK; + } + + apcf = (ngx_rtmp_auto_push_conf_t *) ngx_get_conf(cycle->conf_ctx, + ngx_rtmp_auto_push_module); + if (apcf->auto_push == 0) { + return NGX_OK; + } + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_auto_push_publish; + + next_delete_stream = ngx_rtmp_delete_stream; + ngx_rtmp_delete_stream = ngx_rtmp_auto_push_delete_stream; + + reuseaddr = 1; + s = (ngx_socket_t) -1; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, cycle->log, 0, + "auto_push: creating sockets"); + + /*TODO: clone all RTMP listenings? */ + ls = cycle->listening.elts; + lss = NULL; + for (n = 0; n < cycle->listening.nelts; ++n, ++ls) { + if (ls->handler == ngx_rtmp_init_connection) { + lss = ls; + break; + } + } + + if (lss == NULL) { + return NGX_OK; + } + + ls = ngx_array_push(&cycle->listening); + if (ls == NULL) { + return NGX_ERROR; + } + + *ls = *lss; + + /* Disable unix socket client address extraction + * from accept call + * Nginx generates bad addr_text with this enabled */ + ls->addr_ntop = 0; + + ls->socklen = sizeof(struct sockaddr_un); + saun = ngx_pcalloc(cycle->pool, ls->socklen); + ls->sockaddr = (struct sockaddr *) saun; + if (ls->sockaddr == NULL) { + return NGX_ERROR; + } + saun->sun_family = AF_UNIX; + pid = ngx_getpid(); + *ngx_snprintf((u_char *) saun->sun_path, sizeof(saun->sun_path), + "%V/" NGX_RTMP_AUTO_PUSH_SOCKNAME ".%P", + &apcf->socket_dir, pid) + = 0; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, cycle->log, 0, + "auto_push: create socket '%s'", + saun->sun_path); + + if (ngx_file_info(saun->sun_path, &fi) != ENOENT) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, cycle->log, 0, + "auto_push: delete existing socket '%s'", + saun->sun_path); + ngx_delete_file(saun->sun_path); + } + + ls->addr_text.data = (u_char *) saun->sun_path; + ls->addr_text.len = ngx_strlen(saun->sun_path); + + s = ngx_socket(AF_UNIX, SOCK_STREAM, 0); + if (s == -1) { + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, + ngx_socket_n " %s failed", saun->sun_path); + return NGX_ERROR; + } + + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, + (const void *) &reuseaddr, sizeof(int)) + == -1) + { + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, + "setsockopt(SO_REUSEADDR) %s failed", saun->sun_path); + goto sock_error; + } + + if (!(ngx_event_flags & NGX_USE_AIO_EVENT)) { + if (ngx_nonblocking(s) == -1) { + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, + ngx_nonblocking_n " %s failed", saun->sun_path); + return NGX_ERROR; + } + } + + if (bind(s, (struct sockaddr *) saun, sizeof(*saun)) == -1) { + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, + ngx_nonblocking_n " %s bind failed", saun->sun_path); + goto sock_error; + } + + if (listen(s, NGX_LISTEN_BACKLOG) == -1) { + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, + "listen() to %s, backlog %d failed", + saun->sun_path, NGX_LISTEN_BACKLOG); + goto sock_error; + } + + ls->fd = s; + ls->listen = 1; + + /* Socket option `SO_REUSEPORT` has been supported since nginx-1.9.1, + * if option `reuseport` is added for the directive `listen`, listening + * structure of unix domain socket in the non-first process will not be + * initialized, in fact `reuseport` is useless for a unix domain socket + * on which there is only a process listening */ + +#if (NGX_HAVE_REUSEPORT) + ls->reuseport = 0; +#endif + + /* for dynamic module */ +#if (nginx_version >= 1009011) + auto_push_index = -1; + event_core_index = -1; + + modules = cycle->modules; + + for (i = 0; modules[i]; ++i) { + if (ngx_strcmp(modules[i]->name, "ngx_event_core_module") == 0) { + event_core_index = i; + } + + if (ngx_strcmp(modules[i]->name, "ngx_rtmp_auto_push_module") == 0) { + auto_push_index = i; + } + + if (auto_push_index != -1 && event_core_index != -1) { + break; + } + } + + if (auto_push_index > event_core_index) { + c = ngx_get_connection(ls->fd, cycle->log); + if (c == NULL) { + goto sock_error; + } + + rev = c->read; + +#if (nginx_version >= 1009013) + c->type = ls->type; +#endif + c->log = &ls->log; + + c->listening = ls; + ls->connection = c; + + rev->log = c->log; + rev->accept = 1; + +#if (NGX_HAVE_DEFERRED_ACCEPT) + rev->deferred_accept = ls->deferred_accept; +#endif + +#if (nginx_version >= 1009013) + rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept + : ngx_event_recvmsg; +#else + rev->handler = ngx_event_accept; +#endif + + if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) { + return NGX_ERROR; + } + } +#endif + + return NGX_OK; + +sock_error: + if (s != (ngx_socket_t) -1 && ngx_close_socket(s) == -1) { + ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno, + ngx_close_socket_n " %s failed", saun->sun_path); + } + ngx_delete_file(saun->sun_path); + + return NGX_ERROR; + +#else /* NGX_HAVE_UNIX_DOMAIN */ + + return NGX_OK; + +#endif /* NGX_HAVE_UNIX_DOMAIN */ +} + + +static void +ngx_rtmp_auto_push_exit_process(ngx_cycle_t *cycle) +{ +#if (NGX_HAVE_UNIX_DOMAIN) + ngx_rtmp_auto_push_conf_t *apcf; + u_char path[NGX_MAX_PATH]; + + ngx_listening_t *ls; + ngx_connection_t *c; + size_t n; + ngx_pid_t pid; + + apcf = (ngx_rtmp_auto_push_conf_t *) ngx_get_conf(cycle->conf_ctx, + ngx_rtmp_auto_push_module); + if (apcf->auto_push == 0) { + return; + } + + ls = cycle->listening.elts; + + for (n = 0; n < cycle->listening.nelts; ++n, ++ls) { + if ((ls->handler == ngx_rtmp_init_connection) && + (ls->sockaddr && ls->sockaddr->sa_family == AF_UNIX)) + { + c = ls->connection; + + if (c) { + if (c->read->active) { + if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) { + + /* + * delete the old accept events that were bound to + * the old cycle read events array + */ + + ngx_del_event(c->read, + NGX_READ_EVENT, NGX_CLOSE_EVENT); + + ngx_free_connection(c); + + c->fd = (ngx_socket_t) -1; + } + } + } + + if (ngx_close_socket(ls->fd) == -1) { + ngx_log_error(NGX_LOG_ERR, cycle->log, ngx_socket_errno, + ngx_close_socket_n "%V failed", + &ls->addr_text); + } + + ls->fd = (ngx_socket_t) -1; + + break; + } + } + + pid = ngx_getpid(); + *ngx_snprintf(path, sizeof(path), + "%V/" NGX_RTMP_AUTO_PUSH_SOCKNAME ".%P", + &apcf->socket_dir, pid) + = 0; + + ngx_delete_file(path); + +#endif +} + + +static void * +ngx_rtmp_auto_push_create_conf(ngx_cycle_t *cycle) +{ + ngx_rtmp_auto_push_conf_t *apcf; + + apcf = ngx_pcalloc(cycle->pool, sizeof(ngx_rtmp_auto_push_conf_t)); + if (apcf == NULL) { + return NULL; + } + + apcf->auto_push = NGX_CONF_UNSET; + apcf->push_reconnect = NGX_CONF_UNSET_MSEC; + + return apcf; +} + + +static char * +ngx_rtmp_auto_push_init_conf(ngx_cycle_t *cycle, void *conf) +{ + ngx_rtmp_auto_push_conf_t *apcf = conf; + + ngx_conf_init_value(apcf->auto_push, 0); + ngx_conf_init_msec_value(apcf->push_reconnect, 100); + + if (apcf->socket_dir.len == 0) { + ngx_str_set(&apcf->socket_dir, "/tmp"); + } + + return NGX_CONF_OK; +} + + +#if (NGX_HAVE_UNIX_DOMAIN) +static void +ngx_rtmp_auto_push_reconnect(ngx_event_t *ev) +{ + ngx_rtmp_session_t *s = ev->data; + + ngx_rtmp_auto_push_conf_t *apcf; + ngx_rtmp_auto_push_ctx_t *ctx; + ngx_int_t *slot; + ngx_int_t n; + ngx_rtmp_relay_target_t at; + u_char path[sizeof("unix:") + NGX_MAX_PATH]; + u_char flash_ver[sizeof("APSH ,") + + NGX_INT_T_LEN * 2]; + u_char play_path[NGX_RTMP_MAX_NAME]; + ngx_str_t name; + u_char *p; + ngx_str_t *u; + ngx_pid_t pid; + ngx_int_t npushed; + ngx_core_conf_t *ccf; + ngx_file_info_t fi; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "auto_push: reconnect"); + + apcf = (ngx_rtmp_auto_push_conf_t *) ngx_get_conf(ngx_cycle->conf_ctx, + ngx_rtmp_auto_push_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_auto_push_index_module); + if (ctx == NULL) { + return; + } + + name.data = ctx->name; + name.len = ngx_strlen(name.data); + + ngx_memzero(&at, sizeof(at)); + ngx_str_set(&at.page_url, "nginx-auto-push"); + at.tag = &ngx_rtmp_auto_push_module; + + if (s->app.len) { + at.app.data = s->app.data; + at.app.len = s->app.len; + } + + if (ctx->args[0]) { + at.play_path.data = play_path; + at.play_path.len = ngx_snprintf(play_path, sizeof(play_path), + "%s?%s", ctx->name, ctx->args) - + play_path; + } + + slot = ctx->slots; + npushed = 0; + + for (n = 0; n < NGX_MAX_PROCESSES; ++n, ++slot) { + if (n == ngx_process_slot) { + continue; + } + + pid = ngx_processes[n].pid; + if (pid == 0 || pid == NGX_INVALID_PID) { + continue; + } + + if (*slot) { + npushed++; + continue; + } + + at.data = &ngx_processes[n]; + + ngx_memzero(&at.url, sizeof(at.url)); + u = &at.url.url; + p = ngx_snprintf(path, sizeof(path) - 1, + "unix:%V/" NGX_RTMP_AUTO_PUSH_SOCKNAME ".%P", + &apcf->socket_dir, pid); + *p = 0; + + if (ngx_file_info(path + sizeof("unix:") - 1, &fi) != NGX_OK) { + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "auto_push: " ngx_file_info_n " failed: " + "slot=%i pid=%P socket='%s'" "url='%V' name='%s'", + n, pid, path, u, ctx->name); + continue; + } + + u->data = path; + u->len = p - path; + if (ngx_parse_url(s->connection->pool, &at.url) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "auto_push: auto-push parse_url failed " + "url='%V' name='%s'", + u, ctx->name); + continue; + } + + p = ngx_snprintf(flash_ver, sizeof(flash_ver) - 1, "APSH %i,%i", + (ngx_int_t) ngx_process_slot, (ngx_int_t) ngx_pid); + at.flash_ver.data = flash_ver; + at.flash_ver.len = p - flash_ver; + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "auto_push: connect slot=%i pid=%P socket='%s' name='%s'", + n, pid, path, ctx->name); + + if (ngx_rtmp_relay_push(s, &name, &at) == NGX_OK) { + *slot = 1; + npushed++; + continue; + } + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "auto_push: connect failed: slot=%i pid=%P socket='%s'" + "url='%V' name='%s'", + n, pid, path, u, ctx->name); + } + + ccf = (ngx_core_conf_t *) ngx_get_conf(ngx_cycle->conf_ctx, + ngx_core_module); + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "auto_push: pushed=%i total=%i failed=%i", + npushed, ccf->worker_processes, + ccf->worker_processes - 1 - npushed); + + if (ccf->worker_processes == npushed + 1) { + return; + } + + /* several workers failed */ + + slot = ctx->slots; + + for (n = 0; n < NGX_MAX_PROCESSES; ++n, ++slot) { + pid = ngx_processes[n].pid; + + if (n == ngx_process_slot || *slot == 1 || + pid == 0 || pid == NGX_INVALID_PID) + { + continue; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "auto_push: connect failed: slot=%i pid=%P name='%s'", + n, pid, ctx->name); + } + + if (!ctx->push_evt.timer_set) { + ngx_add_timer(&ctx->push_evt, apcf->push_reconnect); + } +} + + +static ngx_int_t +ngx_rtmp_auto_push_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_auto_push_conf_t *apcf; + ngx_rtmp_auto_push_ctx_t *ctx; + + if (s->auto_pushed || (s->relay && !s->static_relay)) { + goto next; + } + + apcf = (ngx_rtmp_auto_push_conf_t *) ngx_get_conf(ngx_cycle->conf_ctx, + ngx_rtmp_auto_push_module); + if (apcf->auto_push == 0) { + goto next; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_auto_push_index_module); + if (ctx == NULL) { + ctx = ngx_palloc(s->connection->pool, + sizeof(ngx_rtmp_auto_push_ctx_t)); + if (ctx == NULL) { + goto next; + } + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_auto_push_index_module); + + } + ngx_memzero(ctx, sizeof(*ctx)); + + ctx->push_evt.data = s; + ctx->push_evt.log = s->connection->log; + ctx->push_evt.handler = ngx_rtmp_auto_push_reconnect; + + ctx->slots = ngx_pcalloc(s->connection->pool, + sizeof(ngx_int_t) * NGX_MAX_PROCESSES); + if (ctx->slots == NULL) { + goto next; + } + + ngx_memcpy(ctx->name, v->name, sizeof(ctx->name)); + ngx_memcpy(ctx->args, v->args, sizeof(ctx->args)); + + ngx_rtmp_auto_push_reconnect(&ctx->push_evt); + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_auto_push_delete_stream(ngx_rtmp_session_t *s, + ngx_rtmp_delete_stream_t *v) +{ + ngx_rtmp_auto_push_conf_t *apcf; + ngx_rtmp_auto_push_ctx_t *ctx, *pctx; + ngx_rtmp_relay_ctx_t *rctx; + ngx_int_t slot; + + apcf = (ngx_rtmp_auto_push_conf_t *) ngx_get_conf(ngx_cycle->conf_ctx, + ngx_rtmp_auto_push_module); + if (apcf->auto_push == 0) { + goto next; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_auto_push_index_module); + if (ctx) { + if (ctx->push_evt.timer_set) { + ngx_del_timer(&ctx->push_evt); + } + goto next; + } + + /* skip non-relays & publishers */ + rctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (rctx == NULL || + rctx->tag != &ngx_rtmp_auto_push_module || + rctx->publish == NULL) + { + goto next; + } + + slot = (ngx_process_t *) rctx->data - &ngx_processes[0]; + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "auto_push: disconnect slot=%i app='%V' name='%V'", + slot, &rctx->app, &rctx->name); + + pctx = ngx_rtmp_get_module_ctx(rctx->publish->session, + ngx_rtmp_auto_push_index_module); + if (pctx == NULL) { + goto next; + } + + pctx->slots[slot] = 0; + + /* push reconnect */ + if (!pctx->push_evt.timer_set) { + ngx_add_timer(&pctx->push_evt, apcf->push_reconnect); + } + +next: + return next_delete_stream(s, v); +} +#endif /* NGX_HAVE_UNIX_DOMAIN */ diff --git a/ngx_http_flv_module/ngx_rtmp_bandwidth.c b/ngx_http_flv_module/ngx_rtmp_bandwidth.c new file mode 100644 index 0000000..82f9f0d --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_bandwidth.c @@ -0,0 +1,26 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp_bandwidth.h" + + +void +ngx_rtmp_update_bandwidth(ngx_rtmp_bandwidth_t *bw, uint32_t bytes) +{ + if (ngx_cached_time->sec > bw->intl_end) { + bw->bandwidth = ngx_cached_time->sec > + bw->intl_end + NGX_RTMP_BANDWIDTH_INTERVAL + ? 0 + : bw->intl_bytes / NGX_RTMP_BANDWIDTH_INTERVAL; + bw->intl_bytes = 0; + bw->intl_end = ngx_cached_time->sec + NGX_RTMP_BANDWIDTH_INTERVAL; + } + + bw->bytes += bytes; + bw->intl_bytes += bytes; +} diff --git a/ngx_http_flv_module/ngx_rtmp_bandwidth.h b/ngx_http_flv_module/ngx_rtmp_bandwidth.h new file mode 100644 index 0000000..b498482 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_bandwidth.h @@ -0,0 +1,31 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_BANDWIDTH_H_INCLUDED_ +#define _NGX_RTMP_BANDWIDTH_H_INCLUDED_ + + +#include +#include + + +/* Bandwidth update interval in seconds */ +#define NGX_RTMP_BANDWIDTH_INTERVAL 10 + + +typedef struct { + uint64_t bytes; + uint64_t bandwidth; /* bytes/sec */ + + time_t intl_end; + uint64_t intl_bytes; +} ngx_rtmp_bandwidth_t; + + +void ngx_rtmp_update_bandwidth(ngx_rtmp_bandwidth_t *bw, uint32_t bytes); + + +#endif /* _NGX_RTMP_BANDWIDTH_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_bitop.c b/ngx_http_flv_module/ngx_rtmp_bitop.c new file mode 100644 index 0000000..855d425 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_bitop.c @@ -0,0 +1,63 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp_bitop.h" + + +void +ngx_rtmp_bit_init_reader(ngx_rtmp_bit_reader_t *br, u_char *pos, u_char *last) +{ + ngx_memzero(br, sizeof(ngx_rtmp_bit_reader_t)); + + br->pos = pos; + br->last = last; +} + + +uint64_t +ngx_rtmp_bit_read(ngx_rtmp_bit_reader_t *br, ngx_uint_t n) +{ + uint64_t v; + ngx_uint_t d; + + v = 0; + + while (n) { + + if (br->pos >= br->last) { + br->err = 1; + return 0; + } + + d = (br->offs + n > 8 ? (ngx_uint_t) (8 - br->offs) : n); + + v <<= d; + v += (*br->pos >> (8 - br->offs - d)) & ((u_char) 0xff >> (8 - d)); + + br->offs += d; + n -= d; + + if (br->offs == 8) { + br->pos++; + br->offs = 0; + } + } + + return v; +} + + +uint64_t +ngx_rtmp_bit_read_golomb(ngx_rtmp_bit_reader_t *br) +{ + ngx_uint_t n; + + for (n = 0; ngx_rtmp_bit_read(br, 1) == 0 && !br->err; n++); + + return ((uint64_t) 1 << n) + ngx_rtmp_bit_read(br, n) - 1; +} diff --git a/ngx_http_flv_module/ngx_rtmp_bitop.h b/ngx_http_flv_module/ngx_rtmp_bitop.h new file mode 100644 index 0000000..25133d2 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_bitop.h @@ -0,0 +1,46 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_BITOP_H_INCLUDED_ +#define _NGX_RTMP_BITOP_H_INCLUDED_ + + +#include +#include + + +typedef struct { + u_char *pos; + u_char *last; + ngx_uint_t offs; + ngx_uint_t err; +} ngx_rtmp_bit_reader_t; + + +void ngx_rtmp_bit_init_reader(ngx_rtmp_bit_reader_t *br, u_char *pos, + u_char *last); +uint64_t ngx_rtmp_bit_read(ngx_rtmp_bit_reader_t *br, ngx_uint_t n); +uint64_t ngx_rtmp_bit_read_golomb(ngx_rtmp_bit_reader_t *br); + + +#define ngx_rtmp_bit_read_err(br) ((br)->err) + +#define ngx_rtmp_bit_read_eof(br) ((br)->pos == (br)->last) + +#define ngx_rtmp_bit_read_8(br) \ + ((uint8_t) ngx_rtmp_bit_read(br, 8)) + +#define ngx_rtmp_bit_read_16(br) \ + ((uint16_t) ngx_rtmp_bit_read(br, 16)) + +#define ngx_rtmp_bit_read_32(br) \ + ((uint32_t) ngx_rtmp_bit_read(br, 32)) + +#define ngx_rtmp_bit_read_64(br) \ + ((uint64_t) ngx_rtmp_bit_read(br, 64)) + + +#endif /* _NGX_RTMP_BITOP_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_cmd_module.c b/ngx_http_flv_module/ngx_rtmp_cmd_module.c new file mode 100644 index 0000000..ede6496 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_cmd_module.c @@ -0,0 +1,973 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_streams.h" + + +#define NGX_RTMP_FMS_VERSION "FMS/3,0,1,123" +#define NGX_RTMP_CAPABILITIES 31 + + +static ngx_int_t ngx_rtmp_cmd_connect(ngx_rtmp_session_t *s, + ngx_rtmp_connect_t *v); +static ngx_int_t ngx_rtmp_cmd_disconnect(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_cmd_create_stream(ngx_rtmp_session_t *s, + ngx_rtmp_create_stream_t *v); +static ngx_int_t ngx_rtmp_cmd_close_stream(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v); +static ngx_int_t ngx_rtmp_cmd_delete_stream(ngx_rtmp_session_t *s, + ngx_rtmp_delete_stream_t *v); +static ngx_int_t ngx_rtmp_cmd_publish(ngx_rtmp_session_t *s, + ngx_rtmp_publish_t *v); +static ngx_int_t ngx_rtmp_cmd_play(ngx_rtmp_session_t *s, + ngx_rtmp_play_t *v); +static ngx_int_t ngx_rtmp_cmd_seek(ngx_rtmp_session_t *s, + ngx_rtmp_seek_t *v); +static ngx_int_t ngx_rtmp_cmd_pause(ngx_rtmp_session_t *s, + ngx_rtmp_pause_t *v); + + +static ngx_int_t ngx_rtmp_cmd_stream_begin(ngx_rtmp_session_t *s, + ngx_rtmp_stream_begin_t *v); +static ngx_int_t ngx_rtmp_cmd_stream_eof(ngx_rtmp_session_t *s, + ngx_rtmp_stream_eof_t *v); +static ngx_int_t ngx_rtmp_cmd_stream_dry(ngx_rtmp_session_t *s, + ngx_rtmp_stream_dry_t *v); +static ngx_int_t ngx_rtmp_cmd_recorded(ngx_rtmp_session_t *s, + ngx_rtmp_recorded_t *v); +static ngx_int_t ngx_rtmp_cmd_set_buflen(ngx_rtmp_session_t *s, + ngx_rtmp_set_buflen_t *v); + + +ngx_rtmp_connect_pt ngx_rtmp_connect; +ngx_rtmp_disconnect_pt ngx_rtmp_disconnect; +ngx_rtmp_create_stream_pt ngx_rtmp_create_stream; +ngx_rtmp_close_stream_pt ngx_rtmp_close_stream; +ngx_rtmp_delete_stream_pt ngx_rtmp_delete_stream; +ngx_rtmp_publish_pt ngx_rtmp_publish; +ngx_rtmp_play_pt ngx_rtmp_play; +ngx_rtmp_seek_pt ngx_rtmp_seek; +ngx_rtmp_pause_pt ngx_rtmp_pause; + + +ngx_rtmp_stream_begin_pt ngx_rtmp_stream_begin; +ngx_rtmp_stream_eof_pt ngx_rtmp_stream_eof; +ngx_rtmp_stream_dry_pt ngx_rtmp_stream_dry; +ngx_rtmp_recorded_pt ngx_rtmp_recorded; +ngx_rtmp_set_buflen_pt ngx_rtmp_set_buflen; + + +static ngx_int_t ngx_rtmp_cmd_postconfiguration(ngx_conf_t *cf); + + +static ngx_rtmp_module_t ngx_rtmp_cmd_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_cmd_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + NULL, /* create app configuration */ + NULL /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_cmd_module = { + NGX_MODULE_V1, + &ngx_rtmp_cmd_module_ctx, /* module context */ + NULL, /* module directives */ + NGX_RTMP_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 +}; + + +void +ngx_rtmp_cmd_fill_args(u_char name[NGX_RTMP_MAX_NAME], + u_char args[NGX_RTMP_MAX_ARGS]) +{ + u_char *p; + + p = (u_char *)ngx_strchr(name, '?'); + if (p == NULL) { + return; + } + + *p++ = 0; + ngx_cpystrn(args, p, NGX_RTMP_MAX_ARGS); +} + + +static ngx_int_t +ngx_rtmp_cmd_connect_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + size_t len; + + static ngx_rtmp_connect_t v; + + static ngx_rtmp_amf_elt_t in_cmd[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("app"), + v.app, sizeof(v.app) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("flashVer"), + v.flashver, sizeof(v.flashver) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("swfUrl"), + v.swf_url, sizeof(v.swf_url) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("tcUrl"), + v.tc_url, sizeof(v.tc_url) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("audioCodecs"), + &v.acodecs, sizeof(v.acodecs) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("videoCodecs"), + &v.vcodecs, sizeof(v.vcodecs) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("pageUrl"), + v.page_url, sizeof(v.page_url) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("serverName"), + v.server_name, sizeof(v.server_name) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("objectEncoding"), + &v.object_encoding, 0}, + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.trans, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_cmd, sizeof(in_cmd) }, + }; + + ngx_memzero(&v, sizeof(v)); + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + if (v.tc_url[0]) { + /* compatibility for case: rtmps -> converter -> rtmp */ + if (ngx_strncasecmp(v.tc_url, (u_char *) "rtmps://", 8) == 0) { + ngx_log_error(NGX_LOG_WARN, s->connection->log, 0, + "connect: rtmps tcUrl received: %s", v.tc_url); + + ngx_memmove(v.tc_url + 4, v.tc_url + 5, ngx_strlen(v.tc_url) - 5); + } + } + +#define NGX_RTMP_SET_STRPAR(name) \ + s->name.len = ngx_strlen(v.name); \ + s->name.data = ngx_palloc(s->connection->pool, s->name.len); \ + if (s->name.data == NULL) { \ + return NGX_ERROR; \ + } \ + ngx_memcpy(s->name.data, v.name, s->name.len) + + NGX_RTMP_SET_STRPAR(app); + NGX_RTMP_SET_STRPAR(args); + NGX_RTMP_SET_STRPAR(flashver); + NGX_RTMP_SET_STRPAR(swf_url); + NGX_RTMP_SET_STRPAR(tc_url); + NGX_RTMP_SET_STRPAR(page_url); + +#undef NGX_RTMP_SET_STRPAR + + if (s->auto_pushed) { + s->host_start = v.server_name; + s->host_end = v.server_name + ngx_strlen(v.server_name); + } + + if (ngx_rtmp_process_virtual_host(s) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "connect: failed to process virtual host"); + + return NGX_ERROR; + } + + ngx_rtmp_cmd_fill_args(v.app, v.args); + + len = ngx_strlen(v.app); + if (len > 10 && !ngx_memcmp(v.app + len - 10, "/_definst_", 10)) { + v.app[len - 10] = 0; + } else if (len && v.app[len - 1] == '/') { + v.app[len - 1] = 0; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "connect: app='%s' args='%s' flashver='%s' swf_url='%s' " + "tc_url='%s' page_url='%s' acodecs=%uD vcodecs=%uD " + "object_encoding=%ui", + v.app, v.args, v.flashver, v.swf_url, v.tc_url, v.page_url, + (uint32_t)v.acodecs, (uint32_t)v.vcodecs, + (ngx_int_t)v.object_encoding); + + return ngx_rtmp_connect(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_connect(ngx_rtmp_session_t *s, ngx_rtmp_connect_t *v) +{ + int tcp_nodelay; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_core_app_conf_t **cacfp, *cacf; + ngx_uint_t n; + ngx_rtmp_header_t h; + ngx_connection_t *c; + u_char *p; + + static double trans; + static double capabilities = NGX_RTMP_CAPABILITIES; + static double object_encoding = 0; + + static ngx_rtmp_amf_elt_t out_obj[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("fmsVer"), + NGX_RTMP_FMS_VERSION, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("capabilities"), + &capabilities, 0 }, + }; + + static ngx_rtmp_amf_elt_t out_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + "status", 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("code"), + "NetConnection.Connect.Success", 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("description"), + "Connection succeeded.", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("objectEncoding"), + &object_encoding, 0 } + }; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "_result", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &trans, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + out_obj, sizeof(out_obj) }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + out_inf, sizeof(out_inf) }, + }; + + if (s->connected) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "connect: duplicate connection"); + return NGX_ERROR; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + trans = v->trans; + + /* fill session parameters */ + s->connected = 1; + + ngx_memzero(&h, sizeof(h)); + h.csid = NGX_RTMP_CSID_AMF_INI; + h.type = NGX_RTMP_MSG_AMF_CMD; + + +#define NGX_RTMP_SET_STRPAR(name) \ + do { \ + if (s->name.len != ngx_strlen(v->name) \ + || ngx_strncasecmp(s->name.data, v->name, s->name.len)) \ + { \ + s->name.len = ngx_strlen(v->name); \ + s->name.data = ngx_palloc(s->connection->pool, s->name.len); \ + if (s->name.data == NULL) { \ + return NGX_ERROR; \ + } \ + ngx_memcpy(s->name.data, v->name, s->name.len); \ + } \ + } while (0) + + NGX_RTMP_SET_STRPAR(app); + NGX_RTMP_SET_STRPAR(args); + NGX_RTMP_SET_STRPAR(flashver); + NGX_RTMP_SET_STRPAR(swf_url); + NGX_RTMP_SET_STRPAR(tc_url); + NGX_RTMP_SET_STRPAR(page_url); + +#undef NGX_RTMP_SET_STRPAR + + p = ngx_strlchr(s->app.data, s->app.data + s->app.len, '?'); + if (p) { + s->app.len = (p - s->app.data); + } + + s->acodecs = (uint32_t) v->acodecs; + s->vcodecs = (uint32_t) v->vcodecs; + + /* find application & set app_conf */ + cacfp = cscf->applications.elts; + for(n = 0; n < cscf->applications.nelts; ++n, ++cacfp) { + if ((*cacfp)->name.len == s->app.len && + ngx_strncmp((*cacfp)->name.data, s->app.data, s->app.len) == 0) + { + /* found app! */ + s->app_conf = (*cacfp)->app_conf; + break; + } + } + + if (s->app_conf == NULL) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "connect: application not found: '%V'", &s->app); + return NGX_ERROR; + } + + object_encoding = v->object_encoding; + + if (s->data == NULL) { + cacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_core_module); + c = s->connection; + + if (!cacf->tcp_nopush) { + c->tcp_nopush = NGX_TCP_NOPUSH_DISABLED; + } + + if (c->tcp_nopush == NGX_TCP_NOPUSH_SET) { + if (ngx_tcp_push(c->fd) == -1) { + ngx_connection_error(c, ngx_socket_errno, + ngx_tcp_push_n " failed"); + return NGX_ERROR; + } + + c->tcp_nopush = NGX_TCP_NOPUSH_UNSET; + tcp_nodelay = ngx_tcp_nodelay_and_tcp_nopush ? 1 : 0; + } else { + tcp_nodelay = 1; + } + + if (tcp_nodelay && cacf->tcp_nodelay && ngx_tcp_nodelay(c) != NGX_OK) { + return NGX_ERROR; + } + } + + return ngx_rtmp_send_ack_size(s, cscf->ack_window) != NGX_OK || + ngx_rtmp_send_bandwidth(s, cscf->ack_window, + NGX_RTMP_LIMIT_DYNAMIC) != NGX_OK || + ngx_rtmp_send_chunk_size(s, cscf->chunk_size) != NGX_OK || + ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])) + != NGX_OK ? NGX_ERROR : NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_create_stream_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_create_stream_t v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.trans, sizeof(v.trans) }, + }; + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, "createStream"); + + return ngx_rtmp_create_stream(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_create_stream(ngx_rtmp_session_t *s, ngx_rtmp_create_stream_t *v) +{ + /* support one message stream per connection */ + static double stream; + static double trans; + ngx_rtmp_header_t h; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "_result", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &stream, sizeof(stream) }, + }; + + trans = v->trans; + stream = NGX_RTMP_MSID; + + ngx_memzero(&h, sizeof(h)); + + h.csid = NGX_RTMP_CSID_AMF_INI; + h.type = NGX_RTMP_MSG_AMF_CMD; + + return ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])) == NGX_OK ? + NGX_DONE : NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_cmd_close_stream_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_close_stream_t v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.stream, 0 }, + }; + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, "closeStream"); + + return ngx_rtmp_close_stream(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_delete_stream_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_delete_stream_t v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.stream, 0 }, + }; + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + return ngx_rtmp_delete_stream(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_delete_stream(ngx_rtmp_session_t *s, ngx_rtmp_delete_stream_t *v) +{ + ngx_rtmp_close_stream_t cv; + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, "deleteStream"); + + cv.stream = 0; + + return ngx_rtmp_close_stream(s, &cv); +} + + +static ngx_int_t +ngx_rtmp_cmd_publish_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_publish_t v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + /* transaction is always 0 */ + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + &v.name, sizeof(v.name) }, + + { NGX_RTMP_AMF_OPTIONAL | NGX_RTMP_AMF_STRING, + ngx_null_string, + &v.type, sizeof(v.type) }, + }; + + ngx_memzero(&v, sizeof(v)); + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_rtmp_cmd_fill_args(v.name, v.args); + + if (ngx_strlen(v.name) == 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "publish: no stream name specified"); + + return NGX_ERROR; + } + + if (ngx_rtmp_process_request_line(s, v.name, v.args, + (const u_char *) "publish") != NGX_OK) + { + return NGX_ERROR; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "publish: name='%s' args='%s' type=%s silent=%d", + v.name, v.args, v.type, v.silent); + + return ngx_rtmp_publish(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + return NGX_OK; +} + +static ngx_int_t +ngx_rtmp_cmd_play_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_play_t v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + /* transaction is always 0 */ + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + &v.name, sizeof(v.name) }, + + { NGX_RTMP_AMF_OPTIONAL | NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.start, 0 }, + + { NGX_RTMP_AMF_OPTIONAL | NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.duration, 0 }, + + { NGX_RTMP_AMF_OPTIONAL | NGX_RTMP_AMF_BOOLEAN, + ngx_null_string, + &v.reset, 0 } + }; + + ngx_memzero(&v, sizeof(v)); + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_rtmp_cmd_fill_args(v.name, v.args); + + if (ngx_strlen(v.name) == 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "play: no stream name specified"); + + return NGX_ERROR; + } + + if (ngx_rtmp_process_request_line(s, v.name, v.args, + (const u_char *) "play") != NGX_OK) + { + return NGX_ERROR; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "play: name='%s' args='%s' start=%i duration=%i " + "reset=%i silent=%i", + v.name, v.args, (ngx_int_t) v.start, + (ngx_int_t) v.duration, (ngx_int_t) v.reset, + (ngx_int_t) v.silent); + + return ngx_rtmp_play(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_add_timer(s->connection->write, s->timeout); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_play2_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_play_t v; + static ngx_rtmp_close_stream_t vc; + + static ngx_rtmp_amf_elt_t in_obj[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_string("start"), + &v.start, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("streamName"), + &v.name, sizeof(v.name) }, + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + /* transaction is always 0 */ + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + &in_obj, sizeof(in_obj) } + }; + + ngx_memzero(&v, sizeof(v)); + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_rtmp_cmd_fill_args(v.name, v.args); + + if (ngx_strlen(v.name) == 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "play2: no stream name specified"); + + return NGX_ERROR; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "play2: name='%s' args='%s' start=%i", + v.name, v.args, (ngx_int_t) v.start); + + /* continue from current timestamp */ + + if (v.start < 0) { + v.start = s->current_time; + } + + ngx_memzero(&vc, sizeof(vc)); + + /* close_stream should be synchronous */ + ngx_rtmp_close_stream(s, &vc); + + return ngx_rtmp_play(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_pause_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_pause_t v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_BOOLEAN, + ngx_null_string, + &v.pause, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.position, 0 }, + }; + + ngx_memzero(&v, sizeof(v)); + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "pause: pause=%i position=%i", + (ngx_int_t) v.pause, (ngx_int_t) v.position); + + return ngx_rtmp_pause(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) +{ + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_disconnect_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, "disconnect"); + + return ngx_rtmp_disconnect(s); +} + + +static ngx_int_t +ngx_rtmp_cmd_disconnect(ngx_rtmp_session_t *s) +{ + return ngx_rtmp_delete_stream(s, NULL); +} + + +static ngx_int_t +ngx_rtmp_cmd_seek_init(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_seek_t v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + /* transaction is always 0 */ + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.offset, sizeof(v.offset) }, + }; + + ngx_memzero(&v, sizeof(v)); + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "seek: offset=%i", (ngx_int_t) v.offset); + + return ngx_rtmp_seek(s, &v); +} + + +static ngx_int_t +ngx_rtmp_cmd_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) +{ + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_stream_begin(ngx_rtmp_session_t *s, ngx_rtmp_stream_begin_t *v) +{ + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_stream_eof(ngx_rtmp_session_t *s, ngx_rtmp_stream_eof_t *v) +{ + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_stream_dry(ngx_rtmp_session_t *s, ngx_rtmp_stream_dry_t *v) +{ + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_recorded(ngx_rtmp_session_t *s, + ngx_rtmp_recorded_t *v) +{ + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_cmd_set_buflen(ngx_rtmp_session_t *s, ngx_rtmp_set_buflen_t *v) +{ + return NGX_OK; +} + + +static ngx_rtmp_amf_handler_t ngx_rtmp_cmd_map[] = { + { ngx_string("connect"), ngx_rtmp_cmd_connect_init }, + { ngx_string("createStream"), ngx_rtmp_cmd_create_stream_init }, + { ngx_string("closeStream"), ngx_rtmp_cmd_close_stream_init }, + { ngx_string("deleteStream"), ngx_rtmp_cmd_delete_stream_init }, + { ngx_string("publish"), ngx_rtmp_cmd_publish_init }, + { ngx_string("play"), ngx_rtmp_cmd_play_init }, + { ngx_string("play2"), ngx_rtmp_cmd_play2_init }, + { ngx_string("seek"), ngx_rtmp_cmd_seek_init }, + { ngx_string("pause"), ngx_rtmp_cmd_pause_init }, + { ngx_string("pauseraw"), ngx_rtmp_cmd_pause_init }, +}; + + +static ngx_int_t +ngx_rtmp_cmd_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + ngx_rtmp_amf_handler_t *ch, *bh; + size_t n, ncalls; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + /* redirect disconnects to deleteStream + * to free client modules from registering + * disconnect callback */ + + h = ngx_array_push(&cmcf->events[NGX_RTMP_DISCONNECT]); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_rtmp_cmd_disconnect_init; + + /* register AMF callbacks */ + + ncalls = sizeof(ngx_rtmp_cmd_map) / sizeof(ngx_rtmp_cmd_map[0]); + + ch = ngx_array_push_n(&cmcf->amf, ncalls); + if (ch == NULL) { + return NGX_ERROR; + } + + bh = ngx_rtmp_cmd_map; + + for(n = 0; n < ncalls; ++n, ++ch, ++bh) { + *ch = *bh; + } + + ngx_rtmp_connect = ngx_rtmp_cmd_connect; + ngx_rtmp_disconnect = ngx_rtmp_cmd_disconnect; + ngx_rtmp_create_stream = ngx_rtmp_cmd_create_stream; + ngx_rtmp_close_stream = ngx_rtmp_cmd_close_stream; + ngx_rtmp_delete_stream = ngx_rtmp_cmd_delete_stream; + ngx_rtmp_publish = ngx_rtmp_cmd_publish; + ngx_rtmp_play = ngx_rtmp_cmd_play; + ngx_rtmp_seek = ngx_rtmp_cmd_seek; + ngx_rtmp_pause = ngx_rtmp_cmd_pause; + + ngx_rtmp_stream_begin = ngx_rtmp_cmd_stream_begin; + ngx_rtmp_stream_eof = ngx_rtmp_cmd_stream_eof; + ngx_rtmp_stream_dry = ngx_rtmp_cmd_stream_dry; + ngx_rtmp_recorded = ngx_rtmp_cmd_recorded; + ngx_rtmp_set_buflen = ngx_rtmp_cmd_set_buflen; + + return NGX_OK; +} + diff --git a/ngx_http_flv_module/ngx_rtmp_cmd_module.h b/ngx_http_flv_module/ngx_rtmp_cmd_module.h new file mode 100644 index 0000000..be448f2 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_cmd_module.h @@ -0,0 +1,152 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_CMD_H_INCLUDED_ +#define _NGX_RTMP_CMD_H_INCLUDED_ + + +#include +#include +#include +#include "ngx_rtmp.h" + + +#define NGX_RTMP_MAX_NAME 256 +#define NGX_RTMP_MAX_URL 256 +#define NGX_RTMP_MAX_ARGS NGX_RTMP_MAX_NAME + + +/* Basic RTMP call support */ + +typedef struct { + double trans; + u_char app[NGX_RTMP_MAX_NAME]; + u_char args[NGX_RTMP_MAX_ARGS]; + u_char flashver[64]; + u_char swf_url[NGX_RTMP_MAX_URL]; + u_char tc_url[NGX_RTMP_MAX_URL]; + double acodecs; + double vcodecs; + u_char page_url[NGX_RTMP_MAX_URL]; + u_char server_name[NGX_RTMP_MAX_URL]; + double object_encoding; +} ngx_rtmp_connect_t; + + +typedef struct { + double trans; + double stream; +} ngx_rtmp_create_stream_t; + + +typedef struct { + double stream; +} ngx_rtmp_delete_stream_t; + + +typedef struct { + double stream; +} ngx_rtmp_close_stream_t; + + +typedef struct { + u_char name[NGX_RTMP_MAX_NAME]; + u_char args[NGX_RTMP_MAX_ARGS]; + u_char type[16]; + int silent; +} ngx_rtmp_publish_t; + + +typedef struct { + u_char name[NGX_RTMP_MAX_NAME]; + u_char args[NGX_RTMP_MAX_ARGS]; + double start; + double duration; + int reset; + int silent; +} ngx_rtmp_play_t; + + +typedef struct { + double offset; +} ngx_rtmp_seek_t; + + +typedef struct { + uint8_t pause; + double position; +} ngx_rtmp_pause_t; + + +typedef struct { + uint32_t msid; +} ngx_rtmp_msid_t; + + +typedef ngx_rtmp_msid_t ngx_rtmp_stream_begin_t; +typedef ngx_rtmp_msid_t ngx_rtmp_stream_eof_t; +typedef ngx_rtmp_msid_t ngx_rtmp_stream_dry_t; +typedef ngx_rtmp_msid_t ngx_rtmp_recorded_t; + + +typedef struct { + uint32_t msid; + uint32_t buflen; +} ngx_rtmp_set_buflen_t; + + +void ngx_rtmp_cmd_fill_args(u_char name[NGX_RTMP_MAX_NAME], + u_char args[NGX_RTMP_MAX_ARGS]); + + +typedef ngx_int_t (*ngx_rtmp_connect_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_connect_t *v); +typedef ngx_int_t (*ngx_rtmp_disconnect_pt)(ngx_rtmp_session_t *s); +typedef ngx_int_t (*ngx_rtmp_create_stream_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_create_stream_t *v); +typedef ngx_int_t (*ngx_rtmp_close_stream_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v); +typedef ngx_int_t (*ngx_rtmp_delete_stream_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_delete_stream_t *v); +typedef ngx_int_t (*ngx_rtmp_publish_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_publish_t *v); +typedef ngx_int_t (*ngx_rtmp_play_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_play_t *v); +typedef ngx_int_t (*ngx_rtmp_seek_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_seek_t *v); +typedef ngx_int_t (*ngx_rtmp_pause_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_pause_t *v); + +typedef ngx_int_t (*ngx_rtmp_stream_begin_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_stream_begin_t *v); +typedef ngx_int_t (*ngx_rtmp_stream_eof_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_stream_eof_t *v); +typedef ngx_int_t (*ngx_rtmp_stream_dry_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_stream_dry_t *v); +typedef ngx_int_t (*ngx_rtmp_recorded_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_recorded_t *v); +typedef ngx_int_t (*ngx_rtmp_set_buflen_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_set_buflen_t *v); + + +extern ngx_rtmp_connect_pt ngx_rtmp_connect; +extern ngx_rtmp_disconnect_pt ngx_rtmp_disconnect; +extern ngx_rtmp_create_stream_pt ngx_rtmp_create_stream; +extern ngx_rtmp_close_stream_pt ngx_rtmp_close_stream; +extern ngx_rtmp_delete_stream_pt ngx_rtmp_delete_stream; +extern ngx_rtmp_publish_pt ngx_rtmp_publish; +extern ngx_rtmp_play_pt ngx_rtmp_play; +extern ngx_rtmp_seek_pt ngx_rtmp_seek; +extern ngx_rtmp_pause_pt ngx_rtmp_pause; + +extern ngx_rtmp_stream_begin_pt ngx_rtmp_stream_begin; +extern ngx_rtmp_stream_eof_pt ngx_rtmp_stream_eof; +extern ngx_rtmp_stream_dry_pt ngx_rtmp_stream_dry; +extern ngx_rtmp_set_buflen_pt ngx_rtmp_set_buflen; +extern ngx_rtmp_recorded_pt ngx_rtmp_recorded; + + +#endif /*_NGX_RTMP_CMD_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_codec_module.c b/ngx_http_flv_module/ngx_rtmp_codec_module.c new file mode 100644 index 0000000..3e3f3c0 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_codec_module.c @@ -0,0 +1,1031 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_codec_module.h" +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_bitop.h" + + +#define NGX_RTMP_CODEC_META_OFF 0 +#define NGX_RTMP_CODEC_META_ON 1 +#define NGX_RTMP_CODEC_META_COPY 2 + + +static void * ngx_rtmp_codec_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_codec_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +static ngx_int_t ngx_rtmp_codec_postconfiguration(ngx_conf_t *cf); +static ngx_int_t ngx_rtmp_codec_reconstruct_meta(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_codec_copy_meta(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); +static ngx_int_t ngx_rtmp_codec_prepare_meta(ngx_rtmp_session_t *s, + uint32_t timestamp); +static ngx_int_t ngx_rtmp_codec_parse_aac_header(ngx_rtmp_session_t *s, + ngx_chain_t *in); +static ngx_int_t ngx_rtmp_codec_parse_avc_header(ngx_rtmp_session_t *s, + ngx_chain_t *in); +#if (NGX_DEBUG) +static void ngx_rtmp_codec_dump_header(ngx_rtmp_session_t *s, const char *type, + ngx_chain_t *in); +#endif + + +typedef struct { + ngx_uint_t meta; +} ngx_rtmp_codec_app_conf_t; + + +static ngx_conf_enum_t ngx_rtmp_codec_meta_slots[] = { + { ngx_string("off"), NGX_RTMP_CODEC_META_OFF }, + { ngx_string("on"), NGX_RTMP_CODEC_META_ON }, + { ngx_string("copy"), NGX_RTMP_CODEC_META_COPY }, + { ngx_null_string, 0 } +}; + + +static ngx_command_t ngx_rtmp_codec_commands[] = { + + { ngx_string("meta"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_enum_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_codec_app_conf_t, meta), + &ngx_rtmp_codec_meta_slots }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_codec_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_codec_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + ngx_rtmp_codec_create_app_conf, /* create app configuration */ + ngx_rtmp_codec_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_codec_module = { + NGX_MODULE_V1, + &ngx_rtmp_codec_module_ctx, /* module context */ + ngx_rtmp_codec_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static const char * +audio_codecs[] = { + "", + "ADPCM", + "MP3", + "LinearLE", + "Nellymoser16", + "Nellymoser8", + "Nellymoser", + "G711A", + "G711U", + "", + "AAC", + "Speex", + "", + "", + "MP3-8K", + "DeviceSpecific", + "Uncompressed" +}; + + +static const char * +video_codecs[] = { + "", + "Jpeg", + "Sorenson-H263", + "ScreenVideo", + "On2-VP6", + "On2-VP6-Alpha", + "ScreenVideo2", + "H264", +}; + + +u_char * +ngx_rtmp_get_audio_codec_name(ngx_uint_t id) +{ + return (u_char *)(id < sizeof(audio_codecs) / sizeof(audio_codecs[0]) + ? audio_codecs[id] + : ""); +} + + +u_char * +ngx_rtmp_get_video_codec_name(ngx_uint_t id) +{ + return (u_char *)(id < sizeof(video_codecs) / sizeof(video_codecs[0]) + ? video_codecs[id] + : ""); +} + + +static ngx_uint_t +ngx_rtmp_codec_get_next_version() +{ + ngx_uint_t v; + static ngx_uint_t version; + + do { + v = ++version; + } while (v == 0); + + return v; +} + + +static ngx_int_t +ngx_rtmp_codec_disconnect(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_codec_ctx_t *ctx; + ngx_rtmp_core_srv_conf_t *cscf; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (ctx == NULL) { + return NGX_OK; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + if (ctx->avc_header) { + ngx_rtmp_free_shared_chain(cscf, ctx->avc_header); + ctx->avc_header = NULL; + } + + if (ctx->aac_header) { + ngx_rtmp_free_shared_chain(cscf, ctx->aac_header); + ctx->aac_header = NULL; + } + + if (ctx->meta) { + ngx_rtmp_free_shared_chain(cscf, ctx->meta); + ctx->meta = NULL; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_codec_ctx_t *ctx; + ngx_chain_t **header; + uint8_t fmt; + static ngx_uint_t sample_rates[] = + { 5512, 11025, 22050, 44100 }; + + if (h->type != NGX_RTMP_MSG_AUDIO && h->type != NGX_RTMP_MSG_VIDEO) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_codec_ctx_t)); + if (ctx == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "codec: failed to allocate for ctx"); + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_codec_module); + } + + /* save codec */ + if (in->buf->last - in->buf->pos < 1) { + return NGX_OK; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + fmt = in->buf->pos[0]; + if (h->type == NGX_RTMP_MSG_AUDIO) { + ctx->audio_codec_id = (fmt & 0xf0) >> 4; + ctx->audio_channels = (fmt & 0x01) + 1; + ctx->sample_size = (fmt & 0x02) ? 2 : 1; + + if (ctx->sample_rate == 0) { + ctx->sample_rate = sample_rates[(fmt & 0x0c) >> 2]; + } + + } else { + ctx->video_codec_id = (fmt & 0x0f); + } + + /* save AVC/AAC header */ + if (in->buf->last - in->buf->pos < 3) { + return NGX_OK; + } + + /* PacketType = 0, FLV TAG MUST be sequence header */ + /* PacketType = 1, FLV TAG MAY be AVC NALU or AAC Raw */ + if (!ngx_rtmp_is_codec_header(in)) { + return NGX_OK; + } + + header = NULL; + + /* MUST be audio / video sequence header */ + if (h->type == NGX_RTMP_MSG_AUDIO) { + if (ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC) { + header = &ctx->aac_header; + if (ngx_rtmp_codec_parse_aac_header(s, in) == NGX_ERROR) { + return NGX_ERROR; + } + } + } else { + if (ctx->video_codec_id == NGX_RTMP_VIDEO_H264) { + header = &ctx->avc_header; + if (ngx_rtmp_codec_parse_avc_header(s, in) == NGX_ERROR) { + return NGX_ERROR; + } + } + } + + if (header == NULL) { + return NGX_OK; + } + + if (*header) { + ngx_rtmp_free_shared_chain(cscf, *header); + } + + *header = ngx_rtmp_append_shared_bufs(cscf, NULL, in); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_codec_parse_aac_header(ngx_rtmp_session_t *s, ngx_chain_t *in) +{ + ngx_uint_t idx; + ngx_rtmp_codec_ctx_t *ctx; + ngx_rtmp_bit_reader_t br; + + static ngx_uint_t aac_sample_rates[] = + { 96000, 88200, 64000, 48000, + 44100, 32000, 24000, 22050, + 16000, 12000, 11025, 8000, + 7350, 0, 0, 0 }; + +#if (NGX_DEBUG) + ngx_rtmp_codec_dump_header(s, "aac", in); +#endif + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (in->buf->last - in->buf->pos < 4) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "codec: invalid audio codec header size=%ui", + in->buf->last - in->buf->pos); + + return NGX_ERROR; + } + + ngx_rtmp_bit_init_reader(&br, in->buf->pos, in->buf->last); + + ngx_rtmp_bit_read(&br, 16); + + ctx->aac_profile = (ngx_uint_t) ngx_rtmp_bit_read(&br, 5); + if (ctx->aac_profile == 31) { + ctx->aac_profile = (ngx_uint_t) ngx_rtmp_bit_read(&br, 6) + 32; + } + + idx = (ngx_uint_t) ngx_rtmp_bit_read(&br, 4); + if (idx == 15) { + ctx->sample_rate = (ngx_uint_t) ngx_rtmp_bit_read(&br, 24); + } else { + ctx->sample_rate = aac_sample_rates[idx]; + } + + ctx->aac_chan_conf = (ngx_uint_t) ngx_rtmp_bit_read(&br, 4); + + if (ctx->aac_profile == 5 || ctx->aac_profile == 29) { + + if (ctx->aac_profile == 29) { + ctx->aac_ps = 1; + } + + ctx->aac_sbr = 1; + + idx = (ngx_uint_t) ngx_rtmp_bit_read(&br, 4); + if (idx == 15) { + ctx->sample_rate = (ngx_uint_t) ngx_rtmp_bit_read(&br, 24); + } else { + ctx->sample_rate = aac_sample_rates[idx]; + } + + ctx->aac_profile = (ngx_uint_t) ngx_rtmp_bit_read(&br, 5); + if (ctx->aac_profile == 31) { + ctx->aac_profile = (ngx_uint_t) ngx_rtmp_bit_read(&br, 6) + 32; + } + } + + /* 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 + + if (object_type == 5) + 4 bits: frequency index + if (frequency index == 15) + 24 bits: frequency + 5 bits: object type + if (object type == 31) + 6 bits + 32: object type + + var bits: AOT Specific Config + */ + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "codec: aac header profile=%ui, " + "sample_rate=%ui, chan_conf=%ui", + ctx->aac_profile, ctx->sample_rate, ctx->aac_chan_conf); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_codec_parse_avc_header(ngx_rtmp_session_t *s, ngx_chain_t *in) +{ + ngx_uint_t avc_config_version; + ngx_uint_t profile_idc, width, height, crop_left, crop_right, + crop_top, crop_bottom, frame_mbs_only, n, cf_idc, + num_ref_frames; + ngx_rtmp_codec_ctx_t *ctx; + ngx_rtmp_bit_reader_t br; + +#if (NGX_DEBUG) + ngx_rtmp_codec_dump_header(s, "avc", in); +#endif + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (in->buf->last - in->buf->pos < 18) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "codec: invalid video codec header size=%ui", + in->buf->last - in->buf->pos); + + return NGX_ERROR; + } + + ngx_rtmp_bit_init_reader(&br, in->buf->pos, in->buf->last); + + ngx_rtmp_bit_read(&br, 40); + + avc_config_version = (ngx_uint_t) ngx_rtmp_bit_read_8(&br); + if (avc_config_version == 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "codec: zero configurationVersion"); + + return NGX_ERROR; + } + + ctx->avc_profile = (ngx_uint_t) ngx_rtmp_bit_read_8(&br); + ctx->avc_compat = (ngx_uint_t) ngx_rtmp_bit_read_8(&br); + ctx->avc_level = (ngx_uint_t) ngx_rtmp_bit_read_8(&br); + + /* nal bytes */ + ctx->avc_nal_bytes = (ngx_uint_t) ((ngx_rtmp_bit_read_8(&br) & 0x03) + 1); + if (ctx->avc_nal_bytes != 3 && ctx->avc_nal_bytes != 4) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "codec: invalid lengthSizeMinusOne value=%ui", + ctx->avc_nal_bytes - 1); + + return NGX_ERROR; + } + + /* nnals */ + if ((ngx_rtmp_bit_read_8(&br) & 0x1f) == 0) { + return NGX_ERROR; + } + + /* nal size */ + ngx_rtmp_bit_read(&br, 16); + + /* nal type */ + if (ngx_rtmp_bit_read_8(&br) != 0x67) { + return NGX_OK; + } + + /* SPS */ + + /* profile idc */ + profile_idc = (ngx_uint_t) ngx_rtmp_bit_read(&br, 8); + + /* flags */ + ngx_rtmp_bit_read(&br, 8); + + /* level idc */ + ngx_rtmp_bit_read(&br, 8); + + /* SPS id */ + ngx_rtmp_bit_read_golomb(&br); + + if (profile_idc == 100 || profile_idc == 110 || + profile_idc == 122 || profile_idc == 244 || profile_idc == 44 || + profile_idc == 83 || profile_idc == 86 || profile_idc == 118) + { + /* chroma format idc */ + cf_idc = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + + if (cf_idc == 3) { + + /* separate color plane */ + ngx_rtmp_bit_read(&br, 1); + } + + /* bit depth luma - 8 */ + ngx_rtmp_bit_read_golomb(&br); + + /* bit depth chroma - 8 */ + ngx_rtmp_bit_read_golomb(&br); + + /* qpprime y zero transform bypass */ + ngx_rtmp_bit_read(&br, 1); + + /* seq scaling matrix present */ + if (ngx_rtmp_bit_read(&br, 1)) { + + for (n = 0; n < (cf_idc != 3 ? 8u : 12u); n++) { + + /* seq scaling list present */ + if (ngx_rtmp_bit_read(&br, 1)) { + + /* TODO: scaling_list() + if (n < 6) { + } else { + } + */ + } + } + } + } + + /* log2 max frame num */ + ngx_rtmp_bit_read_golomb(&br); + + /* pic order cnt type */ + switch (ngx_rtmp_bit_read_golomb(&br)) { + case 0: + + /* max pic order cnt */ + ngx_rtmp_bit_read_golomb(&br); + break; + + case 1: + + /* delta pic order alwys zero */ + ngx_rtmp_bit_read(&br, 1); + + /* offset for non-ref pic */ + ngx_rtmp_bit_read_golomb(&br); + + /* offset for top to bottom field */ + ngx_rtmp_bit_read_golomb(&br); + + /* num ref frames in pic order */ + num_ref_frames = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + + for (n = 0; n < num_ref_frames; n++) { + + /* offset for ref frame */ + ngx_rtmp_bit_read_golomb(&br); + } + } + + /* num ref frames */ + ctx->avc_ref_frames = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + + /* gaps in frame num allowed */ + ngx_rtmp_bit_read(&br, 1); + + /* pic width in mbs - 1 */ + width = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + + /* pic height in map units - 1 */ + height = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + + /* frame mbs only flag */ + frame_mbs_only = (ngx_uint_t) ngx_rtmp_bit_read(&br, 1); + + if (!frame_mbs_only) { + + /* mbs adaprive frame field */ + ngx_rtmp_bit_read(&br, 1); + } + + /* direct 8x8 inference flag */ + ngx_rtmp_bit_read(&br, 1); + + /* frame cropping */ + if (ngx_rtmp_bit_read(&br, 1)) { + + crop_left = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + crop_right = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + crop_top = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + crop_bottom = (ngx_uint_t) ngx_rtmp_bit_read_golomb(&br); + + } else { + + crop_left = 0; + crop_right = 0; + crop_top = 0; + crop_bottom = 0; + } + + ctx->width = (width + 1) * 16 - (crop_left + crop_right) * 2; + ctx->height = (2 - frame_mbs_only) * (height + 1) * 16 - + (crop_top + crop_bottom) * 2; + + ngx_log_debug7(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "codec: avc header " + "profile=%ui, compat=%ui, level=%ui, " + "nal_bytes=%ui, ref_frames=%ui, width=%ui, height=%ui", + ctx->avc_profile, ctx->avc_compat, ctx->avc_level, + ctx->avc_nal_bytes, ctx->avc_ref_frames, + ctx->width, ctx->height); + + return NGX_OK; +} + + +#if (NGX_DEBUG) +static void +ngx_rtmp_codec_dump_header(ngx_rtmp_session_t *s, const char *type, + ngx_chain_t *in) +{ + u_char buf[256], *p, *pp; + u_char hex[] = "0123456789abcdef"; + + for (pp = buf, p = in->buf->pos; + p < in->buf->last && pp < buf + sizeof(buf) - 2; + ++p) + { + *pp++ = hex[*p >> 4]; + *pp++ = hex[*p & 0x0f]; + } + + *pp = 0; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "codec: %s header %s", type, buf); +} +#endif + + +static ngx_int_t +ngx_rtmp_codec_reconstruct_meta(ngx_rtmp_session_t *s) +{ + ngx_rtmp_codec_ctx_t *ctx; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_int_t rc; + + static struct { + double width; + double height; + double duration; + double frame_rate; + double video_data_rate; + double video_codec_id; + double audio_data_rate; + double audio_codec_id; + u_char profile[32]; + u_char level[32]; + } v; + + static ngx_rtmp_amf_elt_t out_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("Server"), + "NGINX HTTP-FLV (https://github.com/winshining/nginx-http-flv-module)", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("width"), + &v.width, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("height"), + &v.height, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("displayWidth"), + &v.width, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("displayHeight"), + &v.height, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("duration"), + &v.duration, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("framerate"), + &v.frame_rate, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("fps"), + &v.frame_rate, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("videodatarate"), + &v.video_data_rate, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("videocodecid"), + &v.video_codec_id, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("audiodatarate"), + &v.audio_data_rate, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("audiocodecid"), + &v.audio_codec_id, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("profile"), + &v.profile, sizeof(v.profile) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + &v.level, sizeof(v.level) }, + }; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "onMetaData", 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + out_inf, sizeof(out_inf) }, + }; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (ctx == NULL) { + return NGX_OK; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + if (ctx->meta) { + ngx_rtmp_free_shared_chain(cscf, ctx->meta); + ctx->meta = NULL; + } + + v.width = ctx->width; + v.height = ctx->height; + v.duration = ctx->duration; + v.frame_rate = ctx->frame_rate; + v.video_data_rate = ctx->video_data_rate; + v.video_codec_id = ctx->video_codec_id; + v.audio_data_rate = ctx->audio_data_rate; + v.audio_codec_id = ctx->audio_codec_id; + ngx_memcpy(v.profile, ctx->profile, sizeof(ctx->profile)); + ngx_memcpy(v.level, ctx->level, sizeof(ctx->level)); + + rc = ngx_rtmp_append_amf(s, &ctx->meta, NULL, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); + if (rc != NGX_OK || ctx->meta == NULL) { + return NGX_ERROR; + } + + return ngx_rtmp_codec_prepare_meta(s, 0); +} + + +static ngx_int_t +ngx_rtmp_codec_copy_meta(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_codec_ctx_t *ctx; + ngx_rtmp_core_srv_conf_t *cscf; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + if (ctx->meta) { + ngx_rtmp_free_shared_chain(cscf, ctx->meta); + } + + ctx->meta = ngx_rtmp_append_shared_bufs(cscf, NULL, in); + + if (ctx->meta == NULL) { + return NGX_ERROR; + } + + return ngx_rtmp_codec_prepare_meta(s, h->timestamp); +} + + +static ngx_int_t +ngx_rtmp_codec_prepare_meta(ngx_rtmp_session_t *s, uint32_t timestamp) +{ + ngx_rtmp_header_t h; + ngx_rtmp_codec_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + ngx_memzero(&h, sizeof(h)); + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; + h.type = NGX_RTMP_MSG_AMF_META; + h.timestamp = timestamp; + ngx_rtmp_prepare_message(s, &h, NULL, ctx->meta); + + ctx->meta_version = ngx_rtmp_codec_get_next_version(); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_codec_meta_data(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + uint16_t len; + ngx_uint_t skip; + u_char *p; + ngx_rtmp_codec_app_conf_t *cacf; + ngx_rtmp_codec_ctx_t *ctx; + + static struct { + double width; + double height; + double duration; + double frame_rate; + double video_data_rate; + double video_codec_id_n; + u_char video_codec_id_s[32]; + double audio_data_rate; + double audio_codec_id_n; + u_char audio_codec_id_s[32]; + u_char profile[32]; + u_char level[32]; + } v; + + static ngx_rtmp_amf_elt_t in_video_codec_id[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.video_codec_id_n, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + &v.video_codec_id_s, sizeof(v.video_codec_id_s) }, + }; + + static ngx_rtmp_amf_elt_t in_audio_codec_id[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.audio_codec_id_n, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + &v.audio_codec_id_s, sizeof(v.audio_codec_id_s) }, + }; + + static ngx_rtmp_amf_elt_t in_inf[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_string("width"), + &v.width, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("height"), + &v.height, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("duration"), + &v.duration, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("framerate"), + &v.frame_rate, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("fps"), + &v.frame_rate, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("videodatarate"), + &v.video_data_rate, 0 }, + + { NGX_RTMP_AMF_VARIANT, + ngx_string("videocodecid"), + in_video_codec_id, sizeof(in_video_codec_id) }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("audiodatarate"), + &v.audio_data_rate, 0 }, + + { NGX_RTMP_AMF_VARIANT, + ngx_string("audiocodecid"), + in_audio_codec_id, sizeof(in_audio_codec_id) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("profile"), + &v.profile, sizeof(v.profile) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + &v.level, sizeof(v.level) }, + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_inf, sizeof(in_inf) }, + }; + + cacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_codec_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_codec_ctx_t)); + if (ctx == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "codec: failed to allocate for ctx (meta)"); + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_codec_module); + } + + ngx_memzero(&v, sizeof(v)); + + /* use -1 as a sign of unchanged data; + * 0 is a valid value for uncompressed audio */ + v.audio_codec_id_n = -1; + + if (in->buf->last > in->buf->pos + && in->buf->pos - in->buf->start >= 13 + && in->buf->pos[-13] == NGX_RTMP_AMF_STRING) + { + p = (u_char *) &len; + + *p++ = in->buf->pos[-11]; + *p++ = in->buf->pos[-12]; + + if (ngx_strncasecmp(in->buf->pos - 10, + (u_char *) "onMetaData", len) == 0) + { + in->buf->pos -= 13; + } + } + + /* FFmpeg sends a string in front of actual metadata; ignore it */ + skip = !(in->buf->last > in->buf->pos + && *in->buf->pos == NGX_RTMP_AMF_STRING); + if (ngx_rtmp_receive_amf(s, in, in_elts + skip, + sizeof(in_elts) / sizeof(in_elts[0]) - skip)) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "codec: error parsing data frame"); + return NGX_OK; + } + + ctx->width = (ngx_uint_t) v.width; + ctx->height = (ngx_uint_t) v.height; + ctx->duration = v.duration; + ctx->frame_rate = v.frame_rate; + ctx->video_data_rate = v.video_data_rate; + ctx->video_codec_id = (ngx_uint_t) v.video_codec_id_n; + ctx->audio_data_rate = v.audio_data_rate; + ctx->audio_codec_id = (v.audio_codec_id_n == -1 + ? 0 : v.audio_codec_id_n == 0 + ? NGX_RTMP_AUDIO_UNCOMPRESSED : (ngx_uint_t) v.audio_codec_id_n); + ngx_memcpy(ctx->profile, v.profile, sizeof(v.profile)); + ngx_memcpy(ctx->level, v.level, sizeof(v.level)); + + ngx_log_debug8(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "codec: data frame: " + "width=%ui height=%ui duration=%.3f frame_rate=%.3f " + "video=%s (%ui) audio=%s (%ui)", + ctx->width, ctx->height, ctx->duration, ctx->frame_rate, + ngx_rtmp_get_video_codec_name(ctx->video_codec_id), + ctx->video_codec_id, + ngx_rtmp_get_audio_codec_name(ctx->audio_codec_id), + ctx->audio_codec_id); + + switch (cacf->meta) { + case NGX_RTMP_CODEC_META_ON: + return ngx_rtmp_codec_reconstruct_meta(s); + case NGX_RTMP_CODEC_META_COPY: + return ngx_rtmp_codec_copy_meta(s, h, in); + } + + /* NGX_RTMP_CODEC_META_OFF */ + + return NGX_OK; +} + + +static void * +ngx_rtmp_codec_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_codec_app_conf_t *cacf; + + cacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_codec_app_conf_t)); + if (cacf == NULL) { + return NULL; + } + + cacf->meta = NGX_CONF_UNSET_UINT; + + return cacf; +} + + +static char * +ngx_rtmp_codec_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_codec_app_conf_t *prev = parent; + ngx_rtmp_codec_app_conf_t *conf = child; + + ngx_conf_merge_uint_value(conf->meta, prev->meta, NGX_RTMP_CODEC_META_ON); + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_codec_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + ngx_rtmp_amf_handler_t *ch; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); + *h = ngx_rtmp_codec_av; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); + *h = ngx_rtmp_codec_av; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_DISCONNECT]); + *h = ngx_rtmp_codec_disconnect; + + /* register metadata handler */ + ch = ngx_array_push(&cmcf->amf); + if (ch == NULL) { + return NGX_ERROR; + } + ngx_str_set(&ch->name, "@setDataFrame"); + ch->handler = ngx_rtmp_codec_meta_data; + + ch = ngx_array_push(&cmcf->amf); + if (ch == NULL) { + return NGX_ERROR; + } + ngx_str_set(&ch->name, "onMetaData"); + ch->handler = ngx_rtmp_codec_meta_data; + + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_codec_module.h b/ngx_http_flv_module/ngx_rtmp_codec_module.h new file mode 100644 index 0000000..83180f3 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_codec_module.h @@ -0,0 +1,119 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#ifndef _NGX_RTMP_CODEC_H_INCLUDED_ +#define _NGX_RTMP_CODEC_H_INCLUDED_ + + +#include +#include +#include "ngx_rtmp.h" + + +/* AVC NAL unit types */ +enum { + NGX_RTMP_NALU_SLICE = 1, + NGX_RTMP_NALU_DPA = 2, + NGX_RTMP_NALU_DPB = 3, + NGX_RTMP_NALU_DPC = 4, + NGX_RTMP_NALU_IDR = 5, + NGX_RTMP_NALU_SEI = 6, + NGX_RTMP_NALU_SPS = 7, + NGX_RTMP_NALU_PPS = 8, + NGX_RTMP_NALU_AUD = 9, + NGX_RTMP_NALU_EOSEQ = 10, + NGX_RTMP_NALU_EOSTREAM = 11, + NGX_RTMP_NALU_FILL = 12, + NGX_RTMP_NALU_SPS_EXT = 13, + NGX_RTMP_NALU_AUXILIARY_SLICE = 19 +}; + + +/* AVC frame types */ +enum { + NGX_RTMP_FRAME_IDR = 1, + NGX_RTMP_FRAME_INTER = 2, + NGX_RTMP_FRAME_DISPOSABLE = 3, + NGX_RTMP_FRAME_GENERATED = 4, + NGX_RTMP_FRAME_VIDEOINFOCMD = 5 +}; + + +/* Audio codecs */ +enum { + /* Uncompressed codec id is actually 0, + * but we use another value for consistency */ + NGX_RTMP_AUDIO_UNCOMPRESSED = 16, + NGX_RTMP_AUDIO_ADPCM = 1, + NGX_RTMP_AUDIO_MP3 = 2, + NGX_RTMP_AUDIO_LINEAR_LE = 3, + NGX_RTMP_AUDIO_NELLY16 = 4, + NGX_RTMP_AUDIO_NELLY8 = 5, + NGX_RTMP_AUDIO_NELLY = 6, + NGX_RTMP_AUDIO_G711A = 7, + NGX_RTMP_AUDIO_G711U = 8, + NGX_RTMP_AUDIO_AAC = 10, + NGX_RTMP_AUDIO_SPEEX = 11, + NGX_RTMP_AUDIO_MP3_8 = 14, + NGX_RTMP_AUDIO_DEVSPEC = 15 +}; + + +/* Video codecs */ +enum { + NGX_RTMP_VIDEO_JPEG = 1, + NGX_RTMP_VIDEO_SORENSON_H263 = 2, + NGX_RTMP_VIDEO_SCREEN = 3, + NGX_RTMP_VIDEO_ON2_VP6 = 4, + NGX_RTMP_VIDEO_ON2_VP6_ALPHA = 5, + NGX_RTMP_VIDEO_SCREEN2 = 6, + NGX_RTMP_VIDEO_H264 = 7 +}; + + +u_char * ngx_rtmp_get_audio_codec_name(ngx_uint_t id); +u_char * ngx_rtmp_get_video_codec_name(ngx_uint_t id); + + +#define NGX_RTMP_SPS_MAX_LENGTH 256 + +typedef struct { + ngx_uint_t width; + ngx_uint_t height; + double duration; + double frame_rate; + double video_data_rate; + ngx_uint_t video_codec_id; + double audio_data_rate; + ngx_uint_t audio_codec_id; + ngx_uint_t aac_profile; + ngx_uint_t aac_chan_conf; + ngx_uint_t aac_sbr; + ngx_uint_t aac_ps; + ngx_uint_t avc_profile; + ngx_uint_t avc_compat; + ngx_uint_t avc_level; + ngx_uint_t avc_nal_bytes; + ngx_uint_t avc_ref_frames; + 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 */ + u_char profile[32]; + u_char level[32]; + + ngx_chain_t *avc_header; + ngx_chain_t *aac_header; + + ngx_chain_t *meta; + ngx_uint_t meta_version; +} ngx_rtmp_codec_ctx_t; + + +extern ngx_module_t ngx_rtmp_codec_module; + + +#endif /* _NGX_RTMP_LIVE_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_control_module.c b/ngx_http_flv_module/ngx_rtmp_control_module.c new file mode 100644 index 0000000..adf3ddc --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_control_module.c @@ -0,0 +1,732 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_record_module.h" + + +static char *ngx_rtmp_control(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +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); + + +typedef const char * (*ngx_rtmp_control_handler_t)(ngx_http_request_t *r, + ngx_rtmp_session_t *); + + +#define NGX_RTMP_CONTROL_ALL 0xff +#define NGX_RTMP_CONTROL_RECORD 0x01 +#define NGX_RTMP_CONTROL_DROP 0x02 +#define NGX_RTMP_CONTROL_REDIRECT 0x04 + + +enum { + NGX_RTMP_CONTROL_FILTER_CLIENT = 0, + NGX_RTMP_CONTROL_FILTER_PUBLISHER, + NGX_RTMP_CONTROL_FILTER_SUBSCRIBER +}; + + +typedef struct { + ngx_uint_t count; + ngx_str_t path; + ngx_uint_t filter; + ngx_str_t method; + ngx_array_t sessions; /* ngx_rtmp_session_t * */ +} ngx_rtmp_control_ctx_t; + + +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_string("drop"), NGX_RTMP_CONTROL_DROP }, + { ngx_string("redirect"), NGX_RTMP_CONTROL_REDIRECT }, + { 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_rtmp_control, + 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 */ + NULL, /* 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 +}; + + +static const char * +ngx_rtmp_control_record_handler(ngx_http_request_t *r, ngx_rtmp_session_t *s) +{ + ngx_int_t rc; + ngx_str_t rec; + ngx_uint_t rn; + ngx_rtmp_control_ctx_t *ctx; + ngx_rtmp_core_app_conf_t *cacf; + ngx_rtmp_record_app_conf_t *racf; + + cacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_core_module); + racf = cacf->app_conf[ngx_rtmp_record_module.ctx_index]; + + if (ngx_http_arg(r, (u_char *) "rec", sizeof("rec") - 1, &rec) != NGX_OK) { + rec.len = 0; + } + + rn = ngx_rtmp_record_find(racf, &rec); + if (rn == NGX_CONF_UNSET_UINT) { + return "Recorder not found"; + } + + ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module); + + if (ctx->method.len == sizeof("start") - 1 && + ngx_strncmp(ctx->method.data, "start", ctx->method.len) == 0) + { + rc = ngx_rtmp_record_open(s, rn, &ctx->path); + + } else if (ctx->method.len == sizeof("stop") - 1 && + ngx_strncmp(ctx->method.data, "stop", ctx->method.len) == 0) + { + rc = ngx_rtmp_record_close(s, rn, &ctx->path); + + } else { + return "Undefined method"; + } + + if (rc == NGX_ERROR) { + return "Recorder error"; + } + + return NGX_CONF_OK; +} + + +static const char * +ngx_rtmp_control_drop_handler(ngx_http_request_t *r, ngx_rtmp_session_t *s) +{ + ngx_rtmp_control_ctx_t *ctx; + + ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module); + + ngx_rtmp_finalize_session(s); + + ++ctx->count; + + return NGX_CONF_OK; +} + + +static const char * +ngx_rtmp_control_redirect_handler(ngx_http_request_t *r, ngx_rtmp_session_t *s) +{ + ngx_str_t name; + ngx_rtmp_play_t vplay; + ngx_rtmp_publish_t vpublish; + ngx_rtmp_live_ctx_t *lctx; + ngx_rtmp_control_ctx_t *ctx; + ngx_rtmp_close_stream_t vc; + + if (ngx_http_arg(r, (u_char *) "newname", sizeof("newname") - 1, &name) + != NGX_OK) + { + return "newname not specified"; + } + + if (name.len >= NGX_RTMP_MAX_NAME) { + name.len = NGX_RTMP_MAX_NAME - 1; + } + + ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module); + ctx->count++; + + ngx_memzero(&vc, sizeof(ngx_rtmp_close_stream_t)); + + /* close_stream should be synchronous */ + ngx_rtmp_close_stream(s, &vc); + + lctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + + if (lctx && lctx->publishing) { + /* publish */ + + ngx_memzero(&vpublish, sizeof(ngx_rtmp_publish_t)); + + ngx_memcpy(vpublish.name, name.data, name.len); + + ngx_rtmp_cmd_fill_args(vpublish.name, vpublish.args); + + if (ngx_rtmp_publish(s, &vpublish) != NGX_OK) { + return "publish failed"; + } + + } else { + /* play */ + + ngx_memzero(&vplay, sizeof(ngx_rtmp_play_t)); + + ngx_memcpy(vplay.name, name.data, name.len); + + ngx_rtmp_cmd_fill_args(vplay.name, vplay.args); + + if (ngx_rtmp_play(s, &vplay) != NGX_OK) { + return "play failed"; + } + } + + return NGX_CONF_OK; +} + + +static const char * +ngx_rtmp_control_walk_session(ngx_http_request_t *r, + ngx_rtmp_live_ctx_t *lctx) +{ + ngx_str_t addr, *paddr, clientid; + ngx_rtmp_session_t *s, **ss; + ngx_rtmp_control_ctx_t *ctx; + + s = lctx->session; + + if (s == NULL || s->connection == NULL) { + return NGX_CONF_OK; + } + + if (ngx_http_arg(r, (u_char *) "addr", sizeof("addr") - 1, &addr) + == NGX_OK) + { + paddr = &s->connection->addr_text; + if (paddr->len != addr.len || + ngx_strncmp(paddr->data, addr.data, addr.len)) + { + return NGX_CONF_OK; + } + } + + if (ngx_http_arg(r, (u_char *) "clientid", sizeof("clientid") - 1, + &clientid) + == NGX_OK) + { + if (s->connection->number != + (ngx_uint_t) ngx_atoi(clientid.data, clientid.len)) + { + return NGX_CONF_OK; + } + } + + ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module); + + switch (ctx->filter) { + case NGX_RTMP_CONTROL_FILTER_PUBLISHER: + if (!lctx->publishing) { + return NGX_CONF_OK; + } + break; + + case NGX_RTMP_CONTROL_FILTER_SUBSCRIBER: + if (lctx->publishing) { + return NGX_CONF_OK; + } + break; + + case NGX_RTMP_CONTROL_FILTER_CLIENT: + break; + } + + ss = ngx_array_push(&ctx->sessions); + if (ss == NULL) { + return "allocation error"; + } + + *ss = s; + + return NGX_CONF_OK; +} + + +static const char * +ngx_rtmp_control_walk_stream(ngx_http_request_t *r, + ngx_rtmp_live_stream_t *ls) +{ + const char *s; + ngx_rtmp_live_ctx_t *lctx; + + for (lctx = ls->ctx; lctx; lctx = lctx->next) { + s = ngx_rtmp_control_walk_session(r, lctx); + if (s != NGX_CONF_OK) { + return s; + } + } + + return NGX_CONF_OK; +} + + +static const char * +ngx_rtmp_control_walk_app(ngx_http_request_t *r, + ngx_rtmp_core_app_conf_t *cacf) +{ + size_t len; + ngx_str_t name; + const char *s; + ngx_uint_t n; + ngx_rtmp_live_stream_t *ls; + ngx_rtmp_live_app_conf_t *lacf; + + lacf = cacf->app_conf[ngx_rtmp_live_module.ctx_index]; + + if (ngx_http_arg(r, (u_char *) "name", sizeof("name") - 1, &name) != NGX_OK) + { + for (n = 0; n < (ngx_uint_t) lacf->nbuckets; ++n) { + for (ls = lacf->streams[n]; ls; ls = ls->next) { + s = ngx_rtmp_control_walk_stream(r, ls); + if (s != NGX_CONF_OK) { + return s; + } + } + } + + return NGX_CONF_OK; + } + + 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)) { + continue; + } + + s = ngx_rtmp_control_walk_stream(r, ls); + if (s != NGX_CONF_OK) { + return s; + } + } + + return NGX_CONF_OK; +} + + +static const char * +ngx_rtmp_control_walk_server(ngx_http_request_t *r, + ngx_rtmp_core_srv_conf_t *cscf) +{ + ngx_str_t app; + ngx_uint_t n; + const char *s; + ngx_rtmp_core_app_conf_t **pcacf; + + if (ngx_http_arg(r, (u_char *) "app", sizeof("app") - 1, &app) != NGX_OK) { + app.len = 0; + } + + pcacf = cscf->applications.elts; + + for (n = 0; n < cscf->applications.nelts; ++n, ++pcacf) { + if (app.len && ((*pcacf)->name.len != app.len || + ngx_strncmp((*pcacf)->name.data, app.data, app.len))) + { + continue; + } + + s = ngx_rtmp_control_walk_app(r, *pcacf); + if (s != NGX_CONF_OK) { + return s; + } + } + + return NGX_CONF_OK; +} + + +static const char * +ngx_rtmp_control_walk(ngx_http_request_t *r, ngx_rtmp_control_handler_t h) +{ + ngx_rtmp_core_main_conf_t *cmcf = ngx_rtmp_core_main_conf; + + ngx_str_t srv; + ngx_uint_t sn, n; + const char *msg; + ngx_rtmp_session_t **s; + ngx_rtmp_control_ctx_t *ctx; + ngx_rtmp_core_srv_conf_t **pcscf; + + 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 >= cmcf->servers.nelts) { + return "Server index out of range"; + } + + pcscf = cmcf->servers.elts; + pcscf += sn; + + msg = ngx_rtmp_control_walk_server(r, *pcscf); + if (msg != NGX_CONF_OK) { + return msg; + } + + ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module); + + s = ctx->sessions.elts; + for (n = 0; n < ctx->sessions.nelts; n++) { + msg = h(r, s[n]); + if (msg != NGX_CONF_OK) { + return msg; + } + } + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_control_record(ngx_http_request_t *r, ngx_str_t *method) +{ + ngx_buf_t *b; + const char *msg; + ngx_chain_t cl; + ngx_rtmp_control_ctx_t *ctx; + + ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module); + ctx->filter = NGX_RTMP_CONTROL_FILTER_PUBLISHER; + + msg = ngx_rtmp_control_walk(r, ngx_rtmp_control_record_handler); + if (msg != NGX_CONF_OK) { + goto error; + } + + if (ctx->path.len == 0) { + return NGX_HTTP_NO_CONTENT; + } + + /* output record path */ + + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = ctx->path.len; + + b = ngx_create_temp_buf(r->pool, ctx->path.len); + if (b == NULL) { + goto error; + } + + ngx_memzero(&cl, sizeof(cl)); + cl.buf = b; + + b->last = ngx_cpymem(b->pos, ctx->path.data, ctx->path.len); + b->last_buf = 1; + + ngx_http_send_header(r); + + return ngx_http_output_filter(r, &cl); + +error: + return NGX_HTTP_INTERNAL_SERVER_ERROR; +} + + +static ngx_int_t +ngx_rtmp_control_drop(ngx_http_request_t *r, ngx_str_t *method) +{ + size_t len; + u_char *p; + ngx_buf_t *b; + ngx_chain_t cl; + const char *msg; + ngx_rtmp_control_ctx_t *ctx; + + ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module); + + if (ctx->method.len == sizeof("publisher") - 1 && + ngx_memcmp(ctx->method.data, "publisher", ctx->method.len) == 0) + { + ctx->filter = NGX_RTMP_CONTROL_FILTER_PUBLISHER; + + } else if (ctx->method.len == sizeof("subscriber") - 1 && + ngx_memcmp(ctx->method.data, "subscriber", ctx->method.len) + == 0) + { + ctx->filter = NGX_RTMP_CONTROL_FILTER_SUBSCRIBER; + + } else if (method->len == sizeof("client") - 1 && + ngx_memcmp(ctx->method.data, "client", ctx->method.len) == 0) + { + ctx->filter = NGX_RTMP_CONTROL_FILTER_CLIENT; + + } else { + msg = "Undefined filter"; + goto error; + } + + msg = ngx_rtmp_control_walk(r, ngx_rtmp_control_drop_handler); + if (msg != NGX_CONF_OK) { + goto error; + } + + /* output count */ + + len = NGX_INT_T_LEN; + + p = ngx_palloc(r->connection->pool, len); + if (p == NULL) { + return NGX_ERROR; + } + + len = (size_t) (ngx_snprintf(p, len, "%ui", ctx->count) - p); + + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = len; + + b = ngx_calloc_buf(r->pool); + if (b == NULL) { + goto 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; + + ngx_http_send_header(r); + + return ngx_http_output_filter(r, &cl); + +error: + return NGX_HTTP_INTERNAL_SERVER_ERROR; +} + + +static ngx_int_t +ngx_rtmp_control_redirect(ngx_http_request_t *r, ngx_str_t *method) +{ + size_t len; + u_char *p; + ngx_buf_t *b; + ngx_chain_t cl; + const char *msg; + ngx_rtmp_control_ctx_t *ctx; + + ctx = ngx_http_get_module_ctx(r, ngx_rtmp_control_module); + + if (ctx->method.len == sizeof("publisher") - 1 && + ngx_memcmp(ctx->method.data, "publisher", ctx->method.len) == 0) + { + ctx->filter = NGX_RTMP_CONTROL_FILTER_PUBLISHER; + + } else if (ctx->method.len == sizeof("subscriber") - 1 && + ngx_memcmp(ctx->method.data, "subscriber", ctx->method.len) + == 0) + { + ctx->filter = NGX_RTMP_CONTROL_FILTER_SUBSCRIBER; + + } else if (ctx->method.len == sizeof("client") - 1 && + ngx_memcmp(ctx->method.data, "client", ctx->method.len) == 0) + { + ctx->filter = NGX_RTMP_CONTROL_FILTER_CLIENT; + + } else { + msg = "Undefined filter"; + goto error; + } + + msg = ngx_rtmp_control_walk(r, ngx_rtmp_control_redirect_handler); + if (msg != NGX_CONF_OK) { + goto error; + } + + /* output count */ + + len = NGX_INT_T_LEN; + + p = ngx_palloc(r->connection->pool, len); + if (p == NULL) { + goto error; + } + + len = (size_t) (ngx_snprintf(p, len, "%ui", ctx->count) - p); + + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = len; + + b = ngx_calloc_buf(r->pool); + if (b == NULL) { + goto 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; + + ngx_http_send_header(r); + + return ngx_http_output_filter(r, &cl); + +error: + return NGX_HTTP_INTERNAL_SERVER_ERROR; +} + + +static ngx_int_t +ngx_rtmp_control_handler(ngx_http_request_t *r) +{ + u_char *p; + ngx_str_t section, method; + ngx_uint_t n; + ngx_rtmp_control_ctx_t *ctx; + 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; + } + + /* uri format: .../section/method?args */ + + ngx_str_null(§ion); + ngx_str_null(&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); + + ctx = ngx_pcalloc(r->pool, sizeof(ngx_rtmp_control_ctx_t)); + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_http_set_ctx(r, ctx, ngx_rtmp_control_module); + + if (ngx_array_init(&ctx->sessions, r->pool, 1, sizeof(void *)) != NGX_OK) { + return NGX_ERROR; + } + + ctx->method = 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); + NGX_RTMP_CONTROL_SECTION(DROP, drop); + NGX_RTMP_CONTROL_SECTION(REDIRECT, redirect); + +#undef NGX_RTMP_CONTROL_SECTION + + 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 char * +ngx_rtmp_control(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_core_loc_conf_t *clcf; + + clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); + clcf->handler = ngx_rtmp_control_handler; + + return ngx_conf_set_bitmask_slot(cf, cmd, conf); +} diff --git a/ngx_http_flv_module/ngx_rtmp_core_module.c b/ngx_http_flv_module/ngx_rtmp_core_module.c new file mode 100644 index 0000000..6db0e4b --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_core_module.c @@ -0,0 +1,1535 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include +#include +#include "ngx_rtmp.h" + + +static ngx_int_t ngx_rtmp_core_preconfiguration(ngx_conf_t *cf); +static void *ngx_rtmp_core_create_main_conf(ngx_conf_t *cf); +static char *ngx_rtmp_core_init_main_conf(ngx_conf_t *cf, void *conf); +static void *ngx_rtmp_core_create_srv_conf(ngx_conf_t *cf); +static char *ngx_rtmp_core_merge_srv_conf(ngx_conf_t *cf, void *parent, + void *child); +static void *ngx_rtmp_core_create_app_conf(ngx_conf_t *cf); +static char *ngx_rtmp_core_merge_app_conf(ngx_conf_t *cf, void *parent, + void *child); +static char *ngx_rtmp_core_server(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static ngx_int_t ngx_rtmp_add_listen(ngx_conf_t *cf, + ngx_rtmp_core_srv_conf_t *cscf, ngx_rtmp_listen_opt_t *lsopt); +static ngx_int_t ngx_rtmp_add_addresses(ngx_conf_t *cf, + ngx_rtmp_core_srv_conf_t *cscf, ngx_rtmp_conf_port_t *port, + ngx_rtmp_listen_opt_t *lsopt); +static ngx_int_t ngx_rtmp_add_address(ngx_conf_t *cf, + ngx_rtmp_core_srv_conf_t *cscf, ngx_rtmp_conf_port_t *port, + ngx_rtmp_listen_opt_t *lsopt); +static ngx_int_t ngx_rtmp_add_server(ngx_conf_t *cf, + ngx_rtmp_core_srv_conf_t *cscf, ngx_rtmp_conf_addr_t *addr); + +static char *ngx_rtmp_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_rtmp_core_application(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_rtmp_core_server_name(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); + +static char *ngx_rtmp_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_rtmp_core_lowat_check(ngx_conf_t *cf, void *post, void *data); +static char *ngx_rtmp_core_pool_size(ngx_conf_t *cf, void *post, void *data); + + +static ngx_conf_post_t ngx_rtmp_core_lowat_post = + { ngx_rtmp_core_lowat_check }; + +static ngx_conf_post_handler_pt ngx_rtmp_core_pool_size_p = + ngx_rtmp_core_pool_size; + +ngx_rtmp_core_main_conf_t *ngx_rtmp_core_main_conf; + + +static ngx_conf_deprecated_t ngx_conf_deprecated_so_keepalive = { + ngx_conf_deprecated, "so_keepalive", + "so_keepalive\" parameter of the \"listen" +}; + + +static ngx_command_t ngx_rtmp_core_commands[] = { + + { ngx_string("server"), + NGX_RTMP_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS, + ngx_rtmp_core_server, + 0, + 0, + NULL }, + + { ngx_string("listen"), + NGX_RTMP_SRV_CONF|NGX_CONF_1MORE, + ngx_rtmp_core_listen, + NGX_RTMP_SRV_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("server_name"), + NGX_RTMP_SRV_CONF|NGX_CONF_1MORE, + ngx_rtmp_core_server_name, + NGX_RTMP_SRV_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("application"), + NGX_RTMP_SRV_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE1, + ngx_rtmp_core_application, + NGX_RTMP_SRV_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("so_keepalive"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, so_keepalive), + &ngx_conf_deprecated_so_keepalive }, + + { ngx_string("timeout"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, timeout), + NULL }, + + { ngx_string("ping"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, ping), + NULL }, + + { ngx_string("ping_timeout"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, ping_timeout), + NULL }, + + { ngx_string("max_streams"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, max_streams), + NULL }, + + { ngx_string("ack_window"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, ack_window), + NULL }, + + { ngx_string("chunk_size"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, chunk_size), + NULL }, + + { ngx_string("max_message"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, max_message), + NULL }, + + { ngx_string("out_queue"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, out_queue), + NULL }, + + { ngx_string("out_cork"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_SRV_CONF_OFFSET, + 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, + ngx_conf_set_flag_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, play_time_fix), + NULL }, + + { ngx_string("publish_time_fix"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, publish_time_fix), + NULL }, + + { ngx_string("buflen"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, buflen), + NULL }, + + { ngx_string("tcp_nopush"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_core_app_conf_t, tcp_nopush), + NULL }, + + { ngx_string("tcp_nodelay"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_core_app_conf_t, tcp_nodelay), + NULL }, + + { ngx_string("send_timeout"), + 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_core_app_conf_t, send_timeout), + NULL }, + + { ngx_string("send_lowat"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_core_app_conf_t, send_lowat), + &ngx_rtmp_core_lowat_post }, + + { ngx_string("resolver"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_core_resolver, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("resolver_timeout"), + 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_core_app_conf_t, resolver_timeout), + NULL }, + + { ngx_string("connection_pool_size"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, connection_pool_size), + &ngx_rtmp_core_pool_size_p }, + + { ngx_string("merge_slashes"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_core_srv_conf_t, merge_slashes), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_core_module_ctx = { + ngx_rtmp_core_preconfiguration, /* preconfiguration */ + NULL, /* postconfiguration */ + ngx_rtmp_core_create_main_conf, /* create main configuration */ + ngx_rtmp_core_init_main_conf, /* init main configuration */ + ngx_rtmp_core_create_srv_conf, /* create server configuration */ + ngx_rtmp_core_merge_srv_conf, /* merge server configuration */ + ngx_rtmp_core_create_app_conf, /* create app configuration */ + ngx_rtmp_core_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_core_module = { + NGX_MODULE_V1, + &ngx_rtmp_core_module_ctx, /* module context */ + ngx_rtmp_core_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static ngx_int_t +ngx_rtmp_core_preconfiguration(ngx_conf_t *cf) +{ + return ngx_rtmp_variables_add_core_vars(cf); +} + + +static void * +ngx_rtmp_core_create_main_conf(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + + cmcf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_core_main_conf_t)); + if (cmcf == NULL) { + return NULL; + } + + ngx_rtmp_core_main_conf = cmcf; + + if (ngx_array_init(&cmcf->servers, cf->pool, 4, + sizeof(ngx_rtmp_core_srv_conf_t *)) + != NGX_OK) + { + return NULL; + } + + cmcf->server_names_hash_max_size = NGX_CONF_UNSET_UINT; + cmcf->server_names_hash_bucket_size = NGX_CONF_UNSET_UINT; + + cmcf->variables_hash_max_size = NGX_CONF_UNSET_UINT; + cmcf->variables_hash_bucket_size = NGX_CONF_UNSET_UINT; + + return cmcf; +} + + +static char * +ngx_rtmp_core_init_main_conf(ngx_conf_t *cf, void *conf) +{ + ngx_rtmp_core_main_conf_t *cmcf = conf; + + ngx_conf_init_uint_value(cmcf->server_names_hash_max_size, 512); + ngx_conf_init_uint_value(cmcf->server_names_hash_bucket_size, + ngx_cacheline_size); + + cmcf->server_names_hash_bucket_size = + ngx_align(cmcf->server_names_hash_bucket_size, ngx_cacheline_size); + + ngx_conf_init_uint_value(cmcf->variables_hash_max_size, 1024); + ngx_conf_init_uint_value(cmcf->variables_hash_bucket_size, 64); + + cmcf->variables_hash_bucket_size = + ngx_align(cmcf->variables_hash_bucket_size, ngx_cacheline_size); + + if (cmcf->ncaptures) { + cmcf->ncaptures = (cmcf->ncaptures + 1) * 3; + } + + return NGX_CONF_OK; +} + + +static void * +ngx_rtmp_core_create_srv_conf(ngx_conf_t *cf) +{ + ngx_rtmp_core_srv_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_core_srv_conf_t)); + if (conf == NULL) { + return NULL; + } + + if (ngx_array_init(&conf->server_names, cf->temp_pool, 4, + sizeof(ngx_rtmp_server_name_t)) + != NGX_OK) + { + return NULL; + } + + if (ngx_array_init(&conf->applications, cf->pool, 4, + sizeof(ngx_rtmp_core_app_conf_t *)) + != NGX_OK) + { + return NULL; + } + + conf->timeout = NGX_CONF_UNSET_MSEC; + conf->ping = NGX_CONF_UNSET_MSEC; + conf->ping_timeout = NGX_CONF_UNSET_MSEC; + conf->so_keepalive = NGX_CONF_UNSET; + conf->max_streams = NGX_CONF_UNSET; + conf->chunk_size = NGX_CONF_UNSET; + conf->ack_window = NGX_CONF_UNSET_UINT; + conf->max_message = NGX_CONF_UNSET_SIZE; + conf->out_queue = NGX_CONF_UNSET_SIZE; + conf->out_cork = NGX_CONF_UNSET_SIZE; + conf->play_time_fix = NGX_CONF_UNSET; + conf->publish_time_fix = NGX_CONF_UNSET; + conf->buflen = NGX_CONF_UNSET_MSEC; + conf->busy = NGX_CONF_UNSET; + conf->connection_pool_size = NGX_CONF_UNSET_SIZE; + conf->merge_slashes = NGX_CONF_UNSET; + + return conf; +} + + +static void +ngx_rtmp_core_free_pool_cleanup(void *data) +{ + ngx_rtmp_core_srv_conf_t *conf = data; + + if (conf->pool != NULL) { + ngx_destroy_pool(conf->pool); + conf->pool = NULL; + } +} + + +static char * +ngx_rtmp_core_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_core_srv_conf_t *prev = parent; + ngx_rtmp_core_srv_conf_t *conf = child; + + ngx_str_t name; + ngx_pool_cleanup_t *cln; + ngx_rtmp_server_name_t *sn; + + ngx_conf_merge_msec_value(conf->timeout, prev->timeout, 60000); + ngx_conf_merge_msec_value(conf->ping, prev->ping, 60000); + ngx_conf_merge_msec_value(conf->ping_timeout, prev->ping_timeout, 30000); + + ngx_conf_merge_value(conf->so_keepalive, prev->so_keepalive, 0); + ngx_conf_merge_value(conf->max_streams, prev->max_streams, 32); + ngx_conf_merge_value(conf->chunk_size, prev->chunk_size, + NGX_RTMP_DEFAULT_CHUNK_SIZE); + ngx_conf_merge_uint_value(conf->ack_window, prev->ack_window, 5000000); + ngx_conf_merge_size_value(conf->max_message, prev->max_message, + 1 * 1024 * 1024); + ngx_conf_merge_size_value(conf->out_queue, prev->out_queue, 256); + ngx_conf_merge_size_value(conf->out_cork, prev->out_cork, + 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_msec_value(conf->buflen, prev->buflen, 1000); + ngx_conf_merge_value(conf->busy, prev->busy, 0); + ngx_conf_merge_size_value(conf->connection_pool_size, + prev->connection_pool_size, 64 * sizeof(void *)); + ngx_conf_merge_value(conf->merge_slashes, prev->merge_slashes, 1); + + if (prev->pool == NULL) { + prev->pool = ngx_create_pool(4096, &cf->cycle->new_log); + if (prev->pool == NULL) { + return NGX_CONF_ERROR; + } + + cln = ngx_pool_cleanup_add(cf->pool, 0); + if (cln == NULL) { + return NGX_CONF_ERROR; + } + + cln->handler = ngx_rtmp_core_free_pool_cleanup; + cln->data = conf; + } + + conf->pool = prev->pool; + + if (conf->server_names.nelts == 0) { + /* the array has 4 empty preallocated elements, so push cannot fail */ + sn = ngx_array_push(&conf->server_names); +#if (NGX_PCRE) + sn->regex = NULL; +#endif + sn->server = conf; + ngx_str_set(&sn->name, ""); + } + + sn = conf->server_names.elts; + name = sn[0].name; + +#if (NGX_PCRE) + if (sn->regex) { + name.len++; + name.data--; + } else +#endif + + if (name.data[0] == '.') { + name.len--; + name.data++; + } + + conf->server_name.len = name.len; + conf->server_name.data = ngx_pstrdup(cf->pool, &name); + if (conf->server_name.data == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + +static void * +ngx_rtmp_core_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_core_app_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_core_app_conf_t)); + if (conf == NULL) { + return NULL; + } + + if (ngx_array_init(&conf->applications, cf->pool, 1, + sizeof(ngx_rtmp_core_app_conf_t *)) + != NGX_OK) + { + return NULL; + } + + conf->tcp_nopush = NGX_CONF_UNSET; + conf->tcp_nodelay = NGX_CONF_UNSET; + conf->send_timeout = NGX_CONF_UNSET_MSEC; + conf->send_lowat = NGX_CONF_UNSET_SIZE; + conf->resolver_timeout = NGX_CONF_UNSET_MSEC; + + return conf; +} + + +static char * +ngx_rtmp_core_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_core_app_conf_t *prev = parent; + ngx_rtmp_core_app_conf_t *conf = child; + + ngx_conf_merge_value(conf->tcp_nopush, prev->tcp_nopush, 0); + ngx_conf_merge_value(conf->tcp_nodelay, prev->tcp_nodelay, 1); + + ngx_conf_merge_msec_value(conf->send_timeout, prev->send_timeout, 60000); + ngx_conf_merge_size_value(conf->send_lowat, prev->send_lowat, 0); + + ngx_conf_merge_msec_value(conf->resolver_timeout, + prev->resolver_timeout, 30000); + + if (conf->resolver == NULL) { + + if (prev->resolver == NULL) { + + /* + * create dummy resolver in rtmp {} context + * to inherit it in all servers + */ + + prev->resolver = ngx_resolver_create(cf, NULL, 0); + if (prev->resolver == NULL) { + return NGX_CONF_ERROR; + } + } + + conf->resolver = prev->resolver; + } + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + char *rv; + void *mconf; + ngx_uint_t m; + ngx_conf_t pcf; + ngx_module_t **modules; + ngx_rtmp_module_t *module; + struct sockaddr_in *sin; + ngx_rtmp_conf_ctx_t *ctx, *rtmp_ctx; + ngx_rtmp_listen_opt_t lsopt; + ngx_rtmp_core_srv_conf_t *cscf, **cscfp; + ngx_rtmp_core_main_conf_t *cmcf; + + ctx = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_conf_ctx_t)); + if (ctx == NULL) { + return NGX_CONF_ERROR; + } + + rtmp_ctx = cf->ctx; + ctx->main_conf = rtmp_ctx->main_conf; + + /* the server{}'s srv_conf */ + + ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_rtmp_max_module); + if (ctx->srv_conf == NULL) { + return NGX_CONF_ERROR; + } + + ctx->app_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_rtmp_max_module); + if (ctx->app_conf == NULL) { + return NGX_CONF_ERROR; + } + +#if (nginx_version >= 1009011) + modules = cf->cycle->modules; +#else + modules = ngx_modules; +#endif + + for (m = 0; modules[m]; m++) { + if (modules[m]->type != NGX_RTMP_MODULE) { + continue; + } + + module = modules[m]->ctx; + + if (module->create_srv_conf) { + mconf = module->create_srv_conf(cf); + if (mconf == NULL) { + return NGX_CONF_ERROR; + } + + ctx->srv_conf[modules[m]->ctx_index] = mconf; + } + + if (module->create_app_conf) { + mconf = module->create_app_conf(cf); + if (mconf == NULL) { + return NGX_CONF_ERROR; + } + + ctx->app_conf[modules[m]->ctx_index] = mconf; + } + } + + /* the server configuration context */ + + cscf = ctx->srv_conf[ngx_rtmp_core_module.ctx_index]; + cscf->ctx = ctx; + + cmcf = ctx->main_conf[ngx_rtmp_core_module.ctx_index]; + + cscfp = ngx_array_push(&cmcf->servers); + if (cscfp == NULL) { + return NGX_CONF_ERROR; + } + + *cscfp = cscf; + + + /* parse inside server{} */ + + pcf = *cf; + cf->ctx = ctx; + cf->cmd_type = NGX_RTMP_SRV_CONF; + + rv = ngx_conf_parse(cf, NULL); + + *cf = pcf; + + if (rv == NGX_CONF_OK && !cscf->listen) { + ngx_memzero(&lsopt, sizeof(ngx_rtmp_listen_opt_t)); + + sin = &lsopt.sockaddr.sockaddr_in; + + sin->sin_family = AF_INET; + sin->sin_port = htons(1935); + sin->sin_addr.s_addr = INADDR_ANY; + + lsopt.socklen = sizeof(struct sockaddr_in); + + lsopt.backlog = NGX_LISTEN_BACKLOG; + lsopt.rcvbuf = -1; + lsopt.sndbuf = -1; +#if (NGX_HAVE_SETFIB) + lsopt.setfib = -1; +#endif +#if (NGX_HAVE_TCP_FASTOPEN) + lsopt.fastopen = -1; +#endif + lsopt.wildcard = 1; + + (void) ngx_sock_ntop(&lsopt.sockaddr.sockaddr, +#if (nginx_version >= 1005003) + lsopt.socklen, +#endif + lsopt.addr, NGX_SOCKADDR_STRLEN, 1); + + if (ngx_rtmp_add_listen(cf, cscf, &lsopt) != NGX_OK) { + return NGX_CONF_ERROR; + } + + cscf->port = 1935; + } + + return rv; +} + + +static ngx_int_t +ngx_rtmp_add_listen(ngx_conf_t *cf, ngx_rtmp_core_srv_conf_t *cscf, + ngx_rtmp_listen_opt_t *lsopt) +{ + in_port_t p; + ngx_uint_t i; + struct sockaddr *sa; + ngx_rtmp_conf_port_t *port; + ngx_rtmp_core_main_conf_t *cmcf; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + if (cmcf->ports == NULL) { + cmcf->ports = ngx_array_create(cf->temp_pool, 2, + sizeof(ngx_rtmp_conf_port_t)); + if (cmcf->ports == NULL) { + return NGX_ERROR; + } + } + + sa = &lsopt->sockaddr.sockaddr; + p = ngx_inet_get_port(sa); + + port = cmcf->ports->elts; + for (i = 0; i < cmcf->ports->nelts; i++) { + + if (p != port[i].port || sa->sa_family != port[i].family) { + continue; + } + + /* a port is already in the port list */ + + return ngx_rtmp_add_addresses(cf, cscf, &port[i], lsopt); + } + + /* add a port to the port list */ + + port = ngx_array_push(cmcf->ports); + if (port == NULL) { + return NGX_ERROR; + } + + port->family = sa->sa_family; + port->port = p; + port->addrs.elts = NULL; + + return ngx_rtmp_add_address(cf, cscf, port, lsopt); +} + + +static ngx_int_t +ngx_rtmp_add_addresses(ngx_conf_t *cf, ngx_rtmp_core_srv_conf_t *cscf, + ngx_rtmp_conf_port_t *port, ngx_rtmp_listen_opt_t *lsopt) +{ +#if (nginx_version <= 1005007) + u_char *p, *sockaddr_data, *sa_data; + size_t len, off; + struct sockaddr *sa; + +#if (NGX_HAVE_UNIX_DOMAIN) + struct sockaddr_un *saun; +#endif + +#endif + +#if (nginx_version >= 1005012) + ngx_uint_t proxy_protocol; +#endif + + ngx_uint_t i, default_server; + ngx_rtmp_conf_addr_t *addr; + + /* + * we cannot compare whole sockaddr struct's as kernel + * may fill some fields in inherited sockaddr struct's + */ + +#if (nginx_version <= 1005007) + sa = (struct sockaddr *) &lsopt->sockaddr; + sockaddr_data = (u_char *) &lsopt->sockaddr; + + switch (sa->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + off = offsetof(struct sockaddr_in6, sin6_addr); + len = 16; + break; +#endif + +#if (NGX_HAVE_UNIX_DOMAIN) + case AF_UNIX: + off = offsetof(struct sockaddr_un, sun_path); + len = sizeof(saun->sun_path); + break; +#endif + + default: /* AF_INET */ + off = offsetof(struct sockaddr_in, sin_addr); + len = 4; + break; + } + + p = sockaddr_data + off; +#endif + + addr = port->addrs.elts; + + for (i = 0; i < port->addrs.nelts; i++) { +#if (nginx_version <= 1005007) + sa_data = (u_char *) &addr[i].opt.sockaddr; + if (ngx_memcmp(p, sa_data + off, len) != 0) +#else + if (ngx_cmp_sockaddr(&lsopt->sockaddr.sockaddr, lsopt->socklen, + &addr[i].opt.sockaddr.sockaddr, + addr[i].opt.socklen, 0) + != NGX_OK) +#endif + { + continue; + } + + /* the address is already in the address list */ + + if (ngx_rtmp_add_server(cf, cscf, &addr[i]) != NGX_OK) { + return NGX_ERROR; + } + + /* preserve default_server bit during listen options overwriting */ + default_server = addr[i].opt.default_server; +#if (nginx_version >= 1005012) + proxy_protocol = lsopt->proxy_protocol || addr[i].opt.proxy_protocol; +#endif + + if (lsopt->set) { + + if (addr[i].opt.set) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "duplicate listen options for %s", addr[i].opt.addr); + return NGX_ERROR; + } + + addr[i].opt = *lsopt; + } + + /* check the duplicate "default" server for this address:port */ + + if (lsopt->default_server) { + + if (default_server) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "a duplicate default server for %s", addr[i].opt.addr); + return NGX_ERROR; + } + + default_server = 1; + addr[i].default_server = cscf; + } + + addr[i].opt.default_server = default_server; +#if (nginx_version >= 1005012) + addr[i].opt.proxy_protocol = proxy_protocol; +#endif + + return NGX_OK; + } + + /* add the address to the addresses list that bound to this port */ + + return ngx_rtmp_add_address(cf, cscf, port, lsopt); +} + + +/* + * add the server address, the server names and the server core module + * configurations to the port list + */ + +static ngx_int_t +ngx_rtmp_add_address(ngx_conf_t *cf, ngx_rtmp_core_srv_conf_t *cscf, + ngx_rtmp_conf_port_t *port, ngx_rtmp_listen_opt_t *lsopt) +{ + ngx_rtmp_conf_addr_t *addr; + + if (port->addrs.elts == NULL) { + if (ngx_array_init(&port->addrs, cf->temp_pool, 4, + sizeof(ngx_rtmp_conf_addr_t)) + != NGX_OK) + { + return NGX_ERROR; + } + } + + addr = ngx_array_push(&port->addrs); + if (addr == NULL) { + return NGX_ERROR; + } + + addr->opt = *lsopt; + addr->hash.buckets = NULL; + addr->hash.size = 0; + addr->wc_head = NULL; + addr->wc_tail = NULL; +#if (NGX_PCRE) + addr->nregex = 0; + addr->regex = NULL; +#endif + addr->default_server = cscf; + addr->servers.elts = NULL; + + return ngx_rtmp_add_server(cf, cscf, addr); +} + + +/* add the server core module configuration to the address:port */ + +static ngx_int_t +ngx_rtmp_add_server(ngx_conf_t *cf, ngx_rtmp_core_srv_conf_t *cscf, + ngx_rtmp_conf_addr_t *addr) +{ + ngx_uint_t i; + ngx_rtmp_core_srv_conf_t **server; + + if (addr->servers.elts == NULL) { + if (ngx_array_init(&addr->servers, cf->temp_pool, 4, + sizeof(ngx_rtmp_core_srv_conf_t *)) + != NGX_OK) + { + return NGX_ERROR; + } + + } else { + server = addr->servers.elts; + for (i = 0; i < addr->servers.nelts; i++) { + if (server[i] == cscf) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "a duplicate listen %s", addr->opt.addr); + return NGX_ERROR; + } + } + } + + server = ngx_array_push(&addr->servers); + if (server == NULL) { + return NGX_ERROR; + } + + *server = cscf; + + return NGX_OK; +} + + +static char * +ngx_rtmp_core_application(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + char *rv; + ngx_int_t i; + ngx_uint_t n; + ngx_str_t *value; + ngx_conf_t save; + ngx_module_t **modules; + ngx_rtmp_module_t *module; + ngx_rtmp_conf_ctx_t *ctx, *pctx; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_core_app_conf_t *cacf, **cacfp; + + ctx = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_conf_ctx_t)); + if (ctx == NULL) { + return NGX_CONF_ERROR; + } + + pctx = cf->ctx; + ctx->main_conf = pctx->main_conf; + ctx->srv_conf = pctx->srv_conf; + + ctx->app_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_rtmp_max_module); + if (ctx->app_conf == NULL) { + return NGX_CONF_ERROR; + } + +#if (nginx_version >= 1009011) + modules = cf->cycle->modules; +#else + modules = ngx_modules; +#endif + + for (i = 0; modules[i]; i++) { + if (modules[i]->type != NGX_RTMP_MODULE) { + continue; + } + + module = modules[i]->ctx; + + if (module->create_app_conf) { + ctx->app_conf[modules[i]->ctx_index] = module->create_app_conf(cf); + if (ctx->app_conf[modules[i]->ctx_index] == NULL) { + return NGX_CONF_ERROR; + } + } + } + + cacf = ctx->app_conf[ngx_rtmp_core_module.ctx_index]; + cacf->app_conf = ctx->app_conf; + + value = cf->args->elts; + + cacf->name = value[1]; + cscf = pctx->srv_conf[ngx_rtmp_core_module.ctx_index]; + + cacfp = cscf->applications.elts; + for (n = 0; n < cscf->applications.nelts; n++) { + if (cacf->name.len == cacfp[n]->name.len + && ngx_strncmp(cacf->name.data, + cacfp[n]->name.data, cacf->name.len) == 0) + { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "duplicate application: \"%V\"", &cacf->name); + + return NGX_CONF_ERROR; + } + } + + cacfp = ngx_array_push(&cscf->applications); + if (cacfp == NULL) { + return NGX_CONF_ERROR; + } + + *cacfp = cacf; + + save = *cf; + cf->ctx = ctx; + cf->cmd_type = NGX_RTMP_APP_CONF; + + rv = ngx_conf_parse(cf, NULL); + + *cf= save; + + return rv; +} + + +static char * +ngx_rtmp_core_resolver(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_core_app_conf_t *cacf = conf; + + ngx_str_t *value; + + if (cacf->resolver) { + return "is duplicate"; + } + + value = cf->args->elts; + + cacf->resolver = ngx_resolver_create(cf, &value[1], cf->args->nelts - 1); + if (cacf->resolver == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_core_lowat_check(ngx_conf_t *cf, void *post, void *data) +{ +#if (NGX_FREEBSD) + ssize_t *np = data; + + if ((u_long) *np >= ngx_freebsd_net_inet_tcp_sendspace) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "\"send_lowat\" must be less than %d " + "(sysctl net.inet.tcp.sendspace)", + ngx_freebsd_net_inet_tcp_sendspace); + + return NGX_CONF_ERROR; + } + +#elif !(NGX_HAVE_SO_SNDLOWAT) + ssize_t *np = data; + + ngx_conf_log_error(NGX_LOG_WARN, cf, 0, + "\"send_lowat\" is not supported, ignored"); + + *np = 0; + +#endif + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_core_pool_size(ngx_conf_t *cf, void *post, void *data) +{ + size_t *sp = data; + + if (*sp < NGX_MIN_POOL_SIZE) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the pool size must be no less than %uz", + NGX_MIN_POOL_SIZE); + return NGX_CONF_ERROR; + } + + if (*sp % NGX_POOL_ALIGNMENT) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the pool size must be a multiple of %uz", + NGX_POOL_ALIGNMENT); + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_core_srv_conf_t *cscf = conf; + + ngx_str_t *value, size; + ngx_url_t u; + ngx_uint_t n; + ngx_rtmp_listen_opt_t lsopt; + + cscf->listen = 1; + + value = cf->args->elts; + + ngx_memzero(&u, sizeof(ngx_url_t)); + + u.url = value[1]; + u.listen = 1; + u.default_port = 1935; + + if (ngx_parse_url(cf->pool, &u) != NGX_OK) { + if (u.err) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "%s in \"%V\" of the \"listen\" directive", + u.err, &u.url); + } + + return NGX_CONF_ERROR; + } + + ngx_memzero(&lsopt, sizeof(ngx_rtmp_listen_opt_t)); + + ngx_memcpy(&lsopt.sockaddr.sockaddr, &u.sockaddr, u.socklen); + + lsopt.socklen = u.socklen; + lsopt.backlog = NGX_LISTEN_BACKLOG; + lsopt.rcvbuf = -1; + lsopt.sndbuf = -1; +#if (NGX_HAVE_SETFIB) + lsopt.setfib = -1; +#endif +#if (NGX_HAVE_TCP_FASTOPEN) + lsopt.fastopen = -1; +#endif + lsopt.wildcard = u.wildcard; +#if (NGX_HAVE_INET6) + lsopt.ipv6only = 1; +#endif + + (void) ngx_sock_ntop(&lsopt.sockaddr.sockaddr, +#if (nginx_version >= 1005003) + lsopt.socklen, +#endif + lsopt.addr, + NGX_SOCKADDR_STRLEN, 1); + + for (n = 2; n < cf->args->nelts; n++) { + + if (ngx_strcmp(value[n].data, "default_server") == 0 + || ngx_strcmp(value[n].data, "default") == 0) + { + lsopt.default_server = 1; + continue; + } + + if (ngx_strcmp(value[n].data, "bind") == 0) { + lsopt.set = 1; + lsopt.bind = 1; + continue; + } + +#if (NGX_HAVE_SETFIB) + if (ngx_strncmp(value[n].data, "setfib=", 7) == 0) { + lsopt.setfib = ngx_atoi(value[n].data + 7, value[n].len - 7); + lsopt.set = 1; + lsopt.bind = 1; + + if (lsopt.setfib == NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid setfib \"%V\"", &value[n]); + return NGX_CONF_ERROR; + } + + continue; + } +#endif + +#if (NGX_HAVE_TCP_FASTOPEN) + if (ngx_strncmp(value[n].data, "fastopen=", 9) == 0) { + lsopt.fastopen = ngx_atoi(value[n].data + 9, value[n].len - 9); + lsopt.set = 1; + lsopt.bind = 1; + + if (lsopt.fastopen == NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid fastopen \"%V\"", &value[n]); + return NGX_CONF_ERROR; + } + + continue; + } +#endif + + if (ngx_strncmp(value[n].data, "backlog=", 8) == 0) { + lsopt.backlog = ngx_atoi(value[n].data + 8, value[n].len - 8); + lsopt.set = 1; + lsopt.bind = 1; + + if (lsopt.backlog == NGX_ERROR || lsopt.backlog == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid backlog \"%V\"", &value[n]); + return NGX_CONF_ERROR; + } + + continue; + } + + if (ngx_strncmp(value[n].data, "rcvbuf=", 7) == 0) { + size.len = value[n].len - 7; + size.data = value[n].data + 7; + + lsopt.rcvbuf = ngx_parse_size(&size); + lsopt.set = 1; + lsopt.bind = 1; + + if (lsopt.rcvbuf == NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid rcvbuf \"%V\"", &value[n]); + return NGX_CONF_ERROR; + } + + continue; + } + + if (ngx_strncmp(value[n].data, "sndbuf=", 7) == 0) { + size.len = value[n].len - 7; + size.data = value[n].data + 7; + + lsopt.sndbuf = ngx_parse_size(&size); + lsopt.set = 1; + lsopt.bind = 1; + + if (lsopt.sndbuf == NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid sndbuf \"%V\"", &value[n]); + return NGX_CONF_ERROR; + } + + continue; + } + + if (ngx_strncmp(value[n].data, "accept_filter=", 14) == 0) { +#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER) + lsopt.accept_filter = (char *) &value[n].data[14]; + lsopt.set = 1; + lsopt.bind = 1; +#else + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "accept filters \"%V\" are not supported " + "on this platform, ignored", + &value[n]); +#endif + continue; + } + + if (ngx_strcmp(value[n].data, "deferred") == 0) { +#if (NGX_HAVE_DEFERRED_ACCEPT && defined TCP_DEFER_ACCEPT) + lsopt.deferred_accept = 1; + lsopt.set = 1; + lsopt.bind = 1; +#else + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the deferred accept is not supported " + "on this platform, ignored"); +#endif + continue; + } + + if (ngx_strncmp(value[n].data, "ipv6only=o", 10) == 0) { +#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY) + struct sockaddr *sa; + + sa = &lsopt.sockaddr.sockaddr; + + if (sa->sa_family == AF_INET6) { + + if (ngx_strcmp(&value[n].data[10], "n") == 0) { + lsopt.ipv6only = 1; + + } else if (ngx_strcmp(&value[n].data[10], "ff") == 0) { + lsopt.ipv6only = 0; + + } else { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid ipv6only flags \"%s\"", + &value[n].data[9]); + return NGX_CONF_ERROR; + } + + lsopt.set = 1; + lsopt.bind = 1; + + } else { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "ipv6only is not supported " + "on addr \"%s\", ignored", lsopt.addr); + } + + continue; +#else + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "ipv6only is not supported " + "on this platform"); + return NGX_CONF_ERROR; +#endif + } + + if (ngx_strcmp(value[n].data, "reuseport") == 0) { +#if (NGX_HAVE_REUSEPORT) + lsopt.reuseport = 1; + lsopt.set = 1; + lsopt.bind = 1; +#else + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "reuseport is not supported " + "on this platform, ignored"); +#endif + continue; + } + + if (ngx_strncmp(value[n].data, "so_keepalive=", 13) == 0) { + + if (ngx_strcmp(&value[n].data[13], "on") == 0) { + lsopt.so_keepalive = 1; + + } else if (ngx_strcmp(&value[n].data[13], "off") == 0) { + lsopt.so_keepalive = 2; + + } else { + +#if (NGX_HAVE_KEEPALIVE_TUNABLE) + u_char *p, *end; + ngx_str_t s; + + end = value[n].data + value[n].len; + s.data = value[n].data + 13; + + p = ngx_strlchr(s.data, end, ':'); + if (p == NULL) { + p = end; + } + + if (p > s.data) { + s.len = p - s.data; + + lsopt.tcp_keepidle = ngx_parse_time(&s, 1); + if (lsopt.tcp_keepidle == (time_t) NGX_ERROR) { + goto invalid_so_keepalive; + } + } + + s.data = (p < end) ? (p + 1) : end; + + p = ngx_strlchr(s.data, end, ':'); + if (p == NULL) { + p = end; + } + + if (p > s.data) { + s.len = p - s.data; + + lsopt.tcp_keepintvl = ngx_parse_time(&s, 1); + if (lsopt.tcp_keepintvl == (time_t) NGX_ERROR) { + goto invalid_so_keepalive; + } + } + + s.data = (p < end) ? (p + 1) : end; + + if (s.data < end) { + s.len = end - s.data; + + lsopt.tcp_keepcnt = ngx_atoi(s.data, s.len); + if (lsopt.tcp_keepcnt == NGX_ERROR) { + goto invalid_so_keepalive; + } + } + + if (lsopt.tcp_keepidle == 0 && lsopt.tcp_keepintvl == 0 + && lsopt.tcp_keepcnt == 0) + { + goto invalid_so_keepalive; + } + + lsopt.so_keepalive = 1; + +#else + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the \"so_keepalive\" parameter accepts " + "only \"on\" or \"off\" on this platform"); + return NGX_CONF_ERROR; + +#endif + } + + lsopt.set = 1; + lsopt.bind = 1; + + continue; + +#if (NGX_HAVE_KEEPALIVE_TUNABLE) + invalid_so_keepalive: + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid so_keepalive value: \"%s\"", + &value[n].data[13]); + return NGX_CONF_ERROR; +#endif + } + + if (ngx_strcmp(value[n].data, "proxy_protocol") == 0) { + lsopt.proxy_protocol = 1; + continue; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid parameter \"%V\"", &value[n]); + return NGX_CONF_ERROR; + } + + cscf->port = u.port; + + if (ngx_rtmp_add_listen(cf, cscf, &lsopt) == NGX_OK) { + return NGX_CONF_OK; + } + + return NGX_CONF_ERROR; +} + + +static char * +ngx_rtmp_core_server_name(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_core_srv_conf_t *cscf = conf; + + u_char ch; + ngx_str_t *value; + ngx_uint_t i; + ngx_rtmp_server_name_t *sn; + + value = cf->args->elts; + + for (i = 1; i < cf->args->nelts; i++) { + + ch = value[i].data[0]; + + if ((ch == '*' && (value[i].len < 3 || value[i].data[1] != '.')) + || (ch == '.' && value[i].len < 2)) + { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "server name \"%V\" is invalid", &value[i]); + return NGX_CONF_ERROR; + } + + if (ngx_strchr(value[i].data, '/')) { + ngx_conf_log_error(NGX_LOG_WARN, cf, 0, + "server name \"%V\" has suspicious symbols", + &value[i]); + } + + sn = ngx_array_push(&cscf->server_names); + if (sn == NULL) { + return NGX_CONF_ERROR; + } + +#if (NGX_PCRE) + sn->regex = NULL; +#endif + sn->server = cscf; + + if (ngx_strcasecmp(value[i].data, (u_char *) "$hostname") == 0) { + sn->name = cf->cycle->hostname; + + } else { + sn->name = value[i]; + } + + if (value[i].data[0] != '~') { + ngx_strlow(sn->name.data, sn->name.data, sn->name.len); + continue; + } + +#if (NGX_PCRE) + { + u_char *p; + ngx_regex_compile_t rc; + u_char errstr[NGX_MAX_CONF_ERRSTR]; + + if (value[i].len == 1) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "empty regex in server name \"%V\"", &value[i]); + return NGX_CONF_ERROR; + } + + value[i].len--; + value[i].data++; + + ngx_memzero(&rc, sizeof(ngx_regex_compile_t)); + + rc.pattern = value[i]; + rc.err.len = NGX_MAX_CONF_ERRSTR; + rc.err.data = errstr; + + for (p = value[i].data; p < value[i].data + value[i].len; p++) { + if (*p >= 'A' && *p <= 'Z') { + rc.options = NGX_REGEX_CASELESS; + break; + } + } + + sn->regex = ngx_rtmp_regex_compile(cf, &rc); + if (sn->regex == NULL) { + return NGX_CONF_ERROR; + } + + sn->name = value[i]; + cscf->captures = (rc.captures > 0); + } +#else + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "using regex \"%V\" " + "requires PCRE library", &value[i]); + + return NGX_CONF_ERROR; +#endif + } + + return NGX_CONF_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_eval.c b/ngx_http_flv_module/ngx_rtmp_eval.c new file mode 100644 index 0000000..360819f --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_eval.c @@ -0,0 +1,293 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp_eval.h" + + +#define NGX_RTMP_EVAL_BUFLEN 16 + + +static void +ngx_rtmp_eval_session_str(void *ctx, ngx_rtmp_eval_t *e, ngx_str_t *ret) +{ + *ret = *(ngx_str_t *) ((u_char *) ctx + e->offset); +} + + +static void +ngx_rtmp_eval_connection_str(void *ctx, ngx_rtmp_eval_t *e, ngx_str_t *ret) +{ + ngx_rtmp_session_t *s = ctx; + + *ret = *(ngx_str_t *) ((u_char *) s->connection + e->offset); +} + + +ngx_rtmp_eval_t ngx_rtmp_eval_session[] = { + + { ngx_string("app"), + ngx_rtmp_eval_session_str, + offsetof(ngx_rtmp_session_t, app) }, + + { ngx_string("flashver"), + ngx_rtmp_eval_session_str, + offsetof(ngx_rtmp_session_t, flashver) }, + + { ngx_string("swfurl"), + ngx_rtmp_eval_session_str, + offsetof(ngx_rtmp_session_t, swf_url) }, + + { ngx_string("tcurl"), + ngx_rtmp_eval_session_str, + offsetof(ngx_rtmp_session_t, tc_url) }, + + { ngx_string("pageurl"), + ngx_rtmp_eval_session_str, + offsetof(ngx_rtmp_session_t, page_url) }, + + { ngx_string("addr"), + ngx_rtmp_eval_connection_str, + offsetof(ngx_connection_t, addr_text) }, + + ngx_rtmp_null_eval +}; + + +static void +ngx_rtmp_eval_append(ngx_buf_t *b, void *data, size_t len, ngx_log_t *log) +{ + size_t buf_len; + u_char *old; + + if (b->last + len > b->end) { + buf_len = (2 * (b->last - b->pos) + len + + NGX_RTMP_EVAL_BUFLEN - 1) & ~(NGX_RTMP_EVAL_BUFLEN - 1); + + old = b->start; + + b->start = ngx_alloc(buf_len, log); + if (b->start == NULL) { + return; + } + + b->last = ngx_cpymem(b->start, b->pos, b->last - b->pos); + b->pos = b->start; + b->end = b->start + buf_len; + + ngx_free(old); + } + + b->last = ngx_cpymem(b->last, data, len); +} + + +static void +ngx_rtmp_eval_append_var(void *ctx, ngx_buf_t *b, ngx_rtmp_eval_t **e, + ngx_str_t *name, ngx_log_t *log) +{ + ngx_str_t v; + ngx_rtmp_eval_t *ee; + + for (; *e; ++e) { + for (ee = *e; ee->handler; ++ee) { + if (ee->name.len == name->len && + ngx_memcmp(ee->name.data, name->data, name->len) == 0) + { + ee->handler(ctx, ee, &v); + ngx_rtmp_eval_append(b, v.data, v.len, log); + } + } + } +} + + +ngx_int_t +ngx_rtmp_eval(void *ctx, ngx_str_t *in, ngx_rtmp_eval_t **e, ngx_str_t *out, + ngx_log_t *log) +{ + u_char c, *p; + ngx_str_t name; + ngx_buf_t b; + ngx_uint_t n; + + enum { + NORMAL, + ESCAPE, + NAME, + SNAME + } state = NORMAL; + + b.pos = b.last = b.start = ngx_alloc(NGX_RTMP_EVAL_BUFLEN, log); + if (b.pos == NULL) { + return NGX_ERROR; + } + + b.end = b.pos + NGX_RTMP_EVAL_BUFLEN; + name.data = NULL; + + for (n = 0; n < in->len; ++n) { + p = &in->data[n]; + c = *p; + + switch (state) { + case SNAME: + if (c != '}') { + continue; + } + + name.len = p - name.data; + ngx_rtmp_eval_append_var(ctx, &b, e, &name, log); + + state = NORMAL; + + continue; + + case NAME: + if (c == '{' && name.data == p) { + ++name.data; + state = SNAME; + continue; + } + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + continue; + } + + name.len = p - name.data; + ngx_rtmp_eval_append_var(ctx, &b, e, &name, log); + + /* fall through */ + + case NORMAL: + switch (c) { + case '$': + name.data = p + 1; + state = NAME; + continue; + case '\\': + state = ESCAPE; + continue; + default: + break; + } + + /* fall through */ + + case ESCAPE: + ngx_rtmp_eval_append(&b, &c, 1, log); + state = NORMAL; + break; + + } + } + + if (state == NAME) { + p = &in->data[n]; + name.len = p - name.data; + ngx_rtmp_eval_append_var(ctx, &b, e, &name, log); + } + + c = 0; + ngx_rtmp_eval_append(&b, &c, 1, log); + + out->data = b.pos; + out->len = b.last - b.pos - 1; + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_eval_streams(ngx_str_t *in) +{ +#if !(NGX_WIN32) + ngx_int_t mode, create, v, close_src; + ngx_fd_t dst, src; + u_char *path; + + path = in->data; + + while (*path >= '0' && *path <= '9') { + path++; + } + + switch ((char) *path) { + + case '>': + + v = (path == in->data ? 1 : ngx_atoi(in->data, path - in->data)); + if (v == NGX_ERROR) { + return NGX_ERROR; + } + + dst = (ngx_fd_t) v; + mode = NGX_FILE_WRONLY; + create = NGX_FILE_TRUNCATE; + path++; + + if (*path == (u_char) '>') { + mode = NGX_FILE_APPEND; + create = NGX_FILE_CREATE_OR_OPEN; + path++; + } + + break; + + case '<': + + v = (path == in->data ? 0 : ngx_atoi(in->data, path - in->data)); + if (v == NGX_ERROR) { + return NGX_ERROR; + } + + dst = (ngx_fd_t) v; + mode = NGX_FILE_RDONLY; + create = NGX_FILE_OPEN; + path++; + + break; + + default: + + return NGX_DONE; + } + + if (*path == (u_char) '&') { + + path++; + v = ngx_atoi(path, in->data + in->len - path); + if (v == NGX_ERROR) { + return NGX_ERROR; + } + src = (ngx_fd_t) v; + close_src = 0; + + } else { + + src = ngx_open_file(path, mode, create, NGX_FILE_DEFAULT_ACCESS); + if (src == NGX_INVALID_FILE) { + return NGX_ERROR; + } + close_src = 1; + + } + + if (src == dst) { + return NGX_OK; + } + + dup2(src, dst); + + if (close_src) { + ngx_close_file(src); + } + return NGX_OK; + +#else + return NGX_DONE; +#endif +} diff --git a/ngx_http_flv_module/ngx_rtmp_eval.h b/ngx_http_flv_module/ngx_rtmp_eval.h new file mode 100644 index 0000000..b05d16b --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_eval.h @@ -0,0 +1,44 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_EVAL_H_INCLUDED_ +#define _NGX_RTMP_EVAL_H_INCLUDED_ + + +#include +#include +#include "ngx_rtmp.h" + + +typedef struct ngx_rtmp_eval_s ngx_rtmp_eval_t; + + +typedef void (* ngx_rtmp_eval_pt)(void *ctx, ngx_rtmp_eval_t *e, + ngx_str_t *ret); + + +struct ngx_rtmp_eval_s { + ngx_str_t name; + ngx_rtmp_eval_pt handler; + ngx_uint_t offset; +}; + + +#define ngx_rtmp_null_eval { ngx_null_string, NULL, 0 } + + +/* standard session eval variables */ +extern ngx_rtmp_eval_t ngx_rtmp_eval_session[]; + + +ngx_int_t ngx_rtmp_eval(void *ctx, ngx_str_t *in, ngx_rtmp_eval_t **e, + ngx_str_t *out, ngx_log_t *log); + + +ngx_int_t ngx_rtmp_eval_streams(ngx_str_t *in); + + +#endif /* _NGX_RTMP_EVAL_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_exec_module.c b/ngx_http_flv_module/ngx_rtmp_exec_module.c new file mode 100644 index 0000000..fb36511 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_exec_module.c @@ -0,0 +1,1604 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_record_module.h" +#include "ngx_rtmp_eval.h" +#include + +#ifdef NGX_LINUX +#include +#endif + + +#if !(NGX_WIN32) +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_close_stream_pt next_close_stream; +static ngx_rtmp_record_done_pt next_record_done; +#endif + + +static ngx_int_t ngx_rtmp_exec_init_process(ngx_cycle_t *cycle); +static ngx_int_t ngx_rtmp_exec_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_exec_create_main_conf(ngx_conf_t *cf); +static char * ngx_rtmp_exec_init_main_conf(ngx_conf_t *cf, void *conf); +static void * ngx_rtmp_exec_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_exec_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +/*static char * ngx_rtmp_exec_block(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf);*/ +static char * ngx_rtmp_exec_conf(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); + + +#define NGX_RTMP_EXEC_RESPAWN 0x01 +#define NGX_RTMP_EXEC_KILL 0x02 + + +#define NGX_RTMP_EXEC_PUBLISHING 0x01 +#define NGX_RTMP_EXEC_PLAYING 0x02 + + +enum { + NGX_RTMP_EXEC_PUSH, + NGX_RTMP_EXEC_PULL, + + NGX_RTMP_EXEC_PUBLISH, + NGX_RTMP_EXEC_PUBLISH_DONE, + NGX_RTMP_EXEC_PLAY, + NGX_RTMP_EXEC_PLAY_DONE, + NGX_RTMP_EXEC_RECORD_DONE, + + NGX_RTMP_EXEC_MAX, + + NGX_RTMP_EXEC_STATIC +}; + + +typedef struct { + ngx_str_t id; + ngx_uint_t type; + ngx_str_t cmd; + ngx_array_t args; /* ngx_str_t */ + ngx_array_t names; +} ngx_rtmp_exec_conf_t; + + +typedef struct { + ngx_rtmp_exec_conf_t *conf; + ngx_log_t *log; + ngx_rtmp_eval_t **eval; + void *eval_ctx; + unsigned active:1; + unsigned managed:1; + ngx_pid_t pid; + ngx_pid_t *save_pid; + int pipefd; + ngx_connection_t dummy_conn; /*needed by ngx_xxx_event*/ + ngx_event_t read_evt, write_evt; + ngx_event_t respawn_evt; + ngx_msec_t respawn_timeout; + ngx_int_t kill_signal; +} ngx_rtmp_exec_t; + + +typedef struct { + ngx_array_t static_conf; /* ngx_rtmp_exec_conf_t */ + ngx_array_t static_exec; /* ngx_rtmp_exec_t */ + ngx_msec_t respawn_timeout; + ngx_int_t kill_signal; + ngx_log_t *log; +} ngx_rtmp_exec_main_conf_t; + + +typedef struct ngx_rtmp_exec_pull_ctx_s ngx_rtmp_exec_pull_ctx_t; + +struct ngx_rtmp_exec_pull_ctx_s { + ngx_pool_t *pool; + ngx_uint_t counter; + ngx_str_t name; + ngx_str_t app; + ngx_array_t pull_exec; /* ngx_rtmp_exec_t */ + ngx_rtmp_exec_pull_ctx_t *next; +}; + + +typedef struct { + ngx_int_t active; + ngx_array_t conf[NGX_RTMP_EXEC_MAX]; + /* ngx_rtmp_exec_conf_t */ + ngx_flag_t respawn; + ngx_flag_t options; + ngx_uint_t nbuckets; + ngx_rtmp_exec_pull_ctx_t **pull; +} ngx_rtmp_exec_app_conf_t; + + +typedef struct { + ngx_uint_t flags; + ngx_str_t path; /* /tmp/rec/myfile-123.flv */ + ngx_str_t filename; /* myfile-123.flv */ + ngx_str_t basename; /* myfile-123 */ + ngx_str_t dirname; /* /tmp/rec */ + ngx_str_t recorder; + u_char name[NGX_RTMP_MAX_NAME]; + u_char args[NGX_RTMP_MAX_ARGS]; + ngx_array_t push_exec; /* ngx_rtmp_exec_t */ + ngx_rtmp_exec_pull_ctx_t *pull; +} ngx_rtmp_exec_ctx_t; + + +#if !(NGX_WIN32) +static void ngx_rtmp_exec_respawn(ngx_event_t *ev); +static ngx_int_t ngx_rtmp_exec_kill(ngx_rtmp_exec_t *e, ngx_int_t kill_signal); +static ngx_int_t ngx_rtmp_exec_run(ngx_rtmp_exec_t *e); +#endif + + +static ngx_command_t ngx_rtmp_exec_commands[] = { +/* + { ngx_string("exec_block"), + NGX_RTMP_APP_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS|NGX_CONF_TAKE1, + ngx_rtmp_exec_block, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, +*/ + { ngx_string("exec"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, conf) + + NGX_RTMP_EXEC_PUSH * sizeof(ngx_array_t), + NULL }, + + { ngx_string("exec_push"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, conf) + + NGX_RTMP_EXEC_PUSH * sizeof(ngx_array_t), + NULL }, + + { ngx_string("exec_pull"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, conf) + + NGX_RTMP_EXEC_PULL * sizeof(ngx_array_t), + NULL }, + + { ngx_string("exec_publish"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, conf) + + NGX_RTMP_EXEC_PUBLISH * sizeof(ngx_array_t), + NULL }, + + { ngx_string("exec_publish_done"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, conf) + + NGX_RTMP_EXEC_PUBLISH_DONE * sizeof(ngx_array_t), + NULL }, + + { ngx_string("exec_play"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, conf) + + NGX_RTMP_EXEC_PLAY * sizeof(ngx_array_t), + NULL }, + + { ngx_string("exec_play_done"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, conf) + + NGX_RTMP_EXEC_PLAY_DONE * sizeof(ngx_array_t), + NULL }, + + { ngx_string("exec_record_done"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_RTMP_REC_CONF| + NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, conf) + + NGX_RTMP_EXEC_RECORD_DONE * sizeof(ngx_array_t), + NULL }, + + { ngx_string("exec_static"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_exec_conf, + NGX_RTMP_MAIN_CONF_OFFSET, + offsetof(ngx_rtmp_exec_main_conf_t, static_conf), + NULL }, + + { ngx_string("respawn"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, respawn), + NULL }, + + { ngx_string("respawn_timeout"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_MAIN_CONF_OFFSET, + offsetof(ngx_rtmp_exec_main_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_MAIN_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("exec_options"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_exec_app_conf_t, options), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_exec_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_exec_postconfiguration, /* postconfiguration */ + ngx_rtmp_exec_create_main_conf, /* create main configuration */ + ngx_rtmp_exec_init_main_conf, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + ngx_rtmp_exec_create_app_conf, /* create app configuration */ + ngx_rtmp_exec_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_exec_module = { + NGX_MODULE_V1, + &ngx_rtmp_exec_module_ctx, /* module context */ + ngx_rtmp_exec_commands, /* module directives */ + NGX_RTMP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + ngx_rtmp_exec_init_process, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static void +ngx_rtmp_exec_eval_ctx_cstr(void *sctx, ngx_rtmp_eval_t *e, ngx_str_t *ret) +{ + ngx_rtmp_session_t *s = sctx; + + ngx_rtmp_exec_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + if (ctx == NULL) { + ret->len = 0; + return; + } + + ret->data = (u_char *) ctx + e->offset; + ret->len = ngx_strlen(ret->data); +} + + +static void +ngx_rtmp_exec_eval_ctx_str(void *sctx, ngx_rtmp_eval_t *e, ngx_str_t *ret) +{ + ngx_rtmp_session_t *s = sctx; + + ngx_rtmp_exec_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + if (ctx == NULL) { + ret->len = 0; + return; + } + + *ret = * (ngx_str_t *) ((u_char *) ctx + e->offset); +} + + +static void +ngx_rtmp_exec_eval_pctx_str(void *ctx, ngx_rtmp_eval_t *e, ngx_str_t *ret) +{ + *ret = *(ngx_str_t *) ((u_char *) ctx + e->offset); +} + + +static ngx_rtmp_eval_t ngx_rtmp_exec_push_specific_eval[] = { + + { ngx_string("name"), + ngx_rtmp_exec_eval_ctx_cstr, + offsetof(ngx_rtmp_exec_ctx_t, name) }, + + { ngx_string("args"), + ngx_rtmp_exec_eval_ctx_cstr, + offsetof(ngx_rtmp_exec_ctx_t, args) }, + + ngx_rtmp_null_eval +}; + + +static ngx_rtmp_eval_t * ngx_rtmp_exec_push_eval[] = { + ngx_rtmp_eval_session, + ngx_rtmp_exec_push_specific_eval, + NULL +}; + + +static ngx_rtmp_eval_t ngx_rtmp_exec_pull_specific_eval[] = { + + { ngx_string("name"), + ngx_rtmp_exec_eval_pctx_str, + offsetof(ngx_rtmp_exec_pull_ctx_t, name) }, + + { ngx_string("app"), + ngx_rtmp_exec_eval_pctx_str, + offsetof(ngx_rtmp_exec_pull_ctx_t, app) }, + + ngx_rtmp_null_eval +}; + + +static ngx_rtmp_eval_t * ngx_rtmp_exec_pull_eval[] = { + ngx_rtmp_exec_pull_specific_eval, + NULL +}; + + +static ngx_rtmp_eval_t ngx_rtmp_exec_event_specific_eval[] = { + + { ngx_string("name"), + ngx_rtmp_exec_eval_ctx_cstr, + offsetof(ngx_rtmp_exec_ctx_t, name) }, + + { ngx_string("args"), + ngx_rtmp_exec_eval_ctx_cstr, + offsetof(ngx_rtmp_exec_ctx_t, args) }, + + { ngx_string("path"), + ngx_rtmp_exec_eval_ctx_str, + offsetof(ngx_rtmp_exec_ctx_t, path) }, + + { ngx_string("filename"), + ngx_rtmp_exec_eval_ctx_str, + offsetof(ngx_rtmp_exec_ctx_t, filename) }, + + { ngx_string("basename"), + ngx_rtmp_exec_eval_ctx_str, + offsetof(ngx_rtmp_exec_ctx_t, basename) }, + + { ngx_string("dirname"), + ngx_rtmp_exec_eval_ctx_str, + offsetof(ngx_rtmp_exec_ctx_t, dirname) }, + + { ngx_string("recorder"), + ngx_rtmp_exec_eval_ctx_str, + offsetof(ngx_rtmp_exec_ctx_t, recorder) }, + + ngx_rtmp_null_eval +}; + + +static ngx_rtmp_eval_t * ngx_rtmp_exec_event_eval[] = { + ngx_rtmp_eval_session, + ngx_rtmp_exec_event_specific_eval, + NULL +}; + + +static void * +ngx_rtmp_exec_create_main_conf(ngx_conf_t *cf) +{ + ngx_rtmp_exec_main_conf_t *emcf; + + emcf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_exec_main_conf_t)); + if (emcf == NULL) { + return NULL; + } + + emcf->respawn_timeout = NGX_CONF_UNSET_MSEC; + emcf->kill_signal = NGX_CONF_UNSET; + + if (ngx_array_init(&emcf->static_conf, cf->pool, 1, + sizeof(ngx_rtmp_exec_conf_t)) != NGX_OK) + { + return NULL; + } + + return emcf; +} + + +static char * +ngx_rtmp_exec_init_main_conf(ngx_conf_t *cf, void *conf) +{ + ngx_rtmp_exec_main_conf_t *emcf = conf; + ngx_rtmp_exec_conf_t *ec; + ngx_rtmp_exec_t *e; + ngx_uint_t n; + + if (emcf->respawn_timeout == NGX_CONF_UNSET_MSEC) { + emcf->respawn_timeout = 5000; + } + +#if !(NGX_WIN32) + if (emcf->kill_signal == NGX_CONF_UNSET) { + emcf->kill_signal = SIGKILL; + } +#endif + + if (ngx_array_init(&emcf->static_exec, cf->pool, + emcf->static_conf.nelts, + sizeof(ngx_rtmp_exec_t)) != NGX_OK) + { + return NGX_CONF_ERROR; + } + + e = ngx_array_push_n(&emcf->static_exec, emcf->static_conf.nelts); + if (e == NULL) { + return NGX_CONF_ERROR; + } + + emcf->log = &cf->cycle->new_log; + + ec = emcf->static_conf.elts; + + for (n = 0; n < emcf->static_conf.nelts; n++, e++, ec++) { + ngx_memzero(e, sizeof(*e)); + e->conf = ec; + e->managed = 1; + e->log = emcf->log; + e->respawn_timeout = emcf->respawn_timeout; + e->kill_signal = emcf->kill_signal; + } + + return NGX_CONF_OK; +} + + +static void * +ngx_rtmp_exec_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_exec_app_conf_t *eacf; + + eacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_exec_app_conf_t)); + if (eacf == NULL) { + return NULL; + } + + eacf->respawn = NGX_CONF_UNSET; + eacf->options = NGX_CONF_UNSET; + eacf->nbuckets = NGX_CONF_UNSET_UINT; + + return eacf; +} + + +static ngx_int_t +ngx_rtmp_exec_merge_confs(ngx_array_t *conf, ngx_array_t *prev) +{ + size_t n; + ngx_rtmp_exec_conf_t *ec, *pec; + + if (prev->nelts == 0) { + return NGX_OK; + } + + if (conf->nelts == 0) { + *conf = *prev; + return NGX_OK; + } + + ec = ngx_array_push_n(conf, prev->nelts); + if (ec == NULL) { + return NGX_ERROR; + } + + pec = prev->elts; + for (n = 0; n < prev->nelts; n++, ec++, pec++) { + *ec = *pec; + } + + return NGX_OK; +} + + +static char * +ngx_rtmp_exec_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_exec_app_conf_t *prev = parent; + ngx_rtmp_exec_app_conf_t *conf = child; + + ngx_uint_t n; + + ngx_conf_merge_value(conf->respawn, prev->respawn, 1); + ngx_conf_merge_uint_value(conf->nbuckets, prev->nbuckets, 1024); + + for (n = 0; n < NGX_RTMP_EXEC_MAX; n++) { + if (ngx_rtmp_exec_merge_confs(&conf->conf[n], &prev->conf[n]) != NGX_OK) + { + return NGX_CONF_ERROR; + } + + if (conf->conf[n].nelts) { + conf->active = 1; + prev->active = 1; + } + } + + if (conf->conf[NGX_RTMP_EXEC_PULL].nelts > 0) { + conf->pull = ngx_pcalloc(cf->pool, sizeof(void *) * conf->nbuckets); + if (conf->pull == NULL) { + return NGX_CONF_ERROR; + } + } + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_exec_init_process(ngx_cycle_t *cycle) +{ +#if !(NGX_WIN32) + ngx_rtmp_core_main_conf_t *cmcf = ngx_rtmp_core_main_conf; + ngx_rtmp_core_srv_conf_t **cscf; + ngx_rtmp_conf_ctx_t *cctx; + ngx_rtmp_exec_main_conf_t *emcf; + ngx_rtmp_exec_t *e; + ngx_uint_t n; + + if (cmcf == NULL || cmcf->servers.nelts == 0) { + return NGX_OK; + } + + /* execs are always started by the first worker */ + if (ngx_process_slot) { + return NGX_OK; + } + + cscf = cmcf->servers.elts; + cctx = (*cscf)->ctx; + emcf = cctx->main_conf[ngx_rtmp_exec_module.ctx_index]; + + /* FreeBSD note: + * When worker is restarted, child process (ffmpeg) will + * not be terminated if it's connected to another + * (still alive) worker. That leads to starting + * another instance of exec_static process. + * Need to kill previously started processes. + * + * On Linux "prctl" syscall is used to kill child + * when nginx worker is terminated. + */ + + e = emcf->static_exec.elts; + for (n = 0; n < emcf->static_exec.nelts; ++n, ++e) { + e->respawn_evt.data = e; + e->respawn_evt.log = e->log; + e->respawn_evt.handler = ngx_rtmp_exec_respawn; + ngx_post_event((&e->respawn_evt), &ngx_rtmp_init_queue); + } +#endif + + return NGX_OK; +} + + +#if !(NGX_WIN32) +static void +ngx_rtmp_exec_respawn(ngx_event_t *ev) +{ + ngx_rtmp_exec_run((ngx_rtmp_exec_t *) ev->data); +} + + +static void +ngx_rtmp_exec_child_dead(ngx_event_t *ev) +{ + ngx_connection_t *dummy_conn = ev->data; + ngx_rtmp_exec_t *e; + + e = dummy_conn->data; + + ngx_log_error(NGX_LOG_INFO, e->log, 0, + "exec: child %ui exited; %s", (ngx_int_t) e->pid, + e->respawn_timeout == NGX_CONF_UNSET_MSEC ? "respawning" : + "ignoring"); + + ngx_rtmp_exec_kill(e, 0); + + if (e->respawn_timeout == NGX_CONF_UNSET_MSEC) { + return; + } + + if (e->respawn_timeout == 0) { + ngx_rtmp_exec_run(e); + return; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, e->log, 0, + "exec: schedule respawn %Mmsec", e->respawn_timeout); + + e->respawn_evt.data = e; + e->respawn_evt.log = e->log; + e->respawn_evt.handler = ngx_rtmp_exec_respawn; + + ngx_add_timer(&e->respawn_evt, e->respawn_timeout); +} + + +static ngx_int_t +ngx_rtmp_exec_kill(ngx_rtmp_exec_t *e, ngx_int_t kill_signal) +{ + if (e->respawn_evt.timer_set) { + ngx_del_timer(&e->respawn_evt); + } + + if (e->read_evt.active) { + ngx_del_event(&e->read_evt, NGX_READ_EVENT, 0); + } + + if (e->active == 0) { + return NGX_OK; + } + + ngx_log_error(NGX_LOG_INFO, e->log, 0, + "exec: terminating child %ui", (ngx_int_t) e->pid); + + e->active = 0; + close(e->pipefd); + if (e->save_pid) { + *e->save_pid = NGX_INVALID_PID; + } + + if (kill_signal == 0) { + return NGX_OK; + } + + if (kill(e->pid, kill_signal) == -1) { + ngx_log_error(NGX_LOG_INFO, e->log, ngx_errno, + "exec: kill failed pid=%i", (ngx_int_t) e->pid); + } else { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, e->log, 0, + "exec: killed pid=%i", (ngx_int_t) e->pid); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_exec_run(ngx_rtmp_exec_t *e) +{ + int fd, ret, maxfd, pipefd[2]; + char **args, **arg_out; + ngx_pid_t pid; + ngx_str_t *arg_in, a; + ngx_uint_t n; + ngx_rtmp_exec_conf_t *ec; + + ec = e->conf; + + ngx_log_error(NGX_LOG_INFO, e->log, 0, + "exec: starting %s child '%V'", + e->managed ? "managed" : "unmanaged", &ec->cmd); + + pipefd[0] = -1; + pipefd[1] = -1; + + if (e->managed) { + + if (e->active) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, e->log, 0, + "exec: already active '%V'", &ec->cmd); + return NGX_OK; + } + + if (pipe(pipefd) == -1) { + ngx_log_error(NGX_LOG_INFO, e->log, ngx_errno, + "exec: pipe failed"); + return NGX_ERROR; + } + + /* make pipe write end survive through exec */ + + ret = fcntl(pipefd[1], F_GETFD); + + if (ret != -1) { + ret &= ~FD_CLOEXEC; + ret = fcntl(pipefd[1], F_SETFD, ret); + } + + if (ret == -1) { + + close(pipefd[0]); + close(pipefd[1]); + + ngx_log_error(NGX_LOG_INFO, e->log, ngx_errno, + "exec: fcntl failed"); + + return NGX_ERROR; + } + } + + pid = fork(); + + switch (pid) { + + case -1: + + /* failure */ + + if (pipefd[0] != -1) { + close(pipefd[0]); + } + + if (pipefd[1] != -1) { + close(pipefd[1]); + } + + ngx_log_error(NGX_LOG_INFO, e->log, ngx_errno, + "exec: fork failed"); + + return NGX_ERROR; + + case 0: + + /* child */ + +#if (NGX_LINUX) + if (e->managed) { + prctl(PR_SET_PDEATHSIG, e->kill_signal, 0, 0, 0); + } +#endif + + /* 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); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + + args = ngx_alloc((ec->args.nelts + 2) * sizeof(char *), e->log); + if (args == NULL) { + exit(1); + } + + arg_in = ec->args.elts; + arg_out = args; + *arg_out++ = (char *) ec->cmd.data; + + for (n = 0; n < ec->args.nelts; n++, ++arg_in) { + + if (e->eval == NULL) { + a = *arg_in; + } else { + ngx_rtmp_eval(e->eval_ctx, arg_in, e->eval, &a, e->log); + } + + if (ngx_rtmp_eval_streams(&a) != NGX_DONE) { + continue; + } + + *arg_out++ = (char *) a.data; + } + + *arg_out = NULL; + +#if (NGX_DEBUG) + { + char **p; + + for (p = args; *p; p++) { + ngx_write_fd(STDERR_FILENO, "'", 1); + ngx_write_fd(STDERR_FILENO, *p, strlen(*p)); + ngx_write_fd(STDERR_FILENO, "' ", 2); + } + + ngx_write_fd(STDERR_FILENO, "\n", 1); + } +#endif + + if (execvp((char *) ec->cmd.data, args) == -1) { + char *msg; + + msg = strerror(errno); + + ngx_write_fd(STDERR_FILENO, "execvp error: ", 14); + ngx_write_fd(STDERR_FILENO, msg, strlen(msg)); + ngx_write_fd(STDERR_FILENO, "\n", 1); + + exit(1); + } + + break; + + default: + + /* parent */ + + if (pipefd[1] != -1) { + close(pipefd[1]); + } + + if (pipefd[0] != -1) { + + e->active = 1; + e->pid = pid; + e->pipefd = pipefd[0]; + + if (e->save_pid) { + *e->save_pid = pid; + } + + e->dummy_conn.fd = e->pipefd; + e->dummy_conn.data = e; + e->dummy_conn.read = &e->read_evt; + e->dummy_conn.write = &e->write_evt; + e->read_evt.data = &e->dummy_conn; + e->write_evt.data = &e->dummy_conn; + + e->read_evt.log = e->log; + e->read_evt.handler = ngx_rtmp_exec_child_dead; + + if (ngx_add_event(&e->read_evt, NGX_READ_EVENT, 0) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, e->log, ngx_errno, + "exec: failed to add child control event"); + } + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, e->log, 0, + "exec: child '%V' started pid=%i", + &ec->cmd, (ngx_int_t) pid); + break; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_exec_init_ctx(ngx_rtmp_session_t *s, u_char name[NGX_RTMP_MAX_NAME], + u_char args[NGX_RTMP_MAX_ARGS], ngx_uint_t flags) +{ + ngx_uint_t n; + ngx_array_t *push_conf; + ngx_rtmp_exec_t *e; + ngx_rtmp_exec_ctx_t *ctx; + ngx_rtmp_exec_conf_t *ec; + ngx_rtmp_exec_app_conf_t *eacf; + ngx_rtmp_exec_main_conf_t *emcf; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + + if (ctx != NULL) { + goto done; + } + + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_exec_ctx_t)); + + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_exec_module); + + eacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_exec_module); + + emcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_exec_module); + + push_conf = &eacf->conf[NGX_RTMP_EXEC_PUSH]; + + if (push_conf->nelts > 0) { + + if (ngx_array_init(&ctx->push_exec, s->connection->pool, + push_conf->nelts, + sizeof(ngx_rtmp_exec_t)) != NGX_OK) + { + return NGX_ERROR; + } + + e = ngx_array_push_n(&ctx->push_exec, push_conf->nelts); + + if (e == NULL) { + return NGX_ERROR; + } + + ec = push_conf->elts; + + for (n = 0; n < push_conf->nelts; n++, e++, ec++) { + ngx_memzero(e, sizeof(*e)); + e->conf = ec; + e->managed = 1; + e->log = s->connection->log; + e->eval = ngx_rtmp_exec_push_eval; + e->eval_ctx = s; + e->kill_signal = emcf->kill_signal; + e->respawn_timeout = (eacf->respawn ? emcf->respawn_timeout : + NGX_CONF_UNSET_MSEC); + } + } + +done: + + ngx_memcpy(ctx->name, name, NGX_RTMP_MAX_NAME); + ngx_memcpy(ctx->args, args, NGX_RTMP_MAX_ARGS); + + ctx->flags |= flags; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_exec_init_pull_ctx(ngx_rtmp_session_t *s, + u_char name[NGX_RTMP_MAX_NAME]) +{ + size_t len; + ngx_uint_t n; + ngx_pool_t *pool; + ngx_array_t *pull_conf; + ngx_rtmp_exec_t *e; + ngx_rtmp_exec_ctx_t *ctx; + ngx_rtmp_exec_conf_t *ec; + ngx_rtmp_exec_pull_ctx_t *pctx, **ppctx; + ngx_rtmp_exec_app_conf_t *eacf; + ngx_rtmp_exec_main_conf_t *emcf; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + if (ctx->pull != NULL) { + return NGX_OK; + } + + eacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_exec_module); + + pull_conf = &eacf->conf[NGX_RTMP_EXEC_PULL]; + + if (pull_conf->nelts == 0) { + return NGX_OK; + } + + emcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_exec_module); + + len = ngx_strlen(name); + + ppctx = &eacf->pull[ngx_hash_key(name, len) % eacf->nbuckets]; + + for (; *ppctx; ppctx = &(*ppctx)->next) { + pctx = *ppctx; + + if (pctx->name.len == len && + ngx_strncmp(name, pctx->name.data, len) == 0) + { + goto done; + } + } + + pool = ngx_create_pool(4096, emcf->log); + if (pool == NULL) { + return NGX_ERROR; + } + + pctx = ngx_pcalloc(pool, sizeof(ngx_rtmp_exec_pull_ctx_t)); + if (pctx == NULL) { + goto error; + } + + pctx->pool = pool; + pctx->name.len = len; + pctx->name.data = ngx_palloc(pool, len); + + if (pctx->name.data == NULL) { + goto error; + } + + ngx_memcpy(pctx->name.data, name, len); + + pctx->app.len = s->app.len; + pctx->app.data = ngx_palloc(pool, s->app.len); + + if (pctx->app.data == NULL) { + goto error; + } + + ngx_memcpy(pctx->app.data, s->app.data, s->app.len); + + if (ngx_array_init(&pctx->pull_exec, pool, pull_conf->nelts, + sizeof(ngx_rtmp_exec_t)) != NGX_OK) + { + goto error; + } + + e = ngx_array_push_n(&pctx->pull_exec, pull_conf->nelts); + if (e == NULL) { + goto error; + } + + ec = pull_conf->elts; + for (n = 0; n < pull_conf->nelts; n++, e++, ec++) { + ngx_memzero(e, sizeof(*e)); + e->conf = ec; + e->managed = 1; + e->log = emcf->log; + e->eval = ngx_rtmp_exec_pull_eval; + e->eval_ctx = pctx; + e->kill_signal = emcf->kill_signal; + e->respawn_timeout = (eacf->respawn ? emcf->respawn_timeout : + NGX_CONF_UNSET_MSEC); + } + + *ppctx = pctx; + +done: + + ctx->pull = pctx; + ctx->pull->counter++; + + return NGX_OK; + +error: + + ngx_destroy_pool(pool); + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_exec_filter(ngx_rtmp_session_t *s, ngx_rtmp_exec_conf_t *ec) +{ + size_t len; + ngx_str_t *v; + ngx_uint_t n; + ngx_rtmp_exec_ctx_t *ctx; + + if (ec->names.nelts == 0) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + + len = ngx_strlen(ctx->name); + + v = ec->names.elts; + for (n = 0; n < ec->names.nelts; n++, s++) { + if (v->len == len && ngx_strncmp(v->data, ctx->name, len) == 0) { + return NGX_OK; + } + } + + return NGX_DECLINED; +} + + +static void +ngx_rtmp_exec_unmanaged(ngx_rtmp_session_t *s, ngx_array_t *e, const char *op) +{ + ngx_uint_t n; + ngx_rtmp_exec_t en; + ngx_rtmp_exec_conf_t *ec; + + if (e->nelts == 0) { + return; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "exec: %s %uz unmanaged command(s)", op, e->nelts); + + ec = e->elts; + for (n = 0; n < e->nelts; n++, ec++) { + if (ngx_rtmp_exec_filter(s, ec) != NGX_OK) { + continue; + } + + ngx_memzero(&en, sizeof(ngx_rtmp_exec_t)); + + en.conf = ec; + en.eval = ngx_rtmp_exec_event_eval; + en.eval_ctx = s; + en.log = s->connection->log; + + ngx_rtmp_exec_run(&en); + } +} + + +static void +ngx_rtmp_exec_managed(ngx_rtmp_session_t *s, ngx_array_t *e, const char *op) +{ + ngx_uint_t n; + ngx_rtmp_exec_t *en; + + if (e->nelts == 0) { + return; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "exec: %s %uz managed command(s)", op, e->nelts); + + en = e->elts; + for (n = 0; n < e->nelts; n++, en++) { + if (ngx_rtmp_exec_filter(s, en->conf) == NGX_OK) { + ngx_rtmp_exec_run(en); + } + } +} + + +static ngx_int_t +ngx_rtmp_exec_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_exec_ctx_t *ctx; + ngx_rtmp_exec_app_conf_t *eacf; + + eacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_exec_module); + + if (eacf == NULL || !eacf->active) { + goto next; + } + + if (s->auto_pushed) { + goto next; + } + + if (ngx_rtmp_exec_init_ctx(s, v->name, v->args, NGX_RTMP_EXEC_PUBLISHING) + != NGX_OK) + { + goto next; + } + + ngx_rtmp_exec_unmanaged(s, &eacf->conf[NGX_RTMP_EXEC_PUBLISH], "publish"); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + + ngx_rtmp_exec_managed(s, &ctx->push_exec, "push"); + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_exec_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_exec_ctx_t *ctx; + ngx_rtmp_exec_pull_ctx_t *pctx; + ngx_rtmp_exec_app_conf_t *eacf; + + eacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_exec_module); + + if (eacf == NULL || !eacf->active) { + goto next; + } + + if (ngx_rtmp_exec_init_ctx(s, v->name, v->args, NGX_RTMP_EXEC_PLAYING) + != NGX_OK) + { + goto next; + } + + ngx_rtmp_exec_unmanaged(s, &eacf->conf[NGX_RTMP_EXEC_PLAY], "play"); + + if (ngx_rtmp_exec_init_pull_ctx(s, v->name) != NGX_OK) { + goto next; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + pctx = ctx->pull; + + if (pctx && pctx->counter == 1) { + ngx_rtmp_exec_managed(s, &pctx->pull_exec, "pull"); + } + +next: + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_exec_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + size_t n; + ngx_rtmp_exec_t *e; + ngx_rtmp_exec_ctx_t *ctx; + ngx_rtmp_exec_pull_ctx_t *pctx, **ppctx; + ngx_rtmp_exec_app_conf_t *eacf; + + eacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_exec_module); + if (eacf == NULL) { + goto next; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + if (ctx == NULL) { + goto next; + } + + if (ctx->flags & NGX_RTMP_EXEC_PUBLISHING) { + ngx_rtmp_exec_unmanaged(s, &eacf->conf[NGX_RTMP_EXEC_PUBLISH_DONE], + "publish_done"); + } + + if (ctx->flags & NGX_RTMP_EXEC_PLAYING) { + ngx_rtmp_exec_unmanaged(s, &eacf->conf[NGX_RTMP_EXEC_PLAY_DONE], + "play_done"); + } + + ctx->flags = 0; + + if (ctx->push_exec.nelts > 0) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "exec: delete %uz push command(s)", + ctx->push_exec.nelts); + + e = ctx->push_exec.elts; + for (n = 0; n < ctx->push_exec.nelts; n++, e++) { + ngx_rtmp_exec_kill(e, e->kill_signal); + } + } + + pctx = ctx->pull; + + if (pctx && --pctx->counter == 0) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "exec: delete %uz pull command(s)", + pctx->pull_exec.nelts); + + e = pctx->pull_exec.elts; + for (n = 0; n < pctx->pull_exec.nelts; n++, e++) { + ngx_rtmp_exec_kill(e, e->kill_signal); + } + + ppctx = &eacf->pull[ngx_hash_key(pctx->name.data, pctx->name.len) % + eacf->nbuckets]; + + for (; *ppctx; ppctx = &(*ppctx)->next) { + if (pctx == *ppctx) { + *ppctx = pctx->next; + break; + } + } + + ngx_destroy_pool(pctx->pool); + } + + ctx->pull = NULL; + +next: + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_exec_record_done(ngx_rtmp_session_t *s, ngx_rtmp_record_done_t *v) +{ + u_char c; + ngx_uint_t ext, dir; + ngx_rtmp_exec_ctx_t *ctx; + ngx_rtmp_exec_app_conf_t *eacf; + + if (s->auto_pushed) { + goto next; + } + + eacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_exec_module); + if (eacf == NULL || !eacf->active) { + goto next; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_exec_module); + if (ctx == NULL) { + goto next; + } + + ctx->recorder = v->recorder; + ctx->path = v->path; + + ctx->dirname.data = ctx->path.data; + ctx->dirname.len = 0; + + for (dir = ctx->path.len; dir > 0; dir--) { + c = ctx->path.data[dir - 1]; + if (c == '/' || c == '\\') { + ctx->dirname.len = dir - 1; + break; + } + } + + ctx->filename.data = ctx->path.data + dir; + ctx->filename.len = ctx->path.len - dir; + + ctx->basename = ctx->filename; + + for (ext = ctx->filename.len; ext > 0; ext--) { + if (ctx->filename.data[ext - 1] == '.') { + ctx->basename.len = ext - 1; + break; + } + } + + ngx_rtmp_exec_unmanaged(s, &eacf->conf[NGX_RTMP_EXEC_RECORD_DONE], + "record_done"); + + ngx_str_null(&v->recorder); + ngx_str_null(&v->path); + +next: + return next_record_done(s, v); +} +#endif /* NGX_WIN32 */ + + +static char * +ngx_rtmp_exec_conf(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + char *p = conf; + + size_t n, nargs; + ngx_str_t *s, *value, v; + ngx_array_t *confs; + ngx_rtmp_exec_conf_t *ec; + ngx_rtmp_exec_app_conf_t *eacf; + + confs = (ngx_array_t *) (p + cmd->offset); + + eacf = ngx_rtmp_conf_get_module_app_conf(cf, ngx_rtmp_exec_module); + + if (confs->nalloc == 0 && ngx_array_init(confs, cf->pool, 1, + sizeof(ngx_rtmp_exec_conf_t)) + != NGX_OK) + { + return NGX_CONF_ERROR; + } + + value = cf->args->elts; + + ec = ngx_array_push(confs); + if (ec == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memzero(ec, sizeof(ngx_rtmp_exec_conf_t)); + + /* type is undefined for explicit execs */ + + ec->type = NGX_CONF_UNSET_UINT; + ec->cmd = value[1]; + + if (ngx_array_init(&ec->names, cf->pool, 1, sizeof(ngx_str_t)) != NGX_OK) { + return NGX_CONF_ERROR; + } + + if (cf->args->nelts == 2) { + return NGX_CONF_OK; + } + + nargs = cf->args->nelts - 2; + if (ngx_array_init(&ec->args, cf->pool, nargs, sizeof(ngx_str_t)) != NGX_OK) + { + return NGX_CONF_ERROR; + } + + for (n = 2; n < cf->args->nelts; n++) { + + v = value[n]; + + if (eacf->options == 1) { + + if (v.len >= 5 && ngx_strncmp(v.data, "name=", 5) == 0) { + + s = ngx_array_push(&ec->names); + if (s == NULL) { + return NGX_CONF_ERROR; + } + + v.data += 5; + v.len -= 5; + + *s = v; + + continue; + } + } + + s = ngx_array_push(&ec->args); + if (s == NULL) { + return NGX_CONF_ERROR; + } + + *s = v; + } + + return NGX_CONF_OK; +} + +/* +static char * +ngx_rtmp_exec_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + char *rv; + ngx_str_t *value; + ngx_conf_t save; + ngx_array_t *confs; + ngx_rtmp_conf_ctx_t *ctx, *pctx; + ngx_rtmp_exec_conf_t *ec, *eec; + ngx_rtmp_exec_app_conf_t *eacf; + ngx_rtmp_exec_main_conf_t *emcf; + + value = cf->args->elts; + + eacf = ngx_rtmp_conf_get_module_app_conf(cf, ngx_rtmp_exec_module); + + emcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_exec_module); + + ctx = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_conf_ctx_t)); + if (ctx == NULL) { + return NGX_CONF_ERROR; + } + + pctx = cf->ctx; + + ctx->main_conf = pctx->main_conf; + ctx->srv_conf = pctx->srv_conf; + + ctx->app_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_rtmp_max_module); + if (ctx->app_conf == NULL) { + return NGX_CONF_ERROR; + } + + ec = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_exec_conf_t)); + if (ec == NULL) { + return NGX_CONF_ERROR; + } + + ec->id = value[1]; + ec->type = NGX_CONF_UNSET_UINT; + + ctx->app_conf[ngx_rtmp_exec_module.ctx_index] = ec; + + save = *cf; + + cf->ctx = ctx; + cf->cmd_type = NGX_RTMP_EXEC_CONF; + + rv = ngx_conf_parse(cf, NULL); + *cf= save; + + switch (ec->type) { + + case NGX_RTMP_EXEC_STATIC: + confs = &emcf->static_conf; + break; + + case NGX_CONF_UNSET_UINT: + return "unspecified exec type"; + + default: + confs = &eacf->conf[ec->type]; + } + + if (confs->nalloc == 0 && ngx_array_init(confs, cf->pool, 1, + sizeof(ngx_rtmp_exec_conf_t)) + != NGX_OK) + { + return NGX_CONF_ERROR; + } + + eec = ngx_array_push(confs); + if (eec == NULL) { + return NGX_CONF_ERROR; + } + + *eec = *ec; + + return rv; +} +*/ + +static char * +ngx_rtmp_exec_kill_signal(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_exec_main_conf_t *emcf = conf; + + ngx_str_t *value; + + value = cf->args->elts; + value++; + + emcf->kill_signal = ngx_atoi(value->data, value->len); + if (emcf->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) \ + { \ + emcf->kill_signal = SIG##name; \ + return NGX_CONF_OK; \ + } + + /* POSIX.1-1990 signals */ + +#if !(NGX_WIN32) + 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); +#endif + +#undef NGX_RMTP_EXEC_SIGNAL + + return "unknown signal"; +} + + +static ngx_int_t +ngx_rtmp_exec_postconfiguration(ngx_conf_t *cf) +{ +#if !(NGX_WIN32) + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_exec_publish; + + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_exec_play; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_exec_close_stream; + + next_record_done = ngx_rtmp_record_done; + ngx_rtmp_record_done = ngx_rtmp_exec_record_done; + +#endif /* NGX_WIN32 */ + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_flv_live_index_module.c b/ngx_http_flv_module/ngx_rtmp_flv_live_index_module.c new file mode 100644 index 0000000..30a12d8 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_flv_live_index_module.c @@ -0,0 +1,65 @@ + +/* + * Copyright (C) Winshining + */ + +#include +#include +#include "ngx_http_flv_live_module.h" + + +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_close_stream_pt next_close_stream; + + +static ngx_int_t ngx_rtmp_flv_live_index_postconfiguration(ngx_conf_t *cf); + + +static ngx_rtmp_module_t ngx_rtmp_flv_live_module_ctx = { + NULL, + ngx_rtmp_flv_live_index_postconfiguration, /* postconfiguration */ + NULL, + NULL, + NULL, + NULL, + NULL, + NULL +}; + + +static ngx_command_t ngx_rtmp_flv_live_index_commands[] = { + ngx_null_command +}; + + +ngx_module_t ngx_rtmp_flv_live_index_module = { + NGX_MODULE_V1, + &ngx_rtmp_flv_live_module_ctx, + ngx_rtmp_flv_live_index_commands, + NGX_RTMP_MODULE, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NGX_MODULE_V1_PADDING +}; + + +static ngx_int_t +ngx_rtmp_flv_live_index_postconfiguration(ngx_conf_t *cf) +{ + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_http_flv_live_play; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_http_flv_live_close_stream; + + http_flv_live_next_play = next_play; + http_flv_live_next_close_stream = next_close_stream; + + return NGX_OK; +} + diff --git a/ngx_http_flv_module/ngx_rtmp_flv_module.c b/ngx_http_flv_module/ngx_rtmp_flv_module.c new file mode 100644 index 0000000..8a735f9 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_flv_module.c @@ -0,0 +1,669 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_play_module.h" +#include "ngx_rtmp_codec_module.h" +#include "ngx_rtmp_streams.h" + + +static ngx_int_t ngx_rtmp_flv_postconfiguration(ngx_conf_t *cf); +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, + ngx_int_t aindex, ngx_int_t vindex); +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, + ngx_uint_t *ts); + + +typedef struct { + ngx_uint_t nelts; + ngx_uint_t offset; +} ngx_rtmp_flv_index_t; + + +typedef struct { + ngx_int_t offset; + ngx_int_t start_timestamp; + ngx_event_t write_evt; + uint32_t last_audio; + uint32_t last_video; + ngx_uint_t msg_mask; + uint32_t epoch; + + unsigned meta_read:1; + ngx_rtmp_flv_index_t filepositions; + ngx_rtmp_flv_index_t times; +} ngx_rtmp_flv_ctx_t; + + +#define NGX_RTMP_FLV_BUFFER (1024*1024) +#define NGX_RTMP_FLV_BUFLEN_ADDON 1000 +#define NGX_RTMP_FLV_TAG_HEADER 11 +#define NGX_RTMP_FLV_DATA_OFFSET 13 + + +static u_char ngx_rtmp_flv_buffer[ + NGX_RTMP_FLV_BUFFER]; +static u_char ngx_rtmp_flv_header[ + NGX_RTMP_FLV_TAG_HEADER]; + + +static ngx_rtmp_module_t ngx_rtmp_flv_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_flv_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + NULL, /* create app configuration */ + NULL /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_flv_module = { + NGX_MODULE_V1, + &ngx_rtmp_flv_module_ctx, /* module context */ + NULL, /* module directives */ + NGX_RTMP_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 +}; + + +static ngx_int_t +ngx_rtmp_flv_fill_index(ngx_rtmp_amf_ctx_t *ctx, ngx_rtmp_flv_index_t *idx) +{ + uint32_t nelts; + ngx_buf_t *b; + + /* we have AMF array pointed by context; + * need to extract its size (4 bytes) & + * save offset of actual array data */ + + b = ctx->link->buf; + + if (b->last - b->pos < (ngx_int_t) ctx->offset + 4) { + return NGX_ERROR; + } + + nelts = htonl(*(uint32_t *) (b->pos + ctx->offset)); + + idx->nelts = nelts; + idx->offset = ctx->offset + 4; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_flv_init_index(ngx_rtmp_session_t *s, ngx_chain_t *in) +{ + ngx_rtmp_flv_ctx_t *ctx; + + static ngx_rtmp_amf_ctx_t filepositions_ctx; + static ngx_rtmp_amf_ctx_t times_ctx; + + static ngx_rtmp_amf_elt_t in_keyframes[] = { + + { NGX_RTMP_AMF_ARRAY | NGX_RTMP_AMF_CONTEXT, + ngx_string("filepositions"), + &filepositions_ctx, 0 }, + + { NGX_RTMP_AMF_ARRAY | NGX_RTMP_AMF_CONTEXT, + ngx_string("times"), + ×_ctx, 0 } + }; + + static ngx_rtmp_amf_elt_t in_inf[] = { + + { NGX_RTMP_AMF_OBJECT, + ngx_string("keyframes"), + in_keyframes, sizeof(in_keyframes) } + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_inf, sizeof(in_inf) }, + }; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_flv_module); + + if (ctx == NULL || in == NULL) { + return NGX_OK; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: init index"); + + ngx_memzero(&filepositions_ctx, sizeof(filepositions_ctx)); + ngx_memzero(×_ctx, sizeof(times_ctx)); + + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: init index error"); + return NGX_OK; + } + + if (filepositions_ctx.link && ngx_rtmp_flv_fill_index(&filepositions_ctx, + &ctx->filepositions) + != NGX_OK) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: failed to init filepositions"); + return NGX_ERROR; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: filepositions nelts=%ui offset=%ui", + ctx->filepositions.nelts, ctx->filepositions.offset); + + if (times_ctx.link && ngx_rtmp_flv_fill_index(×_ctx, + &ctx->times) + != NGX_OK) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: failed to init times"); + return NGX_ERROR; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: times nelts=%ui offset=%ui", + ctx->times.nelts, ctx->times.offset); + + return NGX_OK; +} + + +static double +ngx_rtmp_flv_index_value(void *src) +{ + return *(double *) src; +} + + +static ngx_int_t +ngx_rtmp_flv_timestamp_to_offset(ngx_rtmp_session_t *s, ngx_file_t *f, + ngx_int_t timestamp) +{ + ngx_rtmp_flv_ctx_t *ctx; + ssize_t n, size; + ngx_uint_t offset, index, ret, nelts; + double v; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_flv_module); + + if (ctx == NULL) { + goto rewind; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: lookup index start timestamp=%i", + timestamp); + + if (ctx->meta_read == 0) { + ngx_rtmp_flv_read_meta(s, f); + ctx->meta_read = 1; + } + + if (timestamp <= 0 || ctx->filepositions.nelts == 0 + || ctx->times.nelts == 0) + { + goto rewind; + } + + /* read index table from file given offset */ + offset = NGX_RTMP_FLV_DATA_OFFSET + NGX_RTMP_FLV_TAG_HEADER + + ctx->times.offset; + + /* index should fit in the buffer */ + nelts = ngx_min(ctx->times.nelts, sizeof(ngx_rtmp_flv_buffer) / 9); + size = nelts * 9; + + n = ngx_read_file(f, ngx_rtmp_flv_buffer, size, offset); + + if (n != size) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: could not read times index"); + goto rewind; + } + + /*TODO: implement binary search */ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: lookup times nelts=%ui", nelts); + + for (index = 0; index < nelts - 1; ++index) { + v = ngx_rtmp_flv_index_value(ngx_rtmp_flv_buffer + + index * 9 + 1) * 1000; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: lookup times index=%ui value=%ui", + index, (ngx_uint_t) v); + + if (timestamp < v) { + break; + } + } + + if (index >= ctx->filepositions.nelts) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: index out of bounds: %ui>=%ui", + index, ctx->filepositions.nelts); + goto rewind; + } + + /* take value from filepositions */ + offset = NGX_RTMP_FLV_DATA_OFFSET + NGX_RTMP_FLV_TAG_HEADER + + ctx->filepositions.offset + index * 9; + + n = ngx_read_file(f, ngx_rtmp_flv_buffer, 8, offset + 1); + + if (n != 8) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: could not read filepositions index"); + goto rewind; + } + + ret = (ngx_uint_t) ngx_rtmp_flv_index_value(ngx_rtmp_flv_buffer); + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: lookup index timestamp=%i offset=%ui", + timestamp, ret); + + return ret; + +rewind: + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: lookup index timestamp=%i offset=begin", + timestamp); + + return NGX_RTMP_FLV_DATA_OFFSET; +} + + +static void +ngx_rtmp_flv_read_meta(ngx_rtmp_session_t *s, ngx_file_t *f) +{ + ngx_rtmp_flv_ctx_t *ctx; + ssize_t n; + ngx_rtmp_header_t h; + ngx_chain_t *out, in; + ngx_buf_t in_buf; + ngx_rtmp_core_srv_conf_t *cscf; + uint32_t size; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_flv_module); + + if (ctx == NULL) { + return; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: read meta"); + + /* read tag header */ + n = ngx_read_file(f, ngx_rtmp_flv_header, sizeof(ngx_rtmp_flv_header), + NGX_RTMP_FLV_DATA_OFFSET); + + if (n != sizeof(ngx_rtmp_flv_header)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: could not read metadata tag header"); + return; + } + + if (ngx_rtmp_flv_header[0] != NGX_RTMP_MSG_AMF_META) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: first tag is not metadata, giving up"); + return; + } + + ngx_memzero(&h, sizeof(h)); + + h.type = NGX_RTMP_MSG_AMF_META; + h.msid = NGX_RTMP_MSID; + h.csid = NGX_RTMP_CSID_AMF; + + size = ngx_rtmp_n3_to_h4(ngx_rtmp_flv_header + 1); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: metadata size=%D", size); + + if (size > sizeof(ngx_rtmp_flv_buffer)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: too big metadata"); + return; + } + + /* read metadata */ + n = ngx_read_file(f, ngx_rtmp_flv_buffer, size, + sizeof(ngx_rtmp_flv_header) + + NGX_RTMP_FLV_DATA_OFFSET); + + if (n != (ssize_t) size) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: could not read metadata"); + return; + } + + /* prepare input chain */ + ngx_memzero(&in, sizeof(in)); + ngx_memzero(&in_buf, sizeof(in_buf)); + + in.buf = &in_buf; + in_buf.pos = ngx_rtmp_flv_buffer; + in_buf.last = ngx_rtmp_flv_buffer + size; + + ngx_rtmp_flv_init_index(s, &in); + + /* output chain */ + out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); + + ngx_rtmp_prepare_message(s, &h, NULL, out); + ngx_rtmp_send_message(s, out, 0); + ngx_rtmp_free_shared_chain(cscf, out); +} + + +static ngx_int_t +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; + ngx_rtmp_header_t h, lh; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_chain_t *out, in; + ngx_buf_t in_buf; + ngx_int_t rc; + ssize_t n; + uint32_t buflen, end_timestamp, size; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_flv_module); + + if (ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->offset == -1) { + ctx->offset = ngx_rtmp_flv_timestamp_to_offset(s, f, + ctx->start_timestamp); + ctx->start_timestamp = -1; /* set later from actual timestamp */ + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: read tag at offset=%i", ctx->offset); + + /* read tag header */ + n = ngx_read_file(f, ngx_rtmp_flv_header, + sizeof(ngx_rtmp_flv_header), ctx->offset); + + if (n != sizeof(ngx_rtmp_flv_header)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: could not read flv tag header"); + return NGX_DONE; + } + + /* parse header fields */ + ngx_memzero(&h, sizeof(h)); + + h.msid = NGX_RTMP_MSID; + h.type = ngx_rtmp_flv_header[0]; + + size = ngx_rtmp_n3_to_h4(ngx_rtmp_flv_header + 1); + h.timestamp = ngx_rtmp_n3_to_h4(ngx_rtmp_flv_header + 4); + h.timestamp |= ((uint32_t) ngx_rtmp_flv_header[7] << 24); + + ctx->offset += (sizeof(ngx_rtmp_flv_header) + size + 4); + + last_timestamp = 0; + + switch (h.type) { + + case NGX_RTMP_MSG_AUDIO: + h.csid = NGX_RTMP_CSID_AUDIO; + last_timestamp = ctx->last_audio; + ctx->last_audio = h.timestamp; + break; + + case NGX_RTMP_MSG_VIDEO: + h.csid = NGX_RTMP_CSID_VIDEO; + last_timestamp = ctx->last_video; + ctx->last_video = h.timestamp; + break; + + default: + return NGX_OK; + } + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: read tag type=%i size=%uD timestamp=%uD " + "last_timestamp=%uD", + (ngx_int_t) h.type,size, h.timestamp, last_timestamp); + + lh = h; + lh.timestamp = last_timestamp; + + if (size > sizeof(ngx_rtmp_flv_buffer)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: too big message: %D>%uz", size, + sizeof(ngx_rtmp_flv_buffer)); + goto next; + } + + /* read tag body */ + n = ngx_read_file(f, ngx_rtmp_flv_buffer, size, + ctx->offset - size - 4); + + if (n != (ssize_t) size) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "flv: could not read flv tag"); + return NGX_ERROR; + } + + /* prepare input chain */ + ngx_memzero(&in, sizeof(in)); + ngx_memzero(&in_buf, sizeof(in_buf)); + + in.buf = &in_buf; + in_buf.pos = ngx_rtmp_flv_buffer; + in_buf.last = ngx_rtmp_flv_buffer + size; + + /* output chain */ + out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); + + ngx_rtmp_prepare_message(s, &h, + ctx->msg_mask & ((ngx_uint_t) 1 << h.type) ? + &lh : NULL, out); + rc = ngx_rtmp_send_message(s, out, 0); + ngx_rtmp_free_shared_chain(cscf, out); + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + if (rc != NGX_OK) { + return NGX_ERROR; + } + + ctx->msg_mask |= ((ngx_uint_t) 1 << h.type); + +next: + if (ctx->start_timestamp == -1) { + ctx->start_timestamp = h.timestamp; + ctx->epoch = ngx_current_msec; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: start_timestamp=%i", ctx->start_timestamp); + return NGX_OK; + } + + buflen = s->buflen + NGX_RTMP_FLV_BUFLEN_ADDON; + + end_timestamp = (ngx_current_msec - ctx->epoch) + + ctx->start_timestamp + buflen; + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: %s wait=%D timestamp=%D end_timestamp=%D bufen=%i", + h.timestamp > end_timestamp ? "schedule" : "advance", + h.timestamp > end_timestamp ? h.timestamp - end_timestamp : 0, + h.timestamp, end_timestamp, (ngx_int_t) buflen); + + s->current_time = h.timestamp; + + /* too much data sent; schedule timeout */ + if (h.timestamp > end_timestamp) { + return h.timestamp - end_timestamp; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_flv_init(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_int_t aindex, + ngx_int_t vindex) +{ + ngx_rtmp_flv_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_flv_module); + + if (ctx == NULL) { + ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_flv_ctx_t)); + + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_flv_module); + } + + ngx_memzero(ctx, sizeof(*ctx)); + + return NGX_OK; +} + + +static ngx_int_t +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; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_flv_module); + + if (ctx == NULL) { + return NGX_OK; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "flv: seek timestamp=%ui", timestamp); + + ctx->start_timestamp = timestamp; + ctx->epoch = ngx_current_msec; + ctx->offset = -1; + ctx->msg_mask = 0; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_flv_stop(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: stop"); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_flv_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_play_main_conf_t *pmcf; + ngx_rtmp_play_fmt_t **pfmt, *fmt; + + pmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_play_module); + + pfmt = ngx_array_push(&pmcf->fmts); + + if (pfmt == NULL) { + return NGX_ERROR; + } + + fmt = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_play_fmt_t)); + + if (fmt == NULL) { + return NGX_ERROR; + } + + *pfmt = fmt; + + ngx_str_set(&fmt->name, "flv-format"); + + ngx_str_null(&fmt->pfx); /* default fmt */ + ngx_str_set(&fmt->sfx, ".flv"); + + 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; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_gop_cache_module.c b/ngx_http_flv_module/ngx_rtmp_gop_cache_module.c new file mode 100644 index 0000000..2ea1786 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_gop_cache_module.c @@ -0,0 +1,1016 @@ + +/* + * Copyright (C) Gnolizuh + * Copyright (C) Winshining + * Copyright (C) HeyJupiter + */ + + +#include +#include +#include +#include "ngx_http_flv_live_module.h" +#include "ngx_rtmp_gop_cache_module.h" + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_close_stream_pt next_close_stream; + + +static ngx_rtmp_gop_frame_t *ngx_rtmp_gop_cache_alloc_frame( + ngx_rtmp_session_t *s); +static ngx_rtmp_gop_frame_t *ngx_rtmp_gop_cache_free_frame( + ngx_rtmp_session_t *s, ngx_rtmp_gop_frame_t *frame); +static ngx_int_t ngx_rtmp_gop_cache_link_frame(ngx_rtmp_session_t *s, + ngx_rtmp_gop_frame_t *frame); +static ngx_int_t ngx_rtmp_gop_cache_alloc_cache(ngx_rtmp_session_t *s); +static ngx_rtmp_gop_cache_t *ngx_rtmp_gop_cache_free_cache( + ngx_rtmp_session_t *s, ngx_rtmp_gop_cache_t *cache); +static void ngx_rtmp_gop_cache_cleanup(ngx_rtmp_session_t *s); +static void ngx_rtmp_gop_cache_update(ngx_rtmp_session_t *s); +static void ngx_rtmp_gop_cache_frame(ngx_rtmp_session_t *s, ngx_uint_t prio, + ngx_rtmp_header_t *ch, ngx_chain_t *frame); +static void ngx_rtmp_gop_cache_send(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_gop_cache_av(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); +static ngx_int_t ngx_rtmp_gop_cache_publish(ngx_rtmp_session_t *s, + ngx_rtmp_publish_t *v); +static ngx_int_t ngx_rtmp_gop_cache_play(ngx_rtmp_session_t *s, + ngx_rtmp_play_t *v); +static ngx_int_t ngx_rtmp_gop_cache_close_stream(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v); + + +static ngx_int_t ngx_rtmp_gop_cache_postconfiguration(ngx_conf_t *cf); +static void *ngx_rtmp_gop_cache_create_app_conf(ngx_conf_t *cf); +static char *ngx_rtmp_gop_cache_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); + + +extern ngx_rtmp_live_proc_handler_t *ngx_rtmp_live_proc_handlers + [NGX_RTMP_PROTOCOL_HTTP + 1]; +extern ngx_module_t ngx_http_flv_live_module; + + +static ngx_command_t ngx_rtmp_gop_cache_commands[] = { + { ngx_string("gop_cache"), + NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_gop_cache_app_conf_t, gop_cache), + NULL }, + + { ngx_string("gop_max_frame_count"), + NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_gop_cache_app_conf_t, gop_max_frame_count), + NULL }, + + { ngx_string("gop_max_video_count"), + NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_gop_cache_app_conf_t, gop_max_video_count), + NULL }, + + { ngx_string("gop_max_audio_count"), + NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_gop_cache_app_conf_t, gop_max_audio_count), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_gop_cache_module_ctx = { + NULL, + ngx_rtmp_gop_cache_postconfiguration, /* postconfiguration */ + NULL, + NULL, + NULL, + NULL, + ngx_rtmp_gop_cache_create_app_conf, /* create application configuration */ + ngx_rtmp_gop_cache_merge_app_conf /* merge application configuration */ +}; + + +ngx_module_t ngx_rtmp_gop_cache_module = { + NGX_MODULE_V1, + &ngx_rtmp_gop_cache_module_ctx, + ngx_rtmp_gop_cache_commands, + NGX_RTMP_MODULE, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NGX_MODULE_V1_PADDING +}; + + +static void * +ngx_rtmp_gop_cache_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_gop_cache_app_conf_t *gacf; + + gacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_gop_cache_app_conf_t)); + if (gacf == NULL) { + return NULL; + } + + gacf->gop_cache = NGX_CONF_UNSET; + gacf->gop_cache_count = NGX_CONF_UNSET_SIZE; + gacf->gop_max_frame_count = NGX_CONF_UNSET_SIZE; + gacf->gop_max_audio_count = NGX_CONF_UNSET_SIZE; + gacf->gop_max_video_count = NGX_CONF_UNSET_SIZE; + + return (void *) gacf; +} + + +static char * +ngx_rtmp_gop_cache_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_gop_cache_app_conf_t *prev = parent; + ngx_rtmp_gop_cache_app_conf_t *conf = child; + + ngx_conf_merge_value(conf->gop_cache, prev->gop_cache, 0); + ngx_conf_merge_size_value(conf->gop_cache_count, prev->gop_cache_count, 2); + ngx_conf_merge_size_value(conf->gop_max_frame_count, + prev->gop_max_frame_count, 4096); + ngx_conf_merge_size_value(conf->gop_max_audio_count, + prev->gop_max_audio_count, 2048); + ngx_conf_merge_size_value(conf->gop_max_video_count, + prev->gop_max_video_count, 2048); + + return NGX_CONF_OK; +} + + +static ngx_rtmp_gop_frame_t * +ngx_rtmp_gop_cache_alloc_frame(ngx_rtmp_session_t *s) +{ + ngx_rtmp_gop_cache_ctx_t *ctx; + ngx_rtmp_gop_frame_t *frame; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + return NULL; + } + + if (ctx->free_frame) { + frame = ctx->free_frame; + ctx->free_frame = frame->next; + + return frame; + } + + frame = ngx_pcalloc(ctx->pool, sizeof(ngx_rtmp_gop_frame_t)); + + return frame; +} + + +static ngx_rtmp_gop_frame_t * +ngx_rtmp_gop_cache_free_frame(ngx_rtmp_session_t *s, + ngx_rtmp_gop_frame_t *frame) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_gop_cache_ctx_t *ctx; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return NULL; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + return NULL; + } + + if (frame->frame) { + ngx_rtmp_free_shared_chain(cscf, frame->frame); + frame->frame = NULL; + } + + if (frame->h.type == NGX_RTMP_MSG_VIDEO) { + ctx->video_frame_in_all--; + } else if (frame->h.type == NGX_RTMP_MSG_AUDIO) { + ctx->audio_frame_in_all--; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop free frame: type='%s' video_frame_in_cache=%uD " + "audio_frame_in_cache=%uD", + frame->h.type == NGX_RTMP_MSG_VIDEO ? "video" : "audio", + ctx->video_frame_in_all, ctx->audio_frame_in_all); + + return frame->next; +} + + +static ngx_int_t +ngx_rtmp_gop_cache_link_frame(ngx_rtmp_session_t *s, + ngx_rtmp_gop_frame_t *frame) +{ + ngx_rtmp_gop_cache_ctx_t *ctx; + ngx_rtmp_gop_cache_t *cache; + ngx_rtmp_gop_frame_t **iter; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + return NGX_ERROR; + } + + cache = ctx->cache_tail; + if (cache == NULL) { + return NGX_ERROR; + } + + if(cache->frame_head == NULL) { + cache->frame_head = cache->frame_tail = frame; + } else { + iter = &cache->frame_tail->next; + *iter = frame; + cache->frame_tail = frame; + } + + if (frame->h.type == NGX_RTMP_MSG_VIDEO) { + ctx->video_frame_in_all++; + cache->video_frame_in_this++; + } else if(frame->h.type == NGX_RTMP_MSG_AUDIO) { + ctx->audio_frame_in_all++; + cache->audio_frame_in_this++; + } + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop link frame: type='%s' " + "ctx->video_frame_in_all=%uD " + "ctx->audio_frame_in_all=%uD " + "cache->video_frame_in_this=%uD " + "cache->audio_frame_in_this=%uD", + frame->h.type == NGX_RTMP_MSG_VIDEO ? "video" : "audio", + ctx->video_frame_in_all, ctx->audio_frame_in_all, + cache->video_frame_in_this, cache->audio_frame_in_this); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_gop_cache_alloc_cache(ngx_rtmp_session_t *s) +{ + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_gop_cache_ctx_t *ctx; + ngx_rtmp_gop_cache_t *cache, **iter; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + return NGX_ERROR; + } + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (codec_ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->free_cache) { + cache = ctx->free_cache; + ctx->free_cache = cache->next; + + ngx_memzero(cache, sizeof(ngx_rtmp_gop_cache_t)); + } else { + cache = ngx_pcalloc(ctx->pool, sizeof(ngx_rtmp_gop_cache_t)); + if (cache == NULL) { + return NGX_ERROR; + } + } + + if (ctx->cache_head == NULL) { + ctx->cache_tail = ctx->cache_head = cache; + } else { + iter = &ctx->cache_tail->next; + *iter = cache; + ctx->cache_tail = cache; + } + + ctx->gop_cache_count++; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop alloc cache: gop_cache_count=%uD", ctx->gop_cache_count); + + return NGX_OK; +} + + +static ngx_rtmp_gop_cache_t * +ngx_rtmp_gop_cache_free_cache(ngx_rtmp_session_t *s, + ngx_rtmp_gop_cache_t *cache) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_gop_cache_ctx_t *ctx; + ngx_rtmp_gop_frame_t *frame; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return NULL; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + return NULL; + } + + for (frame = cache->frame_head; frame; frame = frame->next) { + ngx_rtmp_gop_cache_free_frame(s, frame); + } + + if (cache->video_seq_header) { + ngx_rtmp_free_shared_chain(cscf, cache->video_seq_header); + cache->video_seq_header = NULL; + } + + if (cache->audio_seq_header) { + ngx_rtmp_free_shared_chain(cscf, cache->audio_seq_header); + cache->audio_seq_header = NULL; + } + + if (cache->meta) { + ngx_rtmp_free_shared_chain(cscf, cache->meta); + cache->meta_version = 0; + cache->meta = NULL; + } + + cache->video_frame_in_this = 0; + cache->audio_frame_in_this = 0; + + // recycle mem of gop frame + cache->frame_tail->next = ctx->free_frame; + ctx->free_frame = cache->frame_head; + + ctx->gop_cache_count--; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop free cache: gop_cache_count=%uD", ctx->gop_cache_count); + + return cache->next; +} + + +static void +ngx_rtmp_gop_cache_cleanup(ngx_rtmp_session_t *s) +{ + ngx_rtmp_gop_cache_ctx_t *ctx; + ngx_rtmp_gop_cache_t *cache; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + return; + } + + for (cache = ctx->cache_head; cache; cache = cache->next) { + ngx_rtmp_gop_cache_free_cache(s, cache); + } + + if (ctx->cache_head) { + ctx->cache_head->next = ctx->free_cache; + ctx->free_cache = ctx->cache_head; + ctx->cache_head = NULL; + } + + ctx->cache_tail = NULL; + ctx->gop_cache_count = 0; + ctx->video_frame_in_all = 0; + ctx->audio_frame_in_all = 0; +} + + +static void +ngx_rtmp_gop_cache_update(ngx_rtmp_session_t *s) +{ + ngx_rtmp_gop_cache_app_conf_t *gacf; + ngx_rtmp_gop_cache_ctx_t *ctx; + ngx_rtmp_gop_cache_t *next; + + gacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_gop_cache_module); + if (gacf == NULL) { + return; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + return; + } + + while (ctx->gop_cache_count > gacf->gop_cache_count) { + if (ctx->cache_head) { + /* remove the 1st gop */ + next = ngx_rtmp_gop_cache_free_cache(s, ctx->cache_head); + + ctx->cache_head->next = ctx->free_cache; + ctx->free_cache = ctx->cache_head; + + ctx->cache_head = next; + } + } +} + + +static void +ngx_rtmp_gop_cache_frame(ngx_rtmp_session_t *s, ngx_uint_t prio, + ngx_rtmp_header_t *ch, ngx_chain_t *frame) +{ + ngx_rtmp_gop_cache_ctx_t *ctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_gop_cache_app_conf_t *gacf; + ngx_rtmp_gop_frame_t *gf; + + gacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_gop_cache_module); + if (gacf == NULL || !gacf->gop_cache) { + return; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + return; + } + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (codec_ctx == NULL) { + return; + } + + if (ch->type == NGX_RTMP_MSG_VIDEO) { + // drop non-IDR + if (prio != NGX_RTMP_VIDEO_KEY_FRAME && ctx->cache_head == NULL) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache: drop video non-keyframe timestamp=%uD", + ch->timestamp); + + return; + } + } + + // audio only + if (ctx->video_frame_in_all == 0 && ch->type == NGX_RTMP_MSG_AUDIO) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache: drop audio frame timestamp=%uD", + ch->timestamp); + + return; + } + + if (ch->type == NGX_RTMP_MSG_VIDEO && prio == NGX_RTMP_VIDEO_KEY_FRAME) { + if (ngx_rtmp_gop_cache_alloc_cache(s) != NGX_OK) { + return; + } + } + + // save video seq header. + if (codec_ctx->avc_header && + (ctx->cache_tail && ctx->cache_tail->video_seq_header == NULL)) + { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache: add video seq header in new cache"); + ctx->cache_tail->video_seq_header = codec_ctx->avc_header; + ngx_rtmp_acquire_shared_chain(ctx->cache_tail->video_seq_header); + } + + // save audio seq header. + if (codec_ctx->aac_header && + (ctx->cache_tail && ctx->cache_tail->audio_seq_header == NULL)) + { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache: add audio seq header in new cache"); + ctx->cache_tail->audio_seq_header = codec_ctx->aac_header; + ngx_rtmp_acquire_shared_chain(ctx->cache_tail->audio_seq_header); + } + + // save metadata. + if (codec_ctx->meta && + (ctx->cache_tail && ctx->cache_tail->meta == NULL)) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache: add meta in new cache, version=%ui", + codec_ctx->meta_version); + ctx->cache_tail->meta_version = codec_ctx->meta_version; + ctx->cache_tail->meta = codec_ctx->meta; + ngx_rtmp_acquire_shared_chain(ctx->cache_tail->meta); + } + + gf = ngx_rtmp_gop_cache_alloc_frame(s); + if (gf == NULL) { + return; + } + + gf->h = *ch; + gf->prio = prio; + gf->next = NULL; + gf->frame = ngx_rtmp_append_shared_bufs(cscf, NULL, frame); + + if (ngx_rtmp_gop_cache_link_frame(s, gf) != NGX_OK) { + ngx_rtmp_free_shared_chain(cscf, gf->frame); + return; + } + + if (ctx->video_frame_in_all > gacf->gop_max_video_count || + ctx->audio_frame_in_all > gacf->gop_max_audio_count || + (ctx->video_frame_in_all + ctx->audio_frame_in_all) + > gacf->gop_max_frame_count) + { + ngx_log_error(NGX_LOG_WARN, s->connection->log, 0, + "gop cache: video_frame_in_cache=%uD " + "audio_frame_in_cache=%uD max_video_count=%uD " + "max_audio_count=%uD gop_max_frame_count=%uD", + ctx->video_frame_in_all, ctx->audio_frame_in_all, + gacf->gop_max_video_count, gacf->gop_max_audio_count, + gacf->gop_max_frame_count); + + ngx_rtmp_gop_cache_cleanup(s); + return; + } + + ngx_rtmp_gop_cache_update(s); + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache: cache packet type='%s' timestamp=%uD", + gf->h.type == NGX_RTMP_MSG_AUDIO ? "audio" : "video", + gf->h.timestamp); +} + + +static void +ngx_rtmp_gop_cache_send(ngx_rtmp_session_t *s) +{ + ngx_rtmp_session_t *rs; + ngx_chain_t *pkt, *apkt, *acopkt, *meta; + ngx_chain_t *header, *coheader; + ngx_rtmp_live_ctx_t *ctx, *pub_ctx; + ngx_http_flv_live_ctx_t *hflctx; + ngx_rtmp_gop_cache_ctx_t *gctx; + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_gop_cache_t *cache; + ngx_rtmp_gop_frame_t *gf; + ngx_rtmp_header_t ch, lh, clh; + ngx_uint_t meta_version; + uint32_t delta; + ngx_int_t csidx; + ngx_rtmp_live_chunk_stream_t *cs; + ngx_rtmp_live_proc_handler_t *handler; + ngx_http_request_t *r; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_flag_t mandatory, error; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + return; + } + + /* pub_ctx saved the publisher info */ + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL || ctx->stream == NULL || + ctx->stream->pub_ctx == NULL || !ctx->stream->publishing) { + return; + } + + pkt = NULL; + apkt = NULL; + acopkt = NULL; + header = NULL; + coheader = NULL; + meta_version = 0; + + pub_ctx = ctx->stream->pub_ctx; + rs = pub_ctx->session; + s->publisher = rs; + handler = ngx_rtmp_live_proc_handlers[ctx->protocol]; + + if (rs == NULL) { + return; + } + + gctx = ngx_rtmp_get_module_ctx(rs, ngx_rtmp_gop_cache_module); + if (gctx == NULL) { + return; + } + + codec_ctx = ngx_rtmp_get_module_ctx(rs, ngx_rtmp_codec_module); + if (codec_ctx == NULL) { + return; + } + + for (cache = gctx->cache_head; cache; cache = cache->next) { + if (s->connection == NULL || s->connection->destroyed) { + return; + } + + if (ctx->protocol == NGX_RTMP_PROTOCOL_HTTP) { + r = s->data; + if (r == NULL) { + return; + } + + hflctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module); + if (!hflctx->header_sent) { + hflctx->header_sent = 1; + ngx_http_flv_live_send_header(s); + } + } + + meta = NULL; + + if (cache->meta && meta_version != cache->meta_version) { + meta = handler->meta_message_pt(s, cache->meta); + if (meta == NULL) { + ngx_rtmp_finalize_session(s); + return; + } + + meta_version = cache->meta_version; + } + + /* send metadata */ + if (meta && meta_version != ctx->meta_version) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache send: meta, version=%ui", meta_version); + + if (handler->send_message_pt(s, meta, 0) == NGX_ERROR) { + ngx_rtmp_finalize_session(s); + return; + } + + ctx->meta_version = meta_version; + handler->free_message_pt(s, meta); + } + + for (gf = cache->frame_head; gf; gf = gf->next) { + if (s->connection == NULL || s->connection->destroyed) { + return; + } + + csidx = !(lacf->interleave || gf->h.type == NGX_RTMP_MSG_VIDEO); + + cs = &ctx->cs[csidx]; + + lh = ch = gf->h; + + if (cs->active) { + lh.timestamp = cs->timestamp; + } + + clh = lh; + clh.type = (gf->h.type == NGX_RTMP_MSG_AUDIO ? NGX_RTMP_MSG_VIDEO : + NGX_RTMP_MSG_AUDIO); + + delta = ch.timestamp - lh.timestamp; + mandatory = 0; + error = 0; + + if (ch.type == NGX_RTMP_MSG_AUDIO) { + if (codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC && + ngx_rtmp_is_codec_header(gf->frame)) + { + mandatory = 1; + } + } else { + if (codec_ctx->video_codec_id == NGX_RTMP_VIDEO_H264 && + ngx_rtmp_is_codec_header(gf->frame)) + { + mandatory = 1; + } + } + + if (!cs->active) { + if (mandatory) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache: skipping header"); + + continue; + } + + switch (gf->h.type) { + case NGX_RTMP_MSG_VIDEO: + header = cache->video_seq_header; + if (lacf->interleave) { + coheader = cache->audio_seq_header; + } + break; + default: + header = cache->audio_seq_header; + if (lacf->interleave) { + coheader = cache->video_seq_header; + } + } + + if (header) { + apkt = handler->append_message_pt(s, &lh, NULL, header); + if (apkt == NULL) { + error = 1; + goto next; + } + } + + if (apkt && handler->send_message_pt(s, apkt, 0) != NGX_OK) { + goto next; + } + + if (coheader) { + acopkt = handler->append_message_pt(s, &clh, NULL, + coheader); + if (acopkt == NULL) { + error = 1; + goto next; + } + } + + if (acopkt && handler->send_message_pt(s, acopkt, 0) != NGX_OK) { + goto next; + } + + cs->timestamp = lh.timestamp; + cs->active = 1; + s->current_time = cs->timestamp; + } + + pkt = handler->append_message_pt(s, &ch, &lh, gf->frame); + if (pkt == NULL) { + error = 1; + goto next; + } + + if (handler->send_message_pt(s, pkt, gf->prio) != NGX_OK) { + ++pub_ctx->ndropped; + + cs->dropped += delta; + + if (mandatory) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache send: mandatory packet failed"); + + error = 1; + } + + goto next; + } + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache send: tag type='%s' prio=%d ctimestamp=%uD " + "ltimestamp=%uD", + gf->h.type == NGX_RTMP_MSG_AUDIO ? "audio" : "video", + gf->prio, ch.timestamp, lh.timestamp); + + cs->timestamp += delta; + s->current_time = cs->timestamp; + + next: + + if (pkt) { + handler->free_message_pt(s, pkt); + pkt = NULL; + } + + if (apkt) { + handler->free_message_pt(s, apkt); + apkt = NULL; + } + + if (acopkt) { + handler->free_message_pt(s, acopkt); + acopkt = NULL; + } + + if (error) { + ngx_rtmp_finalize_session(s); + return; + } + } + } +} + + +static ngx_int_t +ngx_rtmp_gop_cache_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_live_ctx_t *ctx; + ngx_rtmp_gop_cache_app_conf_t *gacf; + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_live_chunk_stream_t *cs; + ngx_rtmp_header_t ch; + ngx_uint_t prio; + ngx_uint_t csidx; + + gacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_gop_cache_module); + if (gacf == NULL || !gacf->gop_cache) { + return NGX_OK; + } + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + return NGX_OK; + } + + if (in == NULL || in->buf == NULL) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL || ctx->stream == NULL) { + return NGX_OK; + } + + if (!ctx->publishing) { + return NGX_OK; + } + + prio = (h->type == NGX_RTMP_MSG_VIDEO ? + ngx_rtmp_get_video_frame_type(in) : 0); + + csidx = !(lacf->interleave || h->type == NGX_RTMP_MSG_VIDEO); + + cs = &ctx->cs[csidx]; + + ngx_memzero(&ch, sizeof(ch)); + + ch.timestamp = h->timestamp; + ch.msid = NGX_RTMP_MSID; + ch.csid = cs->csid; + ch.type = h->type; + + ngx_rtmp_gop_cache_frame(s, prio, &ch, in); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_gop_cache_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_live_ctx_t *lctx; + ngx_rtmp_gop_cache_app_conf_t *gacf; + ngx_rtmp_gop_cache_ctx_t *ctx; + + gacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_gop_cache_module); + if (gacf == NULL || !gacf->gop_cache) { + goto next; + } + + lctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (lctx == NULL || !lctx->publishing) { + goto next; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache publish: name='%s' type='%s'", + v->name, v->type); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, + sizeof(ngx_rtmp_gop_cache_ctx_t)); + if (ctx == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "gop cache publish: failed to allocate for ctx"); + + return NGX_ERROR; + } + + ctx->pool = ngx_create_pool(NGX_GOP_CACHE_POOL_CREATE_SIZE, + s->connection->log); + if (ctx->pool == NULL) { + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_gop_cache_module); + } + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_gop_cache_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_gop_cache_app_conf_t *gacf; +#ifdef NGX_DEBUG + ngx_msec_t start, end; +#endif + + gacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_gop_cache_module); + if (gacf == NULL || !gacf->gop_cache) { + goto next; + } + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache play: name='%s' start=%i duration=%i reset=%d", + v->name, (ngx_int_t) v->start, + (ngx_int_t) v->duration, (ngx_uint_t) v->reset); + +#ifdef NGX_DEBUG + start = ngx_current_msec; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache send: start_time=%uD", start); +#endif + + ngx_rtmp_gop_cache_send(s); + +#ifdef NGX_DEBUG + end = ngx_current_msec; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache send: end_time=%uD", end); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "gop cache send: delta_time=%uD", end - start); +#endif + +next: + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_gop_cache_close_stream(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_live_ctx_t *ctx; + ngx_rtmp_gop_cache_ctx_t *gctx; + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_gop_cache_app_conf_t *gacf; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL) { + goto next; + } + + if (!ctx->publishing) { + goto next; + } + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL || !lacf->live) { + goto next; + } + + gacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_gop_cache_module); + if (gacf == NULL || !gacf->gop_cache) { + goto next; + } + + ngx_rtmp_gop_cache_cleanup(s); + + gctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_gop_cache_module); + if (gctx == NULL) { + goto next; + } + + if (gctx->pool) { + ngx_destroy_pool(gctx->pool); + gctx->pool = NULL; + } + +next: + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_gop_cache_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + /* register raw event handlers */ + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); + *h = ngx_rtmp_gop_cache_av; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); + *h = ngx_rtmp_gop_cache_av; + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_gop_cache_publish; + + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_gop_cache_play; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_gop_cache_close_stream; + + return NGX_OK; +} + diff --git a/ngx_http_flv_module/ngx_rtmp_gop_cache_module.h b/ngx_http_flv_module/ngx_rtmp_gop_cache_module.h new file mode 100644 index 0000000..3b3ccec --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_gop_cache_module.h @@ -0,0 +1,65 @@ + +/* + * Copyright (C) Gnolizuh + * Copyright (C) Winshining + */ + +#ifndef _NGX_RTMP_GOP_CACHE_H_INCLUDE_ +#define _NGX_RTMP_GOP_CACHE_H_INCLUDE_ + + +#define NGX_GOP_CACHE_POOL_CREATE_SIZE 4096 + + +typedef struct ngx_rtmp_gop_frame_s ngx_rtmp_gop_frame_t; +typedef struct ngx_rtmp_gop_cache_s ngx_rtmp_gop_cache_t; + + +struct ngx_rtmp_gop_frame_s { + ngx_rtmp_header_t h; + ngx_uint_t prio; + ngx_chain_t *frame; + ngx_rtmp_gop_frame_t *next; +}; + + +struct ngx_rtmp_gop_cache_s { + ngx_rtmp_gop_frame_t *frame_head; + ngx_rtmp_gop_frame_t *frame_tail; + ngx_rtmp_gop_cache_t *next; + + ngx_chain_t *video_seq_header; + ngx_chain_t *audio_seq_header; + ngx_chain_t *meta; + + ngx_uint_t meta_version; + + ngx_int_t video_frame_in_this; + ngx_int_t audio_frame_in_this; +}; + + +typedef struct ngx_rtmp_gop_cache_app_conf_s { + ngx_flag_t gop_cache; + size_t gop_cache_count; + size_t gop_max_frame_count; + size_t gop_max_video_count; + size_t gop_max_audio_count; +} ngx_rtmp_gop_cache_app_conf_t; + + +typedef struct ngx_rtmp_gop_cache_ctx_s { + ngx_pool_t *pool; + ngx_rtmp_gop_cache_t *cache_head; + ngx_rtmp_gop_cache_t *cache_tail; + ngx_rtmp_gop_cache_t *free_cache; + ngx_rtmp_gop_frame_t *free_frame; + + size_t gop_cache_count; + size_t video_frame_in_all; + size_t audio_frame_in_all; +} ngx_rtmp_gop_cache_ctx_t; + + +#endif + diff --git a/ngx_http_flv_module/ngx_rtmp_handler.c b/ngx_http_flv_module/ngx_rtmp_handler.c new file mode 100644 index 0000000..fd65e4b --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_handler.c @@ -0,0 +1,940 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_amf.h" + + +static void ngx_rtmp_recv(ngx_event_t *rev); +static void ngx_rtmp_send(ngx_event_t *rev); +static void ngx_rtmp_ping(ngx_event_t *rev); + + +ngx_uint_t ngx_rtmp_naccepted; + + +ngx_rtmp_bandwidth_t ngx_rtmp_bw_out; +ngx_rtmp_bandwidth_t ngx_rtmp_bw_in; + + +#ifdef NGX_DEBUG +char* +ngx_rtmp_message_type(uint8_t type) +{ + static char* types[] = { + "?", + "chunk_size", + "abort", + "ack", + "user", + "ack_size", + "bandwidth", + "edge", + "audio", + "video", + "?", + "?", + "?", + "?", + "?", + "amf3_meta", + "amf3_shared", + "amf3_cmd", + "amf_meta", + "amf_shared", + "amf_cmd", + "?", + "aggregate" + }; + + return type < sizeof(types) / sizeof(types[0]) + ? types[type] + : "?"; +} + + +char* +ngx_rtmp_user_message_type(uint16_t evt) +{ + static char* evts[] = { + "stream_begin", + "stream_eof", + "stream dry", + "set_buflen", + "recorded", + "", + "ping_request", + "ping_response", + }; + + return evt < sizeof(evts) / sizeof(evts[0]) + ? evts[evt] + : "?"; +} +#endif + + +void +ngx_rtmp_cycle(ngx_rtmp_session_t *s) +{ + ngx_connection_t *c; + + c = s->connection; + + c->read->handler = ngx_rtmp_recv; + c->write->handler = ngx_rtmp_send; + + s->ping_evt.data = c; + s->ping_evt.log = c->log; + s->ping_evt.handler = ngx_rtmp_ping; + ngx_rtmp_reset_ping(s); + + ngx_rtmp_recv(c->read); +} + + +ngx_chain_t * +ngx_rtmp_alloc_in_buf(ngx_rtmp_session_t *s) +{ + ngx_chain_t *cl; + ngx_buf_t *b; + size_t size; + + if ((cl = ngx_alloc_chain_link(s->in_pool)) == NULL + || (cl->buf = ngx_calloc_buf(s->in_pool)) == NULL) + { + return NULL; + } + + cl->next = NULL; + b = cl->buf; + size = s->in_chunk_size + NGX_RTMP_MAX_CHUNK_HEADER; + + b->start = b->last = b->pos = ngx_palloc(s->in_pool, size); + if (b->start == NULL) { + return NULL; + } + b->end = b->start + size; + + return cl; +} + + +void +ngx_rtmp_reset_ping(ngx_rtmp_session_t *s) +{ + ngx_rtmp_core_srv_conf_t *cscf; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf->ping == 0) { + return; + } + + s->ping_active = 0; + s->ping_reset = 0; + ngx_add_timer(&s->ping_evt, cscf->ping); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "ping: wait %Mms", cscf->ping); +} + + +static void +ngx_rtmp_ping(ngx_event_t *pev) +{ + ngx_connection_t *c; + ngx_rtmp_session_t *s; + ngx_rtmp_core_srv_conf_t *cscf; + + c = pev->data; + s = c->data; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + /* i/o event has happened; no need to ping */ + if (s->ping_reset) { + ngx_rtmp_reset_ping(s); + return; + } + + if (s->ping_active) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "ping: unresponded"); + ngx_rtmp_finalize_session(s); + 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); + + if (ngx_rtmp_send_ping_request(s, (uint32_t)ngx_current_msec) != NGX_OK) { + ngx_rtmp_finalize_session(s); + return; + } + + s->ping_active = 1; + ngx_add_timer(pev, cscf->ping_timeout); +} + + +static void +ngx_rtmp_recv(ngx_event_t *rev) +{ + ngx_int_t n; + ngx_connection_t *c; + ngx_rtmp_session_t *s; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_header_t *h; + ngx_rtmp_stream_t *st, *st0; + ngx_chain_t *in, *head; + ngx_buf_t *b; + u_char *p, *old_pos; + size_t size, fsize, old_size; + uint8_t fmt, ext; + uint32_t csid, timestamp; + + c = rev->data; + s = c->data; + b = NULL; + old_pos = NULL; + old_size = 0; + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + if (c->destroyed) { + return; + } + + for( ;; ) { + + st = &s->in_streams[s->in_csid]; + + /* allocate new buffer */ + if (st->in == NULL) { + st->in = ngx_rtmp_alloc_in_buf(s); + if (st->in == NULL) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "in buf alloc failed"); + ngx_rtmp_finalize_session(s); + return; + } + } + + h = &st->hdr; + in = st->in; + b = in->buf; + + if (old_size) { + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, c->log, 0, + "reusing formerly read data: %d", old_size); + + b->pos = b->start; + + size = ngx_min((size_t) (b->end - b->start), old_size); + b->last = ngx_movemem(b->pos, old_pos, size); + + if (s->in_chunk_size_changing) { + ngx_rtmp_finalize_set_chunk_size(s); + } + + } else { + + if (old_pos) { + b->pos = b->last = b->start; + } + + n = c->recv(c, b->last, b->end - b->last); + + if (n == NGX_ERROR || n == 0) { + ngx_rtmp_finalize_session(s); + return; + } + + if (n == NGX_AGAIN) { + if (ngx_handle_read_event(c->read, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + return; + } + + s->ping_reset = 1; + ngx_rtmp_update_bandwidth(&ngx_rtmp_bw_in, n); + b->last += n; + s->in_bytes += n; + + if (s->in_bytes >= 0xf0000000) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, c->log, 0, + "resetting byte counter"); + s->in_bytes = 0; + s->in_last_ack = 0; + } + + if (s->ack_size && s->in_bytes - s->in_last_ack >= s->ack_size) { + + s->in_last_ack = s->in_bytes; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, c->log, 0, + "sending RTMP ACK(%uD)", s->in_bytes); + + if (ngx_rtmp_send_ack(s, s->in_bytes)) { + ngx_rtmp_finalize_session(s); + return; + } + } + } + + old_pos = NULL; + old_size = 0; + + /* parse headers */ + if (b->pos == b->start) { + p = b->pos; + + /* chunk basic header */ + fmt = (*p >> 6) & 0x03; + csid = *p++ & 0x3f; + + if (csid == 0) { + if (b->last - p < 1) + continue; + csid = 64; + csid += *p++; + + } else if (csid == 1) { + if (b->last - p < 2) + continue; + csid = 64; + csid += *p++; + csid += ((uint32_t) *p++ << 8); + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, c->log, 0, + "RTMP bheader fmt=%d csid=%D", + (int)fmt, csid); + + if (csid >= (uint32_t)cscf->max_streams) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "RTMP in chunk stream too big: %D >= %D", + csid, cscf->max_streams); + ngx_rtmp_finalize_session(s); + return; + } + + /* link orphan */ + if (s->in_csid == 0) { + + /* unlink from stream #0 */ + st->in = st->in->next; + + /* link to new stream */ + s->in_csid = csid; + st = &s->in_streams[csid]; + if (st->in == NULL) { + in->next = in; + } else { + in->next = st->in->next; + st->in->next = in; + } + st->in = in; + h = &st->hdr; + h->csid = csid; + } + + ext = st->ext; + timestamp = st->dtime; + if (fmt <= 2 ) { + if (b->last - p < 3) + continue; + + /* timestamp: big-endian 3B -> little-endian 4B */ + + timestamp = 0; + timestamp |= ((uint32_t) *p++ << 16); + timestamp |= ((uint32_t) *p++ << 8); + timestamp |= *p++; + + ext = (timestamp == 0x00ffffff); + + if (fmt <= 1) { + if (b->last - p < 4) + continue; + + /* size: big-endian 3B -> little-endian 4B */ + + h->mlen = 0; + h->mlen |= ((uint32_t) *p++ << 16); + h->mlen |= ((uint32_t) *p++ << 8); + h->mlen |= *p++; + + h->type = *p++; + + if (fmt == 0) { + if (b->last - p < 4) + continue; + + /* stream: little-endian 4B */ + + h->msid = *p++; + h->msid |= ((uint32_t) *p++ << 8); + h->msid |= ((uint32_t) *p++ << 16); + h->msid |= ((uint32_t) *p++ << 24); + } + } + } + + /* extended header */ + if (ext) { + if (b->last - p < 4) + continue; + + /* timestamp: big-endian 4B */ + + timestamp = 0; + timestamp |= ((uint32_t) *p++ << 24); + timestamp |= ((uint32_t) *p++ << 16); + timestamp |= ((uint32_t) *p++ << 8); + timestamp |= *p++; + } + + if (st->len == 0) { + /* Messages with type=3 should + * never have ext timestamp field + * according to standard. + * However that's not always the case + * in real life */ + st->ext = (ext && cscf->publish_time_fix); + if (fmt) { + st->dtime = timestamp; + } else { + h->timestamp = timestamp; + st->dtime = 0; + } + } + + ngx_log_debug8(NGX_LOG_DEBUG_RTMP, c->log, 0, + "RTMP mheader fmt=%d %s (%d) " + "time=%uD+%uD mlen=%D len=%D msid=%D", + (int)fmt, ngx_rtmp_message_type(h->type), (int)h->type, + h->timestamp, st->dtime, h->mlen, st->len, h->msid); + + /* header done */ + b->pos = p; + + if (h->mlen > cscf->max_message) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "too big message: %uz, %uz", + h->mlen, cscf->max_message); + ngx_rtmp_finalize_session(s); + return; + } + } + + size = b->last - b->pos; + fsize = h->mlen - st->len; + + if (size < ngx_min(fsize, s->in_chunk_size)) + continue; + + /* buffer is ready */ + + if (fsize > s->in_chunk_size) { + /* collect fragmented chunks */ + st->len += s->in_chunk_size; + b->last = b->pos + s->in_chunk_size; + old_pos = b->last; + old_size = size - s->in_chunk_size; + + } else { + /* handle! */ + head = st->in->next; + st->in->next = NULL; + b->last = b->pos + fsize; + old_pos = b->last; + old_size = size - fsize; + st->len = 0; + h->timestamp += st->dtime; + + if (ngx_rtmp_receive_message(s, h, head) != NGX_OK) { + ngx_rtmp_finalize_session(s); + return; + } + + /* server configuration may change due to virtual server match */ + if (s->server_changed) { + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + st = &s->in_streams[s->in_csid]; + + s->server_changed = 0; + } + + if (s->in_chunk_size_changing) { + /* copy old data to a new buffer */ + if (!old_size) { + ngx_rtmp_finalize_set_chunk_size(s); + } + + } else { + /* add used bufs to stream #0 */ + st0 = &s->in_streams[0]; + st->in->next = st0->in; + st0->in = head; + st->in = NULL; + } + } + + s->in_csid = 0; + } +} + + +static void +ngx_rtmp_send(ngx_event_t *wev) +{ + ngx_connection_t *c; + ngx_rtmp_session_t *s; + ngx_int_t n; + ngx_rtmp_live_ctx_t *lctx; + ngx_rtmp_core_srv_conf_t *cscf; + + c = wev->data; + s = c->data; + + if (c->destroyed) { + return; + } + + if (wev->timedout) { + ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, + "client timed out"); + c->timedout = 1; + ngx_rtmp_finalize_session(s); + return; + } + + if (wev->timer_set) { + ngx_del_timer(wev); + } + + if (s->out_chain == NULL && s->out_pos != s->out_last) { + s->out_chain = s->out[s->out_pos]; + s->out_bpos = s->out_chain->buf->pos; + } + + while (s->out_chain) { + n = c->send(c, s->out_bpos, s->out_chain->buf->last - s->out_bpos); + + if (n == NGX_AGAIN || n == 0) { + ngx_add_timer(c->write, s->timeout); + if (ngx_handle_write_event(c->write, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + return; + } + + if (n < 0) { + ngx_rtmp_finalize_session(s); + return; + } + + s->out_bytes += n; + s->ping_reset = 1; + ngx_rtmp_update_bandwidth(&ngx_rtmp_bw_out, n); + s->out_bpos += n; + if (s->out_bpos == s->out_chain->buf->last) { + s->out_chain = s->out_chain->next; + if (s->out_chain == NULL) { + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + ngx_rtmp_free_shared_chain(cscf, s->out[s->out_pos]); + ++s->out_pos; + s->out_pos %= s->out_queue; + if (s->out_pos == s->out_last) { + break; + } + s->out_chain = s->out[s->out_pos]; + } + s->out_bpos = s->out_chain->buf->pos; + } + } + + lctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (lctx && !lctx->publishing && !wev->timer_set) { + ngx_add_timer(wev, s->timeout); + } + + if (wev->active) { + ngx_del_event(wev, NGX_WRITE_EVENT, 0); + } + + ngx_event_process_posted((ngx_cycle_t *) ngx_cycle, &s->posted_dry_events); +} + + +void +ngx_rtmp_prepare_message(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_rtmp_header_t *lh, ngx_chain_t *out) +{ + ngx_chain_t *l; + u_char *p; + ngx_int_t hsize, thsize, nbufs; + uint32_t mlen, timestamp, ext_timestamp; + static uint8_t hdrsize[] = { 12, 8, 4, 1 }; + u_char th[7]; + ngx_rtmp_core_srv_conf_t *cscf; + uint8_t fmt; + ngx_connection_t *c; + + c = s->connection; + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + if (h->csid >= (uint32_t)cscf->max_streams) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "RTMP out chunk stream too big: %D >= %D", + h->csid, cscf->max_streams); + ngx_rtmp_finalize_session(s); + return; + } + + /* detect packet size */ + mlen = 0; + nbufs = 0; + for(l = out; l; l = l->next) { + mlen += (l->buf->last - l->buf->pos); + ++nbufs; + } + + fmt = 0; + if (lh && lh->csid && h->msid == lh->msid) { + ++fmt; + if (h->type == lh->type && mlen && mlen == lh->mlen) { + ++fmt; + if (h->timestamp == lh->timestamp) { + ++fmt; + } + } + + if (h->type == NGX_RTMP_MSG_VIDEO || h->type == NGX_RTMP_MSG_AUDIO) { + timestamp = h->timestamp - s->offset_timestamp - lh->timestamp; + + if (lh->timestamp) { + timestamp += s->offset_timestamp; + } + } else { + timestamp = h->timestamp - lh->timestamp; + } + } else { + if (h->type == NGX_RTMP_MSG_VIDEO || h->type == NGX_RTMP_MSG_AUDIO) { + if (!s->offset_timestamp_set) { + s->offset_timestamp_set = 1; + s->offset_timestamp = h->timestamp; + } else if (h->timestamp == 0) { + s->offset_timestamp = 0; + } + } + + timestamp = h->timestamp - s->offset_timestamp; + } + + /*if (lh) { + *lh = *h; + lh->mlen = mlen; + }*/ + + hsize = hdrsize[fmt]; + + (void) nbufs; + ngx_log_debug8(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "RTMP prep %s (%d) fmt=%d csid=%uD timestamp=%uD " + "mlen=%uD msid=%uD nbufs=%d", + ngx_rtmp_message_type(h->type), (int)h->type, (int)fmt, + h->csid, timestamp, mlen, h->msid, nbufs); + + ext_timestamp = 0; + if (timestamp >= 0x00ffffff) { + ext_timestamp = timestamp; + timestamp = 0x00ffffff; + hsize += 4; + } + + if (h->csid >= 64) { + ++hsize; + if (h->csid >= 320) { + ++hsize; + } + } + + /* fill initial header */ + out->buf->pos -= hsize; + p = out->buf->pos; + + /* basic header */ + *p = (fmt << 6); + if (h->csid >= 2 && h->csid <= 63) { + *p++ |= (((uint8_t)h->csid) & 0x3f); + } else if (h->csid >= 64 && h->csid < 320) { + ++p; + *p++ = (uint8_t)(h->csid - 64); + } else { + *p++ |= 1; + *p++ = (uint8_t)(h->csid - 64); + *p++ = (uint8_t)((h->csid - 64) >> 8); + } + + /* create fmt3 header for successive fragments */ + thsize = p - out->buf->pos; + ngx_memcpy(th, out->buf->pos, thsize); + th[0] |= 0xc0; + + /* message header */ + if (fmt <= 2) { + *p++ = (u_char) (timestamp >> 16); + *p++ = (u_char) (timestamp >> 8); + *p++ = (u_char) timestamp; + + if (fmt <= 1) { + *p++ = (u_char) (mlen >> 16); + *p++ = (u_char) (mlen >> 8); + *p++ = (u_char) mlen; + + *p++ = h->type; + + if (fmt == 0) { + *p++ = (u_char) h->msid; + *p++ = (u_char) (h->msid >> 8); + *p++ = (u_char) (h->msid >> 16); + *p++ = (u_char) (h->msid >> 24); + } + } + } + + /* extended header */ + if (ext_timestamp) { + *p++ = (u_char) (ext_timestamp >> 24); + *p++ = (u_char) (ext_timestamp >> 16); + *p++ = (u_char) (ext_timestamp >> 8); + *p++ = (u_char) ext_timestamp; + + /* This CONTRADICTS the standard + * but that's the way flash client + * wants data to be encoded; + * ffmpeg complains */ + if (cscf->play_time_fix) { + ngx_memcpy(&th[thsize], p - 4, 4); + thsize += 4; + } + } + + /* append headers to successive fragments */ + for(out = out->next; out; out = out->next) { + out->buf->pos -= thsize; + ngx_memcpy(out->buf->pos, th, thsize); + } +} + + +ngx_int_t +ngx_rtmp_send_message(ngx_rtmp_session_t *s, ngx_chain_t *out, + ngx_uint_t priority) +{ + ngx_uint_t nmsg; + + nmsg = (s->out_last + s->out_queue - s->out_pos) % s->out_queue + 1; + + if (priority > 3) { + priority = 3; + } + + /* drop packet? + * Note we always leave 1 slot free */ + if (nmsg + priority * s->out_queue / 4 >= s->out_queue) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "RTMP drop message bufs=%ui, priority=%ui", + nmsg, priority); + + return NGX_AGAIN; + } + + s->out[s->out_last++] = out; + s->out_last %= s->out_queue; + + ngx_rtmp_acquire_shared_chain(out); + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "RTMP send nmsg=%ui, priority=%ui #%ui", + nmsg, priority, s->out_last); + + if (priority && s->out_buffer && nmsg < s->out_cork) { + return NGX_OK; + } + + if (!s->connection->write->active) { + ngx_rtmp_send(s->connection->write); + /*return ngx_add_event(s->connection->write, NGX_WRITE_EVENT, NGX_CLEAR_EVENT);*/ + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_receive_message(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_array_t *evhs; + size_t n; + ngx_rtmp_handler_pt *evh; + + cmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_core_module); + +#ifdef NGX_DEBUG + { + int nbufs; + ngx_chain_t *ch; + + for(nbufs = 1, ch = in; + ch->next; + ch = ch->next, ++nbufs); + + ngx_log_debug7(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "RTMP recv %s (%d) csid=%D timestamp=%D " + "mlen=%D msid=%D nbufs=%d", + ngx_rtmp_message_type(h->type), (int)h->type, + h->csid, h->timestamp, h->mlen, h->msid, nbufs); + } +#endif + + if (h->type > NGX_RTMP_MSG_MAX) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "unexpected RTMP message type: %d", (int)h->type); + return NGX_OK; + } + + evhs = &cmcf->events[h->type]; + evh = evhs->elts; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "nhandlers: %d", evhs->nelts); + + for(n = 0; n < evhs->nelts; ++n, ++evh) { + if (!evh) { + continue; + } + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "calling handler %d", n); + + switch ((*evh)(s, h, in)) { + case NGX_ERROR: + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handler %d failed", n); + return NGX_ERROR; + case NGX_DONE: + return NGX_OK; + } + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_set_chunk_size(ngx_rtmp_session_t *s, ngx_uint_t size) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_chain_t *li, *fli, *lo, *flo; + ngx_buf_t *bi, *bo; + ngx_int_t n; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "setting chunk_size=%ui", size); + + if (size > NGX_RTMP_MAX_CHUNK_SIZE) { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, 0, + "too big RTMP chunk size:%ui", size); + return NGX_ERROR; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + s->in_old_pool = s->in_pool; + s->in_chunk_size = size; + s->in_pool = ngx_create_pool(4096, s->connection->log); + + /* copy existing chunk data */ + if (s->in_old_pool) { + s->in_chunk_size_changing = 1; + s->in_streams[0].in = NULL; + + for(n = 1; n < cscf->max_streams; ++n) { + /* stream buffer is circular + * for all streams except for the current one + * (which caused this chunk size change); + * we can simply ignore it */ + li = s->in_streams[n].in; + if (li == NULL || li->next == NULL) { + s->in_streams[n].in = NULL; + continue; + } + /* move from last to the first */ + li = li->next; + fli = li; + lo = ngx_rtmp_alloc_in_buf(s); + if (lo == NULL) { + return NGX_ERROR; + } + flo = lo; + for ( ;; ) { + bi = li->buf; + bo = lo->buf; + + if (bo->end - bo->last >= bi->last - bi->pos) { + bo->last = ngx_cpymem(bo->last, bi->pos, + bi->last - bi->pos); + li = li->next; + if (li == fli) { + lo->next = flo; + s->in_streams[n].in = lo; + break; + } + continue; + } + + bi->pos += (ngx_cpymem(bo->last, bi->pos, + bo->end - bo->last) - bo->last); + lo->next = ngx_rtmp_alloc_in_buf(s); + lo = lo->next; + if (lo == NULL) { + return NGX_ERROR; + } + } + } + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_finalize_set_chunk_size(ngx_rtmp_session_t *s) +{ + if (s->in_chunk_size_changing && s->in_old_pool) { + ngx_destroy_pool(s->in_old_pool); + s->in_old_pool = NULL; + s->in_chunk_size_changing = 0; + } + return NGX_OK; +} + + diff --git a/ngx_http_flv_module/ngx_rtmp_handshake.c b/ngx_http_flv_module/ngx_rtmp_handshake.c new file mode 100644 index 0000000..130693f --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_handshake.c @@ -0,0 +1,647 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp.h" + +#include +#include + + +static void ngx_rtmp_handshake_send(ngx_event_t *wev); +static void ngx_rtmp_handshake_recv(ngx_event_t *rev); +static void ngx_rtmp_handshake_done(ngx_rtmp_session_t *s); + + +/* RTMP handshake : + * + * =peer1= =peer2= + * challenge ----> (.....[digest1]......) ----> 1537 bytes + * response <---- (...........[digest2]) <---- 1536 bytes + * + * + * - both packets contain random bytes except for digests + * - digest1 position is calculated on random packet bytes + * - digest2 is always at the end of the packet + * + * digest1: HMAC_SHA256(packet, peer1_partial_key) + * digest2: HMAC_SHA256(packet, HMAC_SHA256(digest1, peer2_full_key)) + */ + + +/* Handshake keys */ +static u_char +ngx_rtmp_server_key[] = { + 'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ', + 'F', 'l', 'a', 's', 'h', ' ', 'M', 'e', 'd', 'i', 'a', ' ', + 'S', 'e', 'r', 'v', 'e', 'r', ' ', + '0', '0', '1', + + 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1, + 0x02, 0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB, + 0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE +}; + + +static u_char +ngx_rtmp_client_key[] = { + 'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ', + 'F', 'l', 'a', 's', 'h', ' ', 'P', 'l', 'a', 'y', 'e', 'r', ' ', + '0', '0', '1', + + 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1, + 0x02, 0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB, + 0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE +}; + + +static const u_char +ngx_rtmp_server_version[4] = { + 0x0D, 0x0E, 0x0A, 0x0D +}; + + +static const u_char +ngx_rtmp_client_version[4] = { + 0x0C, 0x00, 0x0D, 0x0E +}; + + +#define NGX_RTMP_HANDSHAKE_KEYLEN SHA256_DIGEST_LENGTH +#define NGX_RTMP_HANDSHAKE_BUFSIZE 1537 + + +#define NGX_RTMP_HANDSHAKE_SERVER_RECV_CHALLENGE 1 +#define NGX_RTMP_HANDSHAKE_SERVER_SEND_CHALLENGE 2 +#define NGX_RTMP_HANDSHAKE_SERVER_SEND_RESPONSE 3 +#define NGX_RTMP_HANDSHAKE_SERVER_RECV_RESPONSE 4 +#define NGX_RTMP_HANDSHAKE_SERVER_DONE 5 + + +#define NGX_RTMP_HANDSHAKE_CLIENT_SEND_CHALLENGE 6 +#define NGX_RTMP_HANDSHAKE_CLIENT_RECV_CHALLENGE 7 +#define NGX_RTMP_HANDSHAKE_CLIENT_RECV_RESPONSE 8 +#define NGX_RTMP_HANDSHAKE_CLIENT_SEND_RESPONSE 9 +#define NGX_RTMP_HANDSHAKE_CLIENT_DONE 10 + + +static ngx_str_t ngx_rtmp_server_full_key + = { sizeof(ngx_rtmp_server_key), ngx_rtmp_server_key }; +static ngx_str_t ngx_rtmp_server_partial_key + = { 36, ngx_rtmp_server_key }; + +static ngx_str_t ngx_rtmp_client_full_key + = { sizeof(ngx_rtmp_client_key), ngx_rtmp_client_key }; +static ngx_str_t ngx_rtmp_client_partial_key + = { 30, ngx_rtmp_client_key }; + + +static ngx_int_t +ngx_rtmp_make_digest(ngx_str_t *key, ngx_buf_t *src, + u_char *skip, u_char *dst, ngx_log_t *log) +{ + static HMAC_CTX *hmac; + unsigned int len; + + if (hmac == NULL) { +#if OPENSSL_VERSION_NUMBER < 0x10100000L + static HMAC_CTX shmac; + hmac = &shmac; + HMAC_CTX_init(hmac); +#else + hmac = HMAC_CTX_new(); + if (hmac == NULL) { + return NGX_ERROR; + } +#endif + } + + HMAC_Init_ex(hmac, key->data, key->len, EVP_sha256(), NULL); + + if (skip && src->pos <= skip && skip <= src->last) { + if (skip != src->pos) { + HMAC_Update(hmac, src->pos, skip - src->pos); + } + if (src->last != skip + NGX_RTMP_HANDSHAKE_KEYLEN) { + HMAC_Update(hmac, skip + NGX_RTMP_HANDSHAKE_KEYLEN, + src->last - skip - NGX_RTMP_HANDSHAKE_KEYLEN); + } + } else { + HMAC_Update(hmac, src->pos, src->last - src->pos); + } + + HMAC_Final(hmac, dst, &len); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_find_digest(ngx_buf_t *b, ngx_str_t *key, size_t base, ngx_log_t *log) +{ + size_t n, offs; + u_char digest[NGX_RTMP_HANDSHAKE_KEYLEN]; + u_char *p; + + offs = 0; + for (n = 0; n < 4; ++n) { + offs += b->pos[base + n]; + } + offs = (offs % 728) + base + 4; + p = b->pos + offs; + + if (ngx_rtmp_make_digest(key, b, p, digest, log) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_memcmp(digest, p, NGX_RTMP_HANDSHAKE_KEYLEN) == 0) { + return offs; + } + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_write_digest(ngx_buf_t *b, ngx_str_t *key, size_t base, + ngx_log_t *log) +{ + size_t n, offs; + u_char *p; + + offs = 0; + for (n = 8; n < 12; ++n) { + offs += b->pos[base + n]; + } + offs = (offs % 728) + base + 12; + p = b->pos + offs; + + if (ngx_rtmp_make_digest(key, b, p, p, log) != NGX_OK) { + return NGX_ERROR; + } + + return NGX_OK; +} + + +static void +ngx_rtmp_fill_random_buffer(ngx_buf_t *b) +{ + for (; b->last != b->end; ++b->last) { + *b->last = (u_char) rand(); + } +} + + +static ngx_buf_t * +ngx_rtmp_alloc_handshake_buffer(ngx_rtmp_session_t *s) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_chain_t *cl; + ngx_buf_t *b; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: allocating buffer"); + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + if (cscf->free_hs) { + cl = cscf->free_hs; + b = cl->buf; + cscf->free_hs = cl->next; + ngx_free_chain(cscf->pool, cl); + + } else { + b = ngx_pcalloc(cscf->pool, sizeof(ngx_buf_t)); + if (b == NULL) { + return NULL; + } + b->memory = 1; + b->start = ngx_pcalloc(cscf->pool, NGX_RTMP_HANDSHAKE_BUFSIZE); + if (b->start == NULL) { + return NULL; + } + b->end = b->start + NGX_RTMP_HANDSHAKE_BUFSIZE; + } + + b->pos = b->last = b->start; + + return b; +} + + +void +ngx_rtmp_free_handshake_buffers(ngx_rtmp_session_t *s) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_chain_t *cl; + + if (s->hs_buf == NULL) { + return; + } + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + cl = ngx_alloc_chain_link(cscf->pool); + if (cl == NULL) { + return; + } + cl->buf = s->hs_buf; + cl->next = cscf->free_hs; + cscf->free_hs = cl; + s->hs_buf = NULL; +} + + +static ngx_int_t +ngx_rtmp_handshake_create_challenge(ngx_rtmp_session_t *s, + const u_char version[4], ngx_str_t *key) +{ + ngx_buf_t *b; + + b = s->hs_buf; + b->last = b->pos = b->start; + *b->last++ = '\x03'; + *(uint32_t *) b->last = htonl(s->epoch); + b->last += 4; + b->last = ngx_cpymem(b->last, version, 4); + ngx_rtmp_fill_random_buffer(b); + ++b->pos; + if (ngx_rtmp_write_digest(b, key, 0, s->connection->log) != NGX_OK) { + return NGX_ERROR; + } + --b->pos; + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_handshake_parse_challenge(ngx_rtmp_session_t *s, + ngx_str_t *peer_key, ngx_str_t *key) +{ + ngx_buf_t *b; + u_char *p; + ngx_int_t offs; + + b = s->hs_buf; + if (*b->pos != '\x03') { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "handshake: unexpected RTMP version: %i", + (ngx_int_t)*b->pos); + return NGX_ERROR; + } + ++b->pos; + s->peer_epoch = ntohl(*(uint32_t *) b->pos); + + p = b->pos + 4; + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: peer version=%i.%i.%i.%i epoch=%uD", + (ngx_int_t)p[3], (ngx_int_t)p[2], + (ngx_int_t)p[1], (ngx_int_t)p[0], + (uint32_t)s->peer_epoch); + if (*(uint32_t *)p == 0) { + s->hs_old = 1; + return NGX_OK; + } + + offs = ngx_rtmp_find_digest(b, peer_key, 772, s->connection->log); + if (offs == NGX_ERROR) { + offs = ngx_rtmp_find_digest(b, peer_key, 8, s->connection->log); + } + if (offs == NGX_ERROR) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "handshake: digest not found"); + s->hs_old = 1; + return NGX_OK; + } + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: digest found at pos=%i", offs); + b->pos += offs; + b->last = b->pos + NGX_RTMP_HANDSHAKE_KEYLEN; + s->hs_digest = ngx_palloc(s->connection->pool, NGX_RTMP_HANDSHAKE_KEYLEN); + if (s->hs_digest == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "handshake: failed to allocate for digest"); + return NGX_ERROR; + } + + if (ngx_rtmp_make_digest(key, b, NULL, s->hs_digest, s->connection->log) + != NGX_OK) + { + return NGX_ERROR; + } + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_handshake_create_response(ngx_rtmp_session_t *s) +{ + ngx_buf_t *b; + u_char *p; + ngx_str_t key; + + b = s->hs_buf; + b->pos = b->last = b->start + 1; + ngx_rtmp_fill_random_buffer(b); + if (s->hs_digest) { + p = b->last - NGX_RTMP_HANDSHAKE_KEYLEN; + key.data = s->hs_digest; + key.len = NGX_RTMP_HANDSHAKE_KEYLEN; + if (ngx_rtmp_make_digest(&key, b, p, p, s->connection->log) != NGX_OK) { + return NGX_ERROR; + } + } + + return NGX_OK; +} + + +static void +ngx_rtmp_handshake_done(ngx_rtmp_session_t *s) +{ + ngx_rtmp_free_handshake_buffers(s); + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: done"); + + if (ngx_rtmp_fire_event(s, NGX_RTMP_HANDSHAKE_DONE, + NULL, NULL) != NGX_OK) + { + ngx_rtmp_finalize_session(s); + return; + } + + ngx_rtmp_cycle(s); +} + + +static void +ngx_rtmp_handshake_recv(ngx_event_t *rev) +{ + ssize_t n; + ngx_connection_t *c; + ngx_rtmp_session_t *s; + ngx_buf_t *b; + + c = rev->data; + s = c->data; + + if (c->destroyed) { + return; + } + + if (rev->timedout) { + ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, + "handshake: recv: client timed out"); + c->timedout = 1; + ngx_rtmp_finalize_session(s); + return; + } + + if (rev->timer_set) { + ngx_del_timer(rev); + } + + b = s->hs_buf; + + while (b->last != b->end) { + n = c->recv(c, b->last, b->end - b->last); + + if (n == NGX_ERROR || n == 0) { + ngx_rtmp_finalize_session(s); + return; + } + + if (n == NGX_AGAIN) { + ngx_add_timer(rev, s->timeout); + if (ngx_handle_read_event(c->read, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + return; + } + + b->last += n; + } + + if (rev->active) { + ngx_del_event(rev, NGX_READ_EVENT, 0); + } + + ++s->hs_stage; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: stage %ui", s->hs_stage); + + switch (s->hs_stage) { + case NGX_RTMP_HANDSHAKE_SERVER_SEND_CHALLENGE: + if (ngx_rtmp_handshake_parse_challenge(s, + &ngx_rtmp_client_partial_key, + &ngx_rtmp_server_full_key) != NGX_OK) + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "handshake: error parsing challenge"); + ngx_rtmp_finalize_session(s); + return; + } + if (s->hs_old) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: old-style challenge"); + s->hs_buf->pos = s->hs_buf->start; + s->hs_buf->last = s->hs_buf->end; + } else if (ngx_rtmp_handshake_create_challenge(s, + ngx_rtmp_server_version, + &ngx_rtmp_server_partial_key) != NGX_OK) + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "handshake: error creating challenge"); + ngx_rtmp_finalize_session(s); + return; + } + ngx_rtmp_handshake_send(c->write); + break; + + case NGX_RTMP_HANDSHAKE_SERVER_DONE: + ngx_rtmp_handshake_done(s); + break; + + case NGX_RTMP_HANDSHAKE_CLIENT_RECV_RESPONSE: + if (ngx_rtmp_handshake_parse_challenge(s, + &ngx_rtmp_server_partial_key, + &ngx_rtmp_client_full_key) != NGX_OK) + { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "handshake: error parsing challenge"); + ngx_rtmp_finalize_session(s); + return; + } + s->hs_buf->pos = s->hs_buf->last = s->hs_buf->start + 1; + ngx_rtmp_handshake_recv(c->read); + break; + + case NGX_RTMP_HANDSHAKE_CLIENT_SEND_RESPONSE: + if (ngx_rtmp_handshake_create_response(s) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "handshake: response error"); + ngx_rtmp_finalize_session(s); + return; + } + ngx_rtmp_handshake_send(c->write); + break; + } +} + + +static void +ngx_rtmp_handshake_send(ngx_event_t *wev) +{ + ngx_int_t n; + ngx_connection_t *c; + ngx_rtmp_session_t *s; + ngx_buf_t *b; + + c = wev->data; + s = c->data; + + if (c->destroyed) { + return; + } + + if (wev->timedout) { + ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, + "handshake: send: client timed out"); + c->timedout = 1; + ngx_rtmp_finalize_session(s); + return; + } + + if (wev->timer_set) { + ngx_del_timer(wev); + } + + b = s->hs_buf; + + while(b->pos != b->last) { + n = c->send(c, b->pos, b->last - b->pos); + + if (n == NGX_ERROR) { + ngx_rtmp_finalize_session(s); + return; + } + + if (n == NGX_AGAIN || n == 0) { + ngx_add_timer(c->write, s->timeout); + if (ngx_handle_write_event(c->write, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + return; + } + + b->pos += n; + } + + if (wev->active) { + ngx_del_event(wev, NGX_WRITE_EVENT, 0); + } + + ++s->hs_stage; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: stage %ui", s->hs_stage); + + switch (s->hs_stage) { + case NGX_RTMP_HANDSHAKE_SERVER_SEND_RESPONSE: + if (s->hs_old) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: old-style response"); + s->hs_buf->pos = s->hs_buf->start + 1; + s->hs_buf->last = s->hs_buf->end; + } else if (ngx_rtmp_handshake_create_response(s) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, c->log, 0, + "handshake: response error"); + ngx_rtmp_finalize_session(s); + return; + } + ngx_rtmp_handshake_send(wev); + break; + + case NGX_RTMP_HANDSHAKE_SERVER_RECV_RESPONSE: + s->hs_buf->pos = s->hs_buf->last = s->hs_buf->start + 1; + ngx_rtmp_handshake_recv(c->read); + break; + + case NGX_RTMP_HANDSHAKE_CLIENT_RECV_CHALLENGE: + s->hs_buf->pos = s->hs_buf->last = s->hs_buf->start; + ngx_rtmp_handshake_recv(c->read); + break; + + case NGX_RTMP_HANDSHAKE_CLIENT_DONE: + ngx_rtmp_handshake_done(s); + break; + } +} + + +void +ngx_rtmp_handshake(ngx_rtmp_session_t *s) +{ + ngx_connection_t *c; + ngx_time_t *tp; + + c = s->connection; + c->read->handler = ngx_rtmp_handshake_recv; + c->write->handler = ngx_rtmp_handshake_send; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: start server handshake"); + + s->hs_buf = ngx_rtmp_alloc_handshake_buffer(s); + s->hs_stage = NGX_RTMP_HANDSHAKE_SERVER_RECV_CHALLENGE; + + tp = ngx_timeofday(); + s->start_sec = tp->sec; + s->start_msec = tp->msec; + + ngx_rtmp_handshake_recv(c->read); +} + + +void +ngx_rtmp_client_handshake(ngx_rtmp_session_t *s, unsigned async) +{ + ngx_connection_t *c; + ngx_time_t *tp; + + c = s->connection; + c->read->handler = ngx_rtmp_handshake_recv; + c->write->handler = ngx_rtmp_handshake_send; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "handshake: start client handshake"); + + s->hs_buf = ngx_rtmp_alloc_handshake_buffer(s); + s->hs_stage = NGX_RTMP_HANDSHAKE_CLIENT_SEND_CHALLENGE; + + if (ngx_rtmp_handshake_create_challenge(s, + ngx_rtmp_client_version, + &ngx_rtmp_client_partial_key) != NGX_OK) + { + ngx_rtmp_finalize_session(s); + return; + } + + tp = ngx_timeofday(); + s->start_sec = tp->sec; + s->start_msec = tp->msec; + + if (async) { + ngx_add_timer(c->write, s->timeout); + if (ngx_handle_write_event(c->write, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + return; + } + + ngx_rtmp_handshake_send(c->write); +} + diff --git a/ngx_http_flv_module/ngx_rtmp_init.c b/ngx_http_flv_module/ngx_rtmp_init.c new file mode 100644 index 0000000..abddb19 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_init.c @@ -0,0 +1,458 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_proxy_protocol.h" + + +static void ngx_rtmp_close_connection(ngx_connection_t *c); +static void ngx_rtmp_process_unix_socket(ngx_rtmp_connection_t *rconn); + + +void +ngx_rtmp_init_connection(ngx_connection_t *c) +{ + ngx_uint_t i; + ngx_rtmp_port_t *port; + struct sockaddr_in *sin; + ngx_rtmp_in_addr_t *addr; + ngx_rtmp_connection_t *rconn; + ngx_rtmp_session_t *s; + ngx_int_t unix_socket; +#if (NGX_HAVE_INET6) + struct sockaddr_in6 *sin6; + ngx_rtmp_in6_addr_t *addr6; +#endif + + rconn = ngx_pcalloc(c->pool, sizeof(ngx_rtmp_connection_t)); + if (rconn == NULL) { + ngx_rtmp_close_connection(c); + return; + } + + ++ngx_rtmp_naccepted; + + c->data = rconn; + + /* find the server configuration for the address:port */ + + port = c->listening->servers; + unix_socket = 0; + + if (port->naddrs > 1) { + + /* + * There are several addresses on this port and one of them + * is the "*:port" wildcard so getsockname() is needed to determine + * the server address. + * + * AcceptEx() already gave this address. + */ + + if (ngx_connection_local_sockaddr(c, NULL, 0) != NGX_OK) { + ngx_rtmp_close_connection(c); + return; + } + + switch (c->local_sockaddr->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + sin6 = (struct sockaddr_in6 *) c->local_sockaddr; + + addr6 = port->addrs; + + /* the last address is "*" */ + + for (i = 0; i < port->naddrs - 1; i++) { + if (ngx_memcmp(&addr6[i].addr6, &sin6->sin6_addr, 16) == 0) { + break; + } + } + + rconn->addr_conf = &addr6[i].conf; + + break; +#endif + + case AF_UNIX: + unix_socket = 1; + + ngx_rtmp_process_unix_socket(rconn); + + break; + + default: /* AF_INET */ + sin = (struct sockaddr_in *) c->local_sockaddr; + + addr = port->addrs; + + /* the last address is "*" */ + + for (i = 0; i < port->naddrs - 1; i++) { + if (addr[i].addr == sin->sin_addr.s_addr) { + break; + } + } + + rconn->addr_conf = &addr[i].conf; + + break; + } + + } else { + switch (c->local_sockaddr->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + addr6 = port->addrs; + rconn->addr_conf = &addr6[0].conf; + break; +#endif + + case AF_UNIX: + unix_socket = 1; + + ngx_rtmp_process_unix_socket(rconn); + + break; + + default: /* AF_INET */ + addr = port->addrs; + rconn->addr_conf = &addr[0].conf; + break; + } + } + + /* the default server configuration for the address:port */ + rconn->conf_ctx = rconn->addr_conf->default_server->ctx; + + ngx_log_error(NGX_LOG_INFO, c->log, 0, "*%ui client connected '%V'", + c->number, &c->addr_text); + + s = ngx_rtmp_init_session(c, rconn->addr_conf); + if (s == NULL) { + return; + } + + /* only auto-pushed connections are + * done through unix socket */ + + s->auto_pushed = unix_socket; + + if (rconn->addr_conf->proxy_protocol) { + ngx_rtmp_proxy_protocol(s); + + } else { + ngx_rtmp_handshake(s); + } +} + + +ngx_rtmp_session_t * +ngx_rtmp_init_session(ngx_connection_t *c, ngx_rtmp_addr_conf_t *addr_conf) +{ + ngx_rtmp_session_t *s; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_error_log_ctx_t *ctx; + + s = ngx_pcalloc(c->pool, sizeof(ngx_rtmp_session_t)); + if (s == NULL) { + goto failed; + } + + s->rtmp_connection = c->data; + + s->main_conf = addr_conf->default_server->ctx->main_conf; + s->srv_conf = addr_conf->default_server->ctx->srv_conf; + + s->addr_text = &addr_conf->addr_text; + + c->data = s; + s->connection = c; + + ctx = ngx_palloc(c->pool, sizeof(ngx_rtmp_error_log_ctx_t)); + if (ctx == NULL) { + goto failed; + } + + ctx->client = &c->addr_text; + ctx->session = s; + + c->log->connection = c->number; + c->log->handler = ngx_rtmp_log_error; + c->log->data = ctx; + c->log->action = NULL; + + c->log_error = NGX_ERROR_INFO; + + s->ctx = ngx_pcalloc(c->pool, sizeof(void *) * ngx_rtmp_max_module); + if (s->ctx == NULL) { + goto failed; + } + + s->out_pool = ngx_create_pool(4096, c->log); + if (s->out_pool == NULL) { + goto failed; + } + + s->out = ngx_pcalloc(s->out_pool, sizeof(ngx_chain_t *) + * ((ngx_rtmp_core_srv_conf_t *) + addr_conf->default_server->ctx->srv_conf + [ngx_rtmp_core_module.ctx_index])->out_queue); + if (s->out == NULL) { + goto failed; + } + + s->in_streams_pool = ngx_create_pool(4096, c->log); + if (s->in_streams_pool == NULL) { + goto failed; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + s->out_queue = cscf->out_queue; + s->out_cork = cscf->out_cork; + s->in_streams = ngx_pcalloc(s->in_streams_pool, sizeof(ngx_rtmp_stream_t) + * cscf->max_streams); + if (s->in_streams == NULL) { + goto failed; + } + +#if (nginx_version >= 1007005) + ngx_queue_init(&s->posted_dry_events); +#endif + + s->epoch = ngx_current_msec; + s->timeout = cscf->timeout; + s->buflen = cscf->buflen; + ngx_rtmp_set_chunk_size(s, NGX_RTMP_DEFAULT_CHUNK_SIZE); + + + if (ngx_rtmp_fire_event(s, NGX_RTMP_CONNECT, NULL, NULL) != NGX_OK) { + ngx_rtmp_finalize_session(s); + return NULL; + } + + return s; + +failed: + if (s && s->out_pool) { + ngx_destroy_pool(s->out_pool); + s->out_pool = NULL; + } + + if (s && s->in_streams_pool) { + ngx_destroy_pool(s->in_streams_pool); + s->in_streams_pool = NULL; + } + + ngx_rtmp_close_connection(c); + return NULL; +} + + +u_char * +ngx_rtmp_log_error(ngx_log_t *log, u_char *buf, size_t len) +{ + u_char *p; + ngx_rtmp_session_t *s; + ngx_rtmp_error_log_ctx_t *ctx; + + if (log->action) { + p = ngx_snprintf(buf, len, " while %s", log->action); + len -= p - buf; + buf = p; + } + + ctx = log->data; + + p = ngx_snprintf(buf, len, ", client: %V", ctx->client); + len -= p - buf; + buf = p; + + s = ctx->session; + + if (s == NULL) { + return p; + } + + p = ngx_snprintf(buf, len, ", server: %V", s->addr_text); + len -= p - buf; + buf = p; + + return p; +} + + +static void +ngx_rtmp_close_connection(ngx_connection_t *c) +{ + ngx_pool_t *pool; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, c->log, 0, "close connection"); + +#if (NGX_STAT_STUB) + (void) ngx_atomic_fetch_add(ngx_stat_active, -1); +#endif + + pool = c->pool; + ngx_close_connection(c); + ngx_destroy_pool(pool); +} + + +static void +ngx_rtmp_close_session_handler(ngx_event_t *e) +{ + ngx_rtmp_session_t *s; + ngx_connection_t *c; + ngx_rtmp_core_srv_conf_t *cscf; + + s = e->data; + c = s->connection; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, c->log, 0, "close session"); + + ngx_rtmp_fire_event(s, NGX_RTMP_DISCONNECT, NULL, NULL); + + if (s->ping_evt.timer_set) { + ngx_del_timer(&s->ping_evt); + } + + if (s->in_old_pool) { + ngx_destroy_pool(s->in_old_pool); + } + + if (s->in_pool) { + ngx_destroy_pool(s->in_pool); + } + + ngx_rtmp_free_handshake_buffers(s); + + while (s->out_pos != s->out_last) { + ngx_rtmp_free_shared_chain(cscf, s->out[s->out_pos]); + + s->out_pos++; + s->out_pos %= s->out_queue; + } + + if (s->in_streams_pool) { + ngx_destroy_pool(s->in_streams_pool); + } + + if (s->out_pool) { + ngx_destroy_pool(s->out_pool); + } + + ngx_rtmp_close_connection(c); +} + + +void +ngx_rtmp_finalize_session(ngx_rtmp_session_t *s) +{ + ngx_event_t *e; + ngx_connection_t *c; + + c = s->connection; + if (c->destroyed) { + return; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, c->log, 0, "finalize session"); + + c->destroyed = 1; + e = &s->close; + e->data = s; + e->handler = ngx_rtmp_close_session_handler; + e->log = c->log; + + ngx_post_event(e, &ngx_posted_events); +} + + +static void +ngx_rtmp_process_unix_socket(ngx_rtmp_connection_t *rconn) +{ + ngx_uint_t i; + ngx_rtmp_port_t *port; + struct sockaddr_in *sin; + ngx_rtmp_in_addr_t *addr; +#if (NGX_HAVE_INET6) + struct sockaddr_in6 *sin6; + ngx_rtmp_in6_addr_t *addr6; +#endif + ngx_listening_t *ls; + + ls = ngx_cycle->listening.elts; + for (i = 0; i < ngx_cycle->listening.nelts; ++i, ++ls) { + if (ls->handler == ngx_rtmp_init_connection) { + break; + } + } + + port = ls->servers; + + if (port->naddrs > 1) { + switch (ls->sockaddr->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + sin6 = (struct sockaddr_in6 *) ls->sockaddr; + + addr6 = port->addrs; + + /* the last address is "*" */ + + for (i = 0; i < port->naddrs - 1; i++) { + if (ngx_memcmp(&addr6[i].addr6, &sin6->sin6_addr, 16) == 0) { + break; + } + } + + rconn->addr_conf = &addr6[i].conf; + + break; +#endif + + default: /* AF_INET */ + sin = (struct sockaddr_in *) ls->sockaddr; + + addr = port->addrs; + + /* the last address is "*" */ + + for (i = 0; i < port->naddrs - 1; i++) { + if (addr[i].addr == sin->sin_addr.s_addr) { + break; + } + } + + rconn->addr_conf = &addr[i].conf; + } + } else { + switch (ls->sockaddr->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + addr6 = port->addrs; + rconn->addr_conf = &addr6[0].conf; + break; +#endif + + default: /* AF_INET */ + addr = port->addrs; + rconn->addr_conf = &addr[0].conf; + } + } +} + diff --git a/ngx_http_flv_module/ngx_rtmp_limit_module.c b/ngx_http_flv_module/ngx_rtmp_limit_module.c new file mode 100644 index 0000000..5e7c46b --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_limit_module.c @@ -0,0 +1,205 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp.h" + + +typedef struct { + ngx_int_t max_conn; + ngx_shm_zone_t *shm_zone; +} ngx_rtmp_limit_main_conf_t; + + +static ngx_str_t shm_name = ngx_string("rtmp_limit"); + + +static ngx_int_t ngx_rtmp_limit_postconfiguration(ngx_conf_t *cf); +static void *ngx_rtmp_limit_create_main_conf(ngx_conf_t *cf); + + +static ngx_command_t ngx_rtmp_limit_commands[] = { + + { ngx_string("max_connections"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_MAIN_CONF_OFFSET, + offsetof(ngx_rtmp_limit_main_conf_t, max_conn), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_limit_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_limit_postconfiguration, /* postconfiguration */ + ngx_rtmp_limit_create_main_conf, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + NULL, /* create app configuration */ + NULL /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_limit_module = { + NGX_MODULE_V1, + &ngx_rtmp_limit_module_ctx, /* module context */ + ngx_rtmp_limit_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static void * +ngx_rtmp_limit_create_main_conf(ngx_conf_t *cf) +{ + ngx_rtmp_limit_main_conf_t *lmcf; + + lmcf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_limit_main_conf_t)); + if (lmcf == NULL) { + return NULL; + } + + lmcf->max_conn = NGX_CONF_UNSET; + + return lmcf; +} + + +static ngx_int_t +ngx_rtmp_limit_connect(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_limit_main_conf_t *lmcf; + ngx_slab_pool_t *shpool; + ngx_shm_zone_t *shm_zone; + uint32_t *nconn, n; + ngx_int_t rc; + + lmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_limit_module); + if (lmcf->max_conn == NGX_CONF_UNSET) { + return NGX_OK; + } + + shm_zone = lmcf->shm_zone; + shpool = (ngx_slab_pool_t *) shm_zone->shm.addr; + nconn = shm_zone->data; + + ngx_shmtx_lock(&shpool->mutex); + n = ++*nconn; + ngx_shmtx_unlock(&shpool->mutex); + + rc = n > (ngx_uint_t) lmcf->max_conn ? NGX_ERROR : NGX_OK; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "limit: inc connection counter: %uD", n); + + if (rc != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "limit: too many connections: %uD > %i", + n, lmcf->max_conn); + } + + return rc; +} + + +static ngx_int_t +ngx_rtmp_limit_disconnect(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_limit_main_conf_t *lmcf; + ngx_slab_pool_t *shpool; + ngx_shm_zone_t *shm_zone; + uint32_t *nconn, n; + + lmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_limit_module); + if (lmcf->max_conn == NGX_CONF_UNSET) { + return NGX_OK; + } + + shm_zone = lmcf->shm_zone; + shpool = (ngx_slab_pool_t *) shm_zone->shm.addr; + nconn = shm_zone->data; + + ngx_shmtx_lock(&shpool->mutex); + n = --*nconn; + ngx_shmtx_unlock(&shpool->mutex); + + (void) n; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "limit: dec connection counter: %uD", n); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_limit_shm_init(ngx_shm_zone_t *shm_zone, void *data) +{ + ngx_slab_pool_t *shpool; + uint32_t *nconn; + + if (data) { + shm_zone->data = data; + return NGX_OK; + } + + shpool = (ngx_slab_pool_t *) shm_zone->shm.addr; + + nconn = ngx_slab_alloc(shpool, 4); + if (nconn == NULL) { + return NGX_ERROR; + } + + *nconn = 0; + + shm_zone->data = nconn; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_limit_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_limit_main_conf_t *lmcf; + ngx_rtmp_handler_pt *h; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + h = ngx_array_push(&cmcf->events[NGX_RTMP_CONNECT]); + *h = ngx_rtmp_limit_connect; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_DISCONNECT]); + *h = ngx_rtmp_limit_disconnect; + + lmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_limit_module); + if (lmcf->max_conn == NGX_CONF_UNSET) { + return NGX_OK; + } + + lmcf->shm_zone = ngx_shared_memory_add(cf, &shm_name, ngx_pagesize * 2, + &ngx_rtmp_limit_module); + if (lmcf->shm_zone == NULL) { + return NGX_ERROR; + } + + lmcf->shm_zone->init = ngx_rtmp_limit_shm_init; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_live_module.c b/ngx_http_flv_module/ngx_rtmp_live_module.c new file mode 100644 index 0000000..76a9800 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_live_module.c @@ -0,0 +1,1682 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_codec_module.h" +#include "ngx_http_flv_live_module.h" + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_close_stream_pt next_close_stream; +static ngx_rtmp_pause_pt next_pause; +static ngx_rtmp_stream_begin_pt next_stream_begin; +static ngx_rtmp_stream_eof_pt next_stream_eof; + + +static ngx_int_t ngx_rtmp_live_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_live_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_live_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +static char *ngx_rtmp_live_set_msec_slot(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static void ngx_rtmp_live_start(ngx_rtmp_session_t *s); +static void ngx_rtmp_live_stop(ngx_rtmp_session_t *s); + +static ngx_int_t ngx_rtmp_live_send_message(ngx_rtmp_session_t *s, + ngx_chain_t *in, ngx_uint_t priority); +static ngx_chain_t *ngx_rtmp_live_meta_message(ngx_rtmp_session_t *s, + ngx_chain_t *in); +static ngx_chain_t *ngx_rtmp_live_append_message(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_rtmp_header_t *lh, ngx_chain_t *in); +static void ngx_rtmp_live_free_message(ngx_rtmp_session_t *s, ngx_chain_t *in); + + +#define ACTION_VAR_LEN 128 +#define STREAM_VAR_LEN 1024 + + +ngx_rtmp_live_proc_handler_t ngx_rtmp_live_proc_handler = { + NULL, + NULL, + NULL, + NULL, + ngx_rtmp_live_send_message, + ngx_rtmp_live_meta_message, + ngx_rtmp_live_append_message, + ngx_rtmp_live_free_message +}; + + +extern ngx_rtmp_live_proc_handler_t *ngx_rtmp_live_proc_handlers + [NGX_RTMP_PROTOCOL_HTTP + 1]; +extern ngx_module_t ngx_http_flv_live_module; + +static ngx_command_t ngx_rtmp_live_commands[] = { + + { ngx_string("live"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, live), + NULL }, + + { ngx_string("stream_buckets"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_num_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, nbuckets), + NULL }, + + { ngx_string("buffer"), + 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_live_app_conf_t, buflen), + NULL }, + + { ngx_string("sync"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_live_set_msec_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, sync), + NULL }, + + { ngx_string("interleave"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, interleave), + NULL }, + + { ngx_string("wait_key"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, wait_key), + NULL }, + + { ngx_string("wait_video"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, wait_video), + NULL }, + + { ngx_string("publish_notify"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, publish_notify), + NULL }, + + { ngx_string("play_restart"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, play_restart), + NULL }, + + { ngx_string("idle_streams"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, idle_streams), + NULL }, + + { ngx_string("drop_idle_publisher"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_live_set_msec_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_live_app_conf_t, idle_timeout), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_live_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_live_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + ngx_rtmp_live_create_app_conf, /* create app configuration */ + ngx_rtmp_live_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_live_module = { + NGX_MODULE_V1, + &ngx_rtmp_live_module_ctx, /* module context */ + ngx_rtmp_live_commands, /* module directives */ + NGX_RTMP_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 +}; + + +ngx_int_t +ngx_rtmp_live_send_message(ngx_rtmp_session_t *s, + ngx_chain_t *in, ngx_uint_t priority) +{ + return ngx_rtmp_send_message(s, in, priority); +} + + +ngx_chain_t * +ngx_rtmp_live_meta_message(ngx_rtmp_session_t *s, ngx_chain_t *in) +{ + ngx_rtmp_core_srv_conf_t *cscf; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return NULL; + } + + return ngx_rtmp_append_shared_bufs(cscf, NULL, in); +} + + +ngx_chain_t * +ngx_rtmp_live_append_message(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_rtmp_header_t *lh, ngx_chain_t *in) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_chain_t *pkt; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return NULL; + } + + pkt = ngx_rtmp_append_shared_bufs(cscf, NULL, in); + if (pkt != NULL) { + ngx_rtmp_prepare_message(s, h, lh, pkt); + } + + return pkt; +} + + +void +ngx_rtmp_live_free_message(ngx_rtmp_session_t *s, ngx_chain_t *in) +{ + ngx_rtmp_core_srv_conf_t *cscf; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + if (cscf == NULL) { + return; + } + + ngx_rtmp_free_shared_chain(cscf, in); +} + + +static void * +ngx_rtmp_live_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_live_app_conf_t *lacf; + + lacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_live_app_conf_t)); + if (lacf == NULL) { + return NULL; + } + + lacf->live = NGX_CONF_UNSET; + lacf->nbuckets = NGX_CONF_UNSET; + lacf->buflen = NGX_CONF_UNSET_MSEC; + lacf->sync = NGX_CONF_UNSET_MSEC; + lacf->idle_timeout = NGX_CONF_UNSET_MSEC; + lacf->interleave = NGX_CONF_UNSET; + lacf->wait_key = NGX_CONF_UNSET; + lacf->wait_video = NGX_CONF_UNSET; + lacf->publish_notify = NGX_CONF_UNSET; + lacf->play_restart = NGX_CONF_UNSET; + lacf->idle_streams = NGX_CONF_UNSET; + + return lacf; +} + + +static void +ngx_rtmp_live_free_pool_cleanup(void *data) +{ + ngx_rtmp_live_app_conf_t *lacf = data; + + if (lacf->pool != NULL) { + ngx_destroy_pool(lacf->pool); + lacf->pool = NULL; + } +} + + +static char * +ngx_rtmp_live_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_pool_cleanup_t *cln; + ngx_rtmp_live_app_conf_t *prev = parent; + ngx_rtmp_live_app_conf_t *conf = child; + + ngx_conf_merge_value(conf->live, prev->live, 0); + ngx_conf_merge_value(conf->nbuckets, prev->nbuckets, 1024); + ngx_conf_merge_msec_value(conf->buflen, prev->buflen, 0); + ngx_conf_merge_msec_value(conf->sync, prev->sync, 300); + ngx_conf_merge_msec_value(conf->idle_timeout, prev->idle_timeout, 0); + ngx_conf_merge_value(conf->interleave, prev->interleave, 0); + ngx_conf_merge_value(conf->wait_key, prev->wait_key, 1); + ngx_conf_merge_value(conf->wait_video, prev->wait_video, 0); + ngx_conf_merge_value(conf->publish_notify, prev->publish_notify, 0); + ngx_conf_merge_value(conf->play_restart, prev->play_restart, 0); + ngx_conf_merge_value(conf->idle_streams, prev->idle_streams, 1); + + conf->pool = ngx_create_pool(4096, &cf->cycle->new_log); + if (conf->pool == NULL) { + return NGX_CONF_ERROR; + } + + cln = ngx_pool_cleanup_add(cf->pool, 0); + if (cln == NULL) { + return NGX_CONF_ERROR; + } + + cln->handler = ngx_rtmp_live_free_pool_cleanup; + cln->data = conf; + + conf->streams = ngx_pcalloc(cf->pool, + sizeof(ngx_rtmp_live_stream_t *) * conf->nbuckets); + if (conf->streams == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_live_set_msec_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + char *p = conf; + ngx_str_t *value; + ngx_msec_t *msp; + + msp = (ngx_msec_t *) (p + cmd->offset); + + value = cf->args->elts; + + if (value[1].len == sizeof("off") - 1 && + ngx_strncasecmp(value[1].data, (u_char *) "off", value[1].len) == 0) + { + *msp = 0; + return NGX_CONF_OK; + } + + return ngx_conf_set_msec_slot(cf, cmd, conf); +} + + +ngx_rtmp_live_stream_t ** +ngx_rtmp_live_get_stream(ngx_rtmp_session_t *s, u_char *name, int create) +{ + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_live_stream_t **stream; + size_t len; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + return NULL; + } + + len = ngx_strlen(name); + stream = &lacf->streams[ngx_hash_key(name, len) % lacf->nbuckets]; + + for (; *stream; stream = &(*stream)->next) { + if (ngx_strcmp(name, (*stream)->name) == 0) { + return stream; + } + } + + if (!create) { + return NULL; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: create stream '%s'", name); + + if (lacf->free_streams) { + *stream = lacf->free_streams; + lacf->free_streams = lacf->free_streams->next; + } else { + *stream = ngx_palloc(lacf->pool, sizeof(ngx_rtmp_live_stream_t)); + if (*stream == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: failed to allocate for stream"); + return NULL; + } + } + ngx_memzero(*stream, sizeof(ngx_rtmp_live_stream_t)); + ngx_memcpy((*stream)->name, name, + ngx_min(sizeof((*stream)->name) - 1, len)); + (*stream)->epoch = ngx_current_msec; + + return stream; +} + + +static void +ngx_rtmp_live_idle(ngx_event_t *pev) +{ + ngx_connection_t *c; + ngx_rtmp_session_t *s; + + c = pev->data; + s = c->data; + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: drop idle publisher"); + + ngx_rtmp_finalize_session(s); +} + + +static void +ngx_rtmp_live_set_status(ngx_rtmp_session_t *s, ngx_chain_t *control, + ngx_chain_t **status, size_t nstatus, + unsigned active) +{ + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_live_ctx_t *ctx, *pctx; + ngx_chain_t **cl; + ngx_event_t *e; + size_t n; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: set active=%ui", active); + + if (ctx->active == active) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: unchanged active=%ui", active); + return; + } + + ctx->active = active; + + if (ctx->publishing) { + + /* publisher */ + + if (lacf->idle_timeout) { + e = &ctx->idle_evt; + + if (active && !ctx->idle_evt.timer_set) { + e->data = s->connection; + e->log = s->connection->log; + e->handler = ngx_rtmp_live_idle; + + ngx_add_timer(e, lacf->idle_timeout); + + } else if (!active && ctx->idle_evt.timer_set) { + ngx_del_timer(e); + } + } + + ctx->stream->active = active; + + for (pctx = ctx->stream->ctx; pctx; pctx = pctx->next) { + if (pctx->publishing == 0) { + pctx->session->publisher = s; + + if (pctx->protocol == NGX_RTMP_PROTOCOL_HTTP) { + ngx_http_flv_live_set_status(pctx->session, active); + } else { + ngx_rtmp_live_set_status(pctx->session, control, status, + nstatus, active); + } + } + } + + return; + } + + /* subscriber */ + + if (ctx->protocol == NGX_RTMP_PROTOCOL_HTTP) { + ngx_http_flv_live_set_status(s, active); + } else { + if (control && ngx_rtmp_send_message(s, control, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + return; + } + + if (!ctx->silent) { + cl = status; + + for (n = 0; n < nstatus; ++n, ++cl) { + if (*cl && ngx_rtmp_send_message(s, *cl, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + return; + } + } + } + + ctx->cs[0].active = 0; + ctx->cs[0].dropped = 0; + + ctx->cs[1].active = 0; + ctx->cs[1].dropped = 0; + } +} + + +static void +ngx_rtmp_live_start(ngx_rtmp_session_t *s) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_live_app_conf_t *lacf; + ngx_chain_t *control; + ngx_chain_t *status[3]; + size_t n, nstatus; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + + control = ngx_rtmp_create_stream_begin(s, NGX_RTMP_MSID); + + nstatus = 0; + + if (lacf->play_restart) { + status[nstatus++] = ngx_rtmp_create_status(s, "NetStream.Play.Start", + "status", "Start live"); + status[nstatus++] = ngx_rtmp_create_sample_access(s); + } + + if (lacf->publish_notify) { + status[nstatus++] = ngx_rtmp_create_status(s, + "NetStream.Play.PublishNotify", + "status", "Start publishing"); + } + + ngx_rtmp_live_set_status(s, control, status, nstatus, 1); + + if (control) { + ngx_rtmp_free_shared_chain(cscf, control); + } + + for (n = 0; n < nstatus; ++n) { + ngx_rtmp_free_shared_chain(cscf, status[n]); + } +} + + +static void +ngx_rtmp_live_stop(ngx_rtmp_session_t *s) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_live_app_conf_t *lacf; + ngx_chain_t *control; + ngx_chain_t *status[3]; + size_t n, nstatus; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + + control = ngx_rtmp_create_stream_eof(s, NGX_RTMP_MSID); + + nstatus = 0; + + if (lacf->play_restart) { + status[nstatus++] = ngx_rtmp_create_status(s, "NetStream.Play.Stop", + "status", "Stop live"); + } + + if (lacf->publish_notify) { + status[nstatus++] = ngx_rtmp_create_status(s, + "NetStream.Play.UnpublishNotify", + "status", "Stop publishing"); + } + + ngx_rtmp_live_set_status(s, control, status, nstatus, 0); + + if (control) { + ngx_rtmp_free_shared_chain(cscf, control); + } + + for (n = 0; n < nstatus; ++n) { + ngx_rtmp_free_shared_chain(cscf, status[n]); + } +} + + +static ngx_int_t +ngx_rtmp_live_stream_begin(ngx_rtmp_session_t *s, ngx_rtmp_stream_begin_t *v) +{ + ngx_rtmp_live_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + + if (ctx == NULL || ctx->stream == NULL || !ctx->publishing) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: stream_begin"); + + ngx_rtmp_live_start(s); + +next: + return next_stream_begin(s, v); +} + + +static ngx_int_t +ngx_rtmp_live_stream_eof(ngx_rtmp_session_t *s, ngx_rtmp_stream_eof_t *v) +{ + ngx_rtmp_live_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + + if (ctx == NULL || ctx->stream == NULL || !ctx->publishing) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: stream_eof"); + + ngx_rtmp_live_stop(s); + +next: + return next_stream_eof(s, v); +} + + +static void +ngx_rtmp_live_join(ngx_rtmp_session_t *s, u_char *name, unsigned publisher) +{ + ngx_rtmp_live_ctx_t *ctx; + ngx_rtmp_live_stream_t **stream; + ngx_rtmp_live_app_conf_t *lacf; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + return; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + 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)); + if (ctx == NULL) { + if (publisher) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: failed to allocate for publish ctx"); + + ngx_rtmp_send_status(s, "NetStream.Publish.Failed", "error", + "Failed to allocate memory"); + } else { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: failed to allocate for play ctx"); + + ngx_rtmp_send_status(s, "NetStream.Play.Failed", "error", + "Failed to allocate memory"); + } + + ngx_rtmp_finalize_session(s); + + return; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_live_module); + } + + ngx_memzero(ctx, sizeof(*ctx)); + + ctx->session = s; + ctx->protocol = NGX_RTMP_PROTOCOL_RTMP; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: join '%s'", name); + + stream = ngx_rtmp_live_get_stream(s, name, publisher || lacf->idle_streams); + + if (stream == NULL || + !(publisher || (*stream)->publishing || lacf->idle_streams)) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: stream not found"); + + ngx_rtmp_send_status(s, "NetStream.Play.StreamNotFound", "error", + "No such stream"); + + ngx_rtmp_finalize_session(s); + + return; + } + + if (publisher) { + if ((*stream)->publishing) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: already publishing"); + + ngx_rtmp_send_status(s, "NetStream.Publish.BadName", "error", + "Already publishing"); + + return; + } + + (*stream)->publishing = 1; + (*stream)->pub_ctx = ctx; + } + + ctx->stream = *stream; + ctx->publishing = publisher; + ctx->next = (*stream)->ctx; + + (*stream)->ctx = ctx; + + if (lacf->buflen) { + s->out_buffer = 1; + } + + ctx->cs[0].csid = NGX_RTMP_CSID_VIDEO; + ctx->cs[1].csid = NGX_RTMP_CSID_AUDIO; + + if (!ctx->publishing && ctx->stream->active) { + ngx_rtmp_live_start(s); + } +} + + +static ngx_int_t +ngx_rtmp_live_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_session_t *ss; + ngx_rtmp_live_ctx_t *ctx, **cctx, *pctx; + ngx_rtmp_live_stream_t **stream; + ngx_rtmp_live_app_conf_t *lacf; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + goto next; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL) { + goto next; + } + + if (ctx->stream == NULL) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: not joined"); + goto next; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: leave '%s'", ctx->stream->name); + + if (ctx->stream->publishing && ctx->publishing) { + ctx->stream->publishing = 0; + } + + if (ctx->publishing) { + ctx->stream->pub_ctx = NULL; + } + + for (cctx = &ctx->stream->ctx; *cctx; cctx = &(*cctx)->next) { + if (*cctx == ctx) { + *cctx = ctx->next; + break; + } + } + + if (ctx->publishing || ctx->stream->active) { + ngx_rtmp_live_stop(s); + } + + if (ctx->publishing) { + ngx_rtmp_send_status(s, "NetStream.Unpublish.Success", + "status", "Stop publishing"); + if (!lacf->idle_streams) { + for (pctx = ctx->stream->ctx; pctx; pctx = pctx->next) { + if (pctx->publishing == 0) { + ss = pctx->session; + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: no publisher"); + ngx_rtmp_finalize_session(ss); + } + } + } + } + + if (ctx->stream->ctx || ctx->stream->pub_ctx) { + ctx->stream = NULL; + goto next; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: delete empty stream '%s'", + ctx->stream->name); + + stream = ngx_rtmp_live_get_stream(s, ctx->stream->name, 0); + if (stream == NULL) { + goto next; + } + *stream = (*stream)->next; + + ctx->stream->next = lacf->free_streams; + lacf->free_streams = ctx->stream; + ctx->stream = NULL; + + if (!ctx->silent && !ctx->publishing && !lacf->play_restart) { + ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stop live"); + } + +next: + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_live_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) +{ + ngx_rtmp_live_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + + if (ctx == NULL || ctx->stream == NULL) { + goto next; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: pause=%i timestamp=%f", + (ngx_int_t) v->pause, v->position); + + if (v->pause) { + if (ngx_rtmp_send_status(s, "NetStream.Pause.Notify", "status", + "Paused live") + != NGX_OK) + { + return NGX_ERROR; + } + + ctx->paused = 1; + + ngx_rtmp_live_stop(s); + + } else { + if (ngx_rtmp_send_status(s, "NetStream.Unpause.Notify", "status", + "Unpaused live") + != NGX_OK) + { + return NGX_ERROR; + } + + ctx->paused = 0; + + ngx_rtmp_live_start(s); + } + +next: + return next_pause(s, v); +} + + +static ngx_int_t +ngx_rtmp_live_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_live_proc_handler_t *handler; + ngx_rtmp_live_ctx_t *ctx, *pctx; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_chain_t *header, *coheader; + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_session_t *ss; + ngx_rtmp_header_t ch, lh, clh; + ngx_int_t rc, mandatory, i; + ngx_uint_t prio; + ngx_uint_t peers; + ngx_uint_t meta_version; + ngx_uint_t csidx; + uint32_t delta; + ngx_rtmp_live_chunk_stream_t *cs; + ngx_http_request_t *r; + ngx_http_flv_live_ctx_t *hctx; +#ifdef NGX_DEBUG + const char *type_s; + + type_s = (h->type == NGX_RTMP_MSG_VIDEO ? "video" : "audio"); +#endif + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + return NGX_ERROR; + } + + if (!lacf->live || in == NULL || in->buf == NULL) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL || ctx->stream == NULL) { + return NGX_OK; + } + + if (!ctx->publishing) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: %s from non-publisher", type_s); + return NGX_OK; + } + + if (!ctx->stream->active) { + ngx_rtmp_live_start(s); + } + + if (ctx->idle_evt.timer_set) { + ngx_add_timer(&ctx->idle_evt, lacf->idle_timeout); + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: %s packet timestamp=%uD", + type_s, h->timestamp); + + s->current_time = h->timestamp; + + peers = 0; + header = NULL; + coheader = NULL; + meta_version = 0; + mandatory = 0; + + for (i = 0; i <= NGX_RTMP_PROTOCOL_HTTP; i++) { + handler = ngx_rtmp_live_proc_handlers[i]; + + handler->meta = NULL; + handler->rpkt = NULL; + handler->apkt = NULL; + handler->acopkt = NULL; + } + + prio = (h->type == NGX_RTMP_MSG_VIDEO ? + ngx_rtmp_get_video_frame_type(in) : 0); + + csidx = !(lacf->interleave || h->type == NGX_RTMP_MSG_VIDEO); + + cs = &ctx->cs[csidx]; + + ngx_memzero(&ch, sizeof(ch)); + + ch.timestamp = h->timestamp; + ch.msid = NGX_RTMP_MSID; + ch.csid = cs->csid; + ch.type = h->type; + + lh = ch; + + if (cs->active) { + lh.timestamp = cs->timestamp; + } + + clh = lh; + clh.type = (h->type == NGX_RTMP_MSG_AUDIO ? NGX_RTMP_MSG_VIDEO : + NGX_RTMP_MSG_AUDIO); + + cs->active = 1; + cs->timestamp = ch.timestamp; + + delta = ch.timestamp - lh.timestamp; +/* + if (delta >> 31) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: clipping non-monotonical timestamp %uD->%uD", + lh.timestamp, ch.timestamp); + + delta = 0; + + ch.timestamp = lh.timestamp; + } +*/ + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + + if (codec_ctx) { + + if (h->type == NGX_RTMP_MSG_AUDIO) { + header = codec_ctx->aac_header; + + if (lacf->interleave) { + coheader = codec_ctx->avc_header; + } + + if (codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC && + ngx_rtmp_is_codec_header(in)) + { + prio = 0; + mandatory = 1; + } + + } else { + header = codec_ctx->avc_header; + + if (lacf->interleave) { + coheader = codec_ctx->aac_header; + } + + if (codec_ctx->video_codec_id == NGX_RTMP_VIDEO_H264 && + ngx_rtmp_is_codec_header(in)) + { + prio = 0; + mandatory = 1; + } + } + + if (codec_ctx->meta) { + meta_version = codec_ctx->meta_version; + } + } + + /* broadcast to all subscribers */ + + for (pctx = ctx->stream->ctx; pctx; pctx = pctx->next) { + if (pctx == ctx || pctx->paused) { + continue; + } + + ss = pctx->session; + cs = &pctx->cs[csidx]; + + handler = ngx_rtmp_live_proc_handlers[pctx->protocol]; + + /* send metadata */ + + if (codec_ctx) { + if (pctx->protocol == NGX_RTMP_PROTOCOL_HTTP) { + r = ss->data; + if (r == NULL + || (r->connection && r->connection->destroyed)) + { + continue; + } + + hctx = ngx_http_get_module_ctx(r, ngx_http_flv_live_module); + if (!hctx->header_sent) { + hctx->header_sent = 1; + ngx_http_flv_live_send_header(ss); + } + } + } + + if (handler->meta == NULL && meta_version != pctx->meta_version) { + if (codec_ctx->meta) { + handler->meta = handler->meta_message_pt(ss, codec_ctx->meta); + if (handler->meta == NULL) { + continue; + } + } else { + ngx_log_error(NGX_LOG_WARN, ss->connection->log, 0, + "live: no meta"); + + pctx->meta_version = meta_version; + } + } + + if (handler->meta && meta_version != pctx->meta_version) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: meta"); + + if (handler->send_message_pt(ss, handler->meta, 0) == NGX_OK) { + pctx->meta_version = meta_version; + } + } + + /* sync stream */ + + if (cs->active && (lacf->sync && cs->dropped > lacf->sync)) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: sync %s dropped=%uD", type_s, cs->dropped); + + cs->active = 0; + cs->dropped = 0; + } + + /* absolute packet */ + + if (!cs->active) { + + if (mandatory) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: skipping header"); + continue; + } + + if (codec_ctx->video_codec_id) { + if (lacf->wait_video && h->type == NGX_RTMP_MSG_AUDIO && + !pctx->cs[0].active) + { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: waiting for video"); + continue; + } + + if (lacf->wait_key && prio != NGX_RTMP_VIDEO_KEY_FRAME && + (lacf->interleave || h->type == NGX_RTMP_MSG_VIDEO)) + { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: skip non-key"); + continue; + } + } + + if (header || coheader) { + + /* send absolute codec header */ + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: abs %s header timestamp=%uD", + type_s, lh.timestamp); + + if (header) { + if (handler->apkt == NULL) { + handler->apkt = handler->append_message_pt(ss, &lh, + NULL, header); + if (handler->apkt == NULL) { + continue; + } + } + + rc = handler->send_message_pt(ss, handler->apkt, 0); + if (rc != NGX_OK) { + continue; + } + } + + if (coheader) { + if (handler->acopkt == NULL) { + handler->acopkt = handler->append_message_pt(ss, &clh, + NULL, coheader); + if (handler->acopkt == NULL) { + continue; + } + } + + rc = handler->send_message_pt(ss, handler->acopkt, 0); + if (rc != NGX_OK) { + continue; + } + + } + + cs->timestamp = lh.timestamp; + cs->active = 1; + ss->current_time = cs->timestamp; + + } else { + + /* send absolute packet */ + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: abs %s packet timestamp=%uD", + type_s, ch.timestamp); + + if (handler->apkt == NULL) { + handler->apkt = handler->append_message_pt(ss, &ch, + NULL, in); + if (handler->apkt == NULL) { + continue; + } + } + + rc = handler->send_message_pt(ss, handler->apkt, prio); + if (rc != NGX_OK) { + continue; + } + + cs->timestamp = ch.timestamp; + cs->active = 1; + ss->current_time = cs->timestamp; + + ++peers; + + continue; + } + } + + if (handler->rpkt == NULL) { + handler->rpkt = handler->append_message_pt(ss, &ch, &lh, in); + if (handler->rpkt == NULL) { + continue; + } + } + + /* send relative packet */ + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: rel %s packet delta=%uD", + type_s, delta); + + if (handler->send_message_pt(ss, handler->rpkt, prio) != NGX_OK) { + ++pctx->ndropped; + + cs->dropped += delta; + + if (mandatory) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, ss->connection->log, 0, + "live: mandatory packet failed"); + ngx_rtmp_finalize_session(ss); + } + + continue; + } + + cs->timestamp += delta; + ++peers; + ss->current_time = cs->timestamp; + } + + for (i = 0; i <= NGX_RTMP_PROTOCOL_HTTP; i++) { + handler = ngx_rtmp_live_proc_handlers[i]; + + if (handler->meta) { + handler->free_message_pt(s, handler->meta); + handler->meta = NULL; + } + + if (handler->rpkt) { + handler->free_message_pt(s, handler->rpkt); + handler->rpkt = NULL; + } + + if (handler->apkt) { + handler->free_message_pt(s, handler->apkt); + handler->apkt = NULL; + } + + if (handler->acopkt) { + handler->free_message_pt(s, handler->acopkt); + handler->acopkt = NULL; + } + } + + ngx_rtmp_update_bandwidth(&ctx->stream->bw_in, h->mlen); + ngx_rtmp_update_bandwidth(&ctx->stream->bw_out, h->mlen * peers); + + ngx_rtmp_update_bandwidth(h->type == NGX_RTMP_MSG_AUDIO ? + &ctx->stream->bw_in_audio : + &ctx->stream->bw_in_video, + h->mlen); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_live_data(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in, ngx_rtmp_amf_elt_t *out_elts, ngx_uint_t out_elts_size) +{ + ngx_rtmp_live_proc_handler_t *handler; + ngx_rtmp_live_ctx_t *ctx, *pctx; + ngx_chain_t *data, *rpkt; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_session_t *ss; + ngx_rtmp_header_t ch; + ngx_int_t rc; + ngx_int_t csidx; + ngx_uint_t prio; + ngx_uint_t peers; + uint32_t delta; + ngx_rtmp_live_chunk_stream_t *cs; + ngx_http_request_t *r; +#ifdef NGX_DEBUG + u_char *msg_type; + + msg_type = (u_char *)out_elts[0].data; +#endif + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + return NGX_ERROR; + } + + if (!lacf->live || in == NULL || in->buf == NULL) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL || ctx->stream == NULL) { + return NGX_OK; + } + + if (!ctx->publishing) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: %s from non-publisher", msg_type); + return NGX_OK; + } + + /* drop the data packet if the stream is not active */ + if (!ctx->stream->active) { + return NGX_OK; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: %s packet timestamp=%uD", + msg_type, h->timestamp); + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + csidx = !(lacf->interleave || h->type == NGX_RTMP_MSG_VIDEO); + + cs = &ctx->cs[csidx]; + cs->active = 1; + + peers = 0; + prio = 0; + data = NULL; + + rc = ngx_rtmp_append_amf(s, &data, NULL, out_elts, out_elts_size); + if (rc != NGX_OK) { + if (data) { + ngx_rtmp_free_shared_chain(cscf, data); + } + + return NGX_ERROR; + } + + ngx_memzero(&ch, sizeof(ch)); + ch.timestamp = h->timestamp; + ch.msid = NGX_RTMP_MSID; + ch.csid = h->csid; + ch.type = NGX_RTMP_MSG_AMF_META; + + delta = ch.timestamp - cs->timestamp; + + rpkt = ngx_rtmp_append_shared_bufs(cscf, data, in); + + for (pctx = ctx->stream->ctx; pctx; pctx = pctx->next) { + if (pctx == ctx || pctx->paused) { + continue; + } + + ss = pctx->session; + handler = ngx_rtmp_live_proc_handlers[pctx->protocol]; + if (pctx->protocol == NGX_RTMP_PROTOCOL_HTTP) { + r = ss->data; + if (r == NULL || (r->connection && r->connection->destroyed)) { + continue; + } + + handler->meta = handler->append_message_pt(ss, &ch, NULL, rpkt); + if (handler->meta == NULL) { + continue; + } + + if (handler->send_message_pt(ss, handler->meta, 0) != NGX_OK) { + ++pctx->ndropped; + cs->dropped += delta; + handler->free_message_pt(ss, handler->meta); + handler->meta = NULL; + continue; + } + + handler->free_message_pt(ss, handler->meta); + handler->meta = NULL; + } else { + ngx_rtmp_prepare_message(s, &ch, NULL, rpkt); + if (ngx_rtmp_send_message(ss, rpkt, prio) != NGX_OK) { + ++pctx->ndropped; + cs->dropped += delta; + continue; + } + } + + cs->timestamp += delta; + ++peers; + ss->current_time = cs->timestamp; + } + + if (data) { + ngx_rtmp_free_shared_chain(cscf, data); + } + + if (rpkt) { + ngx_rtmp_free_shared_chain(cscf, rpkt); + } + + ngx_rtmp_update_bandwidth(&ctx->stream->bw_in, h->mlen); + ngx_rtmp_update_bandwidth(&ctx->stream->bw_out, h->mlen * peers); + ngx_rtmp_update_bandwidth(&ctx->stream->bw_in_data, h->mlen); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_live_on_cue_point(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "onCuePoint", 0 } + }; + + return ngx_rtmp_live_data(s, h, in, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +static ngx_int_t +ngx_rtmp_live_on_text_data(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "onTextData", 0 } + }; + + return ngx_rtmp_live_data(s, h, in, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +static ngx_int_t +ngx_rtmp_live_on_fi(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "onFi", 0 } + }; + + return ngx_rtmp_live_data(s, h, in, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +static ngx_int_t +ngx_rtmp_live_on_fcpublish(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_live_app_conf_t *lacf; + + static struct { + double trans; + u_char action[ACTION_VAR_LEN]; + u_char stream[STREAM_VAR_LEN]; + } v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + &v.stream, sizeof(v.stream) }, + }; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: FCPublish - no live config!"); + return NGX_ERROR; + } + + if (!lacf->live || in == NULL || in->buf == NULL) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: FCPublish - no live or no buffer!"); + return NGX_OK; + } + + ngx_memzero(&v, sizeof(v)); + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: FCPublish - error receiving amf data"); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: onFCPublish: stream='%s'", v.stream); + + return ngx_rtmp_send_fcpublish(s, v.stream); +} + + +static ngx_int_t +ngx_rtmp_live_on_fcunpublish(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_live_app_conf_t *lacf; + + static struct { + double trans; + u_char action[ACTION_VAR_LEN]; + u_char stream[STREAM_VAR_LEN]; + } v; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + &v.stream, sizeof(v.stream) }, + }; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + if (lacf == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: FCUnpublish - no live config!"); + return NGX_ERROR; + } + + if (!lacf->live || in == NULL || in->buf == NULL) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: FCUnpublish - no live or no buffer!"); + return NGX_OK; + } + + ngx_memzero(&v, sizeof(v)); + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "live: FCUnpublish - error receiving amf data"); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: onFCUnpublish: stream='%s'", v.stream); + + return ngx_rtmp_send_fcunpublish(s, v.stream); +} + + +static ngx_int_t +ngx_rtmp_live_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_live_ctx_t *ctx; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + + if (lacf == NULL || !lacf->live) { + goto next; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: publish: name='%s' type='%s'", + v->name, v->type); + + /* join stream as publisher */ + + ngx_rtmp_live_join(s, v->name, 1); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL || !ctx->publishing) { + goto next; + } + + ctx->silent = v->silent; + + if (!ctx->silent) { + ngx_rtmp_send_status(s, "NetStream.Publish.Start", + "status", "Start publishing"); + } + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_live_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_live_app_conf_t *lacf; + ngx_rtmp_live_ctx_t *ctx; + ngx_http_request_t *r; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_live_module); + + if (lacf == NULL || !lacf->live) { + goto next; + } + + if (!s->relay) { + /* request from http */ + r = s->data; + if (r) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "live: play from HTTP"); + goto next; + } + } + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "live: play: name='%s' start=%uD duration=%uD reset=%d", + v->name, (uint32_t) v->start, + (uint32_t) v->duration, (uint32_t) v->reset); + + /* join stream as subscriber */ + + ngx_rtmp_live_join(s, v->name, 0); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_live_module); + if (ctx == NULL) { + goto next; + } + + ctx->silent = v->silent; + + if (!ctx->silent && !lacf->play_restart) { + ngx_rtmp_send_status(s, "NetStream.Play.Start", + "status", "Start live"); + ngx_rtmp_send_sample_access(s); + } + +next: + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_live_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + ngx_rtmp_amf_handler_t *ch; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + /* register raw event handlers */ + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); + *h = ngx_rtmp_live_av; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); + *h = ngx_rtmp_live_av; + + /* chain handlers */ + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_live_publish; + + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_live_play; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_live_close_stream; + + next_pause = ngx_rtmp_pause; + ngx_rtmp_pause = ngx_rtmp_live_pause; + + next_stream_begin = ngx_rtmp_stream_begin; + ngx_rtmp_stream_begin = ngx_rtmp_live_stream_begin; + + next_stream_eof = ngx_rtmp_stream_eof; + ngx_rtmp_stream_eof = ngx_rtmp_live_stream_eof; + + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "onTextData"); + ch->handler = ngx_rtmp_live_on_text_data; + + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "onCuePoint"); + ch->handler = ngx_rtmp_live_on_cue_point; + + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "onFi"); + ch->handler = ngx_rtmp_live_on_fi; + + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "FCPublish"); + ch->handler = ngx_rtmp_live_on_fcpublish; + + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "FCUnpublish"); + ch->handler = ngx_rtmp_live_on_fcunpublish; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_live_module.h b/ngx_http_flv_module/ngx_rtmp_live_module.h new file mode 100644 index 0000000..39591e1 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_live_module.h @@ -0,0 +1,91 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#ifndef _NGX_RTMP_LIVE_H_INCLUDED_ +#define _NGX_RTMP_LIVE_H_INCLUDED_ + + +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_bandwidth.h" +#include "ngx_rtmp_streams.h" + + +typedef struct ngx_rtmp_live_ctx_s ngx_rtmp_live_ctx_t; +typedef struct ngx_rtmp_live_stream_s ngx_rtmp_live_stream_t; + + +typedef struct { + unsigned active:1; + uint32_t timestamp; + uint32_t csid; + uint32_t dropped; +} ngx_rtmp_live_chunk_stream_t; + + +struct ngx_rtmp_live_ctx_s { + ngx_rtmp_session_t *session; + ngx_rtmp_live_stream_t *stream; + ngx_rtmp_live_ctx_t *next; + ngx_uint_t ndropped; + ngx_rtmp_live_chunk_stream_t cs[2]; + ngx_uint_t meta_version; + ngx_event_t idle_evt; + unsigned active:1; + unsigned publishing:1; + unsigned silent:1; + unsigned paused:1; + ngx_uint_t protocol; +}; + + +struct ngx_rtmp_live_stream_s { + u_char name[NGX_RTMP_MAX_NAME]; + ngx_rtmp_live_stream_t *next; + ngx_rtmp_live_ctx_t *ctx; + ngx_rtmp_live_ctx_t *pub_ctx; + ngx_rtmp_bandwidth_t bw_in; + ngx_rtmp_bandwidth_t bw_in_audio; + ngx_rtmp_bandwidth_t bw_in_video; + ngx_rtmp_bandwidth_t bw_in_data; + ngx_rtmp_bandwidth_t bw_out; + ngx_msec_t epoch; + unsigned active:1; + unsigned publishing:1; +}; + + +typedef struct { + ngx_int_t nbuckets; + ngx_rtmp_live_stream_t **streams; + ngx_flag_t live; + ngx_flag_t meta; + ngx_msec_t sync; + ngx_msec_t idle_timeout; + ngx_flag_t atc; + ngx_flag_t interleave; + ngx_flag_t wait_key; + ngx_flag_t wait_video; + ngx_flag_t publish_notify; + ngx_flag_t play_restart; + ngx_flag_t idle_streams; + ngx_msec_t buflen; + ngx_pool_t *pool; + ngx_rtmp_live_stream_t *free_streams; +} ngx_rtmp_live_app_conf_t; + + +extern ngx_module_t ngx_rtmp_live_module; + + +ngx_rtmp_live_stream_t **ngx_rtmp_live_get_stream(ngx_rtmp_session_t *s, + u_char *name, int create); + + +#endif /* _NGX_RTMP_LIVE_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_log_module.c b/ngx_http_flv_module/ngx_rtmp_log_module.c new file mode 100644 index 0000000..38cb49b --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_log_module.c @@ -0,0 +1,1228 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_cmd_module.h" + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_play_pt next_play; + + +static ngx_int_t ngx_rtmp_log_postconfiguration(ngx_conf_t *cf); +static void *ngx_rtmp_log_create_main_conf(ngx_conf_t *cf); +static void * ngx_rtmp_log_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_log_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +static char * ngx_rtmp_log_set_log(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char * ngx_rtmp_log_set_format(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char * ngx_rtmp_log_compile_format(ngx_conf_t *cf, ngx_array_t *ops, + ngx_array_t *args, ngx_uint_t s); +static ngx_int_t ngx_rtmp_log_flush(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in); + + +typedef struct ngx_rtmp_log_op_s ngx_rtmp_log_op_t; + + +typedef size_t (*ngx_rtmp_log_op_getlen_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op); +typedef u_char * (*ngx_rtmp_log_op_getdata_pt)(ngx_rtmp_session_t *s, + u_char *buf, ngx_rtmp_log_op_t *log); + + +struct ngx_rtmp_log_op_s { + ngx_rtmp_log_op_getlen_pt getlen; + ngx_rtmp_log_op_getdata_pt getdata; + ngx_str_t value; + ngx_uint_t offset; +}; + + +typedef struct { + ngx_str_t name; + ngx_rtmp_log_op_getlen_pt getlen; + ngx_rtmp_log_op_getdata_pt getdata; + ngx_uint_t offset; +} ngx_rtmp_log_var_t; + + +typedef struct { + ngx_str_t name; + ngx_array_t *ops; /* ngx_rtmp_log_op_t */ +} ngx_rtmp_log_fmt_t; + + +typedef struct { + ngx_open_file_t *file; + time_t disk_full_time; + time_t error_log_time; + ngx_rtmp_log_fmt_t *format; +} ngx_rtmp_log_t; + + +typedef struct { + ngx_array_t *logs; /* ngx_rtmp_log_t */ + ngx_uint_t off; + ngx_msec_t interval; + size_t size; +} ngx_rtmp_log_app_conf_t; + + +typedef struct { + ngx_array_t formats; /* ngx_rtmp_log_fmt_t */ + ngx_uint_t combined_used; +} ngx_rtmp_log_main_conf_t; + + +typedef struct { + u_char *line; + ngx_event_t ev; + unsigned play:1; + unsigned publish:1; + u_char name[NGX_RTMP_MAX_NAME]; + u_char args[NGX_RTMP_MAX_ARGS]; + uint32_t last_sent; + uint32_t last_received; +} ngx_rtmp_log_ctx_t; + + +static ngx_str_t ngx_rtmp_access_log = ngx_string(NGX_HTTP_LOG_PATH); + + +static ngx_command_t ngx_rtmp_log_commands[] = { + + { ngx_string("access_log"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE12, + ngx_rtmp_log_set_log, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("log_format"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_2MORE, + ngx_rtmp_log_set_format, + NGX_RTMP_MAIN_CONF_OFFSET, + 0, + NULL }, + { + ngx_string("log_interval"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_log_app_conf_t, interval), + NULL + }, + { + ngx_string("log_size"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_log_app_conf_t, size), + NULL + }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_log_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_log_postconfiguration, /* postconfiguration */ + ngx_rtmp_log_create_main_conf, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + ngx_rtmp_log_create_app_conf, /* create app configuration */ + ngx_rtmp_log_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_log_module = { + NGX_MODULE_V1, + &ngx_rtmp_log_module_ctx, /* module context */ + ngx_rtmp_log_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static ngx_str_t ngx_rtmp_combined_fmt = + ngx_string("$remote_addr [$time_local] $command " + "\"$app\" \"$name\" \"$args\" - " + "$bytes_received $bytes_sent " + "\"$pageurl\" \"$flashver\" ($session_readable_time)"); + + +static size_t +ngx_rtmp_log_var_default_getlen(ngx_rtmp_session_t *s, ngx_rtmp_log_op_t *op) +{ + return op->value.len; +} + + +static u_char * +ngx_rtmp_log_var_default_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + return ngx_cpymem(buf, op->value.data, op->value.len); +} + + +static size_t +ngx_rtmp_log_var_connection_getlen(ngx_rtmp_session_t *s, ngx_rtmp_log_op_t *op) +{ + return NGX_INT_T_LEN; +} + +static u_char * +ngx_rtmp_log_var_connection_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + return ngx_sprintf(buf, "%ui", (ngx_uint_t) s->connection->number); +} + + +static size_t +ngx_rtmp_log_var_remote_addr_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return s->connection->addr_text.len; +} + + +static u_char * +ngx_rtmp_log_var_remote_addr_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + return ngx_cpymem(buf, s->connection->addr_text.data, + s->connection->addr_text.len); +} + + +static size_t +ngx_rtmp_log_var_msec_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return NGX_TIME_T_LEN + 4; +} + + +static u_char * +ngx_rtmp_log_var_msec_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + ngx_time_t *tp; + + tp = ngx_timeofday(); + + return ngx_sprintf(buf, "%T.%03M", tp->sec, tp->msec); +} + + +static size_t +ngx_rtmp_log_var_session_string_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return ((ngx_str_t *) ((u_char *) s + op->offset))->len; +} + + +static u_char * +ngx_rtmp_log_var_session_string_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + ngx_str_t *str; + + str = (ngx_str_t *) ((u_char *) s + op->offset); + + return ngx_cpymem(buf, str->data, str->len); +} + + +static size_t +ngx_rtmp_log_var_command_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return sizeof("PLAY+PUBLISH") - 1; +} + + +static u_char * +ngx_rtmp_log_var_command_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + ngx_rtmp_log_ctx_t *ctx; + ngx_str_t *cmd; + ngx_uint_t n; + + static ngx_str_t commands[] = { + ngx_string("NONE"), + ngx_string("PLAY"), + ngx_string("PUBLISH"), + ngx_string("PLAY+PUBLISH") + }; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_log_module); + + n = ctx ? (ctx->play + ctx->publish * 2) : 0; + + cmd = &commands[n]; + + return ngx_cpymem(buf, cmd->data, cmd->len); +} + + +static size_t +ngx_rtmp_log_var_context_cstring_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return ngx_max(NGX_RTMP_MAX_NAME, NGX_RTMP_MAX_ARGS); +} + + +static u_char * +ngx_rtmp_log_var_context_cstring_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + ngx_rtmp_log_ctx_t *ctx; + u_char *p; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_log_module); + if (ctx == NULL) { + return buf; + } + + p = (u_char *) ctx + op->offset; + while (*p) { + *buf++ = *p++; + } + + return buf; +} + + +static size_t +ngx_rtmp_log_var_session_uint32_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return NGX_INT32_LEN; +} + + +#if 0 +static u_char * +ngx_rtmp_log_var_session_uint32_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + uint32_t *v; + + v = (uint32_t *) ((uint8_t *) s + op->offset); + + return ngx_sprintf(buf, "%uD", *v); +} +#endif + + +static size_t +ngx_rtmp_log_var_time_local_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return ngx_cached_http_log_time.len; +} + + +static u_char * +ngx_rtmp_log_var_time_local_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + return ngx_cpymem(buf, ngx_cached_http_log_time.data, + ngx_cached_http_log_time.len); +} + + +static size_t +ngx_rtmp_log_var_session_time_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return NGX_INT64_LEN; +} + + +static u_char * +ngx_rtmp_log_var_session_time_getdata(ngx_rtmp_session_t *s, u_char *buf, + ngx_rtmp_log_op_t *op) +{ + return ngx_sprintf(buf, "%L", + (int64_t) (ngx_current_msec - s->epoch) / 1000); +} + + +static size_t +ngx_rtmp_log_var_session_readable_time_getlen(ngx_rtmp_session_t *s, + ngx_rtmp_log_op_t *op) +{ + return NGX_INT_T_LEN + sizeof("d 23h 59m 59s") - 1; +} + + +static u_char * +ngx_rtmp_log_var_session_readable_time_getdata(ngx_rtmp_session_t *s, + u_char *buf, ngx_rtmp_log_op_t *op) +{ + int64_t v; + ngx_uint_t days, hours, minutes, seconds; + + v = (ngx_current_msec - s->epoch) / 1000; + + days = (ngx_uint_t) (v / (60 * 60 * 24)); + hours = (ngx_uint_t) (v / (60 * 60) % 24); + minutes = (ngx_uint_t) (v / 60 % 60); + seconds = (ngx_uint_t) (v % 60); + + if (days) { + buf = ngx_sprintf(buf, "%uid ", days); + } + + if (days || hours) { + buf = ngx_sprintf(buf, "%uih ", hours); + } + + if (days || hours || minutes) { + buf = ngx_sprintf(buf, "%uim ", minutes); + } + + buf = ngx_sprintf(buf, "%uis", seconds); + + return buf; +} + + +static u_char * +ngx_rtmp_log_var_session_bytesent_getdata(ngx_rtmp_session_t *s, + u_char *buf, ngx_rtmp_log_op_t *op) +{ + ngx_rtmp_log_ctx_t *ctx; + uint32_t sent; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_log_module); + if (ctx == NULL) { + if (s->out_bytes > 0) { + return ngx_sprintf(buf, "%uD", s->out_bytes); + } + + *buf = '0'; + return buf + 1; + } + + sent = s->out_bytes - ctx->last_sent; + ctx->last_sent = s->out_bytes; + + if (sent > 0) { + return ngx_sprintf(buf, "%uD", sent); + } + + *buf = '0'; + return buf + 1; +} + + +static u_char * +ngx_rtmp_log_var_session_bytereceived_getdata(ngx_rtmp_session_t *s, + u_char *buf, ngx_rtmp_log_op_t *op) +{ + ngx_rtmp_log_ctx_t *ctx; + uint32_t received; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_log_module); + if (ctx == NULL) { + if (s->in_bytes > 0) { + return ngx_sprintf(buf, "%uD", s->in_bytes); + } + + *buf = '0'; + return buf + 1; + } + + received = s->in_bytes - ctx->last_received; + ctx->last_received = s->in_bytes; + + if (received > 0) { + return ngx_sprintf(buf, "%uD", received); + } + + *buf = '0'; + return buf + 1; +} + + +static ngx_rtmp_log_var_t ngx_rtmp_log_vars[] = { + { ngx_string("connection"), + ngx_rtmp_log_var_connection_getlen, + ngx_rtmp_log_var_connection_getdata, + 0 }, + + { ngx_string("remote_addr"), + ngx_rtmp_log_var_remote_addr_getlen, + ngx_rtmp_log_var_remote_addr_getdata, + 0 }, + + { ngx_string("app"), + ngx_rtmp_log_var_session_string_getlen, + ngx_rtmp_log_var_session_string_getdata, + offsetof(ngx_rtmp_session_t, app) }, + + { ngx_string("flashver"), + ngx_rtmp_log_var_session_string_getlen, + ngx_rtmp_log_var_session_string_getdata, + offsetof(ngx_rtmp_session_t, flashver) }, + + { ngx_string("swfurl"), + ngx_rtmp_log_var_session_string_getlen, + ngx_rtmp_log_var_session_string_getdata, + offsetof(ngx_rtmp_session_t, swf_url) }, + + { ngx_string("tcurl"), + ngx_rtmp_log_var_session_string_getlen, + ngx_rtmp_log_var_session_string_getdata, + offsetof(ngx_rtmp_session_t, tc_url) }, + + { ngx_string("pageurl"), + ngx_rtmp_log_var_session_string_getlen, + ngx_rtmp_log_var_session_string_getdata, + offsetof(ngx_rtmp_session_t, page_url) }, + + { ngx_string("command"), + ngx_rtmp_log_var_command_getlen, + ngx_rtmp_log_var_command_getdata, + 0 }, + + { ngx_string("name"), + ngx_rtmp_log_var_context_cstring_getlen, + ngx_rtmp_log_var_context_cstring_getdata, + offsetof(ngx_rtmp_log_ctx_t, name) }, + + { ngx_string("args"), + ngx_rtmp_log_var_context_cstring_getlen, + ngx_rtmp_log_var_context_cstring_getdata, + offsetof(ngx_rtmp_log_ctx_t, args) }, + + { ngx_string("bytes_sent"), + ngx_rtmp_log_var_session_uint32_getlen, + ngx_rtmp_log_var_session_bytesent_getdata, + 0 }, + + { ngx_string("bytes_received"), + ngx_rtmp_log_var_session_uint32_getlen, + ngx_rtmp_log_var_session_bytereceived_getdata, + 0 }, + + { ngx_string("time_local"), + ngx_rtmp_log_var_time_local_getlen, + ngx_rtmp_log_var_time_local_getdata, + 0 }, + + { ngx_string("msec"), + ngx_rtmp_log_var_msec_getlen, + ngx_rtmp_log_var_msec_getdata, + 0 }, + + { ngx_string("session_time"), + ngx_rtmp_log_var_session_time_getlen, + ngx_rtmp_log_var_session_time_getdata, + 0 }, + + { ngx_string("session_readable_time"), + ngx_rtmp_log_var_session_readable_time_getlen, + ngx_rtmp_log_var_session_readable_time_getdata, + 0 }, + + { ngx_null_string, NULL, NULL, 0 } +}; + + +static void * +ngx_rtmp_log_create_main_conf(ngx_conf_t *cf) +{ + ngx_rtmp_log_main_conf_t *lmcf; + ngx_rtmp_log_fmt_t *fmt; + + lmcf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_log_main_conf_t)); + if (lmcf == NULL) { + return NULL; + } + + if (ngx_array_init(&lmcf->formats, cf->pool, 4, sizeof(ngx_rtmp_log_fmt_t)) + != NGX_OK) + { + return NULL; + } + + fmt = ngx_array_push(&lmcf->formats); + if (fmt == NULL) { + return NULL; + } + + ngx_str_set(&fmt->name, "combined"); + + fmt->ops = ngx_array_create(cf->pool, 16, sizeof(ngx_rtmp_log_op_t)); + if (fmt->ops == NULL) { + return NULL; + } + + return lmcf; + +} + + +static void * +ngx_rtmp_log_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_log_app_conf_t *lacf; + + lacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_log_app_conf_t)); + if (lacf == NULL) { + return NULL; + } + + lacf->interval = NGX_CONF_UNSET_MSEC; + lacf->size = NGX_CONF_UNSET_SIZE; + + return lacf; +} + + +static char * +ngx_rtmp_log_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_log_app_conf_t *prev = parent; + ngx_rtmp_log_app_conf_t *conf = child; + ngx_rtmp_log_main_conf_t *lmcf; + ngx_rtmp_log_fmt_t *fmt; + ngx_rtmp_log_t *log; + + ngx_conf_merge_msec_value(conf->interval, prev->interval, 0); + ngx_conf_merge_size_value(conf->size, prev->size, 1 * 1024 * 1024); + + if (conf->logs || conf->off) { + return NGX_OK; + } + + conf->logs = prev->logs; + conf->off = prev->off; + + if (conf->logs || conf->off) { + return NGX_OK; + } + + conf->logs = ngx_array_create(cf->pool, 2, sizeof(ngx_rtmp_log_t)); + if (conf->logs == NULL) { + return NGX_CONF_ERROR; + } + + log = ngx_array_push(conf->logs); + if (log == NULL) { + return NGX_CONF_ERROR; + } + + log->file = ngx_conf_open_file(cf->cycle, &ngx_rtmp_access_log); + if (log->file == NULL) { + return NGX_CONF_ERROR; + } + + log->disk_full_time = 0; + log->error_log_time = 0; + + lmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_log_module); + fmt = lmcf->formats.elts; + + log->format = &fmt[0]; + lmcf->combined_used = 1; + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_log_set_log(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_log_app_conf_t *lacf = conf; + + ngx_rtmp_log_main_conf_t *lmcf; + ngx_rtmp_log_fmt_t *fmt; + ngx_rtmp_log_t *log; + ngx_str_t *value, name; + ngx_uint_t n; + + value = cf->args->elts; + + if (ngx_strcmp(value[1].data, "off") == 0) { + lacf->off = 1; + return NGX_CONF_OK; + } + + if (lacf->logs == NULL) { + lacf->logs = ngx_array_create(cf->pool, 2, sizeof(ngx_rtmp_log_t)); + if (lacf->logs == NULL) { + return NGX_CONF_ERROR; + } + } + + log = ngx_array_push(lacf->logs); + if (log == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memzero(log, sizeof(*log)); + + lmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_log_module); + + log->file = ngx_conf_open_file(cf->cycle, &value[1]); + if (log->file == NULL) { + return NGX_CONF_ERROR; + } + + if (cf->args->nelts == 2) { + ngx_str_set(&name, "combined"); + lmcf->combined_used = 1; + + } else { + name = value[2]; + if (ngx_strcmp(name.data, "combined") == 0) { + lmcf->combined_used = 1; + } + } + + fmt = lmcf->formats.elts; + for (n = 0; n < lmcf->formats.nelts; ++n, ++fmt) { + if (fmt->name.len == name.len && + ngx_strncasecmp(fmt->name.data, name.data, name.len) == 0) + { + log->format = fmt; + break; + } + } + + if (log->format == NULL) { + ngx_conf_log_error(NGX_LOG_WARN, cf, 0, "unknown log format \"%V\"", + &name); + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_log_set_format(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_log_main_conf_t *lmcf = conf; + ngx_rtmp_log_fmt_t *fmt; + ngx_str_t *value; + ngx_uint_t i; + + value = cf->args->elts; + + if (cf->cmd_type != NGX_RTMP_MAIN_CONF) { + ngx_conf_log_error(NGX_LOG_WARN, cf, 0, + "\"log_format\" directive can only be used on " + "\"rtmp\" level"); + } + + fmt = lmcf->formats.elts; + for (i = 0; i < lmcf->formats.nelts; i++) { + if (fmt[i].name.len == value[1].len && + ngx_strcmp(fmt[i].name.data, value[1].data) == 0) + { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "duplicate \"log_format\" name \"%V\"", + &value[1]); + return NGX_CONF_ERROR; + } + } + + fmt = ngx_array_push(&lmcf->formats); + if (fmt == NULL) { + return NGX_CONF_ERROR; + } + + fmt->name = value[1]; + + fmt->ops = ngx_array_create(cf->pool, 16, sizeof(ngx_rtmp_log_op_t)); + if (fmt->ops == NULL) { + return NGX_CONF_ERROR; + } + + return ngx_rtmp_log_compile_format(cf, fmt->ops, cf->args, 2); +} + + +static char * +ngx_rtmp_log_compile_format(ngx_conf_t *cf, ngx_array_t *ops, ngx_array_t *args, + ngx_uint_t s) +{ + size_t i, len; + u_char *data, *d, c; + ngx_uint_t bracket; + ngx_str_t *value, var; + ngx_rtmp_log_op_t *op; + ngx_rtmp_log_var_t *v; + + value = args->elts; + + for (; s < args->nelts; ++s) { + i = 0; + + len = value[s].len; + d = value[s].data; + + while (i < len) { + + op = ngx_array_push(ops); + if (op == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memzero(op, sizeof(*op)); + + data = &d[i]; + + if (d[i] == '$') { + if (++i == len) { + goto invalid; + } + + if (d[i] == '{') { + bracket = 1; + if (++i == len) { + goto invalid; + } + } else { + bracket = 0; + } + + var.data = &d[i]; + + for (var.len = 0; i < len; ++i, ++var.len) { + c = d[i]; + + if (c == '}' && bracket) { + ++i; + bracket = 0; + break; + } + + if ((c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || + (c == '_')) + { + continue; + } + + break; + } + + if (bracket) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "missing closing bracket in \"%V\"", + &var); + return NGX_CONF_ERROR; + } + + if (var.len == 0) { + goto invalid; + } + + for (v = ngx_rtmp_log_vars; v->name.len; ++v) { + if (v->name.len == var.len && + ngx_strncmp(v->name.data, var.data, var.len) == 0) + { + op->getlen = v->getlen; + op->getdata = v->getdata; + op->offset = v->offset; + break; + } + } + + if (v->name.len == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "unknown variable \"%V\"", &var); + return NGX_CONF_ERROR; + } + + continue; + } + + ++i; + + while (i < len && d[i] != '$') { + ++i; + } + + op->getlen = ngx_rtmp_log_var_default_getlen; + op->getdata = ngx_rtmp_log_var_default_getdata; + + op->value.len = &d[i] - data; + + op->value.data = ngx_pnalloc(cf->pool, op->value.len); + if (op->value.data == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memcpy(op->value.data, data, op->value.len); + } + } + + return NGX_CONF_OK; + +invalid: + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%s\"", data); + + return NGX_CONF_ERROR; +} + + +static void +ngx_rtmp_log_split_output_handler(ngx_event_t *ev) +{ + ngx_rtmp_session_t *s; + ngx_rtmp_log_app_conf_t *lacf; + + s = ev->data; + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_log_module); + if (lacf == NULL || lacf->off || lacf->logs == NULL) { + return; + } + + ngx_add_timer(ev, lacf->interval); + ngx_rtmp_log_flush(s, 0, 0); +} + + +static ngx_rtmp_log_ctx_t * +ngx_rtmp_log_set_names(ngx_rtmp_session_t *s, u_char *name, u_char *args) +{ + ngx_rtmp_log_ctx_t *ctx; + ngx_rtmp_log_app_conf_t *lacf; + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_log_module); + if (lacf == NULL || lacf->off || lacf->logs == NULL) { + return NULL; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_log_module); + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_log_ctx_t)); + if (ctx == NULL) { + return NULL; + } + + ctx->line = ngx_pcalloc(s->connection->pool, lacf->size); + if (ctx->line == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "failed to allocate buffer for log line"); + return NULL; + } + + if (lacf->interval) { + ctx->ev.handler = ngx_rtmp_log_split_output_handler; + ctx->ev.log = s->connection->log; + ctx->ev.data = s; + ctx->ev.timer_set = 0; + ctx->last_sent = 0; + ctx->last_received = 0; + + ngx_add_timer(&ctx->ev, lacf->interval); + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_log_module); + } + + ngx_memcpy(ctx->name, name, NGX_RTMP_MAX_NAME); + ngx_memcpy(ctx->args, args, NGX_RTMP_MAX_ARGS); + + return ctx; +} + + +static ngx_int_t +ngx_rtmp_log_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_log_ctx_t *ctx; + + if (s->auto_pushed || s->relay) { + goto next; + } + + ctx = ngx_rtmp_log_set_names(s, v->name, v->args); + if (ctx == NULL) { + goto next; + } + + ctx->publish = 1; + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_log_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_log_ctx_t *ctx; + + if (s->auto_pushed || s->relay) { + goto next; + } + + ctx = ngx_rtmp_log_set_names(s, v->name, v->args); + if (ctx == NULL) { + goto next; + } + + ctx->play = 1; + +next: + return next_play(s, v); +} + + +static void +ngx_rtmp_log_write(ngx_rtmp_session_t *s, ngx_rtmp_log_t *log, u_char *buf, + size_t len) +{ + u_char *name; + time_t now; + ssize_t n; + int err; + + err = 0; + name = log->file->name.data; + n = ngx_write_fd(log->file->fd, buf, len); + + if (n == (ssize_t) len) { + return; + } + + now = ngx_time(); + + if (n == -1) { + err = ngx_errno; + + if (err == NGX_ENOSPC) { + log->disk_full_time = now; + } + + if (now - log->error_log_time > 59) { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, err, + ngx_write_fd_n " to \"%s\" failed", name); + log->error_log_time = now; + } + } + + if (now - log->error_log_time > 59) { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, err, + ngx_write_fd_n " to \"%s\" was incomplete: %z of %uz", + name, n, len); + log->error_log_time = now; + } +} + + +static ngx_int_t +ngx_rtmp_log_flush(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_log_app_conf_t *lacf; + ngx_rtmp_log_t *log; + ngx_rtmp_log_op_t *op; + ngx_rtmp_log_ctx_t *ctx; + ngx_uint_t n, i; + u_char *p; + size_t len; + + if (s->auto_pushed || s->relay) { + return NGX_OK; + } + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_log_module); + if (lacf == NULL || lacf->off || lacf->logs == NULL) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_log_module); + if (ctx == NULL) { + return NGX_OK; + } + + log = lacf->logs->elts; + for (i = 0; i < lacf->logs->nelts; ++i, ++log) { + + if (ngx_time() == log->disk_full_time) { + /* FreeBSD full disk protection; + * nginx http logger does the same */ + continue; + } + + len = 0; + op = log->format->ops->elts; + for (n = 0; n < log->format->ops->nelts; ++n, ++op) { + if (len + NGX_LINEFEED_SIZE <= lacf->size) { + len += op->getlen(s, op); + } else { + break; + } + } + + len += NGX_LINEFEED_SIZE; + + p = ctx->line; + op = log->format->ops->elts; + for (n = 0; n < log->format->ops->nelts; ++n, ++op) { + if (p + NGX_LINEFEED_SIZE <= ctx->line + lacf->size) { + p = op->getdata(s, p, op); + } else { + break; + } + } + + ngx_linefeed(p); + + ngx_rtmp_log_write(s, log, ctx->line, p - ctx->line); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_log_disconnect(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_log_app_conf_t *lacf; + ngx_rtmp_log_t *log; + ngx_rtmp_log_op_t *op; + ngx_uint_t n, i; + u_char *p; + ngx_rtmp_log_ctx_t *ctx; + size_t len; + + if (s->auto_pushed || s->relay) { + return NGX_OK; + } + + lacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_log_module); + if (lacf == NULL || lacf->off || lacf->logs == NULL) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_log_module); + if (ctx == NULL) { + return NGX_OK; + } + + log = lacf->logs->elts; + for (i = 0; i < lacf->logs->nelts; ++i, ++log) { + + if (ngx_time() == log->disk_full_time) { + /* FreeBSD full disk protection; + * nginx http logger does the same */ + continue; + } + + len = 0; + op = log->format->ops->elts; + for (n = 0; n < log->format->ops->nelts; ++n, ++op) { + if (len + NGX_LINEFEED_SIZE <= lacf->size) { + len += op->getlen(s, op); + } else { + break; + } + } + + len += NGX_LINEFEED_SIZE; + + p = ctx->line; + op = log->format->ops->elts; + for (n = 0; n < log->format->ops->nelts; ++n, ++op) { + if (p + NGX_LINEFEED_SIZE <= ctx->line + lacf->size) { + p = op->getdata(s, p, op); + } else { + break; + } + } + + ngx_linefeed(p); + + ngx_rtmp_log_write(s, log, ctx->line, p - ctx->line); + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_log_module); + if(ctx && ctx->ev.timer_set) { + ngx_del_timer(&ctx->ev); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_log_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + ngx_rtmp_log_main_conf_t *lmcf; + ngx_array_t a; + ngx_rtmp_log_fmt_t *fmt; + ngx_str_t *value; + + lmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_log_module); + if (lmcf->combined_used) { + if (ngx_array_init(&a, cf->pool, 1, sizeof(ngx_str_t)) != NGX_OK) { + return NGX_ERROR; + } + + value = ngx_array_push(&a); + if (value == NULL) { + return NGX_ERROR; + } + + *value = ngx_rtmp_combined_fmt; + fmt = lmcf->formats.elts; + + if (ngx_rtmp_log_compile_format(cf, fmt->ops, &a, 0) + != NGX_CONF_OK) + { + return NGX_ERROR; + } + } + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + h = ngx_array_push(&cmcf->events[NGX_RTMP_DISCONNECT]); + *h = ngx_rtmp_log_disconnect; + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_log_publish; + + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_log_play; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_mp4_module.c b/ngx_http_flv_module/ngx_rtmp_mp4_module.c new file mode 100644 index 0000000..03aca03 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_mp4_module.c @@ -0,0 +1,2600 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp_play_module.h" +#include "ngx_rtmp_codec_module.h" +#include "ngx_rtmp_streams.h" + + +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, + ngx_int_t aindex, ngx_int_t vindex); +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); + + +#define NGX_RTMP_MP4_MAX_FRAMES 8 + + +#pragma pack(push,4) + + +/* disable zero-sized array warning by msvc */ + +#if (NGX_WIN32) +#pragma warning(push) +#pragma warning(disable:4200) +#endif + + +typedef struct { + uint32_t first_chunk; + uint32_t samples_per_chunk; + uint32_t sample_descrption_index; +} ngx_rtmp_mp4_chunk_entry_t; + + +typedef struct { + uint32_t version_flags; + uint32_t entry_count; + ngx_rtmp_mp4_chunk_entry_t entries[0]; +} ngx_rtmp_mp4_chunks_t; + + +typedef struct { + uint32_t sample_count; + uint32_t sample_delta; +} ngx_rtmp_mp4_time_entry_t; + + +typedef struct { + uint32_t version_flags; + uint32_t entry_count; + ngx_rtmp_mp4_time_entry_t entries[0]; +} ngx_rtmp_mp4_times_t; + + +typedef struct { + uint32_t sample_count; + uint32_t sample_offset; +} ngx_rtmp_mp4_delay_entry_t; + + +typedef struct { + uint32_t version_flags; + uint32_t entry_count; + ngx_rtmp_mp4_delay_entry_t entries[0]; +} ngx_rtmp_mp4_delays_t; + + +typedef struct { + uint32_t version_flags; + uint32_t entry_count; + uint32_t entries[0]; +} ngx_rtmp_mp4_keys_t; + + +typedef struct { + uint32_t version_flags; + uint32_t sample_size; + uint32_t sample_count; + uint32_t entries[0]; +} ngx_rtmp_mp4_sizes_t; + + +typedef struct { + uint32_t version_flags; + uint32_t field_size; + uint32_t sample_count; + uint32_t entries[0]; +} ngx_rtmp_mp4_sizes2_t; + + +typedef struct { + uint32_t version_flags; + uint32_t entry_count; + uint32_t entries[0]; +} ngx_rtmp_mp4_offsets_t; + + +typedef struct { + uint32_t version_flags; + uint32_t entry_count; + uint64_t entries[0]; +} ngx_rtmp_mp4_offsets64_t; + + +#if (NGX_WIN32) +#pragma warning(pop) +#endif + + +#pragma pack(pop) + + +typedef struct { + uint32_t timestamp; + uint32_t last_timestamp; + off_t offset; + size_t size; + ngx_int_t key; + uint32_t delay; + + unsigned not_first:1; + unsigned valid:1; + + ngx_uint_t pos; + + ngx_uint_t key_pos; + + ngx_uint_t chunk; + ngx_uint_t chunk_pos; + ngx_uint_t chunk_count; + + ngx_uint_t time_pos; + ngx_uint_t time_count; + + ngx_uint_t delay_pos; + ngx_uint_t delay_count; + + ngx_uint_t size_pos; +} ngx_rtmp_mp4_cursor_t; + + +typedef struct { + ngx_uint_t id; + + ngx_int_t type; + ngx_int_t codec; + uint32_t csid; + u_char fhdr; + ngx_int_t time_scale; + uint64_t duration; + + u_char *header; + size_t header_size; + unsigned header_sent:1; + + ngx_rtmp_mp4_times_t *times; + ngx_rtmp_mp4_delays_t *delays; + ngx_rtmp_mp4_keys_t *keys; + ngx_rtmp_mp4_chunks_t *chunks; + ngx_rtmp_mp4_sizes_t *sizes; + ngx_rtmp_mp4_sizes2_t *sizes2; + ngx_rtmp_mp4_offsets_t *offsets; + ngx_rtmp_mp4_offsets64_t *offsets64; + ngx_rtmp_mp4_cursor_t cursor; +} ngx_rtmp_mp4_track_t; + + +typedef struct { + void *mmaped; + size_t mmaped_size; + ngx_fd_t extra; + + unsigned meta_sent:1; + + ngx_rtmp_mp4_track_t tracks[2]; + ngx_rtmp_mp4_track_t *track; + ngx_uint_t ntracks; + + ngx_uint_t width; + ngx_uint_t height; + ngx_uint_t nchannels; + ngx_uint_t sample_size; + ngx_uint_t sample_rate; + + ngx_int_t atracks, vtracks; + ngx_int_t aindex, vindex; + + uint32_t start_timestamp, epoch; +} ngx_rtmp_mp4_ctx_t; + + +#if (NGX_HAVE_LITTLE_ENDIAN) +#define ngx_rtmp_mp4_make_tag(a, b, c, d) \ + ((uint32_t)d << 24 | (uint32_t)c << 16 | (uint32_t)b << 8 | (uint32_t)a) +#else +#define ngx_rtmp_mp4_make_tag(a, b, c, d) \ + ((uint32_t)a << 24 | (uint32_t)b << 16 | (uint32_t)c << 8 | (uint32_t)d) +#endif + + +static ngx_inline uint32_t +ngx_rtmp_mp4_to_rtmp_timestamp(ngx_rtmp_mp4_track_t *t, uint64_t ts) +{ + return (uint32_t) (ts * 1000 / t->time_scale); +} + + +static ngx_inline uint32_t +ngx_rtmp_mp4_from_rtmp_timestamp(ngx_rtmp_mp4_track_t *t, uint32_t ts) +{ + return (uint64_t) ts * t->time_scale / 1000; +} + + +#define NGX_RTMP_MP4_BUFLEN_ADDON 1000 + + +static u_char ngx_rtmp_mp4_buffer[1024*1024]; + + +#if (NGX_WIN32) +static void * +ngx_rtmp_mp4_mmap(ngx_fd_t fd, size_t size, off_t offset, ngx_fd_t *extra) +{ + void *data; + + *extra = CreateFileMapping(fd, NULL, PAGE_READONLY, + (DWORD) ((uint64_t) size >> 32), + (DWORD) (size & 0xffffffff), + NULL); + if (*extra == NULL) { + return NULL; + } + + data = MapViewOfFile(*extra, FILE_MAP_READ, + (DWORD) ((uint64_t) offset >> 32), + (DWORD) (offset & 0xffffffff), + size); + + if (data == NULL) { + CloseHandle(*extra); + } + + /* + * non-NULL result means map view handle is open + * and should be closed later + */ + + return data; +} + + +static ngx_int_t +ngx_rtmp_mp4_munmap(void *data, size_t size, ngx_fd_t *extra) +{ + ngx_int_t rc; + + rc = NGX_OK; + + if (UnmapViewOfFile(data) == 0) { + rc = NGX_ERROR; + } + + if (CloseHandle(*extra) == 0) { + rc = NGX_ERROR; + } + + return rc; +} + +#else + +static void * +ngx_rtmp_mp4_mmap(ngx_fd_t fd, size_t size, off_t offset, ngx_fd_t *extra) +{ + void *data; + + data = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset); + + /* valid address is never NULL since there's no MAP_FIXED */ + + return data == MAP_FAILED ? NULL : data; +} + + +static ngx_int_t +ngx_rtmp_mp4_munmap(void *data, size_t size, ngx_fd_t *extra) +{ + return munmap(data, size); +} + +#endif + + +static ngx_int_t ngx_rtmp_mp4_parse(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_trak(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_mdhd(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_stsd(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_stsc(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_stts(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_ctts(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_stss(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_stsz(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_stz2(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_stco(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_co64(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_avc1(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_avcC(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_mp4a(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_mp4v(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_esds(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_mp3(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_nmos(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_spex(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); + + +typedef ngx_int_t (*ngx_rtmp_mp4_box_pt)(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); + +typedef struct { + uint32_t tag; + ngx_rtmp_mp4_box_pt handler; +} ngx_rtmp_mp4_box_t; + + +static ngx_rtmp_mp4_box_t ngx_rtmp_mp4_boxes[] = { + { ngx_rtmp_mp4_make_tag('t','r','a','k'), ngx_rtmp_mp4_parse_trak }, + { ngx_rtmp_mp4_make_tag('m','d','i','a'), ngx_rtmp_mp4_parse }, + { ngx_rtmp_mp4_make_tag('m','d','h','d'), ngx_rtmp_mp4_parse_mdhd }, + { ngx_rtmp_mp4_make_tag('h','d','l','r'), ngx_rtmp_mp4_parse_hdlr }, + { ngx_rtmp_mp4_make_tag('m','i','n','f'), ngx_rtmp_mp4_parse }, + { ngx_rtmp_mp4_make_tag('s','t','b','l'), ngx_rtmp_mp4_parse }, + { ngx_rtmp_mp4_make_tag('s','t','s','d'), ngx_rtmp_mp4_parse_stsd }, + { ngx_rtmp_mp4_make_tag('s','t','s','c'), ngx_rtmp_mp4_parse_stsc }, + { ngx_rtmp_mp4_make_tag('s','t','t','s'), ngx_rtmp_mp4_parse_stts }, + { ngx_rtmp_mp4_make_tag('c','t','t','s'), ngx_rtmp_mp4_parse_ctts }, + { ngx_rtmp_mp4_make_tag('s','t','s','s'), ngx_rtmp_mp4_parse_stss }, + { ngx_rtmp_mp4_make_tag('s','t','s','z'), ngx_rtmp_mp4_parse_stsz }, + { ngx_rtmp_mp4_make_tag('s','t','z','2'), ngx_rtmp_mp4_parse_stz2 }, + { ngx_rtmp_mp4_make_tag('s','t','c','o'), ngx_rtmp_mp4_parse_stco }, + { ngx_rtmp_mp4_make_tag('c','o','6','4'), ngx_rtmp_mp4_parse_co64 }, + { ngx_rtmp_mp4_make_tag('a','v','c','1'), ngx_rtmp_mp4_parse_avc1 }, + { ngx_rtmp_mp4_make_tag('a','v','c','C'), ngx_rtmp_mp4_parse_avcC }, + { ngx_rtmp_mp4_make_tag('m','p','4','a'), ngx_rtmp_mp4_parse_mp4a }, + { ngx_rtmp_mp4_make_tag('m','p','4','v'), ngx_rtmp_mp4_parse_mp4v }, + { ngx_rtmp_mp4_make_tag('e','s','d','s'), ngx_rtmp_mp4_parse_esds }, + { ngx_rtmp_mp4_make_tag('.','m','p','3'), ngx_rtmp_mp4_parse_mp3 }, + { ngx_rtmp_mp4_make_tag('n','m','o','s'), ngx_rtmp_mp4_parse_nmos }, + { ngx_rtmp_mp4_make_tag('s','p','e','x'), ngx_rtmp_mp4_parse_spex }, + { ngx_rtmp_mp4_make_tag('w','a','v','e'), ngx_rtmp_mp4_parse } +}; + + +static ngx_int_t ngx_rtmp_mp4_parse_descr(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_es(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_dc(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); +static ngx_int_t ngx_rtmp_mp4_parse_ds(ngx_rtmp_session_t *s, u_char *pos, + u_char *last); + + +typedef ngx_int_t (*ngx_rtmp_mp4_descriptor_pt)(ngx_rtmp_session_t *s, + u_char *pos, u_char *last); + +typedef struct { + uint8_t tag; + ngx_rtmp_mp4_descriptor_pt handler; +} ngx_rtmp_mp4_descriptor_t; + + +static ngx_rtmp_mp4_descriptor_t ngx_rtmp_mp4_descriptors[] = { + { 0x03, ngx_rtmp_mp4_parse_es }, /* MPEG ES Descriptor */ + { 0x04, ngx_rtmp_mp4_parse_dc }, /* MPEG DecoderConfig Descriptor */ + { 0x05, ngx_rtmp_mp4_parse_ds } /* MPEG DecoderSpec Descriptor */ +}; + + +static ngx_rtmp_module_t ngx_rtmp_mp4_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_mp4_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + NULL, /* create app configuration */ + NULL /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_mp4_module = { + NGX_MODULE_V1, + &ngx_rtmp_mp4_module_ctx, /* module context */ + NULL, /* module directives */ + NGX_RTMP_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 +}; + + +static ngx_int_t +ngx_rtmp_mp4_parse_trak(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx->track) { + return NGX_OK; + } + + ctx->track = (ctx->ntracks == sizeof(ctx->tracks) / sizeof(ctx->tracks[0])) + ? NULL : &ctx->tracks[ctx->ntracks]; + + if (ctx->track) { + ngx_memzero(ctx->track, sizeof(*ctx->track)); + ctx->track->id = ctx->ntracks; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: trying track %ui", ctx->ntracks); + } + + if (ngx_rtmp_mp4_parse(s, pos, last) != NGX_OK) { + return NGX_ERROR; + } + + if (ctx->track && ctx->track->type && + (ctx->ntracks == 0 || + ctx->tracks[0].type != ctx->tracks[ctx->ntracks].type)) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: adding track %ui", ctx->ntracks); + + if (ctx->track->type == NGX_RTMP_MSG_AUDIO) { + if (ctx->atracks++ != ctx->aindex) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: skipping audio track %ui!=%ui", + ctx->atracks - 1, ctx->aindex); + ctx->track = NULL; + return NGX_OK; + } + + } else { + if (ctx->vtracks++ != ctx->vindex) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: skipping video track %i!=%i", + ctx->vtracks - 1, ctx->vindex); + ctx->track = NULL; + return NGX_OK; + } + } + + ++ctx->ntracks; + + } else { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: ignoring track %ui", ctx->ntracks); + } + + ctx->track = NULL; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_mdhd(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + uint8_t version; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx->track == NULL) { + return NGX_OK; + } + + t = ctx->track; + + if (pos + 1 > last) { + return NGX_ERROR; + } + + version = *(uint8_t *) pos; + + switch (version) { + case 0: + if (pos + 20 > last) { + return NGX_ERROR; + } + + pos += 12; + t->time_scale = ngx_rtmp_r32(*(uint32_t *) pos); + pos += 4; + t->duration = ngx_rtmp_r32(*(uint32_t *) pos); + break; + + case 1: + if (pos + 28 > last) { + return NGX_ERROR; + } + + pos += 20; + t->time_scale = ngx_rtmp_r32(*(uint32_t *) pos); + pos += 4; + t->duration = ngx_rtmp_r64(*(uint64_t *) pos); + break; + + default: + return NGX_ERROR; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: duration time_scale=%ui duration=%uL", + t->time_scale, t->duration); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + uint32_t type; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx->track == NULL) { + return NGX_OK; + } + + if (pos + 12 > last) { + return NGX_ERROR; + } + + type = *(uint32_t *)(pos + 8); + + if (type == ngx_rtmp_mp4_make_tag('v','i','d','e')) { + ctx->track->type = NGX_RTMP_MSG_VIDEO; + ctx->track->csid = NGX_RTMP_CSID_VIDEO; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: video track"); + + } else if (type == ngx_rtmp_mp4_make_tag('s','o','u','n')) { + ctx->track->type = NGX_RTMP_MSG_AUDIO; + ctx->track->csid = NGX_RTMP_CSID_AUDIO; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: audio track"); + } else { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: unknown track"); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_video(ngx_rtmp_session_t *s, u_char *pos, u_char *last, + ngx_int_t codec) +{ + ngx_rtmp_mp4_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx->track == NULL) { + return NGX_OK; + } + + ctx->track->codec = codec; + + if (pos + 78 > last) { + return NGX_ERROR; + } + + pos += 24; + + ctx->width = ngx_rtmp_r16(*(uint16_t *) pos); + + pos += 2; + + ctx->height = ngx_rtmp_r16(*(uint16_t *) pos); + + pos += 52; + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: video settings codec=%i, width=%ui, height=%ui", + codec, ctx->width, ctx->height); + + if (ngx_rtmp_mp4_parse(s, pos, last) != NGX_OK) { + return NGX_ERROR; + } + + ctx->track->fhdr = (u_char) ctx->track->codec; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_audio(ngx_rtmp_session_t *s, u_char *pos, u_char *last, + ngx_int_t codec) +{ + ngx_rtmp_mp4_ctx_t *ctx; + u_char *p; + ngx_uint_t version; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx->track == NULL) { + return NGX_OK; + } + + ctx->track->codec = codec; + + if (pos + 28 > last) { + return NGX_ERROR; + } + + pos += 8; + + version = ngx_rtmp_r16(*(uint16_t *) pos); + + pos += 8; + + ctx->nchannels = ngx_rtmp_r16(*(uint16_t *) pos); + + pos += 2; + + ctx->sample_size = ngx_rtmp_r16(*(uint16_t *) pos); + + pos += 6; + + ctx->sample_rate = ngx_rtmp_r16(*(uint16_t *) pos); + + pos += 4; + + p = &ctx->track->fhdr; + + *p = 0; + + if (ctx->nchannels == 2) { + *p |= 0x01; + } + + if (ctx->sample_size == 16) { + *p |= 0x02; + } + + switch (ctx->sample_rate) { + case 5512: + break; + + case 11025: + *p |= 0x04; + break; + + case 22050: + *p |= 0x08; + break; + + default: /*44100 etc */ + *p |= 0x0c; + break; + } + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: audio settings version=%ui, codec=%i, nchannels==%ui, " + "sample_size=%ui, sample_rate=%ui", + version, codec, ctx->nchannels, ctx->sample_size, + ctx->sample_rate); + + switch (version) { + case 1: + pos += 16; + break; + + case 2: + pos += 36; + } + + if (pos > last) { + return NGX_ERROR; + } + + if (ngx_rtmp_mp4_parse(s, pos, last) != NGX_OK) { + return NGX_ERROR; + } + + *p |= (ctx->track->codec << 4); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_avc1(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + return ngx_rtmp_mp4_parse_video(s, pos, last, NGX_RTMP_VIDEO_H264); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_mp4v(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + return ngx_rtmp_mp4_parse_video(s, pos, last, NGX_RTMP_VIDEO_H264); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_avcC(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + + if (pos == last) { + return NGX_OK; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx->track == NULL || ctx->track->codec != NGX_RTMP_VIDEO_H264) { + return NGX_OK; + } + + ctx->track->header = pos; + ctx->track->header_size = (size_t) (last - pos); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: video h264 header size=%uz", + ctx->track->header_size); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_mp4a(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + return ngx_rtmp_mp4_parse_audio(s, pos, last, NGX_RTMP_AUDIO_MP3); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_ds(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->header = pos; + t->header_size = (size_t) (last - pos); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: decoder header size=%uz", t->header_size); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_dc(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + uint8_t id; + ngx_rtmp_mp4_ctx_t *ctx; + ngx_int_t *pc; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx->track == NULL) { + return NGX_OK; + } + + if (pos + 13 > last) { + return NGX_ERROR; + } + + id = * (uint8_t *) pos; + pos += 13; + pc = &ctx->track->codec; + + switch (id) { + case 0x21: + *pc = NGX_RTMP_VIDEO_H264; + break; + + case 0x40: + case 0x66: + case 0x67: + case 0x68: + *pc = NGX_RTMP_AUDIO_AAC; + break; + + case 0x69: + case 0x6b: + *pc = NGX_RTMP_AUDIO_MP3; + break; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: decoder descriptor id=%i codec=%i", + (ngx_int_t) id, *pc); + + return ngx_rtmp_mp4_parse_descr(s, pos, last); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_es(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + uint16_t id; + uint8_t flags; + + if (pos + 3 > last) { + return NGX_ERROR; + } + + id = ngx_rtmp_r16(*(uint16_t *) pos); + pos += 2; + + flags = *(uint8_t *) pos; + ++pos; + + if (flags & 0x80) { /* streamDependenceFlag */ + pos += 2; + } + + if (flags & 0x40) { /* URL_FLag */ + return NGX_OK; + } + + if (flags & 0x20) { /* OCRstreamFlag */ + pos += 2; + } + + if (pos > last) { + return NGX_ERROR; + } + + (void) id; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: es descriptor es id=%i flags=%i", + (ngx_int_t) id, (ngx_int_t) flags); + + return ngx_rtmp_mp4_parse_descr(s, pos, last); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_descr(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + uint8_t tag, v; + uint32_t size; + ngx_uint_t n, ndesc; + ngx_rtmp_mp4_descriptor_t *ds; + + ndesc = sizeof(ngx_rtmp_mp4_descriptors) + / sizeof(ngx_rtmp_mp4_descriptors[0]); + + while (pos < last) { + tag = *(uint8_t *) pos++; + + for (size = 0, n = 0; n < 4; ++n) { + if (pos == last) { + return NGX_ERROR; + } + + v = *(uint8_t *) pos++; + + size = (size << 7) | (v & 0x7f); + + if (!(v & 0x80)) { + break; + } + } + + if (pos + size > last) { + return NGX_ERROR; + } + + ds = ngx_rtmp_mp4_descriptors;; + + for (n = 0; n < ndesc; ++n, ++ds) { + if (tag == ds->tag) { + break; + } + } + + if (n == ndesc) { + ds = NULL; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: descriptor%s tag=%i size=%uD", + ds ? "" : " unhandled", (ngx_int_t) tag, size); + + if (ds && ds->handler(s, pos, pos + size) != NGX_OK) { + return NGX_ERROR; + } + + pos += size; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_esds(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + if (pos + 4 > last) { + return NGX_ERROR; + } + + pos += 4; /* version */ + + return ngx_rtmp_mp4_parse_descr(s, pos, last); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_mp3(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + return ngx_rtmp_mp4_parse_audio(s, pos, last, NGX_RTMP_AUDIO_MP3); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_nmos(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + return ngx_rtmp_mp4_parse_audio(s, pos, last, NGX_RTMP_AUDIO_NELLY); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_spex(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + return ngx_rtmp_mp4_parse_audio(s, pos, last, NGX_RTMP_AUDIO_SPEEX); +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_stsd(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + if (pos + 8 > last) { + return NGX_ERROR; + } + + pos += 8; + + ngx_rtmp_mp4_parse(s, pos, last); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_stsc(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->chunks = (ngx_rtmp_mp4_chunks_t *) pos; + + if (pos + sizeof(*t->chunks) + ngx_rtmp_r32(t->chunks->entry_count) * + sizeof(t->chunks->entries[0]) + <= last) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: chunks entries=%uD", + ngx_rtmp_r32(t->chunks->entry_count)); + return NGX_OK; + } + + t->chunks = NULL; + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_stts(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->times = (ngx_rtmp_mp4_times_t *) pos; + + if (pos + sizeof(*t->times) + ngx_rtmp_r32(t->times->entry_count) * + sizeof(t->times->entries[0]) + <= last) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: times entries=%uD", + ngx_rtmp_r32(t->times->entry_count)); + return NGX_OK; + } + + t->times = NULL; + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_ctts(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->delays = (ngx_rtmp_mp4_delays_t *) pos; + + if (pos + sizeof(*t->delays) + ngx_rtmp_r32(t->delays->entry_count) * + sizeof(t->delays->entries[0]) + <= last) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: delays entries=%uD", + ngx_rtmp_r32(t->delays->entry_count)); + return NGX_OK; + } + + t->delays = NULL; + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_stss(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->keys = (ngx_rtmp_mp4_keys_t *) pos; + + if (pos + sizeof(*t->keys) + ngx_rtmp_r32(t->keys->entry_count) * + sizeof(t->keys->entries[0]) + <= last) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: keys entries=%uD", + ngx_rtmp_r32(t->keys->entry_count)); + return NGX_OK; + } + + t->keys = NULL; + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_stsz(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->sizes = (ngx_rtmp_mp4_sizes_t *) pos; + + if (pos + sizeof(*t->sizes) <= last && t->sizes->sample_size) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: sizes size=%uD", + ngx_rtmp_r32(t->sizes->sample_size)); + return NGX_OK; + } + + if (pos + sizeof(*t->sizes) + ngx_rtmp_r32(t->sizes->sample_count) * + sizeof(t->sizes->entries[0]) + <= last) + + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: sizes entries=%uD", + ngx_rtmp_r32(t->sizes->sample_count)); + return NGX_OK; + } + + t->sizes = NULL; + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_stz2(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->sizes2 = (ngx_rtmp_mp4_sizes2_t *) pos; + + if (pos + sizeof(*t->sizes) + ngx_rtmp_r32(t->sizes2->sample_count) * + ngx_rtmp_r32(t->sizes2->field_size) / 8 + <= last) + { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: sizes2 field_size=%uD entries=%uD", + ngx_rtmp_r32(t->sizes2->field_size), + ngx_rtmp_r32(t->sizes2->sample_count)); + return NGX_OK; + } + + t->sizes2 = NULL; + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_stco(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->offsets = (ngx_rtmp_mp4_offsets_t *) pos; + + if (pos + sizeof(*t->offsets) + ngx_rtmp_r32(t->offsets->entry_count) * + sizeof(t->offsets->entries[0]) + <= last) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: offsets entries=%uD", + ngx_rtmp_r32(t->offsets->entry_count)); + return NGX_OK; + } + + t->offsets = NULL; + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse_co64(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + t = ctx->track; + + if (t == NULL) { + return NGX_OK; + } + + t->offsets64 = (ngx_rtmp_mp4_offsets64_t *) pos; + + if (pos + sizeof(*t->offsets64) + ngx_rtmp_r32(t->offsets64->entry_count) * + sizeof(t->offsets64->entries[0]) + <= last) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: offsets64 entries=%uD", + ngx_rtmp_r32(t->offsets64->entry_count)); + return NGX_OK; + } + + t->offsets64 = NULL; + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_parse(ngx_rtmp_session_t *s, u_char *pos, u_char *last) +{ + uint32_t *hdr, tag; + size_t size, nboxes; + ngx_uint_t n; + ngx_rtmp_mp4_box_t *b; + + while (pos != last) { + if (pos + 8 > last) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: too small box: size=%i", last - pos); + return NGX_ERROR; + } + + hdr = (uint32_t *) pos; + size = ngx_rtmp_r32(hdr[0]); + if (size == 0) { + return NGX_ERROR; + } + + tag = hdr[1]; + + if (pos + size > last) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "mp4: too big box '%*s': size=%uz", + 4, &tag, size); + return NGX_ERROR; + } + + b = ngx_rtmp_mp4_boxes; + nboxes = sizeof(ngx_rtmp_mp4_boxes) / sizeof(ngx_rtmp_mp4_boxes[0]); + + for (n = 0; n < nboxes && b->tag != tag; ++n, ++b); + + if (n == nboxes) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: box unhandled '%*s'", 4, &tag); + } else { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: box '%*s'", 4, &tag); + b->handler(s, pos + 8, pos + size); + } + + pos += size; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_next_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_time_entry_t *te; + + if (t->times == NULL) { + return NGX_ERROR; + } + + cr = &t->cursor; + + if (cr->time_pos >= ngx_rtmp_r32(t->times->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui time[%ui/%uD] overflow", + t->id, cr->time_pos, + ngx_rtmp_r32(t->times->entry_count)); + + return NGX_ERROR; + } + + te = &t->times->entries[cr->time_pos]; + + cr->last_timestamp = cr->timestamp; + cr->timestamp += ngx_rtmp_r32(te->sample_delta); + + cr->not_first = 1; + + ngx_log_debug8(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui time[%ui] [%ui/%uD][%ui/%uD]=%uD t=%uD", + t->id, cr->pos, cr->time_pos, + ngx_rtmp_r32(t->times->entry_count), + cr->time_count, ngx_rtmp_r32(te->sample_count), + ngx_rtmp_r32(te->sample_delta), + cr->timestamp); + + cr->time_count++; + cr->pos++; + + if (cr->time_count >= ngx_rtmp_r32(te->sample_count)) { + cr->time_pos++; + cr->time_count = 0; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_seek_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, + uint32_t timestamp) +{ + ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_time_entry_t *te; + uint32_t dt; + + if (t->times == NULL) { + return NGX_ERROR; + } + + cr = &t->cursor; + + te = t->times->entries; + + while (cr->time_pos < ngx_rtmp_r32(t->times->entry_count)) { + dt = ngx_rtmp_r32(te->sample_delta) * ngx_rtmp_r32(te->sample_count); + + if (cr->timestamp + dt >= timestamp) { + if (te->sample_delta == 0) { + return NGX_ERROR; + } + + 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; + + break; + } + + cr->timestamp += dt; + cr->pos += ngx_rtmp_r32(te->sample_count); + cr->time_pos++; + te++; + } + + if (cr->time_pos >= ngx_rtmp_r32(t->times->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek time[%ui/%uD] overflow", + t->id, cr->time_pos, + ngx_rtmp_r32(t->times->entry_count)); + + return NGX_ERROR; + } + + ngx_log_debug8(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek time[%ui] [%ui/%uD][%ui/%uD]=%uD " + "t=%uD", + t->id, cr->pos, cr->time_pos, + ngx_rtmp_r32(t->times->entry_count), + cr->time_count, + ngx_rtmp_r32(te->sample_count), + ngx_rtmp_r32(te->sample_delta), + cr->timestamp); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + ngx_rtmp_mp4_cursor_t *cr; + ngx_uint_t chunk; + + cr = &t->cursor; + + if (cr->chunk < 1) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui offset[%ui] underflow", + t->id, cr->chunk); + return NGX_ERROR; + } + + chunk = cr->chunk - 1; + + if (t->offsets) { + if (chunk >= ngx_rtmp_r32(t->offsets->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui offset[%ui/%uD] overflow", + t->id, cr->chunk, + ngx_rtmp_r32(t->offsets->entry_count)); + + return NGX_ERROR; + } + + cr->offset = (off_t) ngx_rtmp_r32(t->offsets->entries[chunk]); + cr->size = 0; + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui offset[%ui/%uD]=%O", + t->id, cr->chunk, + ngx_rtmp_r32(t->offsets->entry_count), + cr->offset); + + return NGX_OK; + } + + if (t->offsets64) { + if (chunk >= ngx_rtmp_r32(t->offsets64->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui offset64[%ui/%uD] overflow", + t->id, cr->chunk, + ngx_rtmp_r32(t->offsets->entry_count)); + + return NGX_ERROR; + } + + cr->offset = (off_t) ngx_rtmp_r64(t->offsets64->entries[chunk]); + cr->size = 0; + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui offset64[%ui/%uD]=%O", + t->id, cr->chunk, + ngx_rtmp_r32(t->offsets->entry_count), + cr->offset); + + return NGX_OK; + } + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_next_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_chunk_entry_t *ce, *nce; + ngx_int_t new_chunk; + + if (t->chunks == NULL) { + return NGX_OK; + } + + cr = &t->cursor; + + if (cr->chunk_pos >= ngx_rtmp_r32(t->chunks->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui chunk[%ui/%uD] overflow", + t->id, cr->chunk_pos, + ngx_rtmp_r32(t->chunks->entry_count)); + + return NGX_ERROR; + } + + ce = &t->chunks->entries[cr->chunk_pos]; + + cr->chunk_count++; + + if (cr->chunk_count >= ngx_rtmp_r32(ce->samples_per_chunk)) { + cr->chunk_count = 0; + cr->chunk++; + + if (cr->chunk_pos + 1 < ngx_rtmp_r32(t->chunks->entry_count)) { + nce = ce + 1; + if (cr->chunk >= ngx_rtmp_r32(nce->first_chunk)) { + cr->chunk_pos++; + ce = nce; + } + } + + new_chunk = 1; + + } else { + new_chunk = 0; + } + + ngx_log_debug7(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui chunk[%ui/%uD][%uD..%ui][%ui/%uD]", + t->id, cr->chunk_pos, + ngx_rtmp_r32(t->chunks->entry_count), + ngx_rtmp_r32(ce->first_chunk), + cr->chunk, cr->chunk_count, + ngx_rtmp_r32(ce->samples_per_chunk)); + + + if (new_chunk) { + return ngx_rtmp_mp4_update_offset(s, t); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_seek_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_chunk_entry_t *ce, *nce; + ngx_uint_t pos, dpos, dchunk; + + cr = &t->cursor; + + if (t->chunks == NULL || t->chunks->entry_count == 0) { + cr->chunk = 1; + return NGX_OK; + } + + ce = t->chunks->entries; + pos = 0; + + while (cr->chunk_pos + 1 < ngx_rtmp_r32(t->chunks->entry_count)) { + nce = ce + 1; + + dpos = (ngx_rtmp_r32(nce->first_chunk) - + ngx_rtmp_r32(ce->first_chunk)) * + ngx_rtmp_r32(ce->samples_per_chunk); + + if (pos + dpos > cr->pos) { + break; + } + + pos += dpos; + ce++; + cr->chunk_pos++; + } + + if (ce->samples_per_chunk == 0) { + return NGX_ERROR; + } + + dchunk = (cr->pos - pos) / ngx_rtmp_r32(ce->samples_per_chunk); + + cr->chunk = ngx_rtmp_r32(ce->first_chunk) + dchunk; + cr->chunk_pos = (ngx_uint_t) (ce - t->chunks->entries); + cr->chunk_count = (ngx_uint_t) (cr->pos - pos - dchunk * + ngx_rtmp_r32(ce->samples_per_chunk)); + + ngx_log_debug7(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek chunk[%ui/%uD][%uD..%ui][%ui/%uD]", + t->id, cr->chunk_pos, + ngx_rtmp_r32(t->chunks->entry_count), + ngx_rtmp_r32(ce->first_chunk), + cr->chunk, cr->chunk_count, + ngx_rtmp_r32(ce->samples_per_chunk)); + + return ngx_rtmp_mp4_update_offset(s, t); +} + + +static ngx_int_t +ngx_rtmp_mp4_next_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + ngx_rtmp_mp4_cursor_t *cr; + + cr = &t->cursor; + + cr->offset += cr->size; + + if (t->sizes) { + if (t->sizes->sample_size) { + cr->size = ngx_rtmp_r32(t->sizes->sample_size); + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui size fix=%uz", + t->id, cr->size); + + return NGX_OK; + } + + cr->size_pos++; + + if (cr->size_pos >= ngx_rtmp_r32(t->sizes->sample_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui size[%ui/%uD] overflow", + t->id, cr->size_pos, + ngx_rtmp_r32(t->sizes->sample_count)); + + return NGX_ERROR; + } + + cr->size = ngx_rtmp_r32(t->sizes->entries[cr->size_pos]); + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui size[%ui/%uD]=%uz", + t->id, cr->size_pos, + ngx_rtmp_r32(t->sizes->sample_count), + cr->size); + + return NGX_OK; + } + + if (t->sizes2) { + if (cr->size_pos >= ngx_rtmp_r32(t->sizes2->sample_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui size[%ui/%uD] overflow", + t->id, cr->size_pos, + ngx_rtmp_r32(t->sizes2->sample_count)); + + return NGX_ERROR; + } + + /*TODO*/ + + return NGX_OK; + } + + return NGX_ERROR; +} + + +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); + + return NGX_OK; + } + + if (cr->pos >= ngx_rtmp_r32(t->sizes->sample_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek size[%ui/%uD] overflow", + t->id, cr->pos, + ngx_rtmp_r32(t->sizes->sample_count)); + + 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]); + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek size[%ui/%uD]=%uz", + t->id, cr->size_pos, + ngx_rtmp_r32(t->sizes->sample_count), + cr->size); + + return NGX_OK; + } + + if (t->sizes2) { + if (cr->size_pos >= ngx_rtmp_r32(t->sizes2->sample_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek size2[%ui/%uD] overflow", + t->id, cr->size_pos, + ngx_rtmp_r32(t->sizes->sample_count)); + + return NGX_ERROR; + } + + cr->size_pos = cr->pos; + + /* TODO */ + return NGX_OK; + } + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_next_key(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + ngx_rtmp_mp4_cursor_t *cr; + uint32_t *ke; + + cr = &t->cursor; + + if (t->keys == NULL) { + return NGX_OK; + } + + if (cr->key) { + cr->key_pos++; + } + + if (cr->key_pos >= ngx_rtmp_r32(t->keys->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui key[%ui/%uD] overflow", + t->id, cr->key_pos, + ngx_rtmp_r32(t->keys->entry_count)); + + cr->key = 0; + + return NGX_OK; + } + + ke = &t->keys->entries[cr->key_pos]; + cr->key = (cr->pos + 1 == ngx_rtmp_r32(*ke)); + + ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui key[%ui/%uD][%ui/%uD]=%s", + t->id, cr->key_pos, + ngx_rtmp_r32(t->keys->entry_count), + cr->pos, ngx_rtmp_r32(*ke), + cr->key ? "match" : "miss"); + + return NGX_OK; +} + + +static ngx_int_t +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; + + if (t->keys == NULL) { + return NGX_OK; + } + + while (cr->key_pos < ngx_rtmp_r32(t->keys->entry_count)) { + if (ngx_rtmp_r32(t->keys->entries[cr->key_pos]) > cr->pos) { + break; + } + + cr->key_pos++; + } + + if (cr->key_pos >= ngx_rtmp_r32(t->keys->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek key[%ui/%uD] overflow", + t->id, cr->key_pos, + ngx_rtmp_r32(t->keys->entry_count)); + return NGX_OK; + } + + ke = &t->keys->entries[cr->key_pos]; + /*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", + t->id, cr->key_pos, + ngx_rtmp_r32(t->keys->entry_count), + cr->pos, ngx_rtmp_r32(*ke), + cr->key ? "match" : "miss"); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_next_delay(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_delay_entry_t *de; + + cr = &t->cursor; + + if (t->delays == NULL) { + return NGX_OK; + } + + if (cr->delay_pos >= ngx_rtmp_r32(t->delays->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui delay[%ui/%uD] overflow", + t->id, cr->delay_pos, + ngx_rtmp_r32(t->delays->entry_count)); + + return NGX_OK; + } + + cr->delay_count++; + de = &t->delays->entries[cr->delay_pos]; + + if (cr->delay_count >= ngx_rtmp_r32(de->sample_count)) { + cr->delay_pos++; + de++; + cr->delay_count = 0; + } + + if (cr->delay_pos >= ngx_rtmp_r32(t->delays->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui delay[%ui/%uD] overflow", + t->id, cr->delay_pos, + ngx_rtmp_r32(t->delays->entry_count)); + + return NGX_OK; + } + + cr->delay = ngx_rtmp_r32(de->sample_offset); + + ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui delay[%ui/%uD][%ui/%uD]=%ui", + t->id, cr->delay_pos, + ngx_rtmp_r32(t->delays->entry_count), + cr->delay_count, + ngx_rtmp_r32(de->sample_count), cr->delay); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_seek_delay(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_delay_entry_t *de; + uint32_t pos, dpos; + + cr = &t->cursor; + + if (t->delays == NULL) { + return NGX_OK; + } + + pos = 0; + de = t->delays->entries; + + while (cr->delay_pos < ngx_rtmp_r32(t->delays->entry_count)) { + dpos = ngx_rtmp_r32(de->sample_count); + + if (pos + dpos > cr->pos) { + cr->delay_count = cr->pos - pos; + cr->delay = ngx_rtmp_r32(de->sample_offset); + break; + } + + cr->delay_pos++; + pos += dpos; + de++; + } + + if (cr->delay_pos >= ngx_rtmp_r32(t->delays->entry_count)) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek delay[%ui/%uD] overflow", + t->id, cr->delay_pos, + ngx_rtmp_r32(t->delays->entry_count)); + + return NGX_OK; + } + + ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui seek delay[%ui/%uD][%ui/%uD]=%ui", + t->id, cr->delay_pos, + ngx_rtmp_r32(t->delays->entry_count), + cr->delay_count, + ngx_rtmp_r32(de->sample_count), cr->delay); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_next(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + if (ngx_rtmp_mp4_next_time(s, t) != NGX_OK || + ngx_rtmp_mp4_next_key(s, t) != NGX_OK || + ngx_rtmp_mp4_next_chunk(s, t) != NGX_OK || + ngx_rtmp_mp4_next_size(s, t) != NGX_OK || + ngx_rtmp_mp4_next_delay(s, t) != NGX_OK) + { + t->cursor.valid = 0; + return NGX_ERROR; + } + + t->cursor.valid = 1; + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) +{ + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_int_t rc; + ngx_uint_t n; + ngx_rtmp_header_t h; + ngx_chain_t *out; + ngx_rtmp_mp4_track_t *t; + double d; + + static struct { + double width; + double height; + double duration; + double video_codec_id; + double audio_codec_id; + double audio_sample_rate; + } v; + + static ngx_rtmp_amf_elt_t out_inf[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_string("width"), + &v.width, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("height"), + &v.height, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("displayWidth"), + &v.width, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("displayHeight"), + &v.height, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("duration"), + &v.duration, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("videocodecid"), + &v.video_codec_id, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("audiocodecid"), + &v.audio_codec_id, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("audiosamplerate"), + &v.audio_sample_rate, 0 }, + }; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "onMetaData", 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + out_inf, sizeof(out_inf) }, + }; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + if (ctx == NULL) { + return NGX_OK; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + ngx_memzero(&v, sizeof(v)); + + v.width = ctx->width; + v.height = ctx->height; + v.audio_sample_rate = ctx->sample_rate; + + t = &ctx->tracks[0]; + for (n = 0; n < ctx->ntracks; ++n, ++t) { + d = ngx_rtmp_mp4_to_rtmp_timestamp(t, t->duration) / 1000.; + + if (v.duration < d) { + v.duration = d; + } + + switch (t->type) { + case NGX_RTMP_MSG_AUDIO: + v.audio_codec_id = t->codec; + break; + case NGX_RTMP_MSG_VIDEO: + v.video_codec_id = t->codec; + break; + } + } + + out = NULL; + rc = ngx_rtmp_append_amf(s, &out, NULL, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); + if (rc != NGX_OK || out == NULL) { + return NGX_ERROR; + } + + ngx_memzero(&h, sizeof(h)); + + 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, out); + rc = ngx_rtmp_send_message(s, out, 0); + ngx_rtmp_free_shared_chain(cscf, out); + + return rc; +} + + +static ngx_int_t +ngx_rtmp_mp4_seek_track(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, + ngx_int_t timestamp) +{ + ngx_rtmp_mp4_cursor_t *cr; + + cr = &t->cursor; + ngx_memzero(cr, sizeof(*cr)); + + if (ngx_rtmp_mp4_seek_time(s, t, ngx_rtmp_mp4_from_rtmp_timestamp( + t, timestamp)) != NGX_OK || + ngx_rtmp_mp4_seek_key(s, t) != NGX_OK || + ngx_rtmp_mp4_seek_chunk(s, t) != NGX_OK || + ngx_rtmp_mp4_seek_size(s, t) != NGX_OK || + ngx_rtmp_mp4_seek_delay(s, t) != NGX_OK) + { + return NGX_ERROR; + } + + cr->valid = 1; + return NGX_OK; +} + + +static ngx_int_t +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; + ngx_rtmp_header_t h, lh; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_chain_t *out, in; + ngx_rtmp_mp4_track_t *t, *cur_t; + ngx_rtmp_mp4_cursor_t *cr, *cur_cr; + uint32_t buflen, end_timestamp, + timestamp, last_timestamp, rdelay, + cur_timestamp; + ssize_t ret; + u_char fhdr[5]; + size_t fhdr_size; + ngx_int_t rc; + ngx_uint_t n, counter; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL) { + return NGX_ERROR; + } + + if (!ctx->meta_sent) { + rc = ngx_rtmp_mp4_send_meta(s); + + if (rc == NGX_OK) { + ctx->meta_sent = 1; + } + + return rc; + } + + buflen = s->buflen + NGX_RTMP_MP4_BUFLEN_ADDON; + + counter = 0; + last_timestamp = 0; + end_timestamp = ctx->start_timestamp + + (ngx_current_msec - ctx->epoch) + buflen; + + for ( ;; ) { + counter++; + if (counter > NGX_RTMP_MP4_MAX_FRAMES) { + return NGX_OK; + } + + timestamp = 0; + t = NULL; + + for (n = 0; n < ctx->ntracks; n++) { + cur_t = &ctx->tracks[n]; + cur_cr = &cur_t->cursor; + + if (!cur_cr->valid) { + continue; + } + + cur_timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(cur_t, + cur_cr->timestamp); + + if (t == NULL || cur_timestamp < timestamp) { + timestamp = cur_timestamp; + t = cur_t; + } + } + + if (t == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "mp4: no track"); + return NGX_DONE; + } + + if (timestamp > end_timestamp) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui ahead %uD > %uD", + t->id, timestamp, end_timestamp); + + if (ts) { + *ts = last_timestamp; + } + + return (uint32_t) (timestamp - end_timestamp); + } + + cr = &t->cursor; + + last_timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->last_timestamp); + + ngx_memzero(&h, sizeof(h)); + + h.msid = NGX_RTMP_MSID; + h.type = (uint8_t) t->type; + h.csid = t->csid; + + lh = h; + + h.timestamp = timestamp; + lh.timestamp = last_timestamp; + + ngx_memzero(&in, sizeof(in)); + ngx_memzero(&in_buf, sizeof(in_buf)); + + if (t->header && !t->header_sent) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui sending header of size=%uz", + t->id, t->header_size); + + fhdr[0] = t->fhdr; + fhdr[1] = 0; + + if (t->type == NGX_RTMP_MSG_VIDEO) { + fhdr[0] |= 0x10; + fhdr[2] = fhdr[3] = fhdr[4] = 0; + fhdr_size = 5; + } else { + fhdr_size = 2; + } + + in.buf = &in_buf; + in_buf.pos = fhdr; + in_buf.last = fhdr + fhdr_size; + + out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); + + in.buf = &in_buf; + in_buf.pos = t->header; + in_buf.last = t->header + t->header_size; + + ngx_rtmp_append_shared_bufs(cscf, out, &in); + + ngx_rtmp_prepare_message(s, &h, NULL, out); + rc = ngx_rtmp_send_message(s, out, 0); + ngx_rtmp_free_shared_chain(cscf, out); + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + t->header_sent = 1; + } + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui read frame offset=%O, size=%uz, " + "timestamp=%uD, last_timestamp=%uD", + t->id, cr->offset, cr->size, timestamp, + last_timestamp); + + ngx_rtmp_mp4_buffer[0] = t->fhdr; + fhdr_size = 1; + + if (t->type == NGX_RTMP_MSG_VIDEO) { + if (cr->key) { + ngx_rtmp_mp4_buffer[0] |= 0x10; + } else if (cr->delay) { + ngx_rtmp_mp4_buffer[0] |= 0x20; + } else { + ngx_rtmp_mp4_buffer[0] |= 0x30; + } + + 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] = (rdelay >> 16) & 0xff; + ngx_rtmp_mp4_buffer[3] = (rdelay >> 8) & 0xff; + ngx_rtmp_mp4_buffer[4] = rdelay & 0xff; + } + + } else { /* NGX_RTMP_MSG_AUDIO */ + if (t->header) { + fhdr_size = 2; + ngx_rtmp_mp4_buffer[1] = 1; + } + } + + if (cr->size + fhdr_size > sizeof(ngx_rtmp_mp4_buffer)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "mp4: track#%ui too big frame: %D>%uz", + t->id, cr->size, sizeof(ngx_rtmp_mp4_buffer)); + goto next; + } + + ret = ngx_read_file(f, ngx_rtmp_mp4_buffer + fhdr_size, + cr->size, cr->offset); + + if (ret != (ssize_t) cr->size) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "mp4: track#%ui could not read frame", t->id); + goto next; + } + + in.buf = &in_buf; + in_buf.pos = ngx_rtmp_mp4_buffer; + in_buf.last = ngx_rtmp_mp4_buffer + cr->size + fhdr_size; + + out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); + + ngx_rtmp_prepare_message(s, &h, cr->not_first ? &lh : NULL, out); + rc = ngx_rtmp_send_message(s, out, 0); + ngx_rtmp_free_shared_chain(cscf, out); + + if (rc == NGX_AGAIN) { + return NGX_AGAIN; + } + + s->current_time = timestamp; + +next: + if (ngx_rtmp_mp4_next(s, t) != NGX_OK) { + return NGX_DONE; + } + } +} + + +static ngx_int_t +ngx_rtmp_mp4_init(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_int_t aindex, + ngx_int_t vindex) +{ + ngx_rtmp_mp4_ctx_t *ctx; + uint32_t hdr[2]; + ssize_t n; + size_t offset, page_offset, size, shift; + uint64_t extended_size; + ngx_file_info_t fi; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL) { + ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_mp4_ctx_t)); + + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_mp4_module); + } + + ngx_memzero(ctx, sizeof(*ctx)); + + ctx->aindex = aindex; + ctx->vindex = vindex; + + offset = 0; + size = 0; + + for ( ;; ) { + n = ngx_read_file(f, (u_char *) &hdr, sizeof(hdr), offset); + + if (n != sizeof(hdr)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "mp4: error reading file at offset=%uz " + "while searching for moov box", offset); + return NGX_ERROR; + } + + size = (size_t) ngx_rtmp_r32(hdr[0]); + shift = sizeof(hdr); + + if (size == 1) { + n = ngx_read_file(f, (u_char *) &extended_size, + sizeof(extended_size), offset + sizeof(hdr)); + + if (n != sizeof(extended_size)) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "mp4: error reading file at offset=%uz " + "while searching for moov box", offset + 8); + return NGX_ERROR; + } + + size = (size_t) ngx_rtmp_r64(extended_size); + shift += sizeof(extended_size); + + } else if (size == 0) { + if (ngx_fd_info(f->fd, &fi) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "mp4: " ngx_fd_info_n " failed"); + return NGX_ERROR; + } + size = ngx_file_size(&fi) - offset; + } + + if (hdr[1] == ngx_rtmp_mp4_make_tag('m','o','o','v')) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: found moov box"); + break; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: skipping box '%*s'", 4, hdr + 1); + + offset += size; + } + + if (size < shift) { + return NGX_ERROR; + } + + size -= shift; + offset += shift; + + page_offset = offset & (ngx_pagesize - 1); + ctx->mmaped_size = page_offset + size; + + ctx->mmaped = ngx_rtmp_mp4_mmap(f->fd, ctx->mmaped_size, + offset - page_offset, &ctx->extra); + if (ctx->mmaped == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "mp4: mmap failed at offset=%ui, size=%uz", + offset, size); + return NGX_ERROR; + } + + return ngx_rtmp_mp4_parse(s, (u_char *) ctx->mmaped + page_offset, + (u_char *) ctx->mmaped + page_offset + size); +} + + +static ngx_int_t +ngx_rtmp_mp4_done(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 || ctx->mmaped == NULL) { + return NGX_OK; + } + + if (ngx_rtmp_mp4_munmap(ctx->mmaped, ctx->mmaped_size, &ctx->extra) + != NGX_OK) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "mp4: munmap failed"); + return NGX_ERROR; + } + + ctx->mmaped = NULL; + ctx->mmaped_size = 0; + + return NGX_OK; +} + + +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); + + if (ctx == NULL) { + return NGX_OK; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "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); + + ngx_rtmp_mp4_seek_track(s, &ctx->tracks[n], timestamp); + } + + ctx->start_timestamp = timestamp; + ctx->epoch = ngx_current_msec; + + 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; +} + + +static ngx_int_t +ngx_rtmp_mp4_stop(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; + } + + 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);*/ +} + + +static ngx_int_t +ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_play_main_conf_t *pmcf; + ngx_rtmp_play_fmt_t **pfmt, *fmt; + + pmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_play_module); + + pfmt = ngx_array_push(&pmcf->fmts); + + if (pfmt == NULL) { + return NGX_ERROR; + } + + fmt = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_play_fmt_t)); + + if (fmt == NULL) { + return NGX_ERROR; + } + + *pfmt = fmt; + + ngx_str_set(&fmt->name, "mp4-format"); + + ngx_str_set(&fmt->pfx, "mp4:"); + ngx_str_set(&fmt->sfx, ".mp4"); + + 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; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_netcall_module.c b/ngx_http_flv_module/ngx_rtmp_netcall_module.c new file mode 100644 index 0000000..0925248 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_netcall_module.c @@ -0,0 +1,733 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp_netcall_module.h" + + +static ngx_int_t ngx_rtmp_netcall_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_netcall_create_srv_conf(ngx_conf_t *cf); +static char * ngx_rtmp_netcall_merge_srv_conf(ngx_conf_t *cf, + void *parent, void *child); + +static void ngx_rtmp_netcall_close(ngx_connection_t *cc); +static void ngx_rtmp_netcall_detach(ngx_connection_t *cc); + +static void ngx_rtmp_netcall_recv(ngx_event_t *rev); +static void ngx_rtmp_netcall_send(ngx_event_t *wev); + + +typedef struct { + ngx_msec_t timeout; + size_t bufsize; + ngx_log_t *log; +} ngx_rtmp_netcall_srv_conf_t; + + +typedef struct ngx_rtmp_netcall_session_s { + ngx_rtmp_session_t *session; + ngx_peer_connection_t *pc; + ngx_url_t *url; + struct ngx_rtmp_netcall_session_s *next; + 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; + unsigned detached:1; + size_t bufsize; +} ngx_rtmp_netcall_session_t; + + +typedef struct { + ngx_uint_t nb_cs; + ngx_rtmp_netcall_session_t *cs; +} ngx_rtmp_netcall_ctx_t; + + +static ngx_command_t ngx_rtmp_netcall_commands[] = { + + { ngx_string("netcall_timeout"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_SRV_CONF_OFFSET, + offsetof(ngx_rtmp_netcall_srv_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_srv_conf_t, bufsize), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_netcall_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_netcall_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + ngx_rtmp_netcall_create_srv_conf, /* create server configuration */ + ngx_rtmp_netcall_merge_srv_conf, /* merge server configuration */ + NULL, /* create app configuration */ + NULL /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_netcall_module = { + NGX_MODULE_V1, + &ngx_rtmp_netcall_module_ctx, /* module context */ + ngx_rtmp_netcall_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static void * +ngx_rtmp_netcall_create_srv_conf(ngx_conf_t *cf) +{ + ngx_rtmp_netcall_srv_conf_t *nscf; + + nscf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_netcall_srv_conf_t)); + if (nscf == NULL) { + return NULL; + } + + nscf->timeout = NGX_CONF_UNSET_MSEC; + nscf->bufsize = NGX_CONF_UNSET_SIZE; + + nscf->log = &cf->cycle->new_log; + + return nscf; +} + + +static char * +ngx_rtmp_netcall_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_netcall_srv_conf_t *prev = parent; + ngx_rtmp_netcall_srv_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; +} + + +static ngx_int_t +ngx_rtmp_netcall_disconnect(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_netcall_ctx_t *ctx; + ngx_rtmp_netcall_session_t *cs; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_netcall_module); + + if (ctx) { + for (cs = ctx->cs; cs; cs = cs->next) { + ngx_rtmp_netcall_detach(cs->pc->connection); + } + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_netcall_get_peer(ngx_peer_connection_t *pc, void *data) +{ + ngx_rtmp_netcall_session_t *cs = data; + + pc->sockaddr =(struct sockaddr *)&cs->url->sockaddr; + pc->socklen = cs->url->socklen; + pc->name = &cs->url->host; + + return NGX_OK; +} + + +static void +ngx_rtmp_netcall_free_peer(ngx_peer_connection_t *pc, void *data, + ngx_uint_t state) +{ +} + + +ngx_int_t +ngx_rtmp_netcall_create(ngx_rtmp_session_t *s, ngx_rtmp_netcall_init_t *ci) +{ + ngx_rtmp_netcall_ctx_t *ctx; + ngx_peer_connection_t *pc; + ngx_rtmp_netcall_session_t *cs; + ngx_rtmp_netcall_srv_conf_t *nscf; + ngx_connection_t *c, *cc; + ngx_pool_t *pool; + ngx_int_t rc; + + pool = NULL; + c = s->connection; + + nscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_netcall_module); + if (nscf == NULL) { + goto error; + } + + /* get module context */ + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_netcall_module); + if (ctx == NULL) { + ctx = ngx_pcalloc(c->pool, + sizeof(ngx_rtmp_netcall_ctx_t)); + if (ctx == NULL) { + return NGX_ERROR; + } + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_netcall_module); + } else { + /* I don't know why? But it works! */ + if (ctx->nb_cs == 0) { + ctx->cs = NULL; + } + } + + /* Create netcall pool, connection, session. + * Note we use shared (app-wide) log because + * s->connection->log might be unavailable + * in detached netcall when it's being closed */ + pool = ngx_create_pool(4096, nscf->log); + if (pool == NULL) { + goto error; + } + + pc = ngx_pcalloc(pool, sizeof(ngx_peer_connection_t)); + if (pc == NULL) { + goto error; + } + + cs = ngx_pcalloc(pool, sizeof(ngx_rtmp_netcall_session_t)); + if (cs == NULL) { + goto error; + } + + /* copy arg to connection pool */ + if (ci->argsize) { + cs->arg = ngx_pcalloc(pool, ci->argsize); + if (cs->arg == NULL) { + goto error; + } + ngx_memcpy(cs->arg, ci->arg, ci->argsize); + } + + cs->timeout = nscf->timeout; + cs->bufsize = nscf->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; + } + + pc->log = nscf->log; + pc->get = ngx_rtmp_netcall_get_peer; + pc->free = ngx_rtmp_netcall_free_peer; + pc->data = cs; + + /* connect */ + rc = ngx_event_connect_peer(pc); + if (rc != NGX_OK && rc != NGX_AGAIN ) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "netcall: connection failed"); + goto error; + } + + cc = pc->connection; + cc->data = cs; + cc->pool = pool; + cs->pc = pc; + + cs->out = ci->create(s, ci->arg, pool); + if (cs->out == NULL) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "netcall: creation failed"); + ngx_close_connection(pc->connection); + goto error; + } + + cc->write->handler = ngx_rtmp_netcall_send; + cc->read->handler = ngx_rtmp_netcall_recv; + + if (!cs->detached) { + cs->next = ctx->cs; + ctx->cs = cs; + ctx->nb_cs++; + } + + ngx_rtmp_netcall_send(cc->write); + + return c->destroyed ? NGX_ERROR : NGX_OK; + +error: + if (pool) { + ngx_destroy_pool(pool); + } + + return NGX_ERROR; +} + + +static void +ngx_rtmp_netcall_close(ngx_connection_t *cc) +{ + ngx_rtmp_netcall_session_t *cs, **css; + ngx_pool_t *pool; + ngx_rtmp_session_t *s; + ngx_rtmp_netcall_ctx_t *ctx; + ngx_buf_t *b; + + cs = cc->data; + + if (cc->destroyed) { + return; + } + + cc->destroyed = 1; + + if (!cs->detached) { + 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; + ctx->nb_cs--; + break; + } + } + + if (cs->handle && cs->handle(s, cs->arg, cs->in) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + } + + pool = cc->pool; + ngx_close_connection(cc); + ngx_destroy_pool(pool); +} + + +static void +ngx_rtmp_netcall_detach(ngx_connection_t *cc) +{ + ngx_rtmp_netcall_session_t *cs; + + cs = cc->data; + cs->detached = 1; +} + + +static void +ngx_rtmp_netcall_recv(ngx_event_t *rev) +{ + ngx_rtmp_netcall_session_t *cs; + ngx_connection_t *cc; + ngx_chain_t *cl; + ngx_int_t n; + ngx_buf_t *b; + + cc = rev->data; + cs = cc->data; + + if (cc->destroyed) { + return; + } + + if (rev->timedout) { + cc->timedout = 1; + ngx_rtmp_netcall_close(cc); + return; + } + + if (rev->timer_set) { + ngx_del_timer(rev); + } + + for ( ;; ) { + + if (cs->inlast == NULL || + cs->inlast->buf->last == cs->inlast->buf->end) + { + if (cs->in && cs->sink) { + if (!cs->detached) { + if (cs->sink(cs->session, cs->in) != NGX_OK) { + ngx_rtmp_netcall_close(cc); + return; + } + } + + b = cs->in->buf; + b->pos = b->last = b->start; + + } else { + cl = ngx_alloc_chain_link(cc->pool); + if (cl == NULL) { + ngx_rtmp_netcall_close(cc); + return; + } + + 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; + + n = cc->recv(cc, b->last, b->end - b->last); + + if (n == NGX_ERROR || n == 0) { + ngx_rtmp_netcall_close(cc); + return; + } + + if (n == NGX_AGAIN) { + if (cs->filter && cs->in + && cs->filter(cs->in) != NGX_AGAIN) + { + ngx_rtmp_netcall_close(cc); + return; + } + + ngx_add_timer(rev, cs->timeout); + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_rtmp_netcall_close(cc); + } + return; + } + + b->last += n; + } +} + + +static void +ngx_rtmp_netcall_send(ngx_event_t *wev) +{ + ngx_rtmp_netcall_session_t *cs; + ngx_connection_t *cc; + ngx_chain_t *cl; + + cc = wev->data; + cs = cc->data; + + if (cc->destroyed) { + return; + } + + if (wev->timedout) { + ngx_log_error(NGX_LOG_INFO, cc->log, NGX_ETIMEDOUT, + "netcall: client send timed out"); + cc->timedout = 1; + ngx_rtmp_netcall_close(cc); + return; + } + + if (wev->timer_set) { + ngx_del_timer(wev); + } + + cl = cc->send_chain(cc, cs->out, 0); + + if (cl == NGX_CHAIN_ERROR) { + ngx_rtmp_netcall_close(cc); + return; + } + + cs->out = cl; + + /* more data to send? */ + if (cl) { + ngx_add_timer(wev, cs->timeout); + if (ngx_handle_write_event(wev, 0) != NGX_OK) { + ngx_rtmp_netcall_close(cc); + } + return; + } + + /* we've sent everything we had. + * now receive reply */ + ngx_del_event(wev, NGX_WRITE_EVENT, 0); + + ngx_rtmp_netcall_recv(cc->read); +} + + +ngx_chain_t * +ngx_rtmp_netcall_http_format_request(ngx_int_t method, ngx_str_t *host, + ngx_str_t *uri, ngx_chain_t *args, + ngx_chain_t *body, ngx_pool_t *pool, + ngx_str_t *content_type) +{ + ngx_chain_t *al, *bl, *ret; + ngx_buf_t *b; + size_t content_length; + static const char *methods[2] = { "GET", "POST" }; + static const char rq_tmpl[] = " HTTP/1.0\r\n" + "Host: %V\r\n" + "Content-Type: %V\r\n" + "Connection: Close\r\n" + "Content-Length: %uz\r\n" + "\r\n"; + + content_length = 0; + for (al = body; al; al = al->next) { + b = al->buf; + content_length += (b->last - b->pos); + } + + /* create first buffer */ + + al = ngx_alloc_chain_link(pool); + if (al == NULL) { + return NULL; + } + + b = ngx_create_temp_buf(pool, sizeof("POST") + /* longest method + 1 */ + uri->len); + if (b == NULL) { + return NULL; + } + + b->last = ngx_snprintf(b->last, b->end - b->last, "%s %V", + methods[method], uri); + + al->buf = b; + + ret = al; + + if (args) { + *b->last++ = '?'; + al->next = args; + for (al = args; al->next; al = al->next); + } + + /* create second buffer */ + + bl = ngx_alloc_chain_link(pool); + if (bl == NULL) { + return NULL; + } + + b = ngx_create_temp_buf(pool, sizeof(rq_tmpl) + host->len + + content_type->len + NGX_SIZE_T_LEN); + if (b == NULL) { + return NULL; + } + + bl->buf = b; + + b->last = ngx_snprintf(b->last, b->end - b->last, rq_tmpl, + host, content_type, content_length); + + al->next = bl; + bl->next = body; + + return ret; +} + + +ngx_chain_t * +ngx_rtmp_netcall_http_format_session(ngx_rtmp_session_t *s, ngx_pool_t *pool) +{ + ngx_chain_t *cl; + ngx_buf_t *b; + ngx_str_t *addr_text; + + addr_text = &s->connection->addr_text; + + cl = ngx_alloc_chain_link(pool); + if (cl == NULL) { + return NULL; + } + + b = ngx_create_temp_buf(pool, + sizeof("app=") - 1 + s->app.len * 3 + + sizeof("&flashver=") - 1 + s->flashver.len * 3 + + sizeof("&swfurl=") - 1 + s->swf_url.len * 3 + + sizeof("&tcurl=") - 1 + s->tc_url.len * 3 + + sizeof("&pageurl=") - 1 + s->page_url.len * 3 + + sizeof("&addr=") - 1 + addr_text->len * 3 + + sizeof("&clientid=") - 1 + NGX_INT_T_LEN + ); + + if (b == NULL) { + return NULL; + } + + cl->buf = b; + cl->next = NULL; + + b->last = ngx_cpymem(b->last, (u_char*) "app=", sizeof("app=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, s->app.data, s->app.len, + NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&flashver=", + sizeof("&flashver=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, s->flashver.data, + s->flashver.len, NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&swfurl=", + sizeof("&swfurl=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, s->swf_url.data, + s->swf_url.len, NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&tcurl=", + sizeof("&tcurl=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, s->tc_url.data, + s->tc_url.len, NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&pageurl=", + sizeof("&pageurl=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, s->page_url.data, + s->page_url.len, NGX_ESCAPE_ARGS); + + 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, NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&clientid=", + sizeof("&clientid=") - 1); + b->last = ngx_sprintf(b->last, "%ui", (ngx_uint_t) s->connection->number); + + return cl; +} + + +ngx_chain_t * +ngx_rtmp_netcall_http_skip_header(ngx_chain_t *in) +{ + ngx_buf_t *b; + + /* find \n[\r]\n */ + enum { + normal, + lf, + lfcr + } state = normal; + + if (in == NULL) { + return NULL; + } + + b = in->buf; + + for ( ;; ) { + + while (b->pos == b->last) { + in = in->next; + if (in == NULL) { + return NULL; + } + b = in->buf; + } + + switch (*b->pos++) { + case '\r': + state = (state == lf) ? lfcr : normal; + break; + + case '\n': + if (state != normal) { + return in; + } + state = lf; + break; + + default: + state = normal; + } + } +} + + +ngx_chain_t * +ngx_rtmp_netcall_memcache_set(ngx_rtmp_session_t *s, ngx_pool_t *pool, + ngx_str_t *key, ngx_str_t *value, ngx_uint_t flags, ngx_uint_t sec) +{ + ngx_chain_t *cl; + ngx_buf_t *b; + + cl = ngx_alloc_chain_link(pool); + if (cl == NULL) { + return NULL; + } + + b = ngx_create_temp_buf(pool, sizeof("set ") - 1 + key->len + + (1 + NGX_INT_T_LEN) * 3 + + (sizeof("\r\n") - 1) * 2 + value->len); + + if (b == NULL) { + return NULL; + } + + cl->next = NULL; + cl->buf = b; + + b->last = ngx_sprintf(b->pos, "set %V %ui %ui %ui\r\n%V\r\n", + key, flags, sec, (ngx_uint_t) value->len, value); + + return cl; +} + + +static ngx_int_t +ngx_rtmp_netcall_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + h = ngx_array_push(&cmcf->events[NGX_RTMP_DISCONNECT]); + *h = ngx_rtmp_netcall_disconnect; + + return NGX_OK; +} + diff --git a/ngx_http_flv_module/ngx_rtmp_netcall_module.h b/ngx_http_flv_module/ngx_rtmp_netcall_module.h new file mode 100644 index 0000000..4dfa6c0 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_netcall_module.h @@ -0,0 +1,67 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_NETCALL_H_INCLUDED_ +#define _NGX_RTMP_NETCALL_H_INCLUDED_ + + +#include +#include +#include "ngx_rtmp.h" + + +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); + +#define NGX_RTMP_NETCALL_HTTP_GET 0 +#define NGX_RTMP_NETCALL_HTTP_POST 1 + + +/* If handle is NULL then netcall is created detached + * which means it's completely independent of RTMP + * session and its result is never visible to anyone. + * + * WARNING: It's not recommended to create non-detached + * netcalls from disconect handlers. Netcall disconnect + * handler which detaches active netcalls is executed + * BEFORE your handler. It leads to a crash + * after netcall connection is closed */ +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; +} ngx_rtmp_netcall_init_t; + + +ngx_int_t ngx_rtmp_netcall_create(ngx_rtmp_session_t *s, + ngx_rtmp_netcall_init_t *ci); + + +/* 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_request(ngx_int_t method, + ngx_str_t *host, ngx_str_t *uri, ngx_chain_t *args, ngx_chain_t *body, + ngx_pool_t *pool, ngx_str_t *content_type); +ngx_chain_t * ngx_rtmp_netcall_http_skip_header(ngx_chain_t *in); + + +/* Memcache handling */ +ngx_chain_t * ngx_rtmp_netcall_memcache_set(ngx_rtmp_session_t *s, + ngx_pool_t *pool, ngx_str_t *key, ngx_str_t *value, + ngx_uint_t flags, ngx_uint_t sec); + + +#endif /* _NGX_RTMP_NETCALL_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_notify_module.c b/ngx_http_flv_module/ngx_rtmp_notify_module.c new file mode 100644 index 0000000..f27cdba --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_notify_module.c @@ -0,0 +1,1768 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_netcall_module.h" +#include "ngx_rtmp_record_module.h" +#include "ngx_rtmp_relay_module.h" + + +static ngx_rtmp_connect_pt next_connect; +static ngx_rtmp_disconnect_pt next_disconnect; +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_close_stream_pt next_close_stream; +static ngx_rtmp_record_done_pt next_record_done; + + +static char *ngx_rtmp_notify_on_srv_event(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_rtmp_notify_on_app_event(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_rtmp_notify_method(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static ngx_int_t ngx_rtmp_notify_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_notify_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_notify_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +static void *ngx_rtmp_notify_create_srv_conf(ngx_conf_t *cf); +static char *ngx_rtmp_notify_merge_srv_conf(ngx_conf_t *cf, void *parent, + void *child); +static ngx_int_t ngx_rtmp_notify_done(ngx_rtmp_session_t *s, char *cbname, + ngx_uint_t url_idx); + + +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 + + +enum { + NGX_RTMP_NOTIFY_CONNECT, + NGX_RTMP_NOTIFY_DISCONNECT, + NGX_RTMP_NOTIFY_SRV_MAX +}; + + +enum { + NGX_RTMP_NOTIFY_PLAY, + NGX_RTMP_NOTIFY_PUBLISH, + NGX_RTMP_NOTIFY_PLAY_DONE, + NGX_RTMP_NOTIFY_PUBLISH_DONE, + NGX_RTMP_NOTIFY_DONE, + NGX_RTMP_NOTIFY_RECORD_DONE, + NGX_RTMP_NOTIFY_UPDATE, + NGX_RTMP_NOTIFY_APP_MAX +}; + + +typedef struct { + ngx_url_t *url[NGX_RTMP_NOTIFY_APP_MAX]; + ngx_flag_t active; + ngx_uint_t method; + ngx_msec_t update_timeout; + ngx_flag_t update_strict; + ngx_flag_t relay_redirect; + ngx_flag_t no_resolve; +} ngx_rtmp_notify_app_conf_t; + + +typedef struct { + ngx_url_t *url[NGX_RTMP_NOTIFY_SRV_MAX]; + ngx_uint_t method; +} ngx_rtmp_notify_srv_conf_t; + + +typedef struct { + ngx_uint_t flags; + u_char name[NGX_RTMP_MAX_NAME]; + u_char args[NGX_RTMP_MAX_ARGS]; + ngx_event_t update_evt; + time_t start; +} ngx_rtmp_notify_ctx_t; + + +typedef struct { + u_char *cbname; + ngx_uint_t url_idx; +} ngx_rtmp_notify_done_t; + + +static ngx_command_t ngx_rtmp_notify_commands[] = { + + { ngx_string("on_connect"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_on_srv_event, + NGX_RTMP_SRV_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("on_disconnect"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_on_srv_event, + NGX_RTMP_SRV_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("on_publish"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_on_app_event, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("on_play"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_on_app_event, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("on_publish_done"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_on_app_event, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("on_play_done"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_on_app_event, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("on_done"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_on_app_event, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("on_record_done"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_RTMP_REC_CONF| + NGX_CONF_TAKE1, + ngx_rtmp_notify_on_app_event, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("on_update"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_on_app_event, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("notify_method"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_rtmp_notify_method, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("notify_update_timeout"), + 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_notify_app_conf_t, update_timeout), + NULL }, + + { ngx_string("notify_update_strict"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_notify_app_conf_t, update_strict), + NULL }, + + { ngx_string("notify_relay_redirect"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_notify_app_conf_t, relay_redirect), + NULL }, + + { ngx_string("notify_no_resolve"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_notify_app_conf_t, no_resolve), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_notify_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_notify_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + ngx_rtmp_notify_create_srv_conf, /* create server configuration */ + ngx_rtmp_notify_merge_srv_conf, /* merge server configuration */ + ngx_rtmp_notify_create_app_conf, /* create app configuration */ + ngx_rtmp_notify_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_notify_module = { + NGX_MODULE_V1, + &ngx_rtmp_notify_module_ctx, /* module context */ + ngx_rtmp_notify_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static void * +ngx_rtmp_notify_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_notify_app_conf_t *nacf; + ngx_uint_t n; + + nacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_notify_app_conf_t)); + if (nacf == NULL) { + return NULL; + } + + for (n = 0; n < NGX_RTMP_NOTIFY_APP_MAX; ++n) { + nacf->url[n] = NGX_CONF_UNSET_PTR; + } + + nacf->method = NGX_CONF_UNSET_UINT; + nacf->update_timeout = NGX_CONF_UNSET_MSEC; + nacf->update_strict = NGX_CONF_UNSET; + nacf->relay_redirect = NGX_CONF_UNSET; + nacf->no_resolve = NGX_CONF_UNSET; + + return nacf; +} + + +static char * +ngx_rtmp_notify_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_notify_app_conf_t *prev = parent; + ngx_rtmp_notify_app_conf_t *conf = child; + ngx_uint_t n; + + for (n = 0; n < NGX_RTMP_NOTIFY_APP_MAX; ++n) { + ngx_conf_merge_ptr_value(conf->url[n], prev->url[n], NULL); + if (conf->url[n]) { + conf->active = 1; + } + } + + if (conf->active) { + prev->active = 1; + } + + ngx_conf_merge_uint_value(conf->method, prev->method, + NGX_RTMP_NETCALL_HTTP_POST); + ngx_conf_merge_msec_value(conf->update_timeout, prev->update_timeout, + 30000); + ngx_conf_merge_value(conf->update_strict, prev->update_strict, 0); + ngx_conf_merge_value(conf->relay_redirect, prev->relay_redirect, 0); + ngx_conf_merge_value(conf->no_resolve, prev->no_resolve, 1); + + return NGX_CONF_OK; +} + + +static void * +ngx_rtmp_notify_create_srv_conf(ngx_conf_t *cf) +{ + ngx_rtmp_notify_srv_conf_t *nscf; + ngx_uint_t n; + + nscf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_notify_srv_conf_t)); + if (nscf == NULL) { + return NULL; + } + + for (n = 0; n < NGX_RTMP_NOTIFY_SRV_MAX; ++n) { + nscf->url[n] = NGX_CONF_UNSET_PTR; + } + + nscf->method = NGX_CONF_UNSET_UINT; + + return nscf; +} + + +static char * +ngx_rtmp_notify_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_notify_srv_conf_t *prev = parent; + ngx_rtmp_notify_srv_conf_t *conf = child; + ngx_uint_t n; + + for (n = 0; n < NGX_RTMP_NOTIFY_SRV_MAX; ++n) { + ngx_conf_merge_ptr_value(conf->url[n], prev->url[n], NULL); + } + + ngx_conf_merge_uint_value(conf->method, prev->method, + NGX_RTMP_NETCALL_HTTP_POST); + + return NGX_CONF_OK; +} + + +static ngx_chain_t * +ngx_rtmp_notify_create_request(ngx_rtmp_session_t *s, ngx_pool_t *pool, + ngx_uint_t url_idx, ngx_chain_t *args) +{ + ngx_rtmp_notify_app_conf_t *nacf; + ngx_chain_t *al, *bl, *cl; + ngx_url_t *url; + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + + url = nacf->url[url_idx]; + + al = ngx_rtmp_netcall_http_format_session(s, pool); + if (al == NULL) { + return NULL; + } + + al->next = args; + + bl = NULL; + + if (nacf->method == NGX_RTMP_NETCALL_HTTP_POST) { + cl = al; + al = bl; + bl = cl; + } + + return ngx_rtmp_netcall_http_format_request(nacf->method, &url->host, + &url->uri, al, bl, pool, + &ngx_rtmp_notify_urlencoded); +} + + +static ngx_chain_t * +ngx_rtmp_notify_connect_create(ngx_rtmp_session_t *s, void *arg, + ngx_pool_t *pool) +{ + ngx_rtmp_connect_t *v = arg; + + ngx_rtmp_notify_srv_conf_t *nscf; + ngx_url_t *url; + ngx_chain_t *al, *bl; + ngx_buf_t *b; + ngx_str_t *addr_text; + size_t app_len, args_len, flashver_len, + swf_url_len, tc_url_len, page_url_len; + + nscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_notify_module); + + al = ngx_alloc_chain_link(pool); + if (al == NULL) { + return NULL; + } + + /* these values are still missing in session + * so we have to construct the request from + * connection struct */ + + app_len = ngx_strlen(v->app); + args_len = ngx_strlen(v->args); + flashver_len = ngx_strlen(v->flashver); + swf_url_len = ngx_strlen(v->swf_url); + tc_url_len = ngx_strlen(v->tc_url); + page_url_len = ngx_strlen(v->page_url); + + addr_text = &s->connection->addr_text; + + b = ngx_create_temp_buf(pool, + sizeof("call=connect") - 1 + + sizeof("&app=") - 1 + app_len * 3 + + sizeof("&flashver=") - 1 + flashver_len * 3 + + sizeof("&swfurl=") - 1 + swf_url_len * 3 + + sizeof("&tcurl=") - 1 + tc_url_len * 3 + + sizeof("&pageurl=") - 1 + page_url_len * 3 + + sizeof("&addr=") - 1 + addr_text->len * 3 + + sizeof("&epoch=") - 1 + NGX_INT32_LEN + + 1 + args_len + ); + + if (b == NULL) { + return NULL; + } + + al->buf = b; + al->next = NULL; + + b->last = ngx_cpymem(b->last, (u_char*) "app=", sizeof("app=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->app, app_len, + NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&flashver=", + sizeof("&flashver=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->flashver, flashver_len, + NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&swfurl=", + sizeof("&swfurl=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->swf_url, swf_url_len, + NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&tcurl=", + sizeof("&tcurl=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->tc_url, tc_url_len, + NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&pageurl=", + sizeof("&pageurl=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->page_url, page_url_len, + NGX_ESCAPE_ARGS); + + 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, NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&epoch=", sizeof("&epoch=") -1); + b->last = ngx_sprintf(b->last, "%uD", (uint32_t) s->epoch); + + b->last = ngx_cpymem(b->last, (u_char*) "&call=connect", + sizeof("&call=connect") - 1); + + if (args_len) { + *b->last++ = '&'; + b->last = (u_char *) ngx_cpymem(b->last, v->args, args_len); + } + + url = nscf->url[NGX_RTMP_NOTIFY_CONNECT]; + + bl = NULL; + + if (nscf->method == NGX_RTMP_NETCALL_HTTP_POST) { + bl = al; + al = NULL; + } + + return ngx_rtmp_netcall_http_format_request(nscf->method, &url->host, + &url->uri, al, bl, pool, + &ngx_rtmp_notify_urlencoded); +} + + +static ngx_chain_t * +ngx_rtmp_notify_disconnect_create(ngx_rtmp_session_t *s, void *arg, + ngx_pool_t *pool) +{ + ngx_rtmp_notify_srv_conf_t *nscf; + ngx_url_t *url; + ngx_chain_t *al, *bl, *pl; + ngx_buf_t *b; + + nscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_notify_module); + + pl = ngx_alloc_chain_link(pool); + if (pl == NULL) { + return NULL; + } + + b = ngx_create_temp_buf(pool, + sizeof("&call=disconnect") + + sizeof("&app=") + s->app.len * 3 + + 1 + s->args.len); + if (b == NULL) { + return NULL; + } + + pl->buf = b; + pl->next = NULL; + + b->last = ngx_cpymem(b->last, (u_char*) "&call=disconnect", + sizeof("&call=disconnect") - 1); + + b->last = ngx_cpymem(b->last, (u_char*) "&app=", sizeof("&app=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, s->app.data, s->app.len, + NGX_ESCAPE_ARGS); + + if (s->args.len) { + *b->last++ = '&'; + b->last = (u_char *) ngx_cpymem(b->last, s->args.data, s->args.len); + } + + url = nscf->url[NGX_RTMP_NOTIFY_DISCONNECT]; + + al = ngx_rtmp_netcall_http_format_session(s, pool); + if (al == NULL) { + return NULL; + } + + al->next = pl; + + bl = NULL; + + if (nscf->method == NGX_RTMP_NETCALL_HTTP_POST) { + bl = al; + al = NULL; + } + + return ngx_rtmp_netcall_http_format_request(nscf->method, &url->host, + &url->uri, al, bl, pool, + &ngx_rtmp_notify_urlencoded); +} + + +static ngx_chain_t * +ngx_rtmp_notify_publish_create(ngx_rtmp_session_t *s, void *arg, + ngx_pool_t *pool) +{ + ngx_rtmp_publish_t *v = arg; + + ngx_chain_t *pl; + ngx_buf_t *b; + size_t name_len, type_len, args_len; + + pl = ngx_alloc_chain_link(pool); + if (pl == NULL) { + return NULL; + } + + name_len = ngx_strlen(v->name); + type_len = ngx_strlen(v->type); + args_len = ngx_strlen(v->args); + + b = ngx_create_temp_buf(pool, + sizeof("&call=publish") + + sizeof("&name=") + name_len * 3 + + sizeof("&type=") + type_len * 3 + + 1 + args_len); + if (b == NULL) { + return NULL; + } + + pl->buf = b; + pl->next = NULL; + + b->last = ngx_cpymem(b->last, (u_char*) "&call=publish", + sizeof("&call=publish") - 1); + + b->last = ngx_cpymem(b->last, (u_char*) "&name=", sizeof("&name=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->name, name_len, + NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&type=", sizeof("&type=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->type, type_len, + NGX_ESCAPE_ARGS); + + if (args_len) { + *b->last++ = '&'; + b->last = (u_char *) ngx_cpymem(b->last, v->args, args_len); + } + + return ngx_rtmp_notify_create_request(s, pool, NGX_RTMP_NOTIFY_PUBLISH, pl); +} + + +static ngx_chain_t * +ngx_rtmp_notify_play_create(ngx_rtmp_session_t *s, void *arg, + ngx_pool_t *pool) +{ + ngx_rtmp_play_t *v = arg; + + ngx_chain_t *pl; + ngx_buf_t *b; + size_t name_len, args_len; + + pl = ngx_alloc_chain_link(pool); + if (pl == NULL) { + return NULL; + } + + name_len = ngx_strlen(v->name); + args_len = ngx_strlen(v->args); + + b = ngx_create_temp_buf(pool, + sizeof("&call=play") + + sizeof("&name=") + name_len * 3 + + sizeof("&start=&duration=&reset=") + + NGX_INT32_LEN * 3 + 1 + args_len); + if (b == NULL) { + return NULL; + } + + pl->buf = b; + pl->next = NULL; + + b->last = ngx_cpymem(b->last, (u_char*) "&call=play", + sizeof("&call=play") - 1); + + b->last = ngx_cpymem(b->last, (u_char*) "&name=", sizeof("&name=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->name, name_len, + NGX_ESCAPE_ARGS); + + b->last = ngx_snprintf(b->last, b->end - b->last, + "&start=%uD&duration=%uD&reset=%d", + (uint32_t) v->start, (uint32_t) v->duration, + v->reset & 1); + + if (args_len) { + *b->last++ = '&'; + b->last = (u_char *) ngx_cpymem(b->last, v->args, args_len); + } + + return ngx_rtmp_notify_create_request(s, pool, NGX_RTMP_NOTIFY_PLAY, pl); +} + + +static ngx_chain_t * +ngx_rtmp_notify_done_create(ngx_rtmp_session_t *s, void *arg, + ngx_pool_t *pool) +{ + ngx_rtmp_notify_done_t *ds = arg; + + ngx_chain_t *pl; + ngx_buf_t *b; + size_t cbname_len, name_len, args_len; + ngx_rtmp_notify_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_notify_module); + + pl = ngx_alloc_chain_link(pool); + if (pl == NULL) { + return NULL; + } + + cbname_len = ngx_strlen(ds->cbname); + name_len = ctx ? ngx_strlen(ctx->name) : 0; + args_len = ctx ? ngx_strlen(ctx->args) : 0; + + b = ngx_create_temp_buf(pool, + sizeof("&call=") + cbname_len + + sizeof("&name=") + name_len * 3 + + 1 + args_len); + if (b == NULL) { + return NULL; + } + + pl->buf = b; + pl->next = NULL; + + b->last = ngx_cpymem(b->last, (u_char*) "&call=", sizeof("&call=") - 1); + b->last = ngx_cpymem(b->last, ds->cbname, cbname_len); + + if (name_len) { + b->last = ngx_cpymem(b->last, (u_char*) "&name=", sizeof("&name=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, ctx->name, name_len, + NGX_ESCAPE_ARGS); + } + + if (args_len) { + *b->last++ = '&'; + b->last = (u_char *) ngx_cpymem(b->last, ctx->args, args_len); + } + + return ngx_rtmp_notify_create_request(s, pool, ds->url_idx, pl); +} + + +static ngx_chain_t * +ngx_rtmp_notify_update_create(ngx_rtmp_session_t *s, void *arg, + ngx_pool_t *pool) +{ + ngx_chain_t *pl; + ngx_buf_t *b; + size_t name_len, args_len; + ngx_rtmp_notify_ctx_t *ctx; + ngx_str_t sfx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_notify_module); + + pl = ngx_alloc_chain_link(pool); + if (pl == NULL) { + return NULL; + } + + if (ctx->flags & NGX_RTMP_NOTIFY_PUBLISHING) { + ngx_str_set(&sfx, "_publish"); + } else if (ctx->flags & NGX_RTMP_NOTIFY_PLAYING) { + ngx_str_set(&sfx, "_play"); + } else { + ngx_str_null(&sfx); + } + + name_len = ctx ? ngx_strlen(ctx->name) : 0; + args_len = ctx ? ngx_strlen(ctx->args) : 0; + + b = ngx_create_temp_buf(pool, + sizeof("&call=update") + sfx.len + + sizeof("&time=") + NGX_TIME_T_LEN + + sizeof("×tamp=") + NGX_INT32_LEN + + sizeof("&name=") + name_len * 3 + + 1 + args_len); + if (b == NULL) { + return NULL; + } + + pl->buf = b; + pl->next = NULL; + + b->last = ngx_cpymem(b->last, (u_char*) "&call=update", + sizeof("&call=update") - 1); + b->last = ngx_cpymem(b->last, sfx.data, sfx.len); + + b->last = ngx_cpymem(b->last, (u_char *) "&time=", + sizeof("&time=") - 1); + b->last = ngx_sprintf(b->last, "%T", ngx_cached_time->sec - ctx->start); + + b->last = ngx_cpymem(b->last, (u_char *) "×tamp=", + sizeof("×tamp=") - 1); + b->last = ngx_sprintf(b->last, "%D", s->current_time); + + if (name_len) { + b->last = ngx_cpymem(b->last, (u_char*) "&name=", sizeof("&name=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, ctx->name, name_len, + NGX_ESCAPE_ARGS); + } + + if (args_len) { + *b->last++ = '&'; + b->last = (u_char *) ngx_cpymem(b->last, ctx->args, args_len); + } + + return ngx_rtmp_notify_create_request(s, pool, NGX_RTMP_NOTIFY_UPDATE, pl); +} + + +static ngx_chain_t * +ngx_rtmp_notify_record_done_create(ngx_rtmp_session_t *s, void *arg, + ngx_pool_t *pool) +{ + ngx_rtmp_record_done_t *v = arg; + + ngx_rtmp_notify_ctx_t *ctx; + ngx_chain_t *pl; + ngx_buf_t *b; + size_t name_len, args_len; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_notify_module); + + pl = ngx_alloc_chain_link(pool); + if (pl == NULL) { + return NULL; + } + + name_len = ngx_strlen(ctx->name); + args_len = ngx_strlen(ctx->args); + + b = ngx_create_temp_buf(pool, + sizeof("&call=record_done") + + sizeof("&recorder=") + v->recorder.len + + sizeof("&name=") + name_len * 3 + + sizeof("&path=") + v->path.len * 3 + + 1 + args_len); + if (b == NULL) { + return NULL; + } + + pl->buf = b; + pl->next = NULL; + + b->last = ngx_cpymem(b->last, (u_char*) "&call=record_done", + sizeof("&call=record_done") - 1); + + b->last = ngx_cpymem(b->last, (u_char *) "&recorder=", + sizeof("&recorder=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->recorder.data, + v->recorder.len, NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&name=", sizeof("&name=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, ctx->name, name_len, + NGX_ESCAPE_ARGS); + + b->last = ngx_cpymem(b->last, (u_char*) "&path=", sizeof("&path=") - 1); + b->last = (u_char*) ngx_escape_uri(b->last, v->path.data, v->path.len, + NGX_ESCAPE_ARGS); + + if (args_len) { + *b->last++ = '&'; + b->last = (u_char *) ngx_cpymem(b->last, ctx->args, args_len); + } + + return ngx_rtmp_notify_create_request(s, pool, NGX_RTMP_NOTIFY_RECORD_DONE, + pl); +} + + +static ngx_int_t +ngx_rtmp_notify_parse_http_retcode(ngx_rtmp_session_t *s, + ngx_chain_t *in) +{ + ngx_buf_t *b; + ngx_int_t n; + u_char c; + + /* find 10th character */ + + n = 9; + while (in) { + b = in->buf; + if (b->last - b->pos > n) { + c = b->pos[n]; + if (c >= (u_char)'0' && c <= (u_char)'9') { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "notify: HTTP retcode: %dxx", (int)(c - '0')); + switch (c) { + case (u_char) '2': + return NGX_OK; + case (u_char) '3': + return NGX_AGAIN; + default: + return NGX_ERROR; + } + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: invalid HTTP retcode: %d..", (int)c); + + return NGX_ERROR; + } + n -= (b->last - b->pos); + in = in->next; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: empty or broken HTTP response"); + + /* + * not enough data; + * it can happen in case of empty or broken reply + */ + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_notify_parse_http_header(ngx_rtmp_session_t *s, + ngx_chain_t *in, ngx_str_t *name, u_char *data, size_t len) +{ + ngx_buf_t *b; + ngx_int_t matched; + u_char *p, c; + ngx_uint_t n; + + enum { + parse_name, + parse_space, + parse_value, + parse_value_newline + } state = parse_name; + + n = 0; + matched = 0; + + while (in) { + b = in->buf; + + for (p = b->pos; p != b->last; ++p) { + c = *p; + + if (c == '\r') { + continue; + } + + switch (state) { + case parse_value_newline: + if (c == ' ' || c == '\t') { + state = parse_space; + break; + } + + if (matched) { + return n; + } + + if (c == '\n') { + return NGX_OK; + } + + n = 0; + state = parse_name; + + /* fall through */ + + case parse_name: + switch (c) { + case ':': + matched = (n == name->len); + n = 0; + state = parse_space; + break; + case '\n': + n = 0; + break; + default: + if (n < name->len && + ngx_tolower(c) == ngx_tolower(name->data[n])) + { + ++n; + break; + } + n = name->len + 1; + } + break; + + case parse_space: + if (c == ' ' || c == '\t') { + break; + } + state = parse_value; + + /* fall through */ + + case parse_value: + if (c == '\n') { + state = parse_value_newline; + break; + } + + if (matched && n + 1 < len) { + data[n++] = c; + } + + break; + } + } + + in = in->next; + } + + return NGX_OK; +} + + +static void +ngx_rtmp_notify_clear_flag(ngx_rtmp_session_t *s, ngx_uint_t flag) +{ + ngx_rtmp_notify_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_notify_module); + + ctx->flags &= ~flag; +} + + +static ngx_int_t +ngx_rtmp_notify_connect_handle(ngx_rtmp_session_t *s, + void *arg, ngx_chain_t *in) +{ + ngx_rtmp_connect_t *v = arg; + ngx_http_request_t *r; + ngx_int_t rc; + u_char app[NGX_RTMP_MAX_NAME]; + + static ngx_rtmp_play_t p; + static ngx_str_t location = ngx_string("location"); + + rc = ngx_rtmp_notify_parse_http_retcode(s, in); + if (rc == NGX_ERROR) { + return NGX_ERROR; + } + + if (rc == NGX_AGAIN) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "notify: connect redirect received"); + + rc = ngx_rtmp_notify_parse_http_header(s, in, &location, app, + sizeof(app) - 1); + if (rc > 0) { + *ngx_cpymem(v->app, app, rc) = 0; + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: connect redirect to '%s'", v->app); + } + } + + rc = next_connect(s, v); + if (rc == NGX_OK && s->notify_connect) { + r = s->data; + if (r) { + ngx_memzero(&p, sizeof(ngx_rtmp_play_t)); + ngx_memcpy(p.name, s->stream.data, + ngx_min(s->stream.len, NGX_RTMP_MAX_NAME - 1)); + ngx_memcpy(p.args, s->args.data, + ngx_min(s->args.len, NGX_RTMP_MAX_ARGS - 1)); + + rc = ngx_rtmp_play(s, &p); + } + } + + s->notify_connect = 0; + + return rc; +} + + +static void +ngx_rtmp_notify_set_name(u_char *dst, size_t dst_len, u_char *src, + size_t src_len) +{ + u_char result[16], *p; + ngx_md5_t md5; + + ngx_md5_init(&md5); + ngx_md5_update(&md5, src, src_len); + ngx_md5_final(result, &md5); + + p = ngx_hex_dump(dst, result, ngx_min((dst_len - 1) / 2, 16)); + *p = '\0'; +} + + +static ngx_int_t +ngx_rtmp_notify_publish_handle(ngx_rtmp_session_t *s, + void *arg, ngx_chain_t *in) +{ + ngx_rtmp_publish_t *v = arg; + ngx_int_t rc; + ngx_str_t local_name; + ngx_rtmp_relay_target_t target; + ngx_url_t *u; + ngx_rtmp_notify_app_conf_t *nacf; + u_char name[NGX_RTMP_MAX_NAME]; + + static ngx_str_t location = ngx_string("location"); + + rc = ngx_rtmp_notify_parse_http_retcode(s, in); + if (rc == NGX_ERROR) { + ngx_rtmp_notify_clear_flag(s, NGX_RTMP_NOTIFY_PUBLISHING); + return NGX_ERROR; + } + + if (rc != NGX_AGAIN) { + goto next; + } + + /* HTTP 3xx */ + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "notify: publish redirect received"); + + rc = ngx_rtmp_notify_parse_http_header(s, in, &location, name, + sizeof(name) - 1); + if (rc <= 0) { + goto next; + } + + if (ngx_strncasecmp(name, (u_char *) "rtmp://", 7)) { + *ngx_cpymem(v->name, name, rc) = 0; + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: publish redirect to '%s'", v->name); + goto next; + } + + /* push */ + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + if (nacf->relay_redirect) { + ngx_rtmp_notify_set_name(v->name, NGX_RTMP_MAX_NAME, name, (size_t) rc); + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: push '%s' to '%*s'", v->name, rc, name); + + local_name.data = v->name; + local_name.len = ngx_strlen(v->name); + + ngx_memzero(&target, sizeof(target)); + + u = &target.url; + u->url = local_name; + u->url.data = name + 7; + u->url.len = rc - 7; + u->default_port = 1935; + u->uri_part = 1; + u->no_resolve = nacf->no_resolve; /* want ip here */ + + if (ngx_parse_url(s->connection->pool, u) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "notify: push failed '%V'", &local_name); + return NGX_ERROR; + } + + ngx_rtmp_relay_push(s, &local_name, &target); + +next: + + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_notify_play_handle(ngx_rtmp_session_t *s, + void *arg, ngx_chain_t *in) +{ + ngx_rtmp_play_t *v = arg; + ngx_int_t rc; + ngx_str_t local_name; + ngx_rtmp_relay_target_t target; + ngx_url_t *u; + ngx_rtmp_notify_app_conf_t *nacf; + u_char name[NGX_RTMP_MAX_NAME]; + + static ngx_str_t location = ngx_string("location"); + + if (s->notify_play) { + s->notify_play = 0; + } + + rc = ngx_rtmp_notify_parse_http_retcode(s, in); + if (rc == NGX_ERROR) { + ngx_rtmp_notify_clear_flag(s, NGX_RTMP_NOTIFY_PLAYING); + return NGX_ERROR; + } + + if (rc != NGX_AGAIN) { + goto next; + } + + /* HTTP 3xx */ + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "notify: play redirect received"); + + rc = ngx_rtmp_notify_parse_http_header(s, in, &location, name, + sizeof(name) - 1); + if (rc <= 0) { + goto next; + } + + if (ngx_strncasecmp(name, (u_char *) "rtmp://", 7)) { + *ngx_cpymem(v->name, name, rc) = 0; + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: play redirect to '%s'", v->name); + goto next; + } + + /* pull */ + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + if (nacf->relay_redirect) { + ngx_rtmp_notify_set_name(v->name, NGX_RTMP_MAX_NAME, name, (size_t) rc); + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: pull '%s' from '%*s'", v->name, rc, name); + + local_name.data = v->name; + local_name.len = ngx_strlen(v->name); + + ngx_memzero(&target, sizeof(target)); + + u = &target.url; + u->url = local_name; + u->url.data = name + 7; + u->url.len = rc - 7; + u->default_port = 1935; + u->uri_part = 1; + u->no_resolve = nacf->no_resolve; /* want ip here */ + + if (ngx_parse_url(s->connection->pool, u) != NGX_OK) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: pull failed '%V'", &local_name); + return NGX_ERROR; + } + + ngx_rtmp_relay_pull(s, &local_name, &target); + +next: + + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_notify_update_handle(ngx_rtmp_session_t *s, + void *arg, ngx_chain_t *in) +{ + ngx_rtmp_notify_app_conf_t *nacf; + ngx_rtmp_notify_ctx_t *ctx; + ngx_int_t rc; + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + + rc = ngx_rtmp_notify_parse_http_retcode(s, in); + + if ((!nacf->update_strict && rc == NGX_ERROR) || + (nacf->update_strict && rc != NGX_OK)) + { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: update failed"); + + return NGX_ERROR; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_notify_module); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "notify: schedule update %Mms", + nacf->update_timeout); + + ngx_add_timer(&ctx->update_evt, nacf->update_timeout); + + return NGX_OK; +} + + +static void +ngx_rtmp_notify_update(ngx_event_t *e) +{ + ngx_rtmp_session_t *s; + ngx_rtmp_notify_app_conf_t *nacf; + ngx_rtmp_netcall_init_t ci; + ngx_url_t *url; + + s = e->data; + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + + url = nacf->url[NGX_RTMP_NOTIFY_UPDATE]; + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: update '%V'", &url->url); + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = url; + ci.create = ngx_rtmp_notify_update_create; + ci.handle = ngx_rtmp_notify_update_handle; + + if (ngx_rtmp_netcall_create(s, &ci) == NGX_OK) { + return; + } + + /* schedule next update on connection error */ + + ngx_rtmp_notify_update_handle(s, NULL, NULL); +} + + +static void +ngx_rtmp_notify_init(ngx_rtmp_session_t *s, + u_char name[NGX_RTMP_MAX_NAME], u_char args[NGX_RTMP_MAX_ARGS], + ngx_uint_t flags) +{ + ngx_rtmp_notify_ctx_t *ctx; + ngx_rtmp_notify_app_conf_t *nacf; + ngx_event_t *e; + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + if (!nacf->active) { + return; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_notify_module); + + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_notify_ctx_t)); + if (ctx == NULL) { + return; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_notify_module); + } + + ngx_memcpy(ctx->name, name, NGX_RTMP_MAX_NAME); + ngx_memcpy(ctx->args, args, NGX_RTMP_MAX_ARGS); + + ctx->flags |= flags; + + if (nacf->url[NGX_RTMP_NOTIFY_UPDATE] == NULL || + nacf->update_timeout == 0) + { + return; + } + + if (ctx->update_evt.timer_set) { + return; + } + + ctx->start = ngx_cached_time->sec; + + e = &ctx->update_evt; + + e->data = s; + e->log = s->connection->log; + e->handler = ngx_rtmp_notify_update; + + ngx_add_timer(e, nacf->update_timeout); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "notify: schedule initial update %Mms", + nacf->update_timeout); +} + + +static ngx_int_t +ngx_rtmp_notify_connect(ngx_rtmp_session_t *s, ngx_rtmp_connect_t *v) +{ + ngx_rtmp_notify_srv_conf_t *nscf; + ngx_rtmp_netcall_init_t ci; + ngx_url_t *url; + + if (s->auto_pushed || s->relay) { + goto next; + } + + nscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_notify_module); + + url = nscf->url[NGX_RTMP_NOTIFY_CONNECT]; + if (url == NULL) { + goto next; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: connect '%V'", &url->url); + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = url; + ci.create = ngx_rtmp_notify_connect_create; + ci.handle = ngx_rtmp_notify_connect_handle; + ci.arg = v; + ci.argsize = sizeof(*v); + + s->notify_connect = 1; + + return ngx_rtmp_netcall_create(s, &ci); + +next: + return next_connect(s, v); +} + + +static ngx_int_t +ngx_rtmp_notify_disconnect(ngx_rtmp_session_t *s) +{ + ngx_rtmp_notify_srv_conf_t *nscf; + ngx_rtmp_netcall_init_t ci; + ngx_url_t *url; + + if (s->auto_pushed || s->relay) { + goto next; + } + + nscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_notify_module); + + url = nscf->url[NGX_RTMP_NOTIFY_DISCONNECT]; + if (url == NULL) { + goto next; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: disconnect '%V'", &url->url); + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = url; + ci.create = ngx_rtmp_notify_disconnect_create; + + ngx_rtmp_netcall_create(s, &ci); + +next: + return next_disconnect(s); +} + + +static ngx_int_t +ngx_rtmp_notify_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_notify_app_conf_t *nacf; + ngx_rtmp_netcall_init_t ci; + ngx_url_t *url; + + if (s->auto_pushed) { + goto next; + } + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + if (nacf == NULL) { + goto next; + } + + url = nacf->url[NGX_RTMP_NOTIFY_PUBLISH]; + + ngx_rtmp_notify_init(s, v->name, v->args, NGX_RTMP_NOTIFY_PUBLISHING); + + if (url == NULL) { + goto next; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: publish '%V'", &url->url); + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = url; + ci.create = ngx_rtmp_notify_publish_create; + ci.handle = ngx_rtmp_notify_publish_handle; + ci.arg = v; + ci.argsize = sizeof(*v); + + return ngx_rtmp_netcall_create(s, &ci); + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_notify_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_notify_app_conf_t *nacf; + ngx_rtmp_netcall_init_t ci; + ngx_url_t *url; + + if (s->auto_pushed || v->silent) { + goto next; + } + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + if (nacf == NULL) { + goto next; + } + + url = nacf->url[NGX_RTMP_NOTIFY_PLAY]; + + ngx_rtmp_notify_init(s, v->name, v->args, NGX_RTMP_NOTIFY_PLAYING); + + if (url == NULL) { + goto next; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: play '%V'", &url->url); + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = url; + ci.create = ngx_rtmp_notify_play_create; + ci.handle = ngx_rtmp_notify_play_handle; + ci.arg = v; + ci.argsize = sizeof(*v); + + s->notify_play = 1; + + return ngx_rtmp_netcall_create(s, &ci); + +next: + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_notify_close_stream(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_notify_ctx_t *ctx; + ngx_rtmp_notify_app_conf_t *nacf; + + if (s->auto_pushed) { + goto next; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_notify_module); + + if (ctx == NULL) { + goto next; + } + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + + if (nacf == NULL) { + goto next; + } + + if (ctx->flags & NGX_RTMP_NOTIFY_PUBLISHING) { + ngx_rtmp_notify_done(s, "publish_done", NGX_RTMP_NOTIFY_PUBLISH_DONE); + } + + if (ctx->flags & NGX_RTMP_NOTIFY_PLAYING) { + ngx_rtmp_notify_done(s, "play_done", NGX_RTMP_NOTIFY_PLAY_DONE); + } + + if (ctx->flags) { + ngx_rtmp_notify_done(s, "done", NGX_RTMP_NOTIFY_DONE); + } + + if (ctx->update_evt.timer_set) { + ngx_del_timer(&ctx->update_evt); + } + + ctx->flags = 0; + +next: + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_notify_record_done(ngx_rtmp_session_t *s, ngx_rtmp_record_done_t *v) +{ + ngx_rtmp_netcall_init_t ci; + ngx_rtmp_notify_app_conf_t *nacf; + + if (s->auto_pushed) { + goto next; + } + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + if (nacf == NULL || nacf->url[NGX_RTMP_NOTIFY_RECORD_DONE] == NULL) { + goto next; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: record_done recorder=%V path='%V' url='%V'", + &v->recorder, &v->path, + &nacf->url[NGX_RTMP_NOTIFY_RECORD_DONE]->url); + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = nacf->url[NGX_RTMP_NOTIFY_RECORD_DONE]; + ci.create = ngx_rtmp_notify_record_done_create; + ci.arg = v; + + ngx_rtmp_netcall_create(s, &ci); + +next: + return next_record_done(s, v); +} + + +static ngx_int_t +ngx_rtmp_notify_done(ngx_rtmp_session_t *s, char *cbname, ngx_uint_t url_idx) +{ + ngx_rtmp_netcall_init_t ci; + ngx_rtmp_notify_done_t ds; + ngx_rtmp_notify_app_conf_t *nacf; + ngx_url_t *url; + + nacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_notify_module); + + url = nacf->url[url_idx]; + if (url == NULL) { + return NGX_OK; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "notify: %s '%V'", cbname, &url->url); + + ds.cbname = (u_char *) cbname; + ds.url_idx = url_idx; + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = url; + ci.arg = &ds; + ci.create = ngx_rtmp_notify_done_create; + + return ngx_rtmp_netcall_create(s, &ci); +} + + +static ngx_url_t * +ngx_rtmp_notify_parse_url(ngx_conf_t *cf, ngx_str_t *url) +{ + ngx_url_t *u; + size_t add; + + add = 0; + + u = ngx_pcalloc(cf->pool, sizeof(ngx_url_t)); + if (u == NULL) { + return NULL; + } + + if (ngx_strncasecmp(url->data, (u_char *) "http://", 7) == 0) { + add = 7; + } + + u->url.len = url->len - add; + u->url.data = url->data + add; + 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 NULL; + } + + return u; +} + + +static char * +ngx_rtmp_notify_on_srv_event(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_notify_srv_conf_t *nscf = conf; + + ngx_str_t *name, *value; + ngx_url_t *u; + ngx_uint_t n; + + value = cf->args->elts; + + u = ngx_rtmp_notify_parse_url(cf, &value[1]); + if (u == NULL) { + return NGX_CONF_ERROR; + } + + name = &value[0]; + + n = 0; + + switch (name->len) { + case sizeof("on_connect") - 1: + n = NGX_RTMP_NOTIFY_CONNECT; + break; + + case sizeof("on_disconnect") - 1: + n = NGX_RTMP_NOTIFY_DISCONNECT; + break; + } + + nscf->url[n] = u; + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_notify_on_app_event(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_notify_app_conf_t *nacf = conf; + + ngx_str_t *name, *value; + ngx_url_t *u; + ngx_uint_t n; + + value = cf->args->elts; + + u = ngx_rtmp_notify_parse_url(cf, &value[1]); + if (u == NULL) { + return NGX_CONF_ERROR; + } + + name = &value[0]; + + n = 0; + + switch (name->len) { + case sizeof("on_done") - 1: /* and on_play */ + if (name->data[3] == 'd') { + n = NGX_RTMP_NOTIFY_DONE; + } else { + n = NGX_RTMP_NOTIFY_PLAY; + } + break; + + case sizeof("on_update") - 1: + n = NGX_RTMP_NOTIFY_UPDATE; + break; + + case sizeof("on_publish") - 1: + n = NGX_RTMP_NOTIFY_PUBLISH; + break; + + case sizeof("on_play_done") - 1: + n = NGX_RTMP_NOTIFY_PLAY_DONE; + break; + + case sizeof("on_record_done") - 1: + n = NGX_RTMP_NOTIFY_RECORD_DONE; + break; + + case sizeof("on_publish_done") - 1: + n = NGX_RTMP_NOTIFY_PUBLISH_DONE; + break; + } + + nacf->url[n] = u; + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_notify_method(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_rtmp_notify_app_conf_t *nacf = conf; + + ngx_rtmp_notify_srv_conf_t *nscf; + ngx_str_t *value; + + value = cf->args->elts; + value++; + + if (value->len == sizeof("get") - 1 && + ngx_strncasecmp(value->data, (u_char *) "get", value->len) == 0) + { + nacf->method = NGX_RTMP_NETCALL_HTTP_GET; + + } else if (value->len == sizeof("post") - 1 && + ngx_strncasecmp(value->data, (u_char *) "post", value->len) == 0) + { + nacf->method = NGX_RTMP_NETCALL_HTTP_POST; + + } else { + return "got unexpected method"; + } + + nscf = ngx_rtmp_conf_get_module_srv_conf(cf, ngx_rtmp_notify_module); + nscf->method = nacf->method; + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_notify_postconfiguration(ngx_conf_t *cf) +{ + next_connect = ngx_rtmp_connect; + ngx_rtmp_connect = ngx_rtmp_notify_connect; + + next_disconnect = ngx_rtmp_disconnect; + ngx_rtmp_disconnect = ngx_rtmp_notify_disconnect; + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_notify_publish; + + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_notify_play; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_notify_close_stream; + + next_record_done = ngx_rtmp_record_done; + ngx_rtmp_record_done = ngx_rtmp_notify_record_done; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_parse.c b/ngx_http_flv_module/ngx_rtmp_parse.c new file mode 100644 index 0000000..617a0c8 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_parse.c @@ -0,0 +1,820 @@ + +/* + * Copyright (C) Igor Sysoev + * Copyright (C) Nginx, Inc. + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp.h" + + +#define NGX_RTMP_PARSE_INVALID_REQUEST 11 + + +static uint32_t usual[] = { + 0xffffdbfe, /* 1111 1111 1111 1111 1101 1011 1111 1110 */ + + /* ?>=< ;:98 7654 3210 /.-, +*)( '&%$ #"! */ + 0x7fff37d6, /* 0111 1111 1111 1111 0011 0111 1101 0110 */ + + /* _^]\ [ZYX WVUT SRQP ONML KJIH GFED CBA@ */ +#if (NGX_WIN32) + 0xefffffff, /* 1110 1111 1111 1111 1111 1111 1111 1111 */ +#else + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ +#endif + + /* ~}| {zyx wvut srqp onml kjih gfed cba` */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff, /* 1111 1111 1111 1111 1111 1111 1111 1111 */ + 0xffffffff /* 1111 1111 1111 1111 1111 1111 1111 1111 */ +}; + + +ngx_int_t +ngx_rtmp_parse_request_line(ngx_rtmp_session_t *s, ngx_buf_t *b) +{ + u_char c, ch, *p; + enum { + sw_start = 0, + sw_schema, + sw_schema_slash, + sw_schema_slash_slash, + sw_host_start, + sw_host, + sw_host_end, + sw_host_ip_literal, + sw_port, + sw_after_slash_in_uri, + sw_check_uri, + sw_uri + } state; + + state = sw_start; + + for (p = b->pos; p < b->last; p++) { + ch = *p; + + switch (state) { + + case sw_start: + + s->schema_start = p; + state = sw_schema; + + /* fall through */ + + case sw_schema: + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + switch (ch) { + case ':': + s->schema_end = p; + state = sw_schema_slash; + break; + default: + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + break; + + case sw_schema_slash: + switch (ch) { + case '/': + state = sw_schema_slash_slash; + break; + default: + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + break; + + case sw_schema_slash_slash: + switch (ch) { + case '/': + state = sw_host_start; + break; + default: + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + break; + + case sw_host_start: + + s->host_start = p; + + if (ch == '[') { + state = sw_host_ip_literal; + break; + } + + state = sw_host; + + /* fall through */ + + case sw_host: + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-') { + break; + } + + /* fall through */ + + case sw_host_end: + + s->host_end = p; + + switch (ch) { + case ':': + s->port_start = p + 1; + state = sw_port; + break; + case '/': + s->uri_start = p; + state = sw_after_slash_in_uri; + break; + default: + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + break; + + case sw_host_ip_literal: + + if (ch >= '0' && ch <= '9') { + break; + } + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'z') { + break; + } + + switch (ch) { + case ':': + break; + case ']': + state = sw_host_end; + break; + case '-': + case '.': + case '_': + case '~': + /* unreserved */ + break; + case '!': + case '$': + case '&': + case '\'': + case '(': + case ')': + case '*': + case '+': + case ',': + case ';': + case '=': + /* sub-delims */ + break; + default: + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + break; + + case sw_port: + if (ch >= '0' && ch <= '9') { + break; + } + + switch (ch) { + case '/': + s->port_end = p; + s->uri_start = p; + state = sw_after_slash_in_uri; + break; + default: + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + break; + + /* check "/.", "//", "%", and "\" (Win32) in URI */ + case sw_after_slash_in_uri: + + if (usual[ch >> 5] & (1U << (ch & 0x1f))) { + state = sw_check_uri; + break; + } + + switch (ch) { + case '.': + s->complex_uri = 1; + state = sw_uri; + break; + case '%': + s->quoted_uri = 1; + state = sw_uri; + break; + case '/': + s->complex_uri = 1; + state = sw_uri; + break; +#if (NGX_WIN32) + case '\\': + s->complex_uri = 1; + state = sw_uri; + break; +#endif + case '?': + s->args_start = p + 1; + state = sw_uri; + break; + case '#': + s->complex_uri = 1; + state = sw_uri; + break; + case '+': + s->plus_in_uri = 1; + break; + case '\0': + return NGX_RTMP_PARSE_INVALID_REQUEST; + default: + state = sw_check_uri; + break; + } + break; + + /* check "/", "%" and "\" (Win32) in URI */ + case sw_check_uri: + + if (usual[ch >> 5] & (1U << (ch & 0x1f))) { + break; + } + + switch (ch) { + case '/': + state = sw_after_slash_in_uri; + break; + case '.': + break; +#if (NGX_WIN32) + case '\\': + s->complex_uri = 1; + state = sw_after_slash_in_uri; + break; +#endif + case '%': + s->quoted_uri = 1; + state = sw_uri; + break; + case '?': + s->args_start = p + 1; + state = sw_uri; + break; + case '#': + s->complex_uri = 1; + state = sw_uri; + break; + case '+': + s->plus_in_uri = 1; + break; + case '\0': + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + break; + + /* URI */ + case sw_uri: + + if (usual[ch >> 5] & (1U << (ch & 0x1f))) { + break; + } + + switch (ch) { + case '#': + s->complex_uri = 1; + break; + case '\0': + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + } + } + + /* end of request line */ + s->uri_end = p; + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_process_request_uri(ngx_rtmp_session_t *s) +{ + ngx_rtmp_core_srv_conf_t *cscf; + + if (s->args_start) { + s->uri.len = s->args_start - 1 - s->uri_start; + } else { + s->uri.len = s->uri_end - s->uri_start; + } + + if (s->complex_uri || s->quoted_uri) { + + s->uri.data = ngx_pnalloc(s->connection->pool, s->uri.len + 1); + if (s->uri.data == NULL) { + return NGX_ERROR; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + if (ngx_rtmp_parse_complex_uri(s, cscf->merge_slashes) != NGX_OK) { + s->uri.len = 0; + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "client sent invalid request"); + return NGX_ERROR; + } + + } else { + s->uri.data = s->uri_start; + } + + s->unparsed_uri.len = s->uri_end - s->uri_start; + s->unparsed_uri.data = s->uri_start; + + s->valid_unparsed_uri = s->space_in_uri ? 0 : 1; + + if (s->args_start && s->uri_end > s->args_start) { + s->args.len = s->uri_end - s->args_start; + s->args.data = s->args_start; + } + +#if (NGX_WIN32) + { + u_char *p, *last; + + p = s->uri.data; + last = s->uri.data + s->uri.len; + + while (p < last) { + + if (*p++ == ':') { + + /* + * this check covers "::$data", "::$index_allocation" and + * ":$i30:$index_allocation" + */ + + if (p < last && *p == '$') { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "client sent unsafe win32 URI"); + return NGX_ERROR; + } + } + } + + p = s->uri.data + s->uri.len - 1; + + while (p > s->uri.data) { + + if (*p == ' ') { + p--; + continue; + } + + if (*p == '.') { + p--; + continue; + } + + break; + } + + if (p != s->uri.data + s->uri.len - 1) { + s->uri.len = p + 1 - s->uri.data; + } + + } +#endif + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "rtmp uri: \"%V\"", &s->uri); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "rtmp args: \"%V\"", &s->args); + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_parse_complex_uri(ngx_rtmp_session_t *s, ngx_uint_t merge_slashes) +{ + u_char c, ch, decoded, *p, *u; + enum { + sw_usual = 0, + sw_slash, + sw_dot, + sw_dot_dot, + sw_quoted, + sw_quoted_second + } state, quoted_state; + +#if (NGX_SUPPRESS_WARN) + decoded = '\0'; + quoted_state = sw_usual; +#endif + + state = sw_usual; + p = s->uri_start; + u = s->uri.data; + s->args_start = NULL; + + ch = *p++; + + while (p <= s->uri_end) { + + /* + * we use "ch = *p++" inside the cycle, it is safe, + * because after the URI there is a character: '\r' + */ + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "s:%d in:'%Xd:%c'", state, ch, ch); + + switch (state) { + + case sw_usual: + + if (usual[ch >> 5] & (1U << (ch & 0x1f))) { + *u++ = ch; + ch = *p++; + break; + } + + switch (ch) { +#if (NGX_WIN32) + case '\\': + if (u - 2 >= s->uri.data + && *(u - 1) == '.' && *(u - 2) != '.') + { + u--; + } + + if (p == s->uri_start + s->uri.len) { + + /* + * we omit the last "\" to cause redirect because + * the browsers do not treat "\" as "/" in relative URL path + */ + + break; + } + + state = sw_slash; + *u++ = '/'; + break; +#endif + case '/': +#if (NGX_WIN32) + if (u - 2 >= s->uri.data + && *(u - 1) == '.' && *(u - 2) != '.') + { + u--; + } +#endif + state = sw_slash; + *u++ = ch; + break; + case '%': + quoted_state = state; + state = sw_quoted; + break; + case '?': + s->args_start = p; + goto args; + case '#': + goto done; + case '.': + *u++ = ch; + break; + case '+': + s->plus_in_uri = 1; + + /* fall through */ + + default: + *u++ = ch; + break; + } + + ch = *p++; + break; + + case sw_slash: + + if (usual[ch >> 5] & (1U << (ch & 0x1f))) { + state = sw_usual; + *u++ = ch; + ch = *p++; + break; + } + + switch (ch) { +#if (NGX_WIN32) + case '\\': + break; +#endif + case '/': + if (!merge_slashes) { + *u++ = ch; + } + break; + case '.': + state = sw_dot; + *u++ = ch; + break; + case '%': + quoted_state = state; + state = sw_quoted; + break; + case '?': + s->args_start = p; + goto args; + case '#': + goto done; + case '+': + s->plus_in_uri = 1; + + /* fall through */ + + default: + state = sw_usual; + *u++ = ch; + break; + } + + ch = *p++; + break; + + case sw_dot: + + if (usual[ch >> 5] & (1U << (ch & 0x1f))) { + state = sw_usual; + *u++ = ch; + ch = *p++; + break; + } + + switch (ch) { +#if (NGX_WIN32) + case '\\': +#endif + case '/': + state = sw_slash; + u--; + break; + case '.': + state = sw_dot_dot; + *u++ = ch; + break; + case '%': + quoted_state = state; + state = sw_quoted; + break; + case '?': + s->args_start = p; + goto args; + case '#': + goto done; + case '+': + s->plus_in_uri = 1; + + /* fall through */ + + default: + state = sw_usual; + *u++ = ch; + break; + } + + ch = *p++; + break; + + case sw_dot_dot: + + if (usual[ch >> 5] & (1U << (ch & 0x1f))) { + state = sw_usual; + *u++ = ch; + ch = *p++; + break; + } + + switch (ch) { +#if (NGX_WIN32) + case '\\': +#endif + case '/': + state = sw_slash; + u -= 5; + for ( ;; ) { + if (u < s->uri.data) { + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + if (*u == '/') { + u++; + break; + } + u--; + } + break; + case '%': + quoted_state = state; + state = sw_quoted; + break; + case '?': + s->args_start = p; + goto args; + case '#': + goto done; + case '+': + s->plus_in_uri = 1; + + /* fall through */ + + default: + state = sw_usual; + *u++ = ch; + break; + } + + ch = *p++; + break; + + case sw_quoted: + s->quoted_uri = 1; + + if (ch >= '0' && ch <= '9') { + decoded = (u_char) (ch - '0'); + state = sw_quoted_second; + ch = *p++; + break; + } + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'f') { + decoded = (u_char) (c - 'a' + 10); + state = sw_quoted_second; + ch = *p++; + break; + } + + return NGX_RTMP_PARSE_INVALID_REQUEST; + + case sw_quoted_second: + if (ch >= '0' && ch <= '9') { + ch = (u_char) ((decoded << 4) + ch - '0'); + + if (ch == '%' || ch == '#') { + state = sw_usual; + *u++ = ch; + ch = *p++; + break; + + } else if (ch == '\0') { + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + + state = quoted_state; + break; + } + + c = (u_char) (ch | 0x20); + if (c >= 'a' && c <= 'f') { + ch = (u_char) ((decoded << 4) + c - 'a' + 10); + + if (ch == '?') { + state = sw_usual; + *u++ = ch; + ch = *p++; + break; + + } else if (ch == '+') { + s->plus_in_uri = 1; + } + + state = quoted_state; + break; + } + + return NGX_RTMP_PARSE_INVALID_REQUEST; + } + } + +done: + + s->uri.len = *u == CR ? (u - s->uri.data) : (u - s->uri.data + 1); + + return NGX_OK; + +args: + + while (p < s->uri_end) { + if (*p++ != '#') { + continue; + } + + s->args.len = p - 1 - s->args_start; + s->args.data = s->args_start; + s->args_start = NULL; + + break; + } + + s->uri.len = u - s->uri.data; + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_process_request_line(ngx_rtmp_session_t *s, const u_char *name, + const u_char *args, const u_char *cmd) +{ + size_t rlen = 0; + + s->stream.len = name ? ngx_strlen(name) : 0; + if (s->stream.len) { + s->stream.data = ngx_palloc(s->connection->pool, s->stream.len); + if (s->stream.data == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(s->stream.data, name, ngx_strlen(name)); + } + + if (s->tc_url.data[s->tc_url.len - 1] == '/') { + s->tc_url.len -= 1; + } + + rlen = s->tc_url.len; + + if (s->stream.len) { + rlen += 1 + s->stream.len; + } + + if (args && args[0]) { + rlen += 1 + ngx_strlen(args); + } + + s->request_line = ngx_create_temp_buf(s->connection->pool, rlen + 1); + if (s->request_line == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "%s: failed to ngx_pcalloc for request_line", cmd); + return NGX_ERROR; + } + + if (s->stream.len) { + if (args && args[0]) { + *ngx_snprintf(s->request_line->pos, rlen + 1, "%V/%V?%s", &s->tc_url, + &s->stream, args) = CR; + } else { + *ngx_snprintf(s->request_line->pos, rlen + 1, "%V/%V", &s->tc_url, + &s->stream) = CR; + } + } else { + if (args && args[0]) { + *ngx_snprintf(s->request_line->pos, rlen + 1, "%V?%s", &s->tc_url, + args) = CR; + } else { + *ngx_snprintf(s->request_line->pos, rlen + 1, "%V", &s->tc_url) + = CR; + } + } + + s->request_line->last += rlen; + + if (ngx_rtmp_parse_request_line(s, s->request_line) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "%s: invalid request line: '%s'", cmd, s->request_line->pos); + return NGX_ERROR; + } + + if (ngx_rtmp_process_request_uri(s) != NGX_OK) { + return NGX_ERROR; + } + + *s->request_line->last = 0; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_play_module.c b/ngx_http_flv_module/ngx_rtmp_play_module.c new file mode 100644 index 0000000..1f17690 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_play_module.c @@ -0,0 +1,1284 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include +#include "ngx_rtmp_play_module.h" +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_netcall_module.h" +#include "ngx_rtmp_streams.h" + + +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_close_stream_pt next_close_stream; +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); +static char * ngx_rtmp_play_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); + +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); +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_int_t ngx_rtmp_play_next_entry(ngx_rtmp_session_t *s, + ngx_rtmp_play_t *v); +static ngx_rtmp_play_entry_t * ngx_rtmp_play_get_current_entry( + ngx_rtmp_session_t *s); +static void ngx_rtmp_play_cleanup_local_file(ngx_rtmp_session_t *s); +static void ngx_rtmp_play_copy_local_file(ngx_rtmp_session_t *s, u_char *name); +static u_char * ngx_rtmp_play_get_local_file_path(ngx_rtmp_session_t *s); + + +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_1MORE, + 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, temp_path), + NULL }, + + { ngx_string("play_local_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, local_path), + NULL }, + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_play_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_play_postconfiguration, /* postconfiguration */ + ngx_rtmp_play_create_main_conf, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + ngx_rtmp_play_create_app_conf, /* create app configuration */ + ngx_rtmp_play_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_play_module = { + NGX_MODULE_V1, + &ngx_rtmp_play_module_ctx, /* module context */ + ngx_rtmp_play_commands, /* module directives */ + NGX_RTMP_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 +}; + + +#define NGX_RTMP_PLAY_TMP_FILE "nginx-http-flv-vod." + + +static void * +ngx_rtmp_play_create_main_conf(ngx_conf_t *cf) +{ + ngx_rtmp_play_main_conf_t *pmcf; + + pmcf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_play_main_conf_t)); + if (pmcf == NULL) { + return NULL; + } + + if (ngx_array_init(&pmcf->fmts, cf->pool, 1, + sizeof(ngx_rtmp_play_fmt_t *)) + != NGX_OK) + { + return NULL; + } + + return pmcf; +} + + +static void * +ngx_rtmp_play_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_play_app_conf_t *pacf; + + pacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_play_app_conf_t)); + if (pacf == NULL) { + return NULL; + } + + pacf->nbuckets = 1024; + + return pacf; +} + + +static char * +ngx_rtmp_play_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_play_app_conf_t *prev = parent; + ngx_rtmp_play_app_conf_t *conf = child; + ngx_rtmp_play_entry_t **ppe; + + ngx_conf_merge_str_value(conf->temp_path, prev->temp_path, "/tmp"); + ngx_conf_merge_str_value(conf->local_path, prev->local_path, ""); + + if (prev->entries.nelts == 0) { + goto done; + } + + if (conf->entries.nelts == 0) { + conf->entries = prev->entries; + goto done; + } + + ppe = ngx_array_push_n(&conf->entries, prev->entries.nelts); + if (ppe == NULL) { + return NGX_CONF_ERROR; + } + + ngx_memcpy(ppe, prev->entries.elts, prev->entries.nelts * sizeof(void *)); + +done: + + if (conf->entries.nelts == 0) { + return NGX_CONF_OK; + } + + conf->ctx = ngx_pcalloc(cf->pool, sizeof(void *) * conf->nbuckets); + if (conf->ctx == NULL) { + return NGX_CONF_ERROR; + } + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_play_join(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_ctx_t *ctx, **pctx; + ngx_rtmp_play_app_conf_t *pacf; + ngx_uint_t h; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: join"); + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + if (ctx == NULL || ctx->joined) { + return NGX_ERROR; + } + + h = ngx_hash_key(ctx->name, ngx_strlen(ctx->name)); + pctx = &pacf->ctx[h % pacf->nbuckets]; + + while (*pctx) { + if (!ngx_strncmp((*pctx)->name, ctx->name, NGX_RTMP_MAX_NAME)) { + break; + } + pctx = &(*pctx)->next; + } + + ctx->next = *pctx; + *pctx = ctx; + ctx->joined = 1; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_play_leave(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_ctx_t *ctx, **pctx; + ngx_rtmp_play_app_conf_t *pacf; + ngx_uint_t h; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: leave"); + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + if (ctx == NULL || !ctx->joined) { + return NGX_ERROR; + } + + h = ngx_hash_key(ctx->name, ngx_strlen(ctx->name)); + pctx = &pacf->ctx[h % pacf->nbuckets]; + + while (*pctx && *pctx != ctx) { + pctx = &(*pctx)->next; + } + + if (*pctx == NULL) { + return NGX_ERROR; + } + + *pctx = (*pctx)->next; + ctx->joined = 0; + + return NGX_OK; +} + + +static void +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); + + if (ctx == NULL || ctx->fmt == NULL || ctx->fmt->send == NULL) { + return; + } + + ts = 0; + + rc = ctx->fmt->send(s, &ctx->file, &ts); + + if (rc > 0) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: send schedule %i", rc); + + ngx_add_timer(e, rc); + return; + } + + if (rc == NGX_AGAIN) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: send buffer full"); + + ngx_post_event(e, &s->posted_dry_events); + return; + } + + if (rc == NGX_OK) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: send restart"); + + ngx_post_event(e, &ngx_posted_events); + return; + } + + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: send done"); + + ngx_rtmp_send_stream_eof(s, NGX_RTMP_MSID); + + 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_do_init(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->fmt && ctx->fmt->init && + ctx->fmt->init(s, &ctx->file, ctx->aindex, ctx->vindex) != NGX_OK) + { + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_play_do_done(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->fmt && ctx->fmt->done && + ctx->fmt->done(s, &ctx->file) != NGX_OK) + { + return NGX_ERROR; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_play_do_start(ngx_rtmp_session_t *s) +{ + 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_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: start"); + + if (ctx->fmt && ctx->fmt->start && + 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_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; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: stop"); + + if (ctx->send_evt.timer_set) { + ngx_del_timer(&ctx->send_evt); + } + +#if (nginx_version >= 1007005) + if (ctx->send_evt.posted) +#else + if (ctx->send_evt.prev) +#endif + { + ngx_delete_posted_event((&ctx->send_evt)); + } + + if (ctx->fmt && ctx->fmt->stop && + ctx->fmt->stop(s, &ctx->file) != NGX_OK) + { + return NGX_ERROR; + } + + ctx->playing = 0; + + return NGX_OK; +} + + +/* This function returns pointer to a static buffer */ + +static u_char * +ngx_rtmp_play_get_local_file_path(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_app_conf_t *pacf; + ngx_rtmp_play_ctx_t *ctx; + u_char *p; + static u_char path[NGX_MAX_PATH + 1]; + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + p = ngx_snprintf(path, NGX_MAX_PATH, "%V/" NGX_RTMP_PLAY_TMP_FILE "%ui", + &pacf->temp_path, ctx->file_id); + *p = 0; + + return path; +} + + +static void +ngx_rtmp_play_copy_local_file(ngx_rtmp_session_t *s, u_char *name) +{ + ngx_rtmp_play_app_conf_t *pacf; + ngx_rtmp_play_ctx_t *ctx; + u_char *path, *p; + static u_char dpath[NGX_MAX_PATH + 1]; + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + if (pacf == NULL) { + return; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + if (ctx == NULL || ctx->file_id == 0) { + return; + } + + path = ngx_rtmp_play_get_local_file_path(s); + + p = ngx_snprintf(dpath, NGX_MAX_PATH, "%V/%s%V", &pacf->local_path, + name + ctx->pfx_size, &ctx->sfx); + *p = 0; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: copy local file '%s' to '%s'", path, dpath); + + if (ngx_rename_file(path, dpath) == 0) { + ctx->file_id = 0; + return; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, ngx_errno, + "play: error copying local file '%s' to '%s'", + path, dpath); + + ngx_rtmp_play_cleanup_local_file(s); +} + + +static void +ngx_rtmp_play_cleanup_local_file(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_ctx_t *ctx; + u_char *path; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + if (ctx == NULL || ctx->file_id == 0) { + return; + } + + path = ngx_rtmp_play_get_local_file_path(s); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: deleting local file '%s'", path); + + ctx->file_id = 0; + + ngx_delete_file(path); +} + + +static ngx_int_t +ngx_rtmp_play_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_play_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + if (ctx == NULL) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: close_stream"); + + ngx_rtmp_play_do_stop(s); + + ngx_rtmp_play_do_done(s); + + if (ctx->file.fd != NGX_INVALID_FILE) { + ngx_close_file(ctx->file.fd); + ctx->file.fd = NGX_INVALID_FILE; + + ngx_rtmp_send_stream_eof(s, NGX_RTMP_MSID); + + ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", + "Stop video on demand"); + } + + if (ctx->file_id) { + ngx_rtmp_play_cleanup_local_file(s); + } + + ngx_rtmp_play_leave(s); + +next: + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_play_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) +{ + ngx_rtmp_play_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + if (ctx == NULL || ctx->file.fd == NGX_INVALID_FILE) { + goto next; + } + + if (!ctx->opened) { + ctx->post_seek = (ngx_uint_t) v->offset; + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: post seek=%ui", ctx->post_seek); + goto next; + } + + if (ngx_rtmp_send_stream_eof(s, NGX_RTMP_MSID) != NGX_OK) { + return NGX_ERROR; + } + + ngx_rtmp_play_do_seek(s, (ngx_uint_t) v->offset); + + if (ngx_rtmp_send_status(s, "NetStream.Seek.Notify", "status", "Seeking") + != NGX_OK) + { + return NGX_ERROR; + } + + if (ngx_rtmp_send_stream_begin(s, NGX_RTMP_MSID) != NGX_OK) { + return NGX_ERROR; + } + +next: + return next_seek(s, v); +} + + +static ngx_int_t +ngx_rtmp_play_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) +{ + ngx_rtmp_play_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx == NULL || ctx->file.fd == NGX_INVALID_FILE) { + goto next; + } + + if (!ctx->opened) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: pause ignored"); + goto next; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: pause=%i timestamp=%f", + (ngx_int_t) v->pause, v->position); + + if (v->pause) { + if (ngx_rtmp_send_status(s, "NetStream.Pause.Notify", "status", + "Paused video on demand") + != NGX_OK) + { + return NGX_ERROR; + } + + ngx_rtmp_play_do_stop(s); + + } else { + if (ngx_rtmp_send_status(s, "NetStream.Unpause.Notify", "status", + "Unpaused video on demand") + != NGX_OK) + { + return NGX_ERROR; + } + + ngx_rtmp_play_do_start(s); /*TODO: v->position? */ + } + +next: + return next_pause(s, v); +} + + +static ngx_int_t +ngx_rtmp_play_parse_index(char type, u_char *args) +{ + u_char *p, c; + static u_char name[] = "xindex="; + + name[0] = (u_char) type; + + for ( ;; ) { + p = (u_char *) ngx_strstr(args, name); + if (p == NULL) { + return 0; + } + + if (p != args) { + c = *(p - 1); + if (c != '?' && c != '&') { + args = p + 1; + continue; + } + } + + return atoi((char *) p + (sizeof(name) - 1)); + } +} + + +static ngx_int_t +ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_play_main_conf_t *pmcf; + ngx_rtmp_play_app_conf_t *pacf; + ngx_rtmp_play_ctx_t *ctx; + u_char *p; + ngx_rtmp_play_fmt_t *fmt, **pfmt; + ngx_str_t *pfx, *sfx; + ngx_str_t name; + ngx_uint_t n; + + pmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_play_module); + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + if (pacf == NULL || pacf->entries.nelts == 0) { + goto next; + } + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "play: play name='%s' timestamp=%i", + v->name, (ngx_int_t) v->start); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx && ctx->file.fd != NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "play: already playing"); + goto next; + } + + /* check for double-dot in v->name; + * we should not move out of play directory */ + for (p = v->name; *p; ++p) { + if (ngx_path_separator(p[0]) && + p[1] == '.' && p[2] == '.' && + ngx_path_separator(p[3])) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "play: bad name '%s'", v->name); + return NGX_ERROR; + } + } + + if (ctx == NULL) { + ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_play_ctx_t)); + if (ctx == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "play: failed to allocate for ctx"); + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_play_module); + } + + ngx_memzero(ctx, sizeof(*ctx)); + + ctx->session = s; + ctx->aindex = ngx_rtmp_play_parse_index('a', v->args); + ctx->vindex = ngx_rtmp_play_parse_index('v', v->args); + + ctx->file.log = s->connection->log; + + ngx_memcpy(ctx->name, v->name, NGX_RTMP_MAX_NAME); + + name.len = ngx_strlen(v->name); + name.data = v->name; + + pfmt = pmcf->fmts.elts; + + for (n = 0; n < pmcf->fmts.nelts; ++n, ++pfmt) { + fmt = *pfmt; + + pfx = &fmt->pfx; + sfx = &fmt->sfx; + + if (pfx->len == 0 && ctx->fmt == NULL) { + ctx->fmt = fmt; + } + + if (pfx->len && name.len >= pfx->len && + ngx_strncasecmp(pfx->data, name.data, pfx->len) == 0) + { + ctx->pfx_size = pfx->len; + ctx->fmt = fmt; + + break; + } + + if (name.len >= sfx->len && + ngx_strncasecmp(sfx->data, name.data + name.len - sfx->len, + sfx->len) == 0) + { + ctx->fmt = fmt; + } + } + + if (ctx->fmt == NULL) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "play: fmt not found"); + goto next; + } + + ctx->file.fd = NGX_INVALID_FILE; + ctx->nentry = NGX_CONF_UNSET_UINT; + ctx->post_seek = NGX_CONF_UNSET_UINT; + + sfx = &ctx->fmt->sfx; + + if (name.len < sfx->len || + ngx_strncasecmp(sfx->data, name.data + name.len - sfx->len, + sfx->len)) + { + ctx->sfx = *sfx; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: fmt=%V", &ctx->fmt->name); + + return ngx_rtmp_play_next_entry(s, v); + +next: + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_play_next_entry(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_play_entry_t *pe; + u_char *p; + static u_char path[NGX_MAX_PATH + 1]; + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + for ( ;; ) { + + if (ctx->file.fd != NGX_INVALID_FILE) { + ngx_close_file(ctx->file.fd); + ctx->file.fd = NGX_INVALID_FILE; + } + + if (ctx->file_id) { + ngx_rtmp_play_cleanup_local_file(s); + } + + ctx->nentry = (ctx->nentry == NGX_CONF_UNSET_UINT ? + 0 : ctx->nentry + 1); + + if (ctx->nentry >= pacf->entries.nelts) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: all entries failed"); + + ngx_rtmp_send_status(s, "NetStream.Play.StreamNotFound", "error", + "Video on demand stream not found"); + break; + } + + pe = ngx_rtmp_play_get_current_entry(s); + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: trying %s entry %ui/%uz '%V'", + pe->url ? "remote" : "local", + ctx->nentry + 1, pacf->entries.nelts, + pe->url ? &pe->url->url : pe->root); + + /* open remote */ + + if (pe->url) { + return ngx_rtmp_play_open_remote(s, v); + } + + /* open local */ + + p = ngx_snprintf(path, NGX_MAX_PATH, "%V/%s%V", + pe->root, v->name + ctx->pfx_size, &ctx->sfx); + *p = 0; + + 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_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, ngx_errno, + "play: error opening file '%s'", path); + continue; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: open local file '%s'", path); + + if (ngx_rtmp_play_open(s, v->start) != NGX_OK) { + return NGX_ERROR; + } + + break; + } + + 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; + ngx_uint_t timestamp; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx->file.fd == NGX_INVALID_FILE) { + return NGX_ERROR; + } + + if (ngx_rtmp_send_stream_begin(s, NGX_RTMP_MSID) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_send_status(s, "NetStream.Play.Start", "status", + "Start video on demand") + != NGX_OK) + { + return NGX_ERROR; + } + + if (ngx_rtmp_play_join(s) != NGX_OK) { + return NGX_ERROR; + } + + e = &ctx->send_evt; + e->data = s; + e->handler = ngx_rtmp_play_send; + e->log = s->connection->log; + + ngx_rtmp_send_recorded(s, 1); + + if (ngx_rtmp_send_sample_access(s) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_play_do_init(s) != NGX_OK) { + return NGX_ERROR; + } + + timestamp = ctx->post_seek != NGX_CONF_UNSET_UINT ? ctx->post_seek : + (start < 0 ? 0 : (ngx_uint_t) start); + + if (ngx_rtmp_play_do_seek(s, timestamp) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_play_do_start(s) != NGX_OK) { + return NGX_ERROR; + } + + ctx->opened = 1; + + 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_ctx_t *ctx; + ngx_rtmp_play_entry_t *pe; + ngx_str_t *addr_text, uri; + u_char *p, *name; + size_t args_len, name_len, len; + static ngx_str_t text_plain = ngx_string("text/plain"); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + pe = ngx_rtmp_play_get_current_entry(s); + + name = v->name + ctx->pfx_size; + + name_len = ngx_strlen(name); + args_len = ngx_strlen(v->args); + addr_text = &s->connection->addr_text; + + len = pe->url->uri.len + 1 + + name_len + ctx->sfx.len + + sizeof("?addr=") + addr_text->len * 3 + + 1 + args_len; + + uri.data = ngx_palloc(pool, len); + if (uri.data == NULL) { + return NULL; + } + + p = uri.data; + + p = ngx_cpymem(p, pe->url->uri.data, pe->url->uri.len); + + if (p == uri.data || p[-1] != '/') { + *p++ = '/'; + } + + p = ngx_cpymem(p, name, name_len); + p = ngx_cpymem(p, ctx->sfx.data, ctx->sfx.len); + p = ngx_cpymem(p, (u_char*)"?addr=", sizeof("&addr=") -1); + p = (u_char*)ngx_escape_uri(p, addr_text->data, addr_text->len, + NGX_ESCAPE_ARGS); + if (args_len) { + *p++ = '&'; + p = (u_char *) ngx_cpymem(p, v->args, args_len); + } + + uri.len = p - uri.data; + + return ngx_rtmp_netcall_http_format_request(NGX_RTMP_NETCALL_HTTP_GET, + &pe->url->host, &uri, + NULL, NULL, pool, &text_plain); +} + + +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; + + ngx_rtmp_play_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx->nbody == 0) { + return ngx_rtmp_play_next_entry(s, v); + } + + if (ctx->file_id) { + ngx_rtmp_play_copy_local_file(s, v->name); + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: open remote file"); + + 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 */ + 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; + case '\r': + break; + default: + ctx->ncrs = 0; + } + /* 10th header byte is HTTP response header */ + if (++ctx->nheader == 10 && *b->pos != (u_char) '2') { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "play: remote HTTP response code: %cxx", + *b->pos); + return NGX_ERROR; + } + } + + if (b->pos == b->last) { + in = in->next; + } + } + + /* 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; + } + + ctx->nbody += rc; + } + + return NGX_OK; +} + + +static ngx_rtmp_play_entry_t * +ngx_rtmp_play_get_current_entry(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_app_conf_t *pacf; + ngx_rtmp_play_ctx_t *ctx; + ngx_rtmp_play_entry_t **ppe; + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + ppe = pacf->entries.elts; + + return ppe[ctx->nentry]; +} + + +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_play_entry_t *pe; + ngx_rtmp_netcall_init_t ci; + u_char *path; + ngx_err_t err; + static ngx_uint_t file_id; + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_play_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + ctx->ncrs = 0; + ctx->nheader = 0; + ctx->nbody = 0; + + for ( ;; ) { + ctx->file_id = ++file_id; + + /* no zero after overflow */ + if (ctx->file_id == 0) { + continue; + } + + path = ngx_rtmp_play_get_local_file_path(s); + + ctx->file.fd = ngx_open_tempfile(path, pacf->local_path.len, 0); + + if (pacf->local_path.len == 0) { + ctx->file_id = 0; + } + + if (ctx->file.fd != NGX_INVALID_FILE) { + break; + } + + err = ngx_errno; + + if (err != NGX_EEXIST) { + ctx->file_id = 0; + + ngx_log_error(NGX_LOG_INFO, s->connection->log, err, + "play: failed to create temp file"); + + return NGX_ERROR; + } + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: temp file '%s' file_id=%ui", + path, ctx->file_id); + + pe = ngx_rtmp_play_get_current_entry(s); + + ngx_memzero(&ci, sizeof(ci)); + + ci.url = pe->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 = conf; + + ngx_rtmp_play_entry_t *pe, **ppe; + ngx_str_t url; + ngx_url_t *u; + size_t add, n; + ngx_str_t *value; + + if (pacf->entries.nalloc == 0 && + ngx_array_init(&pacf->entries, cf->pool, 1, sizeof(void *)) != NGX_OK) + { + return NGX_CONF_ERROR; + } + + value = cf->args->elts; + + for (n = 1; n < cf->args->nelts; ++n) { + + ppe = ngx_array_push(&pacf->entries); + if (ppe == NULL) { + return NGX_CONF_ERROR; + } + + pe = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_play_entry_t)); + if (pe == NULL) { + return NGX_CONF_ERROR; + } + + *ppe = pe; + + if (ngx_strncasecmp(value[n].data, (u_char *) "http://", 7)) { + + /* local file */ + + pe->root = ngx_palloc(cf->pool, sizeof(ngx_str_t)); + if (pe->root == NULL) { + return NGX_CONF_ERROR; + } + + *pe->root = value[n]; + + continue; + } + + /* http case */ + + url = value[n]; + + 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; + } + + pe->url = u; + } + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_play_postconfiguration(ngx_conf_t *cf) +{ + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_play_play; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_play_close_stream; + + next_seek = ngx_rtmp_seek; + ngx_rtmp_seek = ngx_rtmp_play_seek; + + next_pause = ngx_rtmp_pause; + ngx_rtmp_pause = ngx_rtmp_play_pause; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_play_module.h b/ngx_http_flv_module/ngx_rtmp_play_module.h new file mode 100644 index 0000000..b0650b5 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_play_module.h @@ -0,0 +1,93 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_PLAY_H_INCLUDED_ +#define _NGX_RTMP_PLAY_H_INCLUDED_ + + +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_cmd_module.h" + + +typedef ngx_int_t (*ngx_rtmp_play_init_pt) (ngx_rtmp_session_t *s, + ngx_file_t *f, ngx_int_t aindex, ngx_int_t vindex); +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_uint_t *ts); + + +typedef struct { + ngx_str_t name; + ngx_str_t pfx; + ngx_str_t sfx; + + 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; + + +typedef struct ngx_rtmp_play_ctx_s ngx_rtmp_play_ctx_t; + + +struct ngx_rtmp_play_ctx_s { + ngx_rtmp_session_t *session; + ngx_file_t file; + ngx_rtmp_play_fmt_t *fmt; + ngx_event_t send_evt; + unsigned playing:1; + unsigned opened:1; + unsigned joined:1; + ngx_uint_t ncrs; + ngx_uint_t nheader; + ngx_uint_t nbody; + size_t pfx_size; + ngx_str_t sfx; + ngx_uint_t file_id; + ngx_int_t aindex, vindex; + ngx_uint_t nentry; + ngx_uint_t post_seek; + u_char name[NGX_RTMP_MAX_NAME]; + ngx_rtmp_play_ctx_t *next; +}; + + +typedef struct { + ngx_str_t *root; + ngx_url_t *url; +} ngx_rtmp_play_entry_t; + + +typedef struct { + ngx_str_t temp_path; + ngx_str_t local_path; + ngx_array_t entries; /* ngx_rtmp_play_entry_t * */ + ngx_uint_t nbuckets; + ngx_rtmp_play_ctx_t **ctx; +} ngx_rtmp_play_app_conf_t; + + +typedef struct { + ngx_array_t fmts; /* ngx_rtmp_play_fmt_t * */ +} ngx_rtmp_play_main_conf_t; + + +extern ngx_module_t ngx_rtmp_play_module; + + +#endif /* _NGX_RTMP_PLAY_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_proxy_protocol.c b/ngx_http_flv_module/ngx_rtmp_proxy_protocol.c new file mode 100644 index 0000000..ffa2680 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_proxy_protocol.c @@ -0,0 +1,197 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include +#include "ngx_rtmp_proxy_protocol.h" + + +static void ngx_rtmp_proxy_protocol_recv(ngx_event_t *rev); + + +void +ngx_rtmp_proxy_protocol(ngx_rtmp_session_t *s) +{ + ngx_event_t *rev; + ngx_connection_t *c; + + c = s->connection; + rev = c->read; + rev->handler = ngx_rtmp_proxy_protocol_recv; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "proxy_protocol: start"); + + if (rev->ready) { + /* the deferred accept(), rtsig, aio, iocp */ + + if (ngx_use_accept_mutex) { + ngx_post_event(rev, &ngx_posted_events); + return; + } + + rev->handler(rev); + return; + } + + ngx_add_timer(rev, s->timeout); + + if (ngx_handle_read_event(rev, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + return; + } +} + + +static void +ngx_rtmp_proxy_protocol_recv(ngx_event_t *rev) +{ + u_char buf[107], *p, *pp, *text; + size_t len; + ssize_t n; + ngx_err_t err; + ngx_int_t i; + ngx_addr_t addr; + ngx_connection_t *c; + ngx_rtmp_session_t *s; + + c = rev->data; + s = c->data; + + if (c->destroyed) { + return; + } + + if (rev->timedout) { + ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, + "proxy_protocol: recv: client timed out"); + c->timedout = 1; + ngx_rtmp_finalize_session(s); + return; + } + + if (rev->timer_set) { + ngx_del_timer(rev); + } + + n = recv(c->fd, (char *) buf, sizeof(buf), MSG_PEEK); + + err = ngx_socket_errno; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, c->log, 0, "recv(): %d", n); + + if (n == -1) { + + if (err == NGX_EAGAIN) { + ngx_add_timer(rev, s->timeout); + + if (ngx_handle_read_event(c->read, 0) != NGX_OK) { + ngx_rtmp_finalize_session(s); + } + + return; + } + + ngx_rtmp_finalize_session(s); + + return; + } + + p = buf; + + if (n <= 8 && ngx_strncmp(p, "PROXY ", 6) != 0) { + goto bad_header; + } + + n -= 6; + p += 6; + + ngx_memzero(&addr, sizeof(ngx_addr_t)); + + if (n >= 7 && ngx_strncmp(p, "UNKNOWN", 7) == 0) { + n -= 7; + p += 7; + goto skip; + } + + if (n < 5 || ngx_strncmp(p, "TCP", 3) != 0 + || (p[3] != '4' && p[3] != '6') || p[4] != ' ') + { + goto bad_header; + } + + n -= 5; + p += 5; + + pp = ngx_strlchr(p, p + n, ' '); + + if (pp == NULL) { + goto bad_header; + } + + if (ngx_parse_addr(s->connection->pool, &addr, p, pp - p) != NGX_OK) { + goto bad_header; + } + + n -= pp - p; + p = pp; + +skip: + + for (i = 0; i + 1 < n; i++) { + if (p[i] == CR && p[i + 1] == LF) { + break; + } + } + + if (i + 1 >= n) { + goto bad_header; + } + + n = p - buf + i + 2; + + if (c->recv(c, buf, n) != n) { + goto failed; + } + + if (addr.socklen) { + text = ngx_palloc(s->connection->pool, NGX_SOCKADDR_STRLEN); + + if (text == NULL) { + goto failed; + } + + len = ngx_sock_ntop(addr.sockaddr, +#if (nginx_version >= 1005003) + addr.socklen, +#endif + text, NGX_SOCKADDR_STRLEN, 0); + if (len == 0) { + goto failed; + } + + c->sockaddr = addr.sockaddr; + c->socklen = addr.socklen; + c->addr_text.data = text; + c->addr_text.len = len; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, c->log, 0, + "proxy_protocol: remote_addr:'%V'", &c->addr_text); + } + + ngx_rtmp_handshake(s); + + return; + +bad_header: + + ngx_log_error(NGX_LOG_INFO, c->log, 0, "proxy_protocol: bad header"); + +failed: + + ngx_rtmp_finalize_session(s); +} diff --git a/ngx_http_flv_module/ngx_rtmp_proxy_protocol.h b/ngx_http_flv_module/ngx_rtmp_proxy_protocol.h new file mode 100644 index 0000000..e873c3c --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_proxy_protocol.h @@ -0,0 +1,19 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_PROXY_PROTOCOL_H_INCLUDED_ +#define _NGX_RTMP_PROXY_PROTOCOL_H_INCLUDED_ + + +#include +#include +#include "ngx_rtmp.h" + + +void ngx_rtmp_proxy_protocol(ngx_rtmp_session_t *c); + + +#endif /* _NGX_RTMP_PROXY_PROTOCOL_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_receive.c b/ngx_http_flv_module/ngx_rtmp_receive.c new file mode 100644 index 0000000..1e146f0 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_receive.c @@ -0,0 +1,480 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_amf.h" +#include "ngx_rtmp_cmd_module.h" +#include + + +ngx_int_t +ngx_rtmp_protocol_message_handler(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in) +{ + ngx_buf_t *b; + uint32_t val; + uint8_t limit; + + b = in->buf; + + if (b->last - b->pos < 4) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "too small buffer for %d message: %d", + (int)h->type, b->last - b->pos); + return NGX_OK; + } + + val = ntohl(*(uint32_t *) b->pos); + + switch(h->type) { + case NGX_RTMP_MSG_CHUNK_SIZE: + /* set chunk size =val */ + ngx_rtmp_set_chunk_size(s, val); + break; + + case NGX_RTMP_MSG_ABORT: + /* abort chunk stream =val */ + break; + + case NGX_RTMP_MSG_ACK: + /* receive ack with sequence number =val */ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "receive ack seq=%uD", val); + break; + + case NGX_RTMP_MSG_ACK_SIZE: + /* receive window size =val */ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "receive ack_size=%uD", val); + s->ack_size = val; + break; + + case NGX_RTMP_MSG_BANDWIDTH: + if (b->last - b->pos >= 5) { + limit = *(uint8_t*)&b->pos[4]; + + (void)val; + (void)limit; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "receive bandwidth=%uD limit=%d", + val, (int)limit); + + /* receive window size =val + * && limit */ + } + break; + + default: + return NGX_ERROR; + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_user_message_handler(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_buf_t *b; + uint16_t evt; + uint32_t val; + + b = in->buf; + + if (b->last - b->pos < 6) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "too small buffer for user message: %d", + b->last - b->pos); + return NGX_OK; + } + + evt = ntohs(*(uint16_t *) b->pos); + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "RTMP recv user evt %s (%i)", + ngx_rtmp_user_message_type(evt), (ngx_int_t) evt); + + val = ntohl(*(uint32_t *) (b->pos + 2)); + + switch(evt) { + case NGX_RTMP_USER_STREAM_BEGIN: + { + ngx_rtmp_stream_begin_t v; + + v.msid = val; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "receive: stream_begin msid=%uD", v.msid); + + return ngx_rtmp_stream_begin(s, &v); + } + + case NGX_RTMP_USER_STREAM_EOF: + { + ngx_rtmp_stream_eof_t v; + + v.msid = val; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "receive: stream_eof msid=%uD", v.msid); + + return ngx_rtmp_stream_eof(s, &v); + } + + case NGX_RTMP_USER_STREAM_DRY: + { + ngx_rtmp_stream_dry_t v; + + v.msid = val; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "receive: stream_dry msid=%uD", v.msid); + + return ngx_rtmp_stream_dry(s, &v); + } + + case NGX_RTMP_USER_SET_BUFLEN: + { + ngx_rtmp_set_buflen_t v; + + v.msid = val; + + if (b->last - b->pos < 10) { + return NGX_OK; + } + + v.buflen = ntohl(*(uint32_t *) (b->pos + 6)); + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "receive: set_buflen msid=%uD buflen=%uD", + v.msid, v.buflen); + + /*TODO: move this to play module */ + s->buflen = v.buflen; + + return ngx_rtmp_set_buflen(s, &v); + } + + case NGX_RTMP_USER_RECORDED: + { + ngx_rtmp_recorded_t v; + + v.msid = val; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "receive: recorded msid=%uD", v.msid); + + return ngx_rtmp_recorded(s, &v); + } + + case NGX_RTMP_USER_PING_REQUEST: + return ngx_rtmp_send_ping_response(s, val); + + case NGX_RTMP_USER_PING_RESPONSE: + + /* val = incoming timestamp */ + + ngx_rtmp_reset_ping(s); + + return NGX_OK; + + default: + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "unexpected user event: %i", (ngx_int_t) evt); + + return NGX_OK; + } +} + + +static ngx_int_t +ngx_rtmp_fetch(ngx_chain_t **in, u_char *ret) +{ + while (*in && (*in)->buf->pos >= (*in)->buf->last) { + *in = (*in)->next; + } + + if (*in == NULL) { + return NGX_DONE; + } + + *ret = *(*in)->buf->pos++; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_fetch_uint8(ngx_chain_t **in, uint8_t *ret) +{ + return ngx_rtmp_fetch(in, (u_char *) ret); +} + + +static ngx_int_t +ngx_rtmp_fetch_uint32(ngx_chain_t **in, uint32_t *ret, ngx_int_t n) +{ + u_char r; + ngx_int_t rc; + uint32_t val; + + *ret = 0; + val = 0; + + while (--n >= 0) { + rc = ngx_rtmp_fetch(in, &r); + if (rc != NGX_OK) { + return rc; + } + + val = (val << 8) | r; + } + + *ret = val; + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_aggregate_message_handler(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + uint32_t base_time, timestamp, prev_size; + size_t len; + ngx_int_t first; + u_char *last; + ngx_int_t rc; + ngx_buf_t *b; + ngx_chain_t *cl, *next; + ngx_rtmp_header_t ch; + + ch = *h; + + first = 1; + base_time = 0; + + while (in) { + if (ngx_rtmp_fetch_uint8(&in, &ch.type) != NGX_OK) { + return NGX_OK; + } + + if (ngx_rtmp_fetch_uint32(&in, &ch.mlen, 3) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_fetch_uint32(&in, ×tamp, 3) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_fetch_uint8(&in, (uint8_t *) ×tamp + 3) != NGX_OK) + { + return NGX_ERROR; + } + + if (ngx_rtmp_fetch_uint32(&in, &ch.msid, 3) != NGX_OK) + { + return NGX_ERROR; + } + + if (first) { + base_time = timestamp; + first = 0; + } + + ngx_log_debug6(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "RTMP aggregate %s (%d) len=%uD time=%uD (+%D) msid=%uD", + ngx_rtmp_message_type(ch.type), + (ngx_int_t) ch.type, ch.mlen, ch.timestamp, + timestamp - base_time, ch.msid); + + /* limit chain */ + + len = 0; + cl = in; + while (cl) { + b = cl->buf; + len += (b->last - b->pos); + if (len > ch.mlen) { + break; + } + cl = cl->next; + } + + if (cl == NULL) { + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "RTMP error parsing aggregate"); + return NGX_ERROR; + } + + next = cl->next; + cl->next = NULL; + b = cl->buf; + last = b->last; + b->last -= (len - ch.mlen); + + /* handle aggregated message */ + + ch.timestamp = h->timestamp + timestamp - base_time; + + rc = ngx_rtmp_receive_message(s, &ch, in); + + /* restore chain before checking the result */ + + in = cl; + in->next = next; + b->pos = b->last; + b->last = last; + + if (rc != NGX_OK) { + return rc; + } + + /* read 32-bit previous tag size */ + + if (ngx_rtmp_fetch_uint32(&in, &prev_size, 4) != NGX_OK) { + return NGX_OK; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "RTMP aggregate prev_size=%uD", prev_size); + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_amf_message_handler(ngx_rtmp_session_t *s, + ngx_rtmp_header_t *h, ngx_chain_t *in) +{ + ngx_rtmp_amf_ctx_t act; + ngx_rtmp_core_main_conf_t *cmcf; + ngx_array_t *ch; + ngx_rtmp_handler_pt *ph; + ngx_chain_t *cl; + ngx_int_t amf_len; + size_t len, n; + + static u_char func[128]; + + static ngx_rtmp_amf_elt_t elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + func, sizeof(func) }, + }; + + /* AMF command names come with string type, but shared object names + * come without type */ + if (h->type == NGX_RTMP_MSG_AMF_SHARED || + h->type == NGX_RTMP_MSG_AMF3_SHARED) + { + elts[0].type |= NGX_RTMP_AMF_TYPELESS; + } else { + elts[0].type &= ~NGX_RTMP_AMF_TYPELESS; + } + + if ((h->type == NGX_RTMP_MSG_AMF3_SHARED || + h->type == NGX_RTMP_MSG_AMF3_META || + h->type == NGX_RTMP_MSG_AMF3_CMD) + && in->buf->last > in->buf->pos) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "AMF3 prefix: %ui", (ngx_int_t)*in->buf->pos); + ++in->buf->pos; + } + + cmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_core_module); + + /* + * work around the buggy option `-map` in FFmpeg, see: + * https://trac.ffmpeg.org/ticket/10565 + */ + if (in->buf->pos[0] == NGX_RTMP_AMF_NUMBER) { + cl = in; + amf_len = 0; + + while (cl) { + amf_len += cl->buf->last - cl->buf->pos; + /* type: 1B, number payload: 8B */ + if (amf_len >= 9) { + break; + } + + cl = cl->next; + } + + if (amf_len < 9) { + ngx_log_error(NGX_LOG_WARN, s->connection->log, 0, + "AMF malformed: type=%d, length=%D, ignored", + NGX_RTMP_AMF_NUMBER, amf_len); + return NGX_OK; + } + } + + /* read AMF func name & transaction id */ + ngx_memzero(&act, sizeof(act)); + act.link = in; + act.log = s->connection->log; + memset(func, 0, sizeof(func)); + + if (ngx_rtmp_amf_read(&act, elts, + sizeof(elts) / sizeof(elts[0])) != NGX_OK) + { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "AMF cmd failed"); + return NGX_ERROR; + } + + /* skip name */ + in = act.link; + in->buf->pos += act.offset; + + len = ngx_strlen(func); + + ch = ngx_hash_find(&cmcf->amf_hash, + ngx_hash_strlow(func, func, len), func, len); + + if (ch && ch->nelts) { + ph = ch->elts; + for (n = 0; n < ch->nelts; ++n, ++ph) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "AMF func '%s' passed to handler %d/%d", + func, n, ch->nelts); + switch ((*ph)(s, h, in)) { + case NGX_ERROR: + return NGX_ERROR; + case NGX_DONE: + return NGX_OK; + } + } + } else { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "AMF cmd '%s' no handler", func); + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_receive_amf(ngx_rtmp_session_t *s, ngx_chain_t *in, + ngx_rtmp_amf_elt_t *elts, size_t nelts) +{ + ngx_rtmp_amf_ctx_t act; + + ngx_memzero(&act, sizeof(act)); + act.link = in; + act.log = s->connection->log; + + return ngx_rtmp_amf_read(&act, elts, nelts); +} diff --git a/ngx_http_flv_module/ngx_rtmp_record_module.c b/ngx_http_flv_module/ngx_rtmp_record_module.c new file mode 100644 index 0000000..fec3b96 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_record_module.c @@ -0,0 +1,1327 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_netcall_module.h" +#include "ngx_rtmp_codec_module.h" +#include "ngx_rtmp_record_module.h" + + +ngx_rtmp_record_done_pt ngx_rtmp_record_done; + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_close_stream_pt next_close_stream; +static ngx_rtmp_stream_begin_pt next_stream_begin; +static ngx_rtmp_stream_eof_pt next_stream_eof; + + +static char *ngx_rtmp_record_recorder(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static ngx_int_t ngx_rtmp_record_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_record_create_app_conf(ngx_conf_t *cf); +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_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, + ngx_rtmp_record_rec_ctx_t *rctx, ngx_rtmp_header_t *h, ngx_chain_t *in); +static ngx_int_t ngx_rtmp_record_node_open(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx); +static ngx_int_t ngx_rtmp_record_node_close(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx); +static void ngx_rtmp_record_make_path(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx, ngx_str_t *path); +static ngx_int_t ngx_rtmp_record_init(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_record_init_ctx(ngx_rtmp_session_t *s); + + +static ngx_conf_bitmask_t ngx_rtmp_record_mask[] = { + { ngx_string("off"), NGX_RTMP_RECORD_OFF }, + { ngx_string("all"), NGX_RTMP_RECORD_AUDIO | + NGX_RTMP_RECORD_VIDEO }, + { ngx_string("audio"), NGX_RTMP_RECORD_AUDIO }, + { ngx_string("video"), NGX_RTMP_RECORD_VIDEO }, + { ngx_string("keyframes"), NGX_RTMP_RECORD_KEYFRAMES }, + { ngx_string("manual"), NGX_RTMP_RECORD_MANUAL }, + { ngx_null_string, 0 } +}; + + +static ngx_command_t ngx_rtmp_record_commands[] = { + + { ngx_string("record"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF| + NGX_RTMP_REC_CONF|NGX_CONF_1MORE, + ngx_conf_set_bitmask_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, flags), + ngx_rtmp_record_mask }, + + { ngx_string("record_path"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF| + NGX_RTMP_REC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, path), + NULL }, + + { ngx_string("record_suffix"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF| + NGX_RTMP_REC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, suffix), + NULL }, + + { ngx_string("record_unique"), + 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, unique), + NULL }, + + { ngx_string("record_append"), + 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, append), + NULL }, + + { ngx_string("record_lock"), + 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, lock_file), + NULL }, + + { ngx_string("record_max_size"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF| + NGX_RTMP_REC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, max_size), + NULL }, + + { ngx_string("record_max_frames"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF| + NGX_RTMP_REC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_size_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, max_frames), + NULL }, + + { ngx_string("record_interval"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF| + NGX_RTMP_REC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_record_app_conf_t, interval), + NULL }, + + { ngx_string("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, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_record_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_record_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + ngx_rtmp_record_create_app_conf, /* create app configuration */ + ngx_rtmp_record_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_record_module = { + NGX_MODULE_V1, + &ngx_rtmp_record_module_ctx, /* module context */ + ngx_rtmp_record_commands, /* module directives */ + NGX_RTMP_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 +}; + + +static void * +ngx_rtmp_record_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_record_app_conf_t *racf; + + racf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_record_app_conf_t)); + + if (racf == NULL) { + return NULL; + } + + racf->max_size = NGX_CONF_UNSET_SIZE; + racf->max_frames = NGX_CONF_UNSET_SIZE; + racf->interval = NGX_CONF_UNSET_MSEC; + racf->unique = NGX_CONF_UNSET; + racf->append = NGX_CONF_UNSET; + racf->lock_file = NGX_CONF_UNSET; + racf->notify = NGX_CONF_UNSET; + racf->url = NGX_CONF_UNSET_PTR; + + if (ngx_array_init(&racf->rec, cf->pool, 1, sizeof(void *)) != NGX_OK) { + return NULL; + } + + return racf; +} + + +static char * +ngx_rtmp_record_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_record_app_conf_t *prev = parent; + ngx_rtmp_record_app_conf_t *conf = child; + ngx_rtmp_record_app_conf_t **rracf; + + ngx_conf_merge_str_value(conf->path, prev->path, ""); + ngx_conf_merge_str_value(conf->suffix, prev->suffix, ".flv"); + 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->append, prev->append, 0); + ngx_conf_merge_value(conf->lock_file, prev->lock_file, 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_MSEC); + ngx_conf_merge_bitmask_value(conf->flags, prev->flags, 0); + ngx_conf_merge_ptr_value(conf->url, prev->url, NULL); + + if (conf->flags) { + rracf = ngx_array_push(&conf->rec); + if (rracf == NULL) { + return NGX_CONF_ERROR; + } + + *rracf = conf; + } + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_record_write_header(ngx_file_t *file) +{ + static u_char flv_header[] = { + 0x46, /* 'F' */ + 0x4c, /* 'L' */ + 0x56, /* 'V' */ + 0x01, /* version = 1 */ + 0x05, /* 00000 1 0 1 = has audio & video */ + 0x00, + 0x00, + 0x00, + 0x09, /* header size */ + 0x00, + 0x00, + 0x00, + 0x00 /* PreviousTagSize0 (not actually a header) */ + }; + + return ngx_write_file(file, flv_header, sizeof(flv_header), 0) == NGX_ERROR + ? NGX_ERROR + : NGX_OK; +} + + +static ngx_rtmp_record_rec_ctx_t * +ngx_rtmp_record_get_node_ctx(ngx_rtmp_session_t *s, ngx_uint_t n) +{ + ngx_rtmp_record_ctx_t *ctx; + ngx_rtmp_record_rec_ctx_t *rctx; + + if (ngx_rtmp_record_init(s) != NGX_OK) { + return NULL; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + + if (n >= ctx->rec.nelts) { + return NULL; + } + + rctx = ctx->rec.elts; + + return &rctx[n]; +} + + +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); + + rctx = ngx_rtmp_record_get_node_ctx(s, n); + + if (rctx == NULL) { + return NGX_ERROR; + } + + rc = ngx_rtmp_record_node_open(s, rctx); + if (rc != NGX_OK) { + return rc; + } + + if (path) { + ngx_rtmp_record_make_path(s, rctx, path); + } + + return NGX_OK; +} + + +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); + + rctx = ngx_rtmp_record_get_node_ctx(s, n); + + if (rctx == NULL) { + return NGX_ERROR; + } + + rc = ngx_rtmp_record_node_close(s, rctx); + if (rc != NGX_OK) { + return rc; + } + + if (path) { + ngx_rtmp_record_make_path(s, rctx, path); + } + + return NGX_OK; +} + + +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; +} + + +void +ngx_rtmp_record_get_path(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx, ngx_str_t *path) +{ + ngx_rtmp_record_make_path(s, rctx, path); +} + + +/* This funcion returns pointer to a static buffer */ +static void +ngx_rtmp_record_make_path(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx, ngx_str_t *path) +{ + ngx_rtmp_record_ctx_t *ctx; + ngx_rtmp_record_app_conf_t *rracf; + u_char *p, *l; + struct tm tm; + + static u_char buf[NGX_TIME_T_LEN + 1]; + static u_char pbuf[NGX_MAX_PATH + 1]; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + + rracf = rctx->conf; + + /* create file path */ + p = pbuf; + l = pbuf + sizeof(pbuf) - 1; + + p = ngx_cpymem(p, rracf->path.data, + ngx_min(rracf->path.len, (size_t)(l - p - 1))); + *p++ = '/'; + p = (u_char *)ngx_escape_uri(p, ctx->name, ngx_min(ngx_strlen(ctx->name), + (size_t)(l - p)), NGX_ESCAPE_URI_COMPONENT); + + /* append timestamp */ + if (rracf->unique) { + p = ngx_cpymem(p, buf, ngx_min(ngx_sprintf(buf, "-%T", + rctx->timestamp) - buf, l - p)); + } + + if (ngx_strchr(rracf->suffix.data, '%')) { + ngx_libc_localtime(rctx->timestamp, &tm); + p += strftime((char *) p, l - p, (char *) rracf->suffix.data, &tm); + } else { + p = ngx_cpymem(p, rracf->suffix.data, + ngx_min(rracf->suffix.len, (size_t)(l - p))); + } + + *p = 0; + path->data = pbuf; + path->len = p - pbuf; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V path: '%V'", &rracf->id, path); +} + + +static void +ngx_rtmp_record_notify_error(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx) +{ + ngx_rtmp_record_app_conf_t *rracf = rctx->conf; + + rctx->failed = 1; + + if (!rracf->notify) { + return; + } + + ngx_rtmp_send_status(s, "NetStream.Record.Failed", "error", + rracf->id.data ? (char *) rracf->id.data : ""); +} + + +static ngx_int_t +ngx_rtmp_record_node_open(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx) +{ + ngx_rtmp_record_app_conf_t *rracf; + ngx_err_t err; + ngx_str_t path; + ngx_int_t mode, create_mode; + u_char *p, buf[8]; + off_t file_size; + uint32_t tag_size, mlen, timestamp; + + rracf = rctx->conf; + tag_size = 0; + p = buf; + + if (rctx->file.fd != NGX_INVALID_FILE) { + return NGX_AGAIN; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V opening", &rracf->id); + + ngx_memzero(rctx, sizeof(*rctx)); + rctx->conf = rracf; + rctx->last = *ngx_cached_time; + rctx->timestamp = ngx_cached_time->sec; + + ngx_rtmp_record_make_path(s, rctx, &path); + + mode = rracf->append ? NGX_FILE_RDWR : NGX_FILE_WRONLY; + create_mode = rracf->append ? NGX_FILE_CREATE_OR_OPEN : NGX_FILE_TRUNCATE; + + ngx_memzero(&rctx->file, sizeof(rctx->file)); + rctx->file.offset = 0; + rctx->file.log = s->connection->log; + rctx->file.fd = ngx_open_file(path.data, mode, create_mode, + NGX_FILE_DEFAULT_ACCESS); + ngx_str_set(&rctx->file.name, "recorded"); + + if (rctx->file.fd == NGX_INVALID_FILE) { + err = ngx_errno; + + if (err != NGX_ENOENT) { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, err, + "record: %V failed to open file '%V'", + &rracf->id, &path); + } + + ngx_rtmp_record_notify_error(s, rctx); + + return NGX_OK; + } + +#if !(NGX_WIN32) + if (rracf->lock_file) { + err = ngx_lock_fd(rctx->file.fd); + if (err) { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, err, + "record: %V lock failed", &rracf->id); + } + } +#endif + + 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 : ""); + } + + if (rracf->append) { + mlen = 0; + file_size = 0; + timestamp = 0; + +#if (NGX_WIN32) + { + LONG lo, hi; + + lo = 0; + hi = 0; + lo = SetFilePointer(rctx->file.fd, lo, &hi, FILE_END); + file_size = (lo == INVALID_SET_FILE_POINTER ? + (off_t) -1 : (off_t) hi << 32 | (off_t) lo); + } +#else + file_size = lseek(rctx->file.fd, 0, SEEK_END); +#endif + if (file_size == (off_t) -1) { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno, + "record: %V seek failed", &rracf->id); + goto done; + } + + if (file_size < 4) { + goto done; + } + + if (ngx_read_file(&rctx->file, p, 4, file_size - 4) != 4) { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno, + "record: %V tag size read failed", &rracf->id); + goto done; + } + + tag_size = ntohl(*(uint32_t *) p); + + if (tag_size == 0 || tag_size + 4 > file_size) { + file_size = 0; + goto done; + } + + if (ngx_read_file(&rctx->file, p, 8, file_size - tag_size - 4) != 8) + { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno, + "record: %V tag read failed", &rracf->id); + goto done; + } + + mlen = ngx_rtmp_n3_to_h4(p + 1); + + if (tag_size != mlen + 11) { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno, + "record: %V tag size mismatch: " + "tag_size=%uD, mlen=%uD", &rracf->id, tag_size, mlen); + goto done; + } + + timestamp = ngx_rtmp_n3_to_h4(p + 4); + timestamp |= ((uint32_t) p[7] << 24); + +done: + rctx->file.offset = file_size; + rctx->time_shift = timestamp; + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: append offset=%O, time=%uD, tag_size=%uD", + file_size, timestamp, tag_size); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_record_init(ngx_rtmp_session_t *s) +{ + ngx_uint_t n; + ngx_rtmp_record_app_conf_t *racf, **rracf; + ngx_rtmp_record_rec_ctx_t *rctx; + ngx_rtmp_record_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + + if (ctx && !s->auto_pushed) { + return NGX_OK; + } + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module); + + if (racf == NULL || racf->rec.nelts == 0) { + return NGX_OK; + } + + if (ngx_rtmp_record_init_ctx(s) != NGX_OK) { + return NGX_ERROR; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + + if (ctx->rec.nelts) { + return NGX_OK; + } + + if (ngx_array_init(&ctx->rec, s->connection->pool, racf->rec.nelts, + sizeof(ngx_rtmp_record_rec_ctx_t)) + != NGX_OK) + { + return NGX_ERROR; + } + + rracf = racf->rec.elts; + + rctx = ngx_array_push_n(&ctx->rec, racf->rec.nelts); + + if (rctx == NULL) { + return NGX_ERROR; + } + + for (n = 0; n < racf->rec.nelts; ++n, ++rracf, ++rctx) { + ngx_memzero(rctx, sizeof(*rctx)); + + rctx->conf = *rracf; + rctx->file.fd = NGX_INVALID_FILE; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_record_init_ctx(ngx_rtmp_session_t *s) +{ + ngx_rtmp_record_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_record_ctx_t)); + + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_record_module); + } + + return NGX_OK; +} + + +static void +ngx_rtmp_record_start(ngx_rtmp_session_t *s) +{ + ngx_rtmp_record_app_conf_t *racf; + ngx_rtmp_record_rec_ctx_t *rctx; + ngx_rtmp_record_ctx_t *ctx; + ngx_uint_t n; + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module); + if (racf == NULL || racf->rec.nelts == 0) { + return; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + if (ctx == NULL) { + return; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: start"); + + rctx = ctx->rec.elts; + for (n = 0; n < ctx->rec.nelts; ++n, ++rctx) { + if (rctx->conf->flags & (NGX_RTMP_RECORD_OFF|NGX_RTMP_RECORD_MANUAL)) { + continue; + } + ngx_rtmp_record_node_open(s, rctx); + } +} + + +static void +ngx_rtmp_record_stop(ngx_rtmp_session_t *s) +{ + ngx_rtmp_record_app_conf_t *racf; + ngx_rtmp_record_rec_ctx_t *rctx; + ngx_rtmp_record_ctx_t *ctx; + ngx_uint_t n; + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module); + if (racf == NULL || racf->rec.nelts == 0) { + return; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + if (ctx == NULL) { + return; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: stop"); + + rctx = ctx->rec.elts; + for (n = 0; n < ctx->rec.nelts; ++n, ++rctx) { + ngx_rtmp_record_node_close(s, rctx); + } +} + + +static ngx_int_t +ngx_rtmp_record_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_record_app_conf_t *racf; + ngx_rtmp_record_ctx_t *ctx; + u_char *p; + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module); + + if (racf == NULL || racf->rec.nelts == 0) { + goto next; + } + + if (s->auto_pushed) { + if (ngx_rtmp_record_init_ctx(s) != NGX_OK) { + return NGX_ERROR; + } + } else { + if (ngx_rtmp_record_init(s) != NGX_OK) { + return NGX_ERROR; + } + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + + ngx_memcpy(ctx->name, v->name, sizeof(ctx->name)); + ngx_memcpy(ctx->args, v->args, sizeof(ctx->args)); + + /* terminate name on /../ */ + for (p = ctx->name; *p; ++p) { + if (ngx_path_separator(p[0]) && + p[1] == '.' && p[2] == '.' && + ngx_path_separator(p[3])) + { + *p = 0; + break; + } + } + + if (s->auto_pushed) { + goto next; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: publish %ui nodes", + racf->rec.nelts); + + ngx_rtmp_record_start(s); + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_record_stream_begin(ngx_rtmp_session_t *s, ngx_rtmp_stream_begin_t *v) +{ + if (s->auto_pushed) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: stream_begin"); + + ngx_rtmp_record_start(s); + +next: + return next_stream_begin(s, v); +} + + +static ngx_int_t +ngx_rtmp_record_stream_eof(ngx_rtmp_session_t *s, ngx_rtmp_stream_begin_t *v) +{ + if (s->auto_pushed) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: stream_eof"); + + ngx_rtmp_record_stop(s); + +next: + return next_stream_eof(s, v); +} + + +static ngx_int_t +ngx_rtmp_record_node_close(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx) +{ + ngx_rtmp_record_app_conf_t *rracf; + ngx_err_t err; + void **app_conf; + ngx_int_t rc; + ngx_rtmp_record_done_t v; + u_char av; + + rracf = rctx->conf; + + if (rctx->file.fd == NGX_INVALID_FILE) { + return NGX_AGAIN; + } + + if (rctx->initialized) { + av = 0; + + if (rctx->video) { + av |= 0x01; + } + + if (rctx->audio) { + av |= 0x04; + } + + if (ngx_write_file(&rctx->file, &av, 1, 4) == NGX_ERROR) { + ngx_log_error(NGX_LOG_CRIT, s->connection->log, ngx_errno, + "record: %V error writing av mask", &rracf->id); + } + } + + if (ngx_close_file(rctx->file.fd) == NGX_FILE_ERROR) { + err = ngx_errno; + ngx_log_error(NGX_LOG_CRIT, s->connection->log, err, + "record: %V error closing file", &rracf->id); + + ngx_rtmp_record_notify_error(s, rctx); + } + + rctx->file.fd = NGX_INVALID_FILE; + + 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) { + s->app_conf = rracf->rec_conf; + } + + v.recorder = rracf->id; + ngx_rtmp_record_make_path(s, rctx, &v.path); + + rc = ngx_rtmp_record_done(s, &v); + + s->app_conf = app_conf; + + return rc; +} + + +static ngx_int_t +ngx_rtmp_record_close_stream(ngx_rtmp_session_t *s, + ngx_rtmp_close_stream_t *v) +{ + if (s->auto_pushed) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: close_stream"); + + ngx_rtmp_record_stop(s); + +next: + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_record_write_frame(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx, + ngx_rtmp_header_t *h, ngx_chain_t *in, + ngx_int_t inc_nframes) +{ + u_char hdr[11], *ph; + uint32_t timestamp, tag_size; + ngx_rtmp_record_app_conf_t *rracf; + + rracf = rctx->conf; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V frame: mlen=%uD", + &rracf->id, h->mlen); + + if (h->type == NGX_RTMP_MSG_VIDEO) { + rctx->video = 1; + } else { + rctx->audio = 1; + } + + timestamp = h->timestamp - rctx->epoch; + + if ((int32_t) timestamp < 0) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V cut timestamp=%D", &rracf->id, timestamp); + + timestamp = 0; + } + + /* write tag header */ + ph = hdr; + + *ph++ = (u_char)h->type; + + ngx_rtmp_h4_to_n3(ph, h->mlen); + ph += 3; + + ngx_rtmp_h4_to_n3(ph, timestamp); + ph += 3; + *ph++ = (u_char) (timestamp >> 24); + + *ph++ = 0; + *ph++ = 0; + *ph++ = 0; + + tag_size = (ph - hdr) + h->mlen; + + if (ngx_write_file(&rctx->file, hdr, ph - hdr, rctx->file.offset) + == NGX_ERROR) + { + ngx_rtmp_record_notify_error(s, rctx); + + ngx_close_file(rctx->file.fd); + + return NGX_ERROR; + } + + /* write tag body + * FIXME: NGINX + * ngx_write_chain seems to fit best + * but it suffers from uncontrollable + * allocations. + * we're left with plain writing */ + for(; in; in = in->next) { + if (in->buf->pos == in->buf->last) { + continue; + } + + if (ngx_write_file(&rctx->file, in->buf->pos, in->buf->last + - in->buf->pos, rctx->file.offset) + == NGX_ERROR) + { + return NGX_ERROR; + } + } + + /* write tag size */ + ph = hdr; + *(uint32_t *) ph = htonl(tag_size); + ph += 4; + + if (ngx_write_file(&rctx->file, hdr, ph - hdr, + rctx->file.offset) + == NGX_ERROR) + { + return NGX_ERROR; + } + + rctx->nframes += inc_nframes; + + /* watch max size */ + if ((rracf->max_size && rctx->file.offset >= (ngx_int_t) rracf->max_size) || + (rracf->max_frames && rctx->nframes >= rracf->max_frames)) + { + ngx_rtmp_record_node_close(s, rctx); + } + + return NGX_OK; +} + + +static size_t +ngx_rtmp_record_get_chain_mlen(ngx_chain_t *in) +{ + size_t ret; + + for (ret = 0; in; in = in->next) { + ret += (in->buf->last - in->buf->pos); + } + + return ret; +} + + +static ngx_int_t +ngx_rtmp_record_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_record_ctx_t *ctx; + ngx_rtmp_record_rec_ctx_t *rctx; + ngx_uint_t n; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + + if (ctx == NULL) { + return NGX_OK; + } + + rctx = ctx->rec.elts; + + for (n = 0; n < ctx->rec.nelts; ++n, ++rctx) { + ngx_rtmp_record_node_av(s, rctx, h, in); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_record_node_av(ngx_rtmp_session_t *s, ngx_rtmp_record_rec_ctx_t *rctx, + ngx_rtmp_header_t *h, ngx_chain_t *in) +{ + ngx_time_t next; + ngx_rtmp_header_t ch; + ngx_rtmp_codec_ctx_t *codec_ctx; + ngx_int_t keyframe, brkframe; + ngx_rtmp_record_app_conf_t *rracf; + + rracf = rctx->conf; + + if (rracf->flags & NGX_RTMP_RECORD_OFF) { + ngx_rtmp_record_node_close(s, rctx); + return NGX_OK; + } + + keyframe = (h->type == NGX_RTMP_MSG_VIDEO) + ? (ngx_rtmp_get_video_frame_type(in) == NGX_RTMP_VIDEO_KEY_FRAME) + : 0; + + brkframe = (h->type == NGX_RTMP_MSG_VIDEO) + ? keyframe + : (rracf->flags & NGX_RTMP_RECORD_VIDEO) == 0; + + if (brkframe && (rracf->flags & NGX_RTMP_RECORD_MANUAL) == 0) { + + if (rracf->interval != NGX_CONF_UNSET_MSEC) { + + next = rctx->last; + next.msec += rracf->interval; + next.sec += (next.msec / 1000); + next.msec %= 1000; + + if (ngx_cached_time->sec > next.sec || + (ngx_cached_time->sec == next.sec && + ngx_cached_time->msec > next.msec)) + { + ngx_rtmp_record_node_close(s, rctx); + ngx_rtmp_record_node_open(s, rctx); + } + + } else if (!rctx->failed) { + ngx_rtmp_record_node_open(s, rctx); + } + } + + if ((rracf->flags & NGX_RTMP_RECORD_MANUAL) && + !brkframe && rctx->nframes == 0) + { + return NGX_OK; + } + + if (rctx->file.fd == NGX_INVALID_FILE) { + return NGX_OK; + } + + if (h->type == NGX_RTMP_MSG_AUDIO && + (rracf->flags & NGX_RTMP_RECORD_AUDIO) == 0) + { + return NGX_OK; + } + + if (h->type == NGX_RTMP_MSG_VIDEO && + (rracf->flags & NGX_RTMP_RECORD_VIDEO) == 0 && + ((rracf->flags & NGX_RTMP_RECORD_KEYFRAMES) == 0 || !keyframe)) + { + return NGX_OK; + } + + if (!rctx->initialized) { + + rctx->initialized = 1; + rctx->epoch = h->timestamp - rctx->time_shift; + + if (rctx->file.offset == 0 && + ngx_rtmp_record_write_header(&rctx->file) != NGX_OK) + { + ngx_rtmp_record_node_close(s, rctx); + return NGX_OK; + } + } + + codec_ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + if (codec_ctx) { + ch = *h; + + /* AAC header */ + if (!rctx->aac_header_sent && codec_ctx->aac_header && + (rracf->flags & NGX_RTMP_RECORD_AUDIO)) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V writing AAC header", &rracf->id); + + ch.type = NGX_RTMP_MSG_AUDIO; + ch.mlen = ngx_rtmp_record_get_chain_mlen(codec_ctx->aac_header); + + if (ngx_rtmp_record_write_frame(s, rctx, &ch, + codec_ctx->aac_header, 0) + != NGX_OK) + { + return NGX_OK; + } + + rctx->aac_header_sent = 1; + } + + /* AVC header */ + if (!rctx->avc_header_sent && codec_ctx->avc_header && + (rracf->flags & (NGX_RTMP_RECORD_VIDEO| + NGX_RTMP_RECORD_KEYFRAMES))) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V writing AVC header", &rracf->id); + + ch.type = NGX_RTMP_MSG_VIDEO; + ch.mlen = ngx_rtmp_record_get_chain_mlen(codec_ctx->avc_header); + + if (ngx_rtmp_record_write_frame(s, rctx, &ch, + codec_ctx->avc_header, 0) + != NGX_OK) + { + return NGX_OK; + } + + rctx->avc_header_sent = 1; + } + } + + if (h->type == NGX_RTMP_MSG_VIDEO) { + if (codec_ctx && codec_ctx->video_codec_id == NGX_RTMP_VIDEO_H264 && + !rctx->avc_header_sent) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V skipping until H264 header", &rracf->id); + return NGX_OK; + } + + if (ngx_rtmp_get_video_frame_type(in) == NGX_RTMP_VIDEO_KEY_FRAME && + ((codec_ctx && codec_ctx->video_codec_id != NGX_RTMP_VIDEO_H264) || + !ngx_rtmp_is_codec_header(in))) + { + rctx->video_key_sent = 1; + } + + if (!rctx->video_key_sent) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V skipping until keyframe", &rracf->id); + return NGX_OK; + } + + } else { + if (codec_ctx && codec_ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC && + !rctx->aac_header_sent) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "record: %V skipping until AAC header", &rracf->id); + return NGX_OK; + } + } + + return ngx_rtmp_record_write_frame(s, rctx, h, in, 1); +} + + +static ngx_int_t +ngx_rtmp_record_done_init(ngx_rtmp_session_t *s, ngx_rtmp_record_done_t *v) +{ + return NGX_OK; +} + + +static char * +ngx_rtmp_record_recorder(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + char *rv; + ngx_int_t i; + ngx_str_t *value; + ngx_conf_t save; + ngx_module_t **modules; + ngx_rtmp_module_t *module; + ngx_rtmp_core_app_conf_t *cacf, **pcacf, *rcacf; + ngx_rtmp_record_app_conf_t *racf, **pracf, *rracf; + ngx_rtmp_conf_ctx_t *ctx, *pctx; + + value = cf->args->elts; + + cacf = ngx_rtmp_conf_get_module_app_conf(cf, ngx_rtmp_core_module); + + racf = ngx_rtmp_conf_get_module_app_conf(cf, ngx_rtmp_record_module); + + ctx = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_conf_ctx_t)); + if (ctx == NULL) { + return NGX_CONF_ERROR; + } + + pctx = cf->ctx; + + ctx->main_conf = pctx->main_conf; + ctx->srv_conf = pctx->srv_conf; + + ctx->app_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_rtmp_max_module); + if (ctx->app_conf == NULL) { + return NGX_CONF_ERROR; + } + +#if (nginx_version >= 1009011) + modules = cf->cycle->modules; +#else + modules = ngx_modules; +#endif + + for (i = 0; modules[i]; i++) { + if (modules[i]->type != NGX_RTMP_MODULE) { + continue; + } + + module = modules[i]->ctx; + + if (module->create_app_conf) { + ctx->app_conf[modules[i]->ctx_index] = module->create_app_conf(cf); + if (ctx->app_conf[modules[i]->ctx_index] == NULL) { + return NGX_CONF_ERROR; + } + } + } + + /* add to sub-applications */ + rcacf = ctx->app_conf[ngx_rtmp_core_module.ctx_index]; + rcacf->app_conf = ctx->app_conf; + pcacf = ngx_array_push(&cacf->applications); + if (pcacf == NULL) { + return NGX_CONF_ERROR; + } + *pcacf = rcacf; + + /* add to recorders */ + rracf = ctx->app_conf[ngx_rtmp_record_module.ctx_index]; + rracf->rec_conf = ctx->app_conf; + pracf = ngx_array_push(&racf->rec); + if (pracf == NULL) { + return NGX_CONF_ERROR; + } + *pracf = rracf; + + rracf->id = value[1]; + + + save = *cf; + cf->ctx = ctx; + cf->cmd_type = NGX_RTMP_REC_CONF; + + rv = ngx_conf_parse(cf, NULL); + *cf= save; + + return rv; +} + + +static ngx_int_t +ngx_rtmp_record_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + + ngx_rtmp_record_done = ngx_rtmp_record_done_init; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]); + *h = ngx_rtmp_record_av; + + h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]); + *h = ngx_rtmp_record_av; + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_record_publish; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_record_close_stream; + + next_stream_begin = ngx_rtmp_stream_begin; + ngx_rtmp_stream_begin = ngx_rtmp_record_stream_begin; + + next_stream_eof = ngx_rtmp_stream_eof; + ngx_rtmp_stream_eof = ngx_rtmp_record_stream_eof; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_record_module.h b/ngx_http_flv_module/ngx_rtmp_record_module.h new file mode 100644 index 0000000..90c0061 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_record_module.h @@ -0,0 +1,100 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#ifndef _NGX_RTMP_RECORD_H_INCLUDED_ +#define _NGX_RTMP_RECORD_H_INCLUDED_ + + +#include +#include +#include "ngx_rtmp.h" + + +#define NGX_RTMP_RECORD_OFF 0x01 +#define NGX_RTMP_RECORD_AUDIO 0x02 +#define NGX_RTMP_RECORD_VIDEO 0x04 +#define NGX_RTMP_RECORD_KEYFRAMES 0x08 +#define NGX_RTMP_RECORD_MANUAL 0x10 + + +typedef struct { + ngx_str_t id; + ngx_uint_t flags; + ngx_str_t path; + size_t max_size; + size_t max_frames; + ngx_msec_t interval; + ngx_str_t suffix; + ngx_flag_t unique; + ngx_flag_t append; + ngx_flag_t lock_file; + ngx_flag_t notify; + ngx_url_t *url; + + void **rec_conf; + ngx_array_t rec; /* ngx_rtmp_record_app_conf_t * */ +} ngx_rtmp_record_app_conf_t; + + +typedef struct { + ngx_rtmp_record_app_conf_t *conf; + ngx_file_t file; + ngx_uint_t nframes; + uint32_t epoch, time_shift; + ngx_time_t last; + time_t timestamp; + unsigned failed:1; + unsigned initialized:1; + unsigned aac_header_sent:1; + unsigned avc_header_sent:1; + unsigned video_key_sent:1; + unsigned audio:1; + unsigned video:1; +} ngx_rtmp_record_rec_ctx_t; + + +typedef struct { + ngx_array_t rec; /* ngx_rtmp_record_rec_ctx_t */ + u_char name[NGX_RTMP_MAX_NAME]; + u_char args[NGX_RTMP_MAX_ARGS]; +} ngx_rtmp_record_ctx_t; + + +ngx_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 */ + +ngx_int_t ngx_rtmp_record_open(ngx_rtmp_session_t *s, ngx_uint_t n, + ngx_str_t *path); +ngx_int_t ngx_rtmp_record_close(ngx_rtmp_session_t *s, ngx_uint_t n, + ngx_str_t *path); + +void ngx_rtmp_record_get_path(ngx_rtmp_session_t *s, + ngx_rtmp_record_rec_ctx_t *rctx, ngx_str_t *path); + + +typedef struct { + ngx_str_t recorder; + ngx_str_t path; +} ngx_rtmp_record_done_t; + + +typedef ngx_int_t (*ngx_rtmp_record_done_pt)(ngx_rtmp_session_t *s, + ngx_rtmp_record_done_t *v); + + +extern ngx_rtmp_record_done_pt ngx_rtmp_record_done; + + +extern ngx_module_t ngx_rtmp_record_module; + + +#endif /* _NGX_RTMP_RECORD_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_relay_module.c b/ngx_http_flv_module/ngx_rtmp_relay_module.c new file mode 100644 index 0000000..2aca461 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_relay_module.c @@ -0,0 +1,1954 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include "ngx_rtmp_relay_module.h" +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_eval.h" + + +static ngx_rtmp_publish_pt next_publish; +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_delete_stream_pt next_delete_stream; +static ngx_rtmp_close_stream_pt next_close_stream; + + +static ngx_int_t ngx_rtmp_relay_init_process(ngx_cycle_t *cycle); +static ngx_int_t ngx_rtmp_relay_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_relay_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_relay_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +static char * ngx_rtmp_relay_push_pull(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static ngx_int_t ngx_rtmp_relay_publish(ngx_rtmp_session_t *s, + ngx_rtmp_publish_t *v); +static ngx_rtmp_relay_ctx_t * ngx_rtmp_relay_create_connection( + ngx_rtmp_conf_ctx_t *cctx, ngx_str_t* name, + ngx_rtmp_relay_target_t *target); +static void ngx_rtmp_relay_eval_ctx_str(void *ctx, ngx_rtmp_eval_t *e, + ngx_str_t *ret); + +/* _____ + * =push= | |---publish---> + * ---publish--->| |---publish---> + * (src) | |---publish---> + * ----- (next,relay) + * need reconnect + * =pull= _____ + * -----play---->| | + * -----play---->| |----play-----> + * -----play---->| | (src,relay) + * (next) ----- + */ + + +typedef struct { + ngx_rtmp_conf_ctx_t cctx; + ngx_rtmp_relay_target_t *target; +} ngx_rtmp_relay_static_t; + + +#define NGX_RTMP_RELAY_CONNECT_TRANS 1 +#define NGX_RTMP_RELAY_RELEASE_STREAM_TRANS 2 +#define NGX_RTMP_RELAY_FCPUBLISH_STREAM_TRANS 3 +#define NGX_RTMP_RELAY_CREATE_STREAM_TRANS 4 + + +#define NGX_RTMP_RELAY_CSID_AMF_INI 3 +#define NGX_RTMP_RELAY_CSID_AMF 5 +#define NGX_RTMP_RELAY_MSID 1 + + +/* default flashVer */ +#define NGX_RTMP_RELAY_FLASHVER "LNX.11,1,102,55" + + +static ngx_command_t ngx_rtmp_relay_commands[] = { + + { ngx_string("push"), + NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_relay_push_pull, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("pull"), + NGX_RTMP_APP_CONF|NGX_CONF_1MORE, + ngx_rtmp_relay_push_pull, + NGX_RTMP_APP_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("relay_buffer"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_CONF_TAKE1, + ngx_conf_set_msec_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_relay_app_conf_t, buflen), + NULL }, + + { ngx_string("push_reconnect"), + 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_relay_app_conf_t, push_reconnect), + NULL }, + + { ngx_string("pull_reconnect"), + 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_relay_app_conf_t, pull_reconnect), + NULL }, + + { ngx_string("session_relay"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_flag_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_relay_app_conf_t, session_relay), + NULL }, + + + ngx_null_command +}; + + +static ngx_rtmp_module_t ngx_rtmp_relay_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_relay_postconfiguration, /* postconfiguration */ + NULL, /* create main configuration */ + NULL, /* init main configuration */ + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + ngx_rtmp_relay_create_app_conf, /* create app configuration */ + ngx_rtmp_relay_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_relay_module = { + NGX_MODULE_V1, + &ngx_rtmp_relay_module_ctx, /* module context */ + ngx_rtmp_relay_commands, /* module directives */ + NGX_RTMP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + ngx_rtmp_relay_init_process, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static ngx_rtmp_eval_t ngx_rtmp_relay_specific_eval[] = { + + { ngx_string("name"), + ngx_rtmp_relay_eval_ctx_str, + offsetof(ngx_rtmp_session_t, stream) }, + + { ngx_string("args"), + ngx_rtmp_relay_eval_ctx_str, + offsetof(ngx_rtmp_session_t, args) }, + + ngx_rtmp_null_eval +}; + + +static ngx_rtmp_eval_t *ngx_rtmp_relay_eval[] = { + ngx_rtmp_eval_session, + ngx_rtmp_relay_specific_eval, + NULL +}; + + +static void +ngx_rtmp_relay_eval_ctx_str(void *ctx, ngx_rtmp_eval_t *e, ngx_str_t *ret) +{ + *ret = *(ngx_str_t *) ((u_char *) ctx + e->offset); +} + + +static void * +ngx_rtmp_relay_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_relay_app_conf_t *racf; + + racf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_relay_app_conf_t)); + if (racf == NULL) { + return NULL; + } + + if (ngx_array_init(&racf->pushes, cf->pool, 1, sizeof(void *)) != NGX_OK) { + return NULL; + } + + if (ngx_array_init(&racf->pulls, cf->pool, 1, sizeof(void *)) != NGX_OK) { + return NULL; + } + + if (ngx_array_init(&racf->static_pulls, cf->pool, 1, sizeof(void *)) + != NGX_OK) + { + return NULL; + } + + if (ngx_array_init(&racf->static_events, cf->pool, 1, sizeof(void *)) + != NGX_OK) + { + return NULL; + } + + racf->nbuckets = 1024; + racf->log = &cf->cycle->new_log; + racf->buflen = NGX_CONF_UNSET_MSEC; + racf->session_relay = NGX_CONF_UNSET; + racf->push_reconnect = NGX_CONF_UNSET_MSEC; + racf->pull_reconnect = NGX_CONF_UNSET_MSEC; + + return racf; +} + + +static char * +ngx_rtmp_relay_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_relay_app_conf_t *prev = parent; + ngx_rtmp_relay_app_conf_t *conf = child; + + conf->ctx = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_relay_ctx_t *) + * conf->nbuckets); + if (conf->ctx == NULL) { + return NGX_CONF_ERROR; + } + + ngx_conf_merge_value(conf->session_relay, prev->session_relay, 0); + ngx_conf_merge_msec_value(conf->buflen, prev->buflen, 5000); + ngx_conf_merge_msec_value(conf->push_reconnect, prev->push_reconnect, + 3000); + ngx_conf_merge_msec_value(conf->pull_reconnect, prev->pull_reconnect, + 3000); + + return NGX_CONF_OK; +} + + +static void +ngx_rtmp_relay_static_pull_reconnect(ngx_event_t *ev) +{ + ngx_rtmp_relay_static_t *rs = ev->data; + + ngx_rtmp_relay_ctx_t *ctx; + ngx_rtmp_relay_app_conf_t *racf; + + racf = ngx_rtmp_get_module_app_conf(&rs->cctx, ngx_rtmp_relay_module); + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, racf->log, 0, + "relay: reconnecting static pull"); + + ctx = ngx_rtmp_relay_create_connection(&rs->cctx, &rs->target->name, + rs->target); + if (ctx) { + ctx->session->static_relay = 1; + ctx->static_evt = ev; + return; + } + + ngx_add_timer(ev, racf->pull_reconnect); +} + + +static void +ngx_rtmp_relay_push_reconnect(ngx_event_t *ev) +{ + ngx_rtmp_session_t *s = ev->data; + + ngx_rtmp_relay_app_conf_t *racf; + ngx_rtmp_relay_ctx_t *ctx, *pctx; + ngx_uint_t n; + ngx_rtmp_relay_target_t *target, **t; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "relay: push reconnect"); + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL) { + return; + } + + t = racf->pushes.elts; + for (n = 0; n < racf->pushes.nelts; ++n, ++t) { + target = *t; + + if (target->name.len && (ctx->name.len != target->name.len || + ngx_memcmp(ctx->name.data, target->name.data, ctx->name.len))) + { + continue; + } + + for (pctx = ctx->play; pctx; pctx = pctx->next) { + if (pctx->tag == &ngx_rtmp_relay_module && + pctx->data == target) + { + break; + } + } + + if (pctx) { + continue; + } + + if (ngx_rtmp_relay_push(s, &ctx->name, target) == NGX_OK) { + continue; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "relay: push reconnect failed name='%V' app='%V' " + "playpath='%V' url='%V' args='%V'", + &ctx->name, &target->app, &target->play_path, + &target->url.url, &s->args); + + if (!ctx->push_evt.timer_set) { + ngx_add_timer(&ctx->push_evt, racf->push_reconnect); + } + } +} + + +static ngx_int_t +ngx_rtmp_relay_get_peer(ngx_peer_connection_t *pc, void *data) +{ + return NGX_OK; +} + + +static void +ngx_rtmp_relay_free_peer(ngx_peer_connection_t *pc, void *data, + ngx_uint_t state) +{ +} + + +typedef ngx_rtmp_relay_ctx_t * (* ngx_rtmp_relay_create_ctx_pt) + (ngx_rtmp_session_t *s, ngx_str_t *name, ngx_rtmp_relay_target_t *target); + + +static ngx_int_t +ngx_rtmp_relay_copy_str(ngx_pool_t *pool, ngx_str_t *dst, ngx_str_t *src) +{ + if (src->len == 0) { + return NGX_OK; + } + dst->len = src->len; + dst->data = ngx_palloc(pool, src->len); + if (dst->data == NULL) { + return NGX_ERROR; + } + ngx_memcpy(dst->data, src->data, src->len); + return NGX_OK; +} + + +static ngx_rtmp_relay_ctx_t * +ngx_rtmp_relay_create_connection(ngx_rtmp_conf_ctx_t *cctx, ngx_str_t *name, + ngx_rtmp_relay_target_t *target) +{ + ngx_rtmp_relay_app_conf_t *racf; + ngx_rtmp_relay_ctx_t *rctx; + ngx_rtmp_addr_conf_t *addr_conf; + ngx_rtmp_conf_ctx_t *addr_ctx; + ngx_rtmp_session_t *rs; + ngx_peer_connection_t *pc; + ngx_connection_t *c; + ngx_addr_t *addr; + ngx_pool_t *pool; + size_t len; + ngx_int_t rc; + ngx_str_t v, *uri; + u_char *first, *last, *p; + u_char buf[NGX_SOCKADDR_STRLEN]; + + racf = ngx_rtmp_get_module_app_conf(cctx, ngx_rtmp_relay_module); + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, racf->log, 0, + "relay: create remote context"); + + pool = NULL; + pool = ngx_create_pool(4096, racf->log); + if (pool == NULL) { + return NULL; + } + + rctx = ngx_pcalloc(pool, sizeof(ngx_rtmp_relay_ctx_t)); + if (rctx == NULL) { + goto clear; + } + + if (name && ngx_rtmp_relay_copy_str(pool, &rctx->name, name) != NGX_OK) { + goto clear; + } + + if (ngx_rtmp_relay_copy_str(pool, &rctx->url, &target->url.url) != NGX_OK) { + goto clear; + } + + rctx->tag = target->tag; + rctx->data = target->data; + +#define NGX_RTMP_RELAY_STR_COPY(to, from) \ + if (ngx_rtmp_relay_copy_str(pool, &rctx->to, &target->from) != NGX_OK) { \ + goto clear; \ + } + + NGX_RTMP_RELAY_STR_COPY(app, app); + NGX_RTMP_RELAY_STR_COPY(tc_url, tc_url); + NGX_RTMP_RELAY_STR_COPY(page_url, page_url); + NGX_RTMP_RELAY_STR_COPY(swf_url, swf_url); + NGX_RTMP_RELAY_STR_COPY(flash_ver, flash_ver); + NGX_RTMP_RELAY_STR_COPY(play_path, play_path); + + rctx->live = target->live; + rctx->start = target->start; + rctx->stop = target->stop; + +#undef NGX_RTMP_RELAY_STR_COPY + + if (rctx->app.len == 0 || rctx->play_path.len == 0) { + /* parse uri */ + uri = &target->url.uri; + first = uri->data; + last = uri->data + uri->len; + if (first != last && *first == '/') { + ++first; + } + + if (first != last) { + + /* deduce app */ + p = ngx_strlchr(first, last, '/'); + if (p == NULL) { + p = last; + } + + if (rctx->app.len == 0 && first != p) { + v.data = first; + v.len = p - first; + if (ngx_rtmp_relay_copy_str(pool, &rctx->app, &v) != NGX_OK) { + goto clear; + } + } + + /* deduce play_path */ + if (p != last) { + ++p; + } + + if (rctx->play_path.len == 0 && p != last) { + v.data = p; + v.len = last - p; + if (ngx_rtmp_relay_copy_str(pool, &rctx->play_path, &v) + != NGX_OK) + { + goto clear; + } + } + } + } + + pc = ngx_pcalloc(pool, sizeof(ngx_peer_connection_t)); + if (pc == NULL) { + goto clear; + } + + if (target->url.naddrs == 0) { + ngx_log_error(NGX_LOG_ERR, racf->log, 0, + "relay: no address"); + goto clear; + } + + /* get address */ + addr = &target->url.addrs[target->counter % target->url.naddrs]; + target->counter++; + + /* copy log to keep shared log unchanged */ + rctx->log = *racf->log; + pc->log = &rctx->log; + pc->get = ngx_rtmp_relay_get_peer; + pc->free = ngx_rtmp_relay_free_peer; + + pc->name = ngx_palloc(pool, sizeof(ngx_str_t) + addr->name.len); + if (pc->name == NULL) { + goto clear; + } + + pc->name->len = addr->name.len; + pc->name->data = (u_char *) pc->name + sizeof(ngx_str_t); + ngx_memcpy(pc->name->data, addr->name.data, addr->name.len); + + pc->socklen = addr->socklen; + pc->sockaddr = (struct sockaddr *)ngx_palloc(pool, pc->socklen); + if (pc->sockaddr == NULL) { + goto clear; + } + ngx_memcpy(pc->sockaddr, addr->sockaddr, pc->socklen); + + rc = ngx_event_connect_peer(pc); + if (rc != NGX_OK && rc != NGX_AGAIN ) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, racf->log, 0, + "relay: connection failed"); + goto clear; + } + c = pc->connection; + c->pool = pool; + + ngx_str_set(&c->addr_text, "ngx-relay"); + + addr_conf = ngx_pcalloc(pool, sizeof(ngx_rtmp_addr_conf_t)); + if (addr_conf == NULL) { + goto clear; + } + +#if (NGX_HAVE_UNIX_DOMAIN) + if (addr->sockaddr->sa_family == AF_UNIX) { + addr_conf->addr_text.len = target->url.host.len; + addr_conf->addr_text.data = ngx_pcalloc(pool, + addr_conf->addr_text.len); + if (addr_conf->addr_text.data == NULL) { + ngx_log_error(NGX_LOG_ERR, racf->log, 0, + "relay: allocation for unix address failed"); + goto clear; + } + + ngx_memcpy(addr_conf->addr_text.data, target->url.host.data, + addr_conf->addr_text.len); + } else +#endif + { + len = ngx_sock_ntop(pc->sockaddr, +#if (nginx_version >= 1005003) + pc->socklen, +#endif + buf, NGX_SOCKADDR_STRLEN, 1); + + addr_conf->addr_text.data = ngx_pcalloc(pool, len); + if (addr_conf->addr_text.data == NULL) { + ngx_log_error(NGX_LOG_ERR, racf->log, 0, + "relay: allocation for address failed"); + goto clear; + } + + addr_conf->addr_text.len = len; + ngx_memcpy(addr_conf->addr_text.data, buf, len); + } + + addr_conf->default_server = ngx_pcalloc(pool, + sizeof(ngx_rtmp_core_srv_conf_t)); + if (addr_conf->default_server == NULL) { + goto clear; + } + + addr_ctx = ngx_pcalloc(pool, sizeof(ngx_rtmp_conf_ctx_t)); + if (addr_ctx == NULL) { + goto clear; + } + + addr_conf->default_server->ctx = addr_ctx; + addr_ctx->main_conf = cctx->main_conf; + addr_ctx->srv_conf = cctx->srv_conf; + + rs = ngx_rtmp_init_session(c, addr_conf); + if (rs == NULL) { + /* no need to destroy pool */ + return NULL; + } + rs->app_conf = cctx->app_conf; + rs->relay = 1; + rctx->session = rs; + ngx_rtmp_set_ctx(rs, rctx, ngx_rtmp_relay_module); + ngx_str_set(&rs->flashver, "ngx-local-relay"); + +#if (NGX_STAT_STUB) + (void) ngx_atomic_fetch_add(ngx_stat_active, 1); +#endif + + ngx_rtmp_client_handshake(rs, 1); + return rctx; + +clear: + if (pool) { + ngx_destroy_pool(pool); + } + return NULL; +} + + +static ngx_rtmp_relay_ctx_t * +ngx_rtmp_relay_create_remote_ctx(ngx_rtmp_session_t *s, ngx_str_t *name, + ngx_rtmp_relay_target_t *target) +{ + ngx_str_t url; + ngx_url_t save; + ngx_rtmp_conf_ctx_t cctx; + ngx_rtmp_relay_ctx_t *rctx; + + cctx.app_conf = s->app_conf; + cctx.srv_conf = s->srv_conf; + cctx.main_conf = s->main_conf; + + rctx = NULL; + save = target->url; + ngx_memzero(&url, sizeof(ngx_str_t)); + + if(ngx_strlchr(target->url.url.data, + target->url.url.data + target->url.url.len, '$')) + { + ngx_memzero(&url, sizeof(ngx_str_t)); + + if(ngx_rtmp_eval(s, &target->url.url, ngx_rtmp_relay_eval, + &url, s->connection->log) == NGX_OK) + { + target->url.default_port = 1935; + target->url.uri_part = 1; + target->url.url = url; + + target->url.addrs = NULL; + target->url.naddrs = 0; + + if(ngx_parse_url(s->connection->pool, &target->url) != NGX_OK) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "relay: invalid url='%V'", &target->url.url); + goto error; + } + } else { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "relay: failed to eval url='%V'", &target->url.url); + goto error; + } + } + + rctx = ngx_rtmp_relay_create_connection(&cctx, name, target); + if (rctx) { + rctx->server_name.data = s->host_start; + rctx->server_name.len = s->host_end - s->host_start; + } + +error: + target->url = save; + if (url.len) { + ngx_free(url.data); + ngx_memzero(&url, sizeof(ngx_str_t)); + } + + return rctx; +} + + +static ngx_rtmp_relay_ctx_t * +ngx_rtmp_relay_create_local_ctx(ngx_rtmp_session_t *s, ngx_str_t *name, + ngx_rtmp_relay_target_t *target) +{ + ngx_rtmp_relay_ctx_t *ctx; + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "relay: create local context"); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL) { + ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_relay_ctx_t)); + if (ctx == NULL) { + return NULL; + } + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_relay_module); + } + ctx->session = s; + + ctx->push_evt.data = s; + ctx->push_evt.log = s->connection->log; + ctx->push_evt.handler = ngx_rtmp_relay_push_reconnect; + + if (ctx->publish) { + return NULL; + } + + if (ngx_rtmp_relay_copy_str(s->connection->pool, &ctx->name, name) + != NGX_OK) + { + return NULL; + } + + return ctx; +} + + +static ngx_int_t +ngx_rtmp_relay_create(ngx_rtmp_session_t *s, ngx_str_t *name, + ngx_rtmp_relay_target_t *target, + ngx_rtmp_relay_create_ctx_pt create_publish_ctx, + ngx_rtmp_relay_create_ctx_pt create_play_ctx) +{ + ngx_rtmp_relay_app_conf_t *racf; + ngx_rtmp_relay_ctx_t *publish_ctx, *play_ctx, **cctx; + ngx_uint_t hash; + + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module); + if (racf == NULL) { + return NGX_ERROR; + } + + play_ctx = create_play_ctx(s, name, target); + if (play_ctx == NULL) { + return NGX_ERROR; + } + + hash = ngx_hash_key(name->data, name->len); + cctx = &racf->ctx[hash % racf->nbuckets]; + for (; *cctx; cctx = &(*cctx)->next) { + if ((*cctx)->name.len == name->len + && !ngx_memcmp(name->data, (*cctx)->name.data, + name->len)) + { + break; + } + } + + if (*cctx) { + play_ctx->publish = (*cctx)->publish; + play_ctx->next = (*cctx)->play; + (*cctx)->play = play_ctx; + return NGX_OK; + } + + publish_ctx = create_publish_ctx(s, name, target); + if (publish_ctx == NULL) { + ngx_rtmp_finalize_session(play_ctx->session); + return NGX_ERROR; + } + + publish_ctx->publish = publish_ctx; + publish_ctx->play = play_ctx; + play_ctx->publish = publish_ctx; + *cctx = publish_ctx; + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_relay_pull(ngx_rtmp_session_t *s, ngx_str_t *name, + ngx_rtmp_relay_target_t *target) +{ + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "relay: create pull name='%V' app='%V' playpath='%V' " + "url='%V' args='%V'", + name, &target->app, &target->play_path, + &target->url.url, &s->args); + + return ngx_rtmp_relay_create(s, name, target, + ngx_rtmp_relay_create_remote_ctx, + ngx_rtmp_relay_create_local_ctx); +} + + +ngx_int_t +ngx_rtmp_relay_push(ngx_rtmp_session_t *s, ngx_str_t *name, + ngx_rtmp_relay_target_t *target) +{ + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "relay: create push name='%V' app='%V' playpath='%V' " + "url='%V' args='%V'", + name, &target->app, &target->play_path, + &target->url.url, &s->args); + + return ngx_rtmp_relay_create(s, name, target, + ngx_rtmp_relay_create_local_ctx, + ngx_rtmp_relay_create_remote_ctx); +} + + +static ngx_int_t +ngx_rtmp_relay_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) +{ + ngx_rtmp_relay_app_conf_t *racf; + ngx_rtmp_relay_target_t *target, **t; + ngx_str_t name; + size_t n; + ngx_rtmp_relay_ctx_t *ctx; + + if (s->auto_pushed) { + goto next; + } + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx && s->relay) { + goto next; + } + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module); + if (racf == NULL || racf->pushes.nelts == 0) { + goto next; + } + + name.len = ngx_strlen(v->name); + name.data = v->name; + + t = racf->pushes.elts; + for (n = 0; n < racf->pushes.nelts; ++n, ++t) { + target = *t; + + if (target->name.len && (name.len != target->name.len || + ngx_memcmp(name.data, target->name.data, name.len))) + { + continue; + } + + if (ngx_rtmp_relay_push(s, &name, target) == NGX_OK) { + continue; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "relay: push failed name='%V' app='%V' " + "playpath='%V' url='%V' args='%V'", + &name, &target->app, &target->play_path, + &target->url.url, &s->args); + + /* ctx == NULL && s->relay == 0, BOOM! */ + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx && !ctx->push_evt.timer_set) { + ngx_add_timer(&ctx->push_evt, racf->push_reconnect); + } + } + +next: + return next_publish(s, v); +} + + +static ngx_int_t +ngx_rtmp_relay_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_relay_app_conf_t *racf; + ngx_rtmp_relay_target_t *target, **t; + ngx_str_t name; + size_t n; + ngx_rtmp_relay_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx && s->relay) { + goto next; + } + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module); + if (racf == NULL || racf->pulls.nelts == 0) { + goto next; + } + + name.len = ngx_strlen(v->name); + name.data = v->name; + + t = racf->pulls.elts; + for (n = 0; n < racf->pulls.nelts; ++n, ++t) { + target = *t; + + if (target->name.len && (name.len != target->name.len || + ngx_memcmp(name.data, target->name.data, name.len))) + { + continue; + } + + if (ngx_rtmp_relay_pull(s, &name, target) == NGX_OK) { + continue; + } + + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "relay: pull failed name='%V' app='%V' " + "playpath='%V' url='%V' args='%V'", + &name, &target->app, &target->play_path, + &target->url.url, &s->args); + } + +next: + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_relay_play_local(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_t v; + ngx_rtmp_relay_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_memzero(&v, sizeof(ngx_rtmp_play_t)); + v.silent = 1; + *(ngx_cpymem(v.name, ctx->name.data, + ngx_min(sizeof(v.name) - 1, ctx->name.len))) = 0; + + return ngx_rtmp_play(s, &v); +} + + +static ngx_int_t +ngx_rtmp_relay_publish_local(ngx_rtmp_session_t *s) +{ + ngx_rtmp_publish_t v; + ngx_rtmp_relay_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL) { + return NGX_ERROR; + } + + ngx_memzero(&v, sizeof(ngx_rtmp_publish_t)); + v.silent = 1; + *(ngx_cpymem(v.name, ctx->name.data, + ngx_min(sizeof(v.name) - 1, ctx->name.len))) = 0; + + return ngx_rtmp_publish(s, &v); +} + + +static ngx_int_t +ngx_rtmp_relay_send_connect(ngx_rtmp_session_t *s) +{ + static double trans = NGX_RTMP_RELAY_CONNECT_TRANS; + static double acodecs = 3575; + static double vcodecs = 252; + + static ngx_rtmp_amf_elt_t out_cmd[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("app"), + NULL, 0 }, /* <-- fill */ + + { NGX_RTMP_AMF_STRING, + ngx_string("tcUrl"), + NULL, 0 }, /* <-- fill */ + + { NGX_RTMP_AMF_STRING, + ngx_string("pageUrl"), + NULL, 0 }, /* <-- fill */ + + { NGX_RTMP_AMF_STRING, + ngx_string("swfUrl"), + NULL, 0 }, /* <-- fill */ + + { NGX_RTMP_AMF_STRING, + ngx_string("flashVer"), + NULL, 0 }, /* <-- fill */ + + { NGX_RTMP_AMF_STRING, + ngx_string("serverName"), + NULL, 0 }, /* <-- fill */ + + { NGX_RTMP_AMF_NUMBER, + ngx_string("audioCodecs"), + &acodecs, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_string("videoCodecs"), + &vcodecs, 0 } + }; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "connect", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &trans, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + out_cmd, sizeof(out_cmd) } + }; + + ngx_rtmp_core_app_conf_t *cacf; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_rtmp_relay_ctx_t *ctx; + ngx_rtmp_header_t h; + size_t len, url_len; + u_char *p, *url_end; + + + cacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_core_module); + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (cacf == NULL || ctx == NULL) { + return NGX_ERROR; + } + + /* app */ + if (ctx->app.len) { + out_cmd[0].data = ctx->app.data; + out_cmd[0].len = ctx->app.len; + } else { + out_cmd[0].data = cacf->name.data; + out_cmd[0].len = cacf->name.len; + } + + /* tcUrl */ + if (ctx->tc_url.len) { + out_cmd[1].data = ctx->tc_url.data; + out_cmd[1].len = ctx->tc_url.len; + } else { + len = sizeof("rtmp://") - 1 + ctx->url.len + + sizeof("/") - 1 + ctx->app.len; + p = ngx_palloc(s->connection->pool, len); + if (p == NULL) { + return NGX_ERROR; + } + out_cmd[1].data = p; + p = ngx_cpymem(p, "rtmp://", sizeof("rtmp://") - 1); + + url_len = ctx->url.len; + url_end = ngx_strlchr(ctx->url.data, ctx->url.data + ctx->url.len, '/'); + if (url_end) { + url_len = (size_t) (url_end - ctx->url.data); + } + + p = ngx_cpymem(p, ctx->url.data, url_len); + *p++ = '/'; + p = ngx_cpymem(p, ctx->app.data, ctx->app.len); + out_cmd[1].len = p - (u_char *)out_cmd[1].data; + } + + /* pageUrl */ + out_cmd[2].data = ctx->page_url.data; + out_cmd[2].len = ctx->page_url.len; + + /* swfUrl */ + out_cmd[3].data = ctx->swf_url.data; + out_cmd[3].len = ctx->swf_url.len; + + /* flashVer */ + if (ctx->flash_ver.len) { + out_cmd[4].data = ctx->flash_ver.data; + out_cmd[4].len = ctx->flash_ver.len; + } else { + out_cmd[4].data = NGX_RTMP_RELAY_FLASHVER; + out_cmd[4].len = sizeof(NGX_RTMP_RELAY_FLASHVER) - 1; + } + + /* used in ngx_rtmp_set_virtual_server when auto_pushed */ + out_cmd[5].data = ctx->server_name.data; + out_cmd[5].len = ctx->server_name.len; + + ngx_memzero(&h, sizeof(h)); + h.csid = NGX_RTMP_RELAY_CSID_AMF_INI; + h.type = NGX_RTMP_MSG_AMF_CMD; + + return ngx_rtmp_send_chunk_size(s, cscf->chunk_size) != NGX_OK + || ngx_rtmp_send_ack_size(s, cscf->ack_window) != NGX_OK + || ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK + ? NGX_ERROR + : NGX_OK; +} + + +#if 0 +static ngx_int_t +ngx_rtmp_relay_send_release_stream(ngx_rtmp_session_t *s) +{ + static double trans = NGX_RTMP_RELAY_RELEASE_STREAM_TRANS; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "releaseStream", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("stream"), + NULL, 0 } + }; + + ngx_rtmp_header_t h; + + ngx_rtmp_core_app_conf_t *cacf; + ngx_rtmp_relay_ctx_t *ctx; + + cacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_core_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (cacf == NULL || ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->name.len) { + out_elts[3].data = ctx->name.data; + out_elts[3].len = ctx->name.len; + } + + ngx_memzero(&h, sizeof(h)); + h.csid = NGX_RTMP_RELAY_CSID_AMF_INI; + h.type = NGX_RTMP_MSG_AMF_CMD; + + return ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +static ngx_int_t +ngx_rtmp_relay_send_fcpublish(ngx_rtmp_session_t *s) +{ + static double trans = NGX_RTMP_RELAY_FCPUBLISH_STREAM_TRANS; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "FCPublish", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("stream"), + NULL, 0 } + }; + + ngx_rtmp_header_t h; + + ngx_rtmp_core_app_conf_t *cacf; + ngx_rtmp_relay_ctx_t *ctx; + + cacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_core_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (cacf == NULL || ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->name.len) { + out_elts[3].data = ctx->name.data; + out_elts[3].len = ctx->name.len; + } + + ngx_memzero(&h, sizeof(h)); + h.csid = NGX_RTMP_RELAY_CSID_AMF_INI; + h.type = NGX_RTMP_MSG_AMF_CMD; + + return ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} +#endif + + +static ngx_int_t +ngx_rtmp_relay_send_create_stream(ngx_rtmp_session_t *s) +{ + static double trans = NGX_RTMP_RELAY_CREATE_STREAM_TRANS; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "createStream", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 } + }; + + ngx_rtmp_header_t h; + + ngx_memzero(&h, sizeof(h)); + h.csid = NGX_RTMP_RELAY_CSID_AMF_INI; + h.type = NGX_RTMP_MSG_AMF_CMD; + +#if 0 + return ngx_rtmp_relay_send_release_stream(s) != NGX_OK + || ngx_rtmp_relay_send_fcpublish(s) != NGX_OK + || ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK + ? NGX_ERROR + : NGX_OK; +#endif + + return ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +static ngx_int_t +ngx_rtmp_relay_send_publish(ngx_rtmp_session_t *s) +{ + static double trans; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "publish", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + NULL, 0 }, /* <- to fill */ + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "live", 0 } + }; + + ngx_rtmp_header_t h; + ngx_rtmp_relay_ctx_t *ctx; + + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->play_path.len) { + out_elts[3].data = ctx->play_path.data; + out_elts[3].len = ctx->play_path.len; + } else { + out_elts[3].data = ctx->name.data; + out_elts[3].len = ctx->name.len; + } + + ngx_memzero(&h, sizeof(h)); + h.csid = NGX_RTMP_RELAY_CSID_AMF; + h.msid = NGX_RTMP_RELAY_MSID; + h.type = NGX_RTMP_MSG_AMF_CMD; + + return ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +static ngx_int_t +ngx_rtmp_relay_send_play(ngx_rtmp_session_t *s) +{ + static double trans; + static double start, duration; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "play", 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + NULL, 0 }, /* <- fill */ + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &start, 0 }, + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &duration, 0 }, + }; + + ngx_rtmp_header_t h; + ngx_rtmp_relay_ctx_t *ctx; + ngx_rtmp_relay_app_conf_t *racf; + + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (racf == NULL || ctx == NULL) { + return NGX_ERROR; + } + + if (ctx->play_path.len) { + out_elts[3].data = ctx->play_path.data; + out_elts[3].len = ctx->play_path.len; + } else { + out_elts[3].data = ctx->name.data; + out_elts[3].len = ctx->name.len; + } + + if (ctx->live) { + start = -1000; + duration = -1000; + } else { + start = (ctx->start ? ctx->start : -2000); + duration = (ctx->stop ? ctx->stop - ctx->start : -1000); + } + + ngx_memzero(&h, sizeof(h)); + h.csid = NGX_RTMP_RELAY_CSID_AMF; + h.msid = NGX_RTMP_RELAY_MSID; + h.type = NGX_RTMP_MSG_AMF_CMD; + + return ngx_rtmp_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])) != NGX_OK + || ngx_rtmp_send_set_buflen(s, NGX_RTMP_RELAY_MSID, + racf->buflen) != NGX_OK + ? NGX_ERROR + : NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_relay_on_result(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_relay_ctx_t *ctx; + static struct { + double trans; + u_char level[32]; + u_char code[128]; + u_char desc[1024]; + } v; + + static ngx_rtmp_amf_elt_t in_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + &v.level, sizeof(v.level) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("code"), + &v.code, sizeof(v.code) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("description"), + &v.desc, sizeof(v.desc) }, + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_inf, sizeof(in_inf) }, + }; + + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL || !s->relay) { + return NGX_OK; + } + + ngx_memzero(&v, sizeof(v)); + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "relay: _result: level='%s' code='%s' description='%s'", + v.level, v.code, v.desc); + + switch ((ngx_int_t)v.trans) { + case NGX_RTMP_RELAY_CONNECT_TRANS: + return ngx_rtmp_relay_send_create_stream(s); + + case NGX_RTMP_RELAY_CREATE_STREAM_TRANS: + if (ctx->publish != ctx && !s->static_relay) { + if (ngx_rtmp_relay_send_publish(s) != NGX_OK) { + return NGX_ERROR; + } + return ngx_rtmp_relay_play_local(s); + + } else { + if (ngx_rtmp_relay_send_play(s) != NGX_OK) { + return NGX_ERROR; + } + return ngx_rtmp_relay_publish_local(s); + } + + default: + return NGX_OK; + } +} + + +static ngx_int_t +ngx_rtmp_relay_on_error(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_relay_ctx_t *ctx; + static struct { + double trans; + u_char level[32]; + u_char code[128]; + u_char desc[1024]; + } v; + + static ngx_rtmp_amf_elt_t in_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + &v.level, sizeof(v.level) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("code"), + &v.code, sizeof(v.code) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("description"), + &v.desc, sizeof(v.desc) }, + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_inf, sizeof(in_inf) }, + }; + + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL || !s->relay) { + return NGX_OK; + } + + ngx_memzero(&v, sizeof(v)); + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + return NGX_ERROR; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "relay: _error: level='%s' code='%s' description='%s'", + v.level, v.code, v.desc); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_relay_on_status(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_relay_ctx_t *ctx; + static struct { + double trans; + u_char level[32]; + u_char code[128]; + u_char desc[1024]; + } v; + + static ngx_rtmp_amf_elt_t in_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + &v.level, sizeof(v.level) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("code"), + &v.code, sizeof(v.code) }, + + { NGX_RTMP_AMF_STRING, + ngx_string("description"), + &v.desc, sizeof(v.desc) }, + }; + + static ngx_rtmp_amf_elt_t in_elts[] = { + + { NGX_RTMP_AMF_NUMBER, + ngx_null_string, + &v.trans, 0 }, + + { NGX_RTMP_AMF_NULL, + ngx_null_string, + NULL, 0 }, + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_inf, sizeof(in_inf) }, + }; + + static ngx_rtmp_amf_elt_t in_elts_meta[] = { + + { NGX_RTMP_AMF_OBJECT, + ngx_null_string, + in_inf, sizeof(in_inf) }, + }; + + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL || !s->relay) { + return NGX_OK; + } + + ngx_memzero(&v, sizeof(v)); + if (h->type == NGX_RTMP_MSG_AMF_META) { + if (ngx_rtmp_receive_amf(s, in, in_elts_meta, + sizeof(in_elts_meta) / sizeof(in_elts_meta[0]))) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "relay: error receiving meta"); + + return NGX_ERROR; + } + } else { + if (ngx_rtmp_receive_amf(s, in, in_elts, + sizeof(in_elts) / sizeof(in_elts[0]))) + { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "relay: error receiving status"); + + return NGX_ERROR; + } + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "relay: onStatus: level='%s' code='%s' description='%s'", + v.level, v.code, v.desc); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_relay_handshake_done(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_chain_t *in) +{ + ngx_rtmp_relay_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL || !s->relay) { + return NGX_OK; + } + + return ngx_rtmp_relay_send_connect(s); +} + + +static void +ngx_rtmp_relay_close(ngx_rtmp_session_t *s) +{ + ngx_rtmp_relay_app_conf_t *racf; + ngx_rtmp_relay_ctx_t *ctx, **cctx, **next; + ngx_uint_t hash; + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_relay_module); + if (ctx == NULL) { + return; + } + + if (s->static_relay) { + ngx_add_timer(ctx->static_evt, racf->pull_reconnect); + } + + if (ctx->publish == NULL) { + return; + } + + /* play end disconnect? */ + if (ctx->publish != ctx) { + for (cctx = &ctx->publish->play; *cctx; cctx = &(*cctx)->next) { + if (*cctx == ctx) { + *cctx = ctx->next; + break; + } + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ctx->session->connection->log, 0, + "relay: play disconnect app='%V' name='%V'", + &ctx->app, &ctx->name); + + /* push reconnect */ + if (s->relay && ctx->tag == &ngx_rtmp_relay_module && + !ctx->publish->push_evt.timer_set) + { + ngx_add_timer(&ctx->publish->push_evt, racf->push_reconnect); + } + +#ifdef NGX_DEBUG + { + ngx_uint_t n = 0; + for (cctx = &ctx->publish->play; *cctx; cctx = &(*cctx)->next, ++n); + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, ctx->session->connection->log, 0, + "relay: play left after disconnect app='%V' name='%V': %ui", + &ctx->app, &ctx->name, n); + } +#endif + + if (ctx->publish->play == NULL && ctx->publish->session->relay) { + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, + ctx->publish->session->connection->log, 0, + "relay: publish disconnect empty app='%V' name='%V'", + &ctx->app, &ctx->name); + ngx_rtmp_finalize_session(ctx->publish->session); + } + + ctx->publish = NULL; + + return; + } + + /* publish end disconnect */ + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, ctx->session->connection->log, 0, + "relay: publish disconnect app='%V' name='%V'", + &ctx->app, &ctx->name); + + if (ctx->push_evt.timer_set) { + ngx_del_timer(&ctx->push_evt); + } + + for (cctx = &ctx->play; *cctx; /* cctx = &(*cctx)->next */) { + (*cctx)->publish = NULL; + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, (*cctx)->session->connection->log, + 0, "relay: play disconnect orphan app='%V' name='%V'", + &(*cctx)->app, &(*cctx)->name); + + next = &(*cctx)->next; + + ngx_rtmp_finalize_session((*cctx)->session); + + cctx = next; + } + ctx->publish = NULL; + + hash = ngx_hash_key(ctx->name.data, ctx->name.len); + cctx = &racf->ctx[hash % racf->nbuckets]; + for (; *cctx && *cctx != ctx; cctx = &(*cctx)->next); + if (*cctx) { + *cctx = ctx->next; + } +} + + +static ngx_int_t +ngx_rtmp_relay_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_relay_app_conf_t *racf; + + racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_relay_module); + if (racf && !racf->session_relay) { + ngx_rtmp_relay_close(s); + } + + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_relay_delete_stream(ngx_rtmp_session_t *s, ngx_rtmp_delete_stream_t *v) +{ + ngx_rtmp_relay_close(s); + + return next_delete_stream(s, v); +} + + +static char * +ngx_rtmp_relay_push_pull(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_str_t *value, v, n; + ngx_rtmp_relay_app_conf_t *racf; + ngx_rtmp_relay_target_t *target, **t; + ngx_url_t *u; + ngx_uint_t i; + ngx_int_t is_pull, is_static; + ngx_event_t **ee, *e; + ngx_rtmp_relay_static_t *rs; + u_char *p; + + value = cf->args->elts; + + racf = ngx_rtmp_conf_get_module_app_conf(cf, ngx_rtmp_relay_module); + + is_pull = (value[0].data[3] == 'l'); + is_static = 0; + + target = ngx_pcalloc(cf->pool, sizeof(*target)); + if (target == NULL) { + return NGX_CONF_ERROR; + } + + target->tag = &ngx_rtmp_relay_module; + target->data = target; + + u = &target->url; + u->default_port = 1935; + u->uri_part = 1; + u->url = value[1]; + + if (ngx_strncasecmp(u->url.data, (u_char *) "rtmp://", 7) == 0) { + u->url.data += 7; + u->url.len -= 7; + } + + 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; + } + + value += 2; + for (i = 2; i < cf->args->nelts; ++i, ++value) { + p = ngx_strlchr(value->data, value->data + value->len, '='); + + if (p == NULL) { + n = *value; + ngx_str_set(&v, "1"); + + } else { + n.data = value->data; + n.len = p - value->data; + + v.data = p + 1; + v.len = value->data + value->len - p - 1; + } + +#define NGX_RTMP_RELAY_STR_PAR(name, var) \ + if (n.len == sizeof(name) - 1 \ + && ngx_strncasecmp(n.data, (u_char *) name, n.len) == 0) \ + { \ + target->var = v; \ + continue; \ + } + +#define NGX_RTMP_RELAY_NUM_PAR(name, var) \ + if (n.len == sizeof(name) - 1 \ + && ngx_strncasecmp(n.data, (u_char *) name, n.len) == 0) \ + { \ + target->var = ngx_atoi(v.data, v.len); \ + continue; \ + } + + NGX_RTMP_RELAY_STR_PAR("app", app); + NGX_RTMP_RELAY_STR_PAR("name", name); + NGX_RTMP_RELAY_STR_PAR("tcUrl", tc_url); + NGX_RTMP_RELAY_STR_PAR("pageUrl", page_url); + NGX_RTMP_RELAY_STR_PAR("swfUrl", swf_url); + NGX_RTMP_RELAY_STR_PAR("flashVer", flash_ver); + NGX_RTMP_RELAY_STR_PAR("playPath", play_path); + NGX_RTMP_RELAY_NUM_PAR("live", live); + NGX_RTMP_RELAY_NUM_PAR("start", start); + NGX_RTMP_RELAY_NUM_PAR("stop", stop); + +#undef NGX_RTMP_RELAY_STR_PAR +#undef NGX_RTMP_RELAY_NUM_PAR + + if (n.len == sizeof("static") - 1 && + ngx_strncasecmp(n.data, (u_char *) "static", n.len) == 0 && + ngx_atoi(v.data, v.len)) + { + is_static = 1; + continue; + } + + return "unsuppored parameter"; + } + + if (is_static) { + + if (!is_pull) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "static push is not allowed"); + return NGX_CONF_ERROR; + } + + if (ngx_strlchr(target->url.url.data, + target->url.url.data + target->url.url.len, '$')) + { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "variable is not allowed"); + return NGX_CONF_ERROR; + } + + if (target->name.len == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "stream name missing in static pull " + "declaration"); + return NGX_CONF_ERROR; + } + + ee = ngx_array_push(&racf->static_events); + if (ee == NULL) { + return NGX_CONF_ERROR; + } + + e = ngx_pcalloc(cf->pool, sizeof(ngx_event_t)); + if (e == NULL) { + return NGX_CONF_ERROR; + } + + *ee = e; + + rs = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_relay_static_t)); + if (rs == NULL) { + return NGX_CONF_ERROR; + } + + rs->target = target; + + e->data = rs; + e->log = &cf->cycle->new_log; + e->handler = ngx_rtmp_relay_static_pull_reconnect; + + t = ngx_array_push(&racf->static_pulls); + + } else if (is_pull) { + t = ngx_array_push(&racf->pulls); + + } else { + t = ngx_array_push(&racf->pushes); + } + + if (t == NULL) { + return NGX_CONF_ERROR; + } + + *t = target; + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_rtmp_relay_init_process(ngx_cycle_t *cycle) +{ +#if !(NGX_WIN32) + ngx_rtmp_core_main_conf_t *cmcf = ngx_rtmp_core_main_conf; + ngx_rtmp_core_srv_conf_t **pcscf, *cscf; + ngx_rtmp_core_app_conf_t **pcacf, *cacf; + ngx_rtmp_relay_app_conf_t *racf; + ngx_uint_t n, m, k; + ngx_rtmp_relay_static_t *rs; + ngx_event_t **pevent, *event; + + if (cmcf == NULL || cmcf->servers.nelts == 0) { + return NGX_OK; + } + + /* only first worker does static pulling */ + + if (ngx_process_slot) { + return NGX_OK; + } + + pcscf = cmcf->servers.elts; + for (n = 0; n < cmcf->servers.nelts; ++n, ++pcscf) { + + cscf = *pcscf; + pcacf = cscf->applications.elts; + + for (m = 0; m < cscf->applications.nelts; ++m, ++pcacf) { + + cacf = *pcacf; + racf = cacf->app_conf[ngx_rtmp_relay_module.ctx_index]; + pevent = racf->static_events.elts; + + for (k = 0; k < racf->static_events.nelts; ++k, ++pevent) { + event = *pevent; + + rs = event->data; + rs->cctx = *cscf->ctx; + rs->cctx.app_conf = cacf->app_conf; + + ngx_post_event(event, &ngx_rtmp_init_queue); + } + } + } +#endif + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_relay_postconfiguration(ngx_conf_t *cf) +{ + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_handler_pt *h; + ngx_rtmp_amf_handler_t *ch; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + + h = ngx_array_push(&cmcf->events[NGX_RTMP_HANDSHAKE_DONE]); + *h = ngx_rtmp_relay_handshake_done; + + + next_publish = ngx_rtmp_publish; + ngx_rtmp_publish = ngx_rtmp_relay_publish; + + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_relay_play; + + next_delete_stream = ngx_rtmp_delete_stream; + ngx_rtmp_delete_stream = ngx_rtmp_relay_delete_stream; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_relay_close_stream; + + + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "_result"); + ch->handler = ngx_rtmp_relay_on_result; + + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "_error"); + ch->handler = ngx_rtmp_relay_on_error; + + ch = ngx_array_push(&cmcf->amf); + ngx_str_set(&ch->name, "onStatus"); + ch->handler = ngx_rtmp_relay_on_status; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_relay_module.h b/ngx_http_flv_module/ngx_rtmp_relay_module.h new file mode 100644 index 0000000..c7c5065 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_relay_module.h @@ -0,0 +1,88 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_RELAY_H_INCLUDED_ +#define _NGX_RTMP_RELAY_H_INCLUDED_ + + +#include +#include +#include "ngx_rtmp.h" + + +typedef struct { + ngx_url_t url; + ngx_str_t app; + ngx_str_t name; + ngx_str_t tc_url; + ngx_str_t page_url; + ngx_str_t swf_url; + ngx_str_t flash_ver; + ngx_str_t play_path; + ngx_int_t live; + ngx_int_t start; + ngx_int_t stop; + + void *tag; /* usually module reference */ + void *data; /* module-specific data */ + ngx_uint_t counter; /* mutable connection counter */ +} ngx_rtmp_relay_target_t; + + +typedef struct ngx_rtmp_relay_ctx_s ngx_rtmp_relay_ctx_t; + +struct ngx_rtmp_relay_ctx_s { + ngx_str_t server_name; + ngx_str_t name; + ngx_str_t url; + ngx_log_t log; + ngx_rtmp_session_t *session; + ngx_rtmp_relay_ctx_t *publish; + ngx_rtmp_relay_ctx_t *play; + ngx_rtmp_relay_ctx_t *next; + + ngx_str_t app; + ngx_str_t tc_url; + ngx_str_t page_url; + ngx_str_t swf_url; + ngx_str_t flash_ver; + ngx_str_t play_path; + ngx_int_t live; + ngx_int_t start; + ngx_int_t stop; + + ngx_event_t push_evt; + ngx_event_t *static_evt; + void *tag; + void *data; +}; + + +typedef struct { + ngx_array_t pulls; /* ngx_rtmp_relay_target_t * */ + ngx_array_t pushes; /* ngx_rtmp_relay_target_t * */ + ngx_array_t static_pulls; /* ngx_rtmp_relay_target_t * */ + ngx_array_t static_events; /* ngx_event_t * */ + ngx_log_t *log; + ngx_uint_t nbuckets; + ngx_msec_t buflen; + ngx_flag_t session_relay; + ngx_msec_t push_reconnect; + ngx_msec_t pull_reconnect; + ngx_rtmp_relay_ctx_t **ctx; +} ngx_rtmp_relay_app_conf_t; + + +extern ngx_module_t ngx_rtmp_relay_module; + + +ngx_int_t ngx_rtmp_relay_pull(ngx_rtmp_session_t *s, ngx_str_t *name, + ngx_rtmp_relay_target_t *target); +ngx_int_t ngx_rtmp_relay_push(ngx_rtmp_session_t *s, ngx_str_t *name, + ngx_rtmp_relay_target_t *target); + + +#endif /* _NGX_RTMP_RELAY_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_send.c b/ngx_http_flv_module/ngx_rtmp_send.c new file mode 100644 index 0000000..d3b6f32 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_send.c @@ -0,0 +1,781 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#include +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_amf.h" +#include "ngx_rtmp_streams.h" +#include "ngx_rtmp_cmd_module.h" + + +#define NGX_RTMP_USER_START(s, tp) \ + ngx_rtmp_header_t __h; \ + ngx_chain_t *__l; \ + ngx_buf_t *__b; \ + ngx_rtmp_core_srv_conf_t *__cscf; \ + \ + __cscf = ngx_rtmp_get_module_srv_conf( \ + s, ngx_rtmp_core_module); \ + memset(&__h, 0, sizeof(__h)); \ + __h.type = tp; \ + __h.csid = 2; \ + __l = ngx_rtmp_alloc_shared_buf(__cscf); \ + if (__l == NULL) { \ + return NULL; \ + } \ + __b = __l->buf; + +#define NGX_RTMP_UCTL_START(s, type, utype) \ + NGX_RTMP_USER_START(s, type); \ + *(__b->last++) = (u_char)((utype) >> 8); \ + *(__b->last++) = (u_char)(utype); + +#define NGX_RTMP_USER_OUT1(v) \ + *(__b->last++) = (u_char) v; + +#define NGX_RTMP_USER_OUT4(v) \ + *(__b->last++) = (u_char) (v >> 24); \ + *(__b->last++) = (u_char) (v >> 16); \ + *(__b->last++) = (u_char) (v >> 8); \ + *(__b->last++) = (u_char) v; + +#define NGX_RTMP_USER_END(s) \ + ngx_rtmp_prepare_message(s, &__h, NULL, __l); \ + return __l; + + +static ngx_int_t +ngx_rtmp_send_shared_packet(ngx_rtmp_session_t *s, ngx_chain_t *cl) +{ + ngx_rtmp_core_srv_conf_t *cscf; + ngx_http_request_t *r; + ngx_int_t rc; + + if (cl == NULL) { + return NGX_ERROR; + } + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + if (!s->relay) { + /* rquest from http */ + r = s->data; + if (r) { + ngx_rtmp_free_shared_chain(cscf, cl); + + return NGX_OK; + } + } + + rc = ngx_rtmp_send_message(s, cl, 0); + + ngx_rtmp_free_shared_chain(cscf, cl); + + return rc; +} + + +/* Protocol control messages */ + +ngx_chain_t * +ngx_rtmp_create_chunk_size(ngx_rtmp_session_t *s, uint32_t chunk_size) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "chunk_size=%uD", chunk_size); + + { + NGX_RTMP_USER_START(s, NGX_RTMP_MSG_CHUNK_SIZE); + + NGX_RTMP_USER_OUT4(chunk_size); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_chunk_size(ngx_rtmp_session_t *s, uint32_t chunk_size) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_chunk_size(s, chunk_size)); +} + + +ngx_chain_t * +ngx_rtmp_create_abort(ngx_rtmp_session_t *s, uint32_t csid) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: abort csid=%uD", csid); + + { + NGX_RTMP_USER_START(s, NGX_RTMP_MSG_ABORT); + + NGX_RTMP_USER_OUT4(csid); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_abort(ngx_rtmp_session_t *s, uint32_t csid) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_abort(s, csid)); +} + + +ngx_chain_t * +ngx_rtmp_create_ack(ngx_rtmp_session_t *s, uint32_t seq) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: ack seq=%uD", seq); + + { + NGX_RTMP_USER_START(s, NGX_RTMP_MSG_ACK); + + NGX_RTMP_USER_OUT4(seq); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_ack(ngx_rtmp_session_t *s, uint32_t seq) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_ack(s, seq)); +} + + +ngx_chain_t * +ngx_rtmp_create_ack_size(ngx_rtmp_session_t *s, uint32_t ack_size) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: ack_size=%uD", ack_size); + + { + NGX_RTMP_USER_START(s, NGX_RTMP_MSG_ACK_SIZE); + + NGX_RTMP_USER_OUT4(ack_size); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_ack_size(ngx_rtmp_session_t *s, uint32_t ack_size) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_ack_size(s, ack_size)); +} + + +ngx_chain_t * +ngx_rtmp_create_bandwidth(ngx_rtmp_session_t *s, uint32_t ack_size, + uint8_t limit_type) +{ + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: bandwidth ack_size=%uD limit=%d", + ack_size, (int)limit_type); + + { + NGX_RTMP_USER_START(s, NGX_RTMP_MSG_BANDWIDTH); + + NGX_RTMP_USER_OUT4(ack_size); + NGX_RTMP_USER_OUT1(limit_type); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_bandwidth(ngx_rtmp_session_t *s, uint32_t ack_size, + uint8_t limit_type) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_bandwidth(s, ack_size, limit_type)); +} + + +/* User control messages */ + +ngx_chain_t * +ngx_rtmp_create_stream_begin(ngx_rtmp_session_t *s, uint32_t msid) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: stream_begin msid=%uD", msid); + + { + NGX_RTMP_UCTL_START(s, NGX_RTMP_MSG_USER, NGX_RTMP_USER_STREAM_BEGIN); + + NGX_RTMP_USER_OUT4(msid); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_stream_begin(ngx_rtmp_session_t *s, uint32_t msid) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_stream_begin(s, msid)); +} + + +ngx_chain_t * +ngx_rtmp_create_stream_eof(ngx_rtmp_session_t *s, uint32_t msid) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: stream_end msid=%uD", msid); + + { + NGX_RTMP_UCTL_START(s, NGX_RTMP_MSG_USER, NGX_RTMP_USER_STREAM_EOF); + + NGX_RTMP_USER_OUT4(msid); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_stream_eof(ngx_rtmp_session_t *s, uint32_t msid) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_stream_eof(s, msid)); +} + + +ngx_chain_t * +ngx_rtmp_create_stream_dry(ngx_rtmp_session_t *s, uint32_t msid) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: stream_dry msid=%uD", msid); + + { + NGX_RTMP_UCTL_START(s, NGX_RTMP_MSG_USER, NGX_RTMP_USER_STREAM_DRY); + + NGX_RTMP_USER_OUT4(msid); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_stream_dry(ngx_rtmp_session_t *s, uint32_t msid) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_stream_dry(s, msid)); +} + + +ngx_chain_t * +ngx_rtmp_create_set_buflen(ngx_rtmp_session_t *s, uint32_t msid, + uint32_t buflen_msec) +{ + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: set_buflen msid=%uD buflen=%uD", + msid, buflen_msec); + + { + NGX_RTMP_UCTL_START(s, NGX_RTMP_MSG_USER, NGX_RTMP_USER_SET_BUFLEN); + + NGX_RTMP_USER_OUT4(msid); + NGX_RTMP_USER_OUT4(buflen_msec); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_set_buflen(ngx_rtmp_session_t *s, uint32_t msid, + uint32_t buflen_msec) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_set_buflen(s, msid, buflen_msec)); +} + + +ngx_chain_t * +ngx_rtmp_create_recorded(ngx_rtmp_session_t *s, uint32_t msid) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: recorded msid=%uD", msid); + + { + NGX_RTMP_UCTL_START(s, NGX_RTMP_MSG_USER, NGX_RTMP_USER_RECORDED); + + NGX_RTMP_USER_OUT4(msid); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_recorded(ngx_rtmp_session_t *s, uint32_t msid) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_recorded(s, msid)); +} + + +ngx_chain_t * +ngx_rtmp_create_ping_request(ngx_rtmp_session_t *s, uint32_t timestamp) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: ping_request timestamp=%uD", timestamp); + + { + NGX_RTMP_UCTL_START(s, NGX_RTMP_MSG_USER, NGX_RTMP_USER_PING_REQUEST); + + NGX_RTMP_USER_OUT4(timestamp); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_ping_request(ngx_rtmp_session_t *s, uint32_t timestamp) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_ping_request(s, timestamp)); +} + + +ngx_chain_t * +ngx_rtmp_create_ping_response(ngx_rtmp_session_t *s, uint32_t timestamp) +{ + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: ping_response timestamp=%uD", timestamp); + + { + NGX_RTMP_UCTL_START(s, NGX_RTMP_MSG_USER, NGX_RTMP_USER_PING_RESPONSE); + + NGX_RTMP_USER_OUT4(timestamp); + + NGX_RTMP_USER_END(s); + } +} + + +ngx_int_t +ngx_rtmp_send_ping_response(ngx_rtmp_session_t *s, uint32_t timestamp) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_ping_response(s, timestamp)); +} + + +static ngx_chain_t * +ngx_rtmp_alloc_amf_buf(void *arg) +{ + return ngx_rtmp_alloc_shared_buf((ngx_rtmp_core_srv_conf_t *)arg); +} + + +/* AMF sender */ + +/* NOTE: this function does not free shared bufs on error */ +ngx_int_t +ngx_rtmp_append_amf(ngx_rtmp_session_t *s, + ngx_chain_t **first, ngx_chain_t **last, + ngx_rtmp_amf_elt_t *elts, size_t nelts) +{ + ngx_rtmp_amf_ctx_t act; + ngx_rtmp_core_srv_conf_t *cscf; + ngx_int_t rc; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + memset(&act, 0, sizeof(act)); + act.arg = cscf; + act.alloc = ngx_rtmp_alloc_amf_buf; + act.log = s->connection->log; + + if (first) { + act.first = *first; + } + + if (last) { + act.link = *last; + } + + rc = ngx_rtmp_amf_write(&act, elts, nelts); + + if (first) { + *first = act.first; + } + + if (last) { + *last = act.link; + } + + return rc; +} + + +ngx_chain_t * +ngx_rtmp_create_amf(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_rtmp_amf_elt_t *elts, size_t nelts) +{ + ngx_chain_t *first; + ngx_int_t rc; + ngx_rtmp_core_srv_conf_t *cscf; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: amf nelts=%ui", nelts); + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + first = NULL; + + rc = ngx_rtmp_append_amf(s, &first, NULL, elts, nelts); + + if (rc != NGX_OK && first) { + ngx_rtmp_free_shared_chain(cscf, first); + first = NULL; + } + + if (first) { + ngx_rtmp_prepare_message(s, h, NULL, first); + } + + return first; +} + + +ngx_int_t +ngx_rtmp_send_amf(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + ngx_rtmp_amf_elt_t *elts, size_t nelts) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_amf(s, h, elts, nelts)); +} + + +ngx_chain_t * +ngx_rtmp_create_status(ngx_rtmp_session_t *s, char *code, char* level, + char *desc) +{ + ngx_rtmp_header_t h; + static double trans; + + static ngx_rtmp_amf_elt_t out_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("code"), + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("description"), + NULL, 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_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: status code='%s' level='%s' desc='%s'", + code, level, desc); + + out_inf[0].data = level; + out_inf[1].data = code; + out_inf[2].data = desc; + + memset(&h, 0, sizeof(h)); + + h.type = NGX_RTMP_MSG_AMF_CMD; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; + + return ngx_rtmp_create_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +ngx_int_t +ngx_rtmp_send_status(ngx_rtmp_session_t *s, char *code, char* level, char *desc) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_status(s, code, level, desc)); +} + + +ngx_chain_t * +ngx_rtmp_create_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) }, + }; + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "create: play_status code='%s' level='%s' " + "duration=%ui bytes=%ui", + code, level, duration, bytes); + + 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; + h.timestamp = duration; + + return ngx_rtmp_create_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) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_play_status(s, code, level, duration, bytes)); +} + + +ngx_chain_t * +ngx_rtmp_create_fcpublish(ngx_rtmp_session_t *s, u_char *desc) +{ + ngx_rtmp_header_t h; + static double trans; + + static ngx_rtmp_amf_elt_t out_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + "status", 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("code"), + "NetStream.Publish.Start", 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("description"), + NULL, 0 }, + }; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "onFCPublish", 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_DEBUG, s->connection->log, 0, + "create: fcpublish - set structure data"); + + out_inf[2].data = desc; + trans = 0; + + memset(&h, 0, sizeof(h)); + + h.type = NGX_RTMP_MSG_AMF_CMD; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; + + return ngx_rtmp_create_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +ngx_int_t +ngx_rtmp_send_fcpublish(ngx_rtmp_session_t *s, u_char *desc) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_fcpublish(s, desc)); +} + + +ngx_chain_t * +ngx_rtmp_create_fcunpublish(ngx_rtmp_session_t *s, u_char *desc) +{ + ngx_rtmp_header_t h; + static double trans; + + static ngx_rtmp_amf_elt_t out_inf[] = { + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + "status", 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("code"), + "NetStream.Unpublish.Success", 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("description"), + NULL, 0 }, + }; + + static ngx_rtmp_amf_elt_t out_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "onFCUnpublish", 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_DEBUG, s->connection->log, 0, + "create: fcunpublish - set structure data"); + + out_inf[2].data = desc; + trans = 0; + + memset(&h, 0, sizeof(h)); + + h.type = NGX_RTMP_MSG_AMF_CMD; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; + + return ngx_rtmp_create_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} + + +ngx_int_t +ngx_rtmp_send_fcunpublish(ngx_rtmp_session_t *s, u_char *desc) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_fcunpublish(s, desc)); +} + + +ngx_chain_t * +ngx_rtmp_create_sample_access(ngx_rtmp_session_t *s) +{ + ngx_rtmp_header_t h; + + static int access = 1; + + static ngx_rtmp_amf_elt_t access_elts[] = { + + { NGX_RTMP_AMF_STRING, + ngx_null_string, + "|RtmpSampleAccess", 0 }, + + { NGX_RTMP_AMF_BOOLEAN, + ngx_null_string, + &access, 0 }, + + { NGX_RTMP_AMF_BOOLEAN, + ngx_null_string, + &access, 0 }, + }; + + 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_create_amf(s, &h, access_elts, + sizeof(access_elts) / sizeof(access_elts[0])); +} + + +ngx_int_t +ngx_rtmp_send_sample_access(ngx_rtmp_session_t *s) +{ + return ngx_rtmp_send_shared_packet(s, + ngx_rtmp_create_sample_access(s)); +} diff --git a/ngx_http_flv_module/ngx_rtmp_shared.c b/ngx_http_flv_module/ngx_rtmp_shared.c new file mode 100644 index 0000000..6f6e4e8 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_shared.c @@ -0,0 +1,126 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#include +#include +#include "ngx_rtmp.h" + + +ngx_chain_t * +ngx_rtmp_alloc_shared_buf(ngx_rtmp_core_srv_conf_t *cscf) +{ + u_char *p; + ngx_chain_t *out; + ngx_buf_t *b; + size_t size; + + if (cscf->free) { + out = cscf->free; + cscf->free = out->next; + + } else { + + size = cscf->chunk_size + NGX_RTMP_MAX_CHUNK_HEADER; + + p = ngx_pcalloc(cscf->pool, NGX_RTMP_REFCOUNT_BYTES + + sizeof(ngx_chain_t) + + sizeof(ngx_buf_t) + + size); + if (p == NULL) { + return NULL; + } + + p += NGX_RTMP_REFCOUNT_BYTES; + out = (ngx_chain_t *)p; + + p += sizeof(ngx_chain_t); + out->buf = (ngx_buf_t *)p; + + p += sizeof(ngx_buf_t); + out->buf->start = p; + out->buf->end = p + size; + } + + out->next = NULL; + b = out->buf; + b->pos = b->last = b->start + NGX_RTMP_MAX_CHUNK_HEADER; + b->memory = 1; + + /* buffer has refcount =1 when created! */ + ngx_rtmp_ref_set(out, 1); + + return out; +} + + +void +ngx_rtmp_free_shared_chain(ngx_rtmp_core_srv_conf_t *cscf, ngx_chain_t *in) +{ + ngx_chain_t *cl; + + if (ngx_rtmp_ref_put(in)) { + return; + } + + for (cl = in; ; cl = cl->next) { + if (cl->next == NULL) { + cl->next = cscf->free; + cscf->free = in; + return; + } + } +} + + +ngx_chain_t * +ngx_rtmp_append_shared_bufs(ngx_rtmp_core_srv_conf_t *cscf, + ngx_chain_t *head, ngx_chain_t *in) +{ + ngx_chain_t *l, **ll; + u_char *p; + size_t size; + + ll = &head; + p = in->buf->pos; + l = head; + + if (l) { + for(; l->next; l = l->next); + ll = &l->next; + } + + for ( ;; ) { + + if (l == NULL || l->buf->last == l->buf->end) { + l = ngx_rtmp_alloc_shared_buf(cscf); + if (l == NULL || l->buf == NULL) { + break; + } + + *ll = l; + ll = &l->next; + } + + while (l->buf->end - l->buf->last >= in->buf->last - p) { + l->buf->last = ngx_cpymem(l->buf->last, p, + in->buf->last - p); + in = in->next; + if (in == NULL) { + goto done; + } + p = in->buf->pos; + } + + size = l->buf->end - l->buf->last; + l->buf->last = ngx_cpymem(l->buf->last, p, size); + p += size; + } + +done: + *ll = NULL; + + return head; +} diff --git a/ngx_http_flv_module/ngx_rtmp_stat_module.c b/ngx_http_flv_module/ngx_rtmp_stat_module.c new file mode 100644 index 0000000..6b0340b --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_stat_module.c @@ -0,0 +1,1714 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) plainheart + * Copyright (C) Winshining + */ + + +#include +#include +#include +#include +#include "ngx_rtmp.h" +#include "ngx_rtmp_version.h" +#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_play_module.h" +#include "ngx_rtmp_codec_module.h" +#include "ngx_rtmp_record_module.h" + + +static ngx_int_t ngx_rtmp_stat_init_process(ngx_cycle_t *cycle); +static char *ngx_rtmp_stat(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); +static ngx_int_t ngx_rtmp_stat_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_stat_create_loc_conf(ngx_conf_t *cf); +static char * ngx_rtmp_stat_merge_loc_conf(ngx_conf_t *cf, + void *parent, void *child); + + +static time_t start_time; + + +#define NGX_RTMP_STAT_ALL 0xff +#define NGX_RTMP_STAT_GLOBAL 0x01 +#define NGX_RTMP_STAT_LIVE 0x02 +#define NGX_RTMP_STAT_CLIENTS 0x04 +#define NGX_RTMP_STAT_PLAY 0x08 +#define NGX_RTMP_STAT_RECORD 0x10 + +#define NGX_RTMP_STAT_FORMAT_XML 0x01 +#define NGX_RTMP_STAT_FORMAT_JSON 0x02 + + +/* + * global: stat-{bufs-{total,free,used}, total bytes in/out, bw in/out} - cscf +*/ + + +typedef struct { + ngx_uint_t stat; + ngx_str_t stylesheet; + ngx_uint_t format; +} ngx_rtmp_stat_loc_conf_t; + + +static ngx_conf_bitmask_t ngx_rtmp_stat_masks[] = { + { ngx_string("all"), NGX_RTMP_STAT_ALL }, + { ngx_string("global"), NGX_RTMP_STAT_GLOBAL }, + { ngx_string("live"), NGX_RTMP_STAT_LIVE }, + { ngx_string("clients"), NGX_RTMP_STAT_CLIENTS }, + { ngx_string("record"), NGX_RTMP_STAT_RECORD }, + { ngx_null_string, 0 } +}; + + +static ngx_conf_bitmask_t ngx_rtmp_stat_format_masks[] = { + { ngx_string("xml"), NGX_RTMP_STAT_FORMAT_XML }, + { ngx_string("json"), NGX_RTMP_STAT_FORMAT_JSON }, + { ngx_null_string, 0 } +}; + + +static ngx_command_t ngx_rtmp_stat_commands[] = { + + { ngx_string("rtmp_stat"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_1MORE, + ngx_rtmp_stat, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_rtmp_stat_loc_conf_t, stat), + ngx_rtmp_stat_masks }, + + { ngx_string("rtmp_stat_stylesheet"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_rtmp_stat_loc_conf_t, stylesheet), + NULL }, + + { ngx_string("rtmp_stat_format"), + NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, + ngx_rtmp_stat, + NGX_HTTP_LOC_CONF_OFFSET, + offsetof(ngx_rtmp_stat_loc_conf_t, format), + ngx_rtmp_stat_format_masks }, + + ngx_null_command +}; + + +static ngx_http_module_t ngx_rtmp_stat_module_ctx = { + NULL, /* preconfiguration */ + ngx_rtmp_stat_postconfiguration, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + ngx_rtmp_stat_create_loc_conf, /* create location configuration */ + ngx_rtmp_stat_merge_loc_conf /* merge location configuration */ +}; + + +ngx_module_t ngx_rtmp_stat_module = { + NGX_MODULE_V1, + &ngx_rtmp_stat_module_ctx, /* module context */ + ngx_rtmp_stat_commands, /* module directives */ + NGX_HTTP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + ngx_rtmp_stat_init_process, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +#define NGX_RTMP_STAT_BUFSIZE 256 + + +static ngx_int_t +ngx_rtmp_stat_init_process(ngx_cycle_t *cycle) +{ + /* + * HTTP process initializer is called + * after event module initializer + * so we can run posted events here + */ + + ngx_event_process_posted(cycle, &ngx_rtmp_init_queue); + + return NGX_OK; +} + + +/* ngx_escape_html does not escape characters out of ASCII range + * which are bad for xslt */ + +static void * +ngx_rtmp_stat_escape(ngx_http_request_t *r, void *data, size_t len) +{ + u_char *p, *np; + void *new_data; + size_t n; + + p = data; + + for (n = 0; n < len; ++n, ++p) { + if (*p < 0x20 || *p >= 0x7f) { + break; + } + } + + if (n == len) { + return data; + } + + new_data = ngx_palloc(r->pool, len); + if (new_data == NULL) { + return NULL; + } + + p = data; + np = new_data; + + for (n = 0; n < len; ++n, ++p, ++np) { + *np = (*p < 0x20 || *p >= 0x7f) ? (u_char) ' ' : *p; + } + + return new_data; +} + + +#if (NGX_WIN32) +/* + * Fix broken MSVC memcpy optimization for 4-byte data + * when this function is inlined + */ +__declspec(noinline) +#endif + + +static void +ngx_rtmp_stat_output(ngx_http_request_t *r, ngx_chain_t ***lll, + void *data, size_t len, ngx_uint_t escape) +{ + ngx_chain_t *cl; + ngx_buf_t *b; + size_t real_len; + + if (len == 0) { + return; + } + + if (escape) { + data = ngx_rtmp_stat_escape(r, data, len); + if (data == NULL) { + return; + } + } + + real_len = escape + ? len + ngx_escape_html(NULL, data, len) + : len; + + cl = **lll; + if (cl && cl->buf->last + real_len > cl->buf->end) { + *lll = &cl->next; + } + + if (**lll == NULL) { + cl = ngx_alloc_chain_link(r->pool); + if (cl == NULL) { + return; + } + b = ngx_create_temp_buf(r->pool, + ngx_max(NGX_RTMP_STAT_BUFSIZE, real_len)); + if (b == NULL || b->pos == NULL) { + return; + } + cl->next = NULL; + cl->buf = b; + **lll = cl; + } + + b = (**lll)->buf; + + if (escape) { + b->last = (u_char *)ngx_escape_html(b->last, data, len); + } else { + b->last = ngx_cpymem(b->last, data, len); + } +} + + +/* These shortcuts assume 2 variables exist in current context: + * ngx_http_request_t *r + * ngx_chain_t ***lll */ + +/* plain data */ +#define NGX_RTMP_STAT(data, len) ngx_rtmp_stat_output(r, lll, data, len, 0) + +/* escaped data */ +#define NGX_RTMP_STAT_E(data, len) ngx_rtmp_stat_output(r, lll, data, len, 1) + +/* literal */ +#define NGX_RTMP_STAT_L(s) NGX_RTMP_STAT((s), sizeof(s) - 1) + +/* ngx_str_t */ +#define NGX_RTMP_STAT_S(s) NGX_RTMP_STAT((s)->data, (s)->len) + +/* escaped ngx_str_t */ +#define NGX_RTMP_STAT_ES(s) NGX_RTMP_STAT_E((s)->data, (s)->len) + +/* C string */ +#define NGX_RTMP_STAT_CS(s) NGX_RTMP_STAT((s), ngx_strlen(s)) + +/* escaped C string */ +#define NGX_RTMP_STAT_ECS(s) NGX_RTMP_STAT_E((s), ngx_strlen(s)) + + +#define NGX_RTMP_STAT_BW 0x01 +#define NGX_RTMP_STAT_BYTES 0x02 +#define NGX_RTMP_STAT_BW_BYTES 0x03 + + +static void +ngx_rtmp_stat_bw(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_rtmp_bandwidth_t *bw, char *name, + ngx_uint_t flags) +{ + u_char buf[NGX_INT64_LEN + 9]; + ngx_rtmp_stat_loc_conf_t *slcf; + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + + ngx_rtmp_update_bandwidth(bw, 0); + + if (flags & NGX_RTMP_STAT_BW) { + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("%uLbandwidth * 8) + - buf); + NGX_RTMP_STAT_CS(name); + NGX_RTMP_STAT_L(">\r\n"); + } else { + NGX_RTMP_STAT_L("\"bw_"); + NGX_RTMP_STAT_CS(name); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "\":%uL,", + bw->bandwidth * 8) + - buf); + } + } + + if (flags & NGX_RTMP_STAT_BYTES) { + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("%uLbytes) + - buf); + NGX_RTMP_STAT_CS(name); + NGX_RTMP_STAT_L(">\r\n"); + } else { + NGX_RTMP_STAT_L("\"bytes_"); + NGX_RTMP_STAT_CS(name); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "\":%uL,", + bw->bytes) + - buf); + } + } +} + + +#ifdef NGX_RTMP_POOL_DEBUG +static void +ngx_rtmp_stat_get_pool_size(ngx_pool_t *pool, ngx_uint_t *nlarge, + ngx_uint_t *size) +{ + ngx_pool_large_t *l; + ngx_pool_t *p, *n; + + *nlarge = 0; + for (l = pool->large; l; l = l->next) { + ++*nlarge; + } + + *size = 0; + for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) { + *size += (p->d.last - (u_char *)p); + if (n == NULL) { + break; + } + } +} + + +static void +ngx_rtmp_stat_dump_pool(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_pool_t *pool) +{ + ngx_uint_t nlarge, size; + u_char buf[NGX_INT_T_LEN]; + ngx_rtmp_stat_loc_conf_t *slcf; + + size = 0; + nlarge = 0; + ngx_rtmp_stat_get_pool_size(pool, &nlarge, &size); + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "%ui", nlarge) - buf); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "%ui", size) - buf); + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("\"pool\":{\"nlarge\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "%ui", nlarge) - buf); + NGX_RTMP_STAT_L(",\"size\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "%ui", size) - buf); + NGX_RTMP_STAT_L("}"); + } +} +#endif + + +static void +ngx_rtmp_stat_client(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_rtmp_session_t *s) +{ + u_char buf[NGX_INT_T_LEN]; + ngx_rtmp_stat_loc_conf_t *slcf; + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + +#ifdef NGX_RTMP_POOL_DEBUG + ngx_rtmp_stat_dump_pool(r, lll, s->connection->pool); + if (slcf->format & NGX_RTMP_STAT_FORMAT_JSON) { + NGX_RTMP_STAT_L(","); + } +#endif + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "%ui", + (ngx_uint_t) s->connection->number) - buf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("
"); + NGX_RTMP_STAT_ES(&s->connection->addr_text); + NGX_RTMP_STAT_L("
\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + + if (s->flashver.len) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_ES(&s->flashver); + NGX_RTMP_STAT_L("\r\n"); + } + + if (s->page_url.len) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_ES(&s->page_url); + NGX_RTMP_STAT_L("\r\n"); + } + + if (s->swf_url.len) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_ES(&s->swf_url); + NGX_RTMP_STAT_L("\r\n"); + } + } else { + NGX_RTMP_STAT_L("\"id\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "%ui", + (ngx_uint_t) s->connection->number) - buf); + + NGX_RTMP_STAT_L(",\"address\":\""); + NGX_RTMP_STAT_ES(&s->connection->addr_text); + + NGX_RTMP_STAT_L("\",\"time\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "%i", + (ngx_int_t) (ngx_current_msec - s->epoch)) - buf); + NGX_RTMP_STAT_L(","); + + if (s->flashver.len) { + NGX_RTMP_STAT_L("\"flashver\":\""); + NGX_RTMP_STAT_ES(&s->flashver); + NGX_RTMP_STAT_L("\","); + } + + if (s->page_url.len) { + NGX_RTMP_STAT_L("\"pageurl\":\""); + NGX_RTMP_STAT_ES(&s->page_url); + NGX_RTMP_STAT_L("\","); + } + + if (s->swf_url.len) { + NGX_RTMP_STAT_L("\"swfurl\":\""); + NGX_RTMP_STAT_ES(&s->swf_url); + NGX_RTMP_STAT_L("\","); + } + } +} + + +static char * +ngx_rtmp_stat_get_aac_profile(ngx_uint_t p, ngx_uint_t sbr, ngx_uint_t ps) { + switch (p) { + case 1: + return "Main"; + case 2: + if (ps) { + return "HEv2"; + } + if (sbr) { + return "HE"; + } + return "LC"; + case 3: + return "SSR"; + case 4: + return "LTP"; + case 5: + return "SBR"; + default: + return ""; + } +} + + +static char * +ngx_rtmp_stat_get_avc_profile(ngx_uint_t p) { + switch (p) { + case 66: + return "Baseline"; + case 77: + return "Main"; + case 100: + return "High"; + default: + return ""; + } +} + + +static void +ngx_rtmp_stat_live_records(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_rtmp_session_t *s) +{ + ngx_uint_t i; + u_char buf[NGX_INT_T_LEN]; + ngx_str_t filename; + ngx_file_info_t filebuf; + ngx_rtmp_record_ctx_t *rctx; + ngx_rtmp_record_rec_ctx_t *rrctx; + ngx_rtmp_stat_loc_conf_t *slcf; + + rctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_record_module); + if(rctx == NULL) { + return; + } + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + rrctx = rctx->rec.elts; + + for(i = 0; i < rctx->rec.nelts; ++i, ++rrctx) { + if (rrctx->file.fd == NGX_INVALID_FILE) { + continue; + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + + if(rrctx->conf) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_S(&rrctx->conf->id); + NGX_RTMP_STAT_L("\r\n"); + } + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", rrctx->epoch) - buf); + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", rrctx->time_shift) - buf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + ngx_rtmp_record_get_path(s, rrctx, &filename); + NGX_RTMP_STAT_S(&filename); + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + ngx_file_info((const char *)filename.data, &filebuf); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", ngx_file_size(&filebuf)) - buf); + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", rrctx->nframes) - buf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("{"); + + if(rrctx->conf) { + NGX_RTMP_STAT_L("\"recorder\":\""); + NGX_RTMP_STAT_S(&rrctx->conf->id); + NGX_RTMP_STAT_L("\""); + } else { + NGX_RTMP_STAT_L("\"recorder\":\"\""); + } + + NGX_RTMP_STAT_L(",\"epoch\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", rrctx->epoch) - buf); + NGX_RTMP_STAT_L(",\"time_shift\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", rrctx->time_shift) - buf); + + NGX_RTMP_STAT_L(",\"recording\":true"); + NGX_RTMP_STAT_L(",\"file\":\""); + ngx_rtmp_record_get_path(s, rrctx, &filename); + NGX_RTMP_STAT_S(&filename); + NGX_RTMP_STAT_L("\",\"time\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", ngx_cached_time->sec - rrctx->timestamp) - buf); + NGX_RTMP_STAT_L(",\"size\":"); + ngx_file_info((const char *)filename.data, &filebuf); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", ngx_file_size(&filebuf)) - buf); + NGX_RTMP_STAT_L(",\"nframes\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", rrctx->nframes) - buf); + + NGX_RTMP_STAT_L("}"); + } + } +} + + +static void +ngx_rtmp_stat_live(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_rtmp_live_app_conf_t *lacf) +{ + ngx_rtmp_live_stream_t *stream; + ngx_rtmp_codec_ctx_t *codec; + ngx_rtmp_live_ctx_t *ctx; + ngx_rtmp_session_t *s; + ngx_int_t n; + ngx_uint_t nclients, total_nclients; + ngx_uint_t f; + ngx_flag_t prev; + u_char buf[NGX_INT64_LEN + 4]; + u_char bbuf[NGX_INT32_LEN]; + ngx_rtmp_stat_loc_conf_t *slcf; + u_char *cname; + + if (!lacf->live) { + return; + } + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L(",\"live\":{"); + NGX_RTMP_STAT_L("\"streams\":["); + } + + total_nclients = 0; + prev = 0; + for (n = 0; n < lacf->nbuckets; ++n) { + for (stream = lacf->streams[n]; stream; stream = stream->next) { + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + if (prev) { + NGX_RTMP_STAT_L(","); + } + + prev = 1; + NGX_RTMP_STAT_L("{"); + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_ECS(stream->name); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("\"name\":\""); + NGX_RTMP_STAT_ECS(stream->name); + NGX_RTMP_STAT_L("\","); + + NGX_RTMP_STAT_L("\"time\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), "%i", + (ngx_int_t) (ngx_current_msec - stream->epoch)) + - buf); + NGX_RTMP_STAT_L(","); + } + + ngx_rtmp_stat_bw(r, lll, &stream->bw_in, "in", + NGX_RTMP_STAT_BW_BYTES); + ngx_rtmp_stat_bw(r, lll, &stream->bw_out, "out", + NGX_RTMP_STAT_BW_BYTES); + ngx_rtmp_stat_bw(r, lll, &stream->bw_in_audio, "audio", + NGX_RTMP_STAT_BW); + ngx_rtmp_stat_bw(r, lll, &stream->bw_in_video, "video", + NGX_RTMP_STAT_BW); + + nclients = 0; + codec = NULL; + + if (slcf->stat & NGX_RTMP_STAT_CLIENTS && + slcf->format & NGX_RTMP_STAT_FORMAT_JSON) + { + NGX_RTMP_STAT_L("\"clients\":["); + } + + for (ctx = stream->ctx; ctx; ctx = ctx->next, ++nclients) { + s = ctx->session; + if (slcf->stat & NGX_RTMP_STAT_CLIENTS) { + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("{"); + } + + ngx_rtmp_stat_client(r, lll, s); + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", ctx->ndropped) - buf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + if (!lacf->interleave) { + NGX_RTMP_STAT(bbuf, ngx_snprintf(bbuf, sizeof(bbuf), + "%D", ctx->cs[1].timestamp - + ctx->cs[0].timestamp) - bbuf); + } else { + NGX_RTMP_STAT(bbuf, ngx_snprintf(bbuf, sizeof(bbuf), + "%D", 0) - bbuf); + } + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(bbuf, ngx_snprintf(bbuf, sizeof(bbuf), + "%D", s->current_time) - bbuf); + NGX_RTMP_STAT_L("\r\n"); + + if (ctx->publishing) { + NGX_RTMP_STAT_L("\r\n"); + } + + if (ctx->active) { + NGX_RTMP_STAT_L("\r\n"); + } + } else { + NGX_RTMP_STAT_L("\"dropped\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", ctx->ndropped) - buf); + + NGX_RTMP_STAT_L(",\"avsync\":"); + if (!lacf->interleave) { + NGX_RTMP_STAT(bbuf, ngx_snprintf(bbuf, sizeof(bbuf), + "%D", ctx->cs[1].timestamp - + ctx->cs[0].timestamp) - bbuf); + } else { + NGX_RTMP_STAT(bbuf, ngx_snprintf(bbuf, sizeof(bbuf), + "%D", 0) - bbuf); + } + + NGX_RTMP_STAT_L(",\"timestamp\":"); + NGX_RTMP_STAT(bbuf, ngx_snprintf(bbuf, sizeof(bbuf), + "%D", s->current_time) - bbuf); + + NGX_RTMP_STAT_L(",\"publishing\":"); + if (ctx->publishing) { + NGX_RTMP_STAT_L("true"); + } else { + NGX_RTMP_STAT_L("false"); + } + + NGX_RTMP_STAT_L(",\"active\":"); + if (ctx->active) { + NGX_RTMP_STAT_L("true"); + } else { + NGX_RTMP_STAT_L("false"); + } + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("}"); + if (ctx->next) { + NGX_RTMP_STAT_L(","); + } + } + } + if (ctx->publishing) { + codec = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module); + } + } + total_nclients += nclients; + + if (slcf->stat & NGX_RTMP_STAT_CLIENTS && + slcf->format & NGX_RTMP_STAT_FORMAT_JSON) + { + NGX_RTMP_STAT_L("],"); + } + + if(slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("\"records\":["); + } + + for (ctx = stream->ctx; ctx; ctx = ctx->next) { + /* valid for only publishers */ + if (ctx->publishing) { + s = ctx->session; + + if (slcf->stat & NGX_RTMP_STAT_RECORD) { + ngx_rtmp_stat_live_records(r, lll, s); + } + + break; + } + } + + if(slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + if (codec == NULL) { + NGX_RTMP_STAT_L("],"); + } else { + NGX_RTMP_STAT_L("]"); + } + } + + if (codec) { + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L(",\"meta\":{"); + + NGX_RTMP_STAT_L("\"video\":{"); + NGX_RTMP_STAT_L("\"width\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", codec->width) - buf); + NGX_RTMP_STAT_L(",\"height\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", codec->height) - buf); + NGX_RTMP_STAT_L(",\"frame_rate\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%.3f", codec->frame_rate) - buf); + + cname = ngx_rtmp_get_video_codec_name(codec->video_codec_id); + if (*cname) { + NGX_RTMP_STAT_L(",\"codec\":\""); + NGX_RTMP_STAT_ECS(cname); + NGX_RTMP_STAT_L("\""); + } + if (codec->avc_profile) { + NGX_RTMP_STAT_L(",\"profile\":\""); + NGX_RTMP_STAT_CS(ngx_rtmp_stat_get_avc_profile( + codec->avc_profile)); + NGX_RTMP_STAT_L("\""); + } + if (codec->avc_compat) { + NGX_RTMP_STAT_L(",\"compat\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", codec->avc_compat) - buf); + } + if (codec->avc_level) { + NGX_RTMP_STAT_L(",\"level\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%.1f", codec->avc_level / 10.) - buf); + } + + NGX_RTMP_STAT_L("},\"audio\":{"); + cname = ngx_rtmp_get_audio_codec_name(codec->audio_codec_id); + f = 0; + if (*cname) { + f = 1; + NGX_RTMP_STAT_L("\"codec\":\""); + NGX_RTMP_STAT_ECS(cname); + } + if (codec->aac_profile) { + if (f == 1) NGX_RTMP_STAT_L("\","); + f = 2; + NGX_RTMP_STAT_L("\"profile\":\""); + NGX_RTMP_STAT_CS( + ngx_rtmp_stat_get_aac_profile(codec->aac_profile, + codec->aac_sbr, + codec->aac_ps)); + } + if (codec->aac_chan_conf) { + if (f >= 1) NGX_RTMP_STAT_L("\","); + f = 3; + NGX_RTMP_STAT_L("\"channels\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", codec->aac_chan_conf) - buf); + } else if (codec->audio_channels) { + if (f >= 1) NGX_RTMP_STAT_L("\","); + f = 3; + NGX_RTMP_STAT_L("\"channels\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", codec->audio_channels) - buf); + } + if (codec->sample_rate) { + if (f == 1 || f == 2) { + NGX_RTMP_STAT_L("\","); + } else if (f == 3) { + NGX_RTMP_STAT_L(","); + } + f = 4; + NGX_RTMP_STAT_L("\"sample_rate\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", codec->sample_rate) - buf); + } + if (f == 1 || f == 2) { + NGX_RTMP_STAT_L("\""); + } + NGX_RTMP_STAT_L("}}"); + } + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", nclients) - buf); + NGX_RTMP_STAT_L("\r\n"); + + if (stream->publishing) { + NGX_RTMP_STAT_L("\r\n"); + } + + if (stream->active) { + NGX_RTMP_STAT_L("\r\n"); + } + + NGX_RTMP_STAT_L("\r\n"); + } else { + if (codec) { + NGX_RTMP_STAT_L(","); + } + NGX_RTMP_STAT_L("\"nclients\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", nclients) - buf); + + NGX_RTMP_STAT_L(",\"publishing\":"); + if (stream->publishing) { + NGX_RTMP_STAT_L("true"); + } else { + NGX_RTMP_STAT_L("false"); + } + + NGX_RTMP_STAT_L(",\"active\":"); + if (stream->active) { + NGX_RTMP_STAT_L("true"); + } else { + NGX_RTMP_STAT_L("false"); + } + + NGX_RTMP_STAT_L("}"); + } + } + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", total_nclients) - buf); + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("],\"nclients\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", total_nclients) - buf); + NGX_RTMP_STAT_L("}"); + } +} + + +static void +ngx_rtmp_stat_play(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_rtmp_play_app_conf_t *pacf) +{ + ngx_rtmp_play_ctx_t *ctx, *sctx; + ngx_rtmp_session_t *s; + ngx_uint_t n, nclients, total_nclients; + ngx_flag_t prev; + u_char buf[NGX_INT_T_LEN]; + u_char bbuf[NGX_INT32_LEN]; + ngx_rtmp_stat_loc_conf_t *slcf; + + if (pacf->entries.nelts == 0) { + return; + } + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L(",\"play\":{"); + NGX_RTMP_STAT_L("\"streams\":["); + } + + total_nclients = 0; + prev = 0; + for (n = 0; n < pacf->nbuckets; ++n) { + for (ctx = pacf->ctx[n]; ctx; ) { + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_ECS(ctx->name); + NGX_RTMP_STAT_L("\r\n"); + } else { + if (prev) { + NGX_RTMP_STAT_L(","); + } + + prev = 1; + NGX_RTMP_STAT_L("{\"name\":\""); + NGX_RTMP_STAT_ECS(ctx->name); + NGX_RTMP_STAT_L("\",\"clients\":["); + } + + nclients = 0; + sctx = ctx; + for (; ctx; ctx = ctx->next) { + if (ngx_strcmp(ctx->name, sctx->name)) { + break; + } + + nclients++; + + s = ctx->session; + if (slcf->stat & NGX_RTMP_STAT_CLIENTS) { + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + + ngx_rtmp_stat_client(r, lll, s); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(bbuf, ngx_snprintf(bbuf, sizeof(bbuf), + "%D", s->current_time) - bbuf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("{"); + + ngx_rtmp_stat_client(r, lll, s); + + NGX_RTMP_STAT_L("\"timestamp\":"); + NGX_RTMP_STAT(bbuf, ngx_snprintf(bbuf, sizeof(bbuf), + "%D", s->current_time) - bbuf); + + NGX_RTMP_STAT_L("}"); + } + } + } + total_nclients += nclients; + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", nclients) - buf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("],"); + NGX_RTMP_STAT_L("\"active\":true,"); + NGX_RTMP_STAT_L("\"nclients\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", nclients) - buf); + NGX_RTMP_STAT_L("}"); + } + } + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", total_nclients) - buf); + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("],\"nclients\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", total_nclients) - buf); + NGX_RTMP_STAT_L("}"); + } +} + + +static void +ngx_rtmp_stat_application_recorders(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_rtmp_core_app_conf_t *cacf) +{ + size_t n, len; + u_char flag[NGX_RTMP_MAX_URL]; + u_char buf[NGX_INT_T_LEN]; + ngx_rtmp_record_app_conf_t *racf, *lracf, **rracf; + ngx_rtmp_stat_loc_conf_t *slcf; + + racf = cacf->app_conf[ngx_rtmp_record_module.ctx_index]; + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + + if(slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", racf->rec.nelts) - buf); + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L(",\"recorders\":{"); + NGX_RTMP_STAT_L("\"count\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", racf->rec.nelts) - buf); + NGX_RTMP_STAT_L(",\"lists\":["); + } + + rracf = racf->rec.elts; + for(n = 0; n < racf->rec.nelts; ++n, ++rracf) { + lracf = *rracf; + + if(n > 0 && n < racf->rec.nelts - 1) { + NGX_RTMP_STAT_L(","); + } + + if(slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_S(&lracf->id); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + + if(lracf->flags & NGX_RTMP_RECORD_OFF) { + NGX_RTMP_STAT_L(""); + } + + if(lracf->flags & NGX_RTMP_RECORD_VIDEO) { + NGX_RTMP_STAT_L("\r\n"); + + if(lracf->unique) { + NGX_RTMP_STAT_L("\r\n"); + } + + if(lracf->append) { + NGX_RTMP_STAT_L("\r\n"); + } + + if(lracf->lock_file) { + NGX_RTMP_STAT_L("\r\n"); + } + + if(lracf->notify) { + NGX_RTMP_STAT_L("\r\n"); + } + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_S(&lracf->path); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", (ngx_uint_t)lracf->max_size) - buf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", (ngx_uint_t)lracf->max_frames) - buf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + + if (lracf->interval == NGX_CONF_UNSET_MSEC) { + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%d", -1) - buf); + } else { + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", lracf->interval) - buf); + } + + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_S(&lracf->suffix); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("{\"id\":\""); + NGX_RTMP_STAT_S(&lracf->id); + NGX_RTMP_STAT_L("\",\"flags\":["); + + ngx_memzero(flag, sizeof(flag)); + + if(lracf->flags & NGX_RTMP_RECORD_OFF) { + *ngx_snprintf(flag + ngx_strlen(flag), + NGX_RTMP_MAX_URL - ngx_strlen(flag), + "%s", "\"off\"") = 0; + } + + if(lracf->flags & NGX_RTMP_RECORD_VIDEO) { + len = ngx_strlen(flag); + if (len && + (len + 1) < NGX_RTMP_MAX_URL && + flag[len - 1] != ',') + { + flag[len++] = ','; + } + + if (NGX_RTMP_MAX_URL - len >= sizeof("\"video\"")) { + *ngx_snprintf(flag + ngx_strlen(flag), + NGX_RTMP_MAX_URL - len, + "%s", "\"video\"") = 0; + } else { + if (flag[len - 1] == ',') { + flag[len - 1] = 0; + } + } + } + + if(lracf->flags & NGX_RTMP_RECORD_AUDIO) { + len = ngx_strlen(flag); + if (len && + (len + 1) < NGX_RTMP_MAX_URL && + flag[len - 1] != ',') + { + flag[len++] = ','; + } + + if (NGX_RTMP_MAX_URL - len >= sizeof("\"audio\"")) { + *ngx_snprintf(flag + ngx_strlen(flag), + NGX_RTMP_MAX_URL - len, + "%s", "\"audio\"") = 0; + } else { + if (flag[len - 1] == ',') { + flag[len - 1] = 0; + } + } + } + + if(lracf->flags & NGX_RTMP_RECORD_KEYFRAMES) { + len = ngx_strlen(flag); + if (len && + (len + 1) < NGX_RTMP_MAX_URL && + flag[len - 1] != ',') + { + flag[len++] = ','; + } + + if (NGX_RTMP_MAX_URL - len >= sizeof("\"keyframes\"")) { + *ngx_snprintf(flag + ngx_strlen(flag), + NGX_RTMP_MAX_URL - len, + "%s", "\"keyframes\"") = 0; + } else { + if (flag[len - 1] == ',') { + flag[len - 1] = 0; + } + } + } + + if(lracf->flags & NGX_RTMP_RECORD_MANUAL) { + len = ngx_strlen(flag); + if (len && + (len + 1) < NGX_RTMP_MAX_URL && + flag[len - 1] != ',') + { + flag[len++] = ','; + } + + if (NGX_RTMP_MAX_URL - len >= sizeof("\"manual\"")) { + *ngx_snprintf(flag + ngx_strlen(flag), + NGX_RTMP_MAX_URL - len, + "%s", "\"manual\"") = 0; + } else { + if (flag[len - 1] == ',') { + flag[len - 1] = 0; + } + } + } + + NGX_RTMP_STAT_CS(flag); + + NGX_RTMP_STAT_L("]"); + + if(lracf->unique) { + NGX_RTMP_STAT_L(",\"unique\":true"); + } else { + NGX_RTMP_STAT_L(",\"unique\":false"); + } + + if(lracf->append) { + NGX_RTMP_STAT_L(",\"append\":true"); + } else { + NGX_RTMP_STAT_L(",\"append\":false"); + } + + if(lracf->lock_file) { + NGX_RTMP_STAT_L(",\"lock_file\":true"); + } else { + NGX_RTMP_STAT_L(",\"lock_file\":false"); + } + + if(lracf->notify) { + NGX_RTMP_STAT_L(",\"notify\":true"); + } else { + NGX_RTMP_STAT_L(",\"notify\":false"); + } + + NGX_RTMP_STAT_L(",\"path\":\""); + NGX_RTMP_STAT_S(&lracf->path); + + NGX_RTMP_STAT_L("\",\"max_size\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", (ngx_uint_t)lracf->max_size) - buf); + + NGX_RTMP_STAT_L(",\"max_frames\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", (ngx_uint_t)lracf->max_frames) - buf); + + NGX_RTMP_STAT_L(",\"interval\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", lracf->interval) - buf); + + NGX_RTMP_STAT_L(",\"suffix\":\""); + NGX_RTMP_STAT_S(&lracf->suffix); + NGX_RTMP_STAT_L("\"}"); + } + } + + if(slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("]}"); + } +} + + +static void +ngx_rtmp_stat_application(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_rtmp_core_srv_conf_t *cscf, ngx_rtmp_core_app_conf_t *cacf) +{ + ngx_rtmp_stat_loc_conf_t *slcf; + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT_ES(&cacf->name); + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("{"); + NGX_RTMP_STAT_L("\"name\":\""); + NGX_RTMP_STAT_ES(&cacf->name); + NGX_RTMP_STAT_L("\""); + } + + if (slcf->stat & NGX_RTMP_STAT_LIVE) { + ngx_rtmp_stat_live(r, lll, + cacf->app_conf[ngx_rtmp_live_module.ctx_index]); + } + + if (slcf->stat & NGX_RTMP_STAT_PLAY) { + ngx_rtmp_stat_play(r, lll, + cacf->app_conf[ngx_rtmp_play_module.ctx_index]); + } + + if (slcf->stat & NGX_RTMP_STAT_RECORD) { + ngx_rtmp_stat_application_recorders(r, lll, cacf); + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("}"); + } +} + + +static void +ngx_rtmp_stat_server(ngx_http_request_t *r, ngx_chain_t ***lll, + ngx_rtmp_core_srv_conf_t *cscf) +{ + u_char buf[NGX_INT_T_LEN]; + size_t n; + ngx_rtmp_core_app_conf_t **cacf; + ngx_rtmp_stat_loc_conf_t *slcf; + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", cscf->port) - buf); + NGX_RTMP_STAT_L("\r\n"); + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", cscf->index) - buf); + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("{"); + NGX_RTMP_STAT_L("\"port\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", cscf->port) - buf); + NGX_RTMP_STAT_L(","); + NGX_RTMP_STAT_L("\"server_index\":"); + NGX_RTMP_STAT(buf, ngx_snprintf(buf, sizeof(buf), + "%ui", cscf->index) - buf); + NGX_RTMP_STAT_L(","); + } + +#ifdef NGX_RTMP_POOL_DEBUG + ngx_rtmp_stat_dump_pool(r, lll, cscf->pool); + if (slcf->format & NGX_RTMP_STAT_FORMAT_JSON) { + NGX_RTMP_STAT_L(","); + } +#endif + + if (slcf->format & NGX_RTMP_STAT_FORMAT_JSON) { + NGX_RTMP_STAT_L("\"applications\":["); + } + + cacf = cscf->applications.elts; + for (n = 0; n < cscf->applications.nelts; ++n, ++cacf) { + ngx_rtmp_stat_application(r, lll, cscf, *cacf); + + if (slcf->format & NGX_RTMP_STAT_FORMAT_JSON && + n < cscf->applications.nelts - 1) + { + NGX_RTMP_STAT_L(","); + } + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("]}"); + } +} + + +static ngx_int_t +ngx_rtmp_stat_handler(ngx_http_request_t *r) +{ + ngx_rtmp_stat_loc_conf_t *slcf; + ngx_rtmp_core_main_conf_t *cmcf; + ngx_rtmp_core_srv_conf_t **cscf; + ngx_chain_t *cl, *l, **ll, ***lll; + size_t n; + off_t len; + static u_char tbuf[NGX_TIME_T_LEN]; + static u_char nbuf[NGX_INT_T_LEN]; + + slcf = ngx_http_get_module_loc_conf(r, ngx_rtmp_stat_module); + if (slcf->stat == 0) { + return NGX_DECLINED; + } + + if (slcf->format == 0) { + slcf->format = NGX_RTMP_STAT_FORMAT_XML; + } + + cmcf = ngx_rtmp_core_main_conf; + if (cmcf == NULL) { + goto error; + } + + cl = NULL; + ll = &cl; + lll = ≪ + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + if (slcf->stylesheet.len) { + NGX_RTMP_STAT_L("stylesheet); + NGX_RTMP_STAT_L("\" ?>\r\n"); + } + + NGX_RTMP_STAT_L("\r\n"); + + #ifdef NGINX_VERSION + NGX_RTMP_STAT_L("" NGINX_VERSION "\r\n"); + #endif + + #ifdef NGINX_RTMP_VERSION + NGX_RTMP_STAT_L("" + NGINX_RTMP_VERSION + "\r\n"); + #endif + + #ifdef NGX_COMPILER + NGX_RTMP_STAT_L("" NGX_COMPILER "\r\n"); + #endif + NGX_RTMP_STAT_L("" __DATE__ " " __TIME__ "\r\n"); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(nbuf, ngx_snprintf(nbuf, sizeof(nbuf), + "%ui", (ngx_uint_t) ngx_getpid()) - nbuf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(tbuf, ngx_snprintf(tbuf, sizeof(tbuf), + "%T", ngx_cached_time->sec - start_time) - tbuf); + NGX_RTMP_STAT_L("\r\n"); + + NGX_RTMP_STAT_L(""); + NGX_RTMP_STAT(nbuf, ngx_snprintf(nbuf, sizeof(nbuf), + "%ui", ngx_rtmp_naccepted) - nbuf); + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("{\"http-flv\":{"); + + #ifdef NGINX_VERSION + NGX_RTMP_STAT_L("\"nginx_version\":\"" NGINX_VERSION "\","); + #endif + + #ifdef NGINX_RTMP_VERSION + NGX_RTMP_STAT_L("\"nginx_http_flv_version\":\"" + NGINX_RTMP_VERSION + "\","); + #endif + + #ifdef NGX_COMPILER + NGX_RTMP_STAT_L("\"compiler\":\"" NGX_COMPILER "\","); + #endif + NGX_RTMP_STAT_L("\"built\":\"" __DATE__ " " __TIME__ "\","); + + NGX_RTMP_STAT_L("\"pid\":"); + NGX_RTMP_STAT(nbuf, ngx_snprintf(nbuf, sizeof(nbuf), + "%ui", (ngx_uint_t) ngx_getpid()) - nbuf); + NGX_RTMP_STAT_L(","); + + NGX_RTMP_STAT_L("\"uptime\":"); + NGX_RTMP_STAT(tbuf, ngx_snprintf(tbuf, sizeof(tbuf), + "%T", ngx_cached_time->sec - start_time) - tbuf); + NGX_RTMP_STAT_L(","); + + NGX_RTMP_STAT_L("\"naccepted\":"); + NGX_RTMP_STAT(nbuf, ngx_snprintf(nbuf, sizeof(nbuf), + "%ui", ngx_rtmp_naccepted) - nbuf); + NGX_RTMP_STAT_L(","); + } + + ngx_rtmp_stat_bw(r, lll, &ngx_rtmp_bw_in, "in", NGX_RTMP_STAT_BW_BYTES); + ngx_rtmp_stat_bw(r, lll, &ngx_rtmp_bw_out, "out", NGX_RTMP_STAT_BW_BYTES); + + if (slcf->format & NGX_RTMP_STAT_FORMAT_JSON) { + NGX_RTMP_STAT_L("\"servers\":["); + } + + cscf = cmcf->servers.elts; + for (n = 0; n < cmcf->servers.nelts; ++n, ++cscf) { + ngx_rtmp_stat_server(r, lll, *cscf); + if (n < cmcf->servers.nelts - 1 && + slcf->format & NGX_RTMP_STAT_FORMAT_JSON) + { + NGX_RTMP_STAT_L(","); + } + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + NGX_RTMP_STAT_L("\r\n"); + } else { + NGX_RTMP_STAT_L("]}}"); + } + + len = 0; + for (l = cl; l; l = l->next) { + len += (l->buf->last - l->buf->pos); + } + + if (slcf->format & NGX_RTMP_STAT_FORMAT_XML) { + ngx_str_set(&r->headers_out.content_type, "text/xml"); + } else { + ngx_str_set(&r->headers_out.content_type, "application/json"); + } + r->headers_out.content_length_n = len; + r->headers_out.status = NGX_HTTP_OK; + ngx_http_send_header(r); + (*ll)->buf->last_buf = 1; + return ngx_http_output_filter(r, cl); + +error: + r->headers_out.status = NGX_HTTP_INTERNAL_SERVER_ERROR; + r->headers_out.content_length_n = 0; + return ngx_http_send_header(r); +} + + +static void * +ngx_rtmp_stat_create_loc_conf(ngx_conf_t *cf) +{ + ngx_rtmp_stat_loc_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_stat_loc_conf_t)); + if (conf == NULL) { + return NULL; + } + + conf->stat = 0; + + return conf; +} + + +static char * +ngx_rtmp_stat_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_stat_loc_conf_t *prev = parent; + ngx_rtmp_stat_loc_conf_t *conf = child; + + ngx_conf_merge_bitmask_value(conf->stat, prev->stat, 0); + ngx_conf_merge_str_value(conf->stylesheet, prev->stylesheet, ""); + + return NGX_CONF_OK; +} + + +static char * +ngx_rtmp_stat(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_core_loc_conf_t *clcf; + + clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); + clcf->handler = ngx_rtmp_stat_handler; + + return ngx_conf_set_bitmask_slot(cf, cmd, conf); +} + + +static ngx_int_t +ngx_rtmp_stat_postconfiguration(ngx_conf_t *cf) +{ + start_time = ngx_cached_time->sec; + + return NGX_OK; +} diff --git a/ngx_http_flv_module/ngx_rtmp_streams.h b/ngx_http_flv_module/ngx_rtmp_streams.h new file mode 100644 index 0000000..d957b8e --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_streams.h @@ -0,0 +1,19 @@ + +/* + * Copyright (C) Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_STREAMS_H_INCLUDED_ +#define _NGX_RTMP_STREAMS_H_INCLUDED_ + + +#define NGX_RTMP_MSID 1 + +#define NGX_RTMP_CSID_AMF_INI 3 +#define NGX_RTMP_CSID_AMF 5 +#define NGX_RTMP_CSID_AUDIO 6 +#define NGX_RTMP_CSID_VIDEO 7 + + +#endif /* _NGX_RTMP_STREAMS_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_variables.c b/ngx_http_flv_module/ngx_rtmp_variables.c new file mode 100644 index 0000000..9632329 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_variables.c @@ -0,0 +1,1387 @@ + +/* + * Copyright (C) Igor Sysoev + * Copyright (C) Nginx, Inc. + * Copyright (C) Winshining + */ + + +#include +#include +#include +#include "ngx_rtmp.h" + +static ngx_rtmp_variable_t *ngx_rtmp_add_prefix_variable(ngx_conf_t *cf, + ngx_str_t *name, ngx_uint_t flags); + +static ngx_int_t ngx_rtmp_variable_request(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); + +static ngx_int_t ngx_rtmp_variable_argument(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +#if (NGX_HAVE_TCP_INFO) +static ngx_int_t ngx_rtmp_variable_tcpinfo(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +#endif + +static ngx_int_t ngx_rtmp_variable_host(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_binary_remote_addr(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_remote_addr(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_remote_port(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_server_addr(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_server_port(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static void ngx_rtmp_variable_set_args(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_is_args(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_server_name(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_bytes_sent(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_request_time(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_request_id(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); + +static ngx_int_t ngx_rtmp_variable_connection(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); + +static ngx_int_t ngx_rtmp_variable_nginx_version(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_hostname(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_pid(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_rtmp_variable_msec(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); + + +static ngx_rtmp_variable_t ngx_rtmp_core_variables[] = { + { ngx_string("host"), NULL, ngx_rtmp_variable_host, 0, 0, 0 }, + + { ngx_string("binary_remote_addr"), NULL, + ngx_rtmp_variable_binary_remote_addr, 0, 0, 0 }, + + { ngx_string("remote_addr"), NULL, ngx_rtmp_variable_remote_addr, 0, 0, 0 }, + + { ngx_string("remote_port"), NULL, ngx_rtmp_variable_remote_port, 0, 0, 0 }, + + { ngx_string("server_addr"), NULL, ngx_rtmp_variable_server_addr, 0, 0, 0 }, + + { ngx_string("server_port"), NULL, ngx_rtmp_variable_server_port, 0, 0, 0 }, + + { ngx_string("request_uri"), NULL, ngx_rtmp_variable_request, + offsetof(ngx_rtmp_session_t, unparsed_uri), 0, 0 }, + + { ngx_string("uri"), NULL, ngx_rtmp_variable_request, + offsetof(ngx_rtmp_session_t, uri), + NGX_RTMP_VAR_NOCACHEABLE, 0 }, + + { ngx_string("query_string"), NULL, ngx_rtmp_variable_request, + offsetof(ngx_rtmp_session_t, args), + NGX_RTMP_VAR_NOCACHEABLE, 0 }, + + { ngx_string("args"), + ngx_rtmp_variable_set_args, + ngx_rtmp_variable_request, + offsetof(ngx_rtmp_session_t, args), + NGX_RTMP_VAR_CHANGEABLE|NGX_RTMP_VAR_NOCACHEABLE, 0 }, + + { ngx_string("is_args"), NULL, ngx_rtmp_variable_is_args, + 0, NGX_RTMP_VAR_NOCACHEABLE, 0 }, + + { ngx_string("server_name"), NULL, ngx_rtmp_variable_server_name, 0, 0, 0 }, + + { ngx_string("bytes_sent"), NULL, ngx_rtmp_variable_bytes_sent, + 0, 0, 0 }, + + { ngx_string("request_time"), NULL, ngx_rtmp_variable_request_time, + 0, NGX_RTMP_VAR_NOCACHEABLE, 0 }, + + { ngx_string("request_id"), NULL, + ngx_rtmp_variable_request_id, + 0, 0, 0 }, + + { ngx_string("connection"), NULL, + ngx_rtmp_variable_connection, 0, 0, 0 }, + + { ngx_string("nginx_version"), NULL, ngx_rtmp_variable_nginx_version, + 0, 0, 0 }, + + { ngx_string("hostname"), NULL, ngx_rtmp_variable_hostname, + 0, 0, 0 }, + + { ngx_string("pid"), NULL, ngx_rtmp_variable_pid, + 0, 0, 0 }, + + { ngx_string("msec"), NULL, ngx_rtmp_variable_msec, + 0, NGX_RTMP_VAR_NOCACHEABLE, 0 }, + +#if (NGX_HAVE_TCP_INFO) + { ngx_string("tcpinfo_rtt"), NULL, ngx_rtmp_variable_tcpinfo, + 0, NGX_RTMP_VAR_NOCACHEABLE, 0 }, + + { ngx_string("tcpinfo_rttvar"), NULL, ngx_rtmp_variable_tcpinfo, + 1, NGX_RTMP_VAR_NOCACHEABLE, 0 }, + + { ngx_string("tcpinfo_snd_cwnd"), NULL, ngx_rtmp_variable_tcpinfo, + 2, NGX_RTMP_VAR_NOCACHEABLE, 0 }, + + { ngx_string("tcpinfo_rcv_space"), NULL, ngx_rtmp_variable_tcpinfo, + 3, NGX_RTMP_VAR_NOCACHEABLE, 0 }, +#endif + + { ngx_string("arg_"), NULL, ngx_rtmp_variable_argument, + 0, NGX_RTMP_VAR_NOCACHEABLE|NGX_RTMP_VAR_PREFIX, 0 }, + + { ngx_null_string, NULL, NULL, 0, 0, 0 } +}; + + +ngx_rtmp_variable_value_t ngx_rtmp_variable_null_value = + ngx_rtmp_variable(""); +ngx_rtmp_variable_value_t ngx_rtmp_variable_true_value = + ngx_rtmp_variable("1"); + + +static ngx_uint_t ngx_rtmp_variable_depth = 100; + + +ngx_rtmp_variable_t * +ngx_rtmp_add_variable(ngx_conf_t *cf, ngx_str_t *name, ngx_uint_t flags) +{ + ngx_int_t rc; + ngx_uint_t i; + ngx_hash_key_t *key; + ngx_rtmp_variable_t *v; + ngx_rtmp_core_main_conf_t *cmcf; + + if (name->len == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid variable name \"$\""); + return NULL; + } + + if (flags & NGX_RTMP_VAR_PREFIX) { + return ngx_rtmp_add_prefix_variable(cf, name, flags); + } + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + key = cmcf->variables_keys->keys.elts; + for (i = 0; i < cmcf->variables_keys->keys.nelts; i++) { + if (name->len != key[i].key.len + || ngx_strncasecmp(name->data, key[i].key.data, name->len) != 0) + { + continue; + } + + v = key[i].value; + + if (!(v->flags & NGX_RTMP_VAR_CHANGEABLE)) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the duplicate \"%V\" variable", name); + return NULL; + } + + v->flags &= flags | ~NGX_RTMP_VAR_WEAK; + + return v; + } + + v = ngx_palloc(cf->pool, sizeof(ngx_rtmp_variable_t)); + if (v == NULL) { + return NULL; + } + + v->name.len = name->len; + v->name.data = ngx_pnalloc(cf->pool, name->len); + if (v->name.data == NULL) { + return NULL; + } + + ngx_strlow(v->name.data, name->data, name->len); + + v->set_handler = NULL; + v->get_handler = NULL; + v->data = 0; + v->flags = flags; + v->index = 0; + + rc = ngx_hash_add_key(cmcf->variables_keys, &v->name, v, 0); + + if (rc == NGX_ERROR) { + return NULL; + } + + if (rc == NGX_BUSY) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "conflicting variable name \"%V\"", name); + return NULL; + } + + return v; +} + + +static ngx_rtmp_variable_t * +ngx_rtmp_add_prefix_variable(ngx_conf_t *cf, ngx_str_t *name, ngx_uint_t flags) +{ + ngx_uint_t i; + ngx_rtmp_variable_t *v; + ngx_rtmp_core_main_conf_t *cmcf; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + v = cmcf->prefix_variables.elts; + for (i = 0; i < cmcf->prefix_variables.nelts; i++) { + if (name->len != v[i].name.len + || ngx_strncasecmp(name->data, v[i].name.data, name->len) != 0) + { + continue; + } + + v = &v[i]; + + if (!(v->flags & NGX_RTMP_VAR_CHANGEABLE)) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "the duplicate \"%V\" variable", name); + return NULL; + } + + v->flags &= flags | ~NGX_RTMP_VAR_WEAK; + + return v; + } + + v = ngx_array_push(&cmcf->prefix_variables); + if (v == NULL) { + return NULL; + } + + v->name.len = name->len; + v->name.data = ngx_pnalloc(cf->pool, name->len); + if (v->name.data == NULL) { + return NULL; + } + + ngx_strlow(v->name.data, name->data, name->len); + + v->set_handler = NULL; + v->get_handler = NULL; + v->data = 0; + v->flags = flags; + v->index = 0; + + return v; +} + + +ngx_int_t +ngx_rtmp_get_variable_index(ngx_conf_t *cf, ngx_str_t *name) +{ + ngx_uint_t i; + ngx_rtmp_variable_t *v; + ngx_rtmp_core_main_conf_t *cmcf; + + if (name->len == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid variable name \"$\""); + return NGX_ERROR; + } + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + v = cmcf->variables.elts; + + if (v == NULL) { + if (ngx_array_init(&cmcf->variables, cf->pool, 4, + sizeof(ngx_rtmp_variable_t)) + != NGX_OK) + { + return NGX_ERROR; + } + + } else { + for (i = 0; i < cmcf->variables.nelts; i++) { + if (name->len != v[i].name.len + || ngx_strncasecmp(name->data, v[i].name.data, name->len) != 0) + { + continue; + } + + return i; + } + } + + v = ngx_array_push(&cmcf->variables); + if (v == NULL) { + return NGX_ERROR; + } + + v->name.len = name->len; + v->name.data = ngx_pnalloc(cf->pool, name->len); + if (v->name.data == NULL) { + return NGX_ERROR; + } + + ngx_strlow(v->name.data, name->data, name->len); + + v->set_handler = NULL; + v->get_handler = NULL; + v->data = 0; + v->flags = 0; + v->index = cmcf->variables.nelts - 1; + + return v->index; +} + + +ngx_rtmp_variable_value_t * +ngx_rtmp_get_indexed_variable(ngx_rtmp_session_t *s, ngx_uint_t index) +{ + ngx_rtmp_variable_t *v; + ngx_rtmp_core_main_conf_t *cmcf; + + cmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_core_module); + + if (cmcf->variables.nelts <= index) { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, 0, + "unknown variable index: %ui", index); + return NULL; + } + + if (s->variables[index].not_found || s->variables[index].valid) { + return &s->variables[index]; + } + + v = cmcf->variables.elts; + + if (ngx_rtmp_variable_depth == 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "cycle while evaluating variable \"%V\"", + &v[index].name); + return NULL; + } + + ngx_rtmp_variable_depth--; + + if (v[index].get_handler(s, &s->variables[index], v[index].data) + == NGX_OK) + { + ngx_rtmp_variable_depth++; + + if (v[index].flags & NGX_RTMP_VAR_NOCACHEABLE) { + s->variables[index].no_cacheable = 1; + } + + return &s->variables[index]; + } + + ngx_rtmp_variable_depth++; + + s->variables[index].valid = 0; + s->variables[index].not_found = 1; + + return NULL; +} + + +ngx_rtmp_variable_value_t * +ngx_rtmp_get_flushed_variable(ngx_rtmp_session_t *s, ngx_uint_t index) +{ + ngx_rtmp_variable_value_t *v; + + v = &s->variables[index]; + + if (v->valid || v->not_found) { + if (!v->no_cacheable) { + return v; + } + + v->valid = 0; + v->not_found = 0; + } + + return ngx_rtmp_get_indexed_variable(s, index); +} + + +ngx_rtmp_variable_value_t * +ngx_rtmp_get_variable(ngx_rtmp_session_t *s, ngx_str_t *name, ngx_uint_t key) +{ + size_t len; + ngx_uint_t i, n; + ngx_rtmp_variable_t *v; + ngx_rtmp_variable_value_t *vv; + ngx_rtmp_core_main_conf_t *cmcf; + + cmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_core_module); + + v = ngx_hash_find(&cmcf->variables_hash, key, name->data, name->len); + + if (v) { + if (v->flags & NGX_RTMP_VAR_INDEXED) { + return ngx_rtmp_get_flushed_variable(s, v->index); + } + + if (ngx_rtmp_variable_depth == 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "cycle while evaluating variable \"%V\"", name); + return NULL; + } + + ngx_rtmp_variable_depth--; + + vv = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_variable_value_t)); + + if (vv && v->get_handler(s, vv, v->data) == NGX_OK) { + ngx_rtmp_variable_depth++; + return vv; + } + + ngx_rtmp_variable_depth++; + return NULL; + } + + vv = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_variable_value_t)); + if (vv == NULL) { + return NULL; + } + + len = 0; + + v = cmcf->prefix_variables.elts; + n = cmcf->prefix_variables.nelts; + + for (i = 0; i < cmcf->prefix_variables.nelts; i++) { + if (name->len >= v[i].name.len && name->len > len + && ngx_strncmp(name->data, v[i].name.data, v[i].name.len) == 0) + { + len = v[i].name.len; + n = i; + } + } + + if (n != cmcf->prefix_variables.nelts) { + if (v[n].get_handler(s, vv, (uintptr_t) name) == NGX_OK) { + return vv; + } + + return NULL; + } + + vv->not_found = 1; + + return vv; +} + + +static ngx_int_t +ngx_rtmp_variable_request(ngx_rtmp_session_t *s, ngx_rtmp_variable_value_t *v, + uintptr_t data) +{ + ngx_str_t *str; + + str = (ngx_str_t *) ((char *) s + data); + + if (str->data) { + v->len = str->len; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = str->data; + + } else { + v->not_found = 1; + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_arg(ngx_rtmp_session_t *s, u_char *name, size_t len, ngx_str_t *value) +{ + u_char *p, *last; + + if (s->args.len == 0) { + return NGX_DECLINED; + } + + p = s->args.data; + last = p + s->args.len; + + for ( /* void */ ; p < last; p++) { + + /* we need '=' after name, so drop one char from last */ + + p = ngx_strlcasestrn(p, last - 1, name, len - 1); + + if (p == NULL) { + return NGX_DECLINED; + } + + if ((p == s->args.data || *(p - 1) == '&') && *(p + len) == '=') { + + value->data = p + len + 1; + + p = ngx_strlchr(p, last, '&'); + + if (p == NULL) { + p = s->args.data + s->args.len; + } + + value->len = p - value->data; + + return NGX_OK; + } + } + + return NGX_DECLINED; +} + + +static ngx_int_t +ngx_rtmp_variable_argument(ngx_rtmp_session_t *s, ngx_rtmp_variable_value_t *v, + uintptr_t data) +{ + ngx_str_t *name = (ngx_str_t *) data; + + u_char *arg; + size_t len; + ngx_str_t value; + + len = name->len - (sizeof("arg_") - 1); + arg = name->data + sizeof("arg_") - 1; + + if (ngx_rtmp_arg(s, arg, len, &value) != NGX_OK) { + v->not_found = 1; + return NGX_OK; + } + + v->data = value.data; + v->len = value.len; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + return NGX_OK; +} + + +#if (NGX_HAVE_TCP_INFO) + +static ngx_int_t +ngx_rtmp_variable_tcpinfo(ngx_rtmp_session_t *s, ngx_rtmp_variable_value_t *v, + uintptr_t data) +{ + struct tcp_info ti; + socklen_t len; + uint32_t value; + + len = sizeof(struct tcp_info); + if (getsockopt(s->connection->fd, IPPROTO_TCP, TCP_INFO, &ti, &len) == -1) { + v->not_found = 1; + return NGX_OK; + } + + v->data = ngx_pnalloc(s->connection->pool, NGX_INT32_LEN); + if (v->data == NULL) { + return NGX_ERROR; + } + + switch (data) { + case 0: + value = ti.tcpi_rtt; + break; + + case 1: + value = ti.tcpi_rttvar; + break; + + case 2: + value = ti.tcpi_snd_cwnd; + break; + + case 3: + value = ti.tcpi_rcv_space; + break; + + /* suppress warning */ + default: + value = 0; + break; + } + + v->len = ngx_sprintf(v->data, "%uD", value) - v->data; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + return NGX_OK; +} + +#endif + + +static ngx_int_t +ngx_rtmp_variable_host(ngx_rtmp_session_t *s, ngx_rtmp_variable_value_t *v, + uintptr_t data) +{ + v->len = s->host_end - s->host_start; + v->data = s->host_start; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_binary_remote_addr(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + struct sockaddr_in *sin; +#if (NGX_HAVE_INET6) + struct sockaddr_in6 *sin6; +#endif + + switch (s->connection->sockaddr->sa_family) { + +#if (NGX_HAVE_INET6) + case AF_INET6: + sin6 = (struct sockaddr_in6 *) s->connection->sockaddr; + + v->len = sizeof(struct in6_addr); + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = sin6->sin6_addr.s6_addr; + + break; +#endif + + default: /* AF_INET */ + sin = (struct sockaddr_in *) s->connection->sockaddr; + + v->len = sizeof(in_addr_t); + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = (u_char *) &sin->sin_addr; + + break; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_remote_addr(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + v->len = s->connection->addr_text.len; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = s->connection->addr_text.data; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_remote_port(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + ngx_uint_t port; + + v->len = 0; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + v->data = ngx_pnalloc(s->connection->pool, sizeof("65535") - 1); + if (v->data == NULL) { + return NGX_ERROR; + } + + port = ngx_inet_get_port(s->connection->sockaddr); + + if (port > 0 && port < 65536) { + v->len = ngx_sprintf(v->data, "%ui", port) - v->data; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_server_addr(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + ngx_str_t str; + u_char addr[NGX_SOCKADDR_STRLEN]; + + str.len = NGX_SOCKADDR_STRLEN; + str.data = addr; + + if (ngx_connection_local_sockaddr(s->connection, &str, 0) != NGX_OK) { + return NGX_ERROR; + } + + str.data = ngx_pnalloc(s->connection->pool, str.len); + if (str.data == NULL) { + return NGX_ERROR; + } + + ngx_memcpy(str.data, addr, str.len); + + v->len = str.len; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = str.data; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_server_port(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + ngx_uint_t port; + + v->len = 0; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + if (ngx_connection_local_sockaddr(s->connection, NULL, 0) != NGX_OK) { + return NGX_ERROR; + } + + v->data = ngx_pnalloc(s->connection->pool, sizeof("65535") - 1); + if (v->data == NULL) { + return NGX_ERROR; + } + + port = ngx_inet_get_port(s->connection->local_sockaddr); + + if (port > 0 && port < 65536) { + v->len = ngx_sprintf(v->data, "%ui", port) - v->data; + } + + return NGX_OK; +} + + +static void +ngx_rtmp_variable_set_args(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + s->args.len = v->len; + s->args.data = v->data; + s->valid_unparsed_uri = 0; +} + + +static ngx_int_t +ngx_rtmp_variable_is_args(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + if (s->args.len == 0) { + v->len = 0; + v->data = NULL; + return NGX_OK; + } + + v->len = 1; + v->data = (u_char *) "?"; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_server_name(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + ngx_rtmp_core_srv_conf_t *cscf; + + cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); + + v->len = cscf->server_name.len; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = cscf->server_name.data; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_bytes_sent(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + u_char *p; + + p = ngx_pnalloc(s->connection->pool, NGX_OFF_T_LEN); + if (p == NULL) { + return NGX_ERROR; + } + + v->len = ngx_sprintf(p, "%O", s->connection->sent) - p; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = p; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_request_time(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + u_char *p; + ngx_time_t *tp; + ngx_msec_int_t ms; + + p = ngx_pnalloc(s->connection->pool, NGX_TIME_T_LEN + 4); + if (p == NULL) { + return NGX_ERROR; + } + + tp = ngx_timeofday(); + + ms = (ngx_msec_int_t) + ((tp->sec - s->start_sec) * 1000 + (tp->msec - s->start_msec)); + ms = ngx_max(ms, 0); + + v->len = ngx_sprintf(p, "%T.%03M", (time_t) ms / 1000, ms % 1000) - p; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = p; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_request_id(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + u_char *id; + +#if (NGX_OPENSSL) + u_char random_bytes[16]; +#endif + + id = ngx_pnalloc(s->connection->pool, 32); + if (id == NULL) { + return NGX_ERROR; + } + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + v->len = 32; + v->data = id; + +#if (NGX_OPENSSL) + + if (RAND_bytes(random_bytes, 16) == 1) { + ngx_hex_dump(id, random_bytes, 16); + return NGX_OK; + } + + ngx_ssl_error(NGX_LOG_ERR, s->connection->log, 0, "RAND_bytes() failed"); + +#endif + + ngx_sprintf(id, "%08xD%08xD%08xD%08xD", + (uint32_t) ngx_random(), (uint32_t) ngx_random(), + (uint32_t) ngx_random(), (uint32_t) ngx_random()); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_connection(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + u_char *p; + + p = ngx_pnalloc(s->connection->pool, NGX_ATOMIC_T_LEN); + if (p == NULL) { + return NGX_ERROR; + } + + v->len = ngx_sprintf(p, "%uA", s->connection->number) - p; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = p; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_nginx_version(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + v->len = sizeof(NGINX_VERSION) - 1; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = (u_char *) NGINX_VERSION; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_hostname(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + v->len = ngx_cycle->hostname.len; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = ngx_cycle->hostname.data; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_pid(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + u_char *p; + + p = ngx_pnalloc(s->connection->pool, NGX_INT64_LEN); + if (p == NULL) { + return NGX_ERROR; + } + + v->len = ngx_sprintf(p, "%P", ngx_pid) - p; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = p; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_variable_msec(ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data) +{ + u_char *p; + ngx_time_t *tp; + + p = ngx_pnalloc(s->connection->pool, NGX_TIME_T_LEN + 4); + if (p == NULL) { + return NGX_ERROR; + } + + tp = ngx_timeofday(); + + v->len = ngx_sprintf(p, "%T.%03M", tp->sec, tp->msec) - p; + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + v->data = p; + + return NGX_OK; +} + + +void * +ngx_rtmp_map_find(ngx_rtmp_session_t *s, ngx_rtmp_map_t *map, ngx_str_t *match) +{ + void *value; + u_char *low; + size_t len; + ngx_uint_t key; + + len = match->len; + + if (len) { + low = ngx_pnalloc(s->connection->pool, len); + if (low == NULL) { + return NULL; + } + + } else { + low = NULL; + } + + key = ngx_hash_strlow(low, match->data, len); + + value = ngx_hash_find_combined(&map->hash, key, low, len); + if (value) { + return value; + } + +#if (NGX_PCRE) + + if (len && map->nregex) { + ngx_int_t n; + ngx_uint_t i; + ngx_rtmp_map_regex_t *reg; + + reg = map->regex; + + for (i = 0; i < map->nregex; i++) { + + n = ngx_rtmp_regex_exec(s, reg[i].regex, match); + + if (n == NGX_OK) { + return reg[i].value; + } + + if (n == NGX_DECLINED) { + continue; + } + + /* NGX_ERROR */ + + return NULL; + } + } + +#endif + + return NULL; +} + + +#if (NGX_PCRE) + +static ngx_int_t +ngx_rtmp_variable_not_found(ngx_rtmp_session_t *s, ngx_rtmp_variable_value_t *v, + uintptr_t data) +{ + v->not_found = 1; + return NGX_OK; +} + + +ngx_rtmp_regex_t * +ngx_rtmp_regex_compile(ngx_conf_t *cf, ngx_regex_compile_t *rc) +{ + u_char *p; + size_t size; + ngx_str_t name; + ngx_uint_t i, n; + ngx_rtmp_variable_t *v; + ngx_rtmp_regex_t *re; + ngx_rtmp_regex_variable_t *rv; + ngx_rtmp_core_main_conf_t *cmcf; + + rc->pool = cf->pool; + + if (ngx_regex_compile(rc) != NGX_OK) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "%V", &rc->err); + return NULL; + } + + re = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_regex_t)); + if (re == NULL) { + return NULL; + } + + re->regex = rc->regex; + re->ncaptures = rc->captures; + re->name = rc->pattern; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + cmcf->ncaptures = ngx_max(cmcf->ncaptures, re->ncaptures); + + n = (ngx_uint_t) rc->named_captures; + + if (n == 0) { + return re; + } + + rv = ngx_palloc(rc->pool, n * sizeof(ngx_rtmp_regex_variable_t)); + if (rv == NULL) { + return NULL; + } + + re->variables = rv; + re->nvariables = n; + + size = rc->name_size; + p = rc->names; + + for (i = 0; i < n; i++) { + rv[i].capture = 2 * ((p[0] << 8) + p[1]); + + name.data = &p[2]; + name.len = ngx_strlen(name.data); + + v = ngx_rtmp_add_variable(cf, &name, NGX_RTMP_VAR_CHANGEABLE); + if (v == NULL) { + return NULL; + } + + rv[i].index = ngx_rtmp_get_variable_index(cf, &name); + if (rv[i].index == NGX_ERROR) { + return NULL; + } + + v->get_handler = ngx_rtmp_variable_not_found; + + p += size; + } + + return re; +} + + +ngx_int_t +ngx_rtmp_regex_exec(ngx_rtmp_session_t *s, ngx_rtmp_regex_t *re, ngx_str_t *str) +{ + ngx_int_t rc, index; + ngx_uint_t i, n, len; + ngx_rtmp_variable_value_t *vv; + ngx_rtmp_core_main_conf_t *cmcf; + + cmcf = ngx_rtmp_get_module_main_conf(s, ngx_rtmp_core_module); + + if (re->ncaptures) { + len = cmcf->ncaptures; + + if (s->captures == NULL) { + s->captures = ngx_palloc(s->connection->pool, len * sizeof(int)); + if (s->captures == NULL) { + return NGX_ERROR; + } + } + + } else { + len = 0; + } + + rc = ngx_regex_exec(re->regex, str, s->captures, len); + + if (rc == NGX_REGEX_NO_MATCHED) { + return NGX_DECLINED; + } + + if (rc < 0) { + ngx_log_error(NGX_LOG_ALERT, s->connection->log, 0, + ngx_regex_exec_n " failed: %i on \"%V\" using \"%V\"", + rc, str, &re->name); + return NGX_ERROR; + } + + for (i = 0; i < re->nvariables; i++) { + + n = re->variables[i].capture; + index = re->variables[i].index; + vv = &s->variables[index]; + + vv->len = s->captures[n + 1] - s->captures[n]; + vv->valid = 1; + vv->no_cacheable = 0; + vv->not_found = 0; + vv->data = &str->data[s->captures[n]]; + +#if (NGX_DEBUG) + { + ngx_rtmp_variable_t *v; + + v = cmcf->variables.elts; + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "http regex set $%V to \"%v\"", &v[index].name, vv); + } +#endif + } + + s->ncaptures = rc * 2; + s->captures_data = str->data; + + return NGX_OK; +} + +#endif + + +ngx_int_t +ngx_rtmp_variables_add_core_vars(ngx_conf_t *cf) +{ + ngx_rtmp_variable_t *cv, *v; + ngx_rtmp_core_main_conf_t *cmcf; + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + cmcf->variables_keys = ngx_pcalloc(cf->temp_pool, + sizeof(ngx_hash_keys_arrays_t)); + if (cmcf->variables_keys == NULL) { + return NGX_ERROR; + } + + cmcf->variables_keys->pool = cf->pool; + cmcf->variables_keys->temp_pool = cf->pool; + + if (ngx_hash_keys_array_init(cmcf->variables_keys, NGX_HASH_SMALL) + != NGX_OK) + { + return NGX_ERROR; + } + + if (ngx_array_init(&cmcf->prefix_variables, cf->pool, 8, + sizeof(ngx_rtmp_variable_t)) + != NGX_OK) + { + return NGX_ERROR; + } + + for (cv = ngx_rtmp_core_variables; cv->name.len; cv++) { + v = ngx_rtmp_add_variable(cf, &cv->name, cv->flags); + if (v == NULL) { + return NGX_ERROR; + } + + *v = *cv; + } + + return NGX_OK; +} + + +ngx_int_t +ngx_rtmp_variables_init_vars(ngx_conf_t *cf) +{ + size_t len; + ngx_uint_t i, n; + ngx_hash_key_t *key; + ngx_hash_init_t hash; + ngx_rtmp_variable_t *v, *av, *pv; + ngx_rtmp_core_main_conf_t *cmcf; + + /* set the handlers for the indexed http variables */ + + cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module); + + v = cmcf->variables.elts; + pv = cmcf->prefix_variables.elts; + key = cmcf->variables_keys->keys.elts; + + for (i = 0; i < cmcf->variables.nelts; i++) { + + for (n = 0; n < cmcf->variables_keys->keys.nelts; n++) { + + av = key[n].value; + + if (v[i].name.len == key[n].key.len + && ngx_strncmp(v[i].name.data, key[n].key.data, v[i].name.len) + == 0) + { + v[i].get_handler = av->get_handler; + v[i].data = av->data; + + av->flags |= NGX_RTMP_VAR_INDEXED; + v[i].flags = av->flags; + + av->index = i; + + if (av->get_handler == NULL + || (av->flags & NGX_RTMP_VAR_WEAK)) + { + break; + } + + goto next; + } + } + + len = 0; + av = NULL; + + for (n = 0; n < cmcf->prefix_variables.nelts; n++) { + if (v[i].name.len >= pv[n].name.len && v[i].name.len > len + && ngx_strncmp(v[i].name.data, pv[n].name.data, pv[n].name.len) + == 0) + { + av = &pv[n]; + len = pv[n].name.len; + } + } + + if (av) { + v[i].get_handler = av->get_handler; + v[i].data = (uintptr_t) &v[i].name; + v[i].flags = av->flags; + + goto next; + } + + if (v[i].get_handler == NULL) { + ngx_log_error(NGX_LOG_EMERG, cf->log, 0, + "unknown \"%V\" variable", &v[i].name); + + return NGX_ERROR; + } + + next: + continue; + } + + + for (n = 0; n < cmcf->variables_keys->keys.nelts; n++) { + av = key[n].value; + + if (av->flags & NGX_RTMP_VAR_NOHASH) { + key[n].key.data = NULL; + } + } + + + hash.hash = &cmcf->variables_hash; + hash.key = ngx_hash_key; + hash.max_size = cmcf->variables_hash_max_size; + hash.bucket_size = cmcf->variables_hash_bucket_size; + hash.name = "rtmp_variables_hash"; + hash.pool = cf->pool; + hash.temp_pool = NULL; + + if (ngx_hash_init(&hash, cmcf->variables_keys->keys.elts, + cmcf->variables_keys->keys.nelts) + != NGX_OK) + { + return NGX_ERROR; + } + + cmcf->variables_keys = NULL; + + return NGX_OK; +} + diff --git a/ngx_http_flv_module/ngx_rtmp_variables.h b/ngx_http_flv_module/ngx_rtmp_variables.h new file mode 100644 index 0000000..9730966 --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_variables.h @@ -0,0 +1,111 @@ + +/* + * Copyright (C) Igor Sysoev + * Copyright (C) Nginx, Inc. + * Copyright (C) Winshining + */ + + +#ifndef _NGX_RTMP_VARIABLES_H_INCLUDED_ +#define _NGX_RTMP_VARIABLES_H_INCLUDED_ + + +#include +#include + + +typedef ngx_variable_value_t ngx_rtmp_variable_value_t; + +#define ngx_rtmp_variable(v) { sizeof(v) - 1, 1, 0, 0, 0, (u_char *) v } + +typedef struct ngx_rtmp_variable_s ngx_rtmp_variable_t; + +typedef void (*ngx_rtmp_set_variable_pt) (ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); +typedef ngx_int_t (*ngx_rtmp_get_variable_pt) (ngx_rtmp_session_t *s, + ngx_rtmp_variable_value_t *v, uintptr_t data); + + +#define NGX_RTMP_VAR_CHANGEABLE 1 +#define NGX_RTMP_VAR_NOCACHEABLE 2 +#define NGX_RTMP_VAR_INDEXED 4 +#define NGX_RTMP_VAR_NOHASH 8 +#define NGX_RTMP_VAR_WEAK 16 +#define NGX_RTMP_VAR_PREFIX 32 + + +struct ngx_rtmp_variable_s { + ngx_str_t name; /* must be first to build the hash */ + ngx_rtmp_set_variable_pt set_handler; + ngx_rtmp_get_variable_pt get_handler; + uintptr_t data; + ngx_uint_t flags; + ngx_uint_t index; +}; + + +ngx_rtmp_variable_t *ngx_rtmp_add_variable(ngx_conf_t *cf, ngx_str_t *name, + ngx_uint_t flags); +ngx_int_t ngx_rtmp_get_variable_index(ngx_conf_t *cf, ngx_str_t *name); +ngx_rtmp_variable_value_t *ngx_rtmp_get_indexed_variable(ngx_rtmp_session_t *s, + ngx_uint_t index); +ngx_rtmp_variable_value_t *ngx_rtmp_get_flushed_variable(ngx_rtmp_session_t *s, + ngx_uint_t index); + +ngx_rtmp_variable_value_t *ngx_rtmp_get_variable(ngx_rtmp_session_t *s, + ngx_str_t *name, ngx_uint_t key); + + +#if (NGX_PCRE) + +typedef struct { + ngx_uint_t capture; + ngx_int_t index; +} ngx_rtmp_regex_variable_t; + + +typedef struct { + ngx_regex_t *regex; + ngx_uint_t ncaptures; + ngx_rtmp_regex_variable_t *variables; + ngx_uint_t nvariables; + ngx_str_t name; +} ngx_rtmp_regex_t; + + +typedef struct { + ngx_rtmp_regex_t *regex; + void *value; +} ngx_rtmp_map_regex_t; + + +ngx_rtmp_regex_t *ngx_rtmp_regex_compile(ngx_conf_t *cf, + ngx_regex_compile_t *rc); +ngx_int_t ngx_rtmp_regex_exec(ngx_rtmp_session_t *s, ngx_rtmp_regex_t *re, + ngx_str_t *str); + +#endif + + +typedef struct { + ngx_hash_combined_t hash; +#if (NGX_PCRE) + ngx_rtmp_map_regex_t *regex; + ngx_uint_t nregex; +#endif +} ngx_rtmp_map_t; + + +void *ngx_rtmp_map_find(ngx_rtmp_session_t *s, ngx_rtmp_map_t *map, + ngx_str_t *match); + + +ngx_int_t ngx_rtmp_variables_add_core_vars(ngx_conf_t *cf); +ngx_int_t ngx_rtmp_variables_init_vars(ngx_conf_t *cf); + + +extern ngx_rtmp_variable_value_t ngx_rtmp_variable_null_value; +extern ngx_rtmp_variable_value_t ngx_rtmp_variable_true_value; + + +#endif /* _NGX_RTMP_VARIABLES_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/ngx_rtmp_version.h b/ngx_http_flv_module/ngx_rtmp_version.h new file mode 100644 index 0000000..7dc5c5d --- /dev/null +++ b/ngx_http_flv_module/ngx_rtmp_version.h @@ -0,0 +1,16 @@ + +/* + * Copyright (C) Roman Arutyunyan + * Copyright (C) Winshining + */ + + +#ifndef _NGX_RTMP_VERSION_H_INCLUDED_ +#define _NGX_RTMP_VERSION_H_INCLUDED_ + + +#define nginx_rtmp_version 1002012 +#define NGINX_RTMP_VERSION "1.2.12" + + +#endif /* _NGX_RTMP_VERSION_H_INCLUDED_ */ diff --git a/ngx_http_flv_module/samples/flv.js.png b/ngx_http_flv_module/samples/flv.js.png new file mode 100644 index 0000000..69fc85e Binary files /dev/null and b/ngx_http_flv_module/samples/flv.js.png differ diff --git a/ngx_http_flv_module/samples/jwplayer_vlc.png b/ngx_http_flv_module/samples/jwplayer_vlc.png new file mode 100644 index 0000000..26f410f Binary files /dev/null and b/ngx_http_flv_module/samples/jwplayer_vlc.png differ diff --git a/ngx_http_flv_module/stat.xsl b/ngx_http_flv_module/stat.xsl new file mode 100644 index 0000000..90b2753 --- /dev/null +++ b/ngx_http_flv_module/stat.xsl @@ -0,0 +1,546 @@ + + + + + + + + + + + + + HTTP-FLV statistics + + + +
+ Generated by + nginx-http-flv-module , + nginx , + pid , + built   + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
HTTP-FLV#clientsRecordersServerVideoAudioIn bytesOut bytesIn bits/sOut bits/sStateTime
Accepted: lists#countportindexcodecbits/ssizefpscodecbits/sfreqchan + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + live streams + + + + + + + + + + + + vod streams + + + + + + + + + + + Recorders + + + + var d=document.getElementById('---recorders'); + d.style.display=d.style.display=='none'?'':'none'; + return false; + + config + + + + + + + + ---recorders + + + + + + + + + + + + + + + + +
IdPathSuffixFlagsMax SizeMax FramesIntervalUniqueAppendLock FileNotify
+ + +
+ + + + + + + [DEFAULT] + + + + + + video
+ audio
+ manual + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + on + off + + + + + + + + #cccccc + #dddddd + + + + + + var d=document.getElementById('---'); + d.style.display=d.style.display=='none'?'':'none'; + return false + + + + [EMPTY] + + + + + - + - + + + +    + + + + + + + + + + + + + + + +   + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + --- + + + + + + + + + + + + + + + + +
IdStateAddressFlash versionPage URLSWF URLDroppedTimestampA-VTime
+ + + +
+ + + + + + + + + + + + + + +
RecorderStateEpochTime ShiftFile NameTimeSizeFrames
+
+ + + + + + #cccccc + #eeeeee + + + + + + [DEFAULT] + + + + + recording + idle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + d + + + + h + + + + m + + + s + + + + + + + + + + + + + T + + + G + + + M + + K + + + + b + B + + /s + + + + + + + + + + + + + d + + + + h + + + + m + + + + s + + + + Unset + + + + + active + idle + + + + + + + publishing + playing + + + + + + + + + #cccccc + #eeeeee + + + + + + + + http://apps.db.ripe.net/search/query.html?searchtext= + + whois + + + + + + + + + + + + + + + + + + + + + + + + + + publishing + + + + active + + + + x + + +
diff --git a/ngx_http_flv_module/test/README.md b/ngx_http_flv_module/test/README.md new file mode 100644 index 0000000..32653ff --- /dev/null +++ b/ngx_http_flv_module/test/README.md @@ -0,0 +1,11 @@ +# RTMP tests + +nginx.conf is sample config for testing nginx-rtmp. +Please update paths in it before using. + +RTMP port: 1935, HTTP port: 8080 + +* http://localhost:8080/ - play myapp/mystream with JWPlayer +* http://localhost:8080/record.html - capture myapp/mystream from webcam with old JWPlayer +* http://localhost:8080/rtmp-publisher/player.html - play myapp/mystream with the test flash applet +* http://localhost:8080/rtmp-publisher/publisher.html - capture myapp/mystream with the test flash applet diff --git a/ngx_http_flv_module/test/dump.sh b/ngx_http_flv_module/test/dump.sh new file mode 100644 index 0000000..d67ac15 --- /dev/null +++ b/ngx_http_flv_module/test/dump.sh @@ -0,0 +1 @@ +rtmpdump -v -r "rtmp://localhost/myapp/mystream" diff --git a/ngx_http_flv_module/test/ffstream.sh b/ngx_http_flv_module/test/ffstream.sh new file mode 100644 index 0000000..007e950 --- /dev/null +++ b/ngx_http_flv_module/test/ffstream.sh @@ -0,0 +1 @@ +ffmpeg -loglevel verbose -re -i ~/movie.avi -f flv rtmp://localhost/myapp/mystream diff --git a/ngx_http_flv_module/test/nginx.conf b/ngx_http_flv_module/test/nginx.conf new file mode 100644 index 0000000..35288b1 --- /dev/null +++ b/ngx_http_flv_module/test/nginx.conf @@ -0,0 +1,66 @@ +worker_processes 1; + +error_log logs/error.log debug; + +events { + worker_connections 1024; +} + +rtmp { + server { + listen 1935; + + application myapp { + live on; + + #record keyframes; + #record_path /tmp; + #record_max_size 128K; + #record_interval 30s; + #record_suffix .this.is.flv; + + #on_publish http://localhost:8080/publish; + #on_play http://localhost:8080/play; + #on_record_done http://localhost:8080/record_done; + } + } +} + +http { + server { + listen 8080; + + location /stat { + rtmp_stat all; + rtmp_stat_stylesheet stat.xsl; + } + + location /stat.xsl { + root /path/to/nginx-rtmp-module/; + } + + location /control { + rtmp_control all; + } + + #location /publish { + # return 201; + #} + + #location /play { + # return 202; + #} + + #location /record_done { + # return 203; + #} + + location /rtmp-publisher { + root /path/to/nginx-rtmp-module/test; + } + + location / { + root /path/to/nginx-rtmp-module/test/www; + } + } +} diff --git a/ngx_http_flv_module/test/play.sh b/ngx_http_flv_module/test/play.sh new file mode 100644 index 0000000..3552679 --- /dev/null +++ b/ngx_http_flv_module/test/play.sh @@ -0,0 +1 @@ +ffplay -loglevel verbose "rtmp://localhost/myapp/mystream" diff --git a/ngx_http_flv_module/test/rtmp-publisher/README.md b/ngx_http_flv_module/test/rtmp-publisher/README.md new file mode 100644 index 0000000..c31a2ac --- /dev/null +++ b/ngx_http_flv_module/test/rtmp-publisher/README.md @@ -0,0 +1,15 @@ +# RTMP Publisher + +Simple RTMP publisher. + +Edit the following flashvars in publisher.html & player.html to suite your needs. + +streamer: RTMP endpoint +file: live stream name + +## Compile + +Install flex sdk http://www.adobe.com/devnet/flex/flex-sdk-download.html + + mxmlc RtmpPublisher.mxml + mxmlc RtmpPlayer.mxml diff --git a/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayer.mxml b/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayer.mxml new file mode 100644 index 0000000..d068348 --- /dev/null +++ b/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayer.mxml @@ -0,0 +1,69 @@ + + + + + + + + + + + + diff --git a/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayer.swf b/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayer.swf new file mode 100644 index 0000000..61cdd91 Binary files /dev/null and b/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayer.swf differ diff --git a/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayerLight.mxml b/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayerLight.mxml new file mode 100644 index 0000000..6c77e71 --- /dev/null +++ b/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayerLight.mxml @@ -0,0 +1,101 @@ + + + + + + + + + + diff --git a/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayerLight.swf b/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayerLight.swf new file mode 100644 index 0000000..e01a1c1 Binary files /dev/null and b/ngx_http_flv_module/test/rtmp-publisher/RtmpPlayerLight.swf differ diff --git a/ngx_http_flv_module/test/rtmp-publisher/RtmpPublisher.mxml b/ngx_http_flv_module/test/rtmp-publisher/RtmpPublisher.mxml new file mode 100644 index 0000000..a337c68 --- /dev/null +++ b/ngx_http_flv_module/test/rtmp-publisher/RtmpPublisher.mxml @@ -0,0 +1,86 @@ + + + + + + + + + + + diff --git a/ngx_http_flv_module/test/rtmp-publisher/RtmpPublisher.swf b/ngx_http_flv_module/test/rtmp-publisher/RtmpPublisher.swf new file mode 100644 index 0000000..e175def Binary files /dev/null and b/ngx_http_flv_module/test/rtmp-publisher/RtmpPublisher.swf differ diff --git a/ngx_http_flv_module/test/rtmp-publisher/player.html b/ngx_http_flv_module/test/rtmp-publisher/player.html new file mode 100644 index 0000000..81da0ae --- /dev/null +++ b/ngx_http_flv_module/test/rtmp-publisher/player.html @@ -0,0 +1,22 @@ + + + + RTMP Player + + + + +
+

Flash not installed

+
+ + diff --git a/ngx_http_flv_module/test/rtmp-publisher/publisher.html b/ngx_http_flv_module/test/rtmp-publisher/publisher.html new file mode 100644 index 0000000..cad587e --- /dev/null +++ b/ngx_http_flv_module/test/rtmp-publisher/publisher.html @@ -0,0 +1,19 @@ + + + + RTMP Publisher + + + + +
+

Flash not installed

+
+ + diff --git a/ngx_http_flv_module/test/rtmp-publisher/swfobject.js b/ngx_http_flv_module/test/rtmp-publisher/swfobject.js new file mode 100644 index 0000000..8eafe9d --- /dev/null +++ b/ngx_http_flv_module/test/rtmp-publisher/swfobject.js @@ -0,0 +1,4 @@ +/* SWFObject v2.2 + is released under the MIT License +*/ +var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;abPlay | Record +
+ + +
Loading the player ...
+ diff --git a/ngx_http_flv_module/test/www/jwplayer/jwplayer.flash.swf b/ngx_http_flv_module/test/www/jwplayer/jwplayer.flash.swf new file mode 100644 index 0000000..c4fc624 Binary files /dev/null and b/ngx_http_flv_module/test/www/jwplayer/jwplayer.flash.swf differ diff --git a/ngx_http_flv_module/test/www/jwplayer/jwplayer.js b/ngx_http_flv_module/test/www/jwplayer/jwplayer.js new file mode 100644 index 0000000..f354c7a --- /dev/null +++ b/ngx_http_flv_module/test/www/jwplayer/jwplayer.js @@ -0,0 +1,71 @@ +"undefined"==typeof jwplayer&&(jwplayer=function(d){if(jwplayer.api)return jwplayer.api.selectPlayer(d)},jwplayer.version="6.3.3242",jwplayer.vid=document.createElement("video"),jwplayer.audio=document.createElement("audio"),jwplayer.source=document.createElement("source"),function(d){function a(b){return function(){return e(b)}}var h=document,g=window,c=navigator,b=d.utils=function(){};b.exists=function(b){switch(typeof b){case "string":return 0g||g>c)}else c=void 0;if(c)return a;c=e.substring(0,e.indexOf("://")+3);var g=e.substring(c.length,e.indexOf("/",c.length+1)),k;0===a.indexOf("/")?k=a.split("/"):(k=e.split("?")[0],k=k.substring(c.length+g.length+1,k.lastIndexOf("/")),k=k.split("/").concat(a.split("/"))); +for(var f=[],d=0;de&&0>c&&(!a||!isNaN(a))?k.CDN:k.RELATIVE}};b.getPluginName=function(a){return a.replace(/^(.*\/)?([^-]*)-?.*\.(swf|js)$/,"$2")};b.getPluginVersion=function(a){return a.replace(/[^-]*-?([^\.]*).*$/,"$1")}; +b.isYouTube=function(a){return-1=e.length&&(e[1]=0);for(var k=a.strToLongs(g.encode(b).slice(0,16)),l=e.length,d=e[l-1],n=e[0],q,j=Math.floor(6+52/l),f=0;0>>2&3;for(var r=0;r>>5^n<<2)+(n>>>3^d<<4)^(f^n)+(k[r&3^q]^d),d=e[r]+=d}e=a.longsToStr(e);return h.encode(e)};a.decrypt=function(c,b){if(0==c.length)return"";for(var e=a.strToLongs(h.decode(c)),k=a.strToLongs(g.encode(b).slice(0,16)),d=e.length, +m=e[d-1],n=e[0],q,j=2654435769*Math.floor(6+52/d);0!=j;){q=j>>>2&3;for(var f=d-1;0<=f;f--)m=e[0>>5^n<<2)+(n>>>3^m<<4)^(j^n)+(k[f&3^q]^m),n=e[f]-=m;j-=2654435769}e=a.longsToStr(e);e=e.replace(/\0+$/,"");return g.decode(e)};a.strToLongs=function(a){for(var b=Array(Math.ceil(a.length/4)),e=0;e>>8&255,a[e]>>>16&255,a[e]>>>24&255);return b.join("")};var h={code:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\x3d",encode:function(a,b){var e,k,d,m,n=[],q="",j,f,r=h.code;f=("undefined"==typeof b?0:b)?g.encode(a):a;j=f.length%3;if(0j++;)q+="\x3d",f+="\x00";for(j=0;j>18&63,k=m>>12&63,d=m>>6&63,m&=63,n[j/3]=r.charAt(e)+r.charAt(k)+r.charAt(d)+ +r.charAt(m);n=n.join("");return n=n.slice(0,n.length-q.length)+q},decode:function(a,b){b="undefined"==typeof b?!1:b;var e,k,d,m,n,q=[],j,f=h.code;j=b?g.decode(a):a;for(var r=0;r>>16&255,k=d>>>8&255,d&=255,q[r/4]=String.fromCharCode(e,k,d),64==n&&(q[r/4]=String.fromCharCode(e,k)),64==m&&(q[r/4]=String.fromCharCode(e));m=q.join("");return b?g.decode(m):m}}, +g={encode:function(a){a=a.replace(/[\u0080-\u07ff]/g,function(a){a=a.charCodeAt(0);return String.fromCharCode(192|a>>6,128|a&63)});return a=a.replace(/[\u0800-\uffff]/g,function(a){a=a.charCodeAt(0);return String.fromCharCode(224|a>>12,128|a>>6&63,128|a&63)})},decode:function(a){a=a.replace(/[\u00e0-\u00ef][\u0080-\u00bf][\u0080-\u00bf]/g,function(a){a=(a.charCodeAt(0)&15)<<12|(a.charCodeAt(1)&63)<<6|a.charCodeAt(2)&63;return String.fromCharCode(a)});return a=a.replace(/[\u00c0-\u00df][\u0080-\u00bf]/g, +function(a){a=(a.charCodeAt(0)&31)<<6|a.charCodeAt(1)&63;return String.fromCharCode(a)})}}}(jwplayer.utils),function(d){d.events={COMPLETE:"COMPLETE",ERROR:"ERROR",API_READY:"jwplayerAPIReady",JWPLAYER_READY:"jwplayerReady",JWPLAYER_FULLSCREEN:"jwplayerFullscreen",JWPLAYER_RESIZE:"jwplayerResize",JWPLAYER_ERROR:"jwplayerError",JWPLAYER_MEDIA_BEFOREPLAY:"jwplayerMediaBeforePlay",JWPLAYER_MEDIA_BEFORECOMPLETE:"jwplayerMediaBeforeComplete",JWPLAYER_COMPONENT_SHOW:"jwplayerComponentShow",JWPLAYER_COMPONENT_HIDE:"jwplayerComponentHide", +JWPLAYER_MEDIA_BUFFER:"jwplayerMediaBuffer",JWPLAYER_MEDIA_BUFFER_FULL:"jwplayerMediaBufferFull",JWPLAYER_MEDIA_ERROR:"jwplayerMediaError",JWPLAYER_MEDIA_LOADED:"jwplayerMediaLoaded",JWPLAYER_MEDIA_COMPLETE:"jwplayerMediaComplete",JWPLAYER_MEDIA_SEEK:"jwplayerMediaSeek",JWPLAYER_MEDIA_TIME:"jwplayerMediaTime",JWPLAYER_MEDIA_VOLUME:"jwplayerMediaVolume",JWPLAYER_MEDIA_META:"jwplayerMediaMeta",JWPLAYER_MEDIA_MUTE:"jwplayerMediaMute",JWPLAYER_MEDIA_LEVELS:"jwplayerMediaLevels",JWPLAYER_MEDIA_LEVEL_CHANGED:"jwplayerMediaLevelChanged", +JWPLAYER_CAPTIONS_CHANGED:"jwplayerCaptionsChanged",JWPLAYER_CAPTIONS_LIST:"jwplayerCaptionsList",JWPLAYER_PLAYER_STATE:"jwplayerPlayerState",state:{BUFFERING:"BUFFERING",IDLE:"IDLE",PAUSED:"PAUSED",PLAYING:"PLAYING"},JWPLAYER_PLAYLIST_LOADED:"jwplayerPlaylistLoaded",JWPLAYER_PLAYLIST_ITEM:"jwplayerPlaylistItem",JWPLAYER_PLAYLIST_COMPLETE:"jwplayerPlaylistComplete",JWPLAYER_DISPLAY_CLICK:"jwplayerViewClick",JWPLAYER_CONTROLS:"jwplayerViewControls",JWPLAYER_INSTREAM_CLICK:"jwplayerInstreamClicked", +JWPLAYER_INSTREAM_DESTROYED:"jwplayerInstreamDestroyed"}}(jwplayer),function(d){var a=jwplayer.utils;d.eventdispatcher=function(d,g){var c,b;this.resetEventListeners=function(){c={};b=[]};this.resetEventListeners();this.addEventListener=function(b,k,d){try{a.exists(c[b])||(c[b]=[]),"string"==a.typeOf(k)&&(k=(new Function("return "+k))()),c[b].push({listener:k,count:d})}catch(g){a.log("error",g)}return!1};this.removeEventListener=function(b,d){if(c[b]){try{for(var g=0;gparseFloat(d.version)))m=!0,n="Incompatible player version",b()}0==e&&b()}}var k=a.loaderstatus.NEW,l=!1,m=!1,n,q=c,j=new h.eventdispatcher;a.extend(this,j);this.setupPlugins=function(b,e,c){var f={length:0,plugins:{}},d=0,k={},h=g.getPlugins(),l;for(l in e.plugins){var j=a.getPluginName(l),m=h[j],B=m.getFlashPath(), +n=m.getJS(),q=m.getURL();B&&(f.plugins[B]=a.extend({},e.plugins[l]),f.plugins[B].pluginmode=m.getPluginmode(),f.length++);try{if(n&&e.plugins&&e.plugins[q]){var v=document.createElement("div");v.id=b.id+"_"+j;v.style.position="absolute";v.style.top=0;v.style.zIndex=d+10;k[j]=m.getNewInstance(b,a.extend({},e.plugins[q]),v);d++;b.onReady(c(k[j],v,!0));b.onResize(c(k[j],v))}}catch(C){a.log("ERROR: Failed to load "+j+".")}}b.plugins=k;return f};this.load=function(){if(!(a.exists(c)&&"object"!=a.typeOf(c))){k= +a.loaderstatus.LOADING;for(var b in c)if(a.exists(b)){var d=g.addPlugin(b);d.addEventListener(h.COMPLETE,e);d.addEventListener(h.ERROR,f)}d=g.getPlugins();for(b in d)d[b].load()}e()};var f=this.pluginFailed=function(){m||(m=!0,n="File not found",b())};this.getStatus=function(){return k}}}(jwplayer),function(d){d.playlist=function(a){var h=[];if("array"==d.utils.typeOf(a))for(var g=0;gm.playlist.length&&(0==m.playlist.length||!m.playlist[0].sources||0==m.playlist[0].sources.length))k();else if(s.getStatus()==a.loaderstatus.COMPLETE){for(var d=0;df)return j.sendEvent(h.ERROR,{message:"Flash version must be 10.0 or greater"}), +!1;var d,p=a.extend({},e);c.id+"_wrapper"==c.parentNode.id?document.getElementById(c.id+"_wrapper"):(d=document.createElement("div"),d.id=c.id+"_wrapper",d.style.position="relative",d.style.width=a.styleDimension(p.width),d.style.height=a.styleDimension(p.height),c.parentNode.replaceChild(d,c),d.appendChild(c));d=k.setupPlugins(l,p,n);0=p.height?"transparent":"opaque";for(var s="height width modes events primary base fallback volume".split(" "),u=0;u + Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis + This software is released under the MIT License +*/ +var swfobject=function(){var b="undefined",Q="object",n="Shockwave Flash",p="ShockwaveFlash.ShockwaveFlash",P="application/x-shockwave-flash",m="SWFObjectExprInst",j=window,K=document,T=navigator,o=[],N=[],i=[],d=[],J,Z=null,M=null,l=null,e=false,A=false;var h=function(){var v=typeof K.getElementById!=b&&typeof K.getElementsByTagName!=b&&typeof K.createElement!=b,AC=[0,0,0],x=null;if(typeof T.plugins!=b&&typeof T.plugins[n]==Q){x=T.plugins[n].description;if(x&&!(typeof T.mimeTypes!=b&&T.mimeTypes[P]&&!T.mimeTypes[P].enabledPlugin)){x=x.replace(/^.*\s+(\S+\s+\S+$)/,"$1");AC[0]=parseInt(x.replace(/^(.*)\..*$/,"$1"),10);AC[1]=parseInt(x.replace(/^.*\.(.*)\s.*$/,"$1"),10);AC[2]=/r/.test(x)?parseInt(x.replace(/^.*r(.*)$/,"$1"),10):0}}else{if(typeof j.ActiveXObject!=b){var y=null,AB=false;try{y=new ActiveXObject(p+".7")}catch(t){try{y=new ActiveXObject(p+".6");AC=[6,0,21];y.AllowScriptAccess="always"}catch(t){if(AC[0]==6){AB=true}}if(!AB){try{y=new ActiveXObject(p)}catch(t){}}}if(!AB&&y){try{x=y.GetVariable("$version");if(x){x=x.split(" ")[1].split(",");AC=[parseInt(x[0],10),parseInt(x[1],10),parseInt(x[2],10)]}}catch(t){}}}}var AD=T.userAgent.toLowerCase(),r=T.platform.toLowerCase(),AA=/webkit/.test(AD)?parseFloat(AD.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,q=false,z=r?/win/.test(r):/win/.test(AD),w=r?/mac/.test(r):/mac/.test(AD);/*@cc_on q=true;@if(@_win32)z=true;@elif(@_mac)w=true;@end@*/return{w3cdom:v,pv:AC,webkit:AA,ie:q,win:z,mac:w}}();var L=function(){if(!h.w3cdom){return }f(H);if(h.ie&&h.win){try{K.write(" + + + + + + + diff --git a/ngx_http_geoip2_module/LICENSE b/ngx_http_geoip2_module/LICENSE new file mode 100644 index 0000000..fdc13a7 --- /dev/null +++ b/ngx_http_geoip2_module/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014, Lee Valentine +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ngx_http_geoip2_module/README.md b/ngx_http_geoip2_module/README.md new file mode 100644 index 0000000..ee14e89 --- /dev/null +++ b/ngx_http_geoip2_module/README.md @@ -0,0 +1,152 @@ +Description +=========== + +**ngx_http_geoip2_module** - creates variables with values from the maxmind geoip2 databases based on the client IP (default) or from a specific variable (supports both IPv4 and IPv6) + +The module now supports nginx streams and can be used in the same way the http module can be used. + +## Installing +First install [libmaxminddb](https://github.com/maxmind/libmaxminddb) as described in its [README.md +file](https://github.com/maxmind/libmaxminddb/blob/main/README.md#installing-from-a-tarball). + +#### Download nginx source +``` +wget http://nginx.org/download/nginx-VERSION.tar.gz +tar zxvf nginx-VERSION.tar.gz +cd nginx-VERSION +``` + +##### To build as a dynamic module (nginx 1.9.11+): +``` +./configure --with-compat --add-dynamic-module=/path/to/ngx_http_geoip2_module +make modules +``` + +This will produce ```objs/ngx_http_geoip2_module.so```. It can be copied to your nginx module path manually if you wish. + +Add the following line to your nginx.conf: +``` +load_module modules/ngx_http_geoip2_module.so; +``` + +##### To build as a static module: +``` +./configure --add-module=/path/to/ngx_http_geoip2_module +make +make install +``` + +##### If you need stream support, make sure to compile with stream: +``` +./configure --add-dynamic-module=/path/to/ngx_http_geoip2_module --with-stream +OR +./configure --add-module=/path/to/ngx_http_geoip2_module --with-stream +``` + + +## Download Maxmind GeoLite2 Database (optional) +The free GeoLite2 databases are available from [Maxminds website](http://dev.maxmind.com/geoip/geoip2/geolite2/) (requires signing up) + +## Example Usage: +``` +http { + ... + geoip2 /etc/maxmind-country.mmdb { + auto_reload 5m; + $geoip2_metadata_country_build metadata build_epoch; + $geoip2_data_country_code default=US source=$variable_with_ip country iso_code; + $geoip2_data_country_name country names en; + } + + geoip2 /etc/maxmind-city.mmdb { + $geoip2_data_city_name default=London city names en; + } + .... + + fastcgi_param COUNTRY_CODE $geoip2_data_country_code; + fastcgi_param COUNTRY_NAME $geoip2_data_country_name; + fastcgi_param CITY_NAME $geoip2_data_city_name; + .... +} + +stream { + ... + geoip2 /etc/maxmind-country.mmdb { + $geoip2_data_country_code default=US source=$remote_addr country iso_code; + } + ... +} +``` + +##### Metadata: +Retrieve metadata regarding the geoip database. +``` +$variable_name metadata +``` +Available fields: + - build_epoch: the build timestamp of the maxmind database. + - last_check: the last time the database was checked for changes (when using auto_reload) + - last_change: the last time the database was reloaded (when using auto_reload) + +##### Autoreload (default: disabled): +Enabling auto reload will have nginx check the modification time of the database at the specified +interval and reload it if it has changed. +``` +auto_reload +``` + +##### GeoIP: +``` +$variable_name [default= + "iso_code": + "US" + "names": + { + "de": + "USA" + "en": + "United States" + } + } + } + +$ mmdblookup --file /usr/share/GeoIP/GeoIP2-Country.mmdb --ip 8.8.8.8 country names en + + "United States" +``` + +This translates to: + +``` +$country_name "default=United States" source=$remote_addr country names en +``` + +##### Additional Commands: +These commands works the same as the original ngx_http_geoip_module documented here: http://nginx.org/en/docs/http/ngx_http_geoip_module.html#geoip_proxy. + +However, if you provide the `source=$variable_with_ip` option on a variable, these settings will be ignored for that particular variable. + +``` +geoip2_proxy < cidr > +``` +Defines trusted addresses. When a request comes from a trusted address, an address from the "X-Forwarded-For" request header field will be used instead. + +``` +geoip2_proxy_recursive < on | off > +``` +If recursive search is disabled then instead of the original client address that matches one of the trusted addresses, the last address sent in "X-Forwarded-For" will be used. If recursive search is enabled then instead of the original client address that matches one of the trusted addresses, the last non-trusted address sent in "X-Forwarded-For" will be used. diff --git a/ngx_http_geoip2_module/config b/ngx_http_geoip2_module/config new file mode 100644 index 0000000..48bf15d --- /dev/null +++ b/ngx_http_geoip2_module/config @@ -0,0 +1,43 @@ +ngx_feature="MaxmindDB library" +ngx_feature_name= +ngx_feature_run=no +ngx_feature_incs="#include " +ngx_feature_libs=-lmaxminddb +ngx_feature_test="MMDB_s mmdb" +. auto/feature + +ngx_addon_name="ngx_geoip2_module" + +if [ $ngx_found = yes ]; then + if test -n "$ngx_module_link"; then + if [ $HTTP != NO ]; then + ngx_module_type=HTTP + ngx_module_name="ngx_http_geoip2_module" + ngx_module_incs= + ngx_module_deps= + ngx_module_srcs="$ngx_addon_dir/ngx_http_geoip2_module.c" + ngx_module_libs="$ngx_feature_libs" + . auto/module + fi + + nginx_version=`awk '/^#define nginx_version / {print $3}' src/core/nginx.h` + if [ $STREAM != NO -a $nginx_version -gt 1011001 ]; then + ngx_module_type=STREAM + ngx_module_name="ngx_stream_geoip2_module" + ngx_module_incs= + ngx_module_deps= + ngx_module_srcs="$ngx_addon_dir/ngx_stream_geoip2_module.c" + ngx_module_libs="$ngx_feature_libs" + . auto/module + fi + else + HTTP_MODULES="$HTTP_MODULES ngx_http_geoip2_module" + NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_geoip2_module.c" + CORE_LIBS="$CORE_LIBS $ngx_feature_libs" + fi +else + cat << END +$0: error: the geoip2 module requires the maxminddb library. +END + exit 1 +fi diff --git a/ngx_http_geoip2_module/ngx_http_geoip2_module.c b/ngx_http_geoip2_module/ngx_http_geoip2_module.c new file mode 100644 index 0000000..4667796 --- /dev/null +++ b/ngx_http_geoip2_module/ngx_http_geoip2_module.c @@ -0,0 +1,803 @@ +/* + * Copyright (C) Lee Valentine + * + * Based on nginx's 'ngx_http_geoip_module.c' by Igor Sysoev + */ + + +#include +#include +#include + +#include + + +typedef struct { + MMDB_s mmdb; + MMDB_lookup_result_s result; + time_t last_check; + time_t last_change; + time_t check_interval; +#if (NGX_HAVE_INET6) + uint8_t address[16]; +#else + unsigned long address; +#endif + ngx_queue_t queue; +} ngx_http_geoip2_db_t; + +typedef struct { + ngx_queue_t databases; + ngx_array_t *proxies; + ngx_flag_t proxy_recursive; +} ngx_http_geoip2_conf_t; + +typedef struct { + ngx_http_geoip2_db_t *database; + const char **lookup; + ngx_str_t default_value; + ngx_http_complex_value_t source; +} ngx_http_geoip2_ctx_t; + +typedef struct { + ngx_http_geoip2_db_t *database; + ngx_str_t metavalue; +} ngx_http_geoip2_metadata_t; + + +static ngx_int_t ngx_http_geoip2_variable(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_http_geoip2_metadata(ngx_http_request_t *r, + ngx_http_variable_value_t *v, uintptr_t data); +static void *ngx_http_geoip2_create_conf(ngx_conf_t *cf); +static char *ngx_http_geoip2_init_conf(ngx_conf_t *cf, void *conf); +static char *ngx_http_geoip2(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_http_geoip2_parse_config(ngx_conf_t *cf, ngx_command_t *dummy, + void *conf); +static char *ngx_http_geoip2_add_variable(ngx_conf_t *cf, ngx_command_t *dummy, + void *conf); +static char *ngx_http_geoip2_add_variable_geodata(ngx_conf_t *cf, + ngx_http_geoip2_db_t *database); +static char *ngx_http_geoip2_add_variable_metadata(ngx_conf_t *cf, + ngx_http_geoip2_db_t *database); +static char *ngx_http_geoip2_proxy(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static ngx_int_t ngx_http_geoip2_cidr_value(ngx_conf_t *cf, ngx_str_t *net, + ngx_cidr_t *cidr); +static void ngx_http_geoip2_cleanup(void *data); +static ngx_int_t ngx_http_geoip2_init(ngx_conf_t *cf); + + +#define FORMAT(fmt, ...) do { \ + p = ngx_palloc(r->pool, NGX_OFF_T_LEN); \ + if (p == NULL) { \ + return NGX_ERROR; \ + } \ + v->len = ngx_sprintf(p, fmt, __VA_ARGS__) - p; \ + v->data = p; \ +} while (0) + +static ngx_command_t ngx_http_geoip2_commands[] = { + + { ngx_string("geoip2"), + NGX_HTTP_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE1, + ngx_http_geoip2, + NGX_HTTP_MAIN_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("geoip2_proxy"), + NGX_HTTP_MAIN_CONF|NGX_CONF_TAKE1, + ngx_http_geoip2_proxy, + NGX_HTTP_MAIN_CONF_OFFSET, + 0, + NULL }, + + { ngx_string("geoip2_proxy_recursive"), + NGX_HTTP_MAIN_CONF|NGX_CONF_FLAG, + ngx_conf_set_flag_slot, + NGX_HTTP_MAIN_CONF_OFFSET, + offsetof(ngx_http_geoip2_conf_t, proxy_recursive), + NULL }, + + ngx_null_command +}; + + +static ngx_http_module_t ngx_http_geoip2_module_ctx = { + NULL, /* preconfiguration */ + ngx_http_geoip2_init, /* postconfiguration */ + + ngx_http_geoip2_create_conf, /* create main configuration */ + ngx_http_geoip2_init_conf, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + NULL, /* create location configuration */ + NULL /* merge location configuration */ +}; + + +ngx_module_t ngx_http_geoip2_module = { + NGX_MODULE_V1, + &ngx_http_geoip2_module_ctx, /* module context */ + ngx_http_geoip2_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 +}; + + +static ngx_int_t +ngx_http_geoip2_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, + uintptr_t data) +{ + ngx_http_geoip2_ctx_t *geoip2 = (ngx_http_geoip2_ctx_t *) data; + ngx_http_geoip2_db_t *database = geoip2->database; + int mmdb_error; + MMDB_entry_data_s entry_data; + ngx_http_geoip2_conf_t *gcf; + ngx_addr_t addr; +#if defined(nginx_version) && nginx_version >= 1023000 + ngx_table_elt_t *xfwd; +#else + ngx_array_t *xfwd; +#endif + u_char *p; + ngx_str_t val; + +#if (NGX_HAVE_INET6) + uint8_t address[16], *addressp = address; +#else + unsigned long address; +#endif + + if (geoip2->source.value.len > 0) { + if (ngx_http_complex_value(r, &geoip2->source, &val) != NGX_OK) { + goto not_found; + } + + if (ngx_parse_addr(r->pool, &addr, val.data, val.len) != NGX_OK) { + goto not_found; + } + } else { + gcf = ngx_http_get_module_main_conf(r, ngx_http_geoip2_module); + addr.sockaddr = r->connection->sockaddr; + addr.socklen = r->connection->socklen; + +#if defined(nginx_version) && nginx_version >= 1023000 + xfwd = r->headers_in.x_forwarded_for; + + if (xfwd != NULL && gcf->proxies != NULL) { +#else + xfwd = &r->headers_in.x_forwarded_for; + + if (xfwd->nelts > 0 && gcf->proxies != NULL) { +#endif + (void) ngx_http_get_forwarded_addr(r, &addr, xfwd, NULL, + gcf->proxies, gcf->proxy_recursive); + } + } + + switch (addr.sockaddr->sa_family) { + case AF_INET: +#if (NGX_HAVE_INET6) + ngx_memset(addressp, 0, 12); + ngx_memcpy(addressp + 12, &((struct sockaddr_in *) + addr.sockaddr)->sin_addr.s_addr, 4); + break; + + case AF_INET6: + ngx_memcpy(addressp, &((struct sockaddr_in6 *) + addr.sockaddr)->sin6_addr.s6_addr, 16); +#else + address = ((struct sockaddr_in *)addr.sockaddr)->sin_addr.s_addr; +#endif + break; + + default: + goto not_found; + } + +#if (NGX_HAVE_INET6) + if (ngx_memcmp(&address, &database->address, sizeof(address)) + != 0) { +#else + if (address != database->address) { +#endif + memcpy(&database->address, &address, sizeof(address)); + database->result = MMDB_lookup_sockaddr(&database->mmdb, + addr.sockaddr, &mmdb_error); + + if (mmdb_error != MMDB_SUCCESS) { + goto not_found; + } + } + + if (!database->result.found_entry + || MMDB_aget_value(&database->result.entry, &entry_data, + geoip2->lookup) != MMDB_SUCCESS) { + goto not_found; + } + + if (!entry_data.has_data) { + goto not_found; + } + + switch (entry_data.type) { + case MMDB_DATA_TYPE_BOOLEAN: + FORMAT("%d", entry_data.boolean); + break; + case MMDB_DATA_TYPE_UTF8_STRING: + v->len = entry_data.data_size; + v->data = ngx_pnalloc(r->pool, v->len); + if (v->data == NULL) { + return NGX_ERROR; + } + ngx_memcpy(v->data, (u_char *) entry_data.utf8_string, v->len); + break; + case MMDB_DATA_TYPE_BYTES: + v->len = entry_data.data_size; + v->data = ngx_pnalloc(r->pool, v->len); + if (v->data == NULL) { + return NGX_ERROR; + } + ngx_memcpy(v->data, (u_char *) entry_data.bytes, v->len); + break; + case MMDB_DATA_TYPE_FLOAT: + FORMAT("%.5f", entry_data.float_value); + break; + case MMDB_DATA_TYPE_DOUBLE: + FORMAT("%.5f", entry_data.double_value); + break; + case MMDB_DATA_TYPE_UINT16: + FORMAT("%uD", entry_data.uint16); + break; + case MMDB_DATA_TYPE_UINT32: + FORMAT("%uD", entry_data.uint32); + break; + case MMDB_DATA_TYPE_INT32: + FORMAT("%D", entry_data.int32); + break; + case MMDB_DATA_TYPE_UINT64: + FORMAT("%uL", entry_data.uint64); + break; + case MMDB_DATA_TYPE_UINT128: ; +#if MMDB_UINT128_IS_BYTE_ARRAY + uint8_t *val = (uint8_t *)entry_data.uint128; + FORMAT( "0x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x", + val[0], val[1], val[2], val[3], + val[4], val[5], val[6], val[7], + val[8], val[9], val[10], val[11], + val[12], val[13], val[14], val[15]); +#else + mmdb_uint128_t val = entry_data.uint128; + FORMAT("0x%016uxL%016uxL", + (uint64_t) (val >> 64), (uint64_t) val); +#endif + break; + default: + goto not_found; + } + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + return NGX_OK; + +not_found: + if (geoip2->default_value.len > 0) { + v->data = geoip2->default_value.data; + v->len = geoip2->default_value.len; + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + } else { + v->not_found = 1; + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_geoip2_metadata(ngx_http_request_t *r, ngx_http_variable_value_t *v, + uintptr_t data) +{ + ngx_http_geoip2_metadata_t *metadata = (ngx_http_geoip2_metadata_t *) data; + ngx_http_geoip2_db_t *database = metadata->database; + u_char *p; + + if (ngx_strncmp(metadata->metavalue.data, "build_epoch", 11) == 0) { + FORMAT("%uL", database->mmdb.metadata.build_epoch); + } else if (ngx_strncmp(metadata->metavalue.data, "last_check", 10) == 0) { + FORMAT("%T", database->last_check); + } else if (ngx_strncmp(metadata->metavalue.data, "last_change", 11) == 0) { + FORMAT("%T", database->last_change); + } else { + v->not_found = 1; + return NGX_OK; + } + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + return NGX_OK; +} + + +static void * +ngx_http_geoip2_create_conf(ngx_conf_t *cf) +{ + ngx_pool_cleanup_t *cln; + ngx_http_geoip2_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_geoip2_conf_t)); + if (conf == NULL) { + return NULL; + } + + conf->proxy_recursive = NGX_CONF_UNSET; + + cln = ngx_pool_cleanup_add(cf->pool, 0); + if (cln == NULL) { + return NULL; + } + + ngx_queue_init(&conf->databases); + + cln->handler = ngx_http_geoip2_cleanup; + cln->data = conf; + + return conf; +} + + +static char * +ngx_http_geoip2(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_geoip2_conf_t *gcf = conf; + ngx_str_t *value; + int status; + ngx_http_geoip2_db_t *database; + char *rv; + ngx_conf_t save; + ngx_queue_t *q; + + value = cf->args->elts; + + if (value[1].data && value[1].data[0] != '/') { + if (ngx_conf_full_name(cf->cycle, &value[1], 0) != NGX_OK) { + return NGX_CONF_ERROR; + } + } + + if (!ngx_queue_empty(&gcf->databases)) { + for (q = ngx_queue_head(&gcf->databases); + q != ngx_queue_sentinel(&gcf->databases); + q = ngx_queue_next(q)) + { + database = ngx_queue_data(q, ngx_http_geoip2_db_t, queue); + if (ngx_strcmp(value[1].data, database->mmdb.filename) == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "Duplicate GeoIP2 mmdb - %V", &value[1]); + return NGX_CONF_ERROR; + } + } + } + + database = ngx_pcalloc(cf->pool, sizeof(ngx_http_geoip2_db_t)); + if (database == NULL) { + return NGX_CONF_ERROR; + } + + ngx_queue_insert_tail(&gcf->databases, &database->queue); + database->last_check = database->last_change = ngx_time(); + + status = MMDB_open((char *) value[1].data, MMDB_MODE_MMAP, &database->mmdb); + + if (status != MMDB_SUCCESS) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "MMDB_open(\"%V\") failed - %s", &value[1], + MMDB_strerror(status)); + return NGX_CONF_ERROR; + } + + save = *cf; + cf->handler = ngx_http_geoip2_parse_config; + cf->handler_conf = (void *) database; + + rv = ngx_conf_parse(cf, NULL); + *cf = save; + return rv; +} + + +static char * +ngx_http_geoip2_parse_config(ngx_conf_t *cf, ngx_command_t *dummy, void *conf) +{ + ngx_http_geoip2_db_t *database; + ngx_str_t *value; + time_t interval; + + value = cf->args->elts; + + if (value[0].data[0] == '$') { + return ngx_http_geoip2_add_variable(cf, dummy, conf); + } + + if (value[0].len == 11 + && ngx_strncmp(value[0].data, "auto_reload", 11) == 0) { + if ((int) cf->args->nelts != 2) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid number of arguments for auto_reload"); + return NGX_CONF_ERROR; + } + + interval = ngx_parse_time(&value[1], true); + + if (interval == (time_t) NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid interval for auto_reload \"%V\"", + &value[1]); + return NGX_CONF_ERROR; + } + + + database = (ngx_http_geoip2_db_t *) conf; + database->check_interval = interval; + return NGX_CONF_OK; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid setting \"%V\"", &value[0]); + return NGX_CONF_ERROR; +} + + +static char * +ngx_http_geoip2_add_variable(ngx_conf_t *cf, ngx_command_t *dummy, void *conf) +{ + ngx_http_geoip2_db_t *database; + ngx_str_t *value; + int nelts; + + value = cf->args->elts; + + if (value[0].data[0] != '$') { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid variable name \"%V\"", &value[0]); + return NGX_CONF_ERROR; + } + + value[0].len--; + value[0].data++; + + nelts = (int) cf->args->nelts; + database = (ngx_http_geoip2_db_t *) conf; + + if (nelts > 0 && value[1].len == 8 && ngx_strncmp(value[1].data, "metadata", 8) == 0) { + return ngx_http_geoip2_add_variable_metadata(cf, database); + } + + return ngx_http_geoip2_add_variable_geodata(cf, database); +} + + +static char * +ngx_http_geoip2_add_variable_metadata(ngx_conf_t *cf, ngx_http_geoip2_db_t *database) +{ + ngx_http_geoip2_metadata_t *metadata; + ngx_str_t *value, name; + ngx_http_variable_t *var; + + metadata = ngx_pcalloc(cf->pool, sizeof(ngx_http_geoip2_metadata_t)); + if (metadata == NULL) { + return NGX_CONF_ERROR; + } + + value = cf->args->elts; + name = value[0]; + + metadata->database = database; + metadata->metavalue = value[2]; + + var = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGEABLE); + if (var == NULL) { + return NGX_CONF_ERROR; + } + + var->get_handler = ngx_http_geoip2_metadata; + var->data = (uintptr_t) metadata; + + return NGX_CONF_OK; +} + + +static char * +ngx_http_geoip2_add_variable_geodata(ngx_conf_t *cf, ngx_http_geoip2_db_t *database) +{ + ngx_http_geoip2_ctx_t *geoip2; + ngx_http_compile_complex_value_t ccv; + ngx_str_t *value, name, source; + ngx_http_variable_t *var; + int i, nelts, idx; + + geoip2 = ngx_pcalloc(cf->pool, sizeof(ngx_http_geoip2_ctx_t)); + if (geoip2 == NULL) { + return NGX_CONF_ERROR; + } + + geoip2->database = database; + ngx_str_null(&source); + + value = cf->args->elts; + name = value[0]; + + nelts = (int) cf->args->nelts; + idx = 1; + + if (nelts > idx) { + for (i = idx; i < nelts; i++) { + if (ngx_strnstr(value[idx].data, "=", value[idx].len) == NULL) { + break; + } + + if (value[idx].len > 8 && ngx_strncmp(value[idx].data, "default=", 8) == 0) { + if (geoip2->default_value.len > 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "default has already been declared for \"$%V\"", &name); + return NGX_CONF_ERROR; + } + + geoip2->default_value.len = value[idx].len - 8; + geoip2->default_value.data = value[idx].data + 8; + } else if (value[idx].len > 7 && ngx_strncmp(value[idx].data, "source=", 7) == 0) { + if (source.len > 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "source has already been declared for \"$%V\"", &name); + return NGX_CONF_ERROR; + } + + source.len = value[idx].len - 7; + source.data = value[idx].data + 7; + + if (source.data[0] != '$') { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid source variable name \"%V\"", &source); + return NGX_CONF_ERROR; + } + + ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); + ccv.cf = cf; + ccv.value = &source; + ccv.complex_value = &geoip2->source; + + if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "unable to compile \"%V\" for \"$%V\"", &source, &name); + return NGX_CONF_ERROR; + } + } else { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid setting \"%V\" for \"$%V\"", &value[idx], &name); + return NGX_CONF_ERROR; + } + + idx++; + } + } + + var = ngx_http_add_variable(cf, &name, NGX_HTTP_VAR_CHANGEABLE); + if (var == NULL) { + return NGX_CONF_ERROR; + } + + geoip2->lookup = ngx_pcalloc(cf->pool, sizeof(const char *) * + (cf->args->nelts - (idx - 1))); + + if (geoip2->lookup == NULL) { + return NGX_CONF_ERROR; + } + + for (i = idx; i < nelts; i++) { + geoip2->lookup[i - idx] = (char *) value[i].data; + } + geoip2->lookup[i - idx] = NULL; + + var->get_handler = ngx_http_geoip2_variable; + var->data = (uintptr_t) geoip2; + + return NGX_CONF_OK; +} + + +static char * +ngx_http_geoip2_init_conf(ngx_conf_t *cf, void *conf) +{ + ngx_http_geoip2_conf_t *gcf = conf; + ngx_conf_init_value(gcf->proxy_recursive, 0); + return NGX_CONF_OK; +} + + +static char * +ngx_http_geoip2_proxy(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + ngx_http_geoip2_conf_t *gcf = conf; + ngx_str_t *value; + ngx_cidr_t cidr, *c; + + value = cf->args->elts; + + if (ngx_http_geoip2_cidr_value(cf, &value[1], &cidr) != NGX_OK) { + return NGX_CONF_ERROR; + } + + if (gcf->proxies == NULL) { + gcf->proxies = ngx_array_create(cf->pool, 4, sizeof(ngx_cidr_t)); + if (gcf->proxies == NULL) { + return NGX_CONF_ERROR; + } + } + + c = ngx_array_push(gcf->proxies); + if (c == NULL) { + return NGX_CONF_ERROR; + } + + *c = cidr; + + return NGX_CONF_OK; +} + + +static ngx_int_t +ngx_http_geoip2_cidr_value(ngx_conf_t *cf, ngx_str_t *net, ngx_cidr_t *cidr) +{ + ngx_int_t rc; + + if (ngx_strcmp(net->data, "255.255.255.255") == 0) { + cidr->family = AF_INET; + cidr->u.in.addr = 0xffffffff; + cidr->u.in.mask = 0xffffffff; + + return NGX_OK; + } + + rc = ngx_ptocidr(net, cidr); + + if (rc == NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid network \"%V\"", net); + return NGX_ERROR; + } + + if (rc == NGX_DONE) { + ngx_conf_log_error(NGX_LOG_WARN, cf, 0, + "low address bits of %V are meaningless", net); + } + + return NGX_OK; +} + + +static void +ngx_http_geoip2_cleanup(void *data) +{ + ngx_http_geoip2_conf_t *gcf = data; + ngx_queue_t *q; + ngx_http_geoip2_db_t *database; + + while (!ngx_queue_empty(&gcf->databases)) { + q = ngx_queue_head(&gcf->databases); + ngx_queue_remove(q); + database = ngx_queue_data(q, ngx_http_geoip2_db_t, queue); + MMDB_close(&database->mmdb); + } +} + + +static ngx_int_t +ngx_http_geoip2_log_handler(ngx_http_request_t *r) +{ + int status; + MMDB_s tmpdb; + ngx_queue_t *q; + ngx_file_info_t fi; + ngx_http_geoip2_db_t *database; + ngx_http_geoip2_conf_t *gcf; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "geoip2 http log handler"); + + gcf = ngx_http_get_module_main_conf(r, ngx_http_geoip2_module); + + if (ngx_queue_empty(&gcf->databases)) { + return NGX_OK; + } + + for (q = ngx_queue_head(&gcf->databases); + q != ngx_queue_sentinel(&gcf->databases); + q = ngx_queue_next(q)) + { + database = ngx_queue_data(q, ngx_http_geoip2_db_t, queue); + if (database->check_interval == 0) { + continue; + } + + if ((database->last_check + database->check_interval) + > ngx_time()) + { + continue; + } + + database->last_check = ngx_time(); + + if (ngx_file_info(database->mmdb.filename, &fi) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_EMERG, r->connection->log, ngx_errno, + ngx_file_info_n " \"%s\" failed", + database->mmdb.filename); + + continue; + } + + if (ngx_file_mtime(&fi) <= database->last_change) { + continue; + } + + /* do the reload */ + + ngx_memzero(&tmpdb, sizeof(MMDB_s)); + status = MMDB_open(database->mmdb.filename, MMDB_MODE_MMAP, &tmpdb); + + if (status != MMDB_SUCCESS) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "MMDB_open(\"%s\") failed to reload - %s", + database->mmdb.filename, MMDB_strerror(status)); + + continue; + } + + database->last_change = ngx_file_mtime(&fi); + MMDB_close(&database->mmdb); + database->mmdb = tmpdb; + + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "Reload MMDB \"%s\"", + database->mmdb.filename); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_http_geoip2_init(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_LOG_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_http_geoip2_log_handler; + + return NGX_OK; +} diff --git a/ngx_http_geoip2_module/ngx_stream_geoip2_module.c b/ngx_http_geoip2_module/ngx_stream_geoip2_module.c new file mode 100644 index 0000000..eb59082 --- /dev/null +++ b/ngx_http_geoip2_module/ngx_stream_geoip2_module.c @@ -0,0 +1,694 @@ +/* + * Copyright (C) Lee Valentine + * Copyright (C) Andrei Belov + * + * Based on nginx's 'ngx_stream_geoip_module.c' by Igor Sysoev + */ + + +#include +#include +#include + +#include + + +typedef struct { + MMDB_s mmdb; + MMDB_lookup_result_s result; + time_t last_check; + time_t last_change; + time_t check_interval; +#if (NGX_HAVE_INET6) + uint8_t address[16]; +#else + unsigned long address; +#endif + ngx_queue_t queue; +} ngx_stream_geoip2_db_t; + +typedef struct { + ngx_queue_t databases; +} ngx_stream_geoip2_conf_t; + +typedef struct { + ngx_stream_geoip2_db_t *database; + const char **lookup; + ngx_str_t default_value; + ngx_stream_complex_value_t source; +} ngx_stream_geoip2_ctx_t; + +typedef struct { + ngx_stream_geoip2_db_t *database; + ngx_str_t metavalue; +} ngx_stream_geoip2_metadata_t; + + +static ngx_int_t ngx_stream_geoip2_variable(ngx_stream_session_t *s, + ngx_stream_variable_value_t *v, uintptr_t data); +static ngx_int_t ngx_stream_geoip2_metadata(ngx_stream_session_t *s, + ngx_stream_variable_value_t *v, uintptr_t data); +static void *ngx_stream_geoip2_create_conf(ngx_conf_t *cf); +static char *ngx_stream_geoip2(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_stream_geoip2_parse_config(ngx_conf_t *cf, ngx_command_t *dummy, + void *conf); +static char *ngx_stream_geoip2(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static char *ngx_stream_geoip2_add_variable(ngx_conf_t *cf, ngx_command_t *dummy, + void *conf); +static char *ngx_stream_geoip2_add_variable_geodata(ngx_conf_t *cf, + ngx_stream_geoip2_db_t *database); +static char *ngx_stream_geoip2_add_variable_metadata(ngx_conf_t *cf, + ngx_stream_geoip2_db_t *database); +static void ngx_stream_geoip2_cleanup(void *data); +static ngx_int_t ngx_stream_geoip2_init(ngx_conf_t *cf); + + +#define FORMAT(fmt, ...) do { \ + p = ngx_palloc(s->connection->pool, NGX_OFF_T_LEN); \ + if (p == NULL) { \ + return NGX_ERROR; \ + } \ + v->len = ngx_sprintf(p, fmt, __VA_ARGS__) - p; \ + v->data = p; \ +} while (0) + +static ngx_command_t ngx_stream_geoip2_commands[] = { + + { ngx_string("geoip2"), + NGX_STREAM_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE1, + ngx_stream_geoip2, + NGX_STREAM_MAIN_CONF_OFFSET, + 0, + NULL }, + + ngx_null_command +}; + + +static ngx_stream_module_t ngx_stream_geoip2_module_ctx = { + NULL, /* preconfiguration */ + ngx_stream_geoip2_init, /* postconfiguration */ + + ngx_stream_geoip2_create_conf, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL /* merge server configuration */ +}; + + +ngx_module_t ngx_stream_geoip2_module = { + NGX_MODULE_V1, + &ngx_stream_geoip2_module_ctx, /* module context */ + ngx_stream_geoip2_commands, /* module directives */ + NGX_STREAM_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 +}; + + +static ngx_int_t +ngx_stream_geoip2_variable(ngx_stream_session_t *s, ngx_stream_variable_value_t *v, + uintptr_t data) +{ + int mmdb_error; + u_char *p; + ngx_str_t val; + ngx_addr_t addr; + MMDB_entry_data_s entry_data; + ngx_stream_geoip2_ctx_t *geoip2 = (ngx_stream_geoip2_ctx_t *) data; + ngx_stream_geoip2_db_t *database = geoip2->database; + +#if (NGX_HAVE_INET6) + uint8_t address[16], *addressp = address; +#else + unsigned long address; +#endif + + if (geoip2->source.value.len > 0) { + if (ngx_stream_complex_value(s, &geoip2->source, &val) != NGX_OK) { + goto not_found; + } + + if (ngx_parse_addr(s->connection->pool, &addr, val.data, val.len) != NGX_OK) { + goto not_found; + } + } else { + addr.sockaddr = s->connection->sockaddr; + addr.socklen = s->connection->socklen; + } + + switch (addr.sockaddr->sa_family) { + case AF_INET: +#if (NGX_HAVE_INET6) + ngx_memset(addressp, 0, 12); + ngx_memcpy(addressp + 12, &((struct sockaddr_in *) + addr.sockaddr)->sin_addr.s_addr, 4); + break; + + case AF_INET6: + ngx_memcpy(addressp, &((struct sockaddr_in6 *) + addr.sockaddr)->sin6_addr.s6_addr, 16); +#else + address = ((struct sockaddr_in *)addr.sockaddr)->sin_addr.s_addr; +#endif + break; + + default: + goto not_found; + } + +#if (NGX_HAVE_INET6) + if (ngx_memcmp(&address, &database->address, sizeof(address)) != 0) { +#else + if (address != database->address) { +#endif + memcpy(&database->address, &address, sizeof(address)); + database->result = MMDB_lookup_sockaddr(&database->mmdb, + addr.sockaddr, &mmdb_error); + + if (mmdb_error != MMDB_SUCCESS) { + goto not_found; + } + } + + if (!database->result.found_entry + || MMDB_aget_value(&database->result.entry, &entry_data, geoip2->lookup) + != MMDB_SUCCESS) + { + goto not_found; + } + + if (!entry_data.has_data) { + goto not_found; + } + + switch (entry_data.type) { + case MMDB_DATA_TYPE_BOOLEAN: + FORMAT("%d", entry_data.boolean); + break; + case MMDB_DATA_TYPE_UTF8_STRING: + v->len = entry_data.data_size; + v->data = ngx_pnalloc(s->connection->pool, v->len); + if (v->data == NULL) { + return NGX_ERROR; + } + ngx_memcpy(v->data, (u_char *) entry_data.utf8_string, v->len); + break; + case MMDB_DATA_TYPE_BYTES: + v->len = entry_data.data_size; + v->data = ngx_pnalloc(s->connection->pool, v->len); + if (v->data == NULL) { + return NGX_ERROR; + } + ngx_memcpy(v->data, (u_char *) entry_data.bytes, v->len); + break; + case MMDB_DATA_TYPE_FLOAT: + FORMAT("%.5f", entry_data.float_value); + break; + case MMDB_DATA_TYPE_DOUBLE: + FORMAT("%.5f", entry_data.double_value); + break; + case MMDB_DATA_TYPE_UINT16: + FORMAT("%uD", entry_data.uint16); + break; + case MMDB_DATA_TYPE_UINT32: + FORMAT("%uD", entry_data.uint32); + break; + case MMDB_DATA_TYPE_INT32: + FORMAT("%D", entry_data.int32); + break; + case MMDB_DATA_TYPE_UINT64: + FORMAT("%uL", entry_data.uint64); + break; + case MMDB_DATA_TYPE_UINT128: ; +#if MMDB_UINT128_IS_BYTE_ARRAY + uint8_t *val = (uint8_t *) entry_data.uint128; + FORMAT("0x%02x%02x%02x%02x%02x%02x%02x%02x" + "%02x%02x%02x%02x%02x%02x%02x%02x", + val[0], val[1], val[2], val[3], + val[4], val[5], val[6], val[7], + val[8], val[9], val[10], val[11], + val[12], val[13], val[14], val[15]); +#else + mmdb_uint128_t val = entry_data.uint128; + FORMAT("0x%016uxL%016uxL", + (uint64_t) (val >> 64), (uint64_t) val); +#endif + break; + default: + goto not_found; + } + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + return NGX_OK; + +not_found: + if (geoip2->default_value.len > 0) { + v->data = geoip2->default_value.data; + v->len = geoip2->default_value.len; + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + return NGX_OK; + } + + v->not_found = 1; + + return NGX_OK; +} + + +static ngx_int_t +ngx_stream_geoip2_metadata(ngx_stream_session_t *s, ngx_stream_variable_value_t *v, + uintptr_t data) +{ + ngx_stream_geoip2_metadata_t *metadata = (ngx_stream_geoip2_metadata_t *) data; + ngx_stream_geoip2_db_t *database = metadata->database; + u_char *p; + + if (ngx_strncmp(metadata->metavalue.data, "build_epoch", 11) == 0) { + FORMAT("%uL", database->mmdb.metadata.build_epoch); + } else if (ngx_strncmp(metadata->metavalue.data, "last_check", 10) == 0) { + FORMAT("%T", database->last_check); + } else if (ngx_strncmp(metadata->metavalue.data, "last_change", 11) == 0) { + FORMAT("%T", database->last_change); + } else { + v->not_found = 1; + return NGX_OK; + } + + v->valid = 1; + v->no_cacheable = 0; + v->not_found = 0; + + return NGX_OK; +} + + +static void * +ngx_stream_geoip2_create_conf(ngx_conf_t *cf) +{ + ngx_pool_cleanup_t *cln; + ngx_stream_geoip2_conf_t *conf; + + conf = ngx_pcalloc(cf->pool, sizeof(ngx_stream_geoip2_conf_t)); + if (conf == NULL) { + return NULL; + } + + cln = ngx_pool_cleanup_add(cf->pool, 0); + if (cln == NULL) { + return NULL; + } + + ngx_queue_init(&conf->databases); + + cln->handler = ngx_stream_geoip2_cleanup; + cln->data = conf; + + return conf; +} + + +static char * +ngx_stream_geoip2(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) +{ + int status; + char *rv; + ngx_str_t *value; + ngx_conf_t save; + ngx_stream_geoip2_db_t *database; + ngx_stream_geoip2_conf_t *gcf = conf; + ngx_queue_t *q; + + value = cf->args->elts; + + if (value[1].data && value[1].data[0] != '/') { + if (ngx_conf_full_name(cf->cycle, &value[1], 0) != NGX_OK) { + return NGX_CONF_ERROR; + } + } + + if (!ngx_queue_empty(&gcf->databases)) { + for (q = ngx_queue_head(&gcf->databases); + q != ngx_queue_sentinel(&gcf->databases); + q = ngx_queue_next(q)) + { + database = ngx_queue_data(q, ngx_stream_geoip2_db_t, queue); + if (ngx_strcmp(value[1].data, database->mmdb.filename) == 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "Duplicate GeoIP2 mmdb - %V", &value[1]); + return NGX_CONF_ERROR; + } + } + } + + database = ngx_pcalloc(cf->pool, sizeof(ngx_stream_geoip2_db_t)); + if (database == NULL) { + return NGX_CONF_ERROR; + } + + ngx_queue_insert_tail(&gcf->databases, &database->queue); + database->last_check = database->last_change = ngx_time(); + + status = MMDB_open((char *) value[1].data, MMDB_MODE_MMAP, &database->mmdb); + + if (status != MMDB_SUCCESS) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "MMDB_open(\"%V\") failed - %s", &value[1], + MMDB_strerror(status)); + return NGX_CONF_ERROR; + } + + save = *cf; + cf->handler = ngx_stream_geoip2_parse_config; + cf->handler_conf = (void *) database; + + rv = ngx_conf_parse(cf, NULL); + *cf = save; + return rv; +} + + +static char * +ngx_stream_geoip2_parse_config(ngx_conf_t *cf, ngx_command_t *dummy, void *conf) +{ + ngx_stream_geoip2_db_t *database; + ngx_str_t *value; + time_t interval; + + value = cf->args->elts; + + if (value[0].data[0] == '$') { + return ngx_stream_geoip2_add_variable(cf, dummy, conf); + } + + if (value[0].len == 11 + && ngx_strncmp(value[0].data, "auto_reload", 11) == 0) { + if ((int) cf->args->nelts != 2) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid number of arguments for auto_reload"); + return NGX_CONF_ERROR; + } + + interval = ngx_parse_time(&value[1], true); + + if (interval == (time_t) NGX_ERROR) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid interval for auto_reload \"%V\"", + value[1]); + return NGX_CONF_ERROR; + } + + + database = (ngx_stream_geoip2_db_t *) conf; + database->check_interval = interval; + return NGX_CONF_OK; + } + + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid setting \"%V\"", &value[0]); + return NGX_CONF_ERROR; +} + + +static char * +ngx_stream_geoip2_add_variable(ngx_conf_t *cf, ngx_command_t *dummy, void *conf) +{ + ngx_stream_geoip2_db_t *database; + ngx_str_t *value; + int nelts; + + value = cf->args->elts; + + if (value[0].data[0] != '$') { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid variable name \"%V\"", &value[0]); + return NGX_CONF_ERROR; + } + + value[0].len--; + value[0].data++; + + nelts = (int) cf->args->nelts; + database = (ngx_stream_geoip2_db_t *) conf; + + if (nelts > 0 && value[1].len == 8 && ngx_strncmp(value[1].data, "metadata", 8) == 0) { + return ngx_stream_geoip2_add_variable_metadata(cf, database); + } + + return ngx_stream_geoip2_add_variable_geodata(cf, database); +} + + +static char * +ngx_stream_geoip2_add_variable_metadata(ngx_conf_t *cf, ngx_stream_geoip2_db_t *database) +{ + ngx_stream_geoip2_metadata_t *metadata; + ngx_str_t *value, name; + ngx_stream_variable_t *var; + + metadata = ngx_pcalloc(cf->pool, sizeof(ngx_stream_geoip2_metadata_t)); + if (metadata == NULL) { + return NGX_CONF_ERROR; + } + + value = cf->args->elts; + name = value[0]; + + metadata->database = database; + metadata->metavalue = value[2]; + + var = ngx_stream_add_variable(cf, &name, NGX_STREAM_VAR_CHANGEABLE); + if (var == NULL) { + return NGX_CONF_ERROR; + } + + var->get_handler = ngx_stream_geoip2_metadata; + var->data = (uintptr_t) metadata; + + return NGX_CONF_OK; +} + + +static char * +ngx_stream_geoip2_add_variable_geodata(ngx_conf_t *cf, ngx_stream_geoip2_db_t *database) +{ + ngx_stream_geoip2_ctx_t *geoip2; + ngx_stream_compile_complex_value_t ccv; + ngx_str_t *value, name, source; + ngx_stream_variable_t *var; + int i, nelts, idx; + + geoip2 = ngx_pcalloc(cf->pool, sizeof(ngx_stream_geoip2_ctx_t)); + if (geoip2 == NULL) { + return NGX_CONF_ERROR; + } + + geoip2->database = database; + ngx_str_null(&source); + + value = cf->args->elts; + name = value[0]; + + nelts = (int) cf->args->nelts; + idx = 1; + + if (nelts > idx) { + for (i = idx; i < nelts; i++) { + if (ngx_strnstr(value[idx].data, "=", value[idx].len) == NULL) { + break; + } + + if (value[idx].len > 8 && ngx_strncmp(value[idx].data, "default=", 8) == 0) { + if (geoip2->default_value.len > 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "default has already been declared for \"$%V\"", &name); + return NGX_CONF_ERROR; + } + + geoip2->default_value.len = value[idx].len - 8; + geoip2->default_value.data = value[idx].data + 8; + + } else if (value[idx].len > 7 && ngx_strncmp(value[idx].data, "source=", 7) == 0) { + if (source.len > 0) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "source has already been declared for \"$%V\"", &name); + return NGX_CONF_ERROR; + } + + source.len = value[idx].len - 7; + source.data = value[idx].data + 7; + + if (source.data[0] != '$') { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid source variable name \"%V\"", &source); + return NGX_CONF_ERROR; + } + + ngx_memzero(&ccv, sizeof(ngx_stream_compile_complex_value_t)); + ccv.cf = cf; + ccv.value = &source; + ccv.complex_value = &geoip2->source; + + if (ngx_stream_compile_complex_value(&ccv) != NGX_OK) { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "unable to compile \"%V\" for \"$%V\"", &source, &name); + return NGX_CONF_ERROR; + } + + } else { + ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, + "invalid setting \"%V\" for \"$%V\"", &value[idx], &name); + return NGX_CONF_ERROR; + } + + idx++; + } + } + + var = ngx_stream_add_variable(cf, &name, NGX_STREAM_VAR_CHANGEABLE); + if (var == NULL) { + return NGX_CONF_ERROR; + } + + geoip2->lookup = ngx_pcalloc(cf->pool, + sizeof(const char *) * (cf->args->nelts - (idx - 1))); + + if (geoip2->lookup == NULL) { + return NGX_CONF_ERROR; + } + + for (i = idx; i < nelts; i++) { + geoip2->lookup[i - idx] = (char *) value[i].data; + } + geoip2->lookup[i - idx] = NULL; + + var->get_handler = ngx_stream_geoip2_variable; + var->data = (uintptr_t) geoip2; + + return NGX_CONF_OK; +} + + +static void +ngx_stream_geoip2_cleanup(void *data) +{ + ngx_queue_t *q; + ngx_stream_geoip2_db_t *database; + ngx_stream_geoip2_conf_t *gcf = data; + + while (!ngx_queue_empty(&gcf->databases)) { + q = ngx_queue_head(&gcf->databases); + ngx_queue_remove(q); + database = ngx_queue_data(q, ngx_stream_geoip2_db_t, queue); + MMDB_close(&database->mmdb); + } +} + + +static ngx_int_t +ngx_stream_geoip2_log_handler(ngx_stream_session_t *s) +{ + int status; + MMDB_s tmpdb; + ngx_queue_t *q; + ngx_file_info_t fi; + ngx_stream_geoip2_db_t *database; + ngx_stream_geoip2_conf_t *gcf; + + ngx_log_debug0(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, + "geoip2 stream log handler"); + + gcf = ngx_stream_get_module_main_conf(s, ngx_stream_geoip2_module); + + if (ngx_queue_empty(&gcf->databases)) { + return NGX_OK; + } + + for (q = ngx_queue_head(&gcf->databases); + q != ngx_queue_sentinel(&gcf->databases); + q = ngx_queue_next(q)) + { + database = ngx_queue_data(q, ngx_stream_geoip2_db_t, queue); + if (database->check_interval == 0) { + continue; + } + + if ((database->last_check + database->check_interval) + > ngx_time()) + { + continue; + } + + database->last_check = ngx_time(); + + if (ngx_file_info(database->mmdb.filename, &fi) == NGX_FILE_ERROR) { + ngx_log_error(NGX_LOG_EMERG, s->connection->log, ngx_errno, + ngx_file_info_n " \"%s\" failed", + database->mmdb.filename); + + continue; + } + + if (ngx_file_mtime(&fi) <= database->last_change) { + continue; + } + + /* do the reload */ + + ngx_memzero(&tmpdb, sizeof(MMDB_s)); + status = MMDB_open(database->mmdb.filename, MMDB_MODE_MMAP, &tmpdb); + + if (status != MMDB_SUCCESS) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "MMDB_open(\"%s\") failed to reload - %s", + database->mmdb.filename, MMDB_strerror(status)); + + continue; + } + + database->last_change = ngx_file_mtime(&fi); + MMDB_close(&database->mmdb); + database->mmdb = tmpdb; + + ngx_log_error(NGX_LOG_INFO, s->connection->log, 0, + "Reload MMDB \"%s\"", + database->mmdb.filename); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_stream_geoip2_init(ngx_conf_t *cf) +{ + ngx_stream_handler_pt *h; + ngx_stream_core_main_conf_t *cmcf; + + cmcf = ngx_stream_conf_get_module_main_conf(cf, ngx_stream_core_module); + + h = ngx_array_push(&cmcf->phases[NGX_STREAM_LOG_PHASE].handlers); + if (h == NULL) { + return NGX_ERROR; + } + + *h = ngx_stream_geoip2_log_handler; + + return NGX_OK; +} diff --git a/ngx_http_limit_req_module/.gitignore b/ngx_http_limit_req_module/.gitignore new file mode 100644 index 0000000..7cd2ca3 --- /dev/null +++ b/ngx_http_limit_req_module/.gitignore @@ -0,0 +1,69 @@ +# Prerequisites +*.d + +# Cache +.cache + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb +debugger +out.bin +one.bin +two.bin + +# Kernel Module Compile Results +*.mod* +!go.mod +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# Dev Files +compile_commands.json +.vscode/ + +# nginx files +build/ +nginx-1.26.3/ +nginx.tar.gz diff --git a/ngx_http_limit_req_module/LICENSE b/ngx_http_limit_req_module/LICENSE new file mode 100644 index 0000000..ce273d3 --- /dev/null +++ b/ngx_http_limit_req_module/LICENSE @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2025, tsuru + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ngx_http_limit_req_module/Makefile b/ngx_http_limit_req_module/Makefile new file mode 100644 index 0000000..f8e514d --- /dev/null +++ b/ngx_http_limit_req_module/Makefile @@ -0,0 +1,39 @@ +.PHONY: build clean download-nginx build-nginx configure build-module run test debug + +setup: clean download-nginx configure + +build: build-nginx build-module + +download-nginx: + curl https://nginx.org/download/nginx-1.26.3.tar.gz > nginx.tar.gz + tar -zxvf nginx.tar.gz + +configure: + cd nginx-1.26.3 && ./configure --prefix=$(PWD)/build --add-dynamic-module=.. + +build-nginx: + cd nginx-1.26.3 && make && make install + +build-module: + cd nginx-1.26.3 && make modules && make install + +clean: + rm -rf nginx-1.26.3 + rm -rf nginx.tar.gz + +run: + ./build/sbin/nginx -g "daemon off;" -c $(PWD)/nginx.conf + +test: + ./scripts/test.sh + +debug: + cd ./reader-go; go build -o debugger main.go; mv ./debugger .. + ./debugger one.bin + ./debugger two.bin + +log: + go run log_zone/*.go log + +send: + go run log_zone/*.go send diff --git a/ngx_http_limit_req_module/README.md b/ngx_http_limit_req_module/README.md new file mode 100644 index 0000000..a07a1a8 --- /dev/null +++ b/ngx_http_limit_req_module/README.md @@ -0,0 +1,2 @@ +# ngx-http-limit-req-rw-module +A nginx module that allows to read and write limits defined on: https://nginx.org/en/docs/http/ngx_http_limit_req_module.html diff --git a/ngx_http_limit_req_module/config b/ngx_http_limit_req_module/config new file mode 100644 index 0000000..373d228 --- /dev/null +++ b/ngx_http_limit_req_module/config @@ -0,0 +1,30 @@ +ngx_addon_name=ngx_http_limit_req_rw_module +ngx_module_type=HTTP +ngx_module_name=ngx_http_limit_req_rw_module +ngx_module_srcs="$ngx_addon_dir/ngx_http_limit_req_rw_module.c" + +# Detect platform +OS_NAME="$(uname -s)" + +# Default values +MSGPACK_PKG_NAME="" +MSGPACK_VERSION="" + +# Try to detect msgpack library +if pkg-config --exists 'msgpack-c'; then + MSGPACK_PKG_NAME="msgpack-c" + MSGPACK_VERSION="6.1.0" # Optional: only enforce version if strictly necessary +elif pkg-config --exists 'msgpack'; then + MSGPACK_PKG_NAME="msgpack" + MSGPACK_VERSION="3.1.0" +else + echo "Error: Neither 'msgpack-c' nor 'msgpack' pkg-config package found." + exit 1 +fi + +# Add flags +CFLAGS="$CFLAGS $(pkg-config --cflags "$MSGPACK_PKG_NAME")" +LDFLAGS="$LDFLAGS $(pkg-config --libs "$MSGPACK_PKG_NAME")" +CORE_LIBS="$CORE_LIBS $(pkg-config --libs "$MSGPACK_PKG_NAME")" + +. auto/module diff --git a/ngx_http_limit_req_module/go.mod b/ngx_http_limit_req_module/go.mod new file mode 100644 index 0000000..0f83b49 --- /dev/null +++ b/ngx_http_limit_req_module/go.mod @@ -0,0 +1,13 @@ +module github.com/tsuru/ngx-http-limit-req-rw-module + +go 1.24.3 + +require ( + github.com/sirupsen/logrus v1.9.3 + github.com/vmihailenco/msgpack/v5 v5.4.1 +) + +require ( + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/ngx_http_limit_req_module/go.sum b/ngx_http_limit_req_module/go.sum new file mode 100644 index 0000000..5611ca6 --- /dev/null +++ b/ngx_http_limit_req_module/go.sum @@ -0,0 +1,19 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ngx_http_limit_req_module/log_zone/logs.go b/ngx_http_limit_req_module/log_zone/logs.go new file mode 100644 index 0000000..a08dbc0 --- /dev/null +++ b/ngx_http_limit_req_module/log_zone/logs.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "time" + + "github.com/sirupsen/logrus" + "github.com/vmihailenco/msgpack/v5" +) + +func logs(zone string) { + for { + zone, err := handleRequest(zone) + if err != nil { + logrus.Error("Error handling request", "error", err) + return + } + fmt.Print("\033[H\033[2J") + logrus.Infof("Zone: %s, RateLimitHeader: %+v, RateLimitEntries: %d", + zone.Name, zone.RateLimitHeader, len(zone.RateLimitEntries)) + for _, entry := range zone.RateLimitEntries { + logrus.Infof("Entry Key: %s, Last: %d, Excess: %d", + entry.Key.String(zone.RateLimitHeader), entry.Last, entry.Excess) + } + fmt.Println("\nPress Ctrl+C to exit") + time.Sleep(2 * time.Second) + } +} + +func handleRequest(zone string) (Zone, error) { + endpoint := fmt.Sprintf("http://localhost:9000/api/%s", zone) + req, err := http.NewRequest(http.MethodGet, endpoint, nil) + if err != nil { + return Zone{}, err + } + + response, err := http.DefaultClient.Do(req) + if err != nil { + return Zone{}, fmt.Errorf("error making request to %s: %w", endpoint, err) + } + defer response.Body.Close() + decoder := msgpack.NewDecoder(response.Body) + var rateLimitHeader RateLimitHeader + rateLimitEntries := []RateLimitEntry{} + log := logrus.New() + if err := decoder.Decode(&rateLimitHeader); err != nil { + if err == io.EOF { + return Zone{ + Name: zone, + RateLimitHeader: rateLimitHeader, + RateLimitEntries: rateLimitEntries, + }, nil + } + log.Error("Error decoding header", "error", err) + return Zone{}, err + } + for { + var message RateLimitEntry + if err := decoder.Decode(&message); err != nil { + if err == io.EOF { + break + } + log.Error("Error decoding entry", "error", err) + return Zone{}, err + } + message.Last = toNonMonotonic(message.Last, rateLimitHeader) + rateLimitEntries = append(rateLimitEntries, message) + } + log.Debug("Received rate limit entries", "zone", zone, "entries", len(rateLimitEntries)) + return Zone{ + Name: zone, + RateLimitHeader: rateLimitHeader, + RateLimitEntries: rateLimitEntries, + }, nil +} diff --git a/ngx_http_limit_req_module/log_zone/main.go b/ngx_http_limit_req_module/log_zone/main.go new file mode 100644 index 0000000..b3831a4 --- /dev/null +++ b/ngx_http_limit_req_module/log_zone/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "net" + "os" +) + +func main() { + arg := os.Args[1:] + zone := "one" + if arg[0] == "log" { + if len(arg) >= 2 { + zone = arg[1] + } + logs(zone) + return + } + if arg[0] == "send" { + if len(arg) >= 2 { + zone = arg[1] + } + send(zone) + } +} + +func toNonMonotonic(last int64, header RateLimitHeader) int64 { + return header.Now - (header.NowMonotonic - last) +} + +type Zone struct { + Name string + RateLimitHeader RateLimitHeader + RateLimitEntries []RateLimitEntry +} + +type RateLimitHeader struct { + Key string + Now int64 + NowMonotonic int64 +} + +type RateLimitEntry struct { + Key Key + Last int64 + Excess int64 +} + +const ( + BinaryRemoteAddress = "$binary_remote_addr" + RemoteAddress = "$remote_addr" +) + +type Key []byte + +func (r Key) String(header RateLimitHeader) string { + switch header.Key { + case BinaryRemoteAddress: + return net.IP(r).String() + case RemoteAddress: + fallthrough + default: + return string(r) + } +} diff --git a/ngx_http_limit_req_module/log_zone/send.go b/ngx_http_limit_req_module/log_zone/send.go new file mode 100644 index 0000000..effe051 --- /dev/null +++ b/ngx_http_limit_req_module/log_zone/send.go @@ -0,0 +1,76 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "net/http" + "time" + + "github.com/sirupsen/logrus" + "github.com/vmihailenco/msgpack/v5" +) + +func send(zone string) { + err := sendRequest( + zone, + RateLimitHeader{ + Key: BinaryRemoteAddress, + Now: time.Now().Unix(), + NowMonotonic: time.Now().UnixNano() / int64(time.Millisecond), + }, []RateLimitEntry{ + {Key([]byte{127, 0, 0, 0}), 7, 99}, + {Key([]byte{127, 0, 0, 1}), 7, 12}, + {Key([]byte{127, 6, 4, 00}), 2, 98}, + {Key([]byte{127, 0, 0, 99}), 30, 300}, + {Key([]byte{10, 0, 0, 1}), 444, 21}, + }) + if err != nil { + logrus.Fatalf("Error sending request: %v", err) + } +} + +func sendRequest(zone string, header RateLimitHeader, entries []RateLimitEntry) error { + var buf bytes.Buffer + encoder := msgpack.NewEncoder(&buf) + var values []interface{} = []interface{}{ + headerToArray(header), + } + for _, entry := range entries { + values = append(values, entryToArray(entry, header)) + } + if err := encoder.Encode(values); err != nil { + return fmt.Errorf("error encoding entries: %w", err) + } + endpoint := fmt.Sprintf("http://localhost:9000/api/%s", zone) + req, err := http.NewRequest(http.MethodPost, endpoint, &buf) + if err != nil { + return fmt.Errorf("error creating request: %w", err) + } + req.Header.Set("Content-Type", "application/x-msgpack") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("error sending request to %s: %w", endpoint, err) + } + fmt.Println(resp.Status) + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + logrus.Infof("response status: %s, body: %s", resp.Status, respBody) + return nil +} + +func headerToArray(header RateLimitHeader) []interface{} { + return []interface{}{ + header.Key, + header.Now, + header.NowMonotonic, + } +} + +func entryToArray(entry RateLimitEntry, header RateLimitHeader) []interface{} { + return []interface{}{ + entry.Key, + entry.Last, + entry.Excess, + } +} diff --git a/ngx_http_limit_req_module/nginx.conf b/ngx_http_limit_req_module/nginx.conf new file mode 100644 index 0000000..2b896d1 --- /dev/null +++ b/ngx_http_limit_req_module/nginx.conf @@ -0,0 +1,39 @@ +worker_processes 1; +master_process on; + +error_log ./error.log debug; + +load_module modules/ngx_http_limit_req_rw_module.so; + +events { + worker_connections 1024; +} + +http { + limit_req_zone $binary_remote_addr zone=one:10m rate=10r/m; + limit_req_zone $remote_addr zone=two:10m rate=10r/s; + + limit_req_status 429; + + server { + listen 8888; + server_name localhost; + + location /one { + limit_req zone=one burst=1 nodelay; + } + + location /two { + limit_req zone=two burst=1 nodelay; + } + } + + server { + listen 9000; + server_name localhost; + + location /api { + limit_req_rw_handler; + } + } +} diff --git a/ngx_http_limit_req_module/ngx_http_limit_req_module.h b/ngx_http_limit_req_module/ngx_http_limit_req_module.h new file mode 100644 index 0000000..99187e2 --- /dev/null +++ b/ngx_http_limit_req_module/ngx_http_limit_req_module.h @@ -0,0 +1,50 @@ +/* +TODO: copyright +*/ + +#include +#include + +typedef struct { + ngx_array_t limits; + ngx_uint_t limit_log_level; + ngx_uint_t delay_log_level; + ngx_uint_t status_code; + ngx_flag_t dry_run; +} ngx_http_limit_req_conf_t; + +typedef struct { + ngx_shm_zone_t *shm_zone; + /* integer value, 1 corresponds to 0.001 r/s */ + ngx_uint_t burst; + ngx_uint_t delay; +} ngx_http_limit_req_limit_t; + +typedef struct { + u_char color; + u_char dummy; + u_short len; + ngx_queue_t queue; + ngx_msec_t last; + /* integer value, 1 corresponds to 0.001 r/s */ + ngx_uint_t excess; + ngx_uint_t count; + u_char data[1]; +} ngx_http_limit_req_node_t; + +typedef struct { + ngx_rbtree_t rbtree; + ngx_rbtree_node_t sentinel; + ngx_queue_t queue; +} ngx_http_limit_req_shctx_t; + +typedef struct { + ngx_http_limit_req_shctx_t *sh; + ngx_slab_pool_t *shpool; + /* integer value, 1 corresponds to 0.001 r/s */ + ngx_uint_t rate; + ngx_http_complex_value_t key; + ngx_http_limit_req_node_t *node; +} ngx_http_limit_req_ctx_t; + +extern ngx_module_t ngx_http_limit_req_module; diff --git a/ngx_http_limit_req_module/ngx_http_limit_req_rw_module.c b/ngx_http_limit_req_module/ngx_http_limit_req_rw_module.c new file mode 100644 index 0000000..8017ad6 --- /dev/null +++ b/ngx_http_limit_req_module/ngx_http_limit_req_rw_module.c @@ -0,0 +1,652 @@ +/* +TODO: copyright +*/ + +#include "ngx_http_limit_req_module.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +const int MAX_NUMBER_OF_RATE_LIMIT_ELEMENTS = 30 * 1000; + +typedef struct { + ngx_str_t Key; + uint64_t Last; + uint64_t Excess; +} entities; + +typedef struct { + ngx_str_t Key; // Key of the rate limit zone + uint64_t Now; // Current timestamp in milliseconds + uint64_t NowMonotonic; // Current monotonic timestamp in milliseconds +} header; + +typedef struct { + header *Header; // Header information + entities *Entities; // Array of entities + uint32_t EntitiesSize; // Size of the entities array +} ngx_zone_data_t; + +static ngx_int_t ngx_decode_msg_pack(ngx_http_request_t *r, + ngx_zone_data_t *msg_pack); + +static ngx_int_t ngx_http_limit_req_read_handler(ngx_http_request_t *r); +static void ngx_http_limit_req_write_post_handler(ngx_http_request_t *r); +static ngx_int_t ngx_http_limit_req_write_handler(ngx_http_request_t *r); + +static char *ngx_http_limit_req_rw_handler(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf); +static void strip_zone_name_from_uri(ngx_str_t *uri, ngx_str_t *zone_name); +static ngx_int_t dump_rate_limit_zones(ngx_http_request_t *r, ngx_buf_t *b); +static ngx_shm_zone_t *find_rate_limit_shm_zone_by_name(ngx_http_request_t *r, + ngx_str_t zone_name); +static ngx_int_t dump_req_limits(ngx_pool_t *pool, ngx_shm_zone_t *shm_zone, + ngx_buf_t *buf, ngx_uint_t last_greater_equal); +static ngx_command_t ngx_http_limit_req_rw_commands[] = { + {ngx_string("limit_req_rw_handler"), + NGX_HTTP_MAIN_CONF | NGX_HTTP_SRV_CONF | NGX_HTTP_LOC_CONF | + NGX_CONF_NOARGS | NGX_CONF_TAKE1, + ngx_http_limit_req_rw_handler, 0, 0, NULL}, + ngx_null_command}; + +static ngx_http_module_t ngx_http_limit_req_rw_module_ctx = { + NULL, /* preconfiguration */ + NULL, /* postconfiguration */ + + NULL, /* create main configuration */ + NULL, /* init main configuration */ + + NULL, /* create server configuration */ + NULL, /* merge server configuration */ + + NULL, /* create location configuration */ + NULL /* merge location configuration */ +}; + +ngx_module_t ngx_http_limit_req_rw_module = {NGX_MODULE_V1, + &ngx_http_limit_req_rw_module_ctx, + ngx_http_limit_req_rw_commands, + NGX_HTTP_MODULE, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NGX_MODULE_V1_PADDING}; + +static ngx_int_t ngx_http_limit_req_handler(ngx_http_request_t *r) { + if (r->method == NGX_HTTP_GET) { + return ngx_http_limit_req_read_handler(r); + } + if (r->method == NGX_HTTP_POST) { + r->request_body_in_single_buf = 1; + return ngx_http_read_client_request_body( + r, ngx_http_limit_req_write_post_handler); + } + return NGX_HTTP_SERVICE_UNAVAILABLE; +} + +static ngx_int_t ngx_decode_msg_pack(ngx_http_request_t *r, + ngx_zone_data_t *ngx_zone_data) { + ngx_chain_t *cl; + size_t len = 0; + u_char *data, *p; + size_t size, deserialized_size; + msgpack_zone mempool; + msgpack_object deserialized; + + if (r->request_body == NULL || r->request_body->bufs == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "no request body found"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + for (cl = r->request_body->bufs; cl; cl = cl->next) { + len += cl->buf->last - cl->buf->pos; + } + + data = ngx_pnalloc(r->pool, len); + if (data == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "failed to allocate memory for request body"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + p = data; + for (cl = r->request_body->bufs; cl; cl = cl->next) { + size = cl->buf->last - cl->buf->pos; + ngx_memcpy(p, cl->buf->pos, size); + p += size; + } + + msgpack_zone_init(&mempool, 2048); + + msgpack_unpack((char *)data, len, NULL, &mempool, &deserialized); + ngx_log_error( + NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: deserialized type: %d - size: %d", + deserialized.type, deserialized.via.array.size); + if (deserialized.type != MSGPACK_OBJECT_ARRAY) { + msgpack_zone_destroy(&mempool); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + deserialized_size = deserialized.via.array.size; + msgpack_object *items = deserialized.via.array.ptr; + + header *hdr = ngx_pnalloc(r->pool, sizeof(header)); + if (hdr == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "failed to allocate memory for header"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (items->via.array.size >= 1) { + msgpack_object hdrKey = items[0].via.array.ptr[0]; + msgpack_object hdrNow = items[0].via.array.ptr[1]; + msgpack_object hdrNowMonotonic = items[0].via.array.ptr[2]; + hdr->Key.len = hdrKey.via.str.size; + u_char *keyData = ngx_palloc(r->pool, hdrKey.via.str.size); + if (keyData == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "failed to allocate memory for key"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + ngx_memcpy(keyData, hdrKey.via.str.ptr, hdrKey.via.str.size); + hdr->Key.data = keyData; + hdr->Now = hdrNow.via.u64; + hdr->NowMonotonic = hdrNowMonotonic.via.u64; + } + ngx_zone_data->Header = hdr; + + entities *arr = ngx_pnalloc(r->pool, deserialized_size * sizeof(entities)); + ngx_zone_data->EntitiesSize = deserialized_size - 1; + ngx_zone_data->Entities = arr; + + for (uint32_t i = 1; i < deserialized_size; i++) { + if (items[i].via.array.size != 3) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "invalid number of items in array at index %d", i); + msgpack_zone_destroy(&mempool); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + msgpack_object iItemKey = items[i].via.array.ptr[0]; + msgpack_object iItemLast = items[i].via.array.ptr[1]; + msgpack_object iItemExcess = items[i].via.array.ptr[2]; + + u_char *keyData = ngx_palloc(r->pool, iItemKey.via.str.size); + if (keyData == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "failed to allocate memory for key"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + ngx_memcpy(keyData, iItemKey.via.str.ptr, iItemKey.via.str.size); + + arr[i - 1].Key.len = iItemKey.via.str.size; + arr[i - 1].Key.data = keyData; + arr[i - 1].Last = iItemLast.via.u64; + arr[i - 1].Excess = iItemExcess.via.u64; + } + msgpack_zone_destroy(&mempool); + return NGX_OK; +} + +static void ngx_http_limit_req_write_post_handler(ngx_http_request_t *r) { + ngx_int_t rc; + + rc = ngx_http_limit_req_write_handler(r); + + if (rc != NGX_OK) { + ngx_http_finalize_request(r, rc); + return; + } + + ngx_str_t response = ngx_string("OK\n"); + r->headers_out.status = NGX_HTTP_OK; + r->headers_out.content_length_n = response.len; + ngx_http_send_header(r); + + ngx_buf_t *b = ngx_create_temp_buf(r->pool, response.len); + ngx_memcpy(b->pos, response.data, response.len); + b->last = b->pos + response.len; + b->last_buf = 1; + + ngx_chain_t out = {.buf = b, .next = NULL}; + ngx_http_output_filter(r, &out); +} + +static ngx_int_t ngx_http_limit_req_write_handler(ngx_http_request_t *r) { + ngx_int_t rc, found; + ngx_zone_data_t *msg_pack = NULL; + ngx_str_t zone_name; + ngx_shm_zone_t *shm_zone; + ngx_http_limit_req_ctx_t *ctx; + ngx_str_t key; + size_t size; + uint32_t hash; + ngx_rbtree_node_t *node, *sentinel; + ngx_http_limit_req_node_t *lr = NULL; + + if (r != r->main) { + return NGX_DECLINED; + } + + msg_pack = ngx_pnalloc(r->pool, sizeof(ngx_zone_data_t)); + if (msg_pack == NULL) { + ngx_log_error( + NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: failed to allocate memory for msg_pack"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + rc = ngx_decode_msg_pack(r, msg_pack); + if (rc != NGX_OK) { + return rc; + } + + strip_zone_name_from_uri(&r->uri, &zone_name); + shm_zone = find_rate_limit_shm_zone_by_name(r, zone_name); + if (shm_zone == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: rate limit zone %*s not found", + zone_name); + return NGX_HTTP_NOT_FOUND; + } + + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: Header Key: %*s", + msg_pack->Header->Key); + for (uint32_t i = 0; i < msg_pack->EntitiesSize; i++) { + ctx = shm_zone->data; + ngx_shmtx_lock(&ctx->shpool->mutex); + + key = msg_pack->Entities[i].Key; + + hash = ngx_crc32_short(key.data, key.len); + + node = ctx->sh->rbtree.root; + sentinel = ctx->sh->rbtree.sentinel; + + found = 0; + while (node != sentinel) { + if (hash < node->key) { + node = node->left; + continue; + } + if (hash > node->key) { + node = node->right; + continue; + } + + /* hash == node->key */ + + lr = (ngx_http_limit_req_node_t *)&node->color; + + rc = ngx_memn2cmp(key.data, lr->data, key.len, lr->len); + + if (rc == 0) { + found = 1; + break; + } + + node = (rc < 0) ? node->left : node->right; + } + + if (found) { + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: existing node found %ul", + lr->excess); + lr->last = msg_pack->Entities[i].Last; + lr->excess = msg_pack->Entities[i].Excess; + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: existing node updated %ul", + lr->excess); + } else { + size = offsetof(ngx_rbtree_node_t, color) + + offsetof(ngx_http_limit_req_node_t, data) + key.len; + node = ngx_slab_alloc_locked(ctx->shpool, size); + if (node == NULL) { + ngx_shmtx_unlock(&ctx->shpool->mutex); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "failed to allocate memory for rate limit node"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + node->key = hash; + + lr = (ngx_http_limit_req_node_t *)&node->color; + + lr->len = (u_short)key.len; + lr->excess = msg_pack->Entities[i].Excess; + lr->last = msg_pack->Entities[i].Last; + lr->count = 0; + + ngx_memcpy(lr->data, key.data, key.len); + + ngx_rbtree_insert(&ctx->sh->rbtree, node); + + ngx_queue_insert_head(&ctx->sh->queue, &lr->queue); + + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: new node excess set to %ul", + lr->excess); + } + ngx_shmtx_unlock(&ctx->shpool->mutex); + } + return NGX_OK; +} + +static ngx_int_t ngx_http_limit_req_read_handler(ngx_http_request_t *r) { + ngx_str_t content_type, zone_name, last_greater_equal_arg; + ngx_int_t last_greater_equal; + ngx_int_t rc; + ngx_buf_t *b; + ngx_chain_t out; + ngx_http_core_loc_conf_t *clcf; + ngx_shm_zone_t *shm_zone; + + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + b = ngx_create_temp_buf(r->pool, 1024 * 1024); + if (b == NULL) { + ngx_log_error( + NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: failed to create temporary buffer"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + if (clcf == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: clcf is NULL"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: request URI: %*s", r->uri); + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: clcf->name: %*s", clcf->name); + if (clcf->name.len == r->uri.len) { + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: dumping rate limit zones"); + rc = dump_rate_limit_zones(r, b); + } else { + strip_zone_name_from_uri(&r->uri, &zone_name); + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, "zone name: %*s", + zone_name); + last_greater_equal_arg.len = 0; + last_greater_equal = 0; + if (r->args.len) { + if (ngx_http_arg(r, (u_char *)"last_greater_equal", 18, + &last_greater_equal_arg) == NGX_OK) { + last_greater_equal = + ngx_atoi(last_greater_equal_arg.data, last_greater_equal_arg.len); + if (last_greater_equal == NGX_ERROR || last_greater_equal < 0) { + return NGX_HTTP_BAD_REQUEST; + } + } + } + shm_zone = find_rate_limit_shm_zone_by_name(r, zone_name); + if (shm_zone == NULL) { + ngx_log_error( + NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: rate limit zone %*s not found", + zone_name); + return NGX_HTTP_NOT_FOUND; + } + rc = dump_req_limits(r->pool, shm_zone, b, last_greater_equal); + } + + if (rc != NGX_OK) { + return rc; + } + + ngx_str_set(&content_type, "application/vnd.msgpack"); + r->headers_out.content_type = content_type; + r->headers_out.content_length_n = b->last - b->pos; + r->headers_out.status = NGX_HTTP_OK; /* 200 OK */ + + b->last_buf = (r == r->main) ? 1 : 0; /* if subrequest 0 else 1 */ + b->last_in_chain = 1; + + out.buf = b; + out.next = NULL; + + rc = ngx_http_send_header(r); + if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { + return rc; + } + + return ngx_http_output_filter(r, &out); +} + +static char *ngx_http_limit_req_rw_handler(ngx_conf_t *cf, ngx_command_t *cmd, + void *conf) { + ngx_http_core_loc_conf_t *clcf; + + clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); + clcf->handler = ngx_http_limit_req_handler; + + return NGX_CONF_OK; +} + +static void strip_zone_name_from_uri(ngx_str_t *uri, ngx_str_t *zone_name) { + zone_name->data = (u_char *)ngx_strlchr(uri->data, uri->data + uri->len, '/'); + zone_name->len = 0; + + if (zone_name->data) { + zone_name->data = + (u_char *)(ngx_strlchr(zone_name->data + 1, uri->data + uri->len, '/') + + 1); + zone_name->len = uri->len - (zone_name->data - uri->data); + } +} + +static inline int msgpack_ngx_buf_write(void *data, const char *buf, + size_t len) { + ngx_buf_t *b = (ngx_buf_t *)data; + b->last = ngx_cpymem(b->last, buf, len); + return 0; +} + +static ngx_int_t dump_rate_limit_zones(ngx_http_request_t *r, ngx_buf_t *buf) { + ngx_array_t *zones; + ngx_str_t **zone_name; + ngx_uint_t i; + ngx_shm_zone_t *shm_zone; + volatile ngx_list_part_t *part; + + if (ngx_cycle == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: ngx_cycle is NULL"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + msgpack_packer pk; + msgpack_packer_init(&pk, buf, msgpack_ngx_buf_write); + zones = ngx_array_create(r->pool, 0, sizeof(ngx_str_t *)); + if (zones == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: failed to create zones array"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + part = &ngx_cycle->shared_memory.part; + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: part->nelts %d", part->nelts); + shm_zone = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: part->next is NULL, " + "breaking out of loop"); + break; + } + ngx_log_error( + NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: part->next is not NULL, advancing"); + part = part->next; + shm_zone = part->elts; + i = 0; + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: new part->nelts %d", + part->nelts); + } + + if (shm_zone == NULL) { + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: shm_zone is NULL, skipping"); + continue; + } + + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: comparing shm_zone tag"); + if (shm_zone[i].tag != &ngx_http_limit_req_module) { + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: shm_zone tag is not " + "limit_req_module, skipping"); + continue; + } + + ngx_log_error( + NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: pushing new zone struct to array"); + zone_name = ngx_array_push(zones); + if (zone_name == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: failed to push zone name"); + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: copying zone name"); + *zone_name = &shm_zone[i].shm.name; + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: zone name copied"); + } + + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: packing array of zones"); + msgpack_pack_array(&pk, zones->nelts); + zone_name = zones->elts; + for (i = 0; i < zones->nelts; i++) { + ngx_log_error(NGX_LOG_DEBUG, r->connection->log, 0, + "ngx_http_limit_req_rw_module: packing zone %*s", + *zone_name[i]); + msgpack_pack_str(&pk, zone_name[i]->len); + msgpack_pack_str_body(&pk, zone_name[i]->data, zone_name[i]->len); + } + + return NGX_OK; +} + +static ngx_shm_zone_t *find_rate_limit_shm_zone_by_name(ngx_http_request_t *r, + ngx_str_t zone_name) { + ngx_uint_t i; + ngx_shm_zone_t *shm_zone; + volatile ngx_list_part_t *part; + + if (ngx_cycle == NULL) { + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: ngx_cycle is NULL"); + return NULL; + } + + part = &ngx_cycle->shared_memory.part; + shm_zone = part->elts; + + for (i = 0; /* void */; i++) { + + if (i >= part->nelts) { + if (part->next == NULL) { + break; + } + part = part->next; + shm_zone = part->elts; + i = 0; + } + + if (shm_zone == NULL) { + continue; + } + + if (shm_zone[i].tag != &ngx_http_limit_req_module) { + continue; + } + + if (shm_zone[i].shm.name.len != zone_name.len) { + continue; + } + + if (ngx_strncmp(zone_name.data, shm_zone[i].shm.name.data, zone_name.len) == + 0) { + return shm_zone; + } + } + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, + "ngx_http_limit_req_rw_module: rate limit zone %*s not found", + zone_name); + return NULL; +} + +static ngx_int_t dump_req_limits(ngx_pool_t *pool, ngx_shm_zone_t *shm_zone, + ngx_buf_t *buf, + ngx_uint_t last_greater_equal) { + ngx_http_limit_req_ctx_t *ctx; + ngx_queue_t *head, *q, *last; + ngx_http_limit_req_node_t *lr; + time_t now, now_monotonic; + int i; + + now_monotonic = ngx_current_msec; + // retrieving current timestamp in milliseconds + now = ngx_cached_time->sec * 1000 + ngx_cached_time->msec; + + ctx = shm_zone->data; + + msgpack_packer pk; + msgpack_packer_init(&pk, buf, msgpack_ngx_buf_write); + + // Including header + msgpack_pack_array(&pk, 3); + msgpack_pack_str(&pk, ctx->key.value.len); + msgpack_pack_str_body(&pk, ctx->key.value.data, ctx->key.value.len); + msgpack_pack_uint64(&pk, now); + msgpack_pack_uint64(&pk, now_monotonic); + + ngx_shmtx_lock(&ctx->shpool->mutex); + + if (ngx_queue_empty(&ctx->sh->queue)) { + ngx_shmtx_unlock(&ctx->shpool->mutex); + return NGX_OK; + } + + head = ngx_queue_head(&ctx->sh->queue); + last = ngx_queue_last(head); + q = head; + + for (i = 0; q != last && i < MAX_NUMBER_OF_RATE_LIMIT_ELEMENTS; i++) { + lr = ngx_queue_data(q, ngx_http_limit_req_node_t, queue); + if (last_greater_equal != 0 && lr->last < last_greater_equal) { + break; + } + msgpack_pack_array(&pk, 3); + + msgpack_pack_str(&pk, lr->len); + msgpack_pack_str_body(&pk, lr->data, lr->len); + msgpack_pack_uint64(&pk, lr->last); + msgpack_pack_int(&pk, lr->excess); + + q = q->next; + } + + ngx_shmtx_unlock(&ctx->shpool->mutex); + + return NGX_OK; +} diff --git a/ngx_http_limit_req_module/reader-go/go.mod b/ngx_http_limit_req_module/reader-go/go.mod new file mode 100644 index 0000000..21708e3 --- /dev/null +++ b/ngx_http_limit_req_module/reader-go/go.mod @@ -0,0 +1,7 @@ +module reader-go + +go 1.23.2 + +require github.com/vmihailenco/msgpack/v5 v5.4.1 + +require github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect diff --git a/ngx_http_limit_req_module/reader-go/go.sum b/ngx_http_limit_req_module/reader-go/go.sum new file mode 100644 index 0000000..fd15c1b --- /dev/null +++ b/ngx_http_limit_req_module/reader-go/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ngx_http_limit_req_module/reader-go/main.go b/ngx_http_limit_req_module/reader-go/main.go new file mode 100644 index 0000000..ad3f08c --- /dev/null +++ b/ngx_http_limit_req_module/reader-go/main.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + + msgpack "github.com/vmihailenco/msgpack/v5" +) + +type RateLimitHeader struct { + Key string + Now int64 + NowMonotonic int64 +} + +type RateLimitEntry struct { + Key []byte + Last int64 + Excess int64 +} + +func main() { + argc := len(os.Args) + argv := os.Args + + file := "./out.bin" + if argc == 2 { + file = argv[1] + } + fmt.Printf("Decoding file %s\n", file) + + f, err := os.Open(file) + if err != nil { + fmt.Println("error opening file", err) + return + } + + decoder := msgpack.NewDecoder(f) + var header RateLimitHeader + entries := []RateLimitEntry{} + + if err := decoder.Decode(&header); err != nil { + log.Fatalln(err) + } + for { + var entry RateLimitEntry + if err := decoder.Decode(&entry); err != nil { + if err == io.EOF { + fmt.Println("EOF found") + break + } + log.Fatalln(err) + } + entries = append(entries, entry) + } + fmt.Println(header) + for _, entry := range entries { + fmt.Println(entry) + } + fmt.Println(len(entries)) +} diff --git a/ngx_http_limit_req_module/scripts/test.sh b/ngx_http_limit_req_module/scripts/test.sh new file mode 100644 index 0000000..8e2cb35 --- /dev/null +++ b/ngx_http_limit_req_module/scripts/test.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +test_ip() { + curl -v --parallel --parallel-immediate \ + "http://$1:8888/$2" \ + "http://$1:8888/$2" \ + "http://$1:8888/$2" \ + "http://$1:8888/$2" \ + "http://$1:8888/$2" +} + +echo "Detecting IP addresses on all network interfaces..." +echo "==================================================" + +# Determine the operating system +if [[ "$(uname)" == "Darwin" ]]; then + # macOS + echo "Detected macOS system" + echo "" + + # Get list of network interfaces + interfaces=$(ifconfig -l) + + for interface in $interfaces; do + # Skip loopback and inactive interfaces + if [[ "$interface" != "lo0" && "$(ifconfig $interface 2>/dev/null | grep 'status: active' 2>/dev/null)" != "" ]]; then + echo "Interface: $interface" + + # Get IPv4 addresses + ipv4=$(ifconfig $interface | grep inet | grep -v inet6 | awk '{print $2}') + if [[ -n "$ipv4" ]]; then + test_ip $ipv4 "one" + test_ip $ipv4 "two" + fi + fi + done + +elif [[ "$(uname)" == "Linux" ]]; then + # Linux + echo "Detected Linux system" + echo "" + + # Check if 'ip' command is available, otherwise use ifconfig + if command -v ip &>/dev/null; then + # Using 'ip' command (modern Linux) + interfaces=$(ip -o link show | awk -F': ' '{print $2}') + + for interface in $interfaces; do + # Skip loopback + if [[ "$interface" != "lo" ]]; then + # Check if interface is up + if [[ "$(ip link show dev $interface | grep 'state UP')" != "" ]]; then + echo "Interface: $interface" + + # Get IPv4 addresses + ipv4=$(ip -4 addr show dev $interface | grep inet | awk '{print $2}') + if [[ -n "$ipv4" ]]; then + test_ip $ipv4 "one" + test_ip $ipv4 "two" + fi + fi + fi + done + + else + # Using 'ifconfig' (older Linux distributions) + interfaces=$(ifconfig | grep -E '^[a-zA-Z0-9]+:' | awk '{print $1}' | sed 's/://') + + for interface in $interfaces; do + # Skip loopback + if [[ "$interface" != "lo" ]]; then + echo "Interface: $interface" + + # Get IPv4 addresses + ipv4=$(ifconfig $interface | grep inet | grep -v inet6 | awk '{print $2}' | sed 's/addr://') + if [[ -n "$ipv4" ]]; then + test_ip $ipv4 + fi + fi + done + fi + +else + echo "Unsupported operating system: $(uname)" + exit 1 +fi + +test_ip "127.0.0.1" "one" + +echo "API Request" + +curl "http://localhost:9000/api/one" -v --output one.bin +curl "http://localhost:9000/api/two" -v --output two.bin