From 0d1d7138e570bb314da972a4a716f2ed05875c36 Mon Sep 17 00:00:00 2001 From: anba8005 Date: Fri, 20 Jul 2012 22:44:44 +0300 Subject: [PATCH 01/44] HLS B-Frames fixed --- hls/ngx_rtmp_hls_module.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index 86dee4f..b4aa9c8 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -913,6 +913,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); @@ -948,9 +949,10 @@ 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; @@ -1023,7 +1025,7 @@ 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.pts = packet.dts + cts * 90; packet.stream_index = ctx->out_vstream; /* if (ftype == 1) { From e0f9d944a09f720f7645372a42d59b58eaed0544 Mon Sep 17 00:00:00 2001 From: anba8005 Date: Sun, 22 Jul 2012 13:41:35 +0300 Subject: [PATCH 02/44] dts overflow fixed --- hls/ngx_rtmp_hls_module.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index b4aa9c8..3c9a22d 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -873,7 +873,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; @@ -1024,7 +1024,7 @@ ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, } av_init_packet(&packet); - packet.dts = h->timestamp * 90; + packet.dts = h->timestamp * 90L; packet.pts = packet.dts + cts * 90; packet.stream_index = ctx->out_vstream; /* From dea898a6ea133ca65e1cc37ea6236e80e5e482c3 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 23 Jul 2012 16:38:40 +0400 Subject: [PATCH 03/44] added proper file name check in play module --- ngx_rtmp_play_module.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ngx_rtmp_play_module.c b/ngx_rtmp_play_module.c index c5dd74d..626cfc7 100644 --- a/ngx_rtmp_play_module.c +++ b/ngx_rtmp_play_module.c @@ -706,14 +706,15 @@ 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) { From 219de8ededab324091b49da5a05f6acf9b0603e5 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 23 Jul 2012 17:19:25 +0400 Subject: [PATCH 04/44] added path checks --- ngx_rtmp_record_module.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ngx_rtmp_record_module.c b/ngx_rtmp_record_module.c index ef81261..e6e674a 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; racf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_record_module); @@ -323,6 +324,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; } From 295551947a187a265e5c60e6cccdf4c894a21e24 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 23 Jul 2012 17:34:23 +0400 Subject: [PATCH 05/44] style fix --- hls/ngx_rtmp_hls_module.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index 3c9a22d..5d7b6fb 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -952,7 +952,8 @@ ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, if (ngx_rtmp_hls_copy(s, &cts, &p, 3, &in) != NGX_OK) { return NGX_ERROR; } - cts = ((cts & 0x00FF0000) >> 16) | ((cts & 0x000000FF) << 16) | (cts & 0x0000FF00); + cts = ((cts & 0x00FF0000) >> 16) | ((cts & 0x000000FF) << 16) + | (cts & 0x0000FF00); out.pos = buffer; out.last = buffer + sizeof(buffer) - FF_INPUT_BUFFER_PADDING_SIZE; From d01128a3fbc522f1e41c57afa90abccac11ff746 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 23 Jul 2012 17:40:00 +0400 Subject: [PATCH 06/44] updated TODO list --- TODO | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 From 3d3d8984302083f019cace2a31a7dabf8ad86367 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 13 Aug 2012 16:58:10 +0400 Subject: [PATCH 07/44] fixed FreeBSD/MacOS compilation; Thanks to Ganesh Gunasegaran (itsgg) --- ngx_rtmp_exec_module.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ngx_rtmp_exec_module.c b/ngx_rtmp_exec_module.c index 0a2fdab..18449bd 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 From 50931a9e299994115f54bf18c622e5d548019f68 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 13 Aug 2012 17:02:49 +0400 Subject: [PATCH 08/44] turned on exec on all systems except for Windows; tested & working on FreeBSD --- ngx_rtmp_exec_module.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ngx_rtmp_exec_module.c b/ngx_rtmp_exec_module.c index 18449bd..398d677 100644 --- a/ngx_rtmp_exec_module.c +++ b/ngx_rtmp_exec_module.c @@ -320,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; @@ -416,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; } From 35425eeedb0b90e6c1922f6efcf8138e18451fe1 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 13 Aug 2012 17:09:05 +0400 Subject: [PATCH 09/44] fixed compilation on FreeBSD --- ngx_rtmp_codec_module.c | 2 ++ 1 file changed, 2 insertions(+) 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; From 8e181cdb19086fa5fcce379e8cbf25b40416a3db Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 13 Aug 2012 17:58:18 +0400 Subject: [PATCH 10/44] fixed VOD notifications --- config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config b/config index cd6f634..224b126 100644 --- a/config +++ b/config @@ -6,13 +6,13 @@ CORE_MODULES="$CORE_MODULES ngx_rtmp_cmd_module \ ngx_rtmp_access_module \ ngx_rtmp_live_module \ + ngx_rtmp_play_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 \ " @@ -34,6 +34,7 @@ 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_record_module.c \ $ngx_addon_dir/ngx_rtmp_netcall_module.c \ $ngx_addon_dir/ngx_rtmp_notify_module.c \ @@ -42,7 +43,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 \ " CFLAGS="$CFLAGS -I$ngx_addon_dir" From 4cbe407bd36f1423aa0cc85e48ddc9a2a3eb71c9 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 15 Aug 2012 15:54:25 +0400 Subject: [PATCH 11/44] fixed ipv6only field to comply with the new semantics --- ngx_rtmp_core_module.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 45c9b7e2259113a870c28a660df974a541b3cd17 Mon Sep 17 00:00:00 2001 From: Ganesh Gunasegaran Date: Thu, 16 Aug 2012 01:32:08 +0530 Subject: [PATCH 12/44] Added test rtmp publisher --- test/rtmp-publisher/README.md | 14 ++++ test/rtmp-publisher/RtmpPublisher.mxml | 108 +++++++++++++++++++++++++ test/rtmp-publisher/RtmpPublisher.swf | Bin 0 -> 46156 bytes test/rtmp-publisher/demo.html | 19 +++++ test/rtmp-publisher/swfobject.js | 4 + 5 files changed, 145 insertions(+) create mode 100644 test/rtmp-publisher/README.md create mode 100644 test/rtmp-publisher/RtmpPublisher.mxml create mode 100644 test/rtmp-publisher/RtmpPublisher.swf create mode 100644 test/rtmp-publisher/demo.html create mode 100644 test/rtmp-publisher/swfobject.js diff --git a/test/rtmp-publisher/README.md b/test/rtmp-publisher/README.md new file mode 100644 index 0000000..d0ff69d --- /dev/null +++ b/test/rtmp-publisher/README.md @@ -0,0 +1,14 @@ +# RTMP Publisher + +Simple RTMP publisher. + +Edit the following flashvars in demo.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 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 0000000000000000000000000000000000000000..325a2337f5edca8a2cd76a7371bc28df48c86d0e GIT binary patch literal 46156 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 + + + 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 Date: Thu, 16 Aug 2012 10:55:59 +0400 Subject: [PATCH 13/44] fixed Wirecast connection --- ngx_rtmp_cmd_module.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ngx_rtmp_cmd_module.c b/ngx_rtmp_cmd_module.c index a0733d0..dd70ffb 100644 --- a/ngx_rtmp_cmd_module.c +++ b/ngx_rtmp_cmd_module.c @@ -494,14 +494,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 }, From cbd359e189908b8eafafb244fe6dda3f74434b48 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 16 Aug 2012 11:02:25 +0400 Subject: [PATCH 14/44] added more compatibility details --- README | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README b/README index 5d6af5e..c9b79d4 100644 --- a/README +++ b/README @@ -36,9 +36,10 @@ Features: 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) +* 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 From 38270e5ca0725237ee8ec8d5c478b5c62b9e6606 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 16 Aug 2012 11:11:00 +0400 Subject: [PATCH 15/44] updated README --- README | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README b/README index c9b79d4..32755f2 100644 --- a/README +++ b/README @@ -23,11 +23,11 @@ Features: * H264/AAC support -* Online transcoding with FFmpeg - (experimental; Linux only) +* Online transcoding with FFmpeg -* HLS (HTTP Live Streaming) support - (experimental; libavformat >= 53.31.100) +* HLS (HTTP Live Streaming) support; + experimental; requires recent libavformat + (>= 53.31.100) from ffmpeg (ffmpeg.org) * HTTP callbacks on publish/play/record @@ -61,6 +61,9 @@ Known issue: 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: From 3056478b1cdafdd40386b12253ab74caa9a27938 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 16 Aug 2012 11:53:52 +0400 Subject: [PATCH 16/44] added rtmp test publisher to sample nginx.conf --- test/nginx.conf | 4 ++++ test/rtmp-publisher/{demo.html => index.html} | 0 2 files changed, 4 insertions(+) rename test/rtmp-publisher/{demo.html => index.html} (100%) 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/demo.html b/test/rtmp-publisher/index.html similarity index 100% rename from test/rtmp-publisher/demo.html rename to test/rtmp-publisher/index.html From e80a2c4f577fd002792a1e3c5f3d2bc23488d44f Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 16 Aug 2012 12:07:19 +0400 Subject: [PATCH 17/44] added README to tests --- test/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 test/README.md diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..5119fa9 --- /dev/null +++ b/test/README.md @@ -0,0 +1,10 @@ +# 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 - capture myapp/mystream with the test flash applet From c127250a8b982f98b069ed98b0963367c41abd3a Mon Sep 17 00:00:00 2001 From: Ganesh Gunasegaran Date: Thu, 16 Aug 2012 14:38:24 +0530 Subject: [PATCH 18/44] Added sample RTMP player --- test/rtmp-publisher/README.md | 3 +- test/rtmp-publisher/RtmpPlayer.mxml | 70 ++++++++++++++++++ test/rtmp-publisher/RtmpPlayer.swf | Bin 0 -> 45696 bytes test/rtmp-publisher/RtmpPublisher.swf | Bin 46156 -> 46156 bytes test/rtmp-publisher/player.html | 19 +++++ .../{demo.html => publisher.html} | 0 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 test/rtmp-publisher/RtmpPlayer.mxml create mode 100644 test/rtmp-publisher/RtmpPlayer.swf create mode 100644 test/rtmp-publisher/player.html rename test/rtmp-publisher/{demo.html => publisher.html} (100%) diff --git a/test/rtmp-publisher/README.md b/test/rtmp-publisher/README.md index d0ff69d..c31a2ac 100644 --- a/test/rtmp-publisher/README.md +++ b/test/rtmp-publisher/README.md @@ -2,7 +2,7 @@ Simple RTMP publisher. -Edit the following flashvars in demo.html to suite your needs. +Edit the following flashvars in publisher.html & player.html to suite your needs. streamer: RTMP endpoint file: live stream name @@ -12,3 +12,4 @@ file: live stream name 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 0000000000000000000000000000000000000000..a8aa28525760eb12f7eb08e48b99ffab52a30ad9 GIT binary patch literal 45696 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

h0hsxuF%F$L+AW28|~GWmeH0Xg2 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 IuYg);0yf98&j0`b literal 0 HcmV?d00001 diff --git a/test/rtmp-publisher/RtmpPublisher.swf b/test/rtmp-publisher/RtmpPublisher.swf index 325a2337f5edca8a2cd76a7371bc28df48c86d0e..a3396cf81482ad8f1fdc42a8ae1cbdc8b942cf3f 100644 GIT binary patch delta 34864 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 delta 34879 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 + + + RTMP Player + + + + +

+ + diff --git a/test/rtmp-publisher/demo.html b/test/rtmp-publisher/publisher.html similarity index 100% rename from test/rtmp-publisher/demo.html rename to test/rtmp-publisher/publisher.html From 256587016e6e65111bcf0076d17313557ae9c94a Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 17 Aug 2012 10:23:34 +0400 Subject: [PATCH 19/44] deleted useless file --- test/rtmp-publisher/index.html | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 test/rtmp-publisher/index.html diff --git a/test/rtmp-publisher/index.html b/test/rtmp-publisher/index.html deleted file mode 100644 index b639f0d..0000000 --- a/test/rtmp-publisher/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - RTMP Publisher - - - - -
-

Flash not installed

-
- - From ecb6462d56c4604539fb4e59554505da383606e1 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 17 Aug 2012 10:24:59 +0400 Subject: [PATCH 20/44] updated test README --- test/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/README.md b/test/README.md index 5119fa9..32653ff 100644 --- a/test/README.md +++ b/test/README.md @@ -7,4 +7,5 @@ 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 - capture myapp/mystream with the test flash applet +* 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 From 2bd3d17fbba36058489040569e07f0eb9e625c04 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 17 Aug 2012 18:55:59 +0400 Subject: [PATCH 21/44] fixed choppy audio in HLS: added hls_muxdelay directive --- hls/ngx_rtmp_hls_module.c | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index 5d7b6fb..8d46af6 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -58,6 +58,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 +79,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 +118,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 +272,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; @@ -316,7 +326,7 @@ ngx_rtmp_hls_init_audio(ngx_rtmp_session_t *s) 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; @@ -406,8 +416,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 +442,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 +462,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 +492,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 +671,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); @@ -1028,10 +1049,11 @@ ngx_rtmp_hls_video(ngx_rtmp_session_t *s, ngx_rtmp_header_t *h, 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; @@ -1056,6 +1078,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; @@ -1071,6 +1094,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, From 05927f717bd49c251c1d6058a20997f5ccc115df Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 20 Aug 2012 08:36:32 +0400 Subject: [PATCH 22/44] converted README to markdown format --- README | 257 ------------------------------------------------------ README.md | 257 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 257 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 32755f2..0000000 --- a/README +++ /dev/null @@ -1,257 +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 - -* 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/[/] - - - 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..f920491 --- /dev/null +++ b/README.md @@ -0,0 +1,257 @@ +# 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 + +* 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/[/] + + - 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; + } + + } + } + From 820753e546a3b745562fa1c7a518809ab94e551f Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 20 Aug 2012 08:38:49 +0400 Subject: [PATCH 23/44] updated README --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f920491..347d901 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# nginx-rtmp-module +# NGINX-based RTMP server +#### nginx-rtmp-module -## NGINX-based RTMP server ### Project page: @@ -67,11 +67,11 @@ cd to NGINX source directory & run this: ### RTMP URL format: - rtmp://rtmp.example.com/[/] + rtmp://rtmp.example.com/app[/name] - - should match one of application {} +app - should match one of application {} blocks in config - - interpreted by each application +name - interpreted by each application can be empty @@ -224,7 +224,7 @@ cd to NGINX source directory & run this: } } -# HTTP can be used for accessing RTMP stats + # HTTP can be used for accessing RTMP stats http { server { From 5af9dee9b384782450c75e8b2733bd0192b41295 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 20 Aug 2012 08:40:23 +0400 Subject: [PATCH 24/44] updated README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 347d901..3bce60e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # NGINX-based RTMP server -#### nginx-rtmp-module +## nginx-rtmp-module ### Project page: @@ -71,6 +71,7 @@ cd to NGINX source directory & run this: app - should match one of application {} blocks in config + name - interpreted by each application can be empty From d5a19130727bc18afc20cd4100dbd98640b11619 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 20 Aug 2012 08:46:49 +0400 Subject: [PATCH 25/44] added HLS README --- hls/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 hls/README.md 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) From e243e814abd3989cc78899a64b158addcd3783d1 Mon Sep 17 00:00:00 2001 From: nl 0 Date: Mon, 20 Aug 2012 15:12:00 +0600 Subject: [PATCH 26/44] hls/config: missing libs added (avcodec and avutil) --- hls/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From 06b5712df15cfcb4ab8a7d300915460778b9b2dd Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 20 Aug 2012 14:55:27 +0400 Subject: [PATCH 27/44] added auto-creation of hls folder --- hls/ngx_rtmp_hls_module.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index 8d46af6..5624a49 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; @@ -350,17 +352,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; } From 6b1ba3fe0705c5a2519d8a2e6be46786290a0c38 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 20 Aug 2012 16:44:26 +0400 Subject: [PATCH 28/44] fixed audio-only & video-only HLS --- hls/ngx_rtmp_hls_module.c | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/hls/ngx_rtmp_hls_module.c b/hls/ngx_rtmp_hls_module.c index 5624a49..830e20b 100644 --- a/hls/ngx_rtmp_hls_module.c +++ b/hls/ngx_rtmp_hls_module.c @@ -289,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; } @@ -299,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); @@ -311,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, @@ -318,12 +334,7 @@ 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); @@ -338,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; } @@ -478,7 +497,7 @@ ngx_rtmp_hls_open_file(ngx_rtmp_session_t *s, u_char *fpath) return NGX_OK; } - if (!ctx->video || !ctx->audio) { + if (!ctx->video && !ctx->audio) { return NGX_OK; } From 8b2565640980206114a7baea8a0ab8fb28b684d0 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 22 Aug 2012 20:29:41 +0400 Subject: [PATCH 29/44] first working mp4: mp3/audio/44100 --- config | 2 + ngx_rtmp_mp4_module.c | 1486 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1488 insertions(+) create mode 100644 ngx_rtmp_mp4_module.c diff --git a/config b/config index 224b126..f5c2d00 100644 --- a/config +++ b/config @@ -7,6 +7,7 @@ CORE_MODULES="$CORE_MODULES ngx_rtmp_access_module \ ngx_rtmp_live_module \ ngx_rtmp_play_module \ + ngx_rtmp_mp4_module \ ngx_rtmp_record_module \ ngx_rtmp_netcall_module \ ngx_rtmp_notify_module \ @@ -35,6 +36,7 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $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_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 \ diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c new file mode 100644 index 0000000..df84ad9 --- /dev/null +++ b/ngx_rtmp_mp4_module.c @@ -0,0 +1,1486 @@ +/* + * Copyright (c) 2012 Roman Arutyunyan + */ + + +#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_live_module.h" + + +static ngx_rtmp_play_pt next_play; +static ngx_rtmp_close_stream_pt next_close_stream; +static ngx_rtmp_seek_pt next_seek; +static ngx_rtmp_pause_pt next_pause; + + +static ngx_int_t ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf); +static void * ngx_rtmp_mp4_create_app_conf(ngx_conf_t *cf); +static char * ngx_rtmp_mp4_merge_app_conf(ngx_conf_t *cf, + void *parent, void *child); +static void ngx_rtmp_mp4_send(ngx_event_t *e); +static ngx_int_t ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_t offset); +static ngx_int_t ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s); + + +typedef struct { + ngx_str_t root; +} ngx_rtmp_mp4_app_conf_t; + + +#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 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 duration; + off_t offset; + size_t size; + + ngx_uint_t 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 size_pos; +} ngx_rtmp_mp4_cursor_t; + + +typedef struct { + ngx_int_t type; + uint32_t csid; + u_char fhdr; + + ngx_rtmp_mp4_times_t *times; + 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 { + ngx_file_t file; + + void *mmaped; + size_t mmaped_size; + + ngx_rtmp_mp4_track_t tracks[2]; + ngx_rtmp_mp4_track_t *track; + ngx_uint_t ntracks; + + uint32_t start_timestamp, epoch; + + ngx_event_t write_evt; +} ngx_rtmp_mp4_ctx_t; + + +/* system stuff for mmapping; 4K pages assumed */ +/* TODO: more portable code */ +#define NGX_RTMP_PAGE_SHIFT 12 +#define NGX_RTMP_PAGE_SIZE (1 << NGX_RTMP_PAGE_SHIFT) +#define NGX_RTMP_PAGE_MASK (NGX_RTMP_PAGE_SIZE - 1) + + +#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) + + +/* +#define ngx_rtmp_r32(n) (((n) & 0x000000ffull) << 24 | \ + ((n) & 0x0000ff00ull) << 8 | \ + ((n) & 0x00ff0000ull) >> 8 | \ + ((n) & 0xff000000ull) >> 24) +*/ +static inline uint32_t +ngx_rtmp_r32(uint32_t n) +{ + uint32_t ret; + + /*TODO: optimize */ + ngx_rtmp_rmemcpy(&ret, &n, 4); + return ret; +} + + +static inline uint64_t +ngx_rtmp_r64(uint64_t n) +{ + uint64_t ret; + + ngx_rtmp_rmemcpy(&ret, &n, 8); + return ret; +} + + +#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_hdlr(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_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); + + +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('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','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('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 } +}; + + +static ngx_command_t ngx_rtmp_mp4_commands[] = { + + { ngx_string("play_mp4"), + NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, + ngx_conf_set_str_slot, + NGX_RTMP_APP_CONF_OFFSET, + offsetof(ngx_rtmp_mp4_app_conf_t, root), + NULL }, + + ngx_null_command +}; + + +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 */ + ngx_rtmp_mp4_create_app_conf, /* create app configuration */ + ngx_rtmp_mp4_merge_app_conf /* merge app configuration */ +}; + + +ngx_module_t ngx_rtmp_mp4_module = { + NGX_MODULE_V1, + &ngx_rtmp_mp4_module_ctx, /* module context */ + ngx_rtmp_mp4_commands, /* module directives */ + NGX_RTMP_MODULE, /* module type */ + NULL, /* init master */ + NULL, /* init module */ + NULL, /* init process */ + NULL, /* init thread */ + NULL, /* exit thread */ + NULL, /* exit process */ + NULL, /* exit master */ + NGX_MODULE_V1_PADDING +}; + + +static void * +ngx_rtmp_mp4_create_app_conf(ngx_conf_t *cf) +{ + ngx_rtmp_mp4_app_conf_t *pacf; + + pacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_mp4_app_conf_t)); + + if (pacf == NULL) { + return NULL; + } + + return pacf; +} + + +static char * +ngx_rtmp_mp4_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) +{ + ngx_rtmp_mp4_app_conf_t *prev = parent; + ngx_rtmp_mp4_app_conf_t *conf = child; + + ngx_conf_merge_str_value(conf->root, prev->root, ""); + + return NGX_CONF_OK; +} + + +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)); + 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_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 (last - pos < 12) { + 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_LIVE_CSID_VIDEO; + ctx->track->fhdr = 2; /* TODO; Sorenson */ + + 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_LIVE_CSID_AUDIO; + ctx->track->fhdr = 0x2e; /* TODO: mono, 16bit, 44K, MP3 */ + + 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_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_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) +{ + ngx_rtmp_mp4_ctx_t *ctx; + uint32_t *hdr, tag; + size_t size, nboxes; + ngx_uint_t n; + ngx_rtmp_mp4_box_t *b; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + 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; + } + + /*TODO: implement 64-bit boxes */ + + 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_init(ngx_rtmp_session_t *s) +{ + 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->mmaped || ctx->file.fd == NGX_INVALID_FILE) { + return NGX_OK; + } + + offset = 0; + size = 0; + + /* find moov box */ + for ( ;; ) { + n = ngx_read_file(&ctx->file, (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; + } + + /*TODO: implement 64-bit boxes */ + + 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; + + /* mmap moov box */ + page_offset = (offset & NGX_RTMP_PAGE_MASK); + ctx->mmaped_size = page_offset + size; + + ctx->mmaped = mmap(NULL, ctx->mmaped_size, PROT_READ, MAP_SHARED, + ctx->file.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; + } + + /* locate all required data within mapped area */ + 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_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_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_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next time overflow: time_pos=%ui", + cr->time_pos); + + return NGX_ERROR; + } + + te = &t->times->entries[cr->time_pos]; + + cr->duration = ngx_rtmp_r32(te->sample_delta) / 90; + cr->timestamp += cr->duration; + cr->time_count++; + cr->pos++; + + if (cr->time_count >= ngx_rtmp_r32(te->sample_count)) { + cr->time_pos++; + cr->time_count = 0; + } + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next time time_pos=%ui timestamp=%D " + "time_count=%ui, pos=%ui", + cr->time_pos, cr->timestamp, cr->time_count, cr->pos); + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_seek_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, + ngx_int_t timestamp) +{ + ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_time_entry_t *te; + uint32_t dt; + ngx_uint_t dn; + + 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; + } + + dn = (timestamp - cr->timestamp) / ngx_rtmp_r32(te->sample_delta); + cr->timestamp = ngx_rtmp_r32(te->sample_delta) * dn; + cr->pos += dn; + 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_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek time overflow time_pos=%ui", + cr->time_pos); + + return NGX_ERROR; + } + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek time src_timestamp=%i, timestamp=%D, " + "time_pos=%ui, pos=%ui", + timestamp, cr->timestamp, cr->time_pos, cr->pos); + + 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; + + cr = &t->cursor; + + /*TODO: chunks start with 1, not 0 */ + + if (t->offsets) { + if (cr->chunk >= ngx_rtmp_r32(t->offsets->entry_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: update offset overflow: chunk=%ui", + cr->chunk); + + return NGX_ERROR; + } + + cr->offset = ngx_rtmp_r32(t->offsets->entries[cr->chunk]); + cr->size = 0; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: update offset offset=%O", + cr->offset); + + return NGX_OK; + } + + if (t->offsets64) { + if (cr->chunk >= ngx_rtmp_r32(t->offsets64->entry_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: update offset64 overflow: chunk=%ui", + cr->chunk); + + return NGX_ERROR; + } + + cr->offset = ngx_rtmp_r32(t->offsets64->entries[cr->chunk]); + cr->size = 0; + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: update offset64 offset=%O", + 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; + + if (t->chunks == NULL) { + return NGX_ERROR; + } + + cr = &t->cursor; + + if (cr->chunk_pos >= ngx_rtmp_r32(t->chunks->entry_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: nect chunk overflow chunk_pos=%ui", + cr->chunk_pos); + + 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++; + } + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next chunk chunk=%ui, chunk_pos=%ui " + "chunk_count=%ui", + cr->chunk, cr->chunk_pos, cr->chunk_count); + + return ngx_rtmp_mp4_update_offset(s, t); + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next chunk kept chunk=%ui, chunk_pos=%ui " + "chunk_count=%ui", + cr->chunk, cr->chunk_pos, cr->chunk_count); + + 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) { + return NGX_ERROR; + } + + 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++; + } + + 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 - dchunk * + ngx_rtmp_r32(ce->samples_per_chunk)); + + ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek chunk pos=%ui, chunk=%ui, chunk_pos=%ui, " + "chunk_count=%ui", + cr->pos, cr->chunk, cr->chunk_pos, cr->chunk_count); + + 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_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next size const_size=%uz", + cr->size); + + return NGX_OK; + } + + if (cr->size_pos >= ngx_rtmp_r32(t->sizes->sample_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next size overflow size_pos=%ui", + cr->size_pos); + + return NGX_ERROR; + } + + cr->size_pos++; + cr->size = ngx_rtmp_r32(t->sizes->entries[cr->size_pos]); + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next size size_pos=%ui, size=%uz", + cr->size_pos, cr->size); + + return NGX_OK; + } + + if (t->sizes2) { + if (cr->size_pos >= ngx_rtmp_r32(t->sizes2->sample_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next size2 overflow size_pos=%ui", + cr->size_pos); + + 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_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek size const_size=%uz", + cr->size); + + return NGX_OK; + } + + if (cr->pos >= ngx_rtmp_r32(t->sizes->sample_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek size overflow pos=%ui", + cr->pos); + + return NGX_ERROR; + } + + cr->size_pos = cr->pos; + cr->size = ngx_rtmp_r32(t->sizes->entries[cr->size_pos]); + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek size size_pos=%ui, size=%uz", + cr->size_pos, cr->size); + + return NGX_OK; + } + + if (t->sizes2) { + if (cr->size_pos >= ngx_rtmp_r32(t->sizes2->sample_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next size2 overflow size_pos=%ui", + cr->size_pos); + + return NGX_ERROR; + } + + cr->size_pos = cr->pos; + + /* TODO */ + return NGX_OK; + } + + return NGX_ERROR; +} + + +static ngx_int_t +ngx_rtmp_mp4_next(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) +{ + return ngx_rtmp_mp4_next_time(s, t) != NGX_OK || + ngx_rtmp_mp4_next_chunk(s, t) != NGX_OK || + ngx_rtmp_mp4_next_size(s, t) != NGX_OK + ? NGX_ERROR : NGX_OK; +} + + +static void +ngx_rtmp_mp4_send(ngx_event_t *e) +{ + ngx_rtmp_session_t *s; + 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; + ssize_t ret; + ngx_uint_t n, abs_frame, active; + + s = e->data; + + 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; + } + + 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->size == 0) { + continue; + } + + if (cr->timestamp > end_timestamp) { + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track=%ui ahead %uD > %uD", + n, cr->timestamp, end_timestamp); + goto next; + } + + abs_frame = (cr->duration == 0); + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: read frame of track=%ui, " + "offset=%O, size=%uz, timestamp=%uD, duration=%uD", + n, cr->offset, cr->size, cr->timestamp, cr->duration); + + ngx_memzero(&h, sizeof(h)); + + h.msid = NGX_RTMP_LIVE_MSID; + h.type = t->type; + h.csid = t->csid; + + lh = h; + + h.timestamp = (abs_frame ? cr->timestamp : cr->duration); + + if (cr->size > sizeof(ngx_rtmp_mp4_buffer) - 1) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "mp4: too big frame: %D>%uz", + cr->size, sizeof(ngx_rtmp_mp4_buffer)); + continue; + } + + ret = ngx_read_file(&ctx->file, ngx_rtmp_mp4_buffer + 1, + cr->size, cr->offset); + + if (ret != (ssize_t) cr->size) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "mp4: could not read frame"); + continue; + } + + ngx_rtmp_mp4_buffer[0] = t->fhdr; + + /* TODO: handle video key flag */ + + ngx_memzero(&in, sizeof(in)); + ngx_memzero(&in_buf, sizeof(in_buf)); + + in.buf = &in_buf; + in_buf.pos = ngx_rtmp_mp4_buffer; + in_buf.last = ngx_rtmp_mp4_buffer + cr->size + 1; + + out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); + + ngx_rtmp_prepare_message(s, &h, abs_frame ? NULL : &lh, out); + ngx_rtmp_send_message(s, out, 0); + ngx_rtmp_free_shared_chain(cscf, out); + + if (ngx_rtmp_mp4_next(s, t) != NGX_OK) { + continue; + } + + active = 1; + +next: + if (cr->timestamp > end_timestamp && + (sched == 0 || cr->timestamp - end_timestamp < sched)) + { + sched = (uint32_t) (cr->timestamp - end_timestamp); + } + } + + if (sched) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: scheduling %uD", sched); + ngx_add_timer(e, sched); + return; + } + + if (active) { + ngx_post_event(e, &ngx_posted_events); + } +} + + +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)); + + return ngx_rtmp_mp4_seek_time(s, t, timestamp) != NGX_OK || + ngx_rtmp_mp4_seek_chunk(s, t) != NGX_OK || + ngx_rtmp_mp4_seek_size(s, t) != NGX_OK + ? NGX_ERROR : NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_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=%i", timestamp); + + ngx_rtmp_mp4_stop(s); + + for (n = 0; n < ctx->ntracks; ++n) { + ngx_rtmp_mp4_seek_track(s, &ctx->tracks[n], timestamp); + } + + ctx->epoch = ngx_current_msec; + ctx->start_timestamp = timestamp; + + ngx_post_event((&ctx->write_evt), &ngx_posted_events) + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s) +{ + ngx_rtmp_mp4_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL) { + return NGX_OK; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: stop"); + + if (ctx->write_evt.timer_set) { + ngx_del_timer(&ctx->write_evt); + } + + if (ctx->write_evt.prev) { + ngx_delete_posted_event((&ctx->write_evt)); + } + + return NGX_OK; +} + + +static ngx_int_t +ngx_rtmp_mp4_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) +{ + ngx_rtmp_mp4_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL) { + goto next; + } + + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: close_stream"); + + ngx_rtmp_mp4_stop(s); + + ngx_rtmp_mp4_done(s); + + if (ctx->file.fd != NGX_INVALID_FILE) { + ngx_close_file(ctx->file.fd); + ctx->file.fd = NGX_INVALID_FILE; + } + +next: + return next_close_stream(s, v); +} + + +static ngx_int_t +ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) +{ + ngx_rtmp_mp4_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL || ctx->file.fd == NGX_INVALID_FILE) { + goto next; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek timestamp=%i", (ngx_int_t) v->offset); + + ngx_rtmp_mp4_start(s, v->offset); + +next: + return next_seek(s, v); +} + + +static ngx_int_t +ngx_rtmp_mp4_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) +{ + ngx_rtmp_mp4_ctx_t *ctx; + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx == NULL || ctx->file.fd == NGX_INVALID_FILE) { + goto next; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: pause=%i timestamp=%i", + (ngx_int_t) v->pause, (ngx_int_t) v->position); + + if (v->pause) { + ngx_rtmp_mp4_stop(s); + } else { + ngx_rtmp_mp4_start(s, v->position); + } + +next: + return next_pause(s, v); +} + + +static ngx_int_t +ngx_rtmp_mp4_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) +{ + ngx_rtmp_mp4_app_conf_t *pacf; + ngx_rtmp_mp4_ctx_t *ctx; + u_char *p; + ngx_event_t *e; + size_t len; + static u_char path[NGX_MAX_PATH]; + u_char *name; + + pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_mp4_module); + + if (pacf == NULL || pacf->root.len == 0) { + goto next; + } + + if (ngx_strncasecmp(v->name, (u_char *) "mp4:", sizeof("mp4:") - 1) == 0) { + name = v->name + sizeof("mp4:") - 1; + goto ok; + } + + len = ngx_strlen(v->name); + + if (len >= sizeof(".mp4") && + ngx_strncasecmp(v->name + len - sizeof(".mp4") + 1, (u_char *) ".mp4", + sizeof(".mp4") - 1)) + { + name = v->name; + goto ok; + } + + goto next; +ok: + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: play name='%s' timestamp=%i", + name, (ngx_int_t) v->start); + + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); + + if (ctx && ctx->file.fd != NGX_INVALID_FILE) { + ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, + "mp4: already playing"); + goto next; + } + + /* check for double-dot in name; + * we should not move out of play directory */ + for (p = 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, + "mp4: bad name '%s'", name); + return NGX_ERROR; + } + } + + if (ctx == NULL) { + ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_mp4_ctx_t)); + ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_mp4_module); + } + ngx_memzero(ctx, sizeof(*ctx)); + + ctx->file.log = s->connection->log; + + p = ngx_snprintf(path, sizeof(path), "%V/%s", &pacf->root, name); + *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, + "mp4: error opening file %s", path); + goto next; + } + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: opened file '%s'", path); + + e = &ctx->write_evt; + e->data = s; + e->handler = ngx_rtmp_mp4_send; + e->log = s->connection->log; + + ngx_rtmp_send_user_recorded(s, 1); + + ngx_rtmp_mp4_init(s); + + ngx_rtmp_mp4_start(s, v->start); + +next: + return next_play(s, v); +} + + +static ngx_int_t +ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf) +{ + next_play = ngx_rtmp_play; + ngx_rtmp_play = ngx_rtmp_mp4_play; + + next_close_stream = ngx_rtmp_close_stream; + ngx_rtmp_close_stream = ngx_rtmp_mp4_close_stream; + + next_seek = ngx_rtmp_seek; + ngx_rtmp_seek = ngx_rtmp_mp4_seek; + + next_pause = ngx_rtmp_pause; + ngx_rtmp_pause = ngx_rtmp_mp4_pause; + + return NGX_OK; +} From 1968afdcc5e9969f7ca454f578841fb7495b632a Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 22 Aug 2012 20:38:39 +0400 Subject: [PATCH 30/44] fixed chunk=1 issue --- ngx_rtmp_mp4_module.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index df84ad9..085473d 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -825,13 +825,22 @@ 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; /*TODO: chunks start with 1, not 0 */ + if (cr->chunk < 1) { + ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: update offset underflow"); + return NGX_ERROR; + } + + chunk = cr->chunk - 1; + if (t->offsets) { - if (cr->chunk >= ngx_rtmp_r32(t->offsets->entry_count)) { + if (chunk >= ngx_rtmp_r32(t->offsets->entry_count)) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: update offset overflow: chunk=%ui", cr->chunk); @@ -839,7 +848,7 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) return NGX_ERROR; } - cr->offset = ngx_rtmp_r32(t->offsets->entries[cr->chunk]); + cr->offset = ngx_rtmp_r32(t->offsets->entries[chunk]); cr->size = 0; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, @@ -850,7 +859,7 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) } if (t->offsets64) { - if (cr->chunk >= ngx_rtmp_r32(t->offsets64->entry_count)) { + if (chunk >= ngx_rtmp_r32(t->offsets64->entry_count)) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: update offset64 overflow: chunk=%ui", cr->chunk); @@ -858,7 +867,7 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) return NGX_ERROR; } - cr->offset = ngx_rtmp_r32(t->offsets64->entries[cr->chunk]); + cr->offset = ngx_rtmp_r32(t->offsets64->entries[chunk]); cr->size = 0; ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, From 82b55fce63494e5f4e9ab235500ddd0bb3b2c6f9 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 24 Aug 2012 01:21:35 +0400 Subject: [PATCH 31/44] first video on mp4 streamer --- ngx_rtmp_mp4_module.c | 613 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 578 insertions(+), 35 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 085473d..7aee4a7 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -5,6 +5,7 @@ #include "ngx_rtmp_cmd_module.h" #include "ngx_rtmp_live_module.h" +#include "ngx_rtmp_codec_module.h" static ngx_rtmp_play_pt next_play; @@ -57,6 +58,26 @@ typedef struct { } 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; @@ -94,9 +115,13 @@ typedef struct { uint32_t duration; off_t offset; size_t size; + ngx_int_t key; + uint32_t delay; ngx_uint_t pos; + ngx_uint_t key_pos; + ngx_uint_t chunk; ngx_uint_t chunk_pos; ngx_uint_t chunk_count; @@ -104,16 +129,26 @@ typedef struct { 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_int_t type; + ngx_int_t codec; uint32_t csid; u_char fhdr; + u_char *header; + size_t header_size; + ngx_int_t header_sent; + 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; @@ -133,6 +168,12 @@ typedef struct { 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_event_t write_evt; @@ -158,6 +199,18 @@ typedef struct { ((n) & 0x00ff0000ull) >> 8 | \ ((n) & 0xff000000ull) >> 24) */ + +static inline uint16_t +ngx_rtmp_r16(uint16_t n) +{ + uint16_t ret; + + /*TODO: optimize */ + ngx_rtmp_rmemcpy(&ret, &n, 2); + return ret; +} + + static inline uint32_t ngx_rtmp_r32(uint32_t n) { @@ -191,10 +244,16 @@ 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_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, @@ -203,6 +262,20 @@ 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_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, @@ -220,12 +293,22 @@ static ngx_rtmp_mp4_box_t ngx_rtmp_mp4_boxes[] = { { 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('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('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 } }; @@ -352,7 +435,7 @@ ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos, u_char *last) return NGX_OK; } - if (last - pos < 12) { + if (pos + 12 > last) { return NGX_ERROR; } @@ -361,7 +444,6 @@ ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos, u_char *last) if (type == ngx_rtmp_mp4_make_tag('v','i','d','e')) { ctx->track->type = NGX_RTMP_MSG_VIDEO; ctx->track->csid = NGX_RTMP_LIVE_CSID_VIDEO; - ctx->track->fhdr = 2; /* TODO; Sorenson */ ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: video track"); @@ -369,7 +451,6 @@ ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos, u_char *last) } else if (type == ngx_rtmp_mp4_make_tag('s','o','u','n')) { ctx->track->type = NGX_RTMP_MSG_AUDIO; ctx->track->csid = NGX_RTMP_LIVE_CSID_AUDIO; - ctx->track->fhdr = 0x2e; /* TODO: mono, 16bit, 44K, MP3 */ ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: audio track"); @@ -382,6 +463,218 @@ ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos, u_char *last) } +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; + + ctx->track->fhdr = codec; + + 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); + + return ngx_rtmp_mp4_parse(s, pos, last); +} + + +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) { + *p |= 0x01; + } + + if (ctx->sample_size == 16) { + *p |= 0x02; + } + + switch (ctx->sample_rate) { + case 11025: + *p |= 0x04; + break; + + case 22050: + *p |= 0x08; + break; + + case 44100: + *p |= 0x0c; + break; + } + + *p |= (codec << 4); + + 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); + + return ngx_rtmp_mp4_parse(s, pos, last); +} + + +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_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_esds(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_AUDIO_AAC) { + return NGX_OK; + } + + /*TODO: parse AAC header? */ + + ctx->track->header = pos; + ctx->track->header_size = (size_t) (last - pos); + + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: audio AAC header size=%uz", + ctx->track->header_size); + + return NGX_OK; +} + + +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) { @@ -444,6 +737,68 @@ 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) +{ + 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) { @@ -515,7 +870,6 @@ 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) { @@ -596,8 +950,6 @@ ngx_rtmp_mp4_parse(ngx_rtmp_session_t *s, u_char *pos, u_char *last) return NGX_ERROR; } - /*TODO: implement 64-bit boxes */ - hdr = (uint32_t *) pos; size = ngx_rtmp_r32(hdr[0]); tag = hdr[1]; @@ -758,10 +1110,11 @@ ngx_rtmp_mp4_next_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr->time_count = 0; } - ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next time time_pos=%ui timestamp=%D " - "time_count=%ui, pos=%ui", - cr->time_pos, cr->timestamp, cr->time_count, cr->pos); + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next time time_pos=%ui, timestamp=%uD, " + "duration=%uD, time_count=%ui, pos=%ui", + cr->time_pos, cr->timestamp, cr->duration, + cr->time_count, cr->pos); return NGX_OK; } @@ -829,8 +1182,6 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr = &t->cursor; - /*TODO: chunks start with 1, not 0 */ - if (cr->chunk < 1) { ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: update offset underflow"); @@ -888,14 +1239,14 @@ ngx_rtmp_mp4_next_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) ngx_rtmp_mp4_chunk_entry_t *ce, *nce; if (t->chunks == NULL) { - return NGX_ERROR; + return NGX_OK; } cr = &t->cursor; if (cr->chunk_pos >= ngx_rtmp_r32(t->chunks->entry_count)) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: nect chunk overflow chunk_pos=%ui", + "mp4: next chunk overflow chunk_pos=%ui", cr->chunk_pos); return NGX_ERROR; @@ -943,7 +1294,8 @@ ngx_rtmp_mp4_seek_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr = &t->cursor; if (t->chunks == NULL || t->chunks->entry_count == 0) { - return NGX_ERROR; + cr->chunk = 1; + return NGX_OK; } ce = t->chunks->entries; @@ -962,6 +1314,7 @@ ngx_rtmp_mp4_seek_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) pos += dpos; ce++; + cr->chunk_pos++; } if (ce->samples_per_chunk == 0) { @@ -1004,6 +1357,8 @@ ngx_rtmp_mp4_next_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) return NGX_OK; } + cr->size_pos++; + if (cr->size_pos >= ngx_rtmp_r32(t->sizes->sample_count)) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: next size overflow size_pos=%ui", @@ -1012,7 +1367,6 @@ ngx_rtmp_mp4_next_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) return NGX_ERROR; } - cr->size_pos++; cr->size = ngx_rtmp_r32(t->sizes->entries[cr->size_pos]); ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, @@ -1095,12 +1449,154 @@ ngx_rtmp_mp4_seek_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) } +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; + + cr = &t->cursor; + + if (t->keys == NULL) { + return NGX_OK; + } + + if (cr->key_pos >= ngx_rtmp_r32(t->keys->entry_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next key overflow key_pos=%ui", + cr->key_pos); + + return NGX_OK; + } + + cr->key = (cr->pos + 1 == ngx_rtmp_r32(t->keys->entries[cr->key_pos])); + + if (cr->key) { + cr->key_pos++; + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next key %s pos=%ui, key_pos=%ui", + cr->key ? "match" : "miss", cr->pos, cr->key_pos); + + 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; + + 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++; + } + + cr->key = (cr->key_pos < ngx_rtmp_r32(t->keys->entry_count) && + cr->pos + 1 == ngx_rtmp_r32(t->keys->entries[cr->key_pos])); + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek key %s key_pos=%ui, pos=%ui", + cr->key ? "match" : "miss", cr->key_pos, cr->pos); + + 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; + + cr = &t->cursor; + + if (t->delays == NULL) { + return NGX_OK; + } + + if (cr->delay_pos >= ngx_rtmp_r32(t->delays->entry_count)) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next delay overflow key_pos=%ui", + cr->delay_pos); + + return NGX_OK; + } + + cr->delay_count++; + + if (cr->delay_count >= + ngx_rtmp_r32(t->delays->entries[cr->delay_pos].sample_count)) + { + cr->delay_pos++; + cr->delay_count = 0; + } + + if (cr->delay_pos < ngx_rtmp_r32(t->delays->entry_count)) { + cr->delay = ngx_rtmp_r32(t->delays->entries[cr->delay_pos] + .sample_offset); + } + + ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: next delay delay_pos=%ui, delay_count=%ui, delay=%ui", + cr->delay_pos, cr->delay_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; + uint32_t pos, dpos; + + cr = &t->cursor; + + if (t->delays == NULL) { + return NGX_OK; + } + + pos = 0; + + while (cr->delay_pos < ngx_rtmp_r32(t->delays->entry_count)) { + dpos = ngx_rtmp_r32(t->delays->entries[cr->delay_pos].sample_count); + + if (pos + dpos > cr->pos) { + cr->delay_count = cr->pos - pos; + cr->delay = ngx_rtmp_r32(t->delays->entries[cr->delay_pos] + .sample_offset); + break; + } + + cr->delay_pos++; + pos += dpos; + } + + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek delay delay_pos=%ui, delay_count=%ui", + cr->delay_pos, cr->delay_count); + + return NGX_OK; +} + + static ngx_int_t ngx_rtmp_mp4_next(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) { return 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_size(s, t) != NGX_OK || + ngx_rtmp_mp4_next_delay(s, t) != NGX_OK ? NGX_ERROR : NGX_OK; } @@ -1118,6 +1614,8 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_rtmp_mp4_cursor_t *cr; uint32_t buflen, end_timestamp, sched; ssize_t ret; + u_char fhdr[5]; + size_t fhdr_size; ngx_uint_t n, abs_frame, active; s = e->data; @@ -1156,11 +1654,6 @@ ngx_rtmp_mp4_send(ngx_event_t *e) abs_frame = (cr->duration == 0); - ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: read frame of track=%ui, " - "offset=%O, size=%uz, timestamp=%uD, duration=%uD", - n, cr->offset, cr->size, cr->timestamp, cr->duration); - ngx_memzero(&h, sizeof(h)); h.msid = NGX_RTMP_LIVE_MSID; @@ -1171,14 +1664,53 @@ ngx_rtmp_mp4_send(ngx_event_t *e) h.timestamp = (abs_frame ? cr->timestamp : cr->duration); - if (cr->size > sizeof(ngx_rtmp_mp4_buffer) - 1) { + ngx_memzero(&in, sizeof(in)); + ngx_memzero(&in_buf, sizeof(in_buf)); + + if (t->header && !t->header_sent) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: sending header size=%uz", + t->header_size); + + fhdr[0] = t->fhdr | 0x10; + fhdr[1] = fhdr[2] = fhdr[3] = fhdr[4] = 0; + + in.buf = &in_buf; + in_buf.pos = fhdr; + in_buf.last = fhdr + 5; + + 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); + ngx_rtmp_send_message(s, out, 0); + ngx_rtmp_free_shared_chain(cscf, out); + + t->header_sent = 1; + + goto next; + } + + ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: read frame of track=%ui, " + "offset=%O, size=%uz, timestamp=%uD, duration=%uD", + n, cr->offset, cr->size, cr->timestamp, cr->duration); + + fhdr_size = (t->header ? 5 : 1); + + if (cr->size + fhdr_size > sizeof(ngx_rtmp_mp4_buffer)) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, "mp4: too big frame: %D>%uz", cr->size, sizeof(ngx_rtmp_mp4_buffer)); continue; } - ret = ngx_read_file(&ctx->file, ngx_rtmp_mp4_buffer + 1, + ret = ngx_read_file(&ctx->file, ngx_rtmp_mp4_buffer + fhdr_size, cr->size, cr->offset); if (ret != (ssize_t) cr->size) { @@ -1188,15 +1720,21 @@ ngx_rtmp_mp4_send(ngx_event_t *e) } ngx_rtmp_mp4_buffer[0] = t->fhdr; - - /* TODO: handle video key flag */ - - ngx_memzero(&in, sizeof(in)); - ngx_memzero(&in_buf, sizeof(in_buf)); + if (cr->key) { + ngx_rtmp_mp4_buffer[0] |= 0x10; + } + + if (fhdr_size > 1) { + 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; + } + in.buf = &in_buf; in_buf.pos = ngx_rtmp_mp4_buffer; - in_buf.last = ngx_rtmp_mp4_buffer + cr->size + 1; + in_buf.last = ngx_rtmp_mp4_buffer + cr->size + fhdr_size; out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); @@ -1208,11 +1746,11 @@ ngx_rtmp_mp4_send(ngx_event_t *e) continue; } +next: active = 1; -next: if (cr->timestamp > end_timestamp && - (sched == 0 || cr->timestamp - end_timestamp < sched)) + (sched == 0 || cr->timestamp < end_timestamp + sched)) { sched = (uint32_t) (cr->timestamp - end_timestamp); } @@ -1240,9 +1778,11 @@ ngx_rtmp_mp4_seek_track(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, cr = &t->cursor; ngx_memzero(cr, sizeof(cr)); - return ngx_rtmp_mp4_seek_time(s, t, timestamp) != NGX_OK || + return ngx_rtmp_mp4_seek_time(s, t, timestamp * 90) != 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_size(s, t) != NGX_OK || + ngx_rtmp_mp4_seek_delay(s, t) != NGX_OK ? NGX_ERROR : NGX_OK; } @@ -1265,6 +1805,9 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_t timestamp) ngx_rtmp_mp4_stop(s); for (n = 0; n < ctx->ntracks; ++n) { + ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: seek track %ui", n); + ngx_rtmp_mp4_seek_track(s, &ctx->tracks[n], timestamp); } From a6c10a15bd019e06559679397515298471e43179 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 24 Aug 2012 13:40:44 +0400 Subject: [PATCH 32/44] video & audio working --- ngx_rtmp_mp4_module.c | 410 +++++++++++++++++++++++++++++------------- 1 file changed, 285 insertions(+), 125 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 7aee4a7..7510b40 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -137,10 +137,14 @@ typedef struct { 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; @@ -242,6 +246,8 @@ 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, @@ -290,6 +296,7 @@ typedef struct { 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 }, @@ -396,6 +403,8 @@ ngx_rtmp_mp4_parse_trak(ngx_rtmp_session_t *s, u_char *pos, u_char *last) 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); } @@ -423,6 +432,62 @@ 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) +{ + 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) { @@ -1091,17 +1156,27 @@ ngx_rtmp_mp4_next_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr = &t->cursor; if (cr->time_pos >= ngx_rtmp_r32(t->times->entry_count)) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next time overflow: time_pos=%ui", - cr->time_pos); + 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->duration = ngx_rtmp_r32(te->sample_delta) / 90; + cr->duration = ngx_rtmp_r32(te->sample_delta); cr->timestamp += cr->duration; + + 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++; @@ -1110,12 +1185,6 @@ ngx_rtmp_mp4_next_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr->time_count = 0; } - ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next time time_pos=%ui, timestamp=%uD, " - "duration=%uD, time_count=%ui, pos=%ui", - cr->time_pos, cr->timestamp, cr->duration, - cr->time_count, cr->pos); - return NGX_OK; } @@ -1158,17 +1227,23 @@ ngx_rtmp_mp4_seek_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, } if (cr->time_pos >= ngx_rtmp_r32(t->times->entry_count)) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek time overflow time_pos=%ui", - cr->time_pos); + 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_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek time src_timestamp=%i, timestamp=%D, " - "time_pos=%ui, pos=%ui", - timestamp, cr->timestamp, cr->time_pos, cr->pos); + 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; } @@ -1183,8 +1258,9 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr = &t->cursor; if (cr->chunk < 1) { - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: update offset underflow"); + ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, + "mp4: track#%ui offset[%ui] underflow", + t->id, cr->chunk); return NGX_ERROR; } @@ -1192,9 +1268,10 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) if (t->offsets) { if (chunk >= ngx_rtmp_r32(t->offsets->entry_count)) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: update offset overflow: chunk=%ui", - cr->chunk); + 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; } @@ -1202,8 +1279,10 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr->offset = ngx_rtmp_r32(t->offsets->entries[chunk]); cr->size = 0; - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: update offset offset=%O", + 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; @@ -1211,9 +1290,10 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) if (t->offsets64) { if (chunk >= ngx_rtmp_r32(t->offsets64->entry_count)) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: update offset64 overflow: chunk=%ui", - cr->chunk); + 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; } @@ -1221,8 +1301,10 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr->offset = ngx_rtmp_r32(t->offsets64->entries[chunk]); cr->size = 0; - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: update offset64 offset=%O", + 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; @@ -1237,6 +1319,7 @@ 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; @@ -1245,9 +1328,10 @@ ngx_rtmp_mp4_next_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr = &t->cursor; if (cr->chunk_pos >= ngx_rtmp_r32(t->chunks->entry_count)) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next chunk overflow chunk_pos=%ui", - cr->chunk_pos); + 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; } @@ -1264,21 +1348,28 @@ ngx_rtmp_mp4_next_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) nce = ce + 1; if (cr->chunk >= ngx_rtmp_r32(nce->first_chunk)) { cr->chunk_pos++; + ce = nce; } } - ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next chunk chunk=%ui, chunk_pos=%ui " - "chunk_count=%ui", - cr->chunk, cr->chunk_pos, cr->chunk_count); + new_chunk = 1; - return ngx_rtmp_mp4_update_offset(s, t); + } else { + new_chunk = 0; } - ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next chunk kept chunk=%ui, chunk_pos=%ui " - "chunk_count=%ui", - cr->chunk, cr->chunk_pos, cr->chunk_count); + 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; } @@ -1328,10 +1419,13 @@ ngx_rtmp_mp4_seek_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr->chunk_count = (ngx_uint_t) (cr->pos - dchunk * ngx_rtmp_r32(ce->samples_per_chunk)); - ngx_log_debug4(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek chunk pos=%ui, chunk=%ui, chunk_pos=%ui, " - "chunk_count=%ui", - cr->pos, cr->chunk, cr->chunk_pos, cr->chunk_count); + 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); } @@ -1350,9 +1444,9 @@ ngx_rtmp_mp4_next_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) if (t->sizes->sample_size) { cr->size = ngx_rtmp_r32(t->sizes->sample_size); - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next size const_size=%uz", - cr->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; } @@ -1360,27 +1454,31 @@ ngx_rtmp_mp4_next_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr->size_pos++; if (cr->size_pos >= ngx_rtmp_r32(t->sizes->sample_count)) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next size overflow size_pos=%ui", - cr->size_pos); + 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_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next size size_pos=%ui, size=%uz", - cr->size_pos, cr->size); + 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_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next size2 overflow size_pos=%ui", - cr->size_pos); + 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; } @@ -1405,17 +1503,18 @@ ngx_rtmp_mp4_seek_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) if (t->sizes->sample_size) { cr->size = ngx_rtmp_r32(t->sizes->sample_size); - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek size const_size=%uz", - cr->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_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek size overflow pos=%ui", - cr->pos); + 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; } @@ -1423,18 +1522,21 @@ ngx_rtmp_mp4_seek_size(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr->size_pos = cr->pos; cr->size = ngx_rtmp_r32(t->sizes->entries[cr->size_pos]); - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek size size_pos=%ui, size=%uz", - cr->size_pos, cr->size); + 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_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next size2 overflow size_pos=%ui", - cr->size_pos); + 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; } @@ -1453,6 +1555,7 @@ 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; @@ -1460,23 +1563,30 @@ ngx_rtmp_mp4_next_key(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) return NGX_OK; } - if (cr->key_pos >= ngx_rtmp_r32(t->keys->entry_count)) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next key overflow key_pos=%ui", - cr->key_pos); - - return NGX_OK; - } - - cr->key = (cr->pos + 1 == ngx_rtmp_r32(t->keys->entries[cr->key_pos])); - if (cr->key) { cr->key_pos++; } - ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next key %s pos=%ui, key_pos=%ui", - cr->key ? "match" : "miss", cr->pos, 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; } @@ -1486,6 +1596,7 @@ 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; @@ -1501,12 +1612,23 @@ ngx_rtmp_mp4_seek_key(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) cr->key_pos++; } - cr->key = (cr->key_pos < ngx_rtmp_r32(t->keys->entry_count) && - cr->pos + 1 == ngx_rtmp_r32(t->keys->entries[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; + } - ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek key %s key_pos=%ui, pos=%ui", - cr->key ? "match" : "miss", cr->key_pos, cr->pos); + 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; } @@ -1516,6 +1638,7 @@ 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; @@ -1524,30 +1647,40 @@ ngx_rtmp_mp4_next_delay(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) } if (cr->delay_pos >= ngx_rtmp_r32(t->delays->entry_count)) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next delay overflow key_pos=%ui", - cr->delay_pos); + 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(t->delays->entries[cr->delay_pos].sample_count)) - { + 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)) { - cr->delay = ngx_rtmp_r32(t->delays->entries[cr->delay_pos] - .sample_offset); + 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; } - ngx_log_debug3(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: next delay delay_pos=%ui, delay_count=%ui, delay=%ui", - cr->delay_pos, cr->delay_count, cr->delay); + 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; } @@ -1557,6 +1690,7 @@ 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; @@ -1566,24 +1700,37 @@ ngx_rtmp_mp4_seek_delay(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) } pos = 0; + de = t->delays->entries; while (cr->delay_pos < ngx_rtmp_r32(t->delays->entry_count)) { - dpos = ngx_rtmp_r32(t->delays->entries[cr->delay_pos].sample_count); + dpos = ngx_rtmp_r32(de->sample_count); if (pos + dpos > cr->pos) { cr->delay_count = cr->pos - pos; - cr->delay = ngx_rtmp_r32(t->delays->entries[cr->delay_pos] - .sample_offset); + cr->delay = ngx_rtmp_r32(de->sample_offset); break; } cr->delay_pos++; pos += dpos; + de++; } - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek delay delay_pos=%ui, delay_count=%ui", - cr->delay_pos, cr->delay_count); + 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; } @@ -1601,6 +1748,13 @@ ngx_rtmp_mp4_next(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) } +static 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 void ngx_rtmp_mp4_send(ngx_event_t *e) { @@ -1612,7 +1766,8 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_chain_t *out, in; ngx_rtmp_mp4_track_t *t; ngx_rtmp_mp4_cursor_t *cr; - uint32_t buflen, end_timestamp, sched; + uint32_t buflen, end_timestamp, sched, + timestamp, duration; ssize_t ret; u_char fhdr[5]; size_t fhdr_size; @@ -1645,14 +1800,17 @@ ngx_rtmp_mp4_send(ngx_event_t *e) continue; } - if (cr->timestamp > end_timestamp) { + 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", - n, cr->timestamp, end_timestamp); + "mp4: track#%ui ahead %uD > %uD", + t->id, timestamp, end_timestamp); goto next; } - abs_frame = (cr->duration == 0); + duration = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->duration); + abs_frame = (duration == 0); ngx_memzero(&h, sizeof(h)); @@ -1662,15 +1820,15 @@ ngx_rtmp_mp4_send(ngx_event_t *e) lh = h; - h.timestamp = (abs_frame ? cr->timestamp : cr->duration); + h.timestamp = (abs_frame ? timestamp : duration); ngx_memzero(&in, sizeof(in)); ngx_memzero(&in_buf, sizeof(in_buf)); if (t->header && !t->header_sent) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: sending header size=%uz", - t->header_size); + 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 | 0x10; fhdr[1] = fhdr[2] = fhdr[3] = fhdr[4] = 0; @@ -1697,16 +1855,17 @@ ngx_rtmp_mp4_send(ngx_event_t *e) } ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: read frame of track=%ui, " + "mp4: track#%ui read frame " "offset=%O, size=%uz, timestamp=%uD, duration=%uD", - n, cr->offset, cr->size, cr->timestamp, cr->duration); + t->id, cr->offset, cr->size, timestamp, + cr->duration); fhdr_size = (t->header ? 5 : 1); if (cr->size + fhdr_size > sizeof(ngx_rtmp_mp4_buffer)) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "mp4: too big frame: %D>%uz", - cr->size, sizeof(ngx_rtmp_mp4_buffer)); + "mp4: track#%ui too big frame: %D>%uz", + t->id, cr->size, sizeof(ngx_rtmp_mp4_buffer)); continue; } @@ -1715,7 +1874,7 @@ ngx_rtmp_mp4_send(ngx_event_t *e) if (ret != (ssize_t) cr->size) { ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "mp4: could not read frame"); + "mp4: track#%ui could not read frame", t->id); continue; } @@ -1749,10 +1908,10 @@ ngx_rtmp_mp4_send(ngx_event_t *e) next: active = 1; - if (cr->timestamp > end_timestamp && - (sched == 0 || cr->timestamp < end_timestamp + sched)) + if (timestamp > end_timestamp && + (sched == 0 || timestamp < end_timestamp + sched)) { - sched = (uint32_t) (cr->timestamp - end_timestamp); + sched = (uint32_t) (timestamp - end_timestamp); } } @@ -1778,7 +1937,8 @@ ngx_rtmp_mp4_seek_track(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, cr = &t->cursor; ngx_memzero(cr, sizeof(cr)); - return ngx_rtmp_mp4_seek_time(s, t, timestamp * 90) != NGX_OK || + return ngx_rtmp_mp4_seek_time(s, t, timestamp * + t->time_scale / 1000) != 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 || @@ -1800,13 +1960,13 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_t timestamp) } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: start timestamp=%i", timestamp); + "mp4: start t=%i", timestamp); ngx_rtmp_mp4_stop(s); for (n = 0; n < ctx->ntracks; ++n) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek track %ui", n); + "mp4: track#%ui seek", n); ngx_rtmp_mp4_seek_track(s, &ctx->tracks[n], timestamp); } From db0f76552862a0cbdc5752dab84b2585a45bf31c Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 24 Aug 2012 13:57:13 +0400 Subject: [PATCH 33/44] added metadata --- ngx_rtmp_mp4_module.c | 125 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 7510b40..2c1ce78 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -148,7 +148,7 @@ typedef struct { u_char *header; size_t header_size; - ngx_int_t header_sent; + unsigned header_sent:1; ngx_rtmp_mp4_times_t *times; ngx_rtmp_mp4_delays_t *delays; @@ -168,6 +168,8 @@ 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; @@ -1755,6 +1757,119 @@ ngx_rtmp_mp4_to_rtmp_timestamp(ngx_rtmp_mp4_track_t *t, uint32_t ts) } +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; + } 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 }, + }; + + 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; + + 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_LIVE_CSID_META; + h.msid = NGX_RTMP_LIVE_MSID; + h.type = NGX_RTMP_MSG_AMF_META; + + ngx_rtmp_prepare_message(s, &h, NULL, out); + ngx_rtmp_send_message(s, out, 0); + ngx_rtmp_free_shared_chain(cscf, out); + + return NGX_OK; +} + + static void ngx_rtmp_mp4_send(ngx_event_t *e) { @@ -1783,6 +1898,13 @@ ngx_rtmp_mp4_send(ngx_event_t *e) return; } + if (!ctx->meta_sent) { + ngx_rtmp_mp4_send_meta(s); + ctx->meta_sent = 1; + active = 1; + goto again; + } + buflen = (s->buflen ? s->buflen : NGX_RTMP_MP4_DEFAULT_BUFLEN); t = ctx->tracks; @@ -1922,6 +2044,7 @@ next: return; } +again: if (active) { ngx_post_event(e, &ngx_posted_events); } From f22e72ab21521c0bab618a1891ed2c07387adb57 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 24 Aug 2012 16:08:03 +0400 Subject: [PATCH 34/44] fixed cursor initialization --- ngx_rtmp_mp4_module.c | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 2c1ce78..9e9835e 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -238,6 +238,22 @@ ngx_rtmp_r64(uint64_t n) } +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 @@ -1193,7 +1209,7 @@ ngx_rtmp_mp4_next_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) static ngx_int_t ngx_rtmp_mp4_seek_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, - ngx_int_t timestamp) + uint32_t timestamp) { ngx_rtmp_mp4_cursor_t *cr; ngx_rtmp_mp4_time_entry_t *te; @@ -1211,14 +1227,15 @@ ngx_rtmp_mp4_seek_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, 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 (cr->timestamp + dt >= timestamp) { if (te->sample_delta == 0) { return NGX_ERROR; } dn = (timestamp - cr->timestamp) / ngx_rtmp_r32(te->sample_delta); - cr->timestamp = ngx_rtmp_r32(te->sample_delta) * dn; + cr->timestamp += ngx_rtmp_r32(te->sample_delta) * dn; cr->pos += dn; + break; } @@ -1271,7 +1288,7 @@ ngx_rtmp_mp4_update_offset(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) 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", + "mp4: track#%ui offset[%ui/%uD] overflow", t->id, cr->chunk, ngx_rtmp_r32(t->offsets->entry_count)); @@ -1750,13 +1767,6 @@ ngx_rtmp_mp4_next(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) } -static 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 ngx_int_t ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) { @@ -2058,10 +2068,10 @@ ngx_rtmp_mp4_seek_track(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, ngx_rtmp_mp4_cursor_t *cr; cr = &t->cursor; - ngx_memzero(cr, sizeof(cr)); + ngx_memzero(cr, sizeof(*cr)); - return ngx_rtmp_mp4_seek_time(s, t, timestamp * - t->time_scale / 1000) != NGX_OK || + return 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 || @@ -2083,7 +2093,7 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_t timestamp) } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: start t=%i", timestamp); + "mp4: start timestamp=%i", timestamp); ngx_rtmp_mp4_stop(s); From a92d23d5301cb81b51123af99ce8282f25830255 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Fri, 24 Aug 2012 20:46:40 +0400 Subject: [PATCH 35/44] added AMF shortcuts & project-wide stream numbers; updated mp4 streamer --- ngx_rtmp.h | 25 ++++++++ ngx_rtmp_cmd_module.c | 5 +- ngx_rtmp_live_module.h | 8 +-- ngx_rtmp_mp4_module.c | 141 ++++++++++++++++------------------------- ngx_rtmp_play_module.c | 3 +- ngx_rtmp_send.c | 61 +++++++++++++++++- ngx_rtmp_streams.h | 28 ++++++++ 7 files changed, 173 insertions(+), 98 deletions(-) create mode 100644 ngx_rtmp_streams.h diff --git a/ngx_rtmp.h b/ngx_rtmp.h index 96a9aa5..f8f2061 100644 --- a/ngx_rtmp.h +++ b/ngx_rtmp.h @@ -375,6 +375,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); @@ -469,6 +490,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 dd70ffb..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; 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 index 9e9835e..43b288a 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -4,8 +4,8 @@ #include "ngx_rtmp_cmd_module.h" -#include "ngx_rtmp_live_module.h" #include "ngx_rtmp_codec_module.h" +#include "ngx_rtmp_streams.h" static ngx_rtmp_play_pt next_play; @@ -118,6 +118,8 @@ typedef struct { ngx_int_t key; uint32_t delay; + unsigned valid:1; + ngx_uint_t pos; ngx_uint_t key_pos; @@ -186,56 +188,8 @@ typedef struct { } ngx_rtmp_mp4_ctx_t; -/* system stuff for mmapping; 4K pages assumed */ -/* TODO: more portable code */ -#define NGX_RTMP_PAGE_SHIFT 12 -#define NGX_RTMP_PAGE_SIZE (1 << NGX_RTMP_PAGE_SHIFT) -#define NGX_RTMP_PAGE_MASK (NGX_RTMP_PAGE_SIZE - 1) - - -#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) - - -/* -#define ngx_rtmp_r32(n) (((n) & 0x000000ffull) << 24 | \ - ((n) & 0x0000ff00ull) << 8 | \ - ((n) & 0x00ff0000ull) >> 8 | \ - ((n) & 0xff000000ull) >> 24) -*/ - -static inline uint16_t -ngx_rtmp_r16(uint16_t n) -{ - uint16_t ret; - - /*TODO: optimize */ - ngx_rtmp_rmemcpy(&ret, &n, 2); - return ret; -} - - -static inline uint32_t -ngx_rtmp_r32(uint32_t n) -{ - uint32_t ret; - - /*TODO: optimize */ - ngx_rtmp_rmemcpy(&ret, &n, 4); - return ret; -} - - -static inline uint64_t -ngx_rtmp_r64(uint64_t n) -{ - uint64_t ret; - - ngx_rtmp_rmemcpy(&ret, &n, 8); - return ret; -} +#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 @@ -252,8 +206,6 @@ ngx_rtmp_mp4_from_rtmp_timestamp(ngx_rtmp_mp4_track_t *t, uint32_t ts) } - - #define NGX_RTMP_MP4_DEFAULT_BUFLEN 1000 @@ -526,14 +478,14 @@ ngx_rtmp_mp4_parse_hdlr(ngx_rtmp_session_t *s, u_char *pos, u_char *last) if (type == ngx_rtmp_mp4_make_tag('v','i','d','e')) { ctx->track->type = NGX_RTMP_MSG_VIDEO; - ctx->track->csid = NGX_RTMP_LIVE_CSID_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_LIVE_CSID_AUDIO; + ctx->track->csid = NGX_RTMP_CSID_AUDIO; ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: audio track"); @@ -1082,7 +1034,6 @@ ngx_rtmp_mp4_init(ngx_rtmp_session_t *s) offset = 0; size = 0; - /* find moov box */ for ( ;; ) { n = ngx_read_file(&ctx->file, (u_char *) &hdr, sizeof(hdr), offset); @@ -1093,8 +1044,6 @@ ngx_rtmp_mp4_init(ngx_rtmp_session_t *s) return NGX_ERROR; } - /*TODO: implement 64-bit boxes */ - size = ngx_rtmp_r32(hdr[0]); if (hdr[1] == ngx_rtmp_mp4_make_tag('m','o','o','v')) { @@ -1116,8 +1065,7 @@ ngx_rtmp_mp4_init(ngx_rtmp_session_t *s) size -= 8; offset += 8; - /* mmap moov box */ - page_offset = (offset & NGX_RTMP_PAGE_MASK); + page_offset = offset & (ngx_pagesize - 1); ctx->mmaped_size = page_offset + size; ctx->mmaped = mmap(NULL, ctx->mmaped_size, PROT_READ, MAP_SHARED, @@ -1131,7 +1079,6 @@ ngx_rtmp_mp4_init(ngx_rtmp_session_t *s) return NGX_ERROR; } - /* locate all required data within mapped area */ return ngx_rtmp_mp4_parse(s, (u_char *) ctx->mmaped + page_offset, (u_char *) ctx->mmaped + page_offset + size); } @@ -1214,7 +1161,6 @@ ngx_rtmp_mp4_seek_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; uint32_t dt; - ngx_uint_t dn; if (t->times == NULL) { return NGX_ERROR; @@ -1232,9 +1178,9 @@ ngx_rtmp_mp4_seek_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, return NGX_ERROR; } - dn = (timestamp - cr->timestamp) / ngx_rtmp_r32(te->sample_delta); - cr->timestamp += ngx_rtmp_r32(te->sample_delta) * dn; - cr->pos += dn; + 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; } @@ -1758,12 +1704,18 @@ ngx_rtmp_mp4_seek_delay(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) static ngx_int_t ngx_rtmp_mp4_next(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) { - return 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 - ? NGX_ERROR : NGX_OK; + 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; } @@ -1868,8 +1820,8 @@ ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) ngx_memzero(&h, sizeof(h)); - h.csid = NGX_RTMP_LIVE_CSID_META; - h.msid = NGX_RTMP_LIVE_MSID; + h.csid = NGX_RTMP_CSID_AMF; + h.msid = NGX_RTMP_MSID; h.type = NGX_RTMP_MSG_AMF_META; ngx_rtmp_prepare_message(s, &h, NULL, out); @@ -1928,7 +1880,7 @@ ngx_rtmp_mp4_send(ngx_event_t *e) for (n = 0; n < ctx->ntracks; ++n, ++t) { cr = &t->cursor; - if (cr->size == 0) { + if (!cr->valid) { continue; } @@ -1946,7 +1898,7 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_memzero(&h, sizeof(h)); - h.msid = NGX_RTMP_LIVE_MSID; + h.msid = NGX_RTMP_MSID; h.type = t->type; h.csid = t->csid; @@ -1989,8 +1941,7 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: track#%ui read frame " "offset=%O, size=%uz, timestamp=%uD, duration=%uD", - t->id, cr->offset, cr->size, timestamp, - cr->duration); + t->id, cr->offset, cr->size, timestamp, duration); fhdr_size = (t->header ? 5 : 1); @@ -2012,8 +1963,14 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_rtmp_mp4_buffer[0] = t->fhdr; - if (cr->key) { - ngx_rtmp_mp4_buffer[0] |= 0x10; + if (h.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 (fhdr_size > 1) { @@ -2057,7 +2014,12 @@ next: again: if (active) { ngx_post_event(e, &ngx_posted_events); + return; } + + ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID); + + ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stopped"); } @@ -2070,13 +2032,18 @@ ngx_rtmp_mp4_seek_track(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t, cr = &t->cursor; ngx_memzero(cr, sizeof(*cr)); - return ngx_rtmp_mp4_seek_time(s, t, ngx_rtmp_mp4_from_rtmp_timestamp( + 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 - ? NGX_ERROR : 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; } @@ -2097,6 +2064,10 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_t timestamp) ngx_rtmp_mp4_stop(s); + if (timestamp < 0) { + timestamp = 0; + } + for (n = 0; n < ctx->ntracks; ++n) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: track#%ui seek", n); diff --git a/ngx_rtmp_play_module.c b/ngx_rtmp_play_module.c index 626cfc7..c631ed5 100644 --- a/ngx_rtmp_play_module.c +++ b/ngx_rtmp_play_module.c @@ -453,7 +453,8 @@ ngx_rtmp_play_send(ngx_event_t *e) 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); + ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID); + ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stopped"); return; } 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_ */ From 7f447bbe9500d231f61cd0fbd7d8177797338ad0 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Sat, 25 Aug 2012 16:53:57 +0400 Subject: [PATCH 36/44] implemented MPEG ES descriptor parser; AAC is now fully supported --- ngx_rtmp_mp4_module.c | 289 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 247 insertions(+), 42 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 43b288a..cdf0f24 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -289,6 +289,32 @@ static ngx_rtmp_mp4_box_t ngx_rtmp_mp4_boxes[] = { }; +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 DecoderSpecific Descriptor */ +}; + + static ngx_command_t ngx_rtmp_mp4_commands[] = { { ngx_string("play_mp4"), @@ -526,13 +552,17 @@ ngx_rtmp_mp4_parse_video(ngx_rtmp_session_t *s, u_char *pos, u_char *last, pos += 52; - ctx->track->fhdr = codec; - 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); - return ngx_rtmp_mp4_parse(s, pos, last); + if (ngx_rtmp_mp4_parse(s, pos, last) != NGX_OK) { + return NGX_ERROR; + } + + ctx->track->fhdr = ctx->track->codec; + + return NGX_OK; } @@ -595,14 +625,18 @@ ngx_rtmp_mp4_parse_audio(ngx_rtmp_session_t *s, u_char *pos, u_char *last, break; } - *p |= (codec << 4); - 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); - return ngx_rtmp_mp4_parse(s, pos, last); + if (ngx_rtmp_mp4_parse(s, pos, last) != NGX_OK) { + return NGX_ERROR; + } + + *p |= (ctx->track->codec << 4); + + return NGX_OK; } @@ -647,33 +681,190 @@ ngx_rtmp_mp4_parse_mp4a(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) +ngx_rtmp_mp4_parse_ds(ngx_rtmp_session_t *s, u_char *pos, u_char *last) { - ngx_rtmp_mp4_ctx_t *ctx; - - if (pos == last) { - return NGX_OK; - } + ngx_rtmp_mp4_ctx_t *ctx; + ngx_rtmp_mp4_track_t *t; ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); - if (ctx->track == NULL || ctx->track->codec != NGX_RTMP_AUDIO_AAC) { + t = ctx->track; + + if (t == NULL) { return NGX_OK; } - /*TODO: parse AAC header? */ - - ctx->track->header = pos; - ctx->track->header_size = (size_t) (last - pos); + t->header = pos; + t->header_size = (size_t) (last - pos); ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: audio AAC header size=%uz", - ctx->track->header_size); + "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) { @@ -1914,12 +2105,20 @@ ngx_rtmp_mp4_send(ngx_event_t *e) "mp4: track#%ui sending header of size=%uz", t->id, t->header_size); - fhdr[0] = t->fhdr | 0x10; - fhdr[1] = fhdr[2] = fhdr[3] = fhdr[4] = 0; + 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 + 5; + in_buf.last = fhdr + fhdr_size; out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); @@ -1943,7 +2142,32 @@ ngx_rtmp_mp4_send(ngx_event_t *e) "offset=%O, size=%uz, timestamp=%uD, duration=%uD", t->id, cr->offset, cr->size, timestamp, duration); - fhdr_size = (t->header ? 5 : 1); + 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, @@ -1961,25 +2185,6 @@ ngx_rtmp_mp4_send(ngx_event_t *e) continue; } - ngx_rtmp_mp4_buffer[0] = t->fhdr; - - if (h.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 (fhdr_size > 1) { - 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; - } - in.buf = &in_buf; in_buf.pos = ngx_rtmp_mp4_buffer; in_buf.last = ngx_rtmp_mp4_buffer + cr->size + fhdr_size; From 24539ca1168ac5655fbee819a3e96161ab458584 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Sun, 26 Aug 2012 16:13:23 +0400 Subject: [PATCH 37/44] improved vod scheduling: added dry buffer event --- ngx_rtmp.h | 2 ++ ngx_rtmp_handler.c | 2 ++ ngx_rtmp_mp4_module.c | 31 +++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/ngx_rtmp.h b/ngx_rtmp.h index f8f2061..1e2f12b 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; 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_mp4_module.c b/ngx_rtmp_mp4_module.c index cdf0f24..810232b 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -244,6 +244,8 @@ 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, @@ -282,6 +284,7 @@ static ngx_rtmp_mp4_box_t ngx_rtmp_mp4_boxes[] = { { 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 }, @@ -647,6 +650,13 @@ ngx_rtmp_mp4_parse_avc1(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) +{ + 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) { @@ -2039,6 +2049,7 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ssize_t ret; u_char fhdr[5]; size_t fhdr_size; + ngx_int_t rc; ngx_uint_t n, abs_frame, active; s = e->data; @@ -2129,9 +2140,13 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_rtmp_append_shared_bufs(cscf, out, &in); ngx_rtmp_prepare_message(s, &h, NULL, out); - ngx_rtmp_send_message(s, out, 0); + rc = ngx_rtmp_send_message(s, out, 0); ngx_rtmp_free_shared_chain(cscf, out); + if (rc == NGX_AGAIN) { + goto full; + } + t->header_sent = 1; goto next; @@ -2192,9 +2207,13 @@ ngx_rtmp_mp4_send(ngx_event_t *e) out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); ngx_rtmp_prepare_message(s, &h, abs_frame ? NULL : &lh, out); - ngx_rtmp_send_message(s, out, 0); + rc = ngx_rtmp_send_message(s, out, 0); ngx_rtmp_free_shared_chain(cscf, out); + if (rc == NGX_AGAIN) { + goto full; + } + if (ngx_rtmp_mp4_next(s, t) != NGX_OK) { continue; } @@ -2225,6 +2244,14 @@ again: ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID); ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stopped"); + + return; + +full: + ngx_post_event(e, &s->posted_dry_events); + + return; + } From 224b6d942a42cf16ca507db02076d559011cadf2 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 27 Aug 2012 18:58:28 +0400 Subject: [PATCH 38/44] implemented common base for video-on-demand: added flv & mp4 modules as sub-modules of play module --- config | 2 + ngx_rtmp_flv_module.c | 642 +++++++++++++++++++++++++++++++++++++ ngx_rtmp_mp4_module.c | 585 ++++++++++------------------------ ngx_rtmp_play_module.c | 696 +++++++++++++---------------------------- ngx_rtmp_play_module.h | 58 ++++ 5 files changed, 1084 insertions(+), 899 deletions(-) create mode 100644 ngx_rtmp_flv_module.c create mode 100644 ngx_rtmp_play_module.h diff --git a/config b/config index f5c2d00..16e74ef 100644 --- a/config +++ b/config @@ -7,6 +7,7 @@ CORE_MODULES="$CORE_MODULES 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 \ @@ -36,6 +37,7 @@ NGX_ADDON_SRCS="$NGX_ADDON_SRCS \ $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 \ 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_mp4_module.c b/ngx_rtmp_mp4_module.c index 810232b..a7b61c3 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -3,29 +3,18 @@ */ -#include "ngx_rtmp_cmd_module.h" +#include "ngx_rtmp_play_module.h" #include "ngx_rtmp_codec_module.h" #include "ngx_rtmp_streams.h" -static ngx_rtmp_play_pt next_play; -static ngx_rtmp_close_stream_pt next_close_stream; -static ngx_rtmp_seek_pt next_seek; -static ngx_rtmp_pause_pt next_pause; - - static ngx_int_t ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf); -static void * ngx_rtmp_mp4_create_app_conf(ngx_conf_t *cf); -static char * ngx_rtmp_mp4_merge_app_conf(ngx_conf_t *cf, - void *parent, void *child); -static void ngx_rtmp_mp4_send(ngx_event_t *e); -static ngx_int_t ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_t offset); -static ngx_int_t ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s); - - -typedef struct { - ngx_str_t root; -} ngx_rtmp_mp4_app_conf_t; +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) @@ -165,8 +154,6 @@ typedef struct { typedef struct { - ngx_file_t file; - void *mmaped; size_t mmaped_size; @@ -183,8 +170,6 @@ typedef struct { ngx_uint_t sample_rate; uint32_t start_timestamp, epoch; - - ngx_event_t write_evt; } ngx_rtmp_mp4_ctx_t; @@ -312,22 +297,9 @@ typedef struct { 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 DecoderSpecific Descriptor */ -}; - - -static ngx_command_t ngx_rtmp_mp4_commands[] = { - - { ngx_string("play_mp4"), - NGX_RTMP_MAIN_CONF|NGX_RTMP_SRV_CONF|NGX_RTMP_APP_CONF|NGX_CONF_TAKE1, - ngx_conf_set_str_slot, - NGX_RTMP_APP_CONF_OFFSET, - offsetof(ngx_rtmp_mp4_app_conf_t, root), - NULL }, - - ngx_null_command + { 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 */ }; @@ -338,15 +310,15 @@ static ngx_rtmp_module_t ngx_rtmp_mp4_module_ctx = { NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ - ngx_rtmp_mp4_create_app_conf, /* create app configuration */ - ngx_rtmp_mp4_merge_app_conf /* merge app 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 */ - ngx_rtmp_mp4_commands, /* module directives */ + NULL, /* module directives */ NGX_RTMP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ @@ -359,33 +331,6 @@ ngx_module_t ngx_rtmp_mp4_module = { }; -static void * -ngx_rtmp_mp4_create_app_conf(ngx_conf_t *cf) -{ - ngx_rtmp_mp4_app_conf_t *pacf; - - pacf = ngx_pcalloc(cf->pool, sizeof(ngx_rtmp_mp4_app_conf_t)); - - if (pacf == NULL) { - return NULL; - } - - return pacf; -} - - -static char * -ngx_rtmp_mp4_merge_app_conf(ngx_conf_t *cf, void *parent, void *child) -{ - ngx_rtmp_mp4_app_conf_t *prev = parent; - ngx_rtmp_mp4_app_conf_t *conf = child; - - ngx_conf_merge_str_value(conf->root, prev->root, ""); - - return NGX_CONF_OK; -} - - static ngx_int_t ngx_rtmp_mp4_parse_trak(ngx_rtmp_session_t *s, u_char *pos, u_char *last) { @@ -1218,97 +1163,6 @@ ngx_rtmp_mp4_parse(ngx_rtmp_session_t *s, u_char *pos, u_char *last) } -static ngx_int_t -ngx_rtmp_mp4_init(ngx_rtmp_session_t *s) -{ - 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->mmaped || ctx->file.fd == NGX_INVALID_FILE) { - return NGX_OK; - } - - offset = 0; - size = 0; - - for ( ;; ) { - n = ngx_read_file(&ctx->file, (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, - ctx->file.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_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_next_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) { @@ -2026,17 +1880,40 @@ ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) h.type = NGX_RTMP_MSG_AMF_META; ngx_rtmp_prepare_message(s, &h, NULL, out); - ngx_rtmp_send_message(s, out, 0); + 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 void -ngx_rtmp_mp4_send(ngx_event_t *e) +static ngx_int_t +ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) { - ngx_rtmp_session_t *s; ngx_rtmp_mp4_ctx_t *ctx; ngx_buf_t in_buf; ngx_rtmp_header_t h, lh; @@ -2052,21 +1929,22 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_int_t rc; ngx_uint_t n, abs_frame, active; - s = e->data; - 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; + return NGX_ERROR; } if (!ctx->meta_sent) { - ngx_rtmp_mp4_send_meta(s); - ctx->meta_sent = 1; - active = 1; - goto again; + 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); @@ -2144,7 +2022,7 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_rtmp_free_shared_chain(cscf, out); if (rc == NGX_AGAIN) { - goto full; + return NGX_AGAIN; } t->header_sent = 1; @@ -2191,7 +2069,7 @@ ngx_rtmp_mp4_send(ngx_event_t *e) continue; } - ret = ngx_read_file(&ctx->file, ngx_rtmp_mp4_buffer + fhdr_size, + ret = ngx_read_file(f, ngx_rtmp_mp4_buffer + fhdr_size, cr->size, cr->offset); if (ret != (ssize_t) cr->size) { @@ -2211,7 +2089,7 @@ ngx_rtmp_mp4_send(ngx_event_t *e) ngx_rtmp_free_shared_chain(cscf, out); if (rc == NGX_AGAIN) { - goto full; + return NGX_AGAIN; } if (ngx_rtmp_mp4_next(s, t) != NGX_OK) { @@ -2229,58 +2107,114 @@ next: } if (sched) { - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: scheduling %uD", sched); - ngx_add_timer(e, sched); - return; + return sched; } -again: - if (active) { - ngx_post_event(e, &ngx_posted_events); - return; - } - - ngx_rtmp_send_user_stream_eof(s, NGX_RTMP_MSID); - - ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stopped"); - - return; - -full: - ngx_post_event(e, &s->posted_dry_events); - - return; - + return active ? NGX_OK : NGX_DONE; } 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_init(ngx_rtmp_session_t *s, ngx_file_t *f) { - ngx_rtmp_mp4_cursor_t *cr; + ngx_rtmp_mp4_ctx_t *ctx; + uint32_t hdr[2]; + ssize_t n; + size_t offset, page_offset, size; - cr = &t->cursor; - ngx_memzero(cr, sizeof(*cr)); + ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); - 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) - { + 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; } - cr->valid = 1; + 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_int_t timestamp) +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; @@ -2292,13 +2226,7 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_t timestamp) } ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: start timestamp=%i", timestamp); - - ngx_rtmp_mp4_stop(s); - - if (timestamp < 0) { - timestamp = 0; - } + "mp4: start timestamp=%ui", timestamp); for (n = 0; n < ctx->ntracks; ++n) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, @@ -2310,225 +2238,52 @@ ngx_rtmp_mp4_start(ngx_rtmp_session_t *s, ngx_int_t timestamp) ctx->epoch = ngx_current_msec; ctx->start_timestamp = timestamp; - ngx_post_event((&ctx->write_evt), &ngx_posted_events) - return NGX_OK; } static ngx_int_t -ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s) +ngx_rtmp_mp4_stop(ngx_rtmp_session_t *s, ngx_file_t *f) { - ngx_rtmp_mp4_ctx_t *ctx; - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); - - if (ctx == NULL) { - return NGX_OK; - } - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, "mp4: stop"); - if (ctx->write_evt.timer_set) { - ngx_del_timer(&ctx->write_evt); - } - - if (ctx->write_evt.prev) { - ngx_delete_posted_event((&ctx->write_evt)); - } - return NGX_OK; } -static ngx_int_t -ngx_rtmp_mp4_close_stream(ngx_rtmp_session_t *s, ngx_rtmp_close_stream_t *v) -{ - ngx_rtmp_mp4_ctx_t *ctx; - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); - - if (ctx == NULL) { - goto next; - } - - ngx_log_debug0(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: close_stream"); - - ngx_rtmp_mp4_stop(s); - - ngx_rtmp_mp4_done(s); - - if (ctx->file.fd != NGX_INVALID_FILE) { - ngx_close_file(ctx->file.fd); - ctx->file.fd = NGX_INVALID_FILE; - } - -next: - return next_close_stream(s, v); -} - - -static ngx_int_t -ngx_rtmp_mp4_seek(ngx_rtmp_session_t *s, ngx_rtmp_seek_t *v) -{ - ngx_rtmp_mp4_ctx_t *ctx; - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); - - if (ctx == NULL || ctx->file.fd == NGX_INVALID_FILE) { - goto next; - } - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: seek timestamp=%i", (ngx_int_t) v->offset); - - ngx_rtmp_mp4_start(s, v->offset); - -next: - return next_seek(s, v); -} - - -static ngx_int_t -ngx_rtmp_mp4_pause(ngx_rtmp_session_t *s, ngx_rtmp_pause_t *v) -{ - ngx_rtmp_mp4_ctx_t *ctx; - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); - - if (ctx == NULL || ctx->file.fd == NGX_INVALID_FILE) { - goto next; - } - - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: pause=%i timestamp=%i", - (ngx_int_t) v->pause, (ngx_int_t) v->position); - - if (v->pause) { - ngx_rtmp_mp4_stop(s); - } else { - ngx_rtmp_mp4_start(s, v->position); - } - -next: - return next_pause(s, v); -} - - -static ngx_int_t -ngx_rtmp_mp4_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) -{ - ngx_rtmp_mp4_app_conf_t *pacf; - ngx_rtmp_mp4_ctx_t *ctx; - u_char *p; - ngx_event_t *e; - size_t len; - static u_char path[NGX_MAX_PATH]; - u_char *name; - - pacf = ngx_rtmp_get_module_app_conf(s, ngx_rtmp_mp4_module); - - if (pacf == NULL || pacf->root.len == 0) { - goto next; - } - - if (ngx_strncasecmp(v->name, (u_char *) "mp4:", sizeof("mp4:") - 1) == 0) { - name = v->name + sizeof("mp4:") - 1; - goto ok; - } - - len = ngx_strlen(v->name); - - if (len >= sizeof(".mp4") && - ngx_strncasecmp(v->name + len - sizeof(".mp4") + 1, (u_char *) ".mp4", - sizeof(".mp4") - 1)) - { - name = v->name; - goto ok; - } - - goto next; -ok: - - ngx_log_debug2(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: play name='%s' timestamp=%i", - name, (ngx_int_t) v->start); - - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); - - if (ctx && ctx->file.fd != NGX_INVALID_FILE) { - ngx_log_error(NGX_LOG_ERR, s->connection->log, 0, - "mp4: already playing"); - goto next; - } - - /* check for double-dot in name; - * we should not move out of play directory */ - for (p = 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, - "mp4: bad name '%s'", name); - return NGX_ERROR; - } - } - - if (ctx == NULL) { - ctx = ngx_palloc(s->connection->pool, sizeof(ngx_rtmp_mp4_ctx_t)); - ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_mp4_module); - } - ngx_memzero(ctx, sizeof(*ctx)); - - ctx->file.log = s->connection->log; - - p = ngx_snprintf(path, sizeof(path), "%V/%s", &pacf->root, name); - *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, - "mp4: error opening file %s", path); - goto next; - } - - ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: opened file '%s'", path); - - e = &ctx->write_evt; - e->data = s; - e->handler = ngx_rtmp_mp4_send; - e->log = s->connection->log; - - ngx_rtmp_send_user_recorded(s, 1); - - ngx_rtmp_mp4_init(s); - - ngx_rtmp_mp4_start(s, v->start); - -next: - return next_play(s, v); -} - - static ngx_int_t ngx_rtmp_mp4_postconfiguration(ngx_conf_t *cf) { - next_play = ngx_rtmp_play; - ngx_rtmp_play = ngx_rtmp_mp4_play; + ngx_rtmp_play_main_conf_t *pmcf; + ngx_rtmp_play_fmt_t **pfmt, *fmt; - next_close_stream = ngx_rtmp_close_stream; - ngx_rtmp_close_stream = ngx_rtmp_mp4_close_stream; + pmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_play_module); - next_seek = ngx_rtmp_seek; - ngx_rtmp_seek = ngx_rtmp_mp4_seek; + pfmt = ngx_array_push(&pmcf->fmts); - next_pause = ngx_rtmp_pause; - ngx_rtmp_pause = ngx_rtmp_mp4_pause; + 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_play_module.c b/ngx_rtmp_play_module.c index c631ed5..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,453 +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, NGX_RTMP_MSID); - ngx_rtmp_send_status(s, "NetStream.Play.Stop", "status", "Stopped"); - 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; } @@ -589,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; @@ -614,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; @@ -644,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); @@ -659,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); @@ -681,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); @@ -722,38 +396,92 @@ ngx_rtmp_play_play(ngx_rtmp_session_t *s, ngx_rtmp_play_t *v) 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_ */ From 58491f8feddbe801557704dd1d1ac9d0950cdf3f Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 27 Aug 2012 19:02:45 +0400 Subject: [PATCH 39/44] fixed compilation --- ngx_rtmp_mp4_module.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index a7b61c3..6107d2d 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -1116,14 +1116,11 @@ ngx_rtmp_mp4_parse_co64(ngx_rtmp_session_t *s, u_char *pos, u_char *last) static ngx_int_t ngx_rtmp_mp4_parse(ngx_rtmp_session_t *s, u_char *pos, u_char *last) { - ngx_rtmp_mp4_ctx_t *ctx; uint32_t *hdr, tag; size_t size, nboxes; ngx_uint_t n; ngx_rtmp_mp4_box_t *b; - ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_mp4_module); - while (pos != last) { if (pos + 8 > last) { ngx_log_debug1(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, From 8293c425a5a1e2056e4f9d2cf29b66ba7af37676 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Mon, 27 Aug 2012 19:12:34 +0400 Subject: [PATCH 40/44] updated README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3bce60e..5cedfe0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ * Live streaming of video/audio -* Video on demand (FLV) +* Video on demand FLV/MP4 * Stream relay support for distributed streaming: push & pull models @@ -154,6 +154,10 @@ name - interpreted by each application play /var/flvs; } + application vod2 { + play /var/mp4s; + } + # Many publishers, many subscribers # no checks, no recording application videochat { From 562a6aad7ec8f05956e8210ef0b0a7245818cebc Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 29 Aug 2012 09:58:56 +0400 Subject: [PATCH 41/44] fixed non-standard (48kHz) audio streaming --- ngx_rtmp_mp4_module.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 6107d2d..c928c3c 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -560,6 +560,9 @@ ngx_rtmp_mp4_parse_audio(ngx_rtmp_session_t *s, u_char *pos, u_char *last, } switch (ctx->sample_rate) { + case 5512: + break; + case 11025: *p |= 0x04; break; @@ -568,7 +571,7 @@ ngx_rtmp_mp4_parse_audio(ngx_rtmp_session_t *s, u_char *pos, u_char *last, *p |= 0x08; break; - case 44100: + default: /* 44100, 4800 etc */ *p |= 0x0c; break; } From 1dbd9b228d160c66e032c8a7cf01c0774ceafbaf Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 29 Aug 2012 11:12:32 +0400 Subject: [PATCH 42/44] fixed mp4 seeking --- ngx_rtmp_mp4_module.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index c928c3c..833b72e 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -1436,7 +1436,7 @@ ngx_rtmp_mp4_seek_chunk(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) 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 - dchunk * + 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, @@ -1792,6 +1792,7 @@ ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) double duration; double video_codec_id; double audio_codec_id; + double audio_sample_rate; } v; static ngx_rtmp_amf_elt_t out_inf[] = { @@ -1823,6 +1824,10 @@ ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) { 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[] = { @@ -1856,6 +1861,8 @@ ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) v.duration = d; } + v.audio_sample_rate = ctx->sample_rate; + switch (t->type) { case NGX_RTMP_MSG_AUDIO: v.audio_codec_id = t->codec; From 764a3583b22ff59a985f1a4b91831cae0503c468 Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Wed, 29 Aug 2012 13:54:38 +0400 Subject: [PATCH 43/44] fixed mp4 sync problem because of different rtmp/mp4 time scales --- ngx_rtmp_mp4_module.c | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/ngx_rtmp_mp4_module.c b/ngx_rtmp_mp4_module.c index 833b72e..92b35bb 100644 --- a/ngx_rtmp_mp4_module.c +++ b/ngx_rtmp_mp4_module.c @@ -101,12 +101,13 @@ typedef struct { typedef struct { uint32_t timestamp; - uint32_t duration; + 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; @@ -551,7 +552,7 @@ ngx_rtmp_mp4_parse_audio(ngx_rtmp_session_t *s, u_char *pos, u_char *last, *p = 0; - if (ctx->nchannels) { + if (ctx->nchannels == 2) { *p |= 0x01; } @@ -571,7 +572,7 @@ ngx_rtmp_mp4_parse_audio(ngx_rtmp_session_t *s, u_char *pos, u_char *last, *p |= 0x08; break; - default: /* 44100, 4800 etc */ + default: /*44100 etc */ *p |= 0x0c; break; } @@ -1186,8 +1187,10 @@ ngx_rtmp_mp4_next_time(ngx_rtmp_session_t *s, ngx_rtmp_mp4_track_t *t) te = &t->times->entries[cr->time_pos]; - cr->duration = ngx_rtmp_r32(te->sample_delta); - cr->timestamp += cr->duration; + 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", @@ -1852,6 +1855,7 @@ ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) 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) { @@ -1861,8 +1865,6 @@ ngx_rtmp_mp4_send_meta(ngx_rtmp_session_t *s) v.duration = d; } - v.audio_sample_rate = ctx->sample_rate; - switch (t->type) { case NGX_RTMP_MSG_AUDIO: v.audio_codec_id = t->codec; @@ -1929,12 +1931,12 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) ngx_rtmp_mp4_track_t *t; ngx_rtmp_mp4_cursor_t *cr; uint32_t buflen, end_timestamp, sched, - timestamp, duration; + timestamp, last_timestamp; ssize_t ret; u_char fhdr[5]; size_t fhdr_size; ngx_int_t rc; - ngx_uint_t n, abs_frame, active; + ngx_uint_t n, active; cscf = ngx_rtmp_get_module_srv_conf(s, ngx_rtmp_core_module); @@ -1980,8 +1982,7 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) goto next; } - duration = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->duration); - abs_frame = (duration == 0); + last_timestamp = ngx_rtmp_mp4_to_rtmp_timestamp(t, cr->last_timestamp); ngx_memzero(&h, sizeof(h)); @@ -1991,7 +1992,8 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) lh = h; - h.timestamp = (abs_frame ? timestamp : duration); + h.timestamp = timestamp; + lh.timestamp = last_timestamp; ngx_memzero(&in, sizeof(in)); ngx_memzero(&in_buf, sizeof(in_buf)); @@ -2038,9 +2040,10 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) } ngx_log_debug5(NGX_LOG_DEBUG_RTMP, s->connection->log, 0, - "mp4: track#%ui read frame " - "offset=%O, size=%uz, timestamp=%uD, duration=%uD", - t->id, cr->offset, cr->size, timestamp, duration); + "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; @@ -2091,7 +2094,7 @@ ngx_rtmp_mp4_send(ngx_rtmp_session_t *s, ngx_file_t *f) out = ngx_rtmp_append_shared_bufs(cscf, NULL, &in); - ngx_rtmp_prepare_message(s, &h, abs_frame ? NULL : &lh, out); + 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); From 9aa411409dce1b5e66c8ad880803d81b87f8aecc Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Thu, 30 Aug 2012 15:49:07 +0400 Subject: [PATCH 44/44] fixed on_done argument length; thanks to Paul Howes (phowes). --- ngx_rtmp_notify_module.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ngx_rtmp_notify_module.c b/ngx_rtmp_notify_module.c index aef8241..086acdd 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); }