├── .gitignore ├── LICENSE ├── README.md ├── amf.js ├── buffer-pool.js ├── client.js ├── handshake.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | .idea 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Just a fork. 2 | 3 | Use [doremeet/rtmp-server-nodejs](https://github.com/doremeet/rtmp-server-nodejs) or [illuspas/Node-Media-Server](https://github.com/illuspas/Node-Media-Server). 4 | -------------------------------------------------------------------------------- /amf.js: -------------------------------------------------------------------------------- 1 | var amf3dRules = { 2 | 0x00: amf3decUndefined, 3 | 0x01: amf3decNull, 4 | 0x02: amf3decFalse, 5 | 0x03: amf3decTrue, 6 | 0x04: amf3decInteger, 7 | 0x05: amf3decDouble, 8 | 0x06: amf3decString, 9 | 0x07: amf3decXmlDoc, 10 | 0x08: amf3decDate, 11 | 0x09: amf3decArray, 12 | 0x0A: amf3decObject, 13 | 0x0B: amf3decXml, 14 | 0x0C: amf3decByteArray //, 15 | // 0x0D: amf3decVecInt, 16 | // 0x0E: amf3decVecUInt, 17 | // 0x0F: amf3decVecDouble, 18 | // 0x10: amf3decVecObject, 19 | // 0x11: amf3decDict // No dictionary support for the moment! 20 | }; 21 | 22 | var amf3eRules = { 23 | 'string': amf3encString, 24 | 'integer': amf3encInteger, 25 | 'double': amf3encDouble, 26 | 'xml': amf3encXmlDoc, 27 | 'object': amf3encObject, 28 | 'array': amf3encArray, 29 | 'sarray': amf3encArray, 30 | 'binary': amf3encByteArray, 31 | 'true': amf3encTrue, 32 | 'false': amf3encFalse, 33 | 'undefined': amf3encUndefined, 34 | 'null': amf3encNull 35 | }; 36 | 37 | var amf0dRules = { 38 | 0x00: amf0decNumber, 39 | 0x01: amf0decBool, 40 | 0x02: amf0decString, 41 | 0x03: amf0decObject, 42 | // 0x04: amf0decMovie, // Reserved 43 | 0x05: amf0decNull, 44 | 0x06: amf0decUndefined, 45 | 0x07: amf0decRef, 46 | 0x08: amf0decArray, 47 | // 0x09: amf0decObjEnd, // Should never happen normally 48 | 0x0A: amf0decSArray, 49 | 0x0B: amf0decDate, 50 | 0x0C: amf0decLongString, 51 | // 0x0D: amf0decUnsupported, // Has been never originally implemented by Adobe! 52 | // 0x0E: amf0decRecSet, // Has been never originally implemented by Adobe! 53 | 0x0F: amf0decXmlDoc, 54 | 0x10: amf0decTypedObj 55 | }; 56 | 57 | var amf0eRules = { 58 | 'string': amf0encString, 59 | 'integer': amf0encNumber, 60 | 'double': amf0encNumber, 61 | 'xml': amf0encXmlDoc, 62 | 'object': amf0encObject, 63 | 'array': amf0encArray, 64 | 'sarray': amf0encSArray, 65 | 'binary': amf0encString, 66 | 'true': amf0encBool, 67 | 'false': amf0encBool, 68 | 'undefined': amf0encUndefined, 69 | 'null': amf0encNull 70 | }; 71 | 72 | function amfType(o) { 73 | var jsType = typeof o; 74 | 75 | if (o === null) return 'null'; 76 | if (jsType == 'undefined') return 'undefined'; 77 | if (jsType == 'number') { 78 | if (parseInt(o) == o) return 'integer'; 79 | return 'double'; 80 | } 81 | if (jsType == 'boolean') return o ? 'true' : 'false'; 82 | if (jsType == 'string') return 'string'; 83 | if (jsType == 'object') { 84 | if (o instanceof Array) { 85 | if (o.sarray) return 'sarray'; 86 | return 'array'; 87 | } 88 | return 'object'; 89 | } 90 | throw new Error('Unsupported type!') 91 | } 92 | 93 | // AMF3 implementation 94 | 95 | /** 96 | * AMF3 Decode undefined value 97 | * @returns {{len: number, value: undefined}} 98 | */ 99 | function amf3decUndefined() { 100 | return { 101 | len: 1, 102 | value: undefined 103 | } 104 | } 105 | 106 | /** 107 | * AMF3 Encode undefined value 108 | * @returns {Buffer} 109 | */ 110 | function amf3encUndefined() { 111 | var buf = new Buffer(1); 112 | buf.writeUInt8(0x00); 113 | return buf; 114 | } 115 | 116 | /** 117 | * AMF3 Decode null 118 | * @returns {{len: number, value: null}} 119 | */ 120 | function amf3decNull() { 121 | return { 122 | len: 1, 123 | value: null 124 | } 125 | } 126 | 127 | /** 128 | * AMF3 Encode null 129 | * @returns {Buffer} 130 | */ 131 | function amf3encNull() { 132 | var buf = new Buffer(1); 133 | buf.writeUInt8(0x01); 134 | return buf; 135 | } 136 | 137 | /** 138 | * AMF3 Decode false 139 | * @returns {{len: number, value: boolean}} 140 | */ 141 | function amf3decFalse() { 142 | return { 143 | len: 1, 144 | value: false 145 | } 146 | } 147 | 148 | /** 149 | * AMF3 Encode false 150 | * @returns {Buffer} 151 | */ 152 | function amf3encFalse() { 153 | var buf = new Buffer(1); 154 | buf.writeUInt8(0x02); 155 | return buf; 156 | } 157 | 158 | /** 159 | * AMF3 Decode true 160 | * @returns {{len: number, value: boolean}} 161 | */ 162 | function amf3decTrue() { 163 | return { 164 | len: 1, 165 | value: true 166 | } 167 | } 168 | 169 | /** 170 | * AMF3 Encode true 171 | * @returns {Buffer} 172 | */ 173 | function amf3encTrue() { 174 | var buf = new Buffer(1); 175 | buf.writeUInt8(0x03); 176 | return buf; 177 | } 178 | 179 | /** 180 | * Generic decode of AMF3 UInt29 values 181 | * @param buf 182 | * @returns {{len: number, value: number}} 183 | */ 184 | function amf3decUI29(buf) { 185 | var val = 0; 186 | var len = 1; 187 | var b; 188 | 189 | do { 190 | b = buf.readUInt8(len++); 191 | val = (val << 7) + (b & 0x7F); 192 | } while (len < 5 || b > 0x7F); 193 | 194 | if (len == 5) val = val | b; // Preserve the major bit of the last byte 195 | 196 | return { 197 | len: len, 198 | value: val 199 | } 200 | } 201 | 202 | /** 203 | * Generic encode of AMF3 UInt29 value 204 | * @param num 205 | * @returns {Buffer} 206 | */ 207 | function amf3encUI29(num) { 208 | var len = 0; 209 | if (num < 0x80) len = 1; 210 | if (num < 0x4000) len = 2; 211 | if (num < 0x200000) len = 3; 212 | if (num >= 0x200000) len = 4; 213 | var buf = new Buffer(len); 214 | switch (len) { 215 | case 1: 216 | buf.writeUInt8(num, 0); 217 | break; 218 | case 2: 219 | buf.writeUInt8(num & 0x7F, 0); 220 | buf.writeUInt8((num >> 7) | 0x80, 1); 221 | break; 222 | case 3: 223 | buf.writeUInt8(num & 0x7F, 0); 224 | buf.writeUInt8((num >> 7) & 0x7F, 1); 225 | buf.writeUInt8((num >> 14) | 0x80, 2); 226 | break; 227 | case 4: 228 | buf.writeUInt8(num & 0xFF, 0); 229 | buf.writeUInt8((num >> 8) & 0x7F, 1); 230 | buf.writeUInt8((num >> 15) | 0x7F, 2); 231 | buf.writeUInt8((num >> 22) | 0x7F, 3); 232 | break; 233 | } 234 | return buf; 235 | } 236 | 237 | /** 238 | * AMF3 Decode an integer 239 | * @param buf 240 | * @returns {{len: number, value: number}} 241 | */ 242 | function amf3decInteger(buf) { // Invert the integer 243 | var resp = amf3decUI29(buf); 244 | if (resp.value > 0x0FFFFFFF) resp.value = (resp.value & 0x0FFFFFFF) - 0x10000000; 245 | return resp; 246 | } 247 | 248 | /** 249 | * AMF3 Encode an integer 250 | * @param num 251 | * @returns {Buffer} 252 | */ 253 | function amf3encInteger(num) { 254 | var buf = new Buffer(1); 255 | buf.writeUInt8(0x4, 0); 256 | return Buffer.concat([buf, amf3encUI29(num & 0x3FFFFFFF)]); // This AND will auto convert the 257 | // sign bit! 258 | } 259 | 260 | /** 261 | * AMF3 Decode String 262 | * @param buf 263 | * @returns {{len: *, value: (*|String)}} 264 | */ 265 | function amf3decString(buf) { 266 | var sLen = amf3decUI29(buf); 267 | var s = sLen & 1; 268 | sLen = sLen >> 1; // The real length without the lowest bit 269 | if (s) return { 270 | len: sLen.value + 5, 271 | value: buf.slice(5, sLen.value + 5).toString('utf8') 272 | }; 273 | throw new Error("Error, we have a need to decode a String that is a Reference"); // TODO: 274 | // Implement 275 | // references! 276 | } 277 | 278 | /** 279 | * AMF3 Encode String 280 | * @param str 281 | * @returns {Buffer} 282 | */ 283 | function amf3encString(str) { 284 | var sLen = amf3encUI29(str.length << 1); 285 | var buf = new Buffer(1); 286 | buf.writeUInt8(0x6, 0); 287 | return Buffer.concat([buf, sLen, new Buffer(str, 'utf8')]); 288 | } 289 | 290 | /** 291 | * AMF3 Decode XMLDoc 292 | * @param buf 293 | * @returns {{len: *, value: (*|String)}} 294 | */ 295 | function amf3decXmlDoc(buf) { 296 | var sLen = amf3decUI29(buf); 297 | var s = sLen & 1; 298 | sLen = sLen >> 1; // The real length without the lowest bit 299 | if (s) return { 300 | len: sLen.value + 5, 301 | value: buf.slice(5, sLen.value + 5).toString('utf8') 302 | }; 303 | throw new Error("Error, we have a need to decode a String that is a Reference"); // TODO: 304 | // Implement 305 | // references! 306 | } 307 | 308 | /** 309 | * AMF3 Encode XMLDoc 310 | * @param str 311 | * @returns {Buffer} 312 | */ 313 | function amf3encXmlDoc(str) { 314 | var sLen = amf3encUI29(str.length << 1); 315 | var buf = new Buffer(1); 316 | buf.writeUInt8(0x7, 0); 317 | return Buffer.concat([buf, sLen, new Buffer(str, 'utf8')]); 318 | } 319 | 320 | /** 321 | * AMF3 Decode Generic XML 322 | * @param buf 323 | * @returns {{len: *, value: (*|String)}} 324 | */ 325 | function amf3decXml(buf) { 326 | var sLen = amf3decUI29(buf); 327 | var s = sLen & 1; 328 | sLen = sLen >> 1; // The real length without the lowest bit 329 | if (s) return { 330 | len: sLen.value + 5, 331 | value: buf.slice(5, sLen.value + 5).toString('utf8') 332 | }; 333 | throw new Error("Error, we have a need to decode a String that is a Reference"); // TODO: 334 | // Implement 335 | // references! 336 | } 337 | 338 | /** 339 | * AMF3 Encode Generic XML 340 | * @param str 341 | * @returns {Buffer} 342 | */ 343 | function amf3encXml(str) { 344 | var sLen = amf3encUI29(str.length << 1); 345 | var buf = new Buffer(1); 346 | buf.writeUInt8(0x0B, 0); 347 | return Buffer.concat([buf, sLen, new Buffer(str, 'utf8')]); 348 | } 349 | 350 | /** 351 | * AMF3 Decide Byte Array 352 | * @param buf 353 | * @returns {{len: *, value: (Array|string|*|Buffer|Blob)}} 354 | */ 355 | function amf3decByteArray(buf) { 356 | var sLen = amf3decUI29(buf); 357 | var s = sLen & 1; // TODO: Check if we follow the same rule! 358 | sLen = sLen >> 1; // The real length without the lowest bit 359 | if (s) return { 360 | len: sLen.value + 5, 361 | value: buf.slice(5, sLen.value + 5) 362 | }; 363 | throw new Error("Error, we have a need to decode a String that is a Reference"); // TODO: 364 | // Implement 365 | // references! 366 | } 367 | 368 | /** 369 | * AMF3 Encode Byte Array 370 | * @param str 371 | * @returns {Buffer} 372 | */ 373 | function amf3encByteArray(str) { 374 | var sLen = amf3encUI29(str.length << 1); 375 | var buf = new Buffer(1); 376 | buf.writeUInt8(0x0C, 0); 377 | return Buffer.concat([buf, sLen, (typeof str == 'string') ? new Buffer(str, 'binary') : str]); 378 | } 379 | 380 | /** 381 | * AMF3 Decode Double 382 | * @param buf 383 | * @returns {{len: number, value: (*|Number)}} 384 | */ 385 | function amf3decDouble(buf) { 386 | return { 387 | len: 9, 388 | value: buf.readDoubleBE(1) 389 | } 390 | } 391 | 392 | /** 393 | * AMF3 Encode Double 394 | * @param num 395 | * @returns {Buffer} 396 | */ 397 | function amf3encDouble(num) { 398 | var buf = new Buffer(9); 399 | buf.writeUInt8(0x05, 0); 400 | buf.writeDoubleBE(num, 1); 401 | return buf; 402 | } 403 | 404 | /** 405 | * AMF3 Decode Date 406 | * @param buf 407 | * @returns {{len: *, value: (*|Number)}} 408 | */ 409 | function amf3decDate(buf) { // The UI29 should be 1 410 | var uTz = amf3decUI29(buf); 411 | var ts = buf.readDoubleBE(uTz.len); 412 | return { 413 | len: uTz.len + 8, 414 | value: ts 415 | } 416 | } 417 | 418 | /** 419 | * AMF3 Encode Date 420 | * @param ts 421 | * @returns {Buffer} 422 | */ 423 | function amf3encDate(ts) { 424 | var buf = new Buffer(1); 425 | buf.writeUInt8(0x8, 0); 426 | var tsBuf = new Buffer(8); 427 | tsBuf.writeDoubleBE(ts, 0); 428 | return Buffer.concat([buf, amf3encUI29(1), tsBuf]); // We always do 1 429 | } 430 | 431 | /** 432 | * AMF3 Decode Array 433 | * @param buf 434 | * @returns {{len: *, value: *}} 435 | */ 436 | function amf3decArray(buf) { 437 | var count = amf3decUI29(buf.slice(1)); 438 | var obj = amf3decObject(buf.slice(count.len)); 439 | if (count.value % 2 == 1) throw new Error("This is a reference to another array, which currently we don't support!"); 440 | return { 441 | len: count.len + obj.len, 442 | value: obj.value 443 | } 444 | } 445 | 446 | /** 447 | * AMF3 Encode Array 448 | */ 449 | function amf3encArray() { 450 | throw new Error('Encoding arrays is not supported yet!'); // TODO: Implement encoding of arrays 451 | } 452 | 453 | /** 454 | * AMF3 Decode Object 455 | * @param buf 456 | */ 457 | function amf3decObject(buf) { 458 | 459 | } 460 | 461 | /** 462 | * AMF3 Encode Object 463 | * @param o 464 | */ 465 | function amf3encObject(o) { 466 | 467 | } 468 | 469 | // AMF0 Implementation 470 | 471 | /** 472 | * AMF0 Decode Number 473 | * @param buf 474 | * @returns {{len: number, value: (*|Number)}} 475 | */ 476 | function amf0decNumber(buf) { 477 | return { 478 | len: 9, 479 | value: buf.readDoubleBE(1) 480 | } 481 | } 482 | 483 | /** 484 | * AMF0 Encode Number 485 | * @param num 486 | * @returns {Buffer} 487 | */ 488 | function amf0encNumber(num) { 489 | var buf = new Buffer(9); 490 | buf.writeUInt8(0x00, 0); 491 | buf.writeDoubleBE(num, 1); 492 | return buf; 493 | } 494 | 495 | /** 496 | * AMF0 Decode Boolean 497 | * @param buf 498 | * @returns {{len: number, value: boolean}} 499 | */ 500 | function amf0decBool(buf) { 501 | return { 502 | len: 2, 503 | value: (buf.readUInt8(1) != 0) 504 | } 505 | } 506 | 507 | /** 508 | * AMF0 Encode Boolean 509 | * @param num 510 | * @returns {Buffer} 511 | */ 512 | function amf0encBool(num) { 513 | var buf = new Buffer(2); 514 | buf.writeUInt8(0x01, 0); 515 | buf.writeUInt8((num ? 1 : 0), 1); 516 | return buf; 517 | } 518 | 519 | /** 520 | * AMF0 Decode Null 521 | * @returns {{len: number, value: null}} 522 | */ 523 | function amf0decNull() { 524 | return { 525 | len: 1, 526 | value: null 527 | } 528 | } 529 | 530 | /** 531 | * AMF0 Encode Null 532 | * @returns {Buffer} 533 | */ 534 | function amf0encNull() { 535 | var buf = new Buffer(1); 536 | buf.writeUInt8(0x05, 0); 537 | return buf; 538 | } 539 | 540 | /** 541 | * AMF0 Decode Undefined 542 | * @returns {{len: number, value: undefined}} 543 | */ 544 | function amf0decUndefined() { 545 | return { 546 | len: 1, 547 | value: undefined 548 | } 549 | } 550 | 551 | /** 552 | * AMF0 Encode Undefined 553 | * @returns {Buffer} 554 | */ 555 | function amf0encUndefined() { 556 | var buf = new Buffer(1); 557 | buf.writeUInt8(0x06, 0); 558 | return buf; 559 | } 560 | 561 | /** 562 | * AMF0 Decode Date 563 | * @param buf 564 | * @returns {{len: number, value: (*|Number)}} 565 | */ 566 | function amf0decDate(buf) { 567 | // var s16 = buf.readInt16BE(1); 568 | var ts = buf.readDoubleBE(3); 569 | return { 570 | len: 11, 571 | value: ts 572 | } 573 | } 574 | 575 | /** 576 | * AMF0 Encode Date 577 | * @param ts 578 | * @returns {Buffer} 579 | */ 580 | function amf0encDate(ts) { 581 | var buf = new Buffer(11); 582 | buf.writeUInt8(0x0B, 0); 583 | buf.writeInt16BE(0, 1); 584 | buf.writeDoubleBE(ts, 3); 585 | return buf; 586 | } 587 | 588 | /** 589 | * AMF0 Decode Object 590 | * @param buf 591 | * @returns {{len: number, value: {}}} 592 | */ 593 | function amf0decObject(buf) { // TODO: Implement references! 594 | var obj = {}; 595 | var iBuf = buf.slice(1); 596 | var len = 1; 597 | // console.log('ODec',iBuf.readUInt8(0)); 598 | while (iBuf.readUInt8(0) != 0x09) { 599 | // console.log('Field', iBuf.readUInt8(0), iBuf); 600 | var prop = amf0decUString(iBuf); 601 | // console.log('Got field for property', prop); 602 | len += prop.len; 603 | if (iBuf.slice(prop.len).readUInt8(0) == 0x09) { 604 | len++; 605 | // console.log('Found the end property'); 606 | break; 607 | } // END Object as value, we shall leave 608 | if (prop.value == '') break; 609 | var val = amf0DecodeOne(iBuf.slice(prop.len)); 610 | // console.log('Got field for value', val); 611 | obj[prop.value] = val.value; 612 | len += val.len; 613 | iBuf = iBuf.slice(prop.len + val.len); 614 | } 615 | return { 616 | len: len, 617 | value: obj 618 | } 619 | } 620 | 621 | /** 622 | * AMF0 Encode Object 623 | */ 624 | function amf0encObject(o) { 625 | if (typeof o !== 'object') return; 626 | 627 | var data = new Buffer(1); 628 | data.writeUInt8(0x03, 0); // Type object 629 | var k; 630 | for (k in o) { 631 | data = Buffer.concat([data, amf0encUString(k), amf0EncodeOne(o[k])]); 632 | } 633 | var termCode = new Buffer(1); 634 | termCode.writeUInt8(0x09, 0); 635 | return Buffer.concat([data, amf0encUString(''), termCode]); 636 | } 637 | 638 | /** 639 | * AMF0 Decode Reference 640 | * @param buf 641 | * @returns {{len: number, value: string}} 642 | */ 643 | function amf0decRef(buf) { 644 | var index = buf.readUInt16BE(1); 645 | return { 646 | len: 3, 647 | value: 'ref' + index 648 | } 649 | } 650 | 651 | /** 652 | * AMF0 Encode Reference 653 | * @param index 654 | * @returns {Buffer} 655 | */ 656 | function amf0encRef(index) { 657 | var buf = new Buffer(3); 658 | buf.writeUInt8(0x07, 0); 659 | buf.writeUInt16BE(index, 1); 660 | return buf; 661 | } 662 | 663 | /** 664 | * AMF0 Decode String 665 | * @param buf 666 | * @returns {{len: *, value: (*|string|String)}} 667 | */ 668 | function amf0decString(buf) { 669 | var sLen = buf.readUInt16BE(1); 670 | return { 671 | len: 3 + sLen, 672 | value: buf.toString('utf8', 3, 3 + sLen) 673 | } 674 | } 675 | 676 | /** 677 | * AMF0 Decode Untyped (without the type byte) String 678 | * @param buf 679 | * @returns {{len: *, value: (*|string|String)}} 680 | */ 681 | function amf0decUString(buf) { 682 | var sLen = buf.readUInt16BE(0); 683 | return { 684 | len: 2 + sLen, 685 | value: buf.toString('utf8', 2, 2 + sLen) 686 | } 687 | } 688 | 689 | /** 690 | * Do AMD0 Encode of Untyped String 691 | * @param s 692 | * @returns {Buffer} 693 | */ 694 | function amf0encUString(s) { 695 | var data = new Buffer(s, 'utf8'); 696 | var sLen = new Buffer(2); 697 | sLen.writeUInt16BE(data.length, 0); 698 | return Buffer.concat([sLen, data]); 699 | } 700 | 701 | /** 702 | * AMF0 Encode String 703 | * @param str 704 | * @returns {Buffer} 705 | */ 706 | function amf0encString(str) { 707 | var buf = new Buffer(3); 708 | buf.writeUInt8(0x02, 0); 709 | buf.writeUInt16BE(str.length, 1); 710 | return Buffer.concat([buf, new Buffer(str, 'utf8')]); 711 | } 712 | 713 | /** 714 | * AMF0 Decode Long String 715 | * @param buf 716 | * @returns {{len: *, value: (*|string|String)}} 717 | */ 718 | function amf0decLongString(buf) { 719 | var sLen = buf.readUInt32BE(1); 720 | return { 721 | len: 5 + sLen, 722 | value: buf.toString('utf8', 5, 5 + sLen) 723 | } 724 | } 725 | 726 | /** 727 | * AMF0 Encode Long String 728 | * @param str 729 | * @returns {Buffer} 730 | */ 731 | function amf0encLongString(str) { 732 | var buf = new Buffer(5); 733 | buf.writeUInt8(0x0C, 0); 734 | buf.writeUInt32BE(str.length, 1); 735 | return Buffer.concat([buf, new Buffer(str, 'utf8')]); 736 | } 737 | 738 | /** 739 | * AMF0 Decode Array 740 | * @param buf 741 | * @returns {{len: *, value: ({}|*)}} 742 | */ 743 | function amf0decArray(buf) { 744 | // var count = buf.readUInt32BE(1); 745 | var obj = amf0decObject(buf.slice(4)); 746 | return { 747 | len: 5 + obj.len, 748 | value: obj.value 749 | } 750 | } 751 | 752 | /** 753 | * AMF0 Encode Array 754 | */ 755 | function amf0encArray(a) { 756 | var l = 0; 757 | if (a instanceof Array) l = a.length; 758 | else l = Object.keys(a).length; 759 | // console.log('Array encode', l, a); 760 | var buf = new Buffer(5); 761 | buf.writeUInt8(8, 0); 762 | buf.writeUInt32BE(l, 1); 763 | var data = amf0encObject(a); 764 | return Buffer.concat([buf, data.slice(1)]); 765 | } 766 | 767 | /** 768 | * AMF0 Encode Binary Array into binary Object 769 | * @param aData 770 | * @returns {Buffer} 771 | */ 772 | function amf0cnvArray2Object(aData) { 773 | var buf = new Buffer(1); 774 | buf.writeUInt8(0x3, 0); // Object id 775 | return Buffer.concat([buf, aData.slice(5)]); 776 | } 777 | 778 | /** 779 | * AMF0 Encode Binary Object into binary Array 780 | * @param oData 781 | * @returns {Buffer} 782 | */ 783 | function amf0cnvObject2Array(oData) { 784 | var buf = new Buffer(5); 785 | var o = amf0decObject(oData); 786 | var l = Object.keys(o).length; 787 | buf.writeUInt32BE(l, 1); 788 | return Buffer.concat([buf, oData.slice(1)]); 789 | } 790 | 791 | /** 792 | * AMF0 Decode XMLDoc 793 | * @param buf 794 | * @returns {{len: *, value: (*|string|String)}} 795 | */ 796 | function amf0decXmlDoc(buf) { 797 | var sLen = buf.readUInt16BE(1); 798 | return { 799 | len: 3 + sLen, 800 | value: buf.toString('utf8', 3, 3 + sLen) 801 | } 802 | } 803 | 804 | /** 805 | * AMF0 Encode XMLDoc 806 | * @param str 807 | * @returns {Buffer} 808 | */ 809 | function amf0encXmlDoc(str) { // Essentially it is the same as string 810 | var buf = new Buffer(3); 811 | buf.writeUInt8(0x0F, 0); 812 | buf.writeUInt16BE(str.length, 1); 813 | return Buffer.concat([buf, new Buffer(str, 'utf8')]); 814 | } 815 | 816 | /** 817 | * AMF0 Decode Strict Array 818 | * @param buf 819 | * @returns {{len: number, value: Array}} 820 | */ 821 | function amf0decSArray(buf) { 822 | var a = []; 823 | var len = 5; 824 | var ret; 825 | for (var count = buf.readUInt32BE(1); count; count--) { 826 | ret = amf0DecodeOne(buf.slice(len)); 827 | a.push(ret.value); 828 | len += ret.len; 829 | } 830 | return { 831 | len: len, 832 | value: amf0markSArray(a) 833 | } 834 | } 835 | 836 | /** 837 | * AMF0 Encode Strict Array 838 | * @param a Array 839 | */ 840 | function amf0encSArray(a) { 841 | // console.log('Do strict array!'); 842 | var buf = new Buffer(5); 843 | buf.writeUInt8(0x0A, 0); 844 | buf.writeUInt32BE(a.length, 1); 845 | var i; 846 | for (i = 0; i < a.length; i++) { 847 | buf = Buffer.concat([buf, amf0EncodeOne(a[i])]); 848 | } 849 | return buf; 850 | } 851 | 852 | function amf0markSArray(a) { 853 | Object.defineProperty(a, 'sarray', { 854 | value: true 855 | }); 856 | return a; 857 | } 858 | 859 | /** 860 | * AMF0 Decode Typed Object 861 | * @param buf 862 | * @returns {{len: number, value: ({}|*)}} 863 | */ 864 | function amf0decTypedObj(buf) { 865 | var className = amf0decString(buf); 866 | var obj = amf0decObject(buf.slice(className.len - 1)); 867 | obj.value.__className__ = className.value; 868 | return { 869 | len: className.len + obj.len - 1, 870 | value: obj.value 871 | } 872 | } 873 | 874 | /** 875 | * AMF0 Encode Typed Object 876 | */ 877 | function amf0encTypedObj() { 878 | throw new Error("Error: SArray encoding is not yet implemented!"); // TODO: Error 879 | } 880 | 881 | /** 882 | * Decode one value from the Buffer according to the applied rules 883 | * @param rules 884 | * @param buffer 885 | * @returns {*} 886 | */ 887 | function amfXDecodeOne(rules, buffer) { 888 | if (!rules[buffer.readUInt8(0)]) { 889 | // console.log('Unknown field', buffer.readUInt8(0)); 890 | throw new Error("Error: Unknown field"); 891 | } 892 | return rules[buffer.readUInt8(0)](buffer); 893 | } 894 | 895 | /** 896 | * Decode one AMF0 value 897 | * @param buffer 898 | * @returns {*} 899 | */ 900 | function amf0DecodeOne(buffer) { 901 | return amfXDecodeOne(amf0dRules, buffer); 902 | } 903 | 904 | /** 905 | * Decode one AMF3 value 906 | * @param buffer 907 | * @returns {*} 908 | */ 909 | function amf3DecodeOne(buffer) { 910 | return amfXDecodeOne(amf3dRules, buffer); 911 | } 912 | 913 | /** 914 | * Decode a whole buffer of AMF values according to rules and return in array 915 | * @param rules 916 | * @param buffer 917 | * @returns {Array} 918 | */ 919 | function amfXDecode(rules, buffer) { 920 | // We shall receive clean buffer and will respond with an array of values 921 | var resp = []; 922 | var res; 923 | for (var i = 0; i < buffer.length;) { 924 | res = amfXDecodeOne(rules, buffer.slice(i)); 925 | i += res.len; 926 | resp.push(res.value); // Add the response 927 | } 928 | return resp; 929 | } 930 | 931 | /** 932 | * Decode a buffer of AMF3 values 933 | * @param buffer 934 | * @returns {Array} 935 | */ 936 | function amf3Decode(buffer) { 937 | return amfXDecode(amf3dRules, buffer); 938 | } 939 | 940 | /** 941 | * Decode a buffer of AMF0 values 942 | * @param buffer 943 | * @returns {Array} 944 | */ 945 | function amf0Decode(buffer) { 946 | return amfXDecode(amf0dRules, buffer); 947 | } 948 | 949 | /** 950 | * Encode one AMF value according to rules 951 | * @param rules 952 | * @param o 953 | * @returns {*} 954 | */ 955 | function amfXEncodeOne(rules, o) { 956 | // console.log('amfXEncodeOne type',o,amfType(o),rules[amfType(o)]); 957 | var f = rules[amfType(o)]; 958 | if (f) return f(o); 959 | throw new Error('Unsupported type for encoding!'); 960 | } 961 | 962 | /** 963 | * Encode one AMF0 value 964 | * @param o 965 | * @returns {*} 966 | */ 967 | function amf0EncodeOne(o) { 968 | return amfXEncodeOne(amf0eRules, o); 969 | } 970 | 971 | /** 972 | * Encode one AMF3 value 973 | * @param o 974 | * @returns {*} 975 | */ 976 | function amf3EncodeOne(o) { 977 | return amfXEncodeOne(amf3eRules, o); 978 | } 979 | 980 | /** 981 | * Encode an array of values into a buffer 982 | * @param a 983 | * @returns {Buffer} 984 | */ 985 | function amf3Encode(a) { 986 | var buf = new Buffer(0); 987 | a.forEach(function (o) { 988 | buf = Buffer.concat([buf, amf3EncodeOne(o)]); 989 | }); 990 | return buf; 991 | } 992 | 993 | /** 994 | * Encode an array of values into a buffer 995 | * @param a 996 | * @returns {Buffer} 997 | */ 998 | function amf0Encode(a) { 999 | var buf = new Buffer(0); 1000 | a.forEach(function (o) { 1001 | buf = Buffer.concat([buf, amf0EncodeOne(o)]); 1002 | }); 1003 | return buf; 1004 | } 1005 | 1006 | 1007 | var rtmpCmdDecode = { 1008 | "_result": ["transId", "cmdObj", "info"], 1009 | "_error": ["transId", "cmdObj", "info", "streamId"], // Info / Streamid are optional 1010 | "@setDataFrame": ["method", "cmdObj"], 1011 | "onStatus": ["transId", "cmdObj", "info"], 1012 | "releaseStream": ["transId", "cmdObj", "streamId"], 1013 | "getStreamLength": ["transId", "cmdObj", "streamId"], 1014 | "getMovLen": ["transId", "cmdObj", "streamId"], 1015 | "FCPublish": ["transId", "cmdObj", "streamId"], 1016 | "FCUnpublish": ["transId", "cmdObj", "streamId"], 1017 | "onFCPublish": ["transId", "cmdObj", "info"], 1018 | "connect": ["transId", "cmdObj", "args"], 1019 | "call": ["transId", "cmdObj", "args"], 1020 | "createStream": ["transId", "cmdObj"], 1021 | "close": ["transId", "cmdObj"], 1022 | "play": ["transId", "cmdObj", "streamName", "start", "duration", "reset"], 1023 | "play2": ["transId", "cmdObj", "params"], 1024 | "deleteStream": ["transId", "cmdObj", "streamId"], 1025 | "receiveAudio": ["transId", "cmdObj", "bool"], 1026 | "receiveVideo": ["transId", "cmdObj", "bool"], 1027 | "publish": ["transId", "cmdObj", "streamName", "type"], 1028 | "seek": ["transId", "cmdObj", "ms"], 1029 | "pause": ["transId", "cmdObj", "pause", "ms"], 1030 | "|RtmpSampleAccess": ["bool1", "bool2"], 1031 | "onMetaData": ["cmdObj"] 1032 | }; 1033 | 1034 | /** 1035 | * Decode a command! 1036 | * @param dbuf 1037 | * @returns {{cmd: (*|string|String|*), value: *}} 1038 | */ 1039 | function decodeAMF0Cmd(dbuf) { 1040 | var buffer = dbuf; 1041 | var resp = {}; 1042 | 1043 | var cmd = amf0DecodeOne(buffer); 1044 | resp.cmd = cmd.value; 1045 | buffer = buffer.slice(cmd.len); 1046 | 1047 | if (rtmpCmdDecode[cmd.value]) { 1048 | rtmpCmdDecode[cmd.value].forEach(function (n) { 1049 | if (buffer.length > 0) { 1050 | var r = amf0DecodeOne(buffer); 1051 | buffer = buffer.slice(r.len); 1052 | resp[n] = r.value; 1053 | } 1054 | }); 1055 | } else { 1056 | // console.log('Unknown command', resp); 1057 | } 1058 | return resp 1059 | } 1060 | 1061 | /** 1062 | * Encode AMF0 Command 1063 | * @param opt 1064 | * @returns {*} 1065 | */ 1066 | function encodeAMF0Cmd(opt) { 1067 | var data = amf0EncodeOne(opt.cmd); 1068 | 1069 | if (rtmpCmdDecode[opt.cmd]) { 1070 | rtmpCmdDecode[opt.cmd].forEach(function (n) { 1071 | if (opt.hasOwnProperty(n)) 1072 | data = Buffer.concat([data, amf0EncodeOne(opt[n])]); 1073 | }); 1074 | } else { 1075 | // console.log('Unknown command', opt); 1076 | } 1077 | // console.log('Encoded as',data.toString('hex')); 1078 | return data 1079 | } 1080 | 1081 | /** 1082 | * 1083 | * @param dbuf 1084 | * @returns {{}} 1085 | */ 1086 | function decodeAMF3Cmd(dbuf) { 1087 | var buffer = dbuf; 1088 | var resp = {}; 1089 | 1090 | var cmd = amf3DecodeOne(buffer); 1091 | resp.cmd = cmd.value; 1092 | buffer = buffer.slice(cmd.len); 1093 | 1094 | if (rtmpCmdDecode[cmd.value]) { 1095 | rtmpCmdDecode[cmd.value].forEach(function (n) { 1096 | if (buffer.length > 0) { 1097 | var r = amf3DecodeOne(buffer); 1098 | buffer = buffer.slice(r.len); 1099 | resp[n] = r.value; 1100 | } 1101 | }); 1102 | } else { 1103 | // console.log('Unknown command', resp); 1104 | } 1105 | return resp 1106 | } 1107 | 1108 | /** 1109 | * Encode AMF3 Command 1110 | * @param opt 1111 | * @returns {*} 1112 | */ 1113 | function encodeAMF3Cmd(opt) { 1114 | var data = amf0EncodeOne(opt.cmd); 1115 | 1116 | if (rtmpCmdDecode[opt.cmd]) { 1117 | rtmpCmdDecode[opt.cmd].forEach(function (n) { 1118 | if (opt.hasOwnProperty(n)) 1119 | data = Buffer.concat([data, amf3EncodeOne(opt[n])]); 1120 | }); 1121 | } else { 1122 | // console.log('Unknown command', opt); 1123 | } 1124 | return data 1125 | } 1126 | 1127 | module.exports = { 1128 | decodeAmf3Cmd: decodeAMF3Cmd, 1129 | encodeAmf3Cmd: encodeAMF3Cmd, 1130 | decodeAmf0Cmd: decodeAMF0Cmd, 1131 | encodeAmf0Cmd: encodeAMF0Cmd 1132 | }; -------------------------------------------------------------------------------- /buffer-pool.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream'); 2 | const Readable = stream.Readable; 3 | 4 | class BufferPool extends Readable { 5 | 6 | constructor(options) { 7 | super(options); 8 | } 9 | 10 | init(gFun) { 11 | this.totalBufferLength = 0; 12 | this.needBufferLength = 0; 13 | this.gFun = gFun; 14 | this.gFun.next(); 15 | } 16 | 17 | push(buf) { 18 | super.push(buf); 19 | this.totalBufferLength += buf.length; 20 | if (this.needBufferLength > 0 && this.needBufferLength <= this.totalBufferLength) { 21 | this.gFun.next(); 22 | } 23 | } 24 | 25 | read(size) { 26 | this.totalBufferLength -= size; 27 | return super.read(size); 28 | } 29 | 30 | need(size) { 31 | const ret = this.totalBufferLength < size; 32 | if (ret) { 33 | this.needBufferLength = size; 34 | } 35 | return ret; 36 | } 37 | } 38 | 39 | module.exports = BufferPool; -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events'); 2 | var NMRtmpHandshake = require('./handshake'); 3 | var AMF = require('./amf'); 4 | var BufferPool = require('./buffer-pool'); 5 | 6 | var aac_sample_rates = [ 7 | 96000, 88200, 64000, 48000, 8 | 44100, 32000, 24000, 22050, 9 | 16000, 12000, 11025, 8000, 10 | 7350, 0, 0, 0 11 | ]; 12 | 13 | 14 | class NMRtmpConn extends EventEmitter { 15 | 16 | constructor(id, socket, conns, producers) { 17 | super(); 18 | 19 | this.id = id; 20 | this.socket = socket; 21 | this.conns = conns; 22 | this.producers = producers; 23 | this.rtmpStatus = 0; 24 | this.isStarting = false; 25 | this.inChunkSize = 128; 26 | this.outChunkSize = 128; 27 | this.previousChunkMessage = {}; 28 | this.connectCmdObj = null; 29 | this.isFirstAudioReceived = true; 30 | this.isFirstVideoReceived = true; 31 | this.lastAudioTimestamp = 0; 32 | this.lastVideoTimestamp = 0; 33 | 34 | this.playStreamName = ''; 35 | this.publishStreamName = ''; 36 | 37 | this.bp = new BufferPool(); 38 | this.bp.on('error', () => {}); 39 | 40 | this.parser = parseRtmpMessage(this); 41 | 42 | this.codec = { 43 | width: 0, 44 | height: 0, 45 | duration: 0, 46 | framerate: 0, 47 | videodatarate: 0, 48 | audiosamplerate: 0, 49 | audiosamplesize: 0, 50 | audiodatarate: 0, 51 | spsLen: 0, 52 | sps: null, 53 | ppsLen: 0, 54 | pps: null 55 | }; 56 | 57 | this.sendBufferQueue = []; 58 | } 59 | 60 | generateSessionID(socket, conns, producers) { 61 | this.isStarting = true; 62 | this.bp.init(this.parser); 63 | } 64 | 65 | create(socket, conns, producers) { 66 | this.isStarting = true; 67 | this.bp.init(this.parser); 68 | }; 69 | 70 | run() { 71 | this.isStarting = true; 72 | this.bp.init(this.parser); 73 | } 74 | 75 | stop() { 76 | this.isStarting = false; 77 | if (this.publishStreamName != '') { 78 | // console.info("Send Stream EOF to publiser's consumers. Stream name " + this.publishStreamName); 79 | for (var id in this.consumers) { 80 | this.consumers[id].sendStreamEOF(); 81 | } 82 | // console.info("Delete publiser from producers. Stream name " + this.publishStreamName); 83 | delete this.producers[this.publishStreamName]; 84 | } else if (this.playStreamName != '') { 85 | if (this.producers[this.playStreamName]) { 86 | // console.info("Delete player from consumers. Stream name " + this.playStreamName); 87 | delete this.producers[this.playStreamName].consumers[this.id]; 88 | } 89 | } 90 | // console.info("Delete client from conns. ID: " + this.id); 91 | delete this.conns[this.id]; 92 | 93 | this.emit('stop'); 94 | } 95 | 96 | getRealChunkSize(rtmpBodySize, chunkSize) { 97 | var nn = rtmpBodySize + parseInt(rtmpBodySize / chunkSize); 98 | if (rtmpBodySize % chunkSize) { 99 | return nn; 100 | } else { 101 | return nn - 1; 102 | } 103 | } 104 | 105 | createRtmpMessage(rtmpHeader, rtmpBody) { 106 | var formatTypeID = 0; 107 | var rtmpBodySize = rtmpBody.length; 108 | if (rtmpHeader.chunkStreamID == null) { 109 | console.warn("[rtmp] warning: createRtmpMessage(): chunkStreamID is not set for RTMP" + 110 | " message"); 111 | } 112 | if (rtmpHeader.timestamp == null) { 113 | console.warn("[rtmp] warning: createRtmpMessage(): timestamp is not set for RTMP message"); 114 | } 115 | if (rtmpHeader.messageTypeID == null) { 116 | console.warn("[rtmp] warning: createRtmpMessage(): messageTypeID is not set for RTMP" + 117 | " message"); 118 | } 119 | if (rtmpHeader.messageStreamID == null) { 120 | console.warn("[rtmp] warning: createRtmpMessage(): messageStreamID is not set for RTMP" + 121 | " message"); 122 | } 123 | 124 | var useExtendedTimestamp = false; 125 | var timestamp; 126 | 127 | if (rtmpHeader.timestamp >= 0xffffff) { 128 | useExtendedTimestamp = true; 129 | timestamp = [0xff, 0xff, 0xff]; 130 | } else { 131 | timestamp = [(rtmpHeader.timestamp >> 16) & 0xff, (rtmpHeader.timestamp >> 8) & 0xff, rtmpHeader.timestamp & 0xff]; 132 | } 133 | 134 | var bufs = new Buffer([(formatTypeID << 6) | rtmpHeader.chunkStreamID, timestamp[0], timestamp[1], timestamp[2], (rtmpBodySize >> 16) & 0xff, (rtmpBodySize >> 8) & 0xff, rtmpBodySize & 0xff, rtmpHeader.messageTypeID, rtmpHeader.messageStreamID & 0xff, (rtmpHeader.messageStreamID >>> 8) & 0xff, (rtmpHeader.messageStreamID >>> 16) & 0xff, (rtmpHeader.messageStreamID >>> 24) & 0xff]); 135 | 136 | if (useExtendedTimestamp) { 137 | var extendedTimestamp = new Buffer([(rtmpHeader.timestamp >> 24) & 0xff, (rtmpHeader.timestamp >> 16) & 0xff, (rtmpHeader.timestamp >> 8) & 0xff, rtmpHeader.timestamp & 0xff]); 138 | bufs = Buffer.concat([bufs, extendedTimestamp]); 139 | } 140 | 141 | 142 | var rtmpBodyPos = 0; 143 | var chunkBodySize = this.getRealChunkSize(rtmpBodySize, this.outChunkSize); 144 | var chunkBody = []; 145 | var type3Header = new Buffer([(3 << 6) | rtmpHeader.chunkStreamID]); 146 | 147 | do { 148 | if (rtmpBodySize > this.outChunkSize) { 149 | chunkBody.push(rtmpBody.slice(rtmpBodyPos, rtmpBodyPos + this.outChunkSize)); 150 | rtmpBodySize -= this.outChunkSize 151 | rtmpBodyPos += this.outChunkSize; 152 | chunkBody.push(type3Header); 153 | } else { 154 | chunkBody.push(rtmpBody.slice(rtmpBodyPos, rtmpBodyPos + rtmpBodySize)); 155 | rtmpBodySize -= rtmpBodySize; 156 | rtmpBodyPos += rtmpBodySize; 157 | } 158 | 159 | } while (rtmpBodySize > 0) 160 | var chunkBodyBuffer = Buffer.concat(chunkBody); 161 | bufs = Buffer.concat([bufs, chunkBodyBuffer]); 162 | return bufs; 163 | } 164 | 165 | handleRtmpMessage(rtmpHeader, rtmpBody) { 166 | switch (rtmpHeader.messageTypeID) { 167 | case 0x01: 168 | this.inChunkSize = rtmpBody.readUInt32BE(0); 169 | // console.log('[rtmp handleRtmpMessage] Set In chunkSize:' + this.inChunkSize); 170 | break; 171 | 172 | case 0x04: 173 | var userControlMessage = this.parseUserControlMessage(rtmpBody); 174 | if (userControlMessage.eventType === 3) { 175 | var streamID = (userControlMessage.eventData[0] << 24) + (userControlMessage.eventData[1] << 16) + (userControlMessage.eventData[2] << 8) + userControlMessage.eventData[3]; 176 | var bufferLength = (userControlMessage.eventData[4] << 24) + (userControlMessage.eventData[5] << 16) + (userControlMessage.eventData[6] << 8) + userControlMessage.eventData[7]; 177 | // console.log("[rtmp handleRtmpMessage] SetBufferLength: streamID=" + streamID + 178 | // "bufferLength=" + bufferLength); 179 | } else if (userControlMessage.eventType === 7) { 180 | var timestamp = (userControlMessage.eventData[0] << 24) + (userControlMessage.eventData[1] << 16) + (userControlMessage.eventData[2] << 8) + userControlMessage.eventData[3]; 181 | // console.log("[rtmp handleRtmpMessage] PingResponse: timestamp=" + timestamp); 182 | } else { 183 | // console.log("[rtmp handleRtmpMessage] User Control Message"); 184 | console.log(userControlMessage); 185 | } 186 | break; 187 | case 0x08: 188 | //Audio Data 189 | // console.log(rtmpHeader); 190 | // console.log('Audio Data: '+rtmpBody.length); 191 | this.parseAudioMessage(rtmpHeader, rtmpBody); 192 | break; 193 | case 0x09: 194 | //Video Data 195 | // console.log(rtmpHeader); 196 | // console.log('Video Data: '+rtmpBody.length); 197 | this.parseVideoMessage(rtmpHeader, rtmpBody); 198 | break; 199 | case 0x0F: 200 | //AMF3 Data 201 | var cmd = AMF.decodeAmf0Cmd(rtmpBody.slice(1)); 202 | this.handleAMFDataMessage(cmd, this); 203 | break; 204 | case 0x11: 205 | //AMF3 Command 206 | var cmd = AMF.decodeAmf0Cmd(rtmpBody.slice(1)); 207 | this.handleAMFCommandMessage(cmd, this); 208 | break; 209 | case 0x12: 210 | //AMF0 Data 211 | var cmd = AMF.decodeAmf0Cmd(rtmpBody); 212 | this.handleAMFDataMessage(cmd, this); 213 | break; 214 | case 0x14: 215 | //AMF0 Command 216 | var cmd = AMF.decodeAmf0Cmd(rtmpBody); 217 | this.handleAMFCommandMessage(cmd, this); 218 | break; 219 | 220 | } 221 | } 222 | 223 | handleAMFDataMessage(cmd) { 224 | 225 | switch (cmd.cmd) { 226 | case '@setDataFrame': 227 | this.receiveSetDataFrame(cmd.method, cmd.cmdObj); 228 | break; 229 | default: 230 | // console.warn("[rtmp:receive] unknown AMF data: " + dataMessage.objects[0].value); 231 | } 232 | } 233 | 234 | handleAMFCommandMessage(cmd) { 235 | this.emit('command', cmd); 236 | 237 | switch (cmd.cmd) { 238 | case 'connect': { 239 | // console.log('rtmp connect app: ' + this.connectCmdObj.app); 240 | 241 | this.connectCmdObj = cmd.cmdObj; 242 | this.app = this.connectCmdObj.app; 243 | this.objectEncoding = cmd.cmdObj.objectEncoding != null ? cmd.cmdObj.objectEncoding : 0; 244 | this.windowACK(5000000); 245 | this.setPeerBandwidth(5000000, 2); 246 | this.outChunkSize = 4096; 247 | this.setChunkSize(this.outChunkSize); 248 | 249 | this.emit('connect', cmd); 250 | this.respondConnect(); 251 | break; 252 | } 253 | case 'createStream': 254 | this.respondCreateStream(cmd); 255 | break; 256 | case 'play': { 257 | // console.log('rtmp play stream: ' + cmd.streamName); 258 | 259 | const streamName = this.connectCmdObj.app + '/' + cmd.streamName; 260 | this.playStreamName = streamName; 261 | 262 | this.emit('play', cmd); 263 | this.respondPlay(); 264 | 265 | 266 | if (!this.producers[streamName]) { 267 | // console.info("[rtmp streamPlay] There's no stream named " + streamName + " is" + 268 | // " publushing! Create a producer."); 269 | this.producers[streamName] = { 270 | id: null, 271 | consumers: {} 272 | }; 273 | } else if (this.producers[streamName].id == null) { 274 | // console.info("[rtmp streamPlay] There's no stream named " + streamName + " is " + 275 | // "publushing! But the producer is created."); 276 | } else { 277 | // console.info("[rtmp streamPlay] There's a stream named " + streamName + " is " + 278 | // "publushing! id=" + this.producers[streamName].id); 279 | } 280 | this.producers[streamName].consumers[this.id] = this; 281 | this.startPlay(); 282 | break; 283 | } 284 | case 'closeStream': 285 | this.closeStream(); 286 | break; 287 | case 'deleteStream': 288 | this.deleteStream(); 289 | break; 290 | case 'pause': 291 | // console.log('pause received'); 292 | this.pauseOrUnpauseStream(); 293 | break; 294 | case 'releaseStream': 295 | this.respondReleaseStream(); 296 | break; 297 | case 'FCPublish': 298 | this.respondFCPublish(); 299 | break; 300 | case 'publish': { 301 | // console.log('rtmp publish stream: ' + cmd.streamName); 302 | 303 | const streamName = this.connectCmdObj.app + '/' + cmd.streamName; 304 | if (!this.producers[streamName]) { 305 | this.producers[streamName] = { 306 | id: this.id, 307 | consumers: {} 308 | }; 309 | } else if (this.producers[streamName].id == null) { 310 | this.producers[streamName].id = this.id; 311 | } else { 312 | // console.warn("[rtmp publish] Already has a stream named " + streamName); 313 | this.respondPublishError(); 314 | return; 315 | } 316 | this.publishStreamName = streamName; 317 | this.producer = this.producers[streamName]; 318 | this.consumers = this.producer.consumers; 319 | 320 | this.emit('publish', cmd); 321 | this.respondPublish(); 322 | break; 323 | } 324 | case 'FCUnpublish': 325 | this.respondFCUnpublish(); 326 | break; 327 | default: 328 | // console.warn("[rtmp:receive] unknown AMF command: " + cmd.cmd); 329 | return; 330 | } 331 | } 332 | 333 | windowACK(size) { 334 | var rtmpBuffer = new Buffer('02000000000004050000000000000000', 'hex'); 335 | rtmpBuffer.writeUInt32BE(size, 12); 336 | // console.log('windowACK: '+rtmpBuffer.hex()); 337 | this.socket.write(rtmpBuffer); 338 | } 339 | 340 | setPeerBandwidth(size, type) { 341 | var rtmpBuffer = new Buffer('0200000000000506000000000000000000', 'hex'); 342 | rtmpBuffer.writeUInt32BE(size, 12); 343 | rtmpBuffer[16] = type; 344 | // console.log('setPeerBandwidth: '+rtmpBuffer.hex()); 345 | this.socket.write(rtmpBuffer); 346 | } 347 | 348 | setChunkSize(size) { 349 | var rtmpBuffer = new Buffer('02000000000004010000000000000000', 'hex'); 350 | rtmpBuffer.writeUInt32BE(size, 12); 351 | // console.log('setChunkSize: '+rtmpBuffer.hex()); 352 | this.socket.write(rtmpBuffer); 353 | } 354 | 355 | respondConnect() { 356 | var rtmpHeader = { 357 | chunkStreamID: 3, 358 | timestamp: 0, 359 | messageTypeID: 0x14, 360 | messageStreamID: 0 361 | }; 362 | var opt = { 363 | cmd: '_result', 364 | transId: 1, 365 | cmdObj: { 366 | fmsVer: 'FMS/3,0,1,123', 367 | capabilities: 31 368 | }, 369 | info: { 370 | level: 'status', 371 | code: 'NetConnection.Connect.Success', 372 | description: 'Connection succeeded.', 373 | objectEncoding: this.objectEncoding 374 | } 375 | }; 376 | var rtmpBody = AMF.encodeAmf0Cmd(opt); 377 | var rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody); 378 | this.socket.write(rtmpMessage); 379 | } 380 | 381 | respondRejectConnect() { 382 | var rtmpHeader = { 383 | chunkStreamID: 3, 384 | timestamp: 0, 385 | messageTypeID: 0x14, 386 | messageStreamID: 0 387 | }; 388 | 389 | var opt = { 390 | cmd: '_error', 391 | transId: 1, 392 | cmdObj: { 393 | fmsVer: 'FMS/3,0,1,123', 394 | capabilities: 31 395 | }, 396 | info: { 397 | level: 'error', 398 | code: 'NetConnection.Connect.Rejected', 399 | description: 'Connection failed.', 400 | objectEncoding: this.objectEncoding 401 | } 402 | }; 403 | var rtmpBody = AMF.encodeAmf0Cmd(opt); 404 | var rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody); 405 | this.socket.write(rtmpMessage); 406 | } 407 | 408 | respondCreateStream(cmd) { 409 | // console.log(cmd); 410 | var rtmpHeader = { 411 | chunkStreamID: 3, 412 | timestamp: 0, 413 | messageTypeID: 0x14, 414 | messageStreamID: 0 415 | }; 416 | var opt = { 417 | cmd: "_result", 418 | transId: cmd.transId, 419 | cmdObj: null, 420 | info: 1 421 | 422 | }; 423 | var rtmpBody = AMF.encodeAmf0Cmd(opt); 424 | var rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody); 425 | this.socket.write(rtmpMessage); 426 | 427 | } 428 | 429 | respondPlay() { 430 | var rtmpHeader = { 431 | chunkStreamID: 3, 432 | timestamp: 0, 433 | messageTypeID: 0x14, 434 | messageStreamID: 1 435 | }; 436 | var opt = { 437 | cmd: 'onStatus', 438 | transId: 0, 439 | cmdObj: null, 440 | info: { 441 | level: 'status', 442 | code: 'NetStream.Play.Start', 443 | description: 'Start live' 444 | } 445 | }; 446 | var rtmpBody = AMF.encodeAmf0Cmd(opt); 447 | var rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody); 448 | this.socket.write(rtmpMessage); 449 | 450 | var rtmpHeader = { 451 | chunkStreamID: 5, 452 | timestamp: 0, 453 | messageTypeID: 0x12, 454 | messageStreamID: 1 455 | }; 456 | var opt = { 457 | cmd: '|RtmpSampleAccess', 458 | bool1: true, 459 | bool2: true 460 | }; 461 | 462 | var rtmpBody = AMF.encodeAmf0Cmd(opt); 463 | var rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody); 464 | this.socket.write(rtmpMessage); 465 | } 466 | 467 | startPlay() { 468 | var producer = this.producers[this.playStreamName]; 469 | if (producer.metaData == null || producer.cacheAudioSequenceBuffer == null || producer.cacheVideoSequenceBuffer == null) return; 470 | 471 | var rtmpHeader = { 472 | chunkStreamID: 5, 473 | timestamp: 0, 474 | messageTypeID: 0x12, 475 | messageStreamID: 1 476 | }; 477 | 478 | var opt = { 479 | cmd: 'onMetaData', 480 | cmdObj: producer.metaData 481 | }; 482 | 483 | var rtmpBody = AMF.encodeAmf0Cmd(opt); 484 | var metaDataRtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody); 485 | 486 | 487 | var rtmpHeader = { 488 | chunkStreamID: 4, 489 | timestamp: 0, 490 | messageTypeID: 0x08, 491 | messageStreamID: 1 492 | }; 493 | var audioSequenceRtmpMessage = this.createRtmpMessage(rtmpHeader, producer.cacheAudioSequenceBuffer); 494 | 495 | 496 | var rtmpHeader = { 497 | chunkStreamID: 4, 498 | timestamp: 0, 499 | messageTypeID: 0x09, 500 | messageStreamID: 1 501 | }; 502 | var videoSequenceRtmpMessage = this.createRtmpMessage(rtmpHeader, producer.cacheVideoSequenceBuffer); 503 | 504 | var beginRtmpMessage = new Buffer("020000000000060400000000000000000001", 'hex'); 505 | this.sendBufferQueue.push(beginRtmpMessage); 506 | this.sendBufferQueue.push(metaDataRtmpMessage); 507 | this.sendBufferQueue.push(audioSequenceRtmpMessage); 508 | this.sendBufferQueue.push(videoSequenceRtmpMessage); 509 | this.sendRtmpMessage(this); 510 | } 511 | 512 | closeStream() { 513 | 514 | } 515 | 516 | deleteStream() { 517 | 518 | } 519 | 520 | pauseOrUnpauseStream() { 521 | 522 | } 523 | 524 | respondReleaseStream() { 525 | 526 | } 527 | 528 | respondFCPublish() { 529 | 530 | } 531 | 532 | respondPublish() { 533 | var rtmpHeader = { 534 | chunkStreamID: 5, 535 | timestamp: 0, 536 | messageTypeID: 0x14, 537 | messageStreamID: 1 538 | }; 539 | var opt = { 540 | cmd: 'onStatus', 541 | transId: 0, 542 | cmdObj: null, 543 | info: { 544 | level: 'status', 545 | code: 'NetStream.Publish.Start', 546 | description: 'Start publishing' 547 | } 548 | }; 549 | var rtmpBody = AMF.encodeAmf0Cmd(opt); 550 | var rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody); 551 | this.socket.write(rtmpMessage); 552 | 553 | } 554 | 555 | respondPublishError() { 556 | var rtmpHeader = { 557 | chunkStreamID: 5, 558 | timestamp: 0, 559 | messageTypeID: 0x14, 560 | messageStreamID: 1 561 | }; 562 | var opt = { 563 | cmd: 'onStatus', 564 | transId: 0, 565 | cmdObj: null, 566 | info: { 567 | level: 'error', 568 | code: 'NetStream.Publish.BadName', 569 | description: 'Already publishing' 570 | } 571 | }; 572 | var rtmpBody = AMF.encodeAmf0Cmd(opt); 573 | var rtmpMessage = this.createRtmpMessage(rtmpHeader, rtmpBody); 574 | this.socket.write(rtmpMessage); 575 | } 576 | 577 | respondFCUnpublish() { 578 | 579 | } 580 | 581 | receiveSetDataFrame(method, obj) { 582 | // console.log('[receiveSetDataFrame] method:' + method); 583 | 584 | if (method == 'onMetaData') { 585 | this.producers[this.publishStreamName].metaData = obj; 586 | } 587 | 588 | } 589 | 590 | parseUserControlMessage(buf) { 591 | var eventData, eventType; 592 | var eventType = (buf[0] << 8) + buf[1]; 593 | var eventData = buf.slice(2); 594 | var message = { 595 | eventType: eventType, 596 | eventData: eventData 597 | }; 598 | if (eventType === 3) { 599 | message.streamID = (eventData[0] << 24) + (eventData[1] << 16) + (eventData[2] << 8) + eventData[3]; 600 | message.bufferLength = (eventData[4] << 24) + (eventData[5] << 16) + (eventData[6] << 8) + eventData[7]; 601 | } 602 | return message; 603 | } 604 | 605 | 606 | parseAudioMessage(rtmpHeader, rtmpBody) { 607 | 608 | if (this.isFirstAudioReceived) { 609 | var sound_format = rtmpBody[0]; 610 | var sound_type = sound_format & 0x01; 611 | var sound_size = (sound_format >> 1) & 0x01; 612 | var sound_rate = (sound_format >> 2) & 0x03; 613 | sound_format = (sound_format >> 4) & 0x0f; 614 | if (sound_format != 10) { 615 | this.emit('error', new Error(`Only support audio aac codec. actual=${sound_format}`)); 616 | return -1; 617 | } 618 | // console.info(this.id + " Parse AudioTagHeader sound_format=" + sound_format + 619 | // "sound_type=" + sound_type + " sound_size=" + sound_size + " sound_rate=" + sound_rate); 620 | var aac_packet_type = rtmpBody[1]; 621 | if (aac_packet_type == 0) { 622 | //AudioSpecificConfig 623 | // only need to decode 2bytes: 624 | // audioObjectType, aac_profile, 5bits. 625 | // samplingFrequencyIndex, aac_sample_rate, 4bits. 626 | // channelConfiguration, aac_channels, 4bits 627 | this.codec.aac_profile = rtmpBody[2]; 628 | this.codec.aac_sample_rate = rtmpBody[3]; 629 | 630 | this.codec.aac_channels = (this.codec.aac_sample_rate >> 3) & 0x0f; 631 | this.codec.aac_sample_rate = ((this.codec.aac_profile << 1) & 0x0e) | ((this.codec.aac_sample_rate >> 7) & 0x01); 632 | this.codec.aac_profile = (this.codec.aac_profile >> 3) & 0x1f; 633 | this.codec.audiosamplerate = aac_sample_rates[this.codec.aac_sample_rate]; 634 | if (this.codec.aac_profile == 0 || this.codec.aac_profile == 0x1f) { 635 | this.emit('error', new Error('Parse audio aac sequence header failed,' + 636 | ` adts object=${this.codec.aac_profile} invalid`)); 637 | return -1; 638 | } 639 | this.codec.aac_profile--; 640 | // console.info("Parse audio aac sequence header success! "); 641 | // console.info(this.codec); 642 | this.isFirstAudioReceived = false; 643 | this.producer.cacheAudioSequenceBuffer = new Buffer(rtmpBody); 644 | 645 | for (var id in this.consumers) { 646 | this.consumers[id].startPlay(); 647 | } 648 | } 649 | 650 | } else { 651 | var sendRtmpHeader = { 652 | chunkStreamID: 4, 653 | timestamp: rtmpHeader.timestamp, 654 | messageTypeID: 0x08, 655 | messageStreamID: 1 656 | }; 657 | var rtmpMessage = this.createRtmpMessage(sendRtmpHeader, rtmpBody); 658 | 659 | for (var id in this.consumers) { 660 | this.consumers[id].sendBufferQueue.push(rtmpMessage); 661 | } 662 | /* 663 | var frame_length = rtmpBody.length - 2 + 7; 664 | var audioBuffer = new Buffer(frame_length); 665 | adts_header.copy(audioBuffer); 666 | audioBuffer[2] = (this.codec.aac_profile << 6) & 0xc0; 667 | // sampling_frequency_index 4bits 668 | audioBuffer[2] |= (this.codec.aac_sample_rate << 2) & 0x3c; 669 | // channel_configuration 3bits 670 | audioBuffer[2] |= (this.codec.aac_channels >> 2) & 0x01; 671 | audioBuffer[3] = (this.codec.aac_channels << 6) & 0xc0; 672 | // frame_length 13bits 673 | audioBuffer[3] |= (frame_length >> 11) & 0x03; 674 | audioBuffer[4] = (frame_length >> 3) & 0xff; 675 | audioBuffer[5] = ((frame_length << 5) & 0xe0); 676 | // adts_buffer_fullness; //11bits 677 | audioBuffer[5] |= 0x1f; 678 | console.log(adts_header.hex()); 679 | 680 | rtmpBody.copy(audioBuffer, 7, 2, rtmpBody.length - 2); 681 | return audioBuffer; 682 | */ 683 | } 684 | } 685 | 686 | 687 | parseVideoMessage(rtmpHeader, rtmpBody) { 688 | var index = 0; 689 | var frame_type = rtmpBody[0]; 690 | var codec_id = frame_type & 0x0f; 691 | frame_type = (frame_type >> 4) & 0x0f; 692 | // only support h.264/avc 693 | if (codec_id != 7) { 694 | this.emit('error', new Error(`Only support video h.264/avc codec. actual=${codec_id}`)); 695 | return -1; 696 | } 697 | var avc_packet_type = rtmpBody[1]; 698 | var composition_time = rtmpBody.readIntBE(2, 3); 699 | // printf("v composition_time %d\n",composition_time); 700 | 701 | if (avc_packet_type == 0) { 702 | if (this.isFirstVideoReceived) { 703 | //AVC sequence header 704 | var configurationVersion = rtmpBody[5]; 705 | this.codec.avc_profile = rtmpBody[6]; 706 | var profile_compatibility = rtmpBody[7]; 707 | this.codec.avc_level = rtmpBody[8]; 708 | var lengthSizeMinusOne = rtmpBody[9]; 709 | lengthSizeMinusOne &= 0x03; 710 | this.codec.NAL_unit_length = lengthSizeMinusOne; 711 | 712 | // sps 713 | var numOfSequenceParameterSets = rtmpBody[10]; 714 | numOfSequenceParameterSets &= 0x1f; 715 | 716 | if (numOfSequenceParameterSets != 1) { 717 | this.emit('error', new Error('Decode video avc sequenc header sps failed')); 718 | return -1; 719 | } 720 | 721 | this.codec.spsLen = rtmpBody.readUInt16BE(11); 722 | 723 | index = 11 + 2; 724 | if (this.codec.spsLen > 0) { 725 | this.codec.sps = new Buffer(this.codec.spsLen); 726 | rtmpBody.copy(this.codec.sps, 0, 13, 13 + this.codec.spsLen); 727 | } 728 | // pps 729 | index += this.codec.spsLen; 730 | var numOfPictureParameterSets = rtmpBody[index]; 731 | numOfPictureParameterSets &= 0x1f; 732 | if (numOfPictureParameterSets != 1) { 733 | this.emit('error', new Error('Decode video avc sequenc header pps failed.')); 734 | return -1; 735 | } 736 | 737 | index++; 738 | this.codec.ppsLen = rtmpBody.readUInt16BE(index); 739 | index += 2; 740 | if (this.codec.ppsLen > 0) { 741 | this.codec.pps = new Buffer(this.codec.ppsLen); 742 | rtmpBody.copy(this.codec.pps, 0, index, index + this.codec.ppsLen); 743 | } 744 | this.isFirstVideoReceived = false; 745 | 746 | // console.info("Parse video avc sequence header success! "); 747 | // console.info(this.codec); 748 | // console.info('sps: ' + this.codec.sps.hex()); 749 | // console.info('pps: ' + this.codec.pps.hex()); 750 | // 751 | // 752 | this.producer.cacheVideoSequenceBuffer = new Buffer(rtmpBody); 753 | for (var id in this.consumers) { 754 | this.consumers[id].startPlay(); 755 | } 756 | } 757 | } else if (avc_packet_type == 1) { 758 | var sendRtmpHeader = { 759 | chunkStreamID: 4, 760 | timestamp: rtmpHeader.timestamp, 761 | messageTypeID: 0x09, 762 | messageStreamID: 1 763 | }; 764 | var rtmpMessage = this.createRtmpMessage(sendRtmpHeader, rtmpBody); 765 | 766 | for (var id in this.consumers) { 767 | this.consumers[id].sendBufferQueue.push(rtmpMessage); 768 | } 769 | /* 770 | //AVC NALU 771 | var NALUnitLength = 0; 772 | if (this.codec.NAL_unit_length == 3) { 773 | NALUnitLength = rtmpBody.readUInt32BE(5); 774 | } else if (this.codec.NAL_unit_length == 2) { 775 | NALUnitLength = ReadUInt24BE(rtmpBody, 5); 776 | } else if (this.codec.NAL_unit_length == 1) { 777 | NALUnitLength = rtmpBody.readUInt16BE(5); 778 | } else { 779 | NALUnitLength = rtmpBody.readInt8(5); 780 | } 781 | 782 | var videoBufferLen = 0; 783 | var videoBuffer = null; 784 | if (frame_type == 1) { 785 | videoBufferLen = 4 + this.codec.spsLen + 4 + this.codec.ppsLen + 4 + NALUnitLength; 786 | videoBuffer = new Buffer(videoBufferLen); 787 | NAL_HEADER.copy(videoBuffer); 788 | this.codec.sps.copy(videoBuffer, 4); 789 | NAL_HEADER.copy(videoBuffer, 4 + this.codec.spsLen); 790 | this.codec.pps.copy(videoBuffer, 4 + this.codec.spsLen + 4); 791 | NAL_HEADER.copy(videoBuffer, 4 + this.codec.spsLen + 4 + this.codec.ppsLen); 792 | rtmpBody.copy(videoBuffer, 4 + this.codec.spsLen + 4 + this.codec.ppsLen + 4, 9, NALUnitLength); 793 | } else { 794 | NAL_HEADER.copy(videoBuffer); 795 | rtmpBody.copy(videoBuffer, 4, 9, NALUnitLength); 796 | } 797 | return videoBuffer; 798 | */ 799 | } else { 800 | //AVC end of sequence (lower level NALU sequence ender is not required or supported) 801 | } 802 | } 803 | 804 | sendStreamEOF() { 805 | var rtmpBuffer = new Buffer("020000000000060400000000000100000001", 'hex'); 806 | this.socket.write(rtmpBuffer); 807 | } 808 | 809 | sendRtmpMessage(self) { 810 | if (!self.isStarting) return; 811 | var len = self.sendBufferQueue.length; 812 | for (var i = 0; i < len; i++) { 813 | self.socket.write(self.sendBufferQueue.shift()); 814 | } 815 | ; 816 | setTimeout(self.sendRtmpMessage, 100, self); 817 | } 818 | } 819 | 820 | module.exports = NMRtmpConn; 821 | 822 | 823 | /* local helpers */ 824 | 825 | function *parseRtmpMessage(self) { 826 | // console.log("rtmp handshake [start]"); 827 | if (self.bp.need(1537)) { 828 | yield; 829 | } 830 | var c0c1 = self.bp.read(1537); 831 | var s0s1s2 = NMRtmpHandshake.generateS0S1S2(c0c1); 832 | self.socket.write(s0s1s2); 833 | if (self.bp.need(1536)) { 834 | yield; 835 | } 836 | var c2 = self.bp.read(1536); 837 | // console.log("rtmp handshake [ok]"); 838 | 839 | while (self.isStarting) { 840 | var message = {}; 841 | var chunkMessageHeader = null; 842 | var previousChunk = null; 843 | var pos = 0; 844 | if (self.bp.need(1)) { 845 | yield; 846 | } 847 | var chunkBasicHeader = self.bp.read(1); 848 | message.formatType = chunkBasicHeader[0] >> 6; 849 | message.chunkStreamID = chunkBasicHeader[0] & 0x3F; 850 | if (message.chunkStreamID == 0) { 851 | if (self.bp.need(1)) { 852 | yield; 853 | } 854 | var exStreamID = self.bp.read(1); 855 | message.chunkStreamID = exStreamID[0] + 64; 856 | } else if (message.chunkStreamID == 1) { 857 | if (self.bp.need(2)) { 858 | yield; 859 | } 860 | var exStreamID = self.bp.read(2); 861 | message.chunkStreamID = (exStreamID[0] << 8) + exStreamID[1] + 64; 862 | } 863 | 864 | if (message.formatType == 0) { 865 | // Type 0 (11 bytes) 866 | if (self.bp.need(11)) { 867 | yield; 868 | } 869 | chunkMessageHeader = self.bp.read(11); 870 | message.timestamp = chunkMessageHeader.readIntBE(0, 3); 871 | message.timestampDelta = 0; 872 | message.messageLength = chunkMessageHeader.readIntBE(3, 3); 873 | message.messageTypeID = chunkMessageHeader[6]; 874 | message.messageStreamID = chunkMessageHeader.readInt32LE(7); 875 | } else if (message.formatType == 1) { 876 | // Type 1 (7 bytes) 877 | if (self.bp.need(7)) { 878 | yield; 879 | } 880 | chunkMessageHeader = self.bp.read(7); 881 | message.timestampDelta = chunkMessageHeader.readIntBE(0, 3); 882 | message.messageLength = chunkMessageHeader.readIntBE(3, 3); 883 | message.messageTypeID = chunkMessageHeader[6] 884 | previousChunk = self.previousChunkMessage[message.chunkStreamID]; 885 | if (previousChunk != null) { 886 | message.timestamp = previousChunk.timestamp; 887 | message.messageStreamID = previousChunk.messageStreamID; 888 | } else { 889 | throw new Error("Chunk reference error for type 1: previous chunk for id " + message.chunkStreamID + " is not found"); 890 | } 891 | } else if (message.formatType == 2) { 892 | // Type 2 (3 bytes) 893 | if (self.bp.need(3)) { 894 | yield; 895 | } 896 | chunkMessageHeader = self.bp.read(3); 897 | message.timestampDelta = chunkMessageHeader.readIntBE(0, 3); 898 | previousChunk = self.previousChunkMessage[message.chunkStreamID]; 899 | if (previousChunk != null) { 900 | message.timestamp = previousChunk.timestamp 901 | message.messageStreamID = previousChunk.messageStreamID 902 | message.messageLength = previousChunk.messageLength 903 | message.messageTypeID = previousChunk.messageTypeID 904 | } else { 905 | throw new Error("Chunk reference error for type 2: previous chunk for id " + message.chunkStreamID + " is not found"); 906 | } 907 | } else if (message.formatType == 3) { 908 | // Type 3 (0 byte) 909 | previousChunk = self.previousChunkMessage[message.chunkStreamID]; 910 | if (previousChunk != null) { 911 | message.timestamp = previousChunk.timestamp; 912 | message.messageStreamID = previousChunk.messageStreamID; 913 | message.messageLength = previousChunk.messageLength; 914 | message.timestampDelta = previousChunk.timestampDelta; 915 | message.messageTypeID = previousChunk.messageTypeID; 916 | } else { 917 | throw new Error("Chunk reference error for type 3: previous chunk for id " + message.chunkStreamID + " is not found"); 918 | } 919 | } else { 920 | throw new Error("Unknown format type: " + message.formatType); 921 | } 922 | 923 | //Extended Timestamp 924 | if (message.formatType === 0) { 925 | if (message.timestamp === 0xffffff) { 926 | if (self.bp.need(4)) { 927 | yield; 928 | } 929 | var chunkBodyHeader = self.bp.read(4); 930 | message.timestamp = (chunkBodyHeader[0] * Math.pow(256, 3)) + (chunkBodyHeader[1] << 16) + (chunkBodyHeader[2] << 8) + chunkBodyHeader[3]; 931 | } 932 | } else if (message.timestampDelta === 0xffffff) { 933 | if (self.bp.need(4)) { 934 | yield; 935 | } 936 | var chunkBodyHeader = self.bp.read(4); 937 | message.timestampDelta = (chunkBodyHeader[0] * Math.pow(256, 3)) + (chunkBodyHeader[1] << 16) + (chunkBodyHeader[2] << 8) + chunkBodyHeader[3]; 938 | } 939 | 940 | // console.log(message); 941 | 942 | var rtmpBody = []; 943 | var rtmpBodySize = message.messageLength; 944 | var chunkBodySize = self.getRealChunkSize(rtmpBodySize, self.inChunkSize); 945 | if (self.bp.need(chunkBodySize)) { 946 | yield; 947 | } 948 | var chunkBody = self.bp.read(chunkBodySize); 949 | var chunkBodyPos = 0; 950 | do { 951 | if (rtmpBodySize > self.inChunkSize) { 952 | rtmpBody.push(chunkBody.slice(chunkBodyPos, chunkBodyPos + self.inChunkSize)); 953 | rtmpBodySize -= self.inChunkSize; 954 | chunkBodyPos += self.inChunkSize; 955 | chunkBodyPos++; 956 | } else { 957 | rtmpBody.push(chunkBody.slice(chunkBodyPos, chunkBodyPos + rtmpBodySize)); 958 | rtmpBodySize -= rtmpBodySize; 959 | chunkBodyPos += rtmpBodySize; 960 | } 961 | 962 | } while (rtmpBodySize > 0); 963 | 964 | message.timestamp += message.timestampDelta; 965 | self.previousChunkMessage[message.chunkStreamID] = message; 966 | var rtmpBodyBuf = Buffer.concat(rtmpBody); 967 | self.handleRtmpMessage(message, rtmpBodyBuf); 968 | } 969 | } 970 | -------------------------------------------------------------------------------- /handshake.js: -------------------------------------------------------------------------------- 1 | var Crypto = require('crypto'); 2 | 3 | var MESSAGE_FORMAT_0 = 0; 4 | var MESSAGE_FORMAT_1 = 1; 5 | var MESSAGE_FORMAT_2 = 2; 6 | 7 | var RTMP_SIG_SIZE = 1536; 8 | var SHA256DL = 32; 9 | 10 | var RandomCrud = new Buffer([ 11 | 0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8, 12 | 0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57, 13 | 0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab, 14 | 0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae 15 | ]); 16 | 17 | var GenuineFMSConst = "Genuine Adobe Flash Media Server 001"; 18 | var GenuineFMSConstCrud = Buffer.concat([new Buffer(GenuineFMSConst, "utf8"), RandomCrud]); 19 | 20 | var GenuineFPConst = "Genuine Adobe Flash Player 001"; 21 | var GenuineFPConstCrud = Buffer.concat([new Buffer(GenuineFPConst, "utf8"), RandomCrud]); 22 | 23 | function calcHmac(data, key) { 24 | var hmac = Crypto.createHmac('sha256', key); 25 | hmac.update(data); 26 | return hmac.digest(); 27 | } 28 | 29 | function GetClientGenuineConstDigestOffset(buf) { 30 | var offset = buf[0] + buf[1] + buf[2] + buf[3]; 31 | offset = (offset % 728) + 12; 32 | return offset; 33 | }; 34 | 35 | function GetServerGenuineConstDigestOffset(buf) { 36 | var offset = buf[0] + buf[1] + buf[2] + buf[3]; 37 | offset = (offset % 728) + 776; 38 | return offset; 39 | }; 40 | 41 | function detectClientMessageFormat(clientsig) { 42 | var computedSignature, msg, providedSignature, sdl; 43 | sdl = GetServerGenuineConstDigestOffset(clientsig.slice(772, 776)); 44 | msg = Buffer.concat([clientsig.slice(0, sdl), clientsig.slice(sdl + SHA256DL)], 1504); 45 | computedSignature = calcHmac(msg, GenuineFPConst); 46 | providedSignature = clientsig.slice(sdl, sdl + SHA256DL); 47 | if (computedSignature.equals(providedSignature)) { 48 | return MESSAGE_FORMAT_2; 49 | } 50 | sdl = GetClientGenuineConstDigestOffset(clientsig.slice(8, 12)); 51 | msg = Buffer.concat([clientsig.slice(0, sdl), clientsig.slice(sdl + SHA256DL)], 1504); 52 | computedSignature = calcHmac(msg, GenuineFPConst); 53 | providedSignature = clientsig.slice(sdl, sdl + SHA256DL); 54 | if (computedSignature.equals(providedSignature)) { 55 | return MESSAGE_FORMAT_1; 56 | } 57 | return MESSAGE_FORMAT_0; 58 | }; 59 | 60 | 61 | function generateS1(messageFormat) { 62 | var randomBytes = Crypto.randomBytes(RTMP_SIG_SIZE - 8); 63 | var handshakeBytes = Buffer.concat([new Buffer([0, 0, 0, 0, 1, 2, 3, 4]), randomBytes], RTMP_SIG_SIZE); 64 | 65 | var serverDigestOffset; 66 | if (messageFormat === 1) { 67 | serverDigestOffset = GetClientGenuineConstDigestOffset(handshakeBytes.slice(8, 12)); 68 | } else { 69 | serverDigestOffset = GetServerGenuineConstDigestOffset(handshakeBytes.slice(772, 776)); 70 | } 71 | 72 | msg = Buffer.concat([handshakeBytes.slice(0, serverDigestOffset), handshakeBytes.slice(serverDigestOffset + SHA256DL)], RTMP_SIG_SIZE - SHA256DL); 73 | hash = calcHmac(msg, GenuineFMSConst); 74 | hash.copy(handshakeBytes, serverDigestOffset, 0, 32); 75 | return handshakeBytes; 76 | }; 77 | 78 | function generateS2(messageFormat, clientsig, callback) { 79 | var randomBytes = Crypto.randomBytes(RTMP_SIG_SIZE - 32); 80 | var challengeKeyOffset; 81 | if (messageFormat === 1) { 82 | challengeKeyOffset = GetClientGenuineConstDigestOffset(clientsig.slice(8, 12)); 83 | } else { 84 | challengeKeyOffset = GetServerGenuineConstDigestOffset(clientsig.slice(772, 776)); 85 | } 86 | var challengeKey = clientsig.slice(challengeKeyOffset, challengeKeyOffset + 32); 87 | var hash = calcHmac(challengeKey, GenuineFMSConstCrud); 88 | var signature = calcHmac(randomBytes, hash); 89 | 90 | var s2Bytes = Buffer.concat([randomBytes, signature], RTMP_SIG_SIZE); 91 | return s2Bytes; 92 | }; 93 | 94 | function generateS0S1S2(clientsig, callback) { 95 | var clientType = clientsig.slice(0, 1); 96 | //console.log("[rtmp handshake] client type: " + clientType); 97 | var clientsig = clientsig.slice(1); 98 | 99 | var messageFormat = detectClientMessageFormat(clientsig); 100 | var allBytes; 101 | if (messageFormat === MESSAGE_FORMAT_0) { 102 | // console.log('[rtmp handshake] using simple handshake.'); 103 | allBytes = Buffer.concat([clientType, clientsig, clientsig]); 104 | } else { 105 | // console.log('[rtmp handshake] using complex handshake.'); 106 | allBytes = Buffer.concat([clientType, generateS1(messageFormat), generateS2(messageFormat, clientsig)]); 107 | } 108 | return allBytes; 109 | }; 110 | 111 | module.exports = { 112 | generateS0S1S2: generateS0S1S2 113 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtmp-server", 3 | "version": "0.2.0", 4 | "description": " A Node.js implementation of RTMP Server ", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/doremeet/rtmp-server-nodejs.git" 13 | }, 14 | "keywords": [ 15 | "rtmp", 16 | "server" 17 | ], 18 | "author": "Moshe Simantov", 19 | "license": "GPL-2.0", 20 | "bugs": { 21 | "url": "https://github.com/doremeet/rtmp-server-nodejs/issues" 22 | }, 23 | "homepage": "https://github.com/doremeet/rtmp-server-nodejs#readme" 24 | } 25 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const Client = require('./client'); 3 | 4 | const SESSION_ID_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789'; 5 | const SESSION_ID_LENGTH = 12; 6 | 7 | class NMServer extends net.Server { 8 | constructor(opts) { 9 | super(opts); 10 | 11 | this.conns = {}; 12 | this.producers = {}; 13 | 14 | this.on('connection', socket => { 15 | const id = this.generateNewSessionID(); 16 | 17 | const client = new Client(id, socket, this.conns, this.producers); 18 | client.on('error', err => socket.destroy(err)); 19 | 20 | socket.on('data', data => client.bp.push(data)); 21 | socket.on('end', () => client.stop()); 22 | socket.on('error', err => client.emit('error', err)); 23 | 24 | this.emit('client', client); 25 | client.run(); 26 | }); 27 | } 28 | 29 | generateNewSessionID() { 30 | let sessionId; 31 | 32 | do { 33 | sessionId = ''; 34 | for (let i = 0; i < SESSION_ID_LENGTH; i++) { 35 | const charIndex = (Math.random() * SESSION_ID_CHARS.length) | 0; 36 | sessionId += SESSION_ID_CHARS.charAt(charIndex); 37 | } 38 | } while (this.conns.hasOwnProperty(sessionId)); 39 | 40 | return sessionId; 41 | } 42 | } 43 | 44 | module.exports = NMServer; 45 | --------------------------------------------------------------------------------