diff --git a/README b/README deleted file mode 100644 index 5d6af5e..0000000 --- a/README +++ /dev/null @@ -1,253 +0,0 @@ -== nginx-rtmp-module == - -NGINX-based RTMP server - -Project page: - - http://arut.github.com/nginx-rtmp-module - -Wiki manual: - - https://github.com/arut/nginx-rtmp-module/wiki - -Features: - -* Live streaming of video/audio - -* Video on demand (FLV) - -* Stream relay support for distributed - streaming: push & pull models - -* Recording published streams in FLV file - -* H264/AAC support - -* Online transcoding with FFmpeg - (experimental; Linux only) - -* HLS (HTTP Live Streaming) support - (experimental; libavformat >= 53.31.100) - -* HTTP callbacks on publish/play/record - -* Advanced buffering techniques - to keep memory allocations at a minimum - level for faster streaming and low - memory footprint - -* Works with Flash RTMP clients as well as - ffmpeg/rtmpdump/flvstreamer etc - (see examples in test/ subdir) - -* Statistics in XML/XSL in machine- & human- - readable form - - -Build: - -cd to NGINX source directory & run this: - -./configure --add-module= -make -make install - - -Known issue: - - The module does not share data between workers. - Because of this live streaming is only available - in one-worker mode so far. Video-on-demand has no - such limitations. - - -RTMP URL format: - -rtmp://rtmp.example.com/[/] - - - should match one of application {} - blocks in config - - interpreted by each application - can be empty - - -Example nginx.conf: - -rtmp { - - server { - - listen 1935; - - chunk_size 4000; - - # TV mode: one publisher, many subscribers - application mytv { - - # enable live streaming - live on; - - # record first 1K of stream - record all; - record_path /tmp/av; - record_max_size 1K; - - # append current timestamp to each flv - record_unique on; - - # publish only from localhost - allow publish 127.0.0.1; - deny publish all; - - #allow play all; - } - - # Transcoding (ffmpeg needed) - application big { - live on; - - # On every pusblished stream run this command (ffmpeg) - # with substitutions: $app/${app}, $name/${name} for application & stream name. - # - # This ffmpeg call receives stream from this application & - # reduces the resolution down to 32x32. The stream is the published to - # 'small' application (see below) under the same name. - # - # ffmpeg can do anything with the stream like video/audio - # transcoding, resizing, altering container/codec params etc - # - # Multiple exec lines can be specified. - - exec /usr/bin/ffmpeg -re -i rtmp://localhost:1935/$app/$name -vcodec flv -acodec copy -s 32x32 -f flv rtmp://localhost:1935/small/${name}; - } - - application small { - live on; - # Video with reduced resolution comes here from ffmpeg - } - - application mypush { - live on; - - # Every stream published here - # is automatically pushed to - # these two machines - push rtmp1.example.com; - push rtmp2.example.com:1934; - } - - application mypull { - live on; - - # Pull all streams from remote machine - # and play locally - pull rtmp://rtmp3.example.com pageUrl=www.example.com/index.html; - } - - # video on demand - application vod { - play /var/flvs; - } - - # Many publishers, many subscribers - # no checks, no recording - application videochat { - - live on; - - # The following notifications receive all - # the session variables as well as - # particular call arguments in HTTP POST - # request - - # Make HTTP request & use HTTP retcode - # to decide whether to allow publishing - # from this connection or not - on_publish http://localhost:8080/publish; - - # Same with playing - on_play http://localhost:8080/play; - - # Publish/play end (repeats on disconnect) - on_done http://localhost:8080/done; - - # All above mentioned notifications receive - # standard connect() arguments as well as - # play/publish ones. If any arguments are sent - # with GET-style syntax to play & publish - # these are also included. - # Example URL: - # rtmp://localhost/myapp/mystream?a=b&c=d - - # record 10 video keyframes (no audio) every 2 minutes - record keyframes; - record_path /tmp/vc; - record_max_frames 10; - record_interval 2m; - - # Async notify about an flv recorded - on_record_done http://localhost:8080/record_done; - - } - - - # HLS (experimental) - - # HLS requires libavformat & should be configured as a separate - # NGINX module in addition to nginx-rtmp-module: - # ./configure ... --add-module=/path/to/nginx-rtmp-module/hls ... - - # For HLS to work please create a directory in tmpfs (/tmp/app here) - # for the fragments. The directory contents is served via HTTP (see - # http{} section in config) - # - # Incoming stream must be in H264/AAC/MP3. For iPhones use baseline H264 - # profile (see ffmpeg example). - # This example creates RTMP stream from movie ready for HLS: - # - # ffmpeg -loglevel verbose -re -i movie.avi -vcodec libx264 - # -vprofile baseline -acodec libmp3lame -ar 44100 -ac 1 - # -f flv rtmp://localhost:1935/hls/movie - # - # If you need to transcode live stream use 'exec' feature. - # - application hls { - hls on; - hls_path /tmp/app; - hls_fragment 5s; - } - - } -} - -# HTTP can be used for accessing RTMP stats -http { - - server { - - listen 8080; - - # This URL provides RTMP statistics in XML - location /stat { - rtmp_stat all; - - # Use this stylesheet to view XML as web page - # in browser - rtmp_stat_stylesheet stat.xsl; - } - - location /stat.xsl { - # XML stylesheet to view RTMP stats. - # Copy stat.xsl wherever you want - # and put the full directory path here - root /path/to/stat.xsl/; - } - - location /hls { - # Serve HLS fragments - alias /tmp/app; - } - - } -} - diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cedfe0 --- /dev/null +++ b/README.md @@ -0,0 +1,262 @@ +# NGINX-based RTMP server +## nginx-rtmp-module + + +### Project page: + + http://arut.github.com/nginx-rtmp-module + +### Wiki manual: + + https://github.com/arut/nginx-rtmp-module/wiki + +### Features: + +* Live streaming of video/audio + +* Video on demand FLV/MP4 + +* Stream relay support for distributed + streaming: push & pull models + +* Recording published streams in FLV file + +* H264/AAC support + +* Online transcoding with FFmpeg + +* HLS (HTTP Live Streaming) support; + experimental; requires recent libavformat + (>= 53.31.100) from ffmpeg (ffmpeg.org) + +* HTTP callbacks on publish/play/record + +* Advanced buffering techniques + to keep memory allocations at a minimum + level for faster streaming and low + memory footprint + +* Proved to work with Wirecast,FMS,Wowza, + JWPlayer,FlowPlayer,StrobeMediaPlayback, + ffmpeg,avconv,rtmpdump,flvstreamer + and many more + +* Statistics in XML/XSL in machine- & human- + readable form + + +### Build: + +cd to NGINX source directory & run this: + + ./configure --add-module= + make + make install + + +### Known issue: + + The module does not share data between workers. + Because of this live streaming is only available + in one-worker mode so far. Video-on-demand has no + such limitations. + + You can try auto-push branch with multi-worker + support if you really need that. + + +### RTMP URL format: + + rtmp://rtmp.example.com/app[/name] + +app - should match one of application {} + blocks in config + +name - interpreted by each application + can be empty + + +### Example nginx.conf: + + rtmp { + + server { + + listen 1935; + + chunk_size 4000; + + # TV mode: one publisher, many subscribers + application mytv { + + # enable live streaming + live on; + + # record first 1K of stream + record all; + record_path /tmp/av; + record_max_size 1K; + + # append current timestamp to each flv + record_unique on; + + # publish only from localhost + allow publish 127.0.0.1; + deny publish all; + + #allow play all; + } + + # Transcoding (ffmpeg needed) + application big { + live on; + + # On every pusblished stream run this command (ffmpeg) + # with substitutions: $app/${app}, $name/${name} for application & stream name. + # + # This ffmpeg call receives stream from this application & + # reduces the resolution down to 32x32. The stream is the published to + # 'small' application (see below) under the same name. + # + # ffmpeg can do anything with the stream like video/audio + # transcoding, resizing, altering container/codec params etc + # + # Multiple exec lines can be specified. + + exec /usr/bin/ffmpeg -re -i rtmp://localhost:1935/$app/$name -vcodec flv -acodec copy -s 32x32 -f flv rtmp://localhost:1935/small/${name}; + } + + application small { + live on; + # Video with reduced resolution comes here from ffmpeg + } + + application mypush { + live on; + + # Every stream published here + # is automatically pushed to + # these two machines + push rtmp1.example.com; + push rtmp2.example.com:1934; + } + + application mypull { + live on; + + # Pull all streams from remote machine + # and play locally + pull rtmp://rtmp3.example.com pageUrl=www.example.com/index.html; + } + + # video on demand + application vod { + play /var/flvs; + } + + application vod2 { + play /var/mp4s; + } + + # Many publishers, many subscribers + # no checks, no recording + application videochat { + + live on; + + # The following notifications receive all + # the session variables as well as + # particular call arguments in HTTP POST + # request + + # Make HTTP request & use HTTP retcode + # to decide whether to allow publishing + # from this connection or not + on_publish http://localhost:8080/publish; + + # Same with playing + on_play http://localhost:8080/play; + + # Publish/play end (repeats on disconnect) + on_done http://localhost:8080/done; + + # All above mentioned notifications receive + # standard connect() arguments as well as + # play/publish ones. If any arguments are sent + # with GET-style syntax to play & publish + # these are also included. + # Example URL: + # rtmp://localhost/myapp/mystream?a=b&c=d + + # record 10 video keyframes (no audio) every 2 minutes + record keyframes; + record_path /tmp/vc; + record_max_frames 10; + record_interval 2m; + + # Async notify about an flv recorded + on_record_done http://localhost:8080/record_done; + + } + + + # HLS (experimental) + + # HLS requires libavformat & should be configured as a separate + # NGINX module in addition to nginx-rtmp-module: + # ./configure ... --add-module=/path/to/nginx-rtmp-module/hls ... + + # For HLS to work please create a directory in tmpfs (/tmp/app here) + # for the fragments. The directory contents is served via HTTP (see + # http{} section in config) + # + # Incoming stream must be in H264/AAC/MP3. For iPhones use baseline H264 + # profile (see ffmpeg example). + # This example creates RTMP stream from movie ready for HLS: + # + # ffmpeg -loglevel verbose -re -i movie.avi -vcodec libx264 + # -vprofile baseline -acodec libmp3lame -ar 44100 -ac 1 + # -f flv rtmp://localhost:1935/hls/movie + # + # If you need to transcode live stream use 'exec' feature. + # + application hls { + hls on; + hls_path /tmp/app; + hls_fragment 5s; + } + + } + } + + # HTTP can be used for accessing RTMP stats + http { + + server { + + listen 8080; + + # This URL provides RTMP statistics in XML + location /stat { + rtmp_stat all; + + # Use this stylesheet to view XML as web page + # in browser + rtmp_stat_stylesheet stat.xsl; + } + + location /stat.xsl { + # XML stylesheet to view RTMP stats. + # Copy stat.xsl wherever you want + # and put the full directory path here + root /path/to/stat.xsl/; + } + + location /hls { + # Serve HLS fragments + alias /tmp/app; + } + + } + } + diff --git a/TODO b/TODO index e26a2a8..9c04cd7 100644 --- a/TODO +++ b/TODO @@ -1,14 +1,14 @@ -- Add per-client audio/video bias to stats; - implement synchronization after frames were dropped +- Auto-pushing pulled stream -- File path checks in record & play modules - (check for '/../', maybe smth else) +- Pull secondary address support + +- Binary search in play module - More Wiki docs -- Auto-relays (multi-worker) -- Binary search in play module +Style: +====== - Move out & merge stream ids from live & cmd modules diff --git a/config b/config index 16e0bf9..384c772 100644 --- a/config +++ b/config @@ -6,14 +6,15 @@ CORE_MODULES="$CORE_MODULES ngx_rtmp_cmd_module \ ngx_rtmp_access_module \ ngx_rtmp_live_module \ + ngx_rtmp_play_module \ + ngx_rtmp_flv_module \ + ngx_rtmp_mp4_module \ ngx_rtmp_record_module \ ngx_rtmp_netcall_module \ ngx_rtmp_notify_module \ ngx_rtmp_relay_module \ ngx_rtmp_exec_module \ ngx_rtmp_codec_module \ - ngx_rtmp_play_module \ - ngx_rtmp_auto_push_module \ " @@ -35,6 +36,9 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $ngx_addon_dir/ngx_rtmp_cmd_module.c \ $ngx_addon_dir/ngx_rtmp_access_module.c \ $ngx_addon_dir/ngx_rtmp_live_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_record_module.c \ $ngx_addon_dir/ngx_rtmp_netcall_module.c \ $ngx_addon_dir/ngx_rtmp_notify_module.c \ @@ -43,7 +47,6 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $ngx_addon_dir/ngx_rtmp_bandwidth.c \ $ngx_addon_dir/ngx_rtmp_exec_module.c \ $ngx_addon_dir/ngx_rtmp_codec_module.c \ - $ngx_addon_dir/ngx_rtmp_play_module.c \ $ngx_addon_dir/ngx_rtmp_auto_push_module.c \ " CFLAGS="$CFLAGS -I$ngx_addon_dir" diff --git a/hls/README.md b/hls/README.md new file mode 100644 index 0000000..df3915f --- /dev/null +++ b/hls/README.md @@ -0,0 +1,9 @@ +# HLS (HTTP Live Streaming) module + +This module should be added explicitly when building NGINX: + + ./configure ... --add-module=/path/to/nginx-rtmp-module/hls ... + +## Requirement + +The module requires ffmpeg version>= 53.31.100 from ffmpeg (ffmpeg.org) diff --git a/hls/config b/hls/config index afadaa3..24624cb 100644 --- a/hls/config +++ b/hls/config @@ -9,5 +9,5 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $ngx_addon_dir/ngx_rtmp_hls_module.c \ " -CORE_LIBS="$CORE_LIBS -lavformat" +CORE_LIBS="$CORE_LIBS -lavformat -lavcodec -lavutil" diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index 99baa5c..f7ecbfb 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -49,6 +49,8 @@ ngx_rtmp_hls_av_log_callback(void* avcl, int level, const char* fmt, #define NGX_RTMP_HLS_BUFSIZE (1024*1024) +#define NGX_RTMP_HLS_DIR_ACCESS 0744 + typedef struct { ngx_uint_t flags; @@ -58,6 +60,7 @@ typedef struct { unsigned opened:1; unsigned audio:1; unsigned video:1; + unsigned header_sent:1; ngx_str_t playlist; ngx_str_t playlist_bak; @@ -78,6 +81,7 @@ typedef struct { typedef struct { ngx_flag_t hls; ngx_msec_t fraglen; + ngx_msec_t muxdelay; ngx_msec_t playlen; size_t nfrags; ngx_rtmp_hls_ctx_t **ctx; @@ -116,6 +120,14 @@ static ngx_command_t ngx_rtmp_hls_commands[] = { 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_null_command }; @@ -262,7 +274,7 @@ ngx_rtmp_hls_init_video(ngx_rtmp_session_t *s) stream->codec->codec_id = CODEC_ID_H264; stream->codec->codec_type = AVMEDIA_TYPE_VIDEO; stream->codec->pix_fmt = PIX_FMT_YUV420P; - stream->codec->time_base.den = 1; + stream->codec->time_base.den = 25; stream->codec->time_base.num = 1; stream->codec->width = 100; stream->codec->height = 100; @@ -277,6 +289,14 @@ ngx_rtmp_hls_init_video(ngx_rtmp_session_t *s) ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: video stream: %i", ctx->out_vstream); + if (ctx->header_sent) { + if (av_write_trailer(ctx->out_format) < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: av_write_trailer failed"); + } + ctx->header_sent = 0; + } + return NGX_OK; } @@ -287,6 +307,7 @@ ngx_rtmp_hls_init_audio(ngx_rtmp_session_t *s) AVStream *stream; ngx_rtmp_hls_ctx_t *ctx; ngx_rtmp_codec_ctx_t *codec_ctx; + enum CodecID cid; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); @@ -299,6 +320,13 @@ ngx_rtmp_hls_init_audio(ngx_rtmp_session_t *s) ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: adding audio stream"); + cid = ngx_rtmp_hls_get_audio_codec(codec_ctx->audio_codec_id); + if (cid == CODEC_ID_NONE) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: no audio"); + return NGX_OK; + } + stream = avformat_new_stream(ctx->out_format, NULL); if (stream == NULL) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, @@ -306,17 +334,12 @@ ngx_rtmp_hls_init_audio(ngx_rtmp_session_t *s) return NGX_ERROR; } - stream->codec->codec_id = ngx_rtmp_hls_get_audio_codec( - codec_ctx->audio_codec_id); - if (stream->codec->codec_id == CODEC_ID_NONE) { - return NGX_OK; - } - + stream->codec->codec_id = cid; stream->codec->codec_type = AVMEDIA_TYPE_AUDIO; stream->codec->sample_fmt = (codec_ctx->sample_size == 1 ? AV_SAMPLE_FMT_U8 : AV_SAMPLE_FMT_S16); stream->codec->sample_rate = 48000;/*codec_ctx->sample_rate;*/ - stream->codec->bit_rate = 128000; + stream->codec->bit_rate = 2000000; stream->codec->channels = codec_ctx->audio_channels; ctx->out_astream = stream->index; @@ -326,6 +349,14 @@ ngx_rtmp_hls_init_audio(ngx_rtmp_session_t *s) "hls: audio stream: %i %iHz", ctx->out_astream, codec_ctx->sample_rate); + if (ctx->header_sent) { + if (av_write_trailer(ctx->out_format) < 0) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "hls: av_write_trailer failed"); + } + ctx->header_sent = 0; + } + return NGX_OK; } @@ -340,17 +371,31 @@ ngx_rtmp_hls_update_playlist(ngx_rtmp_session_t *s) ssize_t n; ngx_int_t ffrag; ngx_rtmp_hls_app_conf_t *hacf; + ngx_int_t nretry; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + nretry = 0; + +retry: 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: open failed: '%V'", &ctx->playlist_bak); + /* try to create parent folder */ + if (nretry == 0 && + ngx_create_dir(hacf->path.data, NGX_RTMP_HLS_DIR_ACCESS) != + NGX_INVALID_FILE) + { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "hls: creating target folder: '%V'", &hacf->path); + ++nretry; + goto retry; + } return NGX_ERROR; } @@ -406,8 +451,10 @@ static ngx_int_t ngx_rtmp_hls_initialize(ngx_rtmp_session_t *s) { ngx_rtmp_hls_ctx_t *ctx; + ngx_rtmp_hls_app_conf_t *hacf; AVOutputFormat *format; + 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 == NULL || ctx->out_format || ctx->publishing == 0) { return NGX_OK; @@ -430,6 +477,7 @@ ngx_rtmp_hls_initialize(ngx_rtmp_session_t *s) return NGX_ERROR; } ctx->out_format->oformat = format; + ctx->out_format->max_delay = (int64_t)hacf->muxdelay * AV_TIME_BASE / 1000; return NGX_ERROR; } @@ -449,6 +497,10 @@ ngx_rtmp_hls_open_file(ngx_rtmp_session_t *s, u_char *fpath) return NGX_OK; } + if (!ctx->video && !ctx->audio) { + return NGX_OK; + } + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "hls: open stream file '%s'", fpath); @@ -475,11 +527,14 @@ ngx_rtmp_hls_open_file(ngx_rtmp_session_t *s, u_char *fpath) } /* write header */ - if (avformat_write_header(ctx->out_format, NULL) < 0) { + if (!ctx->header_sent && + avformat_write_header(ctx->out_format, NULL) < 0) + { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: avformat_write_header failed"); return NGX_ERROR; } + ctx->header_sent = 1; if (astream) { astream->codec->extradata = NULL; @@ -651,10 +706,11 @@ ngx_rtmp_hls_close_file(ngx_rtmp_session_t *s) ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); + /* if (av_write_trailer(ctx->out_format) < 0) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "hls: av_write_trailer failed"); - } + }*/ avio_flush(ctx->out_format->pb); @@ -878,7 +934,7 @@ ngx_rtmp_hls_audio(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, /* write to file */ av_init_packet(&packet); - packet.dts = h->timestamp * 90; + packet.dts = h->timestamp * 90L; packet.pts = packet.dts; packet.stream_index = ctx->out_astream; packet.data = buffer; @@ -918,6 +974,7 @@ ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, uint32_t len, rlen; ngx_buf_t out; static u_char buffer[NGX_RTMP_HLS_BUFSIZE]; + int32_t cts; hacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_hls_module); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_hls_module); @@ -953,9 +1010,11 @@ ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, } /* 3 bytes: decoder delay */ - if (ngx_rtmp_hls_copy(s, NULL, &p, 3, &in) != NGX_OK) { + if (ngx_rtmp_hls_copy(s, &cts, &p, 3, &in) != NGX_OK) { return NGX_ERROR; } + cts = ((cts & 0x00FF0000) >> 16) | ((cts & 0x000000FF) << 16) + | (cts & 0x0000FF00); out.pos = buffer; out.last = buffer + sizeof(buffer) - FF_INPUT_BUFFER_PADDING_SIZE; @@ -1027,13 +1086,14 @@ ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, } av_init_packet(&packet); - packet.dts = h->timestamp * 90; - packet.pts = packet.dts; + packet.dts = h->timestamp * 90L; + packet.pts = packet.dts + cts * 90; packet.stream_index = ctx->out_vstream; - /* + if (ftype == 1) { packet.flags |= AV_PKT_FLAG_KEY; - }*/ + } + packet.data = buffer; packet.size = out.pos - buffer; @@ -1058,6 +1118,7 @@ ngx_rtmp_hls_create_app_conf(ngx_conf_t *cf) conf->hls = NGX_CONF_UNSET; conf->fraglen = NGX_CONF_UNSET; + conf->muxdelay = NGX_CONF_UNSET; conf->playlen = NGX_CONF_UNSET; conf->nbuckets = 1024; @@ -1073,6 +1134,7 @@ ngx_rtmp_hls_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) ngx_conf_merge_value(conf->hls, prev->hls, 0); ngx_conf_merge_msec_value(conf->fraglen, prev->fraglen, 5000); + ngx_conf_merge_msec_value(conf->muxdelay, prev->muxdelay, 700); ngx_conf_merge_msec_value(conf->playlen, prev->playlen, 30000); ngx_conf_merge_str_value(conf->path, prev->path, ""); conf->ctx = ngx_pcalloc(cf->pool, diff --git a/ngx_rtmp.h b/ngx_rtmp.h index 015acfb..0f0decb 100644 --- a/ngx_rtmp.h +++ b/ngx_rtmp.h @@ -178,6 +178,8 @@ typedef struct { ngx_str_t *addr_text; int connected; + ngx_event_t *posted_dry_events; + /* client buffer time in msec */ uint32_t buflen; @@ -378,6 +380,27 @@ void * ngx_rtmp_rmemcpy(void *dst, const void* src, size_t n); (((u_char*)ngx_rtmp_rmemcpy(dst, src, n)) + (n)) +static inline uint16_t +ngx_rtmp_r16(uint16_t n) +{ + return (n << 8) | (n >> 8); +} + + +static inline uint32_t +ngx_rtmp_r32(uint32_t n) +{ + return (n << 24) | ((n << 8) & 0xff0000) | ((n >> 8) & 0xff00) | (n >> 24); +} + + +static inline uint64_t +ngx_rtmp_r64(uint64_t n) +{ + return (uint64_t) ngx_rtmp_r32(n) << 32 | ngx_rtmp_r32(n >> 32); +} + + /* Receiving messages */ ngx_int_t ngx_rtmp_protocol_message_handler(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, ngx_chain_t *in); @@ -472,6 +495,10 @@ ngx_int_t ngx_rtmp_send_amf(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, 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); +/* AMF status sender */ +ngx_int_t ngx_rtmp_send_status(ngx_rtmp_session_t *s, char *code, + char* level, char *desc); + /* Frame types */ #define NGX_RTMP_VIDEO_KEY_FRAME 1 diff --git a/ngx_rtmp_cmd_module.c b/ngx_rtmp_cmd_module.c index a0733d0..1995092 100644 --- a/ngx_rtmp_cmd_module.c +++ b/ngx_rtmp_cmd_module.c @@ -3,15 +3,12 @@ */ #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 -#define NGX_RTMP_CMD_CSID_AMF_INI 3 -#define NGX_RTMP_CMD_CSID_AMF 5 -#define NGX_RTMP_CMD_MSID 1 - ngx_rtmp_connect_pt ngx_rtmp_connect; ngx_rtmp_create_stream_pt ngx_rtmp_create_stream; @@ -494,14 +491,14 @@ ngx_rtmp_cmd_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) static ngx_rtmp_amf_elt_t out_inf[] = { - { NGX_RTMP_AMF_STRING, - ngx_string("code"), - "NetStream.Publish.Start", 0 }, - { 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"), "Publish succeeded.", 0 }, diff --git a/ngx_rtmp_codec_module.c b/ngx_rtmp_codec_module.c index b6bc084..25ef0a9 100644 --- a/ngx_rtmp_codec_module.c +++ b/ngx_rtmp_codec_module.c @@ -200,6 +200,8 @@ ngx_rtmp_codec_av(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); header = NULL; + pheader = NULL; + version = NULL; if (h->type == NGX_RTMP_MSG_AUDIO) { if (ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC) { header = &ctx->aac_header; diff --git a/ngx_rtmp_core_module.c b/ngx_rtmp_core_module.c index 9fe9ab8..e57a16c 100644 --- a/ngx_rtmp_core_module.c +++ b/ngx_rtmp_core_module.c @@ -571,7 +571,7 @@ ngx_rtmp_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) ls->ipv6only = 1; } else if (ngx_strcmp(&value[i].data[10], "ff") == 0) { - ls->ipv6only = 2; + ls->ipv6only = 0; } else { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, diff --git a/ngx_rtmp_exec_module.c b/ngx_rtmp_exec_module.c index 84f94a6..d4b73a5 100644 --- a/ngx_rtmp_exec_module.c +++ b/ngx_rtmp_exec_module.c @@ -4,7 +4,10 @@ #include "ngx_rtmp_cmd_module.h" +#include +#ifdef HAVE_MALLOC_H #include +#endif #ifdef NGX_LINUX #include @@ -317,7 +320,7 @@ dollar: static ngx_int_t ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n) { -#ifdef NGX_LINUX +#ifndef NGX_WIN32 ngx_rtmp_exec_app_conf_t *eacf; ngx_rtmp_exec_ctx_t *ctx; int pid; @@ -413,7 +416,7 @@ ngx_rtmp_exec_run(ngx_rtmp_session_t *s, size_t n) &ec->cmd, (ngx_uint_t)pid); break; } -#endif /* NGX_LINUX */ +#endif /* NGX_WIN32 */ return NGX_OK; } diff --git a/ngx_rtmp_flv_module.c b/ngx_rtmp_flv_module.c new file mode 100644 index 0000000..9e8163e --- /dev/null +++ b/ngx_rtmp_flv_module.c @@ -0,0 +1,642 @@ +/* + * Copyright (c) 2012 Roman Arutyunyan + */ + + +#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); +static ngx_int_t ngx_rtmp_flv_start(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); + + +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_DEFAULT_BUFLEN 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; + } + + ngx_rtmp_rmemcpy(&nelts, b->pos + ctx->offset, 4); + + 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) +{ + double v; + + ngx_rtmp_rmemcpy(&v, src, 8); + + return v; +} + + +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_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_LIVE_MSID; + h.csid = NGX_RTMP_LIVE_CSID_META; + + size = 0; + ngx_rtmp_rmemcpy(&size, ngx_rtmp_flv_header + 1, 3); + + 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_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_LIVE_MSID; + h.type = ngx_rtmp_flv_header[0]; + + size = 0; + + ngx_rtmp_rmemcpy(&size, ngx_rtmp_flv_header + 1, 3); + ngx_rtmp_rmemcpy(&h.timestamp, ngx_rtmp_flv_header + 4, 3); + + ((u_char *) &h.timestamp)[3] = ngx_rtmp_flv_header[7]; + + 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 & (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 |= (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 ? s->buflen : NGX_RTMP_FLV_DEFAULT_BUFLEN); + 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); + + /* 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_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_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: start timestamp=%ui", timestamp); + + ctx->start_timestamp = timestamp; + 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->stop = ngx_rtmp_flv_stop; + fmt->send = ngx_rtmp_flv_send; + + return NGX_OK; +} diff --git a/ngx_rtmp_handler.c b/ngx_rtmp_handler.c index b06dd01..79465f7 100644 --- a/ngx_rtmp_handler.c +++ b/ngx_rtmp_handler.c @@ -537,6 +537,8 @@ ngx_rtmp_send(ngx_event_t *wev) if (wev->active) { ngx_del_event(wev, NGX_WRITE_EVENT, 0); } + + ngx_event_process_posted((ngx_cycle_t *) ngx_cycle, &s->posted_dry_events); } diff --git a/ngx_rtmp_live_module.h b/ngx_rtmp_live_module.h index 343efff..fdc5ffd 100644 --- a/ngx_rtmp_live_module.h +++ b/ngx_rtmp_live_module.h @@ -10,19 +10,13 @@ #include "ngx_rtmp.h" #include "ngx_rtmp_cmd_module.h" #include "ngx_rtmp_bandwidth.h" +#include "ngx_rtmp_streams.h" /* session flags */ #define NGX_RTMP_LIVE_PUBLISHING 0x01 -/* Chunk stream ids for output */ -#define NGX_RTMP_LIVE_CSID_META 5 -#define NGX_RTMP_LIVE_CSID_AUDIO 6 -#define NGX_RTMP_LIVE_CSID_VIDEO 7 -#define NGX_RTMP_LIVE_MSID 1 - - typedef struct ngx_rtmp_live_ctx_s ngx_rtmp_live_ctx_t; typedef struct ngx_rtmp_live_stream_s ngx_rtmp_live_stream_t; diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c new file mode 100644 index 0000000..92b35bb --- /dev/null +++ b/ngx_rtmp_mp4_module.c @@ -0,0 +1,2299 @@ +/* + * Copyright (c) 2012 Roman Arutyunyan + */ + + +#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); +static ngx_int_t ngx_rtmp_mp4_done(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_file_t *f, + ngx_uint_t offset); +static ngx_int_t ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s, ngx_file_t *f); +static ngx_int_t ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f); + + +#pragma pack(push,4) + + +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; + +#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; + + 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; + + uint32_t start_timestamp, epoch; +} ngx_rtmp_mp4_ctx_t; + + +#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) + + +static inline uint32_t +ngx_rtmp_mp4_to_rtmp_timestamp(ngx_rtmp_mp4_track_t *t, uint32_t ts) +{ + return (uint64_t) ts * 1000 / t->time_scale; +} + + +static 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_DEFAULT_BUFLEN 1000 + + +static u_char ngx_rtmp_mp4_buffer[1024*1024]; + + +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 } +}; + + +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); + ++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 = 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; + + 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 += 16; + + 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_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: audio settings codec=%i, nchannels==%ui, " + "sample_size=%ui, sample_rate=%ui", + codec, ctx->nchannels, ctx->sample_size, ctx->sample_rate); + + 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->times->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]); + 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 = 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 = ngx_rtmp_r32(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; + + cr = &t->cursor; + + 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 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; + } + + 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; + + 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)); + + 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_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; + ngx_rtmp_mp4_cursor_t *cr; + uint32_t buflen, end_timestamp, sched, + timestamp, last_timestamp; + ssize_t ret; + u_char fhdr[5]; + size_t fhdr_size; + ngx_int_t rc; + ngx_uint_t n, active; + + 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 ? s->buflen : NGX_RTMP_MP4_DEFAULT_BUFLEN); + + t = ctx->tracks; + + sched = 0; + active = 0; + + end_timestamp = ctx->start_timestamp + + (ngx_current_msec - ctx->epoch) + buflen; + + for (n = 0; n < ctx->ntracks; ++n, ++t) { + cr = &t->cursor; + + if (!cr->valid) { + continue; + } + + timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->timestamp); + + 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); + goto next; + } + + last_timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->last_timestamp); + + ngx_memzero(&h, sizeof(h)); + + h.msid = NGX_RTMP_MSID; + h.type = 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; + + goto next; + } + + 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; + ngx_rtmp_mp4_buffer[1] = 1; + ngx_rtmp_mp4_buffer[2] = cr->delay & 0xf00; + ngx_rtmp_mp4_buffer[3] = cr->delay & 0x0f0; + ngx_rtmp_mp4_buffer[4] = cr->delay & 0x00f; + } + + } 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)); + continue; + } + + 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); + continue; + } + + 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; + } + + if (ngx_rtmp_mp4_next(s, t) != NGX_OK) { + continue; + } + +next: + active = 1; + + if (timestamp > end_timestamp && + (sched == 0 || timestamp < end_timestamp + sched)) + { + sched = (uint32_t) (timestamp - end_timestamp); + } + } + + if (sched) { + return sched; + } + + return active ? NGX_OK : NGX_DONE; +} + + +static ngx_int_t +ngx_rtmp_mp4_init(ngx_rtmp_session_t *s, ngx_file_t *f) +{ + ngx_rtmp_mp4_ctx_t *ctx; + uint32_t hdr[2]; + ssize_t n; + size_t offset, page_offset, size; + + 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)); + + 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 = ngx_rtmp_r32(hdr[0]); + + 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 < 8) { + return NGX_ERROR; + } + + size -= 8; + offset += 8; + + page_offset = offset & (ngx_pagesize - 1); + ctx->mmaped_size = page_offset + size; + + ctx->mmaped = mmap(NULL, ctx->mmaped_size, PROT_READ, MAP_SHARED, + f->fd, offset - page_offset); + + if (ctx->mmaped == MAP_FAILED) { + 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 (munmap(ctx->mmaped, ctx->mmaped_size)) { + 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_start(ngx_rtmp_session_t *s, ngx_file_t *f, ngx_uint_t timestamp) +{ + ngx_rtmp_mp4_ctx_t *ctx; + 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: start timestamp=%ui", timestamp); + + for (n = 0; n < ctx->ntracks; ++n) { + 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->epoch = ngx_current_msec; + ctx->start_timestamp = timestamp; + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s, ngx_file_t *f) +{ + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: stop"); + + return NGX_OK; +} + + +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->start = ngx_rtmp_mp4_start; + fmt->stop = ngx_rtmp_mp4_stop; + fmt->send = ngx_rtmp_mp4_send; + + return NGX_OK; +} diff --git a/ngx_rtmp_notify_module.c b/ngx_rtmp_notify_module.c index f8c7928..7f5fc75 100644 --- a/ngx_rtmp_notify_module.c +++ b/ngx_rtmp_notify_module.c @@ -431,8 +431,8 @@ ngx_rtmp_notify_save_name_args(ngx_rtmp_session_t *s, ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_notify_module); } - ngx_memcpy(ctx->name, name, sizeof(name)); - ngx_memcpy(ctx->args, args, sizeof(args)); + ngx_memcpy(ctx->name, name, NGX_RTMP_MAX_NAME); + ngx_memcpy(ctx->args, args, NGX_RTMP_MAX_ARGS); } diff --git a/ngx_rtmp_play_module.c b/ngx_rtmp_play_module.c index c5dd74d..960b19d 100644 --- a/ngx_rtmp_play_module.c +++ b/ngx_rtmp_play_module.c @@ -3,8 +3,9 @@ */ +#include "ngx_rtmp_play_module.h" #include "ngx_rtmp_cmd_module.h" -#include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_streams.h" static ngx_rtmp_play_pt next_play; @@ -13,55 +14,16 @@ static ngx_rtmp_seek_pt next_seek; static ngx_rtmp_pause_pt next_pause; +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 void ngx_rtmp_play_send(ngx_event_t *e); -static void ngx_rtmp_play_read_meta(ngx_rtmp_session_t *s); -static ngx_int_t ngx_rtmp_play_start(ngx_rtmp_session_t *s, ngx_int_t offset); + void *parent, void *child); +static ngx_int_t ngx_rtmp_play_init(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_play_done(ngx_rtmp_session_t *s); +static ngx_int_t ngx_rtmp_play_start(ngx_rtmp_session_t *s, double timestamp); static ngx_int_t ngx_rtmp_play_stop(ngx_rtmp_session_t *s); -static ngx_int_t ngx_rtmp_play_timestamp_to_offset(ngx_rtmp_session_t *s, - ngx_int_t timestamp); - - -typedef struct { - ngx_str_t root; -} ngx_rtmp_play_app_conf_t; - - -typedef struct { - ngx_uint_t nelts; - ngx_uint_t offset; -} ngx_rtmp_play_index_t; - - -typedef struct { - ngx_file_t file; - 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_play_index_t filepositions; - ngx_rtmp_play_index_t times; -} ngx_rtmp_play_ctx_t; - - -#define NGX_RTMP_PLAY_BUFFER (1024*1024) -#define NGX_RTMP_PLAY_DEFAULT_BUFLEN 1000 -#define NGX_RTMP_PLAY_TAG_HEADER 11 -#define NGX_RTMP_PLAY_DATA_OFFSET 13 - - -static u_char ngx_rtmp_play_buffer[ - NGX_RTMP_PLAY_BUFFER]; -static u_char ngx_rtmp_play_header[ - NGX_RTMP_PLAY_TAG_HEADER]; +static void ngx_rtmp_play_send(ngx_event_t *e); static ngx_command_t ngx_rtmp_play_commands[] = { @@ -80,7 +42,7 @@ static ngx_command_t ngx_rtmp_play_commands[] = { static ngx_rtmp_module_t ngx_rtmp_play_module_ctx = { NULL, /* preconfiguration */ ngx_rtmp_play_postconfiguration, /* postconfiguration */ - NULL, /* create main configuration */ + ngx_rtmp_play_create_main_conf, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ @@ -105,12 +67,35 @@ ngx_module_t ngx_rtmp_play_module = { }; +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; } @@ -131,452 +116,123 @@ ngx_rtmp_play_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) } -static ngx_int_t -ngx_rtmp_play_fill_index(ngx_rtmp_amf_ctx_t *ctx, ngx_rtmp_play_index_t *idx) +static void +ngx_rtmp_play_send(ngx_event_t *e) { - uint32_t nelts; - ngx_buf_t *b; + ngx_rtmp_session_t *s = e->data; + ngx_rtmp_play_ctx_t *ctx; + ngx_int_t rc; - /* 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) { + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx == NULL || ctx->fmt == NULL || ctx->fmt->send == NULL) { + return; + } + + rc = ctx->fmt->send(s, &ctx->file); + + 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_user_stream_eof(s, NGX_RTMP_MSID); + + ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stopped"); +} + + +static ngx_int_t +ngx_rtmp_play_init(ngx_rtmp_session_t *s) +{ + ngx_rtmp_play_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + + if (ctx == NULL) { return NGX_ERROR; } - ngx_rtmp_rmemcpy(&nelts, b->pos + ctx->offset, 4); - idx->nelts = nelts; - idx->offset = ctx->offset + 4; + if (ctx->fmt && ctx->fmt->init && + ctx->fmt->init(s, &ctx->file) != NGX_OK) + { + return NGX_ERROR; + } return NGX_OK; } static ngx_int_t -ngx_rtmp_play_init_index(ngx_rtmp_session_t *s, ngx_chain_t *in) +ngx_rtmp_play_done(ngx_rtmp_session_t *s) { ngx_rtmp_play_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_play_module); - if (ctx == NULL || in == NULL) { - return NGX_OK; - } - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: 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, - "play: init index error"); - return NGX_OK; - } - - if (filepositions_ctx.link && ngx_rtmp_play_fill_index(&filepositions_ctx, - &ctx->filepositions) - != NGX_OK) - { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: failed to init filepositions"); + if (ctx == NULL) { return NGX_ERROR; } - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: filepositions nelts=%ui offset=%ui", - ctx->filepositions.nelts, ctx->filepositions.offset); - if (times_ctx.link && ngx_rtmp_play_fill_index(×_ctx, - &ctx->times) - != NGX_OK) + if (ctx->fmt && ctx->fmt->done && + ctx->fmt->done(s, &ctx->file) != NGX_OK) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: failed to init times"); return NGX_ERROR; } - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: times nelts=%ui offset=%ui", - ctx->times.nelts, ctx->times.offset); - return NGX_OK; -} - - -static double -ngx_rtmp_play_index_value(void *src) -{ - double v; - - ngx_rtmp_rmemcpy(&v, src, 8); - return v; + return NGX_OK; } static ngx_int_t -ngx_rtmp_play_timestamp_to_offset(ngx_rtmp_session_t *s, ngx_int_t timestamp) +ngx_rtmp_play_start(ngx_rtmp_session_t *s, double timestamp) { ngx_rtmp_play_ctx_t *ctx; - ssize_t n, size; - ngx_uint_t offset, index, ret, nelts; - double v; + ngx_uint_t ts; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); + if (ctx == NULL) { - goto rewind; + return NGX_ERROR; } - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: lookup index start timestamp=%i", - timestamp); - - if (ctx->meta_read == 0) { - ngx_rtmp_play_read_meta(s); - 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_PLAY_DATA_OFFSET + NGX_RTMP_PLAY_TAG_HEADER - + ctx->times.offset; - - /* index should fit in the buffer */ - nelts = ngx_min(ctx->times.nelts, sizeof(ngx_rtmp_play_buffer) / 9); - size = nelts * 9; - n = ngx_read_file(&ctx->file, ngx_rtmp_play_buffer, size, offset); - if (n != size) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: could not read times index"); - goto rewind; - } - - /*TODO: implement binary search */ - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: lookup times nelts=%ui", nelts); - - for (index = 0; index < nelts - 1; ++index) { - v = ngx_rtmp_play_index_value(ngx_rtmp_play_buffer - + index * 9 + 1) * 1000; - - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: 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, - "play: index out of bounds: %ui>=%ui", - index, ctx->filepositions.nelts); - goto rewind; - } - - /* take value from filepositions */ - offset = NGX_RTMP_PLAY_DATA_OFFSET + NGX_RTMP_PLAY_TAG_HEADER - + ctx->filepositions.offset + index * 9; - n = ngx_read_file(&ctx->file, ngx_rtmp_play_buffer, 8, offset + 1); - if (n != 8) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: could not read filepositions index"); - goto rewind; - } - ret = ngx_rtmp_play_index_value(ngx_rtmp_play_buffer); - - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: lookup index timestamp=%i offset=%ui", - timestamp, ret); - - return ret; - -rewind: - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: lookup index timestamp=%i offset=begin", - timestamp); - - return NGX_RTMP_PLAY_DATA_OFFSET; -} - - -static void -ngx_rtmp_play_read_meta(ngx_rtmp_session_t *s) -{ - ngx_rtmp_play_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_play_module); - if (ctx == NULL) { - return; - } - - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: read meta"); - - /* read tag header */ - n = ngx_read_file(&ctx->file, ngx_rtmp_play_header, - sizeof(ngx_rtmp_play_header), NGX_RTMP_PLAY_DATA_OFFSET); - if (n != sizeof(ngx_rtmp_play_header)) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: could not read metadata tag header"); - return; - } - - if (ngx_rtmp_play_header[0] != NGX_RTMP_MSG_AMF_META) { - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: first tag is not metadata, giving up"); - return; - } - - ngx_memzero(&h, sizeof(h)); - h.type = NGX_RTMP_MSG_AMF_META; - h.msid = NGX_RTMP_LIVE_MSID; - h.csid = NGX_RTMP_LIVE_CSID_META; - size = 0; - ngx_rtmp_rmemcpy(&size, ngx_rtmp_play_header + 1, 3); - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: metadata size=%D", size); - - if (size > sizeof(ngx_rtmp_play_buffer)) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: too big metadata"); - return; - } - - /* read metadata */ - n = ngx_read_file(&ctx->file, ngx_rtmp_play_buffer, - size, sizeof(ngx_rtmp_play_header) + - NGX_RTMP_PLAY_DATA_OFFSET); - if (n != (ssize_t) size) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: 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_play_buffer; - in_buf.last = ngx_rtmp_play_buffer + size; - - ngx_rtmp_play_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 void -ngx_rtmp_play_send(ngx_event_t *e) -{ - ngx_rtmp_session_t *s; - ngx_rtmp_play_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; - ssize_t n; - uint32_t buflen, end_timestamp, size; - - s = e->data; - - cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); - if (ctx == NULL) { - return; - } - - if (ctx->offset == -1) { - ctx->offset = ngx_rtmp_play_timestamp_to_offset(s, - ctx->start_timestamp); - ctx->start_timestamp = -1; /* set later from actual timestamp */ - } - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: read tag at offset=%i", ctx->offset); - - /* read tag header */ - n = ngx_read_file(&ctx->file, ngx_rtmp_play_header, - sizeof(ngx_rtmp_play_header), ctx->offset); - if (n != sizeof(ngx_rtmp_play_header)) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: could not read flv tag header"); - ngx_rtmp_send_user_stream_eof(s, 1); - return; - } - - /* parse header fields */ - ngx_memzero(&h, sizeof(h)); - h.msid = NGX_RTMP_LIVE_MSID; - h.type = ngx_rtmp_play_header[0]; - size = 0; - ngx_rtmp_rmemcpy(&size, ngx_rtmp_play_header + 1, 3); - ngx_rtmp_rmemcpy(&h.timestamp, ngx_rtmp_play_header + 4, 3); - ((u_char *) &h.timestamp)[3] = ngx_rtmp_play_header[7]; - - ctx->offset += (sizeof(ngx_rtmp_play_header) + size + 4); - - last_timestamp = 0; - - switch (h.type) { - case NGX_RTMP_MSG_AUDIO: - h.csid = NGX_RTMP_LIVE_CSID_AUDIO; - last_timestamp = ctx->last_audio; - ctx->last_audio = h.timestamp; - break; - - case NGX_RTMP_MSG_VIDEO: - h.csid = NGX_RTMP_LIVE_CSID_VIDEO; - last_timestamp = ctx->last_video; - ctx->last_video = h.timestamp; - break; - - default: - goto skip; - } - - ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: 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_play_buffer)) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: too big message: %D>%uz", size, - sizeof(ngx_rtmp_play_buffer)); - goto next; - } - - /* read tag body */ - n = ngx_read_file(&ctx->file, ngx_rtmp_play_buffer, size, - ctx->offset - size - 4); - if (n != (ssize_t) size) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "play: could not read flv tag"); - 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_play_buffer; - in_buf.last = ngx_rtmp_play_buffer + size; - - /* output chain */ - out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); - ngx_rtmp_prepare_message(s, &h, ctx->msg_mask & (1 << h.type) ? - &lh : NULL, out); - ngx_rtmp_send_message(s, out, 0); /* TODO: priority */ - ngx_rtmp_free_shared_chain(cscf, out); - - ctx->msg_mask |= (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, - "play: start_timestamp=%i", ctx->start_timestamp); - goto skip; - } - - buflen = (s->buflen ? s->buflen : NGX_RTMP_PLAY_DEFAULT_BUFLEN); - end_timestamp = (ngx_current_msec - ctx->epoch) + - ctx->start_timestamp + buflen; - - ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: %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); - - /* too much data sent; schedule timeout */ - if (h.timestamp > end_timestamp) { - ngx_add_timer(e, h.timestamp - end_timestamp); - return; - } - -skip: - ngx_post_event(e, &ngx_posted_events); -} - - -static ngx_int_t -ngx_rtmp_play_start(ngx_rtmp_session_t *s, ngx_int_t timestamp) -{ - ngx_rtmp_play_ctx_t *ctx; - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); - if (ctx == NULL) { - return NGX_OK; - } - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: start timestamp=%i", timestamp); - ngx_rtmp_play_stop(s); - ctx->start_timestamp = timestamp; - ctx->offset = -1; - ctx->msg_mask = 0; + ts = (timestamp > 0 ? (ngx_uint_t) timestamp : 0); - ngx_post_event((&ctx->write_evt), &ngx_posted_events) + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: start timestamp=%ui", ts); + + if (ctx->fmt && ctx->fmt->start && + ctx->fmt->start(s, &ctx->file, ts) != NGX_OK) + { + return NGX_ERROR; + } + + ngx_post_event((&ctx->send_evt), &ngx_posted_events); return NGX_OK; } @@ -588,19 +244,26 @@ ngx_rtmp_play_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_OK; + return NGX_ERROR; } ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: stop"); + "play: stop"); - if (ctx->write_evt.timer_set) { - ngx_del_timer(&ctx->write_evt); + if (ctx->send_evt.timer_set) { + ngx_del_timer(&ctx->send_evt); } - if (ctx->write_evt.prev) { - ngx_delete_posted_event((&ctx->write_evt)); + if (ctx->send_evt.prev) { + ngx_delete_posted_event((&ctx->send_evt)); + } + + if (ctx->fmt && ctx->fmt->stop && + ctx->fmt->stop(s, &ctx->file) != NGX_OK) + { + return NGX_ERROR; } return NGX_OK; @@ -613,15 +276,18 @@ 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"); + "play: close_stream"); ngx_rtmp_play_stop(s); + ngx_rtmp_play_done(s); + if (ctx->file.fd != NGX_INVALID_FILE) { ngx_close_file(ctx->file.fd); ctx->file.fd = NGX_INVALID_FILE; @@ -643,7 +309,7 @@ ngx_rtmp_play_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: seek timestamp=%i", (ngx_int_t) v->offset); + "play: seek offset=%f", v->offset); ngx_rtmp_play_start(s, v->offset); @@ -658,13 +324,14 @@ 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; } ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: pause=%i timestamp=%i", - (ngx_int_t) v->pause, (ngx_int_t) v->position); + "play: pause=%i timestamp=%f", + (ngx_int_t) v->pause, v->position); if (v->pause) { ngx_rtmp_play_stop(s); @@ -680,20 +347,28 @@ next: 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_event_t *e; - size_t len, slen; + ngx_rtmp_play_fmt_t *fmt, **pfmt; + ngx_str_t *pfx, *sfx; + ngx_str_t name; + ngx_uint_t n; + static ngx_str_t nosfx; static u_char path[NGX_MAX_PATH]; + 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->root.len == 0) { goto next; } ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: play name='%s' timestamp=%i", + "play: play name='%s' timestamp=%i", v->name, (ngx_int_t) v->start); ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_play_module); @@ -706,52 +381,107 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) /* check for double-dot in v->name; * we should not move out of play directory */ - p = v->name; - while (*p) { - if (*p == '.' && *(p + 1) == '.') { + 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; } - ++p; } if (ctx == NULL) { ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_play_ctx_t)); ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_play_module); } + ngx_memzero(ctx, sizeof(*ctx)); ctx->file.log = s->connection->log; - /* make file path */ - len = ngx_strlen(v->name); - slen = sizeof(".flv") - 1; - p = ngx_snprintf(path, sizeof(path), "%V/%s%s", &pacf->root, v->name, - len > slen && ngx_strncasecmp((u_char *) ".flv", - v->name + len - slen, slen) == 0 ? "" : ".flv"); - *p = 0; + name.len = ngx_strlen(v->name); + name.data = v->name; - /* open file */ - ctx->file.fd = ngx_open_file(path, NGX_FILE_RDONLY, NGX_FILE_OPEN, - NGX_FILE_DEFAULT_ACCESS); - if (ctx->file.fd == NGX_INVALID_FILE) { + 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) + { + name.data += pfx->len; + name.len -= 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: error opening file %s", path); + "play: fmt not found"); goto next; } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "play: opened file '%s'", path); + "play: fmt found: '%V'", &ctx->fmt->name); - e = &ctx->write_evt; + sfx = &ctx->fmt->sfx; + + if (name.len >= sfx->len && + ngx_strncasecmp(sfx->data, name.data + name.len - sfx->len, + sfx->len) + == 0) + { + sfx = &nosfx; + } + + p = ngx_snprintf(path, sizeof(path), "%V/%V%V", &pacf->root, &name, sfx); + *p = 0; + + ctx->file.fd = ngx_open_file(path, NGX_FILE_RDONLY, NGX_FILE_OPEN, + NGX_FILE_DEFAULT_ACCESS); + + if (ctx->file.fd == NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "play: error opening file '%s'", path); + return NGX_ERROR; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "play: opened file '%s'", path); + + e = &ctx->send_evt; e->data = s; e->handler = ngx_rtmp_play_send; e->log = s->connection->log; ngx_rtmp_send_user_recorded(s, 1); - ngx_rtmp_play_start(s, v->start); + if (ngx_rtmp_play_init(s) != NGX_OK) { + return NGX_ERROR; + } + + if (ngx_rtmp_play_start(s, v->start) != NGX_OK) { + return NGX_ERROR; + } next: return next_play(s, v); diff --git a/ngx_rtmp_play_module.h b/ngx_rtmp_play_module.h new file mode 100644 index 0000000..77d7856 --- /dev/null +++ b/ngx_rtmp_play_module.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012 Roman Arutyunyan + */ + + +#ifndef _NGX_RTMP_PLAY_H_INCLUDED_ +#define _NGX_RTMP_PLAY_H_INCLUDED_ + + +#include "ngx_rtmp.h" + + +typedef ngx_int_t (*ngx_rtmp_play_init_pt) (ngx_rtmp_session_t *s, + ngx_file_t *f); +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, 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); + + +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_stop_pt stop; + ngx_rtmp_play_send_pt send; +} ngx_rtmp_play_fmt_t; + + +typedef struct { + ngx_file_t file; + ngx_rtmp_play_fmt_t *fmt; + ngx_event_t send_evt; +} ngx_rtmp_play_ctx_t; + + +typedef struct { + ngx_str_t root; +} 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_rtmp_record_module.c b/ngx_rtmp_record_module.c index 5f24e53..07f93fd 100644 --- a/ngx_rtmp_record_module.c +++ b/ngx_rtmp_record_module.c @@ -301,6 +301,7 @@ 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; if (s->auto_pushed) { goto next; @@ -327,6 +328,17 @@ ngx_rtmp_record_publish(ngx_rtmp_session_t *s, ngx_rtmp_publish_t *v) 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 (ngx_rtmp_record_open(s) != NGX_OK) { return NGX_ERROR; } diff --git a/ngx_rtmp_send.c b/ngx_rtmp_send.c index 8f9ba45..eab33bd 100644 --- a/ngx_rtmp_send.c +++ b/ngx_rtmp_send.c @@ -5,6 +5,7 @@ #include "ngx_rtmp.h" #include "ngx_rtmp_amf.h" +#include "ngx_rtmp_streams.h" #define NGX_RTMP_USER_START(s, tp) \ @@ -262,7 +263,9 @@ ngx_rtmp_append_amf(ngx_rtmp_session_t *s, return rc; } -ngx_int_t ngx_rtmp_send_amf(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, + +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) { ngx_chain_t *first; @@ -287,3 +290,59 @@ done: return rc; } + +ngx_int_t +ngx_rtmp_send_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("code"), + NULL, 0 }, + + { NGX_RTMP_AMF_STRING, + ngx_string("level"), + 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) }, + }; + + + out_inf[0].data = code; + out_inf[1].data = level; + 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_send_amf(s, &h, out_elts, + sizeof(out_elts) / sizeof(out_elts[0])); +} diff --git a/ngx_rtmp_streams.h b/ngx_rtmp_streams.h new file mode 100644 index 0000000..bf1ec7b --- /dev/null +++ b/ngx_rtmp_streams.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2012 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 + + +/*legacy*/ +#define NGX_RTMP_CMD_CSID_AMF_INI NGX_RTMP_CSID_AMF_INI +#define NGX_RTMP_CMD_CSID_AMF NGX_RTMP_CSID_AMF +#define NGX_RTMP_CMD_MSID NGX_RTMP_MSID +#define NGX_RTMP_LIVE_CSID_META NGX_RTMP_CSID_AMF +#define NGX_RTMP_LIVE_CSID_AUDIO NGX_RTMP_CSID_AUDIO +#define NGX_RTMP_LIVE_CSID_VIDEO NGX_RTMP_CSID_VIDEO +#define NGX_RTMP_LIVE_MSID NGX_RTMP_MSID + + +#endif /* _NGX_RTMP_STREAMS_H_INCLUDED_ */ diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..32653ff --- /dev/null +++ b/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/test/nginx.conf b/test/nginx.conf index 06ea958..e0f94ea 100644 --- a/test/nginx.conf +++ b/test/nginx.conf @@ -85,6 +85,10 @@ http { root /home/rarutyunyan/nginx-rtmp-module/; } + location /rtmp-publisher { + root /home/rarutyunyan/nginx-rtmp-module/test; + } + location / { root /home/rarutyunyan/nginx-rtmp-module/test/www; } diff --git a/test/rtmp-publisher/README.md b/test/rtmp-publisher/README.md new file mode 100644 index 0000000..c31a2ac --- /dev/null +++ b/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/test/rtmp-publisher/RtmpPlayer.mxml b/test/rtmp-publisher/RtmpPlayer.mxml new file mode 100644 index 0000000..f874394 --- /dev/null +++ b/test/rtmp-publisher/RtmpPlayer.mxml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/test/rtmp-publisher/RtmpPlayer.swf b/test/rtmp-publisher/RtmpPlayer.swf new file mode 100644 index 0000000..a8aa285 Binary files /dev/null and b/test/rtmp-publisher/RtmpPlayer.swf differ diff --git a/test/rtmp-publisher/RtmpPublisher.mxml b/test/rtmp-publisher/RtmpPublisher.mxml new file mode 100644 index 0000000..6d4f209 --- /dev/null +++ b/test/rtmp-publisher/RtmpPublisher.mxml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/rtmp-publisher/RtmpPublisher.swf b/test/rtmp-publisher/RtmpPublisher.swf new file mode 100644 index 0000000..a3396cf Binary files /dev/null and b/test/rtmp-publisher/RtmpPublisher.swf differ diff --git a/test/rtmp-publisher/player.html b/test/rtmp-publisher/player.html new file mode 100644 index 0000000..5a2a2d8 --- /dev/null +++ b/test/rtmp-publisher/player.html @@ -0,0 +1,19 @@ + + + + RTMP Player + + + + +
+

Flash not installed

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

Flash not installed

+
+ + diff --git a/test/rtmp-publisher/swfobject.js b/test/rtmp-publisher/swfobject.js new file mode 100644 index 0000000..8eafe9d --- /dev/null +++ b/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;ab