From 77167f3331da376cddf4508a3f4381dfff230438 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 19 Mar 2016 21:54:46 +0800 Subject: [PATCH] refine srs player for hls, extract a HlsNetStream. --- .../srs_player/.actionScriptProperties | 1 + .../players/srs_player/release/srs_player.swf | Bin 7035 -> 50551 bytes .../research/players/srs_player/src/Consts.as | 11 - trunk/research/players/srs_player/src/Dict.as | 120 -- .../players/srs_player/src/FlvPiece.as | 97 -- .../src/{Hls.as => HlsNetStream.as} | 1073 +++++++++++++---- .../players/srs_player/src/ILogger.as | 12 - .../players/srs_player/src/IPlayer.as | 33 - .../players/srs_player/src/M3u8Player.as | 307 ----- .../src/{CommonPlayer.as => Player.as} | 16 +- .../players/srs_player/src/TraceLogger.as | 93 -- .../players/srs_player/src/Utility.as | 6 +- .../players/srs_player/src/srs_player.as | 10 +- 13 files changed, 846 insertions(+), 933 deletions(-) mode change 100755 => 100644 trunk/research/players/srs_player/release/srs_player.swf delete mode 100644 trunk/research/players/srs_player/src/Consts.as delete mode 100755 trunk/research/players/srs_player/src/Dict.as delete mode 100755 trunk/research/players/srs_player/src/FlvPiece.as rename trunk/research/players/srs_player/src/{Hls.as => HlsNetStream.as} (87%) delete mode 100644 trunk/research/players/srs_player/src/ILogger.as delete mode 100644 trunk/research/players/srs_player/src/IPlayer.as delete mode 100644 trunk/research/players/srs_player/src/M3u8Player.as rename trunk/research/players/srs_player/src/{CommonPlayer.as => Player.as} (91%) delete mode 100644 trunk/research/players/srs_player/src/TraceLogger.as diff --git a/trunk/research/players/srs_player/.actionScriptProperties b/trunk/research/players/srs_player/.actionScriptProperties index 646c521e1..1908de474 100755 --- a/trunk/research/players/srs_player/.actionScriptProperties +++ b/trunk/research/players/srs_player/.actionScriptProperties @@ -41,3 +41,4 @@ + diff --git a/trunk/research/players/srs_player/release/srs_player.swf b/trunk/research/players/srs_player/release/srs_player.swf old mode 100755 new mode 100644 index 061d1594fc42981447dc0ac054860f73fa4c2eb6..359f5677dbb0891da27cdf3f11385da798732c60 GIT binary patch literal 50551 zcmV(zK<2+gS5poPX8{0s0i1mae3aGI_uOZhXJ)bzLI@x-VqDNbW?xX0012QhDG6%T zI8J6sMv|FvW)eU@+qmGqR>j@MrM7jiZPk~$ptjn&CaBn|RkUigRhjzMYF*0rKleVf zg$ejJzubG~?)$mto^$SfHW-w54TkDF42DK#STbpv!C?5|^=Ot_276WirY6EiLQUub;QxIWLjwZL$0P{uY~~#o?HX7<1Db`)f8>bPaH1z-ZSRk4 zu)3_Rh4ehOh(eLWg^C_g)XX5Io)=0Cv?NoB@L&jaiXtnXxHt)AksR#lkEQz}sUrsC z>*9&^aUQGSOL0P}NHCKqjgj-C@cv-DcQDu+IihV9Pn~!3ItDY5BUT1eR=dC1>af{8 zq8j9TcnkGzQE%%$7{DV8)wP4uR;`}f(c0O0?CLd345Kv|<&wW8wI9ld8x~8ucI`Ue zA|uEMqZn^FUc@?m#{QReVop3~NvG0*WPfmDBxQKK{)Iyg2KX9^JZlZ}4D#^sHyXS@ zm5QS;X`6pyZ!B{nBF|0s52jDdq|i+#rXuM`Fcs=MQRJGYaJ>Zl3>|h@!96dSt{#Yl zW5K{cBohn=GeNa0HV{cw@X?<)n2GhLD@240`5QZvsaPgbC4$4TG!;=3O2jj%M1LTf z4h#g->(q1x-L19_VZK$0m=XG&u5Mkjq-{x{YjvQrt95BxLwaL66B!63;(=f|9LOXB zJYg*yJnf1Yg5#0YltO)53P0t@P{z!29}1?@lT(p_#89Lpou!lN(b^yDjhnhwcLY|n zwRfpOa#r(@&QP#FvN92lm{y@ii(6N;nbSG1n)bo|{!UDzNSxBs7Tr*{HW#w2vK95$x|ztY4BC2*%D++Di))cBO5X+Bk@6#K0L7s>U!O?whz%3 z4aWL2%j3(@uW2QfNMtPiiC{RAS{{!kEXiOhh;fdj(#F+2r$j;-DHg8o3#M1Ek9VXJ z$w(@*aX)>b78Yr1r?bUjvw2#2DC<~UZA+yRsVM`4>5Mgox|XkOTT@e5MZ6GNeW{Ek9m!BkYz=jV2*{1M zb53D^fP`a1KzH*Cv5FU?GL(pgC$()rzsH09y#J%YP^4P#TixFr916zzgFXF`##7SP zcp_uv#WY(psg2ftB+?bz*yJ=lKGOijN=!vA17?$>Ajv^Lq%tX>1krdESA|9R`91O=2rGA)! zRz6EGG$?g}!pqJi+G86cVN1H%+A|o9Mp6qZ1zZt{_h$N7+JXR4zXi&=o@CnCY>g(< z3oO8PDIoUtj!sjvmA7NT>^-)dmpxfD&KgTw`JA@~GuA!NNKu=BSTu#yv>qt&!Fw!I zPfU?bxy8!X$%&p**!sfU>7VnS6IZjT+fZh`+zFUjX3SP-nY#^B0$bSb1Gkpn4jNLgF zmP4_y#-reOG8V3)-y&romXu;iwt!hHbM>dbn?uu#mW{Q5GxFh4tj9=ro|Rg+V9#?g zm&QUpB(}VUKp?UKdl@=`??n0jL|AVky$gJVUgHIuNimU7E4*A#seFp|3+i0ib;d&2-`C)O_v*KPqBRT}urC zN*)Mq2oz;nRmfmvaKrR6`NEsB-di65z3ViuAiM-=$%!^K)SpO4ss)m^(*Bf;glFwx zOeso52((gSfYW3(MB3;C8+n*JQ|S)AV${}DD!36OB%zY|RwX%@?lX4?d@GU#xe?(v8Mr|@aoRQU_9KF3dYlP zDAo@@{Mb{SE(tOMmtesZkyLr?2~r+MC|cvieOo}yJrU#`;WUqW3=#Ay?_8tk+nQh| z6Abm`cX@Mq0K|JAT7``#oC>ZF^z;MO2MFKyrl?L;SOt7eE{}&J8>STvexXci+|05e zxoS^u1%DxAUDLI)!|Jp-tZQ%>OkrEY-cdxYBBaUIST-XB6>l$vvcp*1@ie#!flwb# z;E|%!p1x@Lc3!;B3yF(kOes*Nx;O0(6Ld1iD|}0`@Wp#9P2dXta~Yq+hZMCUNBT~% zwbxQPVnY;ZYYHAbU!&OeCIcCO9n37F({O(zf1RA&D>KI(1oj?J_5|;w& zr9ht?NUw+KV;k7Ua@e>`{Y<^7V6rb3N}GEky|H+E48##lvo#odEfNotNBOQQI~nDH zbZ~0Stdu)j3wIS5&hUIhFVfoC+vE0mrW8%{BAkF` zcs)hOKU%x0-N!TXc^qz{QOOS!7uFm?yiWi0g5UV%rrP~Bhrgi^p?FCl(C)SsSER6R z5aw{B34RZn?67%kUe)2Sd0{)9cE5@~wfSKCovxa3XX>hwM0fmSFt6KH2B+Pf=NevBBIRHB>dsYB<$VW8ok zOs-^#$!w{ptg5c5oy3%fnZ^`NQZ=S&vPokWO{vh-N=>QOSdFICYD&E(PtlYHO_{36 zjhZq|Q>JU`3{BZbV>315zM8V1rp(rq{WY~oQx4GBftq%Zrp(clLp0@3O=;GYxtcOh zGubr7p)r@a1tlJ4J{cIep|Ld@>(Z398aq~F-`Cg)8tc|rKx09Tg)|n?l&HqmX-dB~Ij%_wO-X9XshX0~ zl(eP{YNjDg*`O(B%s@F8z+4D(5i?)HnE5h>-m?@AD*dk7WKD<$Xg< zzDUxPi(xK>xeQ@f0A3Gs1I$e@H^clGW&~!9rn~~M9p(s4`4hk!Fn@-58-|&gq^WF; zp8^pq4R2w;L6W0?%Z8sI_=@D^L`yG255&umQ&y=Y2vm`_`_~`e*Ch4p<@%vm< z+IK!#7s8r-3H|yJSyxI1BdS7wH%h}-NmomorE8>XrRyZB;VsfIm4SS2lZJ0ae95rj zc8UE&xSfM*2wy#Rj@;8_8l6X1CP zUJ&3#0bUYdRDdx7wh8dE0Iv%0ngD+k;B^7s6yPlZb_no}0PhO$7XjWA;C%r;kp51C z^RaM!BC!XUVy-Z-!wy<#7^tjdhB*SVLj{!P3MjV-sMrNm&t?k^Nl9kLbMznDCj2-W zKBjZ^ALjG)AC`oOTM-wq@>G#YRZ6(42NxQyWGKA`31%But-KJ^h)q%#8WaPoTe{G2 z5rs}Z0HJ{O=2nBmrg&QoGHYufWMmFsV0F6zrZ#9_Mj2W#4Gu!9L zR)dAjT-a);VEeYU8YcCmvo2~iRI~jSw;F2L>?N&+TDJcYt%gag>HDIF2OKZp zfhP!f(1`*be3F23P8RTxwXKFac4$|tVKQqzywy<8=H7)$Okwlx254X{_W(>~wtE4l zG5dW0)0yLbfEmpB3xIu?>zG!J5hMlF@E0szptsQFFURnp10nU;C)F-OT1>mmz0Kmd%zc zC1ky7)V7n=8CLCNlUX3Ux+q!EMbg?fbLMbr#gUr@N`yc~ldVvystl!2`0< z(QVNKg9l}M0S^w&$@T#r5u8@l~be{}xlItYTZ8%LYJ*M1mUb(%;N z=7~;Un*9OAYVI739^SqH+5C_@j~R^~!JWg5yN?@<9?9Kj3OBlBA$Ol8+$WAkTewFlW_NpMwf8+xxyVDjka<3`P@AkoiiFesJ$KaxqzWvib*wF z-lKM1%EX{w%676oP!T&>Ou8QSDe?_0dl?&zF5O(Gpt+Z`rP(XV*}lv$ynK81YPL-T zT!Vn?S@%)xM;QiR%ih4ckKPtt5nV~vs*xtstp|Ll-PV3I8w@Gg8`)@cW?OW1lhoe9 zMw-`;Y>R$R|Gjaf`Lyg!Y@|nNVm$Elk>>xGy_q4(8r|my*&nl!moYd<%RBxS-n8tk zsMTDw4jCiA>}{+^L86!2+qF5_pHO2}MKW6!psjcEwhkKZV%fX&8o!#o86 zmH$C5)=k^3!?RzrZVIVQhLCY5JBJzm2VK93f$G}H&Slcm02}4EV4ug7w*k&)>U%7^ z1ue#6poRP!CQ!kn3|VMnXN63)Qw#=kkEuu7chmOlV-$1S*tTeAurvE8bC|SA@9f%T zGMTjOlML&UwRd3#KgCAd*JAoV4S0+JRm%RJz1(~;3m%*O1Abh}g2!c_Lr&kfPBx>= z=h@5njldV-cf9a>iTsX7yfOHlApEwG-wE)01%4+AzgNlc#9(*!HTaz*{QgLOCj|r9 z*Wq`v@Oy*&P7Vf}rR-a56Xwp_yn!!M183(O7*ex4Fx)+A^L3aSJ!-Ubr1^R_G9nUC zYhTe@`z{9P21FHO{GL8MhhlgzQRH;Un0=r17@G`}A^p#Oz<^?IgnNXenGb2`ObnCA zhLQV3UofAr?5Auj+OxT?5=nNV>p}*BT>pXJVRYyJQaZDQbOEIMQcw3K%l;efG)5zv z>!`4=P_x?@pU_`pDedB6(eB=8Z*+2f9U=@%gkH(r7mU_c!{@BL&*We$Pi5tEPPtCLv@Wvc@YJ`)gD9rE|x}{e=bVCl=7&^<#8qDan(e5 zT%F6~6y&iPdE71XxR&y$1QBkTgnHaWdE6{@uZymW_AfISs%V#4hJZT}uoGLH;ZA8M zXkT`xl)XzD9qWr{@0Lcl^(C_RN`z2DIu^bU1w2SA>mlF*F?#n?LDiJseB`%<=eLC! zwje(c&|9SJZz#W0vyV`IsqCZ3;W5hL@q7-CQ4Te_{W#swH_&i^wJSWkx ztVTUwN1oX?kY;eGjwZ&N5?1hF`w&*}pQTZ2a4@@rmsB?if$#9V*JFs@MOfd`?0YES zc`;J&Q?_+f)emS3_>|Z6MP|rK*{syPp?Q=YwSgVOhBv;N{RgtavL`y?GieNA+b}=B zKZ5Y~(@^d|5ey7K!JqSBTI$E=gTFv9kO2jM$%9`-@Co_guMmv&PQhRE;Oz)LIUoEj zf`K9^_&+?DWrxsz!f{)C!)0SKy{ z4CkW@Kxh$)jV@Gy1-ty#)SO6#OqG{~z=npS>LZKPvcNLHGWHfpR~?(-PBs{L@| zoa{%G%`{oIRQiz3`zY$zL$+hpoFlRK

)a04;(!`M#X}K<@S+$_MhoMx+9<$I0No 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

*`yey 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%0D-w`tA z=ExfR6X0(g#ed51TOw=ie*%BgDE>6Ve)D)TD; z)#layell^NUPIQ1l8@^lwN#OU!Gt z*Ve0w7r9~k#s<@4>IfC zCPRwR7y}Q1lOeoAggUBsPHzz=DbF(9fbE-sIS(uTRU;^V8QrV+vAsK)758T~V9nm& zQ^^D1|K70DcZZeY$PFvKk-&K&9%Q{m{os8NyximPIrkff?Dv$i z$o|n7^FN?sR^>bg^iP#5JN!PTZ>f(W9rp8zzr*bC-;#}irTEoAzm@D41e(VPa@mVw z$b&^s*e~(nT1iJS^)uc#Q8R5Pb35s`ll`*b-a#h*ev{@;VwESXJgKlXdH+JbZ^0Q- z2|J0e3tr%TyIAB@0Qc*2y6!hzf;o#z*l#FTCjGl<>28uDN&8L3pEQ#!%=JCEFU5!l zynYASZ;3Hh?7I`Q*>CglZ2c~>-%eh1`Q#C=~x`d zpc+^4b;KR`I^)h$bR1Wlatc2d5CbPkX5c6(3_L?B1Fw@~mtyZ$G6uy+-$(Wy-j($M zvhUzsMMrsfr;_nrgPgYyW$IRWAY%@ovyFB3qjRln-=z#dgPOgew##hf$d0b#Bc~Yg ztLr;T#@C8-u}{hP575#BB;k@6P;hIAK-kZ^h=cjX`aza953sR+BnV2gLGS@q9i!18 zD8z<;$XGqTdsqPAzrWdk1`o5lOk&^c>A}&F;X?ZqV}=fm^V=U`6SS)&ib+>Xa#TVc zszjx0NKzL|TS;;(mbQ`PTr3?TNsCES*d4}``rolXtX!4;p?LpF_9sx9H20AAW3oTx z{X1x_+&}BM-LGa$0t>Q8(FE9bjXDT~d6;xm&)Ych;}PPuf_N|K_maH>#H4v2R*}yH zZ(uYwW`Z|z+L2+}O@elW%iW?5E|w(xRO)iy$U{lZnz|Q1zoGQG8{j^Y_o?=7XxX?M z$!#K@xaAphQ_N8EtdDhM_xWAy z^ZW4Ufx&}TNV7js@kUW6us8dMO!^p0f`4MYpoaWcST0E{wP?0#+iFl9JTL^Y9Q!Yv zBZo2nC-15(S$@pB4i>a~Xn&b}Q2!p;f8$6eL*5~f50d>4H4}6Mogz(s!qz|CfjaxE zSbo2Q1@8%x`teA_51IWAdn;5vgviH}_+getw+)Y_!6D!GNiDap1>>9oveFymG zLPGHG$YP*jMB;`lPRQ(Zz=Rp>tjA%(P22?hczTn=dyj12?C?KIOCKd^zzn!*1> Ze!ya83N#fIpYgwd=i~R~{{<1kckON9+D-rf diff --git a/trunk/research/players/srs_player/src/Consts.as b/trunk/research/players/srs_player/src/Consts.as deleted file mode 100644 index 6b13690d8..000000000 --- a/trunk/research/players/srs_player/src/Consts.as +++ /dev/null @@ -1,11 +0,0 @@ -package -{ - public class Consts - { - // refresh every ts_fragment_seconds*M3u8RefreshRatio - public static var M3u8RefreshRatio:Number = 0.5; - - // parse ts every this ms. - public static var TsParseAsyncInterval:Number = 80; - } -} \ No newline at end of file diff --git a/trunk/research/players/srs_player/src/Dict.as b/trunk/research/players/srs_player/src/Dict.as deleted file mode 100755 index 34196d2cc..000000000 --- a/trunk/research/players/srs_player/src/Dict.as +++ /dev/null @@ -1,120 +0,0 @@ -package -{ - import flash.utils.Dictionary; - - public class Dict - { - private var _dict:Dictionary; - private var _size:uint; - - public function Dict() - { - clear(); - } - - /** - * get the underlayer dict. - * @remark for core-ng. - */ - public function get dict():Dictionary - { - return _dict; - } - - public function has(key:Object):Boolean - { - return (key in _dict); - } - - public function get(key:Object):Object - { - return ((key in _dict) ? _dict[key] : null); - } - - public function set(key:Object, object:Object):void - { - if (!(key in _dict)) - { - _size++; - } - _dict[key] = object; - } - - public function remove(key:Object):Object - { - var object:Object; - if (key in _dict) - { - object = _dict[key]; - delete _dict[key]; - _size--; - } - return (object); - } - - public function keys():Array - { - var array:Array = new Array(_size); - var index:int; - for (var key:Object in _dict) - { - var _local6:int = index++; - array[_local6] = key; - } - return (array); - } - - public function values():Array - { - var array:Array = new Array(_size); - var index:int; - for each (var value:Object in _dict) - { - var _local6:int = index++; - array[_local6] = value; - }; - return (array); - } - - public function clear():void - { - _dict = new Dictionary(); - _size = 0; - } - - public function toArray():Array - { - var array:Array = new Array(_size * 2); - var index:int; - for (var key:Object in _dict) - { - var _local6:int = index++; - array[_local6] = key; - var _local7:int = index++; - array[_local7] = _dict[key]; - }; - return (array); - } - - public function toObject():Object - { - return (toArray()); - } - - public function fromObject(object:Object):void - { - clear(); - var index:uint; - while (index < (object as Array).length) { - set((object as Array)[index], (object as Array)[(index + 1)]); - index += 2; - }; - } - - public function get size():uint - { - return (_size); - } - - } -} \ No newline at end of file diff --git a/trunk/research/players/srs_player/src/FlvPiece.as b/trunk/research/players/srs_player/src/FlvPiece.as deleted file mode 100755 index a73bb2519..000000000 --- a/trunk/research/players/srs_player/src/FlvPiece.as +++ /dev/null @@ -1,97 +0,0 @@ -package -{ - import flash.utils.ByteArray; - - /** - * a piece of flv, fetch from cdn or p2p. - */ - public class FlvPiece - { - private var _pieceId:Number; - protected var _flv:ByteArray; - /** - * the private object for the channel, - * for example, the cdn channel will set to CdnEdge object. - */ - private var _privateObject:Object; - /** - * when encoder error, this piece cannot be generated, - * and it should be skip. default to false. - */ - private var _skip:Boolean; - - public function FlvPiece(pieceId:Number) - { - _pieceId = pieceId; - _flv = null; - _skip = false; - } - - /** - * when piece is fetch ok. - */ - public function onPieceDone(flv:ByteArray):void - { - // save body. - _flv = flv; - } - - /** - * when piece is fetch error. - */ - public function onPieceError():void - { - } - - /** - * when piece is empty. - */ - public function onPieceEmpty():void - { - } - - /** - * destroy the object, set reference to null. - */ - public function destroy():void - { - _privateObject = null; - _flv = null; - } - - public function get privateObject():Object - { - return _privateObject; - } - - public function set privateObject(v:Object):void - { - _privateObject = v; - } - - public function get skip():Boolean - { - return _skip; - } - - public function set skip(v:Boolean):void - { - _skip = v; - } - - public function get pieceId():Number - { - return _pieceId; - } - - public function get flv():ByteArray - { - return _flv; - } - - public function get completed():Boolean - { - return _flv != null; - } - } -} \ No newline at end of file diff --git a/trunk/research/players/srs_player/src/Hls.as b/trunk/research/players/srs_player/src/HlsNetStream.as similarity index 87% rename from trunk/research/players/srs_player/src/Hls.as rename to trunk/research/players/srs_player/src/HlsNetStream.as index b7f225bdf..bae7cef55 100755 --- a/trunk/research/players/srs_player/src/Hls.as +++ b/trunk/research/players/srs_player/src/HlsNetStream.as @@ -1,286 +1,865 @@ package { import flash.events.Event; + import flash.events.ProgressEvent; + import flash.external.ExternalInterface; + import flash.net.NetConnection; + import flash.net.NetStream; + import flash.net.NetStreamAppendBytesAction; import flash.net.URLLoader; + import flash.net.URLLoaderDataFormat; import flash.net.URLRequest; + import flash.net.URLRequestHeader; import flash.net.URLRequestMethod; + import flash.net.URLStream; + import flash.net.URLVariables; import flash.utils.ByteArray; + import flash.utils.setTimeout; - /** - * the hls main class. - */ - public class Hls + // the NetStream to play hls or hls+. + public class HlsNetStream extends NetStream { - private var m3u8:M3u8; - - private var avc:SrsRawH264Stream; - private var h264_sps:ByteArray; - private var h264_pps:ByteArray; - - private var aac:SrsRawAacStream; - private var aac_specific_config:ByteArray; - private var width:int; - private var height:int; - - private var video_sh_tag:ByteArray; - private var audio_sh_tag:ByteArray; + private var hls:HlsCodec = null; // parse m3u8 and ts - private var owner:M3u8Player; - private var _log:ILogger = new TraceLogger("HLS"); - - public static const SRS_TS_PACKET_SIZE:int = 188; - - public function Hls(o:M3u8Player) - { - owner = o; - m3u8 = new M3u8(this); - - reset(); - } - - /** - * parse the m3u8. - * @param url, the m3u8 url, for m3u8 to generate the ts url. - * @param v, the m3u8 string. - */ - public function parse(url:String, v:String):void - { - // TODO: FIXME: reset the hls when parse. - m3u8.parse(url, v); - } - - /** - * get the total count of ts in m3u8. - */ - public function get tsCount():Number + // for hls codec. + public var m3u8_refresh_ratio:Number; + public var ts_parse_async_interval:Number; + + // play param url. + private var user_url:String = null; + + /** + * create stream to play hls. + * @param m3u8_refresh_ratio, for example, 0.5, fetch m3u8 every 0.5*ts_duration. + * @param ts_parse_async_interval, for example, 80ms to parse ts async. + */ + public function HlsNetStream(m3u8_refresh_ratio:Number, ts_parse_async_interval:Number, conn:NetConnection) { - return m3u8.tsCount; + super(conn); + + this.m3u8_refresh_ratio = m3u8_refresh_ratio; + this.ts_parse_async_interval = ts_parse_async_interval; + hls = new HlsCodec(this); } - - /** - * get the total duration in seconds of m3u8. - */ - public function get duration():Number - { - return m3u8.duration; + + /** + * to play the hls stream. + * for example, HlsNetStream.play("http://ossrs.net:8080/live/livestream.m3u8"). + * user can set the metadata callback by: + * var ns:NetStream = new HlsNetStream(...); + * ns.client = {}; + * ns.client.onMetaData = system_on_metadata; + */ + public override function play(... params):void + { + super.play(null); + user_url = params[0] as String; + refresh_m3u8(); + } + + ///////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////Private Section////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////// + + private var parsed_ts_seq_no:Number = -1; + private function refresh_m3u8():void { + download(user_url, function(stream:ByteArray):void { + var m3u8:String = stream.toString(); + hls.parse(user_url, m3u8); + + // redirect by variant m3u8. + if (hls.variant) { + var smu:String = hls.getTsUrl(0); + log("variant hls=" + user_url + ", redirect2=" + smu); + user_url = smu; + setTimeout(refresh_m3u8, 0); + return; + } + + // fetch from the last one. + if (parsed_ts_seq_no == -1) { + parsed_ts_seq_no = hls.seq_no + hls.tsCount - 1; + } + + // not changed. + if (parsed_ts_seq_no >= hls.seq_no + hls.tsCount) { + refresh_ts(); + return; + } + + // parse each ts. + var nb_ts:Number = hls.seq_no + hls.tsCount - parsed_ts_seq_no; + log("m3u8 changed, got " + nb_ts + " new ts, count=" + hls.tsCount + ", seqno=" + hls.seq_no + ", parsed=" + parsed_ts_seq_no); + + refresh_ts(); + }) + } + private function refresh_ts():void { + // all ts parsed. + if (parsed_ts_seq_no >= hls.seq_no + hls.tsCount) { + var to:Number = 1000; + if (hls.tsCount > 0) { + to = hls.duration * 1000 / hls.tsCount * m3u8_refresh_ratio; + } + setTimeout(refresh_m3u8, to); + log("m3u8 not changed, retry after " + to.toFixed(2) + "ms"); + return; + } + + // parse current ts. + var uri:String = hls.getTsUrl(parsed_ts_seq_no - hls.seq_no); + + // parse metadata from uri. + if (uri.indexOf("?") >= 0) { + var uv:URLVariables = new URLVariables(uri.substr(uri.indexOf("?") + 1)); + var obj:Object = {}; + for (var k:String in uv) { + var v:String = uv[k]; + if (k == "shp_sip1") { + obj.srs_server_ip = v; + } else if (k == "shp_cid") { + obj.srs_id = v; + } else if (k == "shp_pid") { + obj.srs_pid = v; + } + //log("uv[" + k + "]=" + v); + } + + if (client && client.hasOwnProperty("onMetaData")) { + client.onMetaData(obj); + } + } + + download(uri, function(stream:ByteArray):void{ + log("got ts seqno=" + parsed_ts_seq_no + ", " + stream.length + " bytes"); + + var flv:FlvPiece = new FlvPiece(parsed_ts_seq_no); + var body:ByteArray = new ByteArray(); + stream.position = 0; + hls.parseBodyAsync(flv, stream, body, function():void{ + body.position = 0; + //log("ts parsed, seqno=" + parsed_ts_seq_no + ", flv=" + body.length + "B"); + onFlvBody(uri, body); + + parsed_ts_seq_no++; + setTimeout(refresh_ts, 0); + }); + }); + } + private function download(uri:String, completed:Function):void { + // http get. + var url:URLStream = new URLStream(); + var stream:ByteArray = new ByteArray(); + + url.addEventListener(ProgressEvent.PROGRESS, function(evt:ProgressEvent):void { + if (url.bytesAvailable <= 0) { + return; + } + + //log(uri + " total=" + evt.bytesTotal + ", loaded=" + evt.bytesLoaded + ", available=" + url.bytesAvailable); + var bytes:ByteArray = new ByteArray(); + url.readBytes(bytes, 0, url.bytesAvailable); + stream.writeBytes(bytes); + }); + + url.addEventListener(Event.COMPLETE, function(evt:Event):void { + log(uri + " completed, total=" + stream.length + "bytes"); + if (url.bytesAvailable <= 0) { + completed(stream); + return; + } + + //log(uri + " completed" + ", available=" + url.bytesAvailable); + var bytes:ByteArray = new ByteArray(); + url.readBytes(bytes, 0, url.bytesAvailable); + stream.writeBytes(bytes); + + completed(stream); + }); + + // we set to the query. + uri += ((uri.indexOf("?") == -1)? "?":"&") + "shp_xpsid=" + XPlaybackSessionId; + var r:URLRequest = new URLRequest(uri); + // seems flash not allow set this header. + // @remark disable it for it will cause security exception. + //r.requestHeaders.push(new URLRequestHeader("X-Playback-Session-Id", XPlaybackSessionId)); + + log("start download " + uri); + url.load(r); + } + + // the uuid similar to Safari, to identify this play session. + // @see https://github.com/winlinvip/srs-plus/blob/bms/trunk/src/app/srs_app_http_stream.cpp#L45 + public var XPlaybackSessionId:String = createRandomIdentifier(32); + + private function createRandomIdentifier(length:uint, radix:uint = 61):String { + var characters:Array = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', + 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', + 'z'); + var id:Array = new Array(); + radix = (radix > 61) ? 61 : radix; + while (length--) { + id.push(characters[randomIntegerWithinRange(0, radix)]); + } + return id.join(''); } - /** - * get the sequence number, the id of first ts. - */ - public function get seq_no():Number - { - return m3u8.seq_no; + private function randomIntegerWithinRange(min:int, max:int):int { + return Math.floor(Math.random() * (1 + max - min) + min); } - /** - * whether the m3u8 contains variant m3u8. - */ - public function get variant():Boolean - { - return m3u8.variant; + // callback for hls. + public var flvHeader:ByteArray = null; + public function onSequenceHeader():void { + var s:NetStream = super; + s.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN); + s.appendBytes(flvHeader); + log("FLV: sps/pps " + flvHeader.length + " bytes"); + + writeFlv(flvHeader); } - - /** - * dumps the metadata, for example, set the width and height, - * which is decoded from sps. - */ - public function dumpMetaData(metadata:Object):void - { - if (width > 0) { - metadata.width = width; - } - if (height > 0) { - metadata.height = height; + + private function onFlvBody(uri:String, flv:ByteArray):void { + if (!flvHeader) { + return; } + + var s:NetStream = super; + s.appendBytes(flv); + log("FLV: ts " + uri + " parsed to flv " + flv.length + " bytes"); + + writeFlv(flv); } - - /** - * get the ts url by piece id, which is actually the piece index. - */ - public function getTsUrl(pieceId:Number):String - { - return m3u8.getTsUrl(pieceId); + + private function writeFlv(data:ByteArray):void { + return; + + var r:URLRequest = new URLRequest("http://192.168.1.117:8088/api/v1/flv"); + r.method = URLRequestMethod.POST; + r.data = data; + + var pf:URLLoader = new URLLoader(); + pf.dataFormat = URLLoaderDataFormat.BINARY; + pf.load(r); } - - /** - * reset the HLS when parse m3u8. - */ - public function reset():void - { - avc = new SrsRawH264Stream(); - h264_sps = new ByteArray(); - h264_pps = new ByteArray(); - - aac = new SrsRawAacStream(); - aac_specific_config = new ByteArray(); - - width = 0; - height = 0; - - video_sh_tag = new ByteArray(); - audio_sh_tag = new ByteArray(); + + private function log(msg:String):void { + msg = "[" + new Date() +"][srs-player] " + msg; + + trace(msg); + + if (!flash.external.ExternalInterface.available) { + return; + } + + ExternalInterface.call("console.log", msg); + } + } +} + +import flash.events.Event; +import flash.net.URLLoader; +import flash.net.URLRequest; +import flash.net.URLRequestMethod; +import flash.utils.ByteArray; + +/** + * the hls main class. + */ +class HlsCodec +{ + private var m3u8:M3u8; + + private var avc:SrsRawH264Stream; + private var h264_sps:ByteArray; + private var h264_pps:ByteArray; + + private var aac:SrsRawAacStream; + private var aac_specific_config:ByteArray; + private var width:int; + private var height:int; + + private var video_sh_tag:ByteArray; + private var audio_sh_tag:ByteArray; + + private var owner:HlsNetStream; + private var _log:ILogger = new TraceLogger("HLS"); + + public static const SRS_TS_PACKET_SIZE:int = 188; + + public function HlsCodec(o:HlsNetStream) + { + owner = o; + m3u8 = new M3u8(this); + + reset(); + } + + /** + * parse the m3u8. + * @param url, the m3u8 url, for m3u8 to generate the ts url. + * @param v, the m3u8 string. + */ + public function parse(url:String, v:String):void + { + // TODO: FIXME: reset the hls when parse. + m3u8.parse(url, v); + } + + /** + * get the total count of ts in m3u8. + */ + public function get tsCount():Number + { + return m3u8.tsCount; + } + + /** + * get the total duration in seconds of m3u8. + */ + public function get duration():Number + { + return m3u8.duration; + } + + /** + * get the sequence number, the id of first ts. + */ + public function get seq_no():Number + { + return m3u8.seq_no; + } + + /** + * whether the m3u8 contains variant m3u8. + */ + public function get variant():Boolean + { + return m3u8.variant; + } + + /** + * dumps the metadata, for example, set the width and height, + * which is decoded from sps. + */ + public function dumpMetaData(metadata:Object):void + { + if (width > 0) { + metadata.width = width; } - - /** - * parse the piece in hls format, - * set the piece.skip if error. - * @param onParsed, a function(piece:FlvPiece, body:ByteArray):void callback. - */ - public function parseBodyAsync(piece:FlvPiece, data:ByteArray, body:ByteArray, onParsed:Function):void - { + if (height > 0) { + metadata.height = height; + } + } + + /** + * get the ts url by piece id, which is actually the piece index. + */ + public function getTsUrl(pieceId:Number):String + { + return m3u8.getTsUrl(pieceId); + } + + /** + * reset the HLS when parse m3u8. + */ + public function reset():void + { + avc = new SrsRawH264Stream(); + h264_sps = new ByteArray(); + h264_pps = new ByteArray(); + + aac = new SrsRawAacStream(); + aac_specific_config = new ByteArray(); + + width = 0; + height = 0; + + video_sh_tag = new ByteArray(); + audio_sh_tag = new ByteArray(); + } + + /** + * parse the piece in hls format, + * set the piece.skip if error. + * @param onParsed, a function(piece:FlvPiece, body:ByteArray):void callback. + */ + public function parseBodyAsync(piece:FlvPiece, data:ByteArray, body:ByteArray, onParsed:Function):void + { + var handler:SrsTsHanlder = new SrsTsHanlder( + avc, aac, + h264_sps, h264_pps, + aac_specific_config, + video_sh_tag, audio_sh_tag, + this, body, + _on_size_changed, _on_sequence_changed + ); + + // the context used to parse the whole ts file. + var context:SrsTsContext = new SrsTsContext(this); + + // we assumpt to parse the piece in 10 times. + // the total parse time is 10*AlgP2P.HlsAsyncParseTimeout + var ts_packets:uint = data.length / SRS_TS_PACKET_SIZE; + var each_parse:uint = ts_packets / 10; + var nb_parsed:uint = 0; + var aysncParse:Function = function():void { + try { + // do the parse. + doParseBody(piece, data, body, handler, context, each_parse); + + // check whether parsed. + nb_parsed += each_parse; + + if (nb_parsed < ts_packets) { + flash.utils.setTimeout(aysncParse, owner.ts_parse_async_interval); + return; + } + + // flush the messages in queue. + handler.flush_message_queue(body); + + __report(body); + _log.info("hls async parsed to flv, piece={0}, hls={1}B, flv={2}B", piece.pieceId, data.length, body.length); + } catch (e:Error) { + piece.skip = true; + _log.error("hls async parse piece={0}, exception={1}, stack={2}", + piece.pieceId, e.message, e.getStackTrace()); + } + + onParsed(piece, body); + }; + + aysncParse(); + } + + /** + * parse the piece in hls format, + * set the piece.skip if error. + */ + public function parseBody(piece:FlvPiece, data:ByteArray, body:ByteArray):void + { + try { var handler:SrsTsHanlder = new SrsTsHanlder( avc, aac, h264_sps, h264_pps, aac_specific_config, - video_sh_tag, audio_sh_tag, + video_sh_tag, audio_sh_tag, this, body, _on_size_changed, _on_sequence_changed ); - + // the context used to parse the whole ts file. var context:SrsTsContext = new SrsTsContext(this); - - // we assumpt to parse the piece in 10 times. - // the total parse time is 10*AlgP2P.HlsAsyncParseTimeout - var ts_packets:uint = data.length / SRS_TS_PACKET_SIZE; - var each_parse:uint = ts_packets / 10; - var nb_parsed:uint = 0; - var aysncParse:Function = function():void { - try { - // do the parse. - doParseBody(piece, data, body, handler, context, each_parse); - - // check whether parsed. - nb_parsed += each_parse; - - if (nb_parsed < ts_packets) { - flash.utils.setTimeout(aysncParse, Consts.TsParseAsyncInterval); - return; - } - - // flush the messages in queue. - handler.flush_message_queue(body); - - __report(body); - _log.info("hls async parsed to flv, piece={0}, hls={1}B, flv={2}B", piece.pieceId, data.length, body.length); - } catch (e:Error) { - piece.skip = true; - _log.error("hls async parse piece={0}, exception={1}, stack={2}", - piece.pieceId, e.message, e.getStackTrace()); - } - - onParsed(piece, body); - }; - - aysncParse(); + + // do the parse. + doParseBody(piece, data, body, handler, context, -1); + + // flush the messages in queue. + handler.flush_message_queue(body); + + __report(body); + _log.info("hls sync parsed to flv, piece={0}, hls={1}B, flv={2}B", piece.pieceId, data.length, body.length); + } catch (e:Error) { + piece.skip = true; + _log.error("hls sync parse piece={0}, exception={1}, stack={2}", + piece.pieceId, e.message, e.getStackTrace()); } - - /** - * parse the piece in hls format, - * set the piece.skip if error. - */ - public function parseBody(piece:FlvPiece, data:ByteArray, body:ByteArray):void - { - try { - var handler:SrsTsHanlder = new SrsTsHanlder( - avc, aac, - h264_sps, h264_pps, - aac_specific_config, - video_sh_tag, audio_sh_tag, - this, body, - _on_size_changed, _on_sequence_changed - ); - - // the context used to parse the whole ts file. - var context:SrsTsContext = new SrsTsContext(this); - - // do the parse. - doParseBody(piece, data, body, handler, context, -1); - - // flush the messages in queue. - handler.flush_message_queue(body); - - __report(body); - _log.info("hls sync parsed to flv, piece={0}, hls={1}B, flv={2}B", piece.pieceId, data.length, body.length); - } catch (e:Error) { - piece.skip = true; - _log.error("hls sync parse piece={0}, exception={1}, stack={2}", - piece.pieceId, e.message, e.getStackTrace()); + } + + private function _on_size_changed(w:int, h:int):void + { + width = w; + height = h; + } + + private function _on_sequence_changed( + pavc:SrsRawH264Stream, paac:SrsRawAacStream, + ph264_sps:ByteArray, ph264_pps:ByteArray, + paac_specific_config:ByteArray, + pvideo_sh_tag:ByteArray, paudio_sh_tag:ByteArray, + sh:ByteArray):void + { + // when sequence header not changed, ignore. + if (SrsUtils.array_equals(h264_sps, ph264_sps)) { + if (SrsUtils.array_equals(h264_pps, ph264_pps)) { + if (SrsUtils.array_equals(aac_specific_config, paac_specific_config)) { + return; + } } } - - private function _on_size_changed(w:int, h:int):void + + avc = pavc; + h264_sps = ph264_sps; + h264_pps = ph264_pps; + + aac = paac; + aac_specific_config = paac_specific_config; + + video_sh_tag = pvideo_sh_tag; + audio_sh_tag = paudio_sh_tag; + + _log.info("hls: got sequence header, ash={0}B, bsh={1}B", audio_sh_tag.length, video_sh_tag.length); + owner.flvHeader = sh; + owner.onSequenceHeader(); + + __report(sh); + } + + /** + * do the parse. + * @maxTsPackets the max ts packets to parse, stop when exceed this ts packet. + * -1 to parse all packets. + */ + private function doParseBody( + piece:FlvPiece, data:ByteArray, body:ByteArray, + handler:SrsTsHanlder, context:SrsTsContext, maxTsPackets:int):void + { + for (var i:int = 0; (maxTsPackets == -1 || i < maxTsPackets) && data.bytesAvailable > 0; i++) { + var tsBytes:ByteArray = new ByteArray(); + data.readBytes(tsBytes, 0, HlsCodec.SRS_TS_PACKET_SIZE); + context.decode(tsBytes, handler); + } + } + + private function __report(flv:ByteArray):void + { + // report only for debug. + return; + + var url:URLRequest = new URLRequest("http://192.168.10.108:1980/api/v3/file"); + url.data = flv; + url.method = URLRequestMethod.POST; + + var loader:URLLoader = new URLLoader(); + loader.addEventListener(Event.COMPLETE, function(e:Event):void { + loader.close(); + }); + loader.load(url); + } +} + +import flash.utils.Dictionary; + +class Dict +{ + private var _dict:Dictionary; + private var _size:uint; + + public function Dict() + { + clear(); + } + + /** + * get the underlayer dict. + * @remark for core-ng. + */ + public function get dict():Dictionary + { + return _dict; + } + + public function has(key:Object):Boolean + { + return (key in _dict); + } + + public function get(key:Object):Object + { + return ((key in _dict) ? _dict[key] : null); + } + + public function set(key:Object, object:Object):void + { + if (!(key in _dict)) { - width = w; - height = h; + _size++; } - - private function _on_sequence_changed( - pavc:SrsRawH264Stream, paac:SrsRawAacStream, - ph264_sps:ByteArray, ph264_pps:ByteArray, - paac_specific_config:ByteArray, - pvideo_sh_tag:ByteArray, paudio_sh_tag:ByteArray, - sh:ByteArray):void + _dict[key] = object; + } + + public function remove(key:Object):Object + { + var object:Object; + if (key in _dict) { - // when sequence header not changed, ignore. - if (SrsUtils.array_equals(h264_sps, ph264_sps)) { - if (SrsUtils.array_equals(h264_pps, ph264_pps)) { - if (SrsUtils.array_equals(aac_specific_config, paac_specific_config)) { - return; - } - } - } - - avc = pavc; - h264_sps = ph264_sps; - h264_pps = ph264_pps; - - aac = paac; - aac_specific_config = paac_specific_config; - - video_sh_tag = pvideo_sh_tag; - audio_sh_tag = paudio_sh_tag; - - _log.info("hls: got sequence header, ash={0}B, bsh={1}B", audio_sh_tag.length, video_sh_tag.length); - owner.flvHeader = sh; - owner.onSequenceHeader(); - - __report(sh); + object = _dict[key]; + delete _dict[key]; + _size--; } - - /** - * do the parse. - * @maxTsPackets the max ts packets to parse, stop when exceed this ts packet. - * -1 to parse all packets. - */ - private function doParseBody( - piece:FlvPiece, data:ByteArray, body:ByteArray, - handler:SrsTsHanlder, context:SrsTsContext, maxTsPackets:int):void + return (object); + } + + public function keys():Array + { + var array:Array = new Array(_size); + var index:int; + for (var key:Object in _dict) { - for (var i:int = 0; (maxTsPackets == -1 || i < maxTsPackets) && data.bytesAvailable > 0; i++) { - var tsBytes:ByteArray = new ByteArray(); - data.readBytes(tsBytes, 0, Hls.SRS_TS_PACKET_SIZE); - context.decode(tsBytes, handler); - } + var _local6:int = index++; + array[_local6] = key; } - - private function __report(flv:ByteArray):void + return (array); + } + + public function values():Array + { + var array:Array = new Array(_size); + var index:int; + for each (var value:Object in _dict) + { + var _local6:int = index++; + array[_local6] = value; + }; + return (array); + } + + public function clear():void + { + _dict = new Dictionary(); + _size = 0; + } + + public function toArray():Array + { + var array:Array = new Array(_size * 2); + var index:int; + for (var key:Object in _dict) { - // report only for debug. + var _local6:int = index++; + array[_local6] = key; + var _local7:int = index++; + array[_local7] = _dict[key]; + }; + return (array); + } + + public function toObject():Object + { + return (toArray()); + } + + public function fromObject(object:Object):void + { + clear(); + var index:uint; + while (index < (object as Array).length) { + set((object as Array)[index], (object as Array)[(index + 1)]); + index += 2; + }; + } + + public function get size():uint + { + return (_size); + } + +} + +import flash.utils.ByteArray; + +/** + * a piece of flv, fetch from cdn or p2p. + */ +class FlvPiece +{ + private var _pieceId:Number; + protected var _flv:ByteArray; + /** + * the private object for the channel, + * for example, the cdn channel will set to CdnEdge object. + */ + private var _privateObject:Object; + /** + * when encoder error, this piece cannot be generated, + * and it should be skip. default to false. + */ + private var _skip:Boolean; + + public function FlvPiece(pieceId:Number) + { + _pieceId = pieceId; + _flv = null; + _skip = false; + } + + /** + * when piece is fetch ok. + */ + public function onPieceDone(flv:ByteArray):void + { + // save body. + _flv = flv; + } + + /** + * when piece is fetch error. + */ + public function onPieceError():void + { + } + + /** + * when piece is empty. + */ + public function onPieceEmpty():void + { + } + + /** + * destroy the object, set reference to null. + */ + public function destroy():void + { + _privateObject = null; + _flv = null; + } + + public function get privateObject():Object + { + return _privateObject; + } + + public function set privateObject(v:Object):void + { + _privateObject = v; + } + + public function get skip():Boolean + { + return _skip; + } + + public function set skip(v:Boolean):void + { + _skip = v; + } + + public function get pieceId():Number + { + return _pieceId; + } + + public function get flv():ByteArray + { + return _flv; + } + + public function get completed():Boolean + { + return _flv != null; + } +} + +interface ILogger +{ + function debug0(message:String, ... rest):void; + function debug(message:String, ... rest):void; + function info(message:String, ... rest):void; + function warn(message:String, ... rest):void; + function error(message:String, ... rest):void; + function fatal(message:String, ... rest):void; +} + +import flash.globalization.DateTimeFormatter; +import flash.external.ExternalInterface; + +class TraceLogger implements ILogger +{ + private var _category:String; + + public function get category():String + { + return _category; + } + public function TraceLogger(category:String) + { + _category = category; + } + public function debug0(message:String, ...rest):void + { + } + + public function debug(message:String, ...rest):void + { + } + + public function info(message:String, ...rest):void + { + logMessage(LEVEL_INFO, message, rest); + } + + public function warn(message:String, ...rest):void + { + logMessage(LEVEL_WARN, message, rest); + } + + public function error(message:String, ...rest):void + { + logMessage(LEVEL_ERROR, message, rest); + } + + public function fatal(message:String, ...rest):void + { + logMessage(LEVEL_FATAL, message, rest); + } + protected function logMessage(level:String, message:String, params:Array):void + { + var msg:String = ""; + + // add datetime + var date:Date = new Date(); + var dtf:DateTimeFormatter = new DateTimeFormatter("UTC"); + dtf.setDateTimePattern("yyyy-MM-dd HH:mm:ss"); + + // TODO: FIXME: the SSS format not run, use date.milliseconds instead. + msg += '[' + dtf.format(date) + "." + date.milliseconds + ']'; + msg += " [" + level + "] "; + + // add category and params + msg += "[" + category + "] " + applyParams(message, params); + + // trace the message + trace(msg); + + if (!flash.external.ExternalInterface.available) { return; - - var url:URLRequest = new URLRequest("http://192.168.10.108:1980/api/v3/file"); - url.data = flv; - url.method = URLRequestMethod.POST; - - var loader:URLLoader = new URLLoader(); - loader.addEventListener(Event.COMPLETE, function(e:Event):void { - loader.close(); - }); - loader.load(url); } + + ExternalInterface.call("console.log", msg); + } + private function leadingZeros(x:Number):String + { + if (x < 10) { + return "00" + x.toString(); + } + + if (x < 100) { + return "0" + x.toString(); + } + + return x.toString(); } + private function applyParams(message:String, params:Array):String + { + var result:String = message; + + var numParams:int = params.length; + + for (var i:int = 0; i < numParams; i++) { + result = result.replace(new RegExp("\\{" + i + "\\}", "g"), params[i]); + } + return result; + } + + private static const LEVEL_DEBUG:String = "DEBUG"; + private static const LEVEL_WARN:String = "WARN"; + private static const LEVEL_INFO:String = "INFO"; + private static const LEVEL_ERROR:String = "ERROR"; + private static const LEVEL_FATAL:String = "FATAL"; } import flash.utils.ByteArray; @@ -304,7 +883,7 @@ class SrsTsHanlder implements ISrsTsHandler private var queue:Array; // hls data. - private var _hls:Hls; + private var _hls:HlsCodec; private var _body:ByteArray; private var _on_size_changed:Function; private var _on_sequence_changed:Function; @@ -316,7 +895,7 @@ class SrsTsHanlder implements ISrsTsHandler ph264_sps:ByteArray, ph264_pps:ByteArray, paac_specific_config:ByteArray, pvideo_sh_tag:ByteArray, paudio_sh_tag:ByteArray, - hls:Hls, body:ByteArray, oszc:Function, oshc:Function) + hls:HlsCodec, body:ByteArray, oszc:Function, oshc:Function) { _hls = hls; _body = body; @@ -2383,7 +2962,7 @@ interface ISrsTsHandler */ class SrsTsContext { - private var _hls:Hls; + private var _hls:HlsCodec; // codec // key, a Number indicates the pid, @@ -2393,7 +2972,7 @@ class SrsTsContext // whether hls pure audio stream. private var _pure_audio:Boolean; - public function SrsTsContext(hls:Hls) + public function SrsTsContext(hls:HlsCodec) { _hls = hls; _pure_audio = false; @@ -2620,7 +3199,7 @@ class SrsTsPacket } // calc the user defined data size for payload. - var nb_payload:int = Hls.SRS_TS_PACKET_SIZE - (stream.position - pos); + var nb_payload:int = HlsCodec.SRS_TS_PACKET_SIZE - (stream.position - pos); // optional: payload. if (adaption_field_control == SrsTsAdaptationFieldType.PayloadOnly @@ -4583,7 +5162,7 @@ class SrsTsPayloadPMT extends SrsTsPayloadPSI */ class M3u8 { - private var _hls:Hls; + private var _hls:HlsCodec; private var _log:ILogger = new TraceLogger("HLS"); private var _tses:Array; @@ -4593,7 +5172,7 @@ class M3u8 // when variant, all ts url is sub m3u8 url. private var _variant:Boolean; - public function M3u8(hls:Hls) + public function M3u8(hls:HlsCodec) { _hls = hls; _tses = new Array(); diff --git a/trunk/research/players/srs_player/src/ILogger.as b/trunk/research/players/srs_player/src/ILogger.as deleted file mode 100644 index 5494fc630..000000000 --- a/trunk/research/players/srs_player/src/ILogger.as +++ /dev/null @@ -1,12 +0,0 @@ -package -{ - public interface ILogger - { - function debug0(message:String, ... rest):void; - function debug(message:String, ... rest):void; - function info(message:String, ... rest):void; - function warn(message:String, ... rest):void; - function error(message:String, ... rest):void; - function fatal(message:String, ... rest):void; - } -} \ No newline at end of file diff --git a/trunk/research/players/srs_player/src/IPlayer.as b/trunk/research/players/srs_player/src/IPlayer.as deleted file mode 100644 index 2753cc849..000000000 --- a/trunk/research/players/srs_player/src/IPlayer.as +++ /dev/null @@ -1,33 +0,0 @@ -package -{ - import flash.net.NetStream; - - /** - * the player interface. - */ - public interface IPlayer - { - /** - * initialize the player by flashvars for config. - * @param flashvars the config. - */ - function init(flashvars:Object):void; - - /** - * get the NetStream to play the stream. - * @return the underlayer stream object. - */ - function stream():NetStream; - - /** - * connect and play url. - * @param url the stream url to play. - */ - function play(url:String):void; - - /** - * close the player. - */ - function close():void; - } -} \ No newline at end of file diff --git a/trunk/research/players/srs_player/src/M3u8Player.as b/trunk/research/players/srs_player/src/M3u8Player.as deleted file mode 100644 index 82d7c7c82..000000000 --- a/trunk/research/players/srs_player/src/M3u8Player.as +++ /dev/null @@ -1,307 +0,0 @@ -package -{ - import flash.display.Sprite; - import flash.display.StageAlign; - import flash.display.StageDisplayState; - import flash.display.StageScaleMode; - import flash.events.Event; - import flash.events.FullScreenEvent; - import flash.events.MouseEvent; - import flash.events.NetStatusEvent; - import flash.events.ProgressEvent; - import flash.events.TimerEvent; - import flash.external.ExternalInterface; - import flash.media.SoundTransform; - import flash.media.Video; - import flash.net.NetConnection; - import flash.net.NetStream; - import flash.net.NetStreamAppendBytesAction; - import flash.net.URLLoader; - import flash.net.URLLoaderDataFormat; - import flash.net.URLRequest; - import flash.net.URLRequestHeader; - import flash.net.URLRequestMethod; - import flash.net.URLStream; - import flash.net.URLVariables; - import flash.system.Security; - import flash.ui.ContextMenu; - import flash.ui.ContextMenuItem; - import flash.utils.ByteArray; - import flash.utils.Timer; - import flash.utils.getTimer; - import flash.utils.setTimeout; - - import flashx.textLayout.formats.Float; - - /** - * the m3u8 player. - */ - public class M3u8Player implements IPlayer - { - private var js_id:String = null; - - // play param url. - private var user_url:String = null; - - private var media_stream:NetStream = null; - private var media_conn:NetConnection = null; - - private var owner:srs_player = null; - private var hls:Hls = null; // parse m3u8 and ts - - // the uuid similar to Safari, to identify this play session. - // @see https://github.com/winlinvip/srs-plus/blob/bms/trunk/src/app/srs_app_http_stream.cpp#L45 - public var XPlaybackSessionId:String = createRandomIdentifier(32); - private function createRandomIdentifier(length:uint, radix:uint = 61):String { - var characters:Array = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', - 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', - 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', - 'z'); - var id:Array = new Array(); - radix = (radix > 61) ? 61 : radix; - while (length--) { - id.push(characters[randomIntegerWithinRange(0, radix)]); - } - return id.join(''); - } - private function randomIntegerWithinRange(min:int, max:int):int { - return Math.floor(Math.random() * (1 + max - min) + min); - } - - // callback for hls. - public var flvHeader:ByteArray = null; - public function onSequenceHeader():void { - if (!media_stream) { - setTimeout(onSequenceHeader, 1000); - return; - } - - var s:NetStream = media_stream; - s.appendBytesAction(NetStreamAppendBytesAction.RESET_BEGIN); - s.appendBytes(flvHeader); - log("FLV: sps/pps " + flvHeader.length + " bytes"); - - writeFlv(flvHeader); - } - public function onFlvBody(uri:String, flv:ByteArray):void { - if (!media_stream) { - return; - } - - if (!flvHeader) { - return; - } - - var s:NetStream = media_stream; - s.appendBytes(flv); - log("FLV: ts " + uri + " parsed to flv " + flv.length + " bytes"); - - writeFlv(flv); - } - private function writeFlv(data:ByteArray):void { - return; - - var r:URLRequest = new URLRequest("http://192.168.1.117:8088/api/v1/flv"); - r.method = URLRequestMethod.POST; - r.data = data; - - var pf:URLLoader = new URLLoader(); - pf.dataFormat = URLLoaderDataFormat.BINARY; - pf.load(r); - } - - public function M3u8Player(o:srs_player) { - owner = o; - hls = new Hls(this); - } - - public function init(flashvars:Object):void { - this.js_id = flashvars.id; - } - - public function stream():NetStream { - return this.media_stream; - } - - // owner.on_player_metadata(evt.info.data); - public function play(url:String):void { - var streamName:String; - this.user_url = url; - - this.media_conn = new NetConnection(); - this.media_conn.client = {}; - this.media_conn.client.onBWDone = function():void {}; - this.media_conn.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void { - log("NetConnection: code=" + evt.info.code + ", data is " + evt.info.data); - - // TODO: FIXME: failed event. - if (evt.info.code != "NetConnection.Connect.Success") { - return; - } - - media_stream = new NetStream(media_conn); - media_stream.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void { - log("NetStream: code=" + evt.info.code); - - if (evt.info.code == "NetStream.Video.DimensionChange") { - owner.on_player_dimension_change(); - } else if (evt.info.code == "NetStream.Buffer.Empty") { - owner.on_player_buffer_empty(); - } else if (evt.info.code == "NetStream.Buffer.Full") { - owner.on_player_buffer_full(); - } - - // TODO: FIXME: failed event. - }); - - // setup stream before play. - owner.on_player_before_play(); - - media_stream.play(null); - refresh_m3u8(); - - owner.on_player_play(); - }); - - this.media_conn.connect(null); - } - - public function close():void { - if (this.media_stream) { - this.media_stream.close(); - this.media_stream = null; - } - if (this.media_conn) { - this.media_conn.close(); - this.media_conn = null; - } - } - - private var parsed_ts_seq_no:Number = -1; - private function refresh_m3u8():void { - download(user_url, function(stream:ByteArray):void { - var m3u8:String = stream.toString(); - hls.parse(user_url, m3u8); - - // redirect by variant m3u8. - if (hls.variant) { - var smu:String = hls.getTsUrl(0); - log("variant hls=" + user_url + ", redirect2=" + smu); - user_url = smu; - setTimeout(refresh_m3u8, 0); - return; - } - - // fetch from the last one. - if (parsed_ts_seq_no == -1) { - parsed_ts_seq_no = hls.seq_no + hls.tsCount - 1; - } - - // not changed. - if (parsed_ts_seq_no >= hls.seq_no + hls.tsCount) { - refresh_ts(); - return; - } - - // parse each ts. - var nb_ts:Number = hls.seq_no + hls.tsCount - parsed_ts_seq_no; - log("m3u8 changed, got " + nb_ts + " new ts, count=" + hls.tsCount + ", seqno=" + hls.seq_no + ", parsed=" + parsed_ts_seq_no); - - refresh_ts(); - }) - } - private function refresh_ts():void { - // all ts parsed. - if (parsed_ts_seq_no >= hls.seq_no + hls.tsCount) { - var to:Number = 1000; - if (hls.tsCount > 0) { - to = hls.duration * 1000 / hls.tsCount * Consts.M3u8RefreshRatio; - } - setTimeout(refresh_m3u8, to); - log("m3u8 not changed, retry after " + to.toFixed(2) + "ms"); - return; - } - - // parse current ts. - var uri:String = hls.getTsUrl(parsed_ts_seq_no - hls.seq_no); - - // parse metadata from uri. - if (uri.indexOf("?") >= 0) { - var uv:URLVariables = new URLVariables(uri.substr(uri.indexOf("?") + 1)); - var obj:Object = {}; - for (var k:String in uv) { - var v:String = uv[k]; - if (k == "shp_sip1") { - obj.srs_server_ip = v; - } else if (k == "shp_cid") { - obj.srs_id = v; - } else if (k == "shp_pid") { - obj.srs_pid = v; - } - //log("uv[" + k + "]=" + v); - } - owner.on_player_metadata(obj); - } - - download(uri, function(stream:ByteArray):void{ - log("got ts seqno=" + parsed_ts_seq_no + ", " + stream.length + " bytes"); - - var flv:FlvPiece = new FlvPiece(parsed_ts_seq_no); - var body:ByteArray = new ByteArray(); - stream.position = 0; - hls.parseBodyAsync(flv, stream, body, function():void{ - body.position = 0; - //log("ts parsed, seqno=" + parsed_ts_seq_no + ", flv=" + body.length + "B"); - onFlvBody(uri, body); - - parsed_ts_seq_no++; - setTimeout(refresh_ts, 0); - }); - }); - } - private function download(uri:String, completed:Function):void { - var url:URLStream = new URLStream(); - var stream:ByteArray = new ByteArray(); - - url.addEventListener(ProgressEvent.PROGRESS, function(evt:ProgressEvent):void { - if (url.bytesAvailable <= 0) { - return; - } - - //log(uri + " total=" + evt.bytesTotal + ", loaded=" + evt.bytesLoaded + ", available=" + url.bytesAvailable); - var bytes:ByteArray = new ByteArray(); - url.readBytes(bytes, 0, url.bytesAvailable); - stream.writeBytes(bytes); - }); - - url.addEventListener(Event.COMPLETE, function(evt:Event):void { - log(uri + " completed, total=" + stream.length + "bytes"); - if (url.bytesAvailable <= 0) { - completed(stream); - return; - } - - //log(uri + " completed" + ", available=" + url.bytesAvailable); - var bytes:ByteArray = new ByteArray(); - url.readBytes(bytes, 0, url.bytesAvailable); - stream.writeBytes(bytes); - - completed(stream); - }); - - // we set to the query. - uri += ((uri.indexOf("?") == -1)? "?":"&") + "shp_xpsid=" + XPlaybackSessionId; - var r:URLRequest = new URLRequest(uri); - // seems flash not allow set this header. - r.requestHeaders.push(new URLRequestHeader("X-Playback-Session-Id", XPlaybackSessionId)); - - log("start download " + uri); - url.load(r); - } - - private function log(msg:String):void { - Utility.log(js_id, msg); - } - } -} \ No newline at end of file diff --git a/trunk/research/players/srs_player/src/CommonPlayer.as b/trunk/research/players/srs_player/src/Player.as similarity index 91% rename from trunk/research/players/srs_player/src/CommonPlayer.as rename to trunk/research/players/srs_player/src/Player.as index 749b890b6..15350363c 100644 --- a/trunk/research/players/srs_player/src/CommonPlayer.as +++ b/trunk/research/players/srs_player/src/Player.as @@ -28,8 +28,14 @@ package * common player to play rtmp/flv stream, * use system NetStream. */ - public class CommonPlayer implements IPlayer + public class Player { + // refresh every ts_fragment_seconds*M3u8RefreshRatio + public static var M3u8RefreshRatio:Number = 0.5; + + // parse ts every this ms. + public static var TsParseAsyncInterval:Number = 80; + private var js_id:String = null; // play param url. @@ -40,7 +46,7 @@ package private var owner:srs_player = null; - public function CommonPlayer(o:srs_player) { + public function Player(o:srs_player) { owner = o; } @@ -88,7 +94,11 @@ package return; } - media_stream = new NetStream(media_conn); + if (url.indexOf(".m3u8") > 0) { + media_stream = new HlsNetStream(M3u8RefreshRatio, TsParseAsyncInterval, media_conn); + } else { + media_stream = new NetStream(media_conn); + } media_stream.addEventListener(NetStatusEvent.NET_STATUS, function(evt:NetStatusEvent):void { log("NetStream: code=" + evt.info.code); diff --git a/trunk/research/players/srs_player/src/TraceLogger.as b/trunk/research/players/srs_player/src/TraceLogger.as deleted file mode 100644 index 779261ae9..000000000 --- a/trunk/research/players/srs_player/src/TraceLogger.as +++ /dev/null @@ -1,93 +0,0 @@ -package -{ - import flash.globalization.DateTimeFormatter; - - public class TraceLogger implements ILogger - { - private var _category:String; - - public function get category():String - { - return _category; - } - public function TraceLogger(category:String) - { - _category = category; - } - public function debug0(message:String, ...rest):void - { - } - - public function debug(message:String, ...rest):void - { - } - - public function info(message:String, ...rest):void - { - logMessage(LEVEL_INFO, message, rest); - } - - public function warn(message:String, ...rest):void - { - logMessage(LEVEL_WARN, message, rest); - } - - public function error(message:String, ...rest):void - { - logMessage(LEVEL_ERROR, message, rest); - } - - public function fatal(message:String, ...rest):void - { - logMessage(LEVEL_FATAL, message, rest); - } - protected function logMessage(level:String, message:String, params:Array):void - { - var msg:String = ""; - - // add datetime - var date:Date = new Date(); - var dtf:DateTimeFormatter = new DateTimeFormatter("UTC"); - dtf.setDateTimePattern("yyyy-MM-dd HH:mm:ss"); - - // TODO: FIXME: the SSS format not run, use date.milliseconds instead. - msg += '[' + dtf.format(date) + "." + date.milliseconds + ']'; - msg += " [" + level + "] "; - - // add category and params - msg += "[" + category + "] " + applyParams(message, params); - - // trace the message - Utility.log("CORE", msg); - } - private function leadingZeros(x:Number):String - { - if (x < 10) { - return "00" + x.toString(); - } - - if (x < 100) { - return "0" + x.toString(); - } - - return x.toString(); - } - private function applyParams(message:String, params:Array):String - { - var result:String = message; - - var numParams:int = params.length; - - for (var i:int = 0; i < numParams; i++) { - result = result.replace(new RegExp("\\{" + i + "\\}", "g"), params[i]); - } - return result; - } - - private static const LEVEL_DEBUG:String = "DEBUG"; - private static const LEVEL_WARN:String = "WARN"; - private static const LEVEL_INFO:String = "INFO"; - private static const LEVEL_ERROR:String = "ERROR"; - private static const LEVEL_FATAL:String = "FATAL"; - } -} \ No newline at end of file diff --git a/trunk/research/players/srs_player/src/Utility.as b/trunk/research/players/srs_player/src/Utility.as index 8204ee443..7707954c2 100644 --- a/trunk/research/players/srs_player/src/Utility.as +++ b/trunk/research/players/srs_player/src/Utility.as @@ -32,14 +32,16 @@ package * @param msg the log message. */ public static function log(js_id:String, msg:String):void { - msg = "[" + new Date() +"][srs-player][" + js_id + "] " + msg; + if (js_id) { + msg = "[" + new Date() +"][srs-player][" + js_id + "] " + msg; + } logData += msg + "\n"; trace(msg); if (!flash.external.ExternalInterface.available) { - flash.utils.setTimeout(log, 300, msg); + flash.utils.setTimeout(log, 300, null, msg); return; } diff --git a/trunk/research/players/srs_player/src/srs_player.as b/trunk/research/players/srs_player/src/srs_player.as index 5707fc163..691eb4600 100755 --- a/trunk/research/players/srs_player/src/srs_player.as +++ b/trunk/research/players/srs_player/src/srs_player.as @@ -58,7 +58,7 @@ package private var control_fs_mask:Sprite = new Sprite(); // the common player to play stream. - private var player:IPlayer = null; + private var player:Player = null; // the flashvars config. private var config:Object = null; @@ -438,13 +438,7 @@ package } // create player. - if (url.indexOf(".m3u8") > 0 && Utility.stringStartswith(url, "http://")) { - player = new M3u8Player(this); - log("create M3U8 player."); - } else { - player = new CommonPlayer(this); - log("create Common player."); - } + player = new Player(this); // init player by config. player.init(config);